ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

自动驾驶决策规划全解析:从行为决策到运动规划的工程实践

自动驾驶决策规划全解析:从行为决策到运动规划的工程实践

简介:自动驾驶决策规划入门讲解资料,面向对智能车辆技术感兴趣的学生、工程师及产品经理,旨在帮助读者理解预测、决策与规划在自动驾驶整体架构中的核心作用。压缩包共1个PDF文件,大小7.3MB,内容以幻灯片形式呈现,版式清晰。内容源自丁文超的《自动驾驶预测决策规划》课程,大纲从自动驾驶概述、历史与背景谈起,依次介绍级别与分类、系统组成与功能,并重点梳理了DARPA挑战赛从2004到2007年的技术演进、Waymo从Google项目到商业化运营的发展脉络,以及百度Apollo等中国代表系统的崛起,同时引用了Epsilon等前沿规划系统论文,为读者提供从历史到前沿的完整视角,是系统建立自动驾驶决策规划相关知识体系的高效学习素材。目前已有136人学习浏览。

1. 自动驾驶决策规划简介:先圈定它管哪一段

一辆车在高速路上遇到前车减速,感知系统已经识别到前车位置和速度,但接下来的“要不要踩刹车、要不要变道”并不是感知能回答的,而是由自动驾驶决策规划模块负责。很多刚从感知方向转过来的工程师容易把“识别到”当成“会处理”,实际上感知只回答“周围有什么”,决策规划才回答“我该怎么做”。接下来我会把自动驾驶决策规划按“行为决策→运动规划→落地验证”这条线拆开,先讲清楚每一层解决什么、怎么做最小可复现的Demo、实车测试中的常见翻车点在哪,再给你一套闭环验证和迭代的框架。适合刚进入自动驾驶领域的开发者、正在评估智能驾驶决策方案的团队,以及被长尾场景折磨的决策算法工程师。看完之后,你至少能建立一个完整的工作框架,而不是停留在概念名词上。

2. 自动驾驶决策规划的层级拆解:行为决策与运动规划的分工

2.1 决策规划的模块边界:环境感知之后、控制执行之前

自动驾驶软件栈最常见的划分是:感知 → 预测 → 决策规划 → 控制。感知输出障碍物列表、车道线、红绿灯状态;预测模块把这些信息延拓到未来几秒,输出其他交通参与者的预测轨迹;决策规划接住这两部分信息,再结合定位和高精地图,输出一条可以被底盘执行的时间轨迹;控制再把这套轨迹转换成方向盘转角、油门和刹车指令。

需要强调的是,决策规划内部并不是一个黑匣子,它至少分成两层:行为决策层和运动规划层。行为决策层回答“做什么”,典型答案是“保持当前车道巡航”“切换到左侧车道”“在前方路口停车等待”;运动规划层回答“怎么做到”,典型答案是给出一组带有时间戳的轨迹点,比如第0秒在某个位置,第2秒在另一个位置,并且保证这些点连成的曲线满足车辆运动学约束和舒适度约束。

把这个边界画清楚,最大的价值在于调试。实车测试中如果出现“自车离前车太近”,你要先确认是行为决策误判了目标车道,还是运动规划输了错误的轨迹,而不是直接改控制参数。很多团队把问题堆到控制层,结果越调越乱,最后变成玄学问题。分层之后,每一层的输入输出都固定,替换任意一层不会影响其他层的接口,这也是决策规划能被单独仿真、单独回归验证的前提。

2.2 行为决策:状态机、概率模型与可解释性

行为决策最主流的方案是有限状态机。横向通常有车道保持、向左变道、向右变道;纵向通常有巡航、跟车、减速停车、蠕行。状态之间通过条件表达式切换,例如“当前车道前车距离小于安全车距且前车速度低于自车速度,则进入跟车状态”“左侧车道持续3秒无碰撞风险,则进入向左变道状态”。

这种方案的优点非常明显:每个状态都有清晰的语义,出了问题可以顺着条件表达式一路查下去;安全员能提前读懂车辆下一步意图;在责任判定和生产排障时,状态机自带天然日志。缺点是状态边界靠人写,复杂交互容易漏状态,比如“旁车压线但没完全变道”这种模糊场景,很容易在两个状态之间反复横跳。

