自动驾驶轨迹预测评测:从minADE到场景驱动的落地实践

1. 为什么“跑分高”的轨迹预测模型一上路就拉胯

做自动驾驶轨迹预测这行的朋友,大概都经历过一种很拧巴的场景:论文里的 minADE、minFDE、Miss Rate 刷得一个比一个漂亮,榜单上你追我赶,小数点后两位都要争一争。结果真把模型塞进车端或者仿真环境里跑一圈,该刹不住还是刹不住,该犹豫还是犹豫。问题出在哪?不是模型不行,是评测这件事本身就没对齐真实需求。

轨迹预测这个任务,本质上是在回答一个问题:周围这些车、人、骑车的,接下来几秒会往哪走?听起来简单,但它的难点从来不在“预测一条线”,而在于预测的线要能支撑下游的决策规划。一个模型如果平均误差很低,但在“前车突然急刹”“路口鬼探头”“匝道汇入博弈”这些要命场景里全错,那这个低误差毫无意义。这就是标题里说的 “Measuring What Matters”——测真正重要的东西,而不是测那些让榜单好看的指标。

我接触过不少团队,包括做量产的、做仿真的、做数据闭环的,大家踩的坑高度一致:拿一个在 nuScenes 或者 Argoverse 上训好的模型,直接套一套开源评测脚本,看几个数字就下结论。这套流程最大的问题是场景分布和评测目标脱节。公开数据集里的场景是“自然分布”,大部分是直行、跟车、匀速这种简单情况,真正危险的边缘场景(edge case)占比极低。模型只要学会“预测上一帧的延续”,就能在平均指标上拿到不错的分数。但决策规划模块关心的恰恰是那些低频高危场景。

所以这篇东西我想聊的不是“怎么把 minADE 再降 0.05”,而是怎么建立一套场景驱动的评测方法论,让评测结果真正能回答“这个预测器能不能用、在哪些情况下不能用、离能用还差多远”。这套思路对做量产落地的、做仿真测试的、做数据闭环的都适用,哪怕你只是刚入门想搞清楚评测该怎么设计,也能直接抄作业。

关键词先摆在这:autonomous driving、trajectory predictors、evaluation、scenario-driven。下面我会从整体设计思路、核心指标拆解、实操流程、常见坑四个层面展开,中间会穿插具体的参数计算和代码片段,尽量让不同基础的读者都能落地。

2. 场景驱动评测的整体设计思路

2.1 从“平均主义”到“场景切片”的范式转换

传统评测的逻辑是:把测试集所有样本的误差求平均,得到一个标量。这个标量的隐含假设是——每个样本同等重要。但在自动驾驶里,这个假设是错的。一个“空旷高速上跟车”的样本和一个“无保护左转对向直行”的样本,对安全的影响差了几个数量级。平均主义会把后者的问题稀释掉。

场景驱动的核心转换是:先定义场景,再在场景内评测,最后按场景聚合。也就是说,评测的最小单元不是“一个样本”,而是“一类场景”。你需要先把测试数据按场景标签切分,比如:

  • 跟车(car-following)
  • 换道(lane-change)
  • 无保护左转(unprotected-left-turn)
  • 环岛(roundabout)
  • 匝道汇入/汇出(merge/split)
  • 行人横穿(pedestrian-crossing)
  • 鬼探头(occluded-pedestrian)
  • 施工区(construction-zone)

每个场景单独算指标,单独看分布。这样你得到的不是“模型误差 0.8 米”,而是“模型在跟车场景误差 0.3 米,在无保护左转场景误差 2.1 米且漏检率 35%”。后者才能指导你下一步该补什么数据、改什么结构。

2.2 场景标签从哪来:三层来源

场景标签不会凭空出现,实操中一般有三个来源,我按可靠性从高到低排:

第一层:人工标注。最准,但最贵。适合小规模高价值场景,比如专门标一批“鬼探头”。标注内容不只是场景类别,还要包括关键交互对象、交互类型(让行/抢行/博弈)、危险等级。

第二层:规则挖掘。用运动学规则从原始轨迹里自动切场景。比如“换道”可以定义为:横向位移超过一个车道宽、持续 2 秒以上、且横向速度方向一致。这种规则能覆盖大部分结构化场景,成本低,但边界模糊的场景容易漏。

第三层:模型打标。用一个预训练的场景分类器给样本打标签。适合大规模粗筛,但会有噪声,通常作为前两层的补充。

