轨迹预测评估新思路:用场景驱动替代平均误差指标

发布时间:2026/11/4 17:28:19
轨迹预测评估新思路:用场景驱动替代平均误差指标

做轨迹预测评估这些年,我越发觉得,leaderboard 上那个光鲜的 ADE 数字,有时候更像一种安慰剂。模型报到 0.3、0.4,名次好看,但一跑到高架出口、交叉口左转、大车切入这类场景,行为就像刚拿驾照的新手——该刹车的时候犹豫,该让行的时候硬挤。问题不一定出在模型身上,更多时候是我们拿来"量"它的那把尺子钝了。这篇内容想聊的就是一把更靠谱的尺子:场景驱动评估(Scenario-Driven Evaluation)。

它不是要推翻 ADE、FDE 这些传统指标,而是换一种统计视角,把"平均成绩"拆成一本"场景账本",告诉你在哪类场景下模型真的可靠、在哪类场景下它在裸泳。下面这些内容,都是我在实际项目中踩过坑、改过版、最后沉淀下来的经验。

1. 先搞清楚:我们到底在测什么

1.1 ADE/FDE 的数学本质与视觉错觉

自动驾驶里评估轨迹预测器,最常见的两个指标就是 ADE(Average Displacement Error)和 FDE(Final Displacement Error)。从数学形式上并不复杂:

  • ADE:预测轨迹与真值轨迹在每一时刻的平均点误差,公式可以写成

[ ADE = \frac{1}{N} \sum_{i=1}^{N} \frac{1}{T} \sum_{t=1}^{T} \left| \hat{p}_i(t) - p_i(t) \right|_2 ]

  • FDE:只看终点时刻 T 的误差

[ FDE = \frac{1}{N} \sum_{i=1}^{N} \left| \hat{p}_i(T) - p_i(T) \right|_2 ]

这套指标本身没有错,错的是我们使用它的方式。它会带来一个很隐蔽的视觉错觉:数字越小,模型越好。但"所有样本的平均误差小",完全不能等价于"在关键场景下模型安全"。

举个我做过的对比测试。同样的预测模型,在直行跟车场景里 ADE 能做到 0.12 米,几乎贴着真值走;但换成一次突然的 cut-in 场景,车辆要在极短时间内重新规划目标车道,同一个模型的 ADE 直接飙到 1.6 米,而且预测轨迹有两次明显的方向跳变。两者平均之后,ADE 可能仍然在 0.35 左右,"看起来还不错",可这正是问题所在——平均过程把最危险的现象稀释掉了。

1.2 平均指标掩盖了什么:一个具体的失败案例

我在某次版本迭代中遇到过一件特别典型的事。两个候选模型,模型 A 和模型 B,在同一个验证集上的全局 ADE 几乎一致,A 是 0.321,B 是 0.325,肉眼几乎看不出差别。如果按传统评估流程,工程师很可能直接选 A 上线,因为在 leaderboard 上 A 排名更高。

但当我把验证集按道路拓扑和交互对象拆开统计后,分布完全不同:

场景子集样本占比模型A ADE模型B ADE
高速巡航55%0.080.11
城市路口直行20%0.250.24
交叉口左转15%0.430.31
匝道汇入7%0.580.36
公交站台附近3%1.220.52

模型 A 之所以全局分数好看,是因为它在占比最大的高速巡航场景里优势明显,可到了公交站台、匝道汇入这类安全敏感场景,它的表现是灾难级的。如果只看全局 ADE,这个问题根本不会被发现;一旦落地到包含公交站台的城市道路,模型 A 可能频繁触发舒适性制动,甚至出现误判。

这就是我想强调的第一件事:评估粒度决定你发现问题的能力。全局平均指标是"体检报告上的总胆固醇",只能告诉你偏高还是偏低,却说不清到底是哪一环出了问题。

2. 场景驱动评估框架的设计思路

2.1 场景驱动评估的核心主张

场景驱动评估的基本主张可以用一句话概括:先把测试集切成有意义的场景片段,再在每个片段内单独计算指标,最后按安全重要度加权汇总。

为什么要切片?因为轨迹预测模型本质上是"输入历史状态,输出未来分布"的条件模型,它对不同上下文的条件化能力差异很大。一个模型可能对"道路跟着直走"非常自信,对"旁边有一辆大车正在压线靠近"却无能为力。把这两类样本放在同一个统计池子里,等于把模型的能力缺陷平均掉了。

切片的粒度也不是越细越好。按照我在项目里的经验,场景定义至少要满足两个条件:

  1. 语义上可解释。每一类场景都能说清楚"为什么是重要的",比如"雨天高架出口"、"无保护左转"、"行人近距离穿越"。
  2. 统计上可计算。每个场景类别在测试集里要有足够样本量,否则单独指标没有置信度。