状态机调试的参数设计,往往是整个决策模块的根基。以跟车状态切换为例,实际工程里会同时使用距离阈值和时间阈值:当前车距离小于安全车距,且该状态持续超过0.3秒,才进入跟车;当距离重新大于安全车距与一个滞回增量之和,才退回巡航。这个滞回增量非常关键,第一版没加的话,临界距离上每帧切换状态,输出的加速度指令一会儿正一会儿负,乘坐体验就是一颠一颠的。滞回增量不是越大越好,过大会导致车辆在危险距离上仍然保持巡航,一般取安全车距的10%~20%。

比状态机复杂一点的是把行为决策建模成MDP(马尔可夫决策过程)或POMDP。MDP把自车动作、周围车意图、道路结构放进取价函数里,通过价值迭代或在线搜索选出最优动作;POMDP额外建模感知不确定性和意图不确定性,适合无保护左转这类强交互场景。这类方案的行为选择更灵活,但实时性差,而且代价函数一旦设计不好,决策结果很难解释,调试成本直线上升。

学习型决策这几年也很常见,直接用神经网络从传感器特征或语义地图输出动作。它能把感知和决策打通,但对数据覆盖极度敏感,且黑匣子问题在实车责任判定上很棘手。我的看法是:量产项目里状态机仍然占主导,概率模型用于特定场景,学习型适合预研或作为状态机的辅助分支。选型的关键不是“谁的论文精度高”,而是“出问题时你能不能在第一现场看懂它为什么这么选”。

2.3 运动规划:路径、速度与轨迹的分层关系

运动规划最常见的做法是先规划路径,再规划速度,最后合成轨迹。路径规划在Frenet坐标系里进行:以车道中心线为参考线,用纵向距离s和横向偏移d描述位置。这样做的原因很务实,因为道路不是直线,s/d描述天然贴合车道结构,横向和纵向约束可以被分开处理。

路径层常用方法有三类。第一类是图搜索,比如A在离散栅格上找可行路径,适合停车场等低速场景;Hybrid A额外考虑车辆最小转弯半径,能让路径更接近实际可行驶轨迹,但结果还需要平滑。第二类是采样法,RRT*在连续空间随机扩展树,适合探索未知区域,但轨迹不够平滑,需要后处理才能交给跟踪控制。第三类是Lattice Planner,在末状态采样空间里生成候选曲线,再用代价函数选一条最优轨迹,这是目前工程上可解释性最好的运动规划方案之一。

速度规划是在路径确定后,沿路径分配一条速度曲线。最简单的做法是梯形速度规划,先加速到达目标速度,再匀速,最后减速,但加速度在衔接点跳变,乘客会感觉顿挫。更顺滑的做法是用多项式速度规划,把速度曲线表达为时间的多项式函数,强制首末位置、速度、加速度连续。当路径和速度都确定后,把它们合成一条带时间戳的轨迹,碰撞检测就变成“障碍物的时空占位”与“自车时空轨迹”在三维时空里的求交问题,这也是为什么很多运动规划库会专门维护一个时空障碍物映射表。

Lattice Planner选轨迹时,代价函数一般写成四项加权:横向偏移代价惩罚偏离车道中心,横向速度代价惩罚频繁变道,纵向加速度代价和jerk代价惩罚急加速急减速,障碍物距离代价惩罚贴得太近。权重比例要按场景调:高速主路更重横向偏移与纵向jerk,城区路口更重障碍物距离。权重不是拍脑袋定,而是先在离线数据集里统计,再在仿真里看轨迹分布。常见错误是权重只按主观感受调,结果某个场景修好了、另一个场景又出现新问题。这一层的主要工作量在采样策略和代价函数设计:横向上采样哪些末状态,纵向上采样哪些速度剖面,以及不同候选轨迹之间怎么打分。

3. 把决策规划跑在仿真里:一个最小行为决策与轨迹生成Demo

3.1 选型理由:仿真场景与算法骨架怎么搭