实操建议是三层结合:规则先粗筛出候选,人工抽检修正规则,模型补长尾。别指望一步到位,场景标签体系是迭代出来的。

2.3 评测目标对齐下游:预测器到底服务谁

这是最容易被忽略的一点。轨迹预测不是孤立任务,它的输出要喂给规划控制。所以评测指标必须和下游需求对齐。规划模块关心什么?我总结下来是三条:

  1. 安全性:预测的轨迹不能漏掉真正危险的运动模式。比如前车可能急刹,你只预测了匀速,规划就会跟太近。
  2. 多模态覆盖:真实世界有不确定性,预测器要给出多种可能的未来,且真实未来要落在预测集合里(这个就是 Miss Rate 的由来)。
  3. 时序一致性:连续帧之间的预测不能跳变,否则规划会抖。

所以评测设计时,除了常规的位移误差,必须加入漏检率(Miss Rate)、多模态覆盖度、时序抖动这几个维度。只盯 minADE 的评测是不完整的。

3. 核心指标拆解与参数计算

3.1 minADE / minFDE:别被“min”骗了

minADE(minimum Average Displacement Error)和 minFDE(minimum Final Displacement Error)是轨迹预测最常用的指标。定义是:模型输出 K 条候选轨迹,取其中和真实轨迹平均位移误差最小的那条,算误差。公式:

minADE = min_{k=1..K} (1/T) * Σ_{t=1..T} || p_k(t) - p_gt(t) || minFDE = min_{k=1..K} || p_k(T) - p_gt(T) ||

这里有个大坑:min 操作会让指标虚高。K 越大,min 越容易小。一个模型输出 6 条轨迹,只要有一条蒙对了,minADE 就很好看,哪怕另外 5 条全是垃圾。所以看 minADE 必须同时看 K 的值,K=1 和 K=6 的 minADE 没有可比性。

更合理的做法是同时报告 minADE 和平均 ADE(不取 min,而是对多模态做加权),或者用Miss Rate来约束:真实轨迹如果不在预测集合的某个距离阈值内,就算 miss。Miss Rate 比 minADE 更能反映“覆盖能力”。

3.2 Miss Rate 的阈值怎么定

Miss Rate 的定义是:真实轨迹的终点(或整条轨迹)与预测集合中最近的一条的距离超过阈值,就算 miss。阈值的选择直接决定指标松紧。常见阈值:

阈值类型典型值适用场景
终点距离2.0 m通用,Argoverse 常用
终点距离1.0 m严格,量产验证
整条轨迹平均距离1.5 m关注全程一致性
横向偏差0.5 m换道/避障场景

我的经验是:量产验证用 1.0 m 终点阈值 + 1.5 m 全程阈值双约束。公开数据集对比可以用 2.0 m,但别拿这个当落地标准。

3.3 场景内指标聚合:加权而非平均

切完场景后,怎么把各场景指标合成一个总评?直接平均是错的,因为场景样本量差异巨大。正确做法是按场景危险度加权。可以定义一个危险权重 w_s,比如:

w_s = α * 事故率 + β * 交互复杂度 + γ * 遮挡程度

α、β、γ 根据你的运营数据调。没有运营数据就用专家打分,先粗后细。加权后的总指标才反映“真实风险暴露下的性能”。

3.4 时序一致性指标:抖动率

这个指标公开数据集里很少提,但量产必看。定义:连续两帧预测的同一条模态轨迹,在重叠时间窗内的位移差。抖动率过高,规划会频繁修正,乘坐体验极差。计算方式:

jitter = (1/N) * Σ || p_k(t+1 | t+1) - p_k(t+1 | t) ||

其中 p_k(t+1 | t) 表示在 t 时刻预测的 t+1 位置。这个值一般要求小于 0.2 m/帧,超过 0.5 m/帧基本没法用。

4. 实操流程:从数据到评测报告

4.1 数据准备与场景切分

假设你手头有一批测试数据,格式是每帧包含自车状态、周围物体轨迹、地图信息。第一步是场景切分。我用 Python 写一个规则切分的骨架:

import numpy as np def detect_lane_change(traj, lane_width=3.5, min_duration=2.0, dt=0.1): """检测换道:横向位移超过一个车道宽且持续""" lateral = traj[:, 1] # 假设 y 是横向 lateral_vel = np.gradient(lateral, dt) # 找横向速度方向一致的连续段 sign = np.sign(lateral_vel) segments = [] start = 0 for i in range(1, len(sign)): if sign[i] != sign[start] and abs(lateral[i] - lateral[start]) > lane_width: if (i - start) * dt >= min_duration: segments.append((start, i)) start = i return segments def detect_car_following(ego, obj, thresh=15.0): """跟车:同车道、纵向距离小于阈值、速度相关""" dx = obj[:, 0] - ego[:, 0] dy = obj[:, 1] - ego[:, 1] same_lane = np.abs(dy) < 1.5 close = np.abs(dx) < thresh return np.where(same_lane & close)[0]

这段代码只是示意,实际场景切分要复杂得多,需要结合地图车道线、交通灯状态、交互对象数量。但核心思路是:用运动学特征 + 地图语义做规则切分,再人工抽检修正。

4.2 评测脚本的核心结构

评测脚本我建议分成四层:数据加载层、场景过滤层、指标计算层、报告生成层。指标计算层是核心,下面是一个 minADE/Miss Rate 的计算示例:

def compute_metrics(preds, gt, miss_thresh=2.0): """ preds: (K, T, 2) K条候选轨迹 gt: (T, 2) 真实轨迹 """ K, T, _ = preds.shape # 每条候选的 ADE ade_per_mode = np.mean(np.linalg.norm(preds - gt[None, :, :], axis=-1), axis=1) min_ade = np.min(ade_per_mode) # FDE fde_per_mode = np.linalg.norm(preds[:, -1, :] - gt[-1, :], axis=-1) min_fde = np.min(fde_per_mode) # Miss Rate miss = 1.0 if min_fde > miss_thresh else 0.0 return min_ade, min_fde, miss

注意这里miss_thresh是终点阈值。如果要算全程 Miss Rate,把 FDE 换成整条轨迹的最小平均距离即可。

4.3 场景聚合与报告

算完每个样本的指标后,按场景标签聚合。我习惯输出一张这样的表:

场景样本数minADEminFDEMiss Rate抖动率
跟车120000.320.712.1%0.08
换道34000.581.428.7%0.15
无保护左转8201.212.8531.5%0.42
鬼探头951.873.9258.9%0.51

这张表一眼就能看出问题:鬼探头场景 Miss Rate 接近 60%,说明模型基本没覆盖这种运动模式。下一步就该去补鬼探头数据、加遮挡推理模块。这比看一个总 minADE=0.45 有用得多。

4.4 参数选择与计算过程

阈值不是拍脑袋定的。以 Miss Rate 终点阈值 2.0 m 为例,这个值的来源是:车辆宽度约 1.8 m,预测终点偏差超过一个车身宽度,规划就无法可靠避让。所以 2.0 m 是一个物理意义明确的阈值。再严格一点用 1.0 m,对应“半个车身”,适合低速场景。

抖动率阈值 0.2 m/帧的来源:规划周期通常 100 ms,车速 10 m/s 时每帧位移 1 m,抖动超过 20% 就会引起明显修正。所以 0.2 m 是体验可接受的边界。

这些计算过程写进评测报告,评审时才有说服力。别只写“我们用了 2.0 m 阈值”,要写“因为车身宽度 1.8 m,所以取 2.0 m”。

5. 常见问题与排查技巧实录

5.1 场景标签噪声大怎么办

规则切分出来的场景,边界样本经常标错。比如“换道”和“避障绕行”在运动学上很像。排查方法:对每个场景随机抽 50 个样本可视化,看轨迹和地图,人工判断标签对不对。如果错误率超过 10%,回去改规则。别嫌麻烦,标签错了后面全错。

5.2 模型在简单场景过拟合

有些模型在跟车场景 minADE 极低,但一换道就崩。这通常是训练数据分布问题。排查:看训练集的场景分布和测试集是否一致。如果训练集 80% 是跟车,模型自然只会跟车。解决办法是在训练时对稀有场景过采样,或者用场景加权的损失函数。

5.3 Miss Rate 和 minADE 矛盾

经常出现 minADE 降了但 Miss Rate 升了的情况。原因是模型把多模态的“多样性”牺牲了,6 条轨迹挤在一起,平均误差小但覆盖差。这时候要看多模态方差:如果 6 条轨迹的终点方差小于 1 m²,说明模型退化了。解决办法是加多样性正则项,或者用 Winner-Takes-All 类损失。

5.4 评测结果不可复现