如果切得过细,每类样本只有十几个,算出来的 ADE 波动会非常大,反而失去参考价值。我们后来定的原则是:每个场景类别在验证集中至少有 50 个预测片段,否则就合并到"其他交互类"。

2.2 场景目录怎么建:规则分类与数据驱动聚类

场景目录的构建,行业内大致有两条技术路线:规则分类和数据驱动聚类。

规则分类是"老派但可靠"的办法。工程师根据地图拓扑、动态障碍物状态、天气与光照、道路属性这些字段,用 if-else 去划分场景类型。比如:

  • 如果自车道所在 link 的 curvature 变化率超过阈值,就归为"弯道";
  • 如果车辆所在区域与道路 boundary 距离小于 1 米且相对横摆角大于 10 度,可能标记为"车道紧急修正";
  • 如果目标障碍物是行人且距离小于 5 米,则归为"近距离 VRU 交互"。

这种方法的优点是逻辑清晰、可审计,出问题的时候能顺着规则直接溯源;缺点是规则之间容易重叠,同一个样本可能同时满足"匝道汇入"和"变道超车"的分类条件,需要人工定义优先级。

数据驱动聚类正好相反。你可以把每个预测时刻周围的环境编码成向量,例如使用自车位置、速度、加速度、周围障碍物的结构化状态,甚至拉一个已经训练好的场景编码器,然后用 k-means 或 HDBSCAN 聚类,再给每个簇人工命名。这种方法的优点是能自动发现一些没被规则定义过的长尾场景,比如"前车两秒内连续三次点刹"这种微妙模式;缺点是聚类结果不稳定,语料变化后簇的分布会漂移,命名也非常依赖人工判读。

我在实践中的做法是两者结合:先用规则分类作为主骨架,保证每一类场景都稳定可靠;再用聚类结果作为"场景雷达",不定期发现潜在的新类型,人工审核后补充进规则库。这样既有可解释性,又能不断扩展场景覆盖。

2.3 赋权策略:不是所有场景都该被一视同仁

场景驱动评估的第二步是"加权"。加权逻辑很简单:不同场景对安全的影响等级不同,统计损失时不能简单平均。

我们的默认权重由三部分乘出来:

  • 风险等级 R:由安全团队给每个场景打 1 到 5 分,5 分代表存在高概率碰撞风险或已发生碰撞判定。
  • 逆频率系数 I:频率越低的场景权重越高,避免长尾场景被大头样本淹没。具体可以取[ I_c = \log\left(\frac{N}{\ n_c}\right) ]再归一化。
  • 任务优先级 T:如果近期版本重点要解决"路口左转"的缺陷,那这个场景的临时权重可以上调。

最终评估分数是:

[ E_{总} = \frac{\sum_{c} R_c \cdot I_c \cdot T_c \cdot E_c}{\sum_{c} R_c \cdot I_c \cdot T_c} ]

这样算出来的总分,和传统全局指标比,会更贴近"安全关键场景的实际表现"。它的另一层价值在于:当我们向管理层或合作方汇报模型能力时,可以直接给出"在风险等级 4 以上的场景,FDE 平均是 0.57 米,其中三个版本下降了 22%"这类有业务含义的数字,比一个冷冰冰的全局 ADE 更有说服力。

3. 实操:搭建一套可复用的场景驱动评估流水线

3.1 数据准备与场景标签构建

以我在 Waymo Open Motion Dataset 和 nuScenes 上的实践为例。这两套数据都有比较丰富的语义标注,特别是 Waymo 的 sdc_scenario 结构里直接带有场景类型标签,可以直接作为起步。

我会先做一步"场景标签扩展":把原始标签、地图结构化信息和动态目标状态拼成一张宽表,每一行是"一个预测目标的一个历史片段",列包括:

  • scenario_id、object_id、预测时刻
  • scenario_type(原始标签或自定义规则结果)
  • 自车与目标车的速度、加速度、转向角
  • 最近障碍物距离、相对速度
  • 所在道路曲率、限速、车道数
  • 是否包含行人、自行车
  • 天气与光照(如果数据里有)
  • 人工标注的风险等级

这一步其实最花时间。我的建议是先把"场景标签判定逻辑"写成独立模块,输出一份 CSV,再做评估时直接 join,不要每跑一次实验都重新生成一遍标签。标签不稳定是评估结果反复横跳的最大来源之一。

3.2 分场景指标的代码实现