做最小可行Demo,我的习惯是先用Python搭一个“无仿真器的规划骨架”,把决策和轨迹生成逻辑先跑通,再接入真正的仿真环境。原因是决策规划的bug大多在逻辑层,比如状态切换条件写错、轨迹连接点不连续,这些在没有真实物理引擎的情况下就能暴露。

具体做法上,我构造了一个简化场景:一条双车道高速路,自车沿左侧车道行驶,前方200米有一辆慢车,右侧车道空闲。自车需要先判断“跟车还是变道”,再采样生成候选轨迹,最后选一条无碰撞且横向加速度小的轨迹。这个骨架不涉及感知,也不需要高精地图,只用一个简单的障碍物列表加参考线。每次验证逻辑时,只用几个数值输入就能跑一遍决策,出问题可以单步调试,非常方便。

这里要特别说明:简化不是偷懒,而是为了把行为决策和运动规划两个模块的接口固定下来。等你在简化场景里把逻辑调通,再换到真实仿真器时,只需要替换“障碍物输入格式”和“车辆执行模块”,决策和规划代码基本不用动。反过来,如果一开始就把感知噪声、控制延迟全加进来,出了问题根本分不清是哪一层引入的。仿真场景先易后难,这是决策规划调试里最值得坚持的习惯。

3.2 一个最小纵向行为决策的状态机实现示例

纵向行为决策是最容易上手的部分,因为它只需要两个输入:前车距离和自车速度。下面这个代码片段实现了一个三状态纵向决策:巡航、跟车、停车。

class LongitudinalDecision: """最小纵向行为决策:巡航/跟车/停车""" def __init__(self, min_gap=4.0, safe_time=1.8, hysteresis=2.0): self.min_gap = min_gap # 静态最小车距,单位m self.safe_time = safe_time # 车头时距,单位s self.hysteresis = hysteresis # 状态切换滞回量,单位m self.state = "cruise" def _safe_dist(self, ego_speed): # 安全车距 = 静态距离 + 速度 * 车头时距 return self.min_gap + ego_speed * self.safe_time def decide(self, ego_speed, lead_dist): safe = self._safe_dist(ego_speed) if lead_dist < 1.5: self.state = "stop" elif self.state == "cruise" and lead_dist < safe: self.state = "follow" elif self.state == "follow" and lead_dist < 1.5: self.state = "stop" elif self.state == "follow" and lead_dist > safe + self.hysteresis: self.state = "cruise" return self.state

这段代码的逻辑是:安全车距不是固定值,而是随自车速度线性增长。车头时距safe_time默认1.8秒,意味着如果前车突然急停,自车有1.8秒的反应和减速时间,这是一个比较保守的设定。状态切换加了滞回量hysteresis,避免在临界距离附近反复横跳。stop状态单独判断,是为了在距离极近时无条件接管。

参数上的经验值:safe_time在高速场景取1.8~2.5秒,城区低速取1.0~1.5秒;min_gap在低速拥堵场景可以收到1.5米,高速不要低于3米;hysteresis取safe的10%~20%。这套参数不区分场景,只做Demo验证可以,真要量产必须把参数表格化,按照不同车速区间和环境复杂度分别标定。

3.3 一个基于Frenet坐标系的轨迹生成示例

行为决策输出“变道”之后,运动规划要生成一条从当前车道到目标车道的平滑轨迹。下面用五次多项式在Frenet坐标系里生成横向偏移轨迹,假设横向偏移从当前d=0变到目标d=3.5(一根车道宽),初始和末状态的横向速度、横向加速度都为0。

import numpy as np def fit_quintic(start, end, T): """用五次多项式拟合一维轨迹,保证位置/速度/加速度连续。 start, end 分别是 (位置, 速度, 加速度),T 是轨迹时长。""" A = np.array([ [1, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0], [0, 0, 2, 0, 0, 0], [1, T, T**2, T**3, T**4, T**5], [0, 1, 2*T, 3*T**2, 4*T**3, 5*T**4], [0, 0, 2, 6*T, 12*T**2, 20*T**3] ]) b = np.array([start[0], start[1], start[2], end[0], end[1], end[2]]) coeffs = np.linalg.solve(A, b) def traj(t): return (coeffs[0] + coeffs[1]*t + coeffs[2]*t**2 + coeffs[3]*t**3 + coeffs[4]*t**4 + coeffs[5]*t**5) return traj # 变道示例:横向偏移 d 从 0 到 3.5 米,轨迹时长 4 秒 d_start = (0.0, 0.0, 0.0) d_end = (3.5, 0.0, 0.0) d_traj = fit_quintic(d_start, d_end, T=4.0)

