參數(shù)實踐)
簡介Swarm 2.1 仿真工具面向多智能體系統(tǒng)與大尺度復雜系統(tǒng)建模的研究者、教師及開發(fā)者提供可運行、可編譯的仿真環(huán)境用于探索智能體交互、群體行為與分布式算法。壓縮包為 zip 格式共 221 個文件約 691KB以 73 個 m、58 個 h、27 個 o 等源碼與編譯產物為主并配有 changelog、makefile、readme、scm、exe 及 license 等構建說明和可執(zhí)行文件便于按工程結構查閱與二次開發(fā)。已有 205 人學習下載適合作為課程實驗、算法驗證和項目原型的參考。讀者可從中獲得多智能體建模的源碼框架、Boids 群飛與 FIPA-ACL 通信等經典模型線索、可視化觀察入口以及版本變更記錄從而較快理解 Swarm 的目錄組織、編譯方式與仿真流程并據此開展交通管理、社交網絡分析或生態(tài)系統(tǒng)建模等場景的擴展研究。1. 拿到 swarm2.1 仿真工具先想清楚它適合做哪類仿真有人拿到 swarm 2.1 這套仿真庫第一反應是找個 demo 改兩個參數(shù)把圖跑出來交差。真到要交付結論時才發(fā)現(xiàn)圖是有了卻解釋不了曲線為什么長這樣——調度是先于數(shù)據發(fā)生的模型層和觀察層在資源包里本來就是兩套東西揉在一起改等于把黑匣子焊死。swarm 仿真解決的是這一類問題一群自主個體、局部交互規(guī)則、需要觀察宏觀涌現(xiàn)。它適合群體聚集、擴散、任務分配這類多智能體推演不適合當連續(xù)方程求解器。手上是幾十到幾千個 agent、關注交互規(guī)則而非單點精度這套工具值得花半天摸清結構要的是流體或剛體動力學趁早換方向。2. 拆開資源目錄model swarm 與 observer swarm 的分層怎么落地資源包打開之后最容易被忽略的不是示例代碼而是目錄里那條隱形的分界線。swarm 2.1 這套東西的組織方式從頭到尾都在強調一件事模型該干什么、誰來看、多久看一次是三件獨立的事。你把這三件事混在一個類里編譯當然能過但后面所有關于結果可信度的討論都沒法展開。我一般拿到這類包先不跑示例先把目錄結構和 import 關系看一遍確認它有沒有守住這條線。2.1 為什么必須把模型和觀察拆成兩個 Swarmswarm 2.1 里的 Swarm 不是一群個體的意思而是一個可以被激活、被調度、被嵌套的執(zhí)行上下文。整個仿真樹是一棵樹最上面是頂層 Swarm下面掛模型 Swarm 和觀察者 Swarm模型 Swarm 再往下可以掛子 Swarm 做分組。每個 Swarm 有自己的 Schedule也就是自己的時鐘。這套結構帶來的直接好處是模型層的代碼里不該出現(xiàn)任何 gui、probe、顯示相關的類。模型只負責推進狀態(tài)觀察層負責在某個時刻把這些狀態(tài)讀出來、寫文件或者畫出來。你換一種可視化方式模型層一行都不用改你要跑批量實驗把圖形界面關掉也只是換掉頂層觀察者。反過來說如果模型層里混進了顯示代碼會出現(xiàn)兩個后果。一是無顯示環(huán)境下跑不起來批量實驗直接被堵死。二是采樣頻率被顯示刷新頻率綁架——你本來想每 10 步統(tǒng)計一次結果因為刷新是每步一次數(shù)據里全是冗余點。判斷方法很土但有效在模型層源文件里搜gui、ProbeDisplay、GUISwarm這幾個詞只要出現(xiàn)就說明分層沒守住。合格的資源包里模型層的 import 只到swarm.objectbase、swarm.activity、swarm.space、swarm.defobj這幾層不會往上碰顯示。另一件事是 Zone。swarm 2.1 里所有對象都要掛在一個 Zone 上Zone 管內存分配和對象生命周期。常見做法是在最外層創(chuàng)建一次Globals.env.globalZone然后逐層往下傳不要在子 Swarm 里自己 new 一個新的 Zone。自己 new 的后果是對象被回收的時機跟你想的不一樣表現(xiàn)為跑了幾萬步之后突然空指針而且極難復現(xiàn)。2.2 環(huán)境準備老 Java 庫跑在新機器上的第一步這套 2.1 版本的庫是 Java 實現(xiàn)但它的語法和 API 停留在比較早的年代所以環(huán)境準備有幾個繞不開的點。我一般按下面的順序確認做完再碰代碼。第一步確認 JDK 版本。不要一上來就用最新的 JDK。這類老庫大量使用早期集合接口和已廢棄的寫法新版 JDK 的模塊化限制會直接把反射和內部類訪問卡死。穩(wěn)妥的做法是裝一個 Java 8 的 JDK單獨指過去不要動系統(tǒng)默認的 JAVA_HOME。# 確認當前 java 版本必須能對上資源包里的編譯要求 java -version javac -version # 如果系統(tǒng)默認是新版給這個項目單獨指一個 JDK 8 路徑 export JAVA_HOME/path/to/jdk8 export PATH$JAVA_HOME/bin:$PATH # 再驗一次確保 javac 和 java 是同一套 which java javac這里的邏輯是java和javac必須來自同一個 JDK?;煊檬亲畛R姷姆圏c現(xiàn)象是編譯通過、運行時報版本不匹配或者找不到主類你會以為是代碼問題其實是路徑問題。參數(shù)上沒什么可調的就是把JAVA_HOME指向那個獨立的 JDK 目錄用完這個終端窗口再關掉不影響系統(tǒng)其它工具。第二步找到 jar 的位置并寫進環(huán)境變量。資源包里通常有一個share/swarm/或lib/目錄里面放著主 jar同目錄下還可能有一個 native 庫目錄。這兩個路徑都要記下來。# 按你解壓后的實際路徑改這里用占位符 export SWARM_HOME/path/to/swarm2.1 export SWARM_JAR$SWARM_HOME/share/swarm/swarm.jar export LD_LIBRARY_PATH$SWARM_HOME/lib:$LD_LIBRARY_PATH # 驗證 jar 是否可讀讀不到就是路徑寫錯了 ls -l $SWARM_JARLD_LIBRARY_PATH那一行是給底層庫用的。swarm 2.1 的部分功能依賴本地庫如果只配了 classpath 沒配這個編譯期一切正常運行到初始化那一步才報錯錯誤信息還很不友好。這一步屬于典型的提前五分鐘省下兩小時。2.3 最小可編譯骨架一個不依賴 GUI 的模型層環(huán)境通了之后別急著跑完整示例先自己寫一個能被編譯、能被激活、但什么都不做的空殼。這一步的目的不是產出結果是驗證你的編譯命令、包結構和類路徑是對的。下面是我常用的最小模型層骨架。// ModelSwarm.java —— 純模型層不 import 任何 gui 相關類 import swarm.Globals; import swarm.defobj.Zone; import swarm.objectbase.Swarm; import swarm.objectbase.SwarmImpl; import swarm.activity.ActionGroup; import swarm.activity.ActionGroupImpl; import swarm.activity.Schedule; import swarm.activity.ScheduleImpl; import swarm.activity.Activity; public class ModelSwarm extends SwarmImpl { public Schedule modelSchedule; // 模型層自己的時鐘 public int agentCount 200; // 個體數(shù)量后續(xù)從外部覆蓋 public double worldSize 100.0; // 世界邊長單位由業(yè)務自己定義 public ModelSwarm(Zone aZone) { super(aZone); // 必須向上傳遞 Zone不能自己新建 } public Object buildObjects() { // 領域對象只在這里創(chuàng)建此處刻意留空先驗證鏈路 return this; } public Object buildActions() { ActionGroup group new ActionGroupImpl(getZone()); group.createActionTo$message(this, step); modelSchedule new ScheduleImpl(getZone(), 1); // 1 表示每一步都執(zhí)行 modelSchedule.at$createActionTo$message(0, group, step); return this; } public Activity activateIn(Swarm context) { super.activateIn(context); modelSchedule.activateIn(context); return getActivity(); } public void step() { // 每個 tick 的推進邏輯先空著 } }這段代碼的關鍵點有三個。getZone()返回的是父級傳下來的 Zone所有子對象都該掛在它下面這是內存管理的基準。buildObjects和buildActions是分開的兩個階段前者建對象、后者建調度順序不能顛倒因為動作組引用的是已經存在的對象。activateIn里super必須先調父級沒激活就激活子級的調度事件循環(huán)會直接空轉。編譯命令按下面的寫法走把 classes 目錄單獨分出來避免源碼目錄被 class 文件污染。# 編譯classpath 只放主 jar輸出到 classes 目錄 mkdir -p classes javac -encoding UTF-8 -cp $SWARM_JAR -d classes src/*.java # 如果沒有 main 入口先只驗證編譯是否通過 ls classes參數(shù)說明-cp后面只跟主 jar 路徑不要一次塞一堆不存在的 jar 進去路徑寫錯時 javac 會靜默忽略并報找不到符號很容易誤判成代碼問題。-d指定輸出目錄方便后面打包和清理。-encoding UTF-8不能省舊環(huán)境默認編碼經常不是 UTF-8源碼里有中文注釋就會編譯失敗。到這一步骨架能編譯通過就可以往下填內容了。填的順序我建議是先填調度再填空間最后填采樣。反過來做的話你會寫出一堆沒法復現(xiàn)的統(tǒng)計代碼。3. Schedule、Space、Probe讓仿真真正動起來的三件套上一章只驗證了鏈路這一章要讓仿真跑起來并產出可信數(shù)據。swarm 2.1 里真正承擔這件事的是三個東西Schedule 決定什么時候發(fā)生Space 決定誰和誰相鄰Probe 決定記錄什么、多久記一次。這三個東西的參數(shù)錯一個最后的結論就可能整個反掉而且錯得很隱蔽——圖還是能畫出來只是它是錯的。3.1 Schedule 的調度語義at 和 repeat 到底誰先誰后Schedule 的構造函數(shù)里那個整數(shù)是repeat參數(shù)表示這個調度每隔多少個基本時間單位被觸發(fā)一次。ScheduleImpl(zone, 1)是每步觸發(fā)ScheduleImpl(zone, 10)是每 10 步觸發(fā)一次。這里的步是調度自己的時間軸不是墻鐘時間別把它和真實秒數(shù)混在一起。真正容易錯的是at系列的語義。at$createActionTo$message(t, target, method)的意思是在第 t 個時間點創(chuàng)建一個動作讓 target 執(zhí)行 method。這個 t 是相對于這個 Schedule 被激活的時刻不是全局絕對時間。如果你在模型層和觀察層各建一個 Schedule兩個 Schedule 的 t0 是各自激活的時刻不在同一個基準上。我一般會在模型層里用ScheduleImpl(zone, 1)保證每一步都推進在觀察層里用ScheduleImpl(zone, 10)或者更大減少采樣的密度。如果兩個 Schedule 的 repeat 值相同但激活順序不同就會出現(xiàn)觀察發(fā)生在推進之前的情況第一幀數(shù)據永遠是初始狀態(tài)看著像是模型沒動。解決辦法是在buildActions里顯式地把模型層的activateIn放在觀察層之前。還有一種寫法是中途動態(tài)增刪動作用ActionGroup的createActionForEach之類的方法掛多個動作。這時候要特別注意執(zhí)行順序同一個 ActionGroup 內的動作按添加順序執(zhí)行跨 ActionGroup 就不保證。所以有先后依賴的邏輯寧可拆成兩個 ActionGroup 按順序激活也不要在同一個組里賭順序。關于終止條件常見做法不是在 Schedule 里設而是在模型的 step 里判斷一個計數(shù)變量到步數(shù)之后調用結束流程。直接在調度里做跑 N 步停需要額外寫分支不如在 step 里加一個if (tick maxTicks)來得直接。3.2 Space 空間索引與鄰居查詢半徑和邊界一起定swarm 2.1 提供幾套空間實現(xiàn)二維離散網格是Object2d配一個底層緩沖區(qū)連續(xù)空間和雙緩沖網格又是另外的類。選哪一套取決于你的個體是否需要占據一個格子。群體聚集類問題個體數(shù)量多、交互只看鄰居用二維離散網格加一份對象索引是最省事的。網格初始化時有兩個參數(shù)必須一起考慮網格邊長和鄰居查詢半徑。常見錯誤是網格邊長按個體數(shù)量開平方來定半徑隨便寫個固定值然后跑出來的聚集程度跟別人復現(xiàn)不出來。// 空間初始化片段放在 buildObjects 里 import swarm.space.Object2dImpl; import swarm.space.DblBuffer2dImpl; int gridSize 128; // 網格邊長格子數(shù) DblBuffer2dImpl field new DblBuffer2dImpl(getZone(), gridSize, gridSize); Object2dImpl index new Object2dImpl(getZone(), gridSize, gridSize); double neighborRadius 3.0; // 查詢半徑單位是格子數(shù) // 半徑必須是整數(shù)格或能明確映射到格的數(shù)值否則查詢結果依賴取整方式參數(shù)說明gridSize決定空間分辨率個體與網格的比例最好讓每個個體平均落到若干格子里而不是一對一。一格一個個體會讓鄰居查詢退化成正交搜索反而更慢。neighborRadius建議取 2 到 5 之間的整數(shù)取小數(shù)時要先想清楚取整是向上還是向下不同取整方式會得到不同的鄰居集合結果沒法對比。邊界處理是第二個坑。swarm 2.1 的網格在查詢鄰居時需要你自己裁掉越界的索引它不會幫你做循環(huán)邊界。如果業(yè)務需要周期性邊界越界坐標要手動取模回繞如果是有界空間越界部分直接跳過。兩種方式的宏觀結果完全不同同一個初始構型能跑出兩種聚集形態(tài)所以這個選擇要寫進實驗記錄里不能只留在代碼里。性能上網格法的優(yōu)勢是查詢是常數(shù)時間前提是每個格子里的對象數(shù)量不爆炸。如果局部密度極高單個格子里塞了幾百個對象每次查詢都要線性掃整體復雜度就退化了。判斷方法是偶爾打印一次最大格子占用數(shù)它隨步數(shù)增長得很快說明需要加粒度或者換實現(xiàn)。3.3 Probe 采樣與結果落盤別在觀察層做計算Probe 在 swarm 2.1 里是讀取對象屬性的機制配合 ProbeMap 可以把指定成員變量暴露出來觀察層通過這些接口采樣不用直接訪問模型對象的內部字段。這套設計的本意是解耦但實際用起來經常被繞過——很多人圖省事觀察層直接持有模型對象的引用讀字段、算指標、寫文件一條龍。直接讀字段的后果是模型層一旦改動字段名或者類型觀察層的編譯就崩了而且崩的地方很分散。用 Probe 的話改動集中在一個映射文件或一段注冊代碼里影響面可控。所以我一般要求觀察層只通過 Probe 拿數(shù)據指標計算也放在觀察層但計算所需的原始數(shù)據必須來自 Probe 而不是字段直讀。采樣落盤這塊最實用的格式是 CSV一行一個時間步一列一個指標。不要用帶格式的表格輸出后面做參數(shù)掃描、批量匯總的時候CSV 是最省事的。寫文件時注意兩件事一是每寫若干行就 flush 一次否則進程被中斷時文件是空的二是文件名里帶上關鍵參數(shù)比如種子和網格邊長否則跑十幾組之后你會分不清哪個文件對應哪組配置。// 觀察層的采樣動作掛在每 N 步觸發(fā)一次的 Schedule 上 import java.io.FileWriter; import java.io.PrintWriter; private PrintWriter out; public void sample() { if (out null) { try { // 文件名帶參數(shù)避免多組實驗混淆 out new PrintWriter(new FileWriter(result_seed seed _g gridSize .csv)); out.println(tick,agent_count,mean_neighbors,cluster_index); } catch (Exception e) { throw new RuntimeException(無法創(chuàng)建結果文件, e); } } // 指標全部在觀察層算模型層只提供原始狀態(tài) out.println(tick , count , meanNb , clusterIdx); out.flush(); // 必須 flush否則中斷時文件為空 }邏輯說明out只在第一次采樣時創(chuàng)建之后復用避免每步開關文件。表頭在第 0 步寫一次指標列固定順序方便后面用腳本解析。flush是血淚經驗不加的話仿真跑了兩小時被意外中斷打開文件發(fā)現(xiàn)只剩表頭。參數(shù)上可調的是采樣間隔。間隔太小數(shù)據量大、后面分析慢間隔太大可能錯過快速變化的過程。經驗值是被觀察現(xiàn)象的典型時間尺度的十分之一到二十分之一。比如聚集現(xiàn)象在 300 步左右成型采樣間隔取 10 到 20 即可沒必要每步都記。4. 參數(shù)怎么定網格分辨率、調度步長與隨機種子參數(shù)這件事不是調到一個好看的數(shù)而是定一組能寫進報告、別人能照著重跑的數(shù)。swarm 2.1 的好處是所有關鍵參數(shù)都在代碼里顯式出現(xiàn)沒有隱藏在配置文件里的魔法值壞處是這些參數(shù)彼此耦合改一個會影響另一個的表現(xiàn)。這一章把最常被問到的那幾個參數(shù)拆開講每個都給出取值理由和判斷邊界的方法。4.1 網格與鄰居半徑先看密度再看分辨率網格邊長的選擇不該從數(shù)字本身出發(fā)而要從平均每個個體占多少格子出發(fā)。設個體數(shù)為 N網格邊長為 L則個體密度是 N/L2。這個值小于 0.1 時個體之間幾乎不會互相碰到聚集類現(xiàn)象根本不會出現(xiàn)大于 1 時多個個體擠在同一格的概率顯著上升鄰居查詢退化成線性掃描。我一般的取法是讓 N/L2 落在 0.1 到 0.3 之間。比如 200 個個體、邊長 40 的網格密度是 0.125比較合適邊長 20 的網格密度是 0.5就偏擠了。鄰居半徑和這個密度是配套的半徑太小個體彼此感知不到系統(tǒng)像一堆獨立粒子半徑太大整個圖就退化成一個全連接的均質系統(tǒng)局部交互的意義就沒了。判斷半徑是否合適有個很直接的辦法跑 50 步統(tǒng)計每個個體的平均鄰居數(shù)量。這個數(shù)字應該是個位數(shù)。如果平均鄰居數(shù)是 0 或者 1半徑太小或者密度太低如果是幾十半徑太大你測到的其實是大尺度平均效應。把這個統(tǒng)計量寫進日志比事后看圖猜要靠譜得多。還有一點容易被忽略網格邊長的改變會改變邊界的相對影響。小網格里邊界占比大周期邊界和有界邊界的結果差異非常明顯大網格里邊界影響被稀釋。所以如果你打算對比兩組不同規(guī)模的實驗邊界條件必須固定不能一組開周期一組不開。4.2 調度頻率與時間語義一個 tick 到底代表多少ScheduleImpl(zone, 1)里的 1 沒有任何物理意義它只表示相對于這個調度的時間單位。真實含義完全取決于你給 step 里的邏輯賦予什么解釋一步代表一秒、一天還是一個任意的迭代次數(shù)都行但必須在報告里寫清楚。這個數(shù)字之所以重要是因為它和觀察間隔、現(xiàn)象時間尺度三者必須匹配?,F(xiàn)象在 300 步成型、觀察每 10 步采一次最后會有 30 個有效采樣點夠畫趨勢如果現(xiàn)象 30 步就結束了、觀察每 10 步采一次只剩 3 個點你什么都看不出來還會誤以為系統(tǒng)沒變化。另一種常見做法是讓模型層每步推進觀察層也每步采樣然后在后處理階段再降采樣。這種做法數(shù)據量大但不會丟信息適合前期探索階段。等時間尺度摸清楚了再把觀察間隔調大數(shù)據文件能小一個數(shù)量級。要提醒的是調度頻率和計算開銷是線性關系。把觀察間隔從 10 調到 1采樣和寫文件的時間大約翻十倍仿真總時長可能從半小時變成幾小時。前期探索可以密一點正式批量跑的時候一定要調回來。參數(shù)常見取值直接影響何時該調網格邊長40 到 256空間分辨率與單格占用平均鄰居數(shù)為 0 或超過 30鄰居半徑2 到 5整數(shù)格交互連通度平均鄰居數(shù)偏離個位數(shù)模型調度 repeat1推進粒度一般不動動了就沒法對比觀察調度 repeat10 到 50數(shù)據密度與耗時采樣點少于 20 個個體數(shù)量100 到 2000密度與統(tǒng)計穩(wěn)定性結果方差過大隨機種子任意固定整數(shù)結果可復現(xiàn)性每次實驗都必須顯式給這張表不用背但建議貼在實驗記錄的第一頁。每次開新實驗先把表里六個數(shù)寫下來再去改代碼里的值。沒有這六個數(shù)的實驗等于沒做。4.3 隨機種子可復現(xiàn)性只有一個開關隨機種子是這套工具里最不起眼、代價最大的一個參數(shù)。固定種子同樣的代碼跑兩次結果完全一致你才有資格說這個差異來自參數(shù)改動。不固定種子兩次結果不一樣你永遠分不清是參數(shù)生效了還是隨機波動。固定種子的方式在不同入口下略有差別。命令行啟動的常見做法是工具自身支持一個種子參數(shù)在初始化階段統(tǒng)一設置全局隨機源代碼里啟動的做法是初始化之后立刻給全局隨機生成器設種子。兩種方式不能混用混用的結果是有的隨機源被固定、有的沒被固定表現(xiàn)得大部分時候可復現(xiàn)偶爾不一樣這種最難查。// 啟動階段統(tǒng)一設種子所有隨機調用都走同一個生成器實例 Globals.env.initSwarm(args); // 先完成環(huán)境初始化 int seed 12345; // 從命令行或配置讀不要硬編碼多份 Globals.env.randomGenerator.setSeed(seed); // 領域對象內部一律用 Globals.env.randomGenerator // 不要各自 new 一個 Random否則種子管不到它們邏輯說明initSwarm之后才存在全局生成器所以設種子必須放在它后面。所有需要隨機數(shù)的類都從這個統(tǒng)一實例取種子才真正覆蓋全流程。如果某處寫了new java.util.Random()那個地方就脫離了種子的控制屬于隱性不可復現(xiàn)點??蓮同F(xiàn)還有一個隱藏前提個體的遍歷順序要穩(wěn)定。如果用哈希容器存?zhèn)€體遍歷順序在不同運行之間可能不同即使種子一樣結果也會有細微差異。常見做法是換成有序容器或者在每一步開始時按固定規(guī)則排序。這一步的代價很小但能讓可復現(xiàn)從大概齊變成真的可復現(xiàn)。批量實驗時種子的處理方式有兩種所有組用同一個種子或者每組一個不同的種子。前者的對比較干凈但結論只對一個隨機實現(xiàn)成立后者更接近真實方差但需要每組多跑幾次取平均。我一般先用同種子做參數(shù)敏感性確認趨勢之后再換多組種子驗證穩(wěn)健性兩輪都過才敢寫結論。5. 避坑與常見問題排查五條從崩潰日志里摳出來的經驗這一章的東西不是從文檔里抄的是每次重裝環(huán)境、每次換機器都會重新撞一遍的那幾個。寫在一起省得下次再花半天。現(xiàn)象一編譯全部通過運行就報找不到本地庫錯誤信息里只有一行類似 unsatisfied link 的內容。原因是 classpath 配了但動態(tài)庫搜索路徑沒配或者配的目錄和實際的庫文件目錄差了一層。這類資源包的目錄結構往往是lib/下面還有一層平臺相關子目錄直接指到lib/是不起作用的。解決方法是先find一下庫文件的實際位置再把那個具體目錄加進搜索路徑并在同一個終端里驗證一次。別指望寫進 shell 配置文件就一勞永逸多版本環(huán)境并存時它反而添亂?,F(xiàn)象二程序啟動后卡住不動沒有報錯也沒有輸出CPU 占用很低。大概率是圖形界面初始化在無顯示環(huán)境下阻塞了。swarm 2.1 里帶 GUI 的頂層類需要圖形環(huán)境遠程或者容器里跑就會卡在這一步。解決方法是把頂層換成不帶 GUI 的那個類只激活模型層和一個寫文件的觀察層。如果確實需要看界面就用帶轉發(fā)的圖形環(huán)境只想要數(shù)據的話徹底繞開 GUI 是更省事的選擇。改完之后第一次跑建議把步數(shù)設小確認能正常退出。現(xiàn)象三同一份代碼跑兩次結果不一樣種子明明設了。先查有沒有繞過全局隨機源的地方最常見的來源是某個工具類里自己 new 了一個隨機對象。再查個體容器是不是無序的遍歷順序變化會在統(tǒng)計量上表現(xiàn)為微小抖動。這兩處都排掉之后還不可復現(xiàn)就看時間相關的邏輯——有沒有用到當前系統(tǒng)時間做初始化。解決順序就是按這三條挨個排不要一上來就懷疑浮點精度?,F(xiàn)象四網格跑到一定步數(shù)后變慢或者個體位置出現(xiàn)明顯異常比如全部貼在邊界上。前者通常是局部密度增大導致單格對象數(shù)暴漲鄰居查詢退化成線性掃描后者是邊界處理寫錯了越界坐標沒有回繞也沒有跳過直接落在了邊界格上。這兩個問題經常一起出現(xiàn)因為密度異常本身也可能是邊界處理錯誤導致個體被擠到邊緣。排查方法是每若干步打印一次最大單格占用數(shù)和邊界格上的個體數(shù)兩個數(shù)字都正常才繼續(xù)往下跑?,F(xiàn)象五換了個 JDK 之后編譯報一堆廢棄警告運行時報方法找不到。老版本庫里有相當一部分 API 在新 JDK 上被移除或改變簽名了。別試圖一條條改警告成本極高且容易改出行為差異。正確做法是回到與資源包匹配的 JDK 版本上把版本固定下來寫進說明文檔。如果確實需要在新環(huán)境里跑優(yōu)先考慮容器化隔離而不是改庫。提示這五條的共同點是編譯期看不出來。所以每次換環(huán)境之后不要直接跑長實驗先用 100 步的小規(guī)模跑一遍確認能正常啟停、能產出文件再去跑正式實驗。6. 進階參數(shù)掃描腳本加一個校驗指標把結論坐實單組實驗能出圖但出不了結論。真正讓人信服的做法是做一輪參數(shù)掃描再給每組結果配一個可計算的校驗指標讓看起來聚起來了變成聚集指數(shù)從 0.12 升到 0.47。這一節(jié)講的就是這套流程里我踩過的那部分。掃描本身用 shell 就夠了不需要寫復雜的框架。核心是把參數(shù)從代碼里提出來改成啟動時能覆蓋的形式然后循環(huán)調用。#!/bin/bash # 參數(shù)掃描固定種子做敏感性每組獨立日志 set -e for g in 64 96 128; do for r in 2 3 4; do java -cp $SWARM_JAR:classes Start \ -seed 12345 \ -gridSize $g \ -radius $r log_g${g}_r${r}.txt 21 echo done g$g r$r done doneset -e保證某一組失敗時整輪停下而不是默默跳過留下一個空洞。每個組合一個日志文件出問題時能定位到具體是哪一組。參數(shù)通過命令行傳入代碼里用默認值兜底這樣單獨跑某一組也方便。校驗指標我一般用兩個平均鄰居數(shù)和聚集指數(shù)。前者衡量局部連通度后者衡量空間分布偏離均勻的程度。兩個指標一起看能排除掉鄰居很多但其實是均勻鋪開這種假聚集。指標必須由觀察層在仿真結束時算并寫進結果文件不要事后拿 CSV 在別處算那樣兩邊口徑容易不一致。有一件事我吃過虧掃描跑完直接看指標發(fā)現(xiàn)某幾組的指標明顯比其他組好就當成最優(yōu)參數(shù)寫進報告?;仡^復查才發(fā)現(xiàn)那幾組的仿真根本沒跑滿步數(shù)中途因為網格越界拋了異常退出指標是用殘缺數(shù)據算出來的。從那以后我每次看掃描結果前都強制先跑一遍完整性檢查——確認每組的日志里都有正常結束標記結果文件的行數(shù)符合預期才允許自己去看指標。希望這些能幫到你少走兩小時彎路。本文還有配套的精品資源點擊獲取