下面这个代码片段是我项目里的一个简化版评估器,核心功能是输入预测结果、真值轨迹和场景标签,输出每个场景的 ADE/FDE、样本数和置信区间。

import numpy as np from scipy import stats class ScenarioEvaluator: def __init__(self, scenario_names, weights=None): self.scenario_names = scenario_names self.weights = weights or {} @staticmethod def mean_ci(data, confidence=0.95): data = np.asarray(data, dtype=np.float64) n = len(data) if n < 2: return float(np.mean(data)), 0.0 mean = np.mean(data) se = np.std(data, ddof=1) / np.sqrt(n) h = se * stats.t.ppf((1 + confidence) / 2, n - 1) return float(mean), float(h) def _compute_ade_fde(self, preds, gts, horizon=1.0): ade_list, fde_list = [], [] for pred, gt in zip(preds, gts): err = np.linalg.norm(pred - gt, axis=-1) ade_list.append(np.mean(err)) fde_list.append(err[-1]) return np.array(ade_list), np.array(fde_list) def evaluate(self, preds, gts, scenario_ids): results = {} ade_all, fde_all = [], [] for sid in self.scenario_names: mask = (scenario_ids == sid) count = int(mask.sum()) if count == 0: results[sid] = {"count": 0} continue ade_arr, fde_arr = self._compute_ade_fde( preds[mask], gts[mask] ) ade_mean, ade_ci = self.mean_ci(ade_arr) fde_mean, fde_ci = self.mean_ci(fde_arr) results[sid] = { "count": count, "ADE": ade_mean, "ADE_ci": ade_ci, "FDE": fde_mean, "FDE_ci": fde_ci, } ade_all.extend(ade_arr) fde_all.extend(fde_arr) # 全局值仍然保留,用于对照 results["global"] = { "count": len(ade_all), "ADE": float(np.mean(ade_all)), "FDE": float(np.mean(fde_all)), } return results

这段代码的逻辑很直接:通过布尔掩码把每个场景类别下的样本筛出来,分别计算误差和置信区间。实际工程中,我还会把结果封装成 DataFrame,方便下一步绘图和对比多个模型版本。

需要特别提醒:预测轨迹长度和采样间隔必须一致,否则 ADE 和 FDE 在不同模型之间没有可比性。我遇到过不少团队在评估脚本里把 6 秒轨迹和 3 秒轨迹混在一起算,最后得到的"提升"其实是长度差异造成的假象。

3.3 安全关键场景的专项评估

分场景指标只是第一步。真正让这套评估产生决策价值的,是"安全关键场景专项评估"。这类场景我会单独定义一份清单,不参与全局加权,而是作为硬性门禁条件。

比如我心中的安全关键场景至少包括四类:

  • 切入博弈(cut-in):旁车道目标在预测时段内跨越车道线并自车前方形成冲突点。
  • 无保护左转(unprotected left turn):对向直行车流持续,左转车辆需要判断可穿越间隙。
  • 弱势道路使用者交互(VRU interaction):行人或骑行者位于自车轨迹走廊内,距离小于 3 米。
  • 拥堵收窄(congestion merge):多车道汇入一车道,存在频繁加减速和礼让博弈。

对这四类场景,评估标准不能只看 ADE,我会额外关注两个"行为合理性"指标:碰撞距离余量(distance margin)和百公里期望危险事件数(expected hazard rate)。前者计算预测轨迹与目标物未来最近距离的中位数,后者统计预测轨迹与真值差异导致危险事件的比例。只有当场景样本量和置信区间都达标,模型版本才被允许进入下一轮实车测试。

用这种方式做专项评估之后,我们内部对"模型是否 Ready"的讨论明确了很多。以前是"A 版本比 B 版本分数低 0.02,A 上",现在变成"A 版本在 cut-in 场景的危险事件率是 3.1‰,B 版本是 1.7‰,A 不能上"。这两个讨论的性质完全不一样。

3.4 结果可视化与分析

评估结果一定要可视化。别小看这一步,一张好的"场景 x 模型"热力图,往往比十页指标表格更能说服协作团队。

我常用的做法是画一张矩阵热力图:

  • 行:场景类别,按风险等级从低到高排序;
  • 列:模型版本(比如 v1.0、v1.1、v2.0);
  • 单元格颜色:归一化后的 FDE 或危险事件率。

推荐用 matplotlib 的 imshow 或 seaborn 的 heatmap 都能快速实现。每周跑完评估后直接把这张图挂在 CI 展示墙上,工程师一眼就能看出新版本到底改善了哪个场景、又在哪里退步了。

可视化还有一个隐藏价值:它能帮我们跟数据团队对齐"场景定义是否合理"。如果某个场景类别的直方图非常稀疏,就说明场景标签太严格或样本采集不足;如果某个类别的误差分布呈现双峰,说明这一类内部还混着两种不同性质的子场景,需要再往下拆。