这段代码的核心是三个条件:位置、速度、加速度在起点和终点都连续。为什么必须用五次而不是三次?因为三次多项式只能保证位置和速度连续,加速度在端点会跳变,车辆在变道结束时会有一次顿挫。五次多项式把加速度也约束住,生成的轨迹更贴近真实追求舒适度的变道行为。

轨迹时长T是关键参数。T越大,变道越平缓,但占用路段越长,旁边车道有车时风险越大;T越小,变道越利落,但横向加速度可能超过舒适上限。我的经验是高速变道T取4~6秒,城区变道取3~4秒。算完横向轨迹后,还要用同样的方式生成纵向s轨迹,再合成时间轨迹进行碰撞检测。

拿到横向轨迹公式后,还差纵向s轨迹。纵向轨迹通常是采样一组候选速度剖面,比如匀加速到目标速度、先匀加速再匀速、三段式速度规划,然后对每个剖面做碰撞检测。可以把自车轨迹在时间轴上离散成若干个点,再与障碍物预测轨迹做距离判断。这一段是运动规划里最容易出bug的地方,需要单独写一个碰撞检测函数,输入是两个轨迹数组,输出是最近距离和碰撞时间。这个函数要写成纯函数,方便直接做单元测试。我在验证时会连续测试几组极端case,比如前车急刹、旁车切入,确保返回结果没有数组越界和NaN。

3.4 参数说明:采样间隔、目标代价权重怎么设

运动规划进入Lattice采样后,参数数量明显变多。下面是我常用的参数范围和调试方向表:

参数常用范围调小之后的表现调大之后的表现
纵向采样间隔0.5~1.0 s候选轨迹多,算力开销大候选轨迹稀疏,容易跳过可行解
横向采样间隔0.5~2.0 m变道轨迹更密,选择多轨迹稀疏,偏离参考线大
轨迹时长T3~8 s动作激进,加速度大动作迟缓,占用道路空间大
横向偏移权重0.5~2.0车辆容易偏航车辆贴线太紧,避让不积极
jerk权重3~10曲线灵活但顿挫明显曲线过于平缓,绕障失败

一句话经验:调参的顺序永远是先定采样范围、再调代价权重。采样范围决定“可行解的覆盖度”,如果连可行解都没有,权重怎么调都白搭。代价权重里,jerk权重是舒适度最直接的调节旋钮,但不要一上来就拉满,否则车辆会为了追求平滑而选择一条贴着障碍物边缘走的轨迹,安全余量被吃掉。

还要注意一个常见误用:很多人把横向偏移权重当成“车道居中强度”,其实它惩罚的是相对参考线的偏移。城区有弯道时参考线本身带曲率,权重拉太高会导致车辆在弯道里切弯过度,反而骑线。正确做法是让横向偏移代价尽量小,让横向jerk和障碍物距离代价主导选择。

另外,采样间隔和轨迹时长的关系也必须匹配。轨迹时长3秒、纵向采样间隔0.5秒,意味着每个候选只由6个离散点表征,碰撞检测的分辨率很低,容易漏掉中间时刻的碰撞。实际工程里,我通常保留轨迹生成时的高密度采样点用于碰撞检测,再用稀疏点用于代价计算,两者分开,不要在同一个分辨率上硬凑。这个细节看起来不起眼,但很多在仿真器里偶尔穿透障碍物的case,最后查下来都是碰撞检测采样太粗。

4. 决策规划落地的避坑自检:四个高频翻车点

4.1 现象:自车在弯道里骑线,走的轨迹明显偏外侧