这是工程问题。常见原因:随机种子没固定、数据加载顺序不一致、浮点精度差异。排查清单:

  • 固定所有随机种子(numpy、torch、cuda)
  • 数据加载用确定性 shuffle
  • 指标计算用 float64 累加
  • 记录环境版本(CUDA、驱动、库版本)

我踩过最坑的一次是 CUDA 版本差异导致同一模型两次评测 Miss Rate 差 3 个百分点,查了两天才发现是 cuDNN 的卷积算法选择不同。所以评测环境一定要锁死。

5.5 场景样本太少怎么统计

鬼探头这种场景可能只有几十个样本,算出来的 Miss Rate 置信区间很宽。这时候要报告置信区间,而不是一个点估计。用 bootstrap 重采样算 95% CI:

def bootstrap_ci(data, n_boot=1000, alpha=0.05): stats = [] for _ in range(n_boot): sample = np.random.choice(data, size=len(data), replace=True) stats.append(np.mean(sample)) lower = np.percentile(stats, 100 * alpha / 2) upper = np.percentile(stats, 100 * (1 - alpha / 2)) return lower, upper

如果 CI 宽度超过 20 个百分点,说明样本量不够,结论要谨慎。

5.6 常见问题速查表

问题现象可能原因排查方向解决思路
简单场景好、复杂场景崩训练分布偏斜统计训练集场景分布稀有场景过采样
minADE 低但 Miss Rate 高多模态退化看预测轨迹方差加多样性损失
连续帧预测跳变时序建模弱看抖动率指标加时序平滑约束
评测结果波动大环境不确定锁种子、锁版本固定评测环境
场景样本太少长尾场景稀有看置信区间bootstrap 报告 CI
标签错误率高规则边界模糊人工抽检可视化迭代规则或人工修正

6. 场景驱动评测的扩展方向

6.1 从离线评测到闭环仿真

离线评测只能看预测本身,看不到预测对下游的影响。更进一步是闭环仿真:把预测器接入仿真器,让规划控制基于预测做决策,看最终的安全指标(碰撞率、急刹率、通行效率)。这才是终极评测。但闭环仿真成本高,通常作为离线评测的补充,用于关键场景的深度验证。

6.2 对抗场景生成

自然数据里危险场景太少,可以用对抗方法生成。比如在仿真里让周围车辆主动制造冲突,逼出预测器的弱点。生成的场景再人工筛选,补充到评测集里。这样评测集能持续进化,不会因为模型在固定集上过拟合而失效。

6.3 评测智能体的自动化

最近有个趋势是用智能体(agent)自动做评测分析。思路是:让一个 LLM 驱动的智能体读取评测报告,自动定位薄弱场景,生成改进建议,甚至自动触发数据挖掘流程。这个方向还在早期,但能大幅降低评测的人力成本。实操中可以先做半自动:智能体出报告,人工审核后再执行。

6.4 跨数据集泛化评测

一个模型在 nuScenes 上好,不代表在 Argoverse 上好,更不代表在你的运营数据上好。跨数据集评测能暴露过拟合问题。做法是:在 A 数据集训练,在 B 数据集测试,看指标下降幅度。下降超过 30% 就说明泛化能力不足,需要加域适应或更多样化的训练数据。

7. 我在实际评测中踩过的坑

说几个具体的。第一个是评测集泄漏。有次发现模型在测试集上 Miss Rate 异常低,查了半天发现测试集和训练集有重叠片段,因为数据是按 clip 切的,相邻 clip 有重叠帧。后来改成按场景切分,确保训练测试无重叠。这个坑很隐蔽,建议大家在切数据时就做好隔离。

第二个是指标计算的时间对齐。预测轨迹和真实轨迹的时间戳必须严格对齐,差一帧结果就差很多。有次因为数据加载时做了插值,导致时间戳偏移,minADE 虚高了 0.1。后来加了时间戳校验,对不齐的直接报错。

第三个是场景权重的争议。加权总指标里权重怎么定,团队经常吵。我的经验是:先用专家打分定一版,然后拿运营数据回归。比如用实际接管率作为目标,回归出各场景的权重。这样权重有数据支撑,争议就少了。

最后一个心得:评测报告要可视化。光看数字没感觉,把失败案例的轨迹画出来,标上预测和真实,一眼就能看出问题。我习惯每个场景挑 5 个最差案例画图,附在报告里。评审的时候大家看