4. 踩坑实录与常见问题排查

4.1 场景定义漂移:同一场景在不同实验下不一致

这是我最先踩到的坑。第一版评估脚本里,场景标签写在评估函数内部,每迭代一次代码,标签判定逻辑就跟着改一点。结果同一个模型在两个 commit 下的"intersection"场景 ADE 一次是 0.31,一次是 0.26,我一开始还以为是模型优化有效,后来发现只是标签判断条件变了。

解决办法只有一个:把场景目录与标签定义固定成独立的配置文件,纳入版本管理。任何想调整场景定义的动作,都必须走正式变更流程,并在评估报告里标明 taxonomy_version。只有这样,跨版本、跨模型的横向对比才有意义。

类似的坑还包括:不同线程里加载数据顺序不一致导致场景 ID 错位;真值轨迹坐标没有统一到同一坐标系;预测输出的频率和真值采样频率不一致。这些我都会在评估流程里加入显式的 assert 检查,宁可报错也不让脏数据悄悄通过。

4.2 长尾场景样本太少怎么统计

场景驱动评估做深之后,一定会遇到长尾场景样本不足的问题。比如"夜间横穿马路的行人"这类高风险场景,整个验证集里可能就只有 20 来个片段,算出来的 FDE 置信区间非常大,没法和其他场景并列比较。

我处理这种情况有三个策略:

  1. 只报告不排名。样本量少于 30 的场景只展示平均值和样本数,不参与模型间排名,免得被噪声误导。
  2. Bootstrap 重采样。对每个长尾场景做 1000 次有放回抽样,输出误差分布的分位数,而不是单点均值。
  3. 案例库沉淀。把表现差的典型样本做成固定回归测试集,虽然数量少,但每个都经过人工确认,专门用来捕捉模型在特定长尾场景的退化。

这三个策略可以组合使用。不要因为"样本太少"就直接放弃长尾场景的评估,它们往往恰恰是安全性差异最大的地方。

4.3 权重设定的敏感性与公平对比

权重策略引入后,又一个现实问题出现了:同一个模型,在权重方案 A 下排名第一,在权重方案 B 下却排到第三。怎么办?权重本质上是评估者的价值判断,没有标准答案,但我们可以让判断过程更透明。

我每次发评估报告时,会附带一个简单的敏感性分析表格,展示权重在正负 30% 范围内波动时,模型总排名的变化。如果排名对权重非常敏感,说明两个模型的优势场景正好错开,这时候不能只看总分,而要看"安全关键场景专项结果"谁更好。如果排名对权重不敏感,那说明排名结果比较稳健,可以放心使用。

常见现象可能原因解决方式
同一模型两次实验分数波动大场景标签判定逻辑变化冻结 taxonomy 配置文件,纳入版本管理
长尾场景指标时高时低样本量不足,噪声主导使用置信区间或 bootstrap 分位数,样本少于 30 只报告不排名
加权总分与全局 ADE 排名矛盾不同模型优势场景错开披露权重来源与敏感性分析,参考安全专项结果
两个模型 FDE 接近但安全表现差异大平均指标掩盖极端异常增加碰撞距离余量和危险事件率等行为指标
可视化分布出现双峰场景内部混有未拆分子类型继续拆分场景或重新定义边界

4.4 关于这套方法,我的一些实话

用场景驱动评估将近一年后,我的总体感受是:它把"模型好不好"这个问题,从模糊的玄学变成了可以讨论的工程问题。

但我也要泼一盆冷水。场景驱动评估不是银弹,它解决的是"测量粒度"的问题,不能解决"测试分布本身不够"的问题。如果验证集里本身就没有暴雨、没有故障车、没有宠物突然窜出,那再精细的评估也无济于事,该补数据还是要补数据。

另外,也不要一开始就追求大而全的场景目录。我见过一个团队第一版就定了 40 多个场景类别,结果每个类别样本都稀稀拉拉,统计结果完全没法用。更好的节奏是:先定 8-10 个你最关心的高风险场景,跑通流程,让评估结果真正影响一次模型版本决策,再逐步扩展目录。少而准,好过多而乱。

最后分享一个实操中的小技巧:在评估流水线里保留原始预测 json 和场景标签的 join 字段,比如 scenario_id 和 object_id。这个习惯看似平凡,却能让很多事后分析变得可能——当你发现某个版本在某个场景下退步时,能直接溯源到具体预测样本,而不是面对一堆聚合后无法拆解的数字。真正做到"测量什么,就改进什么"。