在曲率稍大的弯道里,自车沿着规划轨迹行驶时,横向偏移始终偏向弯道外侧,乘客感觉车在往外甩。这个问题在匝道和立交转弯处尤其明显,频率也高。

原因通常是参考线平滑不足。车道中心线如果直接用地图原始点拉出来,曲率在局部会突变,Lattice采样的参考线s坐标和横向偏移d计算都会受影响。规划出的轨迹按平滑代价选了贴近参考线的路径,但参考线本身贴弯不够,轨迹就直接切弯。

解决:先用三次样条或低通滤波对参考线做平滑,平滑后还要检查每个点的曲率突变是否小于阈值。一个常见做法是计算参考线上每一点的曲率,设定最大曲率变化率,超过就减小平滑步长重新处理。平滑之后,把原参考线和平滑参考线的最大偏移量算出来,控制在0.2米以内,避免过度平滑改变真实车道位置。在实车标定中,还要把高精地图与真实车道线偏差考虑进去,平滑后手动抽几个弯道点检查横向偏差。

4.2 现象:变道决策犹豫,车辆在车道线附近来回拉扯

左侧车道有超车空间但自车速度又不太够,状态机一会儿输出“向左变道”,一会儿输出“保持车道”,方向转角反复横摆。这个现象在高速车流量大的时候非常容易复现,车内体验就是车在“画龙”。

原因大多是状态切换条件没有滞回。变道收益函数在临界点附近波动,或者对相邻车道上车辆的预测轨迹抖动太大,导致决策层每帧改变意图。还有一个容易忽略的点:变道收益的计算里,如果只看了当前帧的间隙宽度,而没看持续可用时间,旁车稍微一加速就会让收益从正变负。

解决:在状态机里加入滞回区间和冷却时间。变道收益大于阈值和小于退出阈值的标准不要用同一个数,退出阈值设得更低一些。再加上一个冷却时间,比如进入变道状态后至少1.5秒内不允许切换到保持车道状态,除非遇到紧急障碍。这个冷却时间越长,车辆越果断,但太长会错过避险时机,一般不要超过2.5秒。与此同时,把收益计算改成“连续N帧都满足变道条件才真正输出变道”,N通常取3~5帧,这样能有效过滤预测噪声导致的瞬时跳变。

4.3 现象:前车急刹时自车制动过晚,触发紧急制动之后驾驶员都吓一跳

前车以较大减速度刹车,决策规划输出的减速指令来得慢,最后不得不由底层的紧急制动模块兜底,主观体验就是“决策反应慢”。这类case在实车路测中一旦出现,通常会被直接判定为高危问题。

原因是预测时域和控制延迟没有被计入纵向安全距离。开发时用的前车速度变化率来自仿真理想值,但实车的制动建立时间、感知延迟、控制指令下发延迟叠加起来,实际反应时间比预期多出300~500毫秒。很多人只把安全距离公式写成了“静态距离+速度×车头时距”,忽略了延迟距离,结果仿真里看着安全,实车就露馅。

解决:把延迟时间拆成三个部分:感知延迟、决策周期、控制执行延迟。感知延迟取当前感知模块的p90值,决策周期取规划器实际运行帧周期的1.5倍,控制执行延迟里要包含制动器建压时间。然后把这些延迟折算进安全距离公式:在原有速度×车头时距的基础上,追加一个“延迟距离”,即自车速度×总延迟时间。同时,预测模块对前车速度的预测时域也要拉长到2秒以上,不能只预测一帧就返回。调整后,可以用急刹场景做专门的回归case,把自车制动启动时刻和紧急制动介入时刻都打点记录。

4.4 现象:跟车速度振荡,油门和刹车一直小幅度交替

在通畅但前车速度有波动的情况下,自车速度也跟着上下抖动,车内能感觉到轻微推背和拖拽交替出现,长时间乘坐容易晕车。这属于典型的参数匹配问题。

原因是速度规划采样间隔太密,且代价函数里jerk权重过低。候选速度剖面过多时,最优结果在相邻周期内来回跳变;jerk权重低又不会惩罚这种跳变,控制层跟着不断修正,最终表现为振荡。另一个帮凶是决策层的跟车状态切换阈值太窄,前车速度稍微一波动,状态就在“巡航”和“跟车”之间反复切换。

