優(yōu)化的自動駕駛決策規(guī)劃測試:貝葉斯優(yōu)化與遺傳算法實戰(zhàn))
1. 自動駕駛決策規(guī)劃測試的真實困境做自動駕駛決策規(guī)劃系統(tǒng)的測試最讓人頭疼的不是寫用例而是用例空間爆炸。一個簡單的無保護左轉(zhuǎn)場景如果把前車距離、對向車速度、行人位置、路面附著系數(shù)、光照條件這些參數(shù)各取幾個值做笛卡爾積輕輕松松就能組合出幾十萬條測試用例。我試過用最樸素的網(wǎng)格搜索跑一遍光是仿真環(huán)境啟動和場景加載的時間就夠喝一壺的更別提后面還要跑評價指標(biāo)。這就是為什么基于目標(biāo)優(yōu)化的自動化測試方法值得單獨拿出來聊。它的核心思路不復(fù)雜把測試用例生成看作一個優(yōu)化問題用貝葉斯優(yōu)化或遺傳算法這類黑盒優(yōu)化器在參數(shù)空間里主動尋找那些最可能讓決策規(guī)劃系統(tǒng)“翻車”的角落。相比隨機撒點或者暴力枚舉同樣的仿真預(yù)算下它能挖出更多邊界場景和失效模式。這篇文章適合兩類人看一類是剛接手自動駕駛決策規(guī)劃測試、正被用例數(shù)量壓得喘不過氣的測試工程師另一類是想把優(yōu)化算法落地到實際工程里的算法同學(xué)。我會把方案選型、參數(shù)設(shè)計、實操流程和踩過的坑都攤開講盡量讓你看完就能在自己的仿真環(huán)境里跑起來。2. 為什么是目標(biāo)優(yōu)化而不是隨機測試2.1 決策規(guī)劃系統(tǒng)的失效長什么樣決策規(guī)劃系統(tǒng)的輸出不是單一數(shù)值而是一條軌跡或者一系列駕駛動作。它的失效模式也很“離散”可能是跟車距離過近、可能是換道時后車被迫急剎、可能是路口博弈時卡死不動、也可能是對弱勢道路參與者VRU的避讓過于保守導(dǎo)致通行效率驟降。這些失效往往只在參數(shù)空間的某些狹窄區(qū)域出現(xiàn)隨機撒點很難命中。我做過一個統(tǒng)計在一個包含6個連續(xù)參數(shù)的跟車場景里隨機采樣2000次真正觸發(fā)“碰撞時間低于1.5秒”的用例只有17條命中率不到1%。而用貝葉斯優(yōu)化跑200次迭代能穩(wěn)定找到30條以上同類失效用例。這個差距直接決定了你是在“碰運氣”還是在“做測試”。2.2 貝葉斯優(yōu)化和遺傳算法的分工這兩種算法我都實際用過它們的脾氣不太一樣。貝葉斯優(yōu)化適合連續(xù)參數(shù)、仿真代價高、迭代次數(shù)有限的場景。它用高斯過程或樹結(jié)構(gòu)Parzen估計器建立目標(biāo)函數(shù)的代理模型每次迭代都根據(jù)“提升期望”或“置信上界”來選擇下一個采樣點。優(yōu)點是樣本效率極高通常幾十到幾百次仿真就能收斂到失效區(qū)域。缺點是隨著參數(shù)維度增加代理模型的擬合難度會上升一般建議控制在10維以內(nèi)。遺傳算法適合參數(shù)維度較高、目標(biāo)函數(shù)不連續(xù)、或者需要一次性生成一批多樣化用例的場景。它維護一個種群通過選擇、交叉、變異來進化。優(yōu)點是天然支持并行你可以一次跑幾十個仿真缺點是需要的評估次數(shù)比貝葉斯優(yōu)化多通常要幾千次才能穩(wěn)定收斂。我的建議是先用貝葉斯優(yōu)化做精細搜索找到幾個高價值失效區(qū)域再用遺傳算法在這些區(qū)域附近做種群擴展生成一批多樣性好的回歸用例。兩者不是二選一而是接力關(guān)系。2.3 目標(biāo)函數(shù)怎么定義才不跑偏優(yōu)化算法需要一個標(biāo)量目標(biāo)函數(shù)來指導(dǎo)搜索。但決策規(guī)劃系統(tǒng)的評價指標(biāo)往往有多個安全性、舒適性、通行效率、合規(guī)性。直接加權(quán)求和是最簡單的做法但權(quán)重很難調(diào)而且容易讓優(yōu)化器只盯著某一個指標(biāo)猛攻。我目前用的方案是分層目標(biāo)函數(shù)第一層是硬約束比如碰撞、闖紅燈、壓實線一旦觸發(fā)直接給一個很大的懲罰值第二層是軟指標(biāo)比如加速度均方根、 jerk 最大值、跟車時距用歸一化后的加權(quán)和。這樣優(yōu)化器會優(yōu)先尋找違反硬約束的用例同時在軟指標(biāo)上做細化。注意目標(biāo)函數(shù)里一定要加入“場景有效性”判斷。有些參數(shù)組合會導(dǎo)致仿真一開始就處于不合理狀態(tài)比如自車初始位置已經(jīng)在障礙物內(nèi)部這類用例要直接過濾掉否則優(yōu)化器會被噪聲帶偏。3. 自動化測試框架的搭建要點3.1 仿真環(huán)境與測試框架的接口設(shè)計不管你是用CARLA、Prescan還是自研仿真器測試框架和仿真器之間需要一個清晰的接口。我習(xí)慣把接口分成三層場景配置層接收一組參數(shù)生成仿真器能識別的場景文件比如OpenSCENARIO格式并啟動仿真。執(zhí)行控制層控制仿真步進、同步模式、超時處理確保每次仿真的時間步長和終止條件一致。結(jié)果采集層從仿真器里拉取自車軌跡、障礙物軌跡、碰撞檢測結(jié)果、規(guī)劃模塊內(nèi)部狀態(tài)輸出成結(jié)構(gòu)化數(shù)據(jù)。這三層最好用進程隔離的方式實現(xiàn)。仿真器崩潰或者卡死時測試框架能殺掉進程并記錄一條“無效用例”而不是整個測試任務(wù)掛掉。我早期偷懶把仿真器直接嵌在測試腳本里結(jié)果一個內(nèi)存泄漏就讓整晚的優(yōu)化迭代全廢了。3.2 用pytest組織測試用例的實踐雖然優(yōu)化算法是自動生成用例的但整個測試流程還是需要一個組織框架。我用pytest來管理測試會話主要看中它的fixture機制和參數(shù)化能力。一個典型的做法是把優(yōu)化器的每一次迭代當(dāng)作一個pytest測試項用pytest.mark.parametrize把參數(shù)組合注入。這樣你可以直接用pytest的-n參數(shù)做并行也能用--html生成報告。但要注意優(yōu)化算法本身是有狀態(tài)的并行化時需要把“采樣建議”和“結(jié)果回傳”做成線程安全的隊列否則多個worker會拿到重復(fù)的采樣點。# 簡化示意優(yōu)化器與pytest的橋接 import pytest from bayes_opt import BayesianOptimization def run_simulation(params): # 調(diào)用仿真器返回目標(biāo)函數(shù)值 return objective_value pytest.mark.parametrize(iteration, range(200)) def test_optimization_iteration(iteration, optimizer_state): next_point optimizer_state.suggest() value run_simulation(next_point) optimizer_state.register(next_point, value) assert value is not None實際工程里不會這么寫因為pytest的參數(shù)化是靜態(tài)的而優(yōu)化采樣是動態(tài)的。更合理的做法是用pytest的fixture維護一個全局優(yōu)化器每個測試項從優(yōu)化器拉取下一個采樣點。但這樣又失去了pytest的并行優(yōu)勢。我的折中方案是用pytest做單次批量測試的組織用獨立的優(yōu)化調(diào)度腳本做迭代控制。兩者通過文件或消息隊列交換數(shù)據(jù)。3.3 結(jié)果存儲與失效用例回放每次仿真產(chǎn)生的數(shù)據(jù)量不小尤其是如果錄了傳感器數(shù)據(jù)。我的做法是只存結(jié)構(gòu)化指標(biāo)和軌跡摘要原始數(shù)據(jù)按需保留。具體來說每條用例存一個JSON參數(shù)組合、目標(biāo)函數(shù)值、硬約束觸發(fā)情況、軟指標(biāo)數(shù)值、仿真耗時。失效用例額外存一份軌跡文件自車和關(guān)鍵障礙物的位置序列方便后續(xù)回放分析。用SQLite做本地存儲查詢和篩選都方便不需要額外搭數(shù)據(jù)庫服務(wù)。回放功能很重要。優(yōu)化器找到的失效用例你需要能一鍵復(fù)現(xiàn)否則開發(fā)同學(xué)會懷疑你在編數(shù)據(jù)。我通常會在測試框架里加一個replay命令輸入用例ID自動加載參數(shù)、啟動仿真、并排顯示規(guī)劃軌跡和實際執(zhí)行軌跡。4. 貝葉斯優(yōu)化的參數(shù)調(diào)優(yōu)與實操細節(jié)4.1 參數(shù)空間的定義與歸一化貝葉斯優(yōu)化對參數(shù)尺度比較敏感。如果某個參數(shù)范圍是0到100另一個是0到1高斯過程會傾向于在數(shù)值大的維度上探索。所以所有連續(xù)參數(shù)都要歸一化到[0, 1]優(yōu)化完成后再映射回物理量。對于離散參數(shù)比如“是否開啟某功能”或者“障礙物類型”有兩種處理方式一是當(dāng)作連續(xù)變量取整二是用one-hot編碼后當(dāng)作多個維度。我傾向于把離散參數(shù)單獨拿出來做網(wǎng)格枚舉只對連續(xù)參數(shù)做貝葉斯優(yōu)化。這樣維度更低收斂更快。一個跟車場景的參數(shù)空間示例參數(shù)名物理范圍歸一化范圍類型初始跟車距離5~50 m0~1連續(xù)前車減速度-1~-6 m/s20~1連續(xù)自車期望時距0.8~2.5 s0~1連續(xù)路面附著系數(shù)0.3~0.90~1連續(xù)前車類型{轎車, 卡車}—離散4.2 采集函數(shù)的選擇與迭代終止條件貝葉斯優(yōu)化常用的采集函數(shù)有EI期望提升、UCB置信上界和PI提升概率。我的經(jīng)驗是初期用UCB讓優(yōu)化器多探索不確定區(qū)域。中期切換到EI平衡探索和利用。后期如果已經(jīng)找到滿意的失效用例用PI做局部精細搜索。迭代終止條件不要只設(shè)最大迭代次數(shù)。我一般會同時監(jiān)控連續(xù)N次迭代沒有找到更優(yōu)解、目標(biāo)函數(shù)值超過閾值、總仿真耗時超過預(yù)算。三者滿足其一就停。4.3 初始采樣點的選擇技巧貝葉斯優(yōu)化需要一組初始點來擬合代理模型。純隨機采樣在低維空間沒問題但在5維以上隨機點可能覆蓋不均。我通常用拉丁超立方采樣生成初始點保證每個維度的投影都均勻分布。初始點數(shù)量一般取參數(shù)維度的5到10倍比如6維參數(shù)就采30到60個點。實操心得初始點里最好手動塞幾條“已知危險”的用例比如極小車距、極大相對速度。這樣代理模型從一開始就有正確的梯度方向不會在安全區(qū)域浪費太多迭代。5. 遺傳算法的種群設(shè)計與并行加速5.1 編碼方式與適應(yīng)度函數(shù)遺傳算法的第一步是編碼。對于連續(xù)參數(shù)我直接用實數(shù)編碼每個個體就是一個浮點數(shù)向量。交叉操作用模擬二進制交叉變異用多項式變異。這套組合在連續(xù)優(yōu)化里比較穩(wěn)不會像二進制編碼那樣出現(xiàn)海明懸崖。適應(yīng)度函數(shù)就是前面說的目標(biāo)函數(shù)。但遺傳算法是最大化適應(yīng)度而我們的目標(biāo)函數(shù)可能是“越小越好”比如碰撞時間。所以要么取負值要么用倒數(shù)變換。我習(xí)慣取負值簡單直接。5.2 種群大小與進化參數(shù)種群大小和迭代次數(shù)是一對需要權(quán)衡的參數(shù)。種群太小多樣性不足容易早熟收斂種群太大每代評估次數(shù)多總仿真次數(shù)爆炸。我的經(jīng)驗公式是種群大小 參數(shù)維度 × 10但不超過100。交叉概率 0.9變異概率 1/參數(shù)維度。迭代代數(shù) 50~100代配合精英保留策略每代保留最好的2~5個個體。這樣算下來6維參數(shù)、種群60、迭代80代總評估次數(shù)約4800次。如果單次仿真平均30秒串行要40小時。所以并行是必須的。5.3 用多進程做仿真并行遺傳算法的每一代里個體之間的評估是獨立的天然適合并行。我用Python的multiprocessing.Pool做進程池每個進程獨立啟動一個仿真器實例。注意幾個坑仿真器許可證有些商業(yè)仿真器按并發(fā)數(shù)收費開太多進程會被拒絕。提前確認許可證數(shù)量。資源競爭每個仿真器實例占用的CPU和內(nèi)存不小進程數(shù)不要超過物理核心數(shù)。結(jié)果收集用Pool.map時如果某個仿真崩潰整個map會拋異常。建議用apply_async加超時單個失敗不影響整體。from multiprocessing import Pool def evaluate_individual(params): try: return run_simulation(params) except Exception as e: return float(-inf) # 無效個體給最低適應(yīng)度 with Pool(processes8) as pool: fitness_values pool.map(evaluate_individual, population)5.4 多樣性保持與早熟收斂判斷遺傳算法跑著跑著種群可能都擠在一個局部最優(yōu)附近。判斷早熟收斂的簡單方法是看種群適應(yīng)度標(biāo)準(zhǔn)差如果連續(xù)幾代都低于某個閾值說明多樣性丟了。這時候可以隨機重新初始化一部分個體。增大變異概率。引入小生境技術(shù)對相似個體施加懲罰。我一般用第一種簡單粗暴但有效。重新初始化的比例取種群的20%左右。6. 從失效用例到回歸測試集的轉(zhuǎn)化6.1 失效用例的聚類與去重優(yōu)化器跑完你可能會得到幾百條失效用例。但其中很多是相似的比如都是“前車急剎導(dǎo)致跟車距離過近”只是參數(shù)略有不同。直接全部塞進回歸測試集會導(dǎo)致測試冗余。我通常用DBSCAN對失效用例的參數(shù)向量做聚類每個簇選一個代表用例離簇中心最近的再保留簇內(nèi)目標(biāo)函數(shù)值最好的幾個。這樣能把幾百條壓縮到幾十條覆蓋不同的失效模式。6.2 回歸測試集的版本管理回歸測試集不是一成不變的。每次決策規(guī)劃系統(tǒng)更新都可能修復(fù)一些舊失效同時引入新失效。我的做法是把回歸測試集存成CSV或JSON納入Git管理。每次系統(tǒng)更新后跑一遍全量回歸標(biāo)記哪些用例從“失效”變成“通過”。定期用優(yōu)化算法重新搜索補充新的失效用例。對于長期通過的用例可以降級到“冒煙測試集”減少每次回歸的時間。6.3 與CI/CD流水線的集成自動化測試最終要落到持續(xù)集成里。我的方案是每日構(gòu)建跑冒煙測試集約50條用例控制在30分鐘內(nèi)。每周構(gòu)建跑全量回歸測試集約300條用例控制在4小時內(nèi)。版本發(fā)布前跑優(yōu)化搜索生成一批新用例人工確認后加入回歸集。CI流水線里用pytest做測試執(zhí)行用Allure或JUnit XML輸出報告。如果硬約束用例失敗直接阻斷合并軟指標(biāo)退化超過閾值發(fā)告警但不阻斷。7. 常見問題與排查技巧實錄7.1 優(yōu)化器不收斂怎么辦這是最常見的問題。可能的原因和排查順序現(xiàn)象可能原因排查方法解決措施目標(biāo)函數(shù)值長期不變參數(shù)空間定義太窄檢查參數(shù)范圍是否覆蓋了失效區(qū)域擴大范圍重新歸一化迭代后期震蕩采集函數(shù)過于激進觀察采樣點分布切換采集函數(shù)降低探索權(quán)重初始點就很差初始采樣不合理檢查拉丁超立方實現(xiàn)手動加入已知危險點仿真結(jié)果噪聲大仿真器非確定性同一參數(shù)跑多次看方差固定隨機種子或增加重復(fù)次數(shù)我遇到過一次優(yōu)化器跑了100代還在安全區(qū)域打轉(zhuǎn)。后來發(fā)現(xiàn)是目標(biāo)函數(shù)里軟指標(biāo)的權(quán)重太小硬約束又一直沒觸發(fā)導(dǎo)致整個目標(biāo)函數(shù)值都很接近。把硬約束的懲罰值從1000提高到10000優(yōu)化器立刻開始往危險區(qū)域跑。7.2 仿真超時與無效用例處理仿真器不是永遠可靠的。有時候場景配置不合理自車會卡在原地不動仿真永遠不結(jié)束。我的處理方式是設(shè)置墻鐘超時比如單次仿真不超過120秒。設(shè)置仿真步數(shù)上限比如最多推進1000步。超時后強制殺掉進程記錄為“無效用例”目標(biāo)函數(shù)給一個中間值不是最差避免優(yōu)化器完全避開這個區(qū)域。注意無效用例的比例如果超過20%說明參數(shù)空間里有大量不合理組合。需要回頭檢查參數(shù)范圍或者加入場景有效性預(yù)判邏輯。7.3 優(yōu)化結(jié)果不可復(fù)現(xiàn)的排查優(yōu)化算法本身如果有隨機性比如遺傳算法的變異兩次運行結(jié)果可能不同。要保證可復(fù)現(xiàn)需要固定所有隨機種子Python的random、NumPy的numpy.random、仿真器的隨機種子。記錄每次采樣的參數(shù)和結(jié)果存成日志。如果用了并行確保結(jié)果收集的順序不影響優(yōu)化器狀態(tài)更新。我習(xí)慣在優(yōu)化腳本開頭加一段set_seed(42)把能設(shè)的種子都設(shè)上。雖然不能保證100%復(fù)現(xiàn)仿真器內(nèi)部可能有線程調(diào)度差異但至少優(yōu)化器的采樣序列是可復(fù)現(xiàn)的。7.4 高維參數(shù)空間的降維技巧當(dāng)參數(shù)超過10維貝葉斯優(yōu)化的效果會明顯下降。這時候可以考慮敏感性分析先用Sobol方法或Morris方法篩出對目標(biāo)函數(shù)影響最大的幾個參數(shù)只對它們做優(yōu)化。主成分分析如果參數(shù)之間有相關(guān)性可以降到低維空間再優(yōu)化。分組優(yōu)化把參數(shù)分成幾組輪流優(yōu)化類似坐標(biāo)下降。我在一個包含12個參數(shù)的場景里試過敏感性分析發(fā)現(xiàn)其中5個參數(shù)對結(jié)果幾乎沒有影響固定為典型值后優(yōu)化維度降到7維收斂速度提升了一倍多。8. 一些個人體會這套方法我從去年開始在實際項目里用最大的感受是優(yōu)化算法不是銀彈它只是把“人找用例”變成了“算法找用例”但目標(biāo)函數(shù)的定義、參數(shù)空間的設(shè)計、結(jié)果的分析還是得靠人。我見過有人把優(yōu)化器跑出來的失效用例直接丟給開發(fā)結(jié)果開發(fā)一看參數(shù)組合在物理上根本不可能出現(xiàn)白白浪費溝通成本。另外仿真置信度是個繞不開的問題。優(yōu)化器找到的失效用例如果仿真器本身在這個場景下就不準(zhǔn)那結(jié)論也沒意義。我的做法是對優(yōu)化器找到的高價值失效用例額外做一輪仿真器交叉驗證用另一個仿真器或者實車數(shù)據(jù)回放確認。雖然麻煩但能避免很多扯皮。最后分享一個小技巧優(yōu)化過程中把每次迭代的采樣點和目標(biāo)函數(shù)值實時畫出來用Matplotlib或者TensorBoard都行??粗Ⅻc圖從安全區(qū)域慢慢向危險區(qū)域聚集你能直觀判斷優(yōu)化器是不是在正常工作。如果散點一直均勻分布那大概率是目標(biāo)函數(shù)沒設(shè)計好或者參數(shù)空間有問題。這個可視化習(xí)慣幫我省了很多排查時間。