从端到端轨迹预测到候选路径生成:生成-打分-后处理工程实践 1. 先把问题摆上台面端到端轨迹预测到底卡在哪聊候选路径生成之前得先把端到端轨迹预测这套范式的毛病说透不然很容易变成为了新而新。我这两年参与过几个预测模块的迭代从最早的一体化回归网络到后来的多模态锚点方案再到眼下这套生成候选 打分筛选的路子中间踩的坑基本都能归到同一类问题上模型输出的那条轨迹本质上是一个期望值而开车这件事要的从来不是期望值是一组可控、可解释、可拒收的选项。端到端轨迹预测的典型形态是把自车历史、周围目标历史、地图矢量一股脑塞进编码器解码器直接吐出一条或一组带概率的未来轨迹。训练目标是 L1/L2 或者高斯混合似然。这套东西在公开数据集上的 minADE 指标确实好看但真接到下游规划器里问题就来了——规划器拿到一条轨迹它没法判断这条轨迹有多可信也没法在危险的时候说这条我不要。它只能接受或者用一个完全独立于预测模块的规则代价函数去二次评估等于白干。1.1 回归式预测的三个硬伤第一个硬伤是模式平均。路口右转有两条车道可选直行有三种速度意图端到端回归在 L2 损失下会把这些模式的均值输出成一条哪边都不挨着的轨迹也就是常说的模式坍塌。你可以在损失里加多样性正则加 WTAwinner-takes-all监督但只要解码器的输出维度是固定的、监督信号是单点的模型就会倾向于把概率质量堆到最省事的那一个模式上。第二个硬伤是不确定性不可控。模型给你的方差是学出来的跟真实风险没有必然关系。我见过在加塞场景里模型给出极高置信度、但轨迹直接压在别人车头上的案例。方差大不代表危险方差小也不代表安全这个信号对下游几乎没用。第三个硬伤是分布外表现断崖。端到端模型学到的是数据里长什么样的场景该怎么走遇到施工区、临时锥桶、非标准路口这种训练集里稀少的形态输出会非常离谱而且没有任何中间产物可供排查。你不知道是编码器看错了还是解码器想歪了只能靠可视化硬猜。1.2 候选式范式的核心赌注候选路径生成换了个赌注不求模型一步到位给出正确答案而是让它先把可能性铺开再用一个独立的、可解释的打分环节去挑。这个过程拆成三步——生成generate、打分score、选择/后处理select refine。生成器负责覆盖打分器负责判别后处理负责兜住运动学可行性和舒适性。这个拆法最大的好处是每一环都可以单独验证。生成器好不好看候选集合的召回率真值轨迹在不在候选里在的话排第几。打分器好不好看 top-1 的命中率和闭环碰撞率。出了问题能定位这在实际项目里比多涨两个点指标重要得多。而且它天然是可拒收的。候选全被判定为不可行时规划器可以直接退回保守策略减速、靠边、原地等待而不是被迫执行一条它也不信的轨迹。这个退路在安全论证环节是刚需端到端方案很难提供。1.3 它和经典采样式规划的血缘关系说得直白点候选路径生成不是什么天外来物它跟十几年前就在用的采样式路径规划是同一个祖先。Frenet 坐标系下按横向偏移和纵向速度打网格、用五次多项式连成曲线、再用代价函数挑一条——这套流程在很多量产规划器里跑了很久。候选路径生成做的事是把手工打网格 手工写代价换成学习式生成 学习式打分同时把预测模块的多模态信息融合进来。我个人的判断是这条路线的价值不在于它多先进而在于它把工程上最难的如何保证不犯错拆成了若干可以分别设防的小问题。生成端保证不漏打分端保证不选错后处理端保证即使选错也不至于把车开进沟里。三层冗余比一个大黑盒好交代。2. 候选路径生成的整体架构建模架构层面我建议按四条流水线来理解不要一上来就想着端到端训一个大网络。这四条线分别是输入表征、候选生成器、打分器、选择与后处理。每一环的接口定义清楚后面替换任何一个模块都不会牵一发动全身。2.1 输入表征怎么定BEV 栅格还是矢量化别纠结看团队栈输入表征这块我的经验是别追求最先进追求最匹配。团队如果本来就有成熟的栅格化推理管线硬上矢量化注意力只会增加调试成本。栅格化 BEV 的好处是实现简单、卷积友好的、对遮挡和稀疏目标比较鲁棒。常用配置是自车坐标系下 x 方向 -30 到 70 米、y 方向 -40 到 40 米分辨率 0.1 到 0.25 米每像素通道叠历史帧、速度层、地图语义层车道线、路沿、可行驶区域。缺点是分辨率一高显存就吃紧远距离目标糊成一团。矢量化方案把车道线、路口、目标轨迹都表示成折线加属性用注意力做交互。优势是精度高、天然支持长距离缺点是对地图质量极度敏感地图一错全错而且实现复杂度高一个量级。我的建议是混合近场自车 30 米内用栅格保证细节远场用矢量折线两者在特征层拼接。这套做法在多个项目上验证过收益比纯用一种方案明显。注意坐标系一定要统一到自车当前帧的局部坐标系并且把历史帧按自车运动做补偿。我见过因为历史帧没做位姿补偿导致生成器学到所有车都在往左漂的诡异先验调了两周才发现。2.2 生成器四种主流做法与取舍候选生成器负责把 K 条候选轨迹吐出来K 一般在 32 到 192 之间。主流的四种做法我按落地难度从低到高排第一种锚点式anchor-based。预先定义一组候选轨迹模板锚点网络只负责对每个锚点做偏移回归和置信度分类。锚点的构造可以是在 Frenet 下打格子也可以是对专家轨迹做聚类。这是最容易落地的一种因为锚点本身就是运动学可行的网络只需要学往哪偏一点。第二种查询式query-based。用一组可学习的 query 向量去和场景特征做交叉注意力每个 query 解出一条轨迹。灵活性比锚点高能表达锚点覆盖不到的形状但训练更不稳定需要配合集合预测损失类似 DETR 那套匈牙利匹配。第三种生成模型式VAE / 扩散。用隐变量采样出多样性扩散模型在轨迹生成上这两年很热。优点是分布拟合能力强缺点是推理步数多时延压不住。实测下来在车端算力受限的情况下扩散采样 10 步以上就很难塞进 50 毫秒的预算里。除非你有很强的蒸馏方案否则不建议作为首发方案。第四种混合式。锚点打底保证覆盖和可行性查询或隐变量做残差修正。这是我现在最推荐的做法底座稳上限也不低。方案覆盖能力训练稳定性单帧时延K64落地建议锚点式中高5-8 ms首选做 baseline查询式高中8-12 ms有调参人力时上扩散/VAE很高低30-80 ms需蒸馏后再上车混合式高高10-15 ms推荐终态2.3 打分器分类头够用但要小心假高分打分器是整套方案里最容易被低估的部分。很多人把它做成一个简单的分类头对 K 条候选输出 softmax取 argmax。能跑但效果一般。我的做法是三路代价融合学习式打分 规则代价 不确定性惩罚。学习式打分用交叉熵训练一个候选是否接近真值的分类器标签用与真值轨迹的 FDE 阈值划分比如 FDE 2 米算正样本。规则代价则显式地算碰撞风险、偏离参考线的距离、曲率变化率、纵向加加速度这些量。不确定性惩罚针对那些模型自己也不确定的候选用多次前向的方差或者 ensemble 的分歧度来度量。三路融合的权重不要拍脑袋定用一小批闭环数据做网格搜索我的经验是学习式打分占 0.6、规则代价占 0.3、不确定性占 0.1 起步然后按场景类型微调。城区路口场景规则代价权重要往上提高速场景学习式打分权重可以更高。提示规则代价一定要用可微或至少可批量化的实现不然 K128 的候选逐条算 Python 循环时延直接爆掉。2.4 选择与后处理真正决定体验的一公里选出 top-1 之后不要直接扔给控制。中间至少还要过四道工序一是运动学可行性检查用车辆的运动学模型自行车模型够用验证曲率、侧向加速度、纵向加速度是否在包络内超了就截断或者降速。二是轨迹平滑把离散点用五次样条重新拟合保证曲率连续。生成器输出的点列往往有细微抖动直接送给控制会产生方向盘抖动乘客体验很差。三是时间一致性相邻两帧选出的候选不应该来回跳变。做法是对上一帧的选择加一个惯性项除非新候选的得分优势超过阈值否则保持上一帧的选择。四是兜底策略所有候选都不可行时切到保守的减速靠边或者原地等待并且把无可行候选这个事件上报这是数据闭环里最有价值的信号之一。3. 关键参数与损失设计那些调参调出来的经验这部分是我觉得最值得分享的因为公开资料里对为什么是这个数讲得很少大多只给一个结果。3.1 候选数量 K不是越多越好K 的选取有个隐性的边际收益曲线。我做过一组对比K 从 16 涨到 64候选集合的召回率真值在集合内的比例容差 2 米从 71% 涨到 93%涨幅明显64 到 128 只涨到 95.5%128 到 256 涨到 96.2%但时延翻了将近一倍。所以我的默认值是K64遇到复杂路口场景可以动态上浮到 96 或 128简单的高速巡航场景降到 32 甚至 24。动态 K 的实现方式是按场景复杂度目标数量、车道数、路口标记算一个分数映射到 K 的档位。这个技巧在时延敏感的项目里非常值钱。3.2 时域与步长4 秒和 8 秒是两个不同的产品预测时域 4 秒和 8 秒对应的其实是两种产品定位。4 秒足够覆盖绝大多数城市工况下的决策需求误差累积小模型好学8 秒能覆盖更长的规划视野但后半段的误差会急剧放大模型输出的本质上是一团概率云。我的做法是分段输出0 到 3 秒用 0.1 秒步长控制精度需要3 到 8 秒用 0.5 秒步长只给规划做趋势参考。这样既保证了近端的精度又不会让远端的噪声干扰下游。坐标上还有一个容易忽略的点输出用自车坐标系还是世界坐标系。如果下游要做多帧融合或者跟地图对齐世界坐标系更方便如果只关心相对运动自车坐标系更直接。我倾向于训练时用自车坐标系归一化更好做推理后转成世界坐标系输出。3.3 锚点集合怎么造聚类比打格子更靠谱如果走锚点路线锚点集合的质量直接决定上限。两种造法格子法在 Frenet 下按横向偏移 {-3.5, -1.75, 0, 1.75, 3.5} 米和终端纵向速度 {0, 3, 6, 9, 12} 米每秒打网格得到 25 条基础曲线每条用五次多项式连接。优点是物理意义清晰、覆盖均匀缺点是真实驾驶行为的分布不是均匀的很多格子里的轨迹现实中根本不会出现浪费了候选额度。聚类法从专家驾驶数据里切出未来 4 秒的轨迹归一化到自车坐标系后做 k-means 或者层次聚类聚出 64 个簇心当作锚点。优点是锚点分布贴合真实行为分布同样的 K 值下召回率通常比格子法高 5 到 8 个百分点。缺点是可解释性差一点锚点形状有时候很怪。我的实践是先聚类再人工审查把明显不合理的簇心比如横跨对向车道的剔掉用邻近的合法簇心补位。这一步花不了多少时间但能避免训练初期的灾难性收敛。3.4 损失函数组合多样性正则的权重是个玄学生成器的损失一般是三项叠加回归损失候选对真值的 L1/L2只在匹配上的候选上算、分类损失候选是正样本的概率、多样性正则惩罚候选之间的过度相似。多样性正则的权重非常敏感。权重太小候选全挤在一起等于 K1权重太大候选四散奔逃回归损失学不动。我试过的有效区间大概在 0.05 到 0.2 之间具体值跟 K 有关K 越大权重越小。有个经验公式可以参考权重 ≈ 0.3 / sqrt(K)K64 时约 0.037实际取值在这个附近微调。另外WTA 监督里每个真值只监督最近的候选这种做法会导致候选利用率低很多候选从来没被监督过。改进办法是top-M 监督对每个真值取距离最近的 M 个候选都施加回归损失M 取 3 到 5。实测能明显提升候选集合的整体质量。3.5 标签分配与难负样本决定打分器上限的地方打分器的训练数据构造比模型结构重要得多。正样本好办跟真值 FDE 小于阈值的候选都算正。难的是负样本。随机负采样训出来的打分器在简单场景下表现很好一遇到加塞、鬼探头就抓瞎因为它没见过看起来很顺但实际有碰撞风险的样本。我的做法是三类负样本混合第一类是碰撞负样本从闭环仿真或者重建的真值场景里把那些几何上会撞的候选标出来权重给高。第二类是近失负样本最小距离在 0.5 到 2 米之间的候选这类最难分也最有价值。第三类是分布外负样本从其他场景迁移过来的、形状合理但位置荒谬的候选用来提升打分器的泛化。比例上我一般按 1 : 2 : 1 配正样本占四分之一左右。这个配比在几个项目上验证下来比较稳。4. 从零跑通一条最小链路讲完原理落到能跑的代码。下面这条链路是我建议的起点单卡可训、单帧推理 15 毫秒以内、指标够看。4.1 数据与坐标约定先定死契约这是整个项目的地基。统一约定所有输入输出都在自车当前帧坐标系下x 向前y 向左单位米。历史长度 2 秒20 帧 10Hz预测长度 4 秒40 步 10Hz。每条轨迹表示为 [T, 3]即 (x, y, yaw)速度通过差分得到。有效位掩码单独一个 [T] 的 bool 数组缺失帧用掩码屏蔽而不是填零。这套约定听起来琐碎但我在三个项目里见过因为历史帧未对齐和缺失值填零导致的静默 bug排查成本极高。4.2 生成器最小实现从锚点式起步代码量最小也最容易验证收敛。import torch import torch.nn as nn class AnchorGenerator(nn.Module): def __init__(self, num_anchors64, future_steps40, hidden128): super().__init__() self.num_anchors num_anchors self.future_steps future_steps # anchors: [K, T, 2] 预聚类得到的候选模板注册为 buffer 不参与训练 self.register_buffer(anchors, torch.zeros(num_anchors, future_steps, 2)) # 每个锚点的残差回归头 置信度头 self.res_head nn.Sequential( nn.Linear(hidden, 256), nn.ReLU(), nn.Linear(256, future_steps * 2) ) self.cls_head nn.Sequential( nn.Linear(hidden, 128), nn.ReLU(), nn.Linear(128, 1) ) def forward(self, scene_feat, query_emb): # scene_feat: [B, D] 场景融合特征 # query_emb: [B, K, D] 锚点嵌入可为常数 h scene_feat.unsqueeze(1) query_emb # [B, K, D] residual self.res_head(h).view(-1, self.num_anchors, self.future_steps, 2) logits self.cls_head(h).squeeze(-1) # [B, K] # 残差做缩放避免早期训练把锚点带飞 traj self.anchors.unsqueeze(0) 0.1 * torch.tanh(residual) return traj, logits关键点在0.1 * torch.tanh(residual)这一行。残差如果不做缩放和饱和训练初期网络会输出极大的偏移量锚点的先验信息完全被破坏收敛会非常慢。加上 tanh 约束到 [-0.1, 0.1] 米量级等模型稳定后再逐步放开这个系数是常见做法。4.3 打分器训练与指标打分器单独训练输入是候选轨迹本身加上一组几何特征与车道中心线的横向距离序列、与各目标的最近距离序列、曲率序列、速度序列不依赖生成器的中间特征这样两个模块可以解耦迭代。def score_features(traj, lanes, agents): # traj: [B, K, T, 2] feats [] feats.append(dist_to_lane_center(traj, lanes)) # [B, K, T] feats.append(min_dist_to_agents(traj, agents)) # [B, K, T] feats.append(curvature_series(traj)) # [B, K, T] feats.append(speed_series(traj)) # [B, K, T] return torch.cat(feats, dim-1) # [B, K, T, C]训练目标用加权交叉熵正负样本权重按前面说的 1:2:1 配。评估时除了准确率一定要看top-1 的 FDE 分布和top-1 的碰撞率前者衡量精度后者衡量安全。4.4 推理端集成与时延预算集成到车端时时延预算是硬约束。我按 10Hz 控制周期给一个参考分配环节预算实测K64单卡中端 GPU特征编码含 BEV 构建20 ms12 ms候选生成10 ms6 ms打分与排序8 ms5 ms后处理与平滑5 ms2 ms合计43 ms25 ms留出的余量主要给动态 K 上浮和偶发的复杂场景。注意 BEV 构建这块容易被低估栅格化本身不做优化的话能吃掉 20 毫秒以上一定要用 GPU 上的向量化实现。4.5 评测指标别只看 minADE这是我想强调的一点。minADE 和 minFDE 衡量的是候选集合里最好的那条有多好它压根不关心打分器也不关心下游能不能用。只看这两个指标会得出候选越多越好的错误结论。我建议至少补三个指标候选召回率RecallK真值轨迹与候选的最小 FDE 小于阈值的比例阈值按场景定城区 2 米、高速 3.5 米。这个指标直接反映生成器质量。Top-1 可用率打分器选出的 top-1 轨迹其 FDE 小于阈值的比例。生成器好但打分器差这个指标会很难看。闭环碰撞率把整套链路接到仿真环境里跑统计每千公里碰撞次数。这个指标最贵但也最真实。我一般要求前两个指标达标召回率 90%top-1 可用率 80%之后才投入闭环测试的资源。提示候选召回率要在按场景类型分层的统计下看只看全局平均会掩盖问题。城区路口和高速巡航的召回率能差 20 个百分点全局数字好看但路口拉胯等于没做。5. 常见问题与排查实录这部分是我这些年真正花时间的地方很多问题在论文里根本不会提。5.1 模式坍塌候选看起来有 64 条实际只有 3 种形状现象是可视化时 64 条候选轨迹几乎重叠在三条曲线上。原因通常是多样性正则太弱或者 WTA 监督下大量候选从未被激活梯度为零逐渐退化到同一位置。处理办法先看候选的激活分布统计每个候选被选为 winner 的次数。如果长尾严重前 10 个候选占了 80% 的激活说明锚点集合本身分布不均重做聚类。如果激活均匀但形状相似就是多样性正则的问题把权重往上调一档同时检查回归损失是否用了 top-M 监督。还有一个隐藏原因是批次内场景太单一同一个 batch 全是直行道模型自然学不到分叉需要重做数据采样保证每批至少包含 20% 的路口场景。5.2 生成器和打分器错配召回率 95%top-1 可用率只有 60%这是最常见的组合问题。生成器把真值覆盖得很好但打分器选不出来。根因往往有两个一是打分器的训练数据分布和生成器的输出分布不一致打分器是在旧版生成器的输出上训的迭代时忘了同步二是打分器过拟合了真值轨迹的平滑性导致它偏好那些特别顺滑但方向错误的候选。第一个问题的解法是强制同步迭代每次生成器更新打分器必须在新生成器的输出上重新训练不能复用旧数据。第二个问题要在训练数据里加入足够的平滑但错误的负样本让打分器学会区分顺滑和正确。5.3 候选全不可行兜底策略没设计好会直接卡死在极端场景下被三辆车同时挤压、施工区临时改道K 条候选可能全部被规则代价判为不可行。这时候如果没有兜底系统会输出一个异常值下游直接罢工。兜底策略要分层第一层是放宽阈值重选把碰撞距离阈值从 0.5 米降到 0.3 米再选一次第二层是降级到低速蠕行候选专门预留一个低速候选池横向偏移只允许很小范围第三层是靠边停车。每一层触发都要打点上报因为这些都是高价值的长尾数据。5.4 时延超标八成是后处理写成了 Python 循环推理阶段时延超标我排查过的案例里超过一半是后处理模块对 K 条候选逐条做 Python 循环和几何运算。把后处理改成张量化的批量运算时延通常能从 30 毫秒降到 3 毫秒以内。第二种常见原因是 BEV 栅格构建在 CPU 上做这个必须要挪到 GPU。第三种是动态 K 的判定逻辑本身耗时比如用了复杂的场景复杂度评估网络这个要么简化成规则要么提前一帧算好。问题现象可能根因排查动作处理方式候选形状高度重叠多样性正则过弱 / 锚点不均统计 winner 激活分布重聚类锚点 调权重至 0.1 量级召回高但 top-1 差生成器与打分器分布错配比对新旧生成器输出分布同步迭代禁止复用旧数据全部候选不可行阈值过严 / 场景极端打印各候选的代价明细三层兜底 事件上报单帧时延突然翻倍后处理循环 / BEV 在 CPU分段打点计时张量化 上 GPU路口场景召回骤降训练集中路口占比过低统计场景类型分布重采样至路口占比 20% 以上输出轨迹抖动曲率不连续 / 无时间一致性检查曲率序列五次样条重拟合 惯性项5.5 分布外场景别指望模型指望拒收机制施工区、事故现场、非标准环岛这些场景再怎么加数据也覆盖不全。我的态度很明确不要求模型在这些场景下表现好只要求它能识别出我不确定。实现上靠两件事一是打分器的置信度阈值top-1 得分低于某个值时触发告警二是生成器的候选分散度如果 K 条候选的方差异常小说明模型强行收敛到一个模式也触发告警。两个信号任意一个触发就降级到保守策略。这套机制在实测中能拦住大部分危险情况比硬堆模型能力靠谱得多。6. 影响范围与适用边界最后聊聊这套范式往外溢出的影响以及什么时候别硬上。6.1 对下游规划控制的影响最大的变化是接口语义变了。原来规划器接收的是一条预测轨迹现在接收的是一组带分数的候选。这意味着规划器可以把预测的不确定性直接纳入优化——候选的分布本身就构成了一个软约束空间规划器可以在候选密集的区域自由选在候选稀疏的区域保持警惕。代价是规划器的计算量会上去因为它要考虑的不再是一个点而是一片分布。实际工程里通常的做法是取 top-3 到 top-5 送进规划而不是全部 K 条这样既保留了多模态信息又不至于让优化问题爆炸。控制端的影响主要在平滑性。候选轨迹是离散采样的直接执行会有抖动必须做样条重拟合和曲率约束。这块的工作量常被低估我见过因为控制端没做好平滑导致整套方案体验还不如原来的端到端方案。6.2 对数据闭环与标注成本的影响这套范式对数据的要求跟端到端方案不太一样。端到端方案要的是大量场景的正确轨迹标注候选式方案更看重场景的多样性而不是数量因为生成器的锚点是从数据分布里聚出来的分布的偏斜会直接反映到候选覆盖上。另外无可行候选这个事件本身就是极好的挖掘信号。每次触发兜底就自动把当前场景切片存下来这比按里程随机采样高效得多。我在一个项目里用这个机制两个月挖出了三百多个高价值长尾场景标注成本省了一大截。标注上还有个细节候选式方案需要的是多模态真值也就是同一个场景下多条合理的未来轨迹。单条真值会让生成器学不到其他模式。这个成本是实打实的通常靠仿真推演加人工校验来补。6.3 什么情况下别硬上有三种情况我会明确建议放弃这套方案。第一算力极度受限。候选生成加打分加后处理最低也要十几毫秒的预算如果整个感知加预测只给你 20 毫秒那老老实实用轻量回归模型别折腾。第二场景极度单一。如果产品形态就是封闭园区低速巡航路型固定、交互简单端到端回归完全够用候选式方案带来的复杂度是纯负担。第三团队缺少闭环评测能力。这套方案的价值很大程度上依赖生成-打分-后处理三环的独立验证如果没有仿真环境和分层指标你根本不知道问题出在哪一环调参会变成玄学。这种情况下先把评测基建搭起来比直接上方案更重要。我自己的体会是候选路径生成这套东西的技术门槛其实不高难的是工程纪律——接口约定要死、迭代要同步、指标要分层、兜底要分层。这几个死字做到了方案就能稳做不到K 调到 256 也没用。