解决:加大纵向采样间隔,比如从0.5秒改为1.0秒;把jerk权重提升到与纵向加速度权重接近;还可以在决策输出到规划之间加一个低通滤波,对期望加速度做一阶惯性平滑。注意低通滤波的时间常数不要超过0.3秒,太大会让跟车响应变得迟钝,在加塞场景里非常吃亏。如果是状态机切换阈值的问题,还要把滞回量加大到安全车距的15%左右,让状态切换不那么敏感。

5. 决策规划的闭环验证:从场景回放到关键指标计算

5.1 闭环测试:仿真器里先跑通,再谈实车

决策规划模块和感知、控制不同,它天然是闭环的一环:你输出轨迹,车辆执行后改变周围环境状态,环境状态又反过来影响你的下一帧决策。所以这类模块的验证方式必须闭环,开环测试只能用来验证“这一帧的计算对不对”,验证不了“连续的决策行为会不会震荡”。

常见做法分两步走。第一步是场景回放,把实车采集的感知数据、自车状态、障碍物轨迹录成场景文件,让决策规划模块离线跑一遍,检查逻辑是否产生预期行为。这一步好处是快、可复现,但因为没有闭环,无法回答“如果车辆动作变了,环境会怎么变”。第二步是闭环仿真,把决策规划的输出接到一个简单的车辆动力学模型上,再让环境对车辆新位置做出响应,比如前方障碍物的相对距离随车辆运动变化。所有决策规划团队都应该先跑通第二步再谈实车。

闭环仿真代码的骨架逻辑一般是这样:

sim_steps = 600 # 60秒 * 10Hz dt = 0.1 ego_state = init_ego_state() # 自车起点 scenario = load_scenario("cut_in_urgent") # 场景文件 for t in range(sim_steps): obs = scenario.observe(ego_state) # 观测当前世界 pred = predictor.predict(obs) # 预测障碍物 action = planner.plan(ego_state, pred) # 决策规划输出 ego_state = vehicle_model.step(action, dt) # 执行并更新 if check_collision(ego_state, obs): crash_cases.append(t)

这段骨架强调两件事:场景文件与仿真逻辑解耦,同一个场景可以被反复使用;预测模块在闭环里每帧更新,而不是用离线缓存好的固定轨迹。参数上,仿真步长dt通常要和决策频率一致,比如决策10Hz,dt取0.1秒,过大容易漏掉碰撞帧,过小会拖慢批量回归速度。车辆动力学模型在验证第一阶段可以简化成自行车模型,验证行为合理性足够;第二阶段再换成与实车标定过的动力学参数。

这里还有一个经常被忽略的细节:开环回放时场景文件里的自车轨迹是采集时真实执行的轨迹,如果决策规划输出的轨迹与采集轨迹不一致,两者会在后续帧逐步偏离。开环回放适合验证“决策在给定历史上是否合理”,但不适合评估“决策会带来什么后果”。所以真正可信的指标必须来自闭环,开环只用来做第一轮快速筛查。

5.2 关键指标:碰撞率、舒适度、任务完成率怎么算

判定一个决策规划方案好坏,不能只看一两个demo视频。业界最常用的基础指标有三类:

指标计算方式建议阈值注意点
碰撞率发生碰撞的场景数 / 总场景数0%(仿真回归库内)碰撞必须区分责任,判断是否为自车决策导致
任务完成率成功到达目标区域 / 总场景数按场景难度85%~99%超时也算未完成,需设定最长完成时间
舒适度超过舒适阈值的时长占比jerk超过2.5 m/s^3的占比<5%阈值需要按车速区间分开统计

碰撞率不是越低越好,因为在一个复杂交互场景里,选择“停车等待”可能永远不会碰撞,但会让任务完成率和通行效率下降。所以碰撞率必须和任务完成率绑定看。舒适度方面,jerk是最具代表性的指标,但不代表加速度数值大就一定不舒服,还要看频率和持续时间,这里不需要太复杂,统计超阈值时长占比足够指导参数迭代。

在批量跑完场景后,还要输出一个指标汇总表:每个场景的决策行为序列、关键轨迹点、最小车间距离。不要只存一份平均值,因为长尾问题往往隐藏在某个特定场景的最小距离上。我在实际验证时,习惯把每个case的“最小横向距离”“最小纵向距离”“最大jerk”单独拉出来排序,优先处理那些在指标边缘徘徊的case,而不是只看平均分。

5.3 回归测试:把历史case变成自动化场景

决策规划迭代最大的痛点是“修了A场景,坏了B场景”。避免这个问题只有一个可靠办法——建立场景回归库。每次遇到实车或仿真里的疑难case,把它涉及的障碍物轨迹、自车初始状态、道路结构存成标准化场景文件,并打上标签。回归测试脚本遍历所有场景文件,运行同一个规划器,输出指标汇总。

场景文件的数据格式,我习惯用可读的JSON,关键字段包括道路参考线、障碍物初始状态、障碍物轨迹序列、自车初始速度、目标位置。一次批量运行用简单的shell脚本就能完成:

for case in scenarios/*.json; do scenario_name="$(basename "$case" .json)" run_planner --scenario "$case" --output "results/$scenario_name.csv" done

回归测试脚本的核心要求是确定性。仿真器里所有随机因素,包括障碍物出现的时机、预测模型的噪声,都必须用固定随机种子固定。否则同一个代码版本跑两次结果不同,你根本无法判断指标变化到底是代码改动导致还是随机噪声导致。另外,回归库不是越大越好,而是覆盖度越广越好。每个case要想清楚它代表哪一类场景标签,比如“cut_in_urgent”“pedestrian_jaywalk”“intersection_blocked”,避免同一类场景堆了50个,其他关键场景一个没有。

回归发现指标下降时,我的排查顺序是:先定位到具体场景,再对比该场景新旧两个版本的决策行为序列,看是行为层选择变了,还是规划层轨迹质量变了。有时候只改了一个代价权重,行为层的结果都会跟着变,因为候选轨迹排序变了。建议每次修改只动一个参数,并把决策行为序列打印出来,避免把行为层和规划层的改动混在一起调试,否则最后会陷入调参泥潭。

6. 从简介到进阶:用场景标签管理决策规划的corner case

6.1 按行为分歧点给case打标签,别按路面类型打

标题里说“简介”,但进入这个方向的人才明白,决策规划没有一个“学完就结束”的边界,真正的工作量全在长尾场景。当你的回归库里积累了几百个case,给每个case打场景标签会让迭代效率明显提升。我走过的弯路是早期按路面类型打标签,比如“高速”“城区”“停车场”,结果发现同一个标签下的case行为差异极大,对调参没什么指导意义。

后来我改成按行为分歧点打标签。所谓行为分歧点,就是这个case的核心冲突在哪:是“旁车近距离切入要不要让”,是“行人在车头前犹豫走不走”,还是“路口被前车挡住要不要绕行”。常用标签可以是“cut_in_urgent”“pedestrian_jaywalk”“truck_obstacle”。标签不是越多越好,建议控制在20类以内,尽量覆盖行为分歧点,而不是覆盖所有交通参与者的组合。

调参数时我用的是一个很简单的A/B对比技巧:固定随机种子,只改一个参数,比较新老版本在同一case上的行为序列差异。

diff old_behavior.json new_behavior.json | head -50

这个diff看起来简单,但很实用。行为序列diff不光要看状态名,还要看切换时刻和轨迹最小安全距离。有时候新版本在碰撞率上更好了,但行为序列里多了一次“减速跟车→加速→再减速”的反复切换,这种非预期跳变就是后续隐患。前几年调试决策规划时,我见过最普遍的坑是看到碰撞率下降就急着合入新权重,结果没检查行为序列的突变,把一批简单场景的跟车距离搞大了。后来养成的两个习惯是:回归库合并前必须逐个diff行为序列,每次只动一个参数,并在10个以上代表case上确认没有非预期跳变,才敢相信这次改动。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进