配套:UniAD 论文精读。

本文以本地 HEAD 的标准 stage2 配置为准;论文里的模块名与 Python 类名并不总相同。以下张量形状来自源码推导,未以实际 batch 运行验证。B 是批量大小;标准配置每 GPU 为 1;A 是筛选后的目标数量,随场景变化;特征维度 D=256。源码部分旧注释仍写 300 个检测 Query,应以当前 num_query=900 和实际分配为准。

1. 先建立整体认识

UniAD 用相机和车辆状态,输出自车未来的位置序列,同时预测周围物体、道路和未来占用。

  • 感知:现在有什么、在哪里、同一目标跨帧是否仍是同一个。
  • 预测:其他交通参与者未来怎么走,哪些地面区域未来会被车占用。
  • 规划:结合场景与导航意图,给自车规划未来怎么走。
  • 控制:把轨迹变成方向盘、油门、刹车;UniAD 开源实现这里的规划头没有直接输出这些控制量。

网络不是几个完全独立的头:后面的头会读取前面产出的高维 Query,也会读取框、位置、匹配索引等结构信息。不能简化为“模块之间只传 Query,从不传框”。总调度在 uniad_e2e.py:88 与 uniad_e2e.py:235。

2.1 真正参与前向预测的输入

输入形状或内容实际用途
当前 6 路环视图像单帧 [B,6,3,H,W]提取视觉特征
训练图像队列stage2 [B,3,6,3,H,W];stage1 队列长 5学习时序融合与跨帧跟踪;含当前帧,并非当前帧外再加 3 帧
相机标定和图像元信息 img_metaslidar2img、图像尺寸、场景 token 等把空间采样点投影到各相机
自车状态 can_bus18 维,位置/姿态/运动状态信息BEV 对齐、状态嵌入
timestamp、l2g_r_mat、l2g_t时间戳、局部坐标到世界坐标变换对齐跨帧位置、更新目标参考点
高层指令 command0=右转,1=左转,2=直行告诉规划头希望往哪个方向走
历史网络状态上一帧 BEV、跟踪 Query/ID、目标记忆视频推理时跨帧复用

图像原尺寸通常为 900×1600;标准 pipeline 归一化后补齐到 32 的倍数,所以应写 H_pad,W_pad,不能把固定原图尺寸当作最终张量尺寸。标准 FPN 输出 4 个尺度,各为 [B,6,256,H_l,W_l]。

lidar2img 中的 LiDAR 表示采用的空间坐标系和标定变换,不表示模型读取了激光点云。配置 use_lidar=False、use_map=False:不把激光或现成高精地图送入视觉网络;训练和评测依然会使用数据集的框、地图、未来轨迹标注。

代码:配置 base_e2e.py:10;图像特征 uniad_track.py:153;时序训练 uniad_track.py:493;指令标签 trajectory_api.py:226。

特别注意:离线数据集用未来真实轨迹的横向位移生成左/右/直指令。这是 benchmark 的指令来源,不等于实车能预知未来;实际部署须由导航系统提供指令。

2.2 输出什么

输出内容或形状预测时间
检测与跟踪3D 框、10 类类别分数、速度、跟踪 ID当前时刻
在线地图分隔线、斑马线、道路边界实例与可行驶区域 mask当前局部地图
运动预测最终层 [B,A,6,12,5] 的轨迹分布参数,以及 [B,A,6] 模态 log 概率12 步,每步 0.5s,共 6s
未来占用实例 logits [B,A,5,200,200];合并占用 [B,5,1,200,200]当前 + 未来 4 步,共预测至 2s
自车规划sdc_traj / sdc_traj_all:[B,6,2]未来 6 步,每步 0.5s,共 3s

轨迹的 5 个数是 x,y 和三个二维高斯分布相关参数,实际激活与 NLL 参数化看 bivariate_gaussian_activation / traj_nll,不要把全部 5 维当作 5 个位置坐标。最终位置使用前两维。MotionHead 内部还联合预测自车的六模态轨迹,再把自车 Query 单独取出供规划;A 在不同内部阶段可能包含自车,进入 OccHead 前已剔除自车并筛选车辆类。

地图输出是局部语义/实例 mask,并非 SLAM 的长期全局地图。SLAM 指边定位边建持久地图,这不是 MapFormer 在此承担的任务。

3. 整体结构和数据流 Mermaid

下一帧

6 路环视图像

ResNet101 + DCNv2

FPN:4 尺度,每尺度 256 通道

标定 lidar2img + 自车状态

BEVFormer Encoder:6 层时空注意力

40000 个可学习 BEV Query

历史 BEV:旋转和平移对齐

共享 BEV:200 × 200 × 256

TrackFormer:6 层检测解码器

900 个检测槽 + 1 个自车槽;跨帧更新

目标框、类别、速度、ID

Agent Query + Ego Query

MemoryBank + Query Interaction

MapFormer:6 层 Encoder + 6 层 Decoder

300 个实例 Query + 1 个区域 Query

Thing 4 层 / Stuff 6 层 Mask Head

局部地图 mask

Map Query

MotionFormer:3 轮场景交互

每类组 6 种轨迹锚点 + 可学习模态嵌入

目标六模态轨迹:12 步 / 6s

非自车 Track / Trajectory Query

OccFormer:5 个时间块,各 1 层

占用:当前 + 4 步,200 × 200

Ego Track / Trajectory Query

融合导航指令;3 层 Planning Decoder

右转 / 左转 / 直行

原始自车轨迹:6 步 / 3s

推理时:非线性避碰优化

最终自车未来位置序列

读图时分清:Track 与 Map 共享 BEV、平行提取当前场景;Motion 同时读取两者;Occ 与 Plan 读取 Motion 特征;Occ 到 Plan 的直接路径主要是推理时避碰优化。训练 PlanningHead.forward_train() 将 occ_mask=None,并不把预测占用图直接送进规划解码器。代码 uniad_e2e.py:88、planning_head.py:139、planning_head.py:203。

4. Query 是什么,各自怎么生成

Query 可以理解为一张可训练的信息卡片:起初只有一个向量,经注意力读取特征后,逐步装入某个位置、目标或轨迹的信息。这里的任务 Query 与注意力公式中的 Q 有联系,但不能混为一个概念:任务 Query 经过线性投影才形成注意力计算的 Q,其他信息提供 K/V(用于检索和读取的特征)。

通常可写 Attention(Q,K,V)=softmax(QKᵀ/√d)V;可变形注意力则围绕参考位置采样少量点,不必让每张卡片读取所有像素。

Query 家族数量/维度从哪里来负责什么实现
BEV Query200×200=40000 个,256 维nn.Embedding(H*W,D),加二维位置编码和车辆状态嵌入每个地面格读取图像与历史信息track_head.py:130、transformer.py:101
初始检测/Object Query900 个槽,初始化表每行 512 维nn.Embedding(901,512) 的前 900 行,拆成 256 维位置和 256 维内容寻找新目标、回归类别和 3D 框uniad_track.py:117、uniad_track.py:178
跟踪/Agent Query有效目标数动态变化,输出特征 256 维前一帧已识别目标的特征,经记忆和 QIM 更新继续使用把同一个目标跨帧联系起来modules.py:115、modules.py:7、uniad_track.py:619
自车/Ego/SDC Query1 个初始槽;解码后 256 维同一张 901 行嵌入表的最后一行,下标 900;特殊 ID -2表达自车运动状态,并参与 MotionFormeruniad_track.py:117、uniad_track.py:358
地图实例 Query300 个,初始化 512 维,解码后 256 维PansegformerHead.query_embedding分隔线、斑马线、道路边界等实例panseg_head.py:121、panseg_head.py:182
地图 Stuff Query1 个,初始化 512 维stuff_query=nn.Embedding(num_stuff_classes,2D)可行驶区域这种连续区域panseg_head.py:121、seg_mask_head.py:310
Motion / Trajectory Query每个 Agent 6 个模式,[B,A,6,256]4 个类别组×6 个学习模态嵌入;加离线聚类轨迹锚点的终点位置嵌入,再与目标特征交互同时表达直行、转向等多种未来可能base_motion_head.py:33、base_motion_head.py:47、motion_head.py:212、modules.py:20
Occupancy Instance Query每个有效车辆 1 个,[B,A,256]最后 Motion 层的 6 模态特征经 MLP/max 汇聚,再拼接 track feature 与位置嵌入,用 MLP 融合生成该车辆在各时间的占用 maskocc_head.py:271
Occupancy Dense Query注意力内部约 25×25=625 个空间 token主干 BEV 先重采样至占用网格,卷积降采样至 1/8读取车辆实例信息,更新未来空间状态occ_head.py:198
Planning Query融合后 1 个,[B,1,256]自车 6 模态轨迹 Query + 自车 Track Query + 指令 embedding,拼接 MLP 后对 6 模态取 max从 BEV 提取与自车规划相关的信息planning_head.py:139

几个关键区别:

  1. 检测槽与跟踪 Query 不是两张完全独立的固定学习表。“跟踪 Query”主要指已携带前帧目标信息的动态状态。每帧组合初始槽和保留的 active tracks,实际进入解码器的数量可变化,不能一直写死为 901。
  2. 初始参考点由 Query 的位置半部经过线性层得到,再 sigmoid;检测层逐层细化。后续帧还结合速度、时间间隔与坐标变换更新位置。
  3. 地图实例 mask Query 从地图定位 Decoder 的输出中筛出;区域 Stuff Query 使用单独学习表。送给 MotionFormer 的 lane_query 来自 args_tuple 中的定位 Decoder Query,不能直接说是拼接了全部 Thing/Stuff mask Query。
  4. 轨迹锚点是训练数据离线聚类得到的“典型走法”,从 data/others/motion_anchor_infos_mode6.pkl 读取,不是运行时随机生成,也不直接等于最终预测轨迹。4 类组是机动车、两轮车、行人、障碍物组;每个目标依据类别选择 6 个模态。
  5. OccFormer 的 cross-attention 中,Q 是稠密空间状态,K/V 是车辆实例特征。最后才以实例向量与空间特征的点积生成实例 mask;这与“实例 Query 直接去图上找 mask”的笼统表述有区别。
  6. Planning Query 不是一张独立 6 行轨迹学习表。它由上游自车特征和指令组合,再汇成 1 个 token;最后一次回归全部 6 个未来点。

5. 图像如何映射成 BEV

BEV(Bird’s-Eye View,鸟瞰表征)是统一地面坐标上的特征网格,不是 RGB 图片,也不是已经完成分类的占用图。每格有 256 个可学习特征值。

5.1 从 BEV 位置反查图像,而非先估计每个像素的深度

  1. 提取图像特征。 六个相机共享 ResNet101/DCNv2,再经 FPN 生成 4 个尺度,便于同时处理远处小物体和近处大物体。uniad_track.py:153
  2. 给每个地面格建一根虚拟竖柱。 在已知格子 (x,y) 上选 4 个高度参考点。高度范围来自 pc_range 的 [-5,3]m;具体归一化采样看 get_reference_points。这不是深度相机观测。encoder.py:49
  3. 投影到相机。 对空间点 p=[x,y,z,1]ᵀ,计算 p_img=T_lidar2img·p,再除以投影深度,得到像素 (u,v),按图像尺寸归一化。相机后方与视野外的点用 mask 去掉。encoder.py:91
  4. 空间交叉注意力 SCA。 BEV Query 在这些投影位置附近,通过学习的采样偏移和权重,从各相机、多尺度特征中读取信息,再聚合可见相机。标定提供位置基础,网络学习怎样取特征。spatial_cross_attention.py:31
  5. 时间自注意力 TSA。 先按自车运动对历史 BEV 旋转和平移对齐,再融合历史与当前信息。每层按 TSA → SCA → FFN 顺序处理;FFN 是对每个 token 进一步变换特征的前馈网络。共重复 6 层。transformer.py:101、base_e2e.py:140

它解决单目深度歧义的方式是几何参考 + 多视角 + 多时刻 + 任务监督,不能理解成一次相机投影就知道了所有物体的真实深度。此路径没有单独预测深度图再做 LSS 式 lift/splat。

地面格 x,y 的 BEV Query

沿高度取 4 个 3D 参考点

lidar2img 标定投影

检查深度为正和图像范围

投影位置附近可变形采样

6 相机 × 4 尺度图像特征

相机内加权;可见相机结果平均

更新该格的 256 维 BEV 特征

5.2 BEV 分辨率与范围

用途网格XY 覆盖每格实际大小
主干共享 BEV / Map200×200X/Y 都是 [-51.2,51.2]m,共 102.4m×102.4m0.512m × 0.512m
Occ 输出与规划碰撞栅格200×200X/Y 都是 [-50,50]m,共 100m×100m0.5m × 0.5m
Occ 中间状态50×50,注意力内部再到 25×25对应占用范围降采样计算,最终恢复为 200×200

102.4/200=0.512。配置的 voxel_size=[0.2,0.2,8] 不是主干 BEV 输出每格 0.2m。OccHead 使用 BevFeatureSlicer 按坐标重新采样,不能只因形状相同就把两种网格当成同一物理范围。配置 base_e2e.py:10;占用处理 occ_head.py:198。

6. 到底有多少 Transformer

按功能讲,是 6 个部分:BEV 编码器、TrackFormer、MapFormer、MotionFormer、OccFormer、规划 Decoder。 论文的四个“Former”是任务模块名,不意味着只有 4 个 Transformer 层或只有 4 次注意力。

按源码展开,有下面 9 组主要结构;Motion 那一组内部还要展开:

结构标准配置的层数注意力在问什么监督来源
BEVFormer Encoder6该地面格从哪些图像位置、哪些历史位置取信息?下游任务间接监督;stage2 默认冻结
Track Detection Decoder6目标槽之间相互去重/交流,再从 BEV 读取目标信息类别、3D 框、跨帧 ID、辅助轨迹
Map Encoder6对共享 BEV 再编码,提取地图相关空间上下文地图头间接监督
Map Location Decoder6每个地图实例在哪里、是什么?类别、2D 包围框、GIoU,逐层辅助监督
Map Thing Mask Head4该地图实例覆盖哪些格子?mask Dice,以及对应分类/框辅助损失
Map Stuff Mask Head6可行驶区域覆盖哪些格子?区域 mask Dice / 分类
MotionFormer3 轮;另有 1 层 intention interaction与哪些车、道路、目标位置发生关系?每轮多模态轨迹损失
OccFormer Decoder总计 5 层,分给 5 个时间块,各 1 层该未来空间格从哪些车辆特征获取占用信息?当前及未来实例 mask 主/辅助监督
Planning Decoder3结合导航意图,自车应从 BEV 读取什么?自车真实未来轨迹及配置中的碰撞项

MotionFormer 每轮包含 1 个 Agent interaction DecoderLayer + 1 个 Map interaction DecoderLayer + 1 个 BEV 可变形 AttentionLayer。另在循环外用 1 个 Intention interaction EncoderLayer 让同一目标的 6 种意图交流。因此这里的“3 轮”内部有 1+3×3=10 个这类交互层,而非只 3 个普通 TransformerDecoderLayer。modules.py:20

若明确采用“每个 encoder/decoder/自定义 mask block 算 1 层,Motion 展开到其内部交互层”的口径,上表为 6+6+6+6+4+6+10+5+3=52 个主要 block。52 是这一特定计数口径,不是项目官方统一层数:不含两条 Map Mask Head 的末尾 AttentionTail,也不含跟踪 MemoryBank/QIM 的额外注意力与 FFN;这些可见 seg_mask_head.py:310、modules.py:7、modules.py:115。同一层又可能含 self/cross 两次 attention,所以 52 也不是 attention 算子数量。

配置与实现:base_e2e.py:140、base_e2e.py:246、base_e2e.py:399、base_e2e.py:334、planning_head.py:64。

7. 各部分如何监督,loss 是什么

监督就是用数据集“正确答案”与预测比较,把误差反传给参与训练的参数。GT(ground truth)表示真实标注;mask 表示哪些时间/像素有效。

7.1 总损失

默认五个任务权重均为 1:

L = L t r a c k + L m a p + L m o t i o n + L o c c + L p l a n n i n g . L=L_{track}+L_{map}+L_{motion}+L_{occ}+L_{planning}. L=Ltrack​+Lmap​+Lmotion​+Locc​+Lplanning​.

每项内部已带不同 loss 权重,还包含中间 Decoder 层的辅助损失。UniAD.forward_train() 给键名加 track. / map. / motion. / occ. / planning. 前缀。注意 Motion 字典同时返回 min_ade、mr 等日志值;不能把输出字典中每个标量都再次加进优化目标,MMDetection 按 loss 键命名解析。uniad_e2e.py:88

7.2 Track:分类、框与跨帧身份

  • 2.0 × FocalLoss:判断目标属于哪一类,降低大量简单背景的主导作用。
  • 0.25 × L1:回归编码后的 3D 框、方向、速度等;速度对应 code weights 为 0.2。
  • stage2 默认还有 loss_past_trajs,为带有效 mask 的轨迹 L1,权重默认 1。该分支输出 past_steps+fut_steps=8 个点,名称里有 past,但不能直接等同于 MotionHead 的 12 步未来预测。
  • stage1 明确把 loss_past_traj_weight=0.0;不能把 stage1 的禁用状态套给 stage2。
  • 配置里检测 GIoULoss 权重为 0,不应说 Track 的主要监督包含有效 3D IoU loss。

匹配先利用已跟踪目标的真实 ID 继承对应关系,再用 Hungarian matching 给未匹配槽和新目标做一对一分配。Hungarian matching 就是给多张预测卡片与多个真实目标找一个总误差尽量小、互不重复的配对。不是每帧把所有目标当作全新对象重新匹配,也不是这里靠单独 ID 分类 CE 学跟踪。

自车特殊槽也有自车类别/框监督。检测 Decoder 中间各层同样有监督;跨帧维护由 ClipMatcher、MemoryBank、QIM 配合。实现 track_loss.py:62、track_loss.py:352;stage1 差异 base_track_map.py:145。

7.3 Map:类别、包围框和区域 mask

监督来自 nuScenes 地图,栅格化成分隔线、斑马线、道路边界等实例,以及可行驶区域。GT 地图用于训练和评分,不是网络推理的地图输入。

主要配置:2×FocalLoss + 5×L1_bbox + 2×GIoU + 2×Dice_mask。Dice 衡量预测区域与真实区域的重合程度,适合线条和稀疏区域;GIoU 衡量地图实例 2D 包围框的位置与重叠。Thing/Stuff 分支有自己的 loss 汇聚及比例系数,不能把上述式子当作无条件重复一次的全部最终实现。

地图实例通过 Hungarian 配对;中间定位层、mask 层也有辅助监督。代码 base_e2e.py:246、panseg_head.py:281;类别标签 vector_map.py:7。

7.4 Motion:六种未来里选最佳模态进行训练

对每个匹配目标,拿 6 条预测与真实轨迹比较:

k ∗ = arg ⁡ min ⁡ k A D E k , k^*=\arg\min_k ADE_k, k∗=argkmin​ADEk​,
L m o t i o n , l a y e r = 0.5 ( − log ⁡ p k ∗ ) + 0.5 L N L L , k ∗ + 0.25 min ⁡ k F D E k . L_{motion,layer}=0.5(-\log p_{k^*})+0.5L_{NLL,k^*}+0.25\min_k FDE_k. Lmotion,layer​=0.5(−logpk∗​)+0.5LNLL,k∗​+0.25kmin​FDEk​.

  • ADE:沿整条轨迹各时刻位置误差的平均值。
  • FDE:最后一个有效未来时刻的位置误差。
  • NLL(负对数似然):让预测分布给真实未来更高概率,并学习其不确定性;具体以源码 traj_nll 的参数化为准。
  • 源码先算 minFDE,随后用 minADE 重写选中的 inds,所以最佳模态的分类/NLL 选择依据最终是 minADE。
  • loss_weight_minade=0,表示 minADE 不额外单独加进 loss;它仍参与模态选择。minFDE 权重为 0.25。
  • 3 轮 Motion 输出都有上述监督;GT 从跟踪匹配索引取得。默认启用 nonlinear smoother,会结合预测起点与 GT,对非自车的训练目标轨迹做可行性平滑;自车走单独处理。

训练时用 GT 找到最佳模态是为了学习;推理没有 GT,因此返回全部 6 模态及其预测概率,不能用“找最靠近真值的轨迹”作为实际预测方法。traj_loss.py:16、motion_head.py:383。

7.5 Occ:当前与未来实例占用 mask

GT 由未来真实车辆框投影/栅格化成占用及实例 ID。当前配置 only_vehicle=True,不应把结果理解成对所有行人、任意静态障碍和道路的完整场景占用预测。实例 Query 用跟踪匹配关联真实实例,不是另起一套无关的 mask 配对。

主要为 5×Binary cross entropy + 1×Dice,附加同类辅助 mask loss,辅助权重 1。BCE 对每格做“是否属于该实例”的二分类;只取损失最大的 25% 像素,未来时间乘 0.95^t 折扣;无效帧/255 标签按 mask 处理。监督覆盖当前帧 + 4 个未来帧。

虽然 dataset 为规划准备了 6 个未来时刻的 GT,占用网络 OccHead.n_future 默认仍为 4,不能由 occ_n_future_max=6 推断网络输出 7 帧。代码 base_e2e.py:334、occflow_loss.py:15、occ_head.py:286、occflow_label.py:11。

7.6 Planning:真实轨迹 + 碰撞项

  • PlanningLoss:有效时间内,预测点到 GT 点的欧氏距离平均值。这里叫 L2,但不是把所有平方误差直接求平均的 MSE。
  • 配置 3 项 CollisionLoss:delta=0/0.5/1.0,对应权重 2.5/1.0/0.25,扩张自车框后惩罚它与未来 GT 框的重叠。
  • 具体实现以框角点的轴对齐范围求重叠近似,不能当成精确的旋转矩形碰撞几何。
  • 训练碰撞项用的是 GT future boxes,测试避碰优化用的是 预测 occupancy,二者不是同一个计算。

代码 base_e2e.py:444、planning_loss.py:16、planning_loss.py:30、planning_head.py:241。本次是静态解释配置及源码计算,没有运行 autograd 或训练,因此没有验证每个碰撞实现细节的梯度有效性。

7.7 “端到端”不意味着所有参数每阶段都更新

stage1:队列长 5,训练 6 epochs,先学 Track 与 Map;图像 backbone 冻结,FPN 未冻结,BEV Encoder 参与训练。stage2:载入 uniad_base_track_map.pth,队列长 3,训练 20 epochs,联合训练五个任务头,backbone、FPN、BN、BEV 编码路径默认冻结。

此外,历史 BEV 生成使用 no_grad;规划头对 sdc_track_query.detach();检测/轨迹参考位置迭代也有 detach。detach 表示这条支路保留数值,但切断梯度。因此应理解为“一个统一网络、有任务间特征依赖与联合训练”,而非“所有 loss 都无阻断地回传到全部网络参数”。实现 uniad_track.py:337、planning_head.py:139、modules.py:20。

8.1 一帧的实际推理顺序

  1. 按场景检查并复用/清空历史状态;6 张图通过 backbone/FPN。
  2. 相机特征 + 自车状态 + 历史 BEV,经 BEV Encoder 得到共享特征。
  3. 检测/跟踪 Query 读取 BEV,6 层逐步更新类别与框;目标置信度、历史 ID、速度更新共同维护 tracks。
  4. 地图头读取同一 BEV,预测局部地图;取 Map Query 供 Motion 使用。
  5. Motion 为每个目标/自车构造 6 种意图,做 3 轮交互,每轮更新参考轨迹。回归头输出各时刻位移,cumsum 累加成相对于当前时刻的位置轨迹,分类头给各模态 log 概率。
  6. Occ 将非自车车辆的多模态特征汇聚,演化 5 个时间块,通过实例向量与稠密特征点积生成 [B,A,5,200,200] logits。
  7. Occ 的 sigmoid 概率乘 track score,再对实例取 max,按阈值 0.1 合并为二值占用;实例 ID 用于时序一致性评测。这里的阈值与 attention mask 阈值 0.3 各有不同用途。
  8. Plan 拼接自车轨迹特征、detach 后的自车跟踪特征和指令向量,经 MLP、六模态 max,得到 1 个规划 Query;对 BEV 做 3 层 Decoder。
  9. MLP 一次输出 6×2 位移,cumsum 得到 6 个自车未来位置。不是 Transformer 自回归地一个点一个点生成。
  10. 默认测试开启非线性避碰优化,用预测占用将原始规划轨迹向附近空闲位置调整。Occ 只到 2s,而 Plan 到 3s:源码 cur_t=min(t+1,occ_horizon-1),后两个未来规划时刻复用最后一张占用预测,并没有额外产生 2.5/3s 占用。
  11. 保存跟踪/BEV 状态供下一帧使用,返回最终轨迹及各任务结果。

主入口 uniad_e2e.py:235;目标解码 track_head.py:164;Motion motion_head.py:212;Occ occ_head.py:408;Plan 与优化 planning_head.py:139、planning_head.py:203。

9.1 不是一个 loss 或一个总分

训练 loss 用于更新参数;evaluation metric 用于在验证集上衡量结果。两者可能都用距离,但统计范围、匹配、过滤和聚合不同。整个标准评测先顺序推理验证集,再分别统计任务指标。

任务主要指标白话理解入口
检测mAP、NDS 及位置/尺寸/朝向/速度等误差找到多少目标、框和状态是否准确nuscenes_e2e_dataset.py:981
跟踪AMOTA、AMOTP、Recall 等 nuScenes tracking 指标跨帧身份和轨迹是否连续,漏检/误检/ID 错误如何nuscenes_e2e_dataset.py:981
地图drivable/lanes/divider/crossing/contour IoU预测区域和 GT 区域的交并比nuscenes_e2e_dataset.py:981
运动预测minADE、minFDE、MR;另有 motion mAP、EPA六种猜测里最佳轨迹的误差;检测与预测联合质量nuscenes_eval_motion.py:640、metric_utils.py:64
占用IoU、时序一致 panoptic 指标,常称 VPQ;代码汇总为 pq/sq/rq占用是否准确,是否把同一辆车跨时间识别一致test.py:40、metrics.py:73
规划L2、obj_col、obj_box_col,各 0.5~3s位置误差,以及点/车辆轮廓是否撞到 GT 占用planning_metrics.py:15

9.2 运动预测指标怎样算

对第 k 条候选:ADE_k = mean_t ||p(k,t)-GT(t)||₂,FDE_k 为最后有效时刻距离。

  • minADE=min_k ADE_k;minFDE=min_k FDE_k,二者最优模态可能不同。
  • 本地 miss_rate 是先对每个模式求所有有效时刻的最大距离,再对模式取最小;若仍超过 2m 则记 miss。不能把这里所有 MR 都泛化成“末点超过 2m”。
  • 评测先考虑检测与 GT 匹配,不能只拿 GT 目标框启动轨迹头,然后把指标当完整检测+预测结果。
  • EPA=(N_det_traj_tp-0.5×N_det_fp)/N_gt;此实现用 2m 检测中心距离与 2m 轨迹 FDE 门槛联合匹配,专门惩罚漏检/误检与预测错误。nuscenes_eval_motion.py:640

9.3 占用指标怎样算

custom_multi_gpu_test 对30m×30m 和 100m×100m 两种区域分别统计:前者取 70:130 的中心 60×60 格,后者取完整 200×200。IoU 看车辆前景区域交并比;panoptic 指标进一步考虑实例 ID 和跨时间一致性。存在无效未来帧的样本会按当前代码过滤,并报告参与评测的比例。test.py:40

9.4 规划:必须注明单时刻还是累计平均

标准配置 planning_evaluation_strategy="uniad":

  • 3s L2 = 3s 这一时刻的预测位置误差,在样本间平均。
  • "stp3":3s L2 = 从 0.5s 到 3s 所有时刻误差的平均。
  • 统计点为 0.5/1/1.5/2/2.5/3s;论文常展示 1/2/3s。比较论文时必须使用一致口径。

PlanningMetric 的碰撞检查使用 GT 未来车辆占用;预测占用用于规划优化,不能拿预测占用自己判断自己是否安全。obj_col 检查预测轨迹点;obj_box_col 检查固定车辆 footprint。代码对 GT 轨迹本身已碰撞的时间点屏蔽碰撞分子,最后仍按样本总数归一化;该指标不是实车发生事故概率。

这属于开环评测:给定录制数据预测未来,与记录的未来比较。闭环是让车辆执行预测动作、环境继续变化,再统计驾驶表现。这里的 nuScenes 开环低 L2/低碰撞率不能直接证明可以上车可靠驾驶。

实现 planning_metrics.py:15;两种聚合的实际切换 nuscenes_e2e_dataset.py:981。官方也提示过规划评测定义差异,参考 官方仓库。

9.5 如何运行本地标准评测

先准备好环境、nuScenes 验证集与 stage2 checkpoint。示例使用 1 张 GPU,仅演示入口,不代表实验环境已跑通:

cd ./UniAD
./tools/uniad_dist_eval.sh \
  projects/configs/stage2_e2e/base_e2e.py \
  /实际路径/uniad_stage2_checkpoint.pth \
  1

脚本通过 torchrun → tools/test.py → custom_multi_gpu_test → dataset.evaluate 执行。uniad_dist_eval.sh:7、test.py:40、nuscenes_e2e_dataset.py:981。不同 GPU 数的序列划分可能影响时序结果,复现时须注明;标准 dataset 配置 eval_mod 列出 det/map/track/motion,Occ 与 Planning 在自定义 test loop 中另外累计,没有列在 eval_mod 不代表没有评测。

本地 mini/stage2_e2e_mini.py 继承标准 stage2,只改数据加载 worker、训练轮数、warmup 和日志/评测周期;它没有改 BEV 大小、Query 数和 Transformer 层数。mini 的小数据结果不能当 full nuScenes val 复现成绩。

10. 建议读代码的顺序

  1. base_e2e.py:10:先认清所有固定数值与开关。
  2. uniad_e2e.py:88、uniad_e2e.py:235:看五个任务如何接线,尤其 GT 与预测各走哪里。
  3. uniad_track.py:117、track_head.py:130、panseg_head.py:121:看三种初始学习表,明确 Query 从哪来。
  4. transformer.py:101、encoder.py:91、spatial_cross_attention.py:31:看 BEV 如何读图像和历史。
  5. motion_head.py:212、modules.py:20:看六模态意图与三轮交互。
  6. occ_head.py:271、occ_head.py:198:看实例 Query 融合与稠密时空状态演化。
  7. planning_head.py:139、planning_head.py:203:看单轨迹回归和测试避碰。
  8. track_loss.py:62、traj_loss.py:16、occflow_loss.py:15、planning_loss.py:16:分别看 loss。
  9. test.py:40、nuscenes_e2e_dataset.py:981、planning_metrics.py:15:最后看评测口径,避免拿训练日志代替验证集结论。

11. 证据边界与参考

  • 本次核对本地 commit 609ee083ea51c3521c323f1279dfc4cee0e60467 的配置、模型调用链、Query 构造、损失和评测实现。
  • 没有执行模型推理、训练、梯度检查或 nuScenes 评测;本文不报告实验环境性能或复现成功。
  • 本地已有的未跟踪 mini 配置和准备脚本保持原样;源码未改。
  • UniAD 论文:任务层级与统一 Query 接口的原始设计。
  • OpenDriveLab/UniAD 官方仓库:版本说明、模型与评测说明;具体参数以本文所链接的本地源码为准。

12. 时序信息:究竟记住了什么

存在时序,而且同时记住“场景历史”和“目标历史”。 这里“时序”指多帧按时间先后传递信息,而不是把每张相机图片独立预测。它与“预测未来多个点”不同:未来预测本身不证明输入使用了历史帧。

12.1 四个时间相关位置

位置传递的信息怎样用代码
BEV 时序融合历史整张场景特征 prev_bev结合自车位移/旋转对齐,通过 TemporalSelfAttention 融合transformer.py:101、encoder.py:200
目标跟踪 Query前帧目标 embedding、ID、位置与速度QIM 更新有效 Query;速度和时间间隔预测当前参考位置uniad_track.py:358、modules.py:115
MemoryBank每个目标至多 4 个历史特征当前目标 Query 注意历史特征,补充运动/外观信息modules.py:7
Occ 时间块当前状态依次演化成未来状态5 个时间块依次预测当前和未来 4 帧;属于未来状态演化OccHead.forward,见第 8 节

MemoryBank 的 4 指保存的 embedding 槽数量,不能直接说是“最近连续 4 帧”:推理写入还有分数和 save_period=3 的间隔条件。训练与推理的更新条件也不同。

MapHead 和 PlanningHead 并没有额外独立的跨实际帧隐藏状态;它们可以通过共享 BEV、跟踪与 Motion Query 间接使用过去的信息。MotionFormer 的 3 轮是同一次前向中的特征细化,不是 t-2、t-1、t 三个历史帧;Occ 的未来时间块也不是读取未来图像。

12.2 输入的历史有多长

标准 stage1:queue_length=5,数据样本为 [t-4,t-3,t-2,t-1,t],约覆盖 2s。

标准 stage2:queue_length=3,数据样本为 [t-2,t-1,t],约覆盖 1s。

这里按 nuScenes 相邻标注帧约 0.5s 解释,时长是 (L-1)×0.5s,不是 L×0.5s。当前代码明确 enbale_temporal_aug=False,取同一 scene 内连续索引;不满足队列起止同场景的样本返回 None,由数据集重选。不能照其他版本说这里默认随机跳帧。实现 nuscenes_e2e_dataset.py:169。

union2one() 将图像堆成 [L,6,3,H,W],并把各帧 can_bus 的平移和航向改为相邻帧差值,便于对齐;第一项差值归零。各帧检测框、ID 与辅助轨迹标注保留为列表,Motion 的未来轨迹等取最后一帧的标注。nuscenes_e2e_dataset.py:263

12.3 训练与推理怎样保存历史

训练:有限队列,队列间重新初始化 tracks。 forward_track_train() 每个样本开头 _generate_empty_tracks(),然后依序处理 L 帧,在这个样本内部传递目标状态;不是把上一个随机 batch 的 tracks 接到下一个 batch。

历史 BEV 的实现比简单画一个 RNN 箭头更具体:对队列第 i 帧,prev_img=img[:,:i],由 get_history_bev() 对前缀图像重算历史 BEV;此过程用 eval() + torch.no_grad(),没有对历史 BEV 链做完整反传。源码也保留了 “Generate prev_bev in an RNN way” 的 TODO,说明训练这里并非直接复用上一轮主分支输出 BEV 的高效实现。i=0 时实际传入首帧自身作为 prev_img,因此不能逐字把首帧实现描述成完全未调用历史构造。uniad_track.py:493、uniad_track.py:316

推理:按场景顺序处理,每次输入当前六张图。 保存 self.prev_bev、self.test_track_instances、上帧时间戳与坐标变换;下一帧直接读取。遇到新 scene_token 时清空历史,不能把一个场景的状态带进另一个场景。uniad_track.py:707

推理不是每次重新送入一个固定三帧 batch。虽然显式保存一张 prev_bev,它已融合过更早信息,所以历史影响可以递归传递;这不意味着无限时长无损记忆。

13.1 两个训练阶段,外加前置预训练权重

这里的 epoch 表示大致遍历一轮训练数据;iteration 表示处理一个 batch 并进行一次优化更新。

项目Stage1:先学感知Stage2:联合任务头
初始化权重 load_fromckpts/bevformer_r101_dcn_24ep.pthckpts/uniad_base_track_map.pth
主要任务Track + MapTrack + Map + Motion + Occ + Planning
图像队列长度53
图像 backbone 参数冻结冻结
FPN训练冻结
当前帧 BEV 编码路径训练no_grad,冻结
跟踪/地图头训练继续训练
Motion/Occ/Planning 头未配置训练
Track 辅助 past trajectory loss权重 0默认权重 1
epoch620
默认 checkpoint 保存每 epoch每 epoch
默认评测间隔6 epochs20 epochs

配置 base_track_map.py:49、base_e2e.py:41;初始化 base_track_map.py:587、base_e2e.py:702。

前面的 BEVFormer checkpoint 是已有预训练权重;标准两阶段 UniAD 训练并非所有部分从随机数开始。Stage2 也不是把 Stage1 的所有模块锁住只训练新头:Track/Map 会继续训练。

load_from 主要载入模型参数;新阶段按该阶段配置建立 optimizer 与训练进度。resume_from 则用于中断续训,恢复 runner/optimizer 等状态。当前训练 API 的优先级是:存在的 resume_from 优先,否则使用 load_from。两者不是同一种“加载”。mmdet_train.py:190

13.2 一个 Stage2 batch 的完整顺序

以单 GPU batch size 1、队列 [t-2,t-1,t] 为例:

  1. DataLoader 取训练样本。 读取连续三帧,每帧 6 路相机;执行颜色扰动、归一化、padding,准备框、ID、局部地图、未来轨迹、未来占用及规划 GT。未来图像不作为预测输入。高层指令由离线轨迹标签派生,这是 benchmark 输入条件。
  2. 初始化本队列跟踪状态。 建立 900 个初始目标槽与 1 个自车槽。
  3. 处理第 1 帧。 构造其 BEV,做 6 层目标解码;与该帧 GT 匹配,计算 Track loss;维护 MemoryBank 和 QIM,得到下一帧目标状态。
  4. 处理第 2 帧。 无梯度重算前缀历史 BEV,再编码本帧;带着第 1 帧目标 Query 做检测/跟踪,计算本帧 Track loss,并更新状态。
  5. 处理第 3 帧。 使用前两帧历史与跟踪状态,完成当前帧 Track,得到最后一帧 BEV、目标/自车特征;累计第 3 帧 Track loss。
  6. 只对最后一帧做 Map。 由其 BEV 预测地图,与当前地图 GT 计算 Map loss。
  7. 只对最后一帧做 Motion。 读取目标、自车、地图 Query,预测从该帧开始的 6s 六模态轨迹,用匹配后的未来轨迹 GT 监督。
  8. 只对最后一帧做 Occ。 从该帧开始预测当前 + 未来 4 帧车辆实例占用,与 mask/ID GT 比较。
  9. 只对最后一帧做 Planning。 融合指令和自车特征,预测 3s 轨迹,计算轨迹距离和配置碰撞项。训练不启用测试用的占用避碰优化。
  10. 合并 loss。 汇总三帧 Track loss 与最后一帧另外四个任务 loss,包括各自辅助层损失。不是每处理一帧就 optimizer.step()。
  11. 反向传播并更新一次。 runner/optimizer hook 清梯度,调用反向传播,分布式训练时同步梯度,裁剪梯度,用 AdamW 更新参与训练的参数。
  12. 继续下个 batch。 按调度器调整学习率;按 epoch 间隔保存 checkpoint、运行验证。

队列迭代 uniad_track.py:493;单帧计算和匹配 uniad_track.py:358;最后一帧接四个头 uniad_e2e.py:88;runner/optimizer hook 注册 mmdet_train.py:90。

因此,一个 Stage2 iteration 的监督形状可概括为:

L b a t c h = ∑ i = 0 L − 1 L t r a c k ( i ) + L m a p ( L − 1 ) + L m o t i o n ( L − 1 ) + L o c c ( L − 1 ) + L p l a n n i n g ( L − 1 ) . L_{batch}=\sum_{i=0}^{L-1}L_{track}^{(i)}+L_{map}^{(L-1)}+L_{motion}^{(L-1)}+L_{occ}^{(L-1)}+L_{planning}^{(L-1)}. Lbatch​=i=0∑L−1​Ltrack(i)​+Lmap(L−1)​+Lmotion(L−1)​+Locc(L−1)​+Lplanning(L−1)​.

这表示计算位置;具体实现还含各任务既有的权重、内部归一化和辅助项。Stage1 则是队列每帧 Track 加最后一帧 Map。

未完成训练

已有 BEVFormer 预训练权重

Stage1:Track + Map,5 帧队列,6 epochs

Stage1 感知 checkpoint

Stage2:冻结视觉与 BEV 编码,联合五个任务头,20 epochs

读取一个三帧训练队列;准备各任务 GT

初始化本队列 Track 状态

t-2:BEV + Track + Track loss

t-1:历史 BEV + Track + Track loss

t:历史 BEV + Track + Track loss

最后一帧 Map + Map loss

最后一帧 Motion + Motion loss

最后一帧 Occ + Occ loss

最后一帧 Planning + Planning loss

合并队列 Track loss 和最后一帧其余 loss

一次反向传播;梯度同步与裁剪

AdamW 更新可训练参数

下一 batch;按周期保存和验证

图中是一套权重贯穿各帧,不是给每帧独立训练一个模型。

13.3 哪些历史信息不反传梯度

  • 历史前缀 BEV:get_history_bev() 中 no_grad,只提供已算出的历史特征。
  • Stage2 当前 BEV 编码也在 no_grad 中,视觉表示固定,任务头仍能读取并学习。
  • Track Query、MemoryBank、QIM 负责队列内信息传递;不能因历史 BEV 无梯度就说“整个模型没有跨帧学习”。
  • 一些几何参考点和轨迹迭代使用 detach;规划头显式 detach 自车 track query。也不能反过来说“所有时序分支都做了完整 BPTT”。BPTT 指沿整个时间链反向传播,这里不是完全无截断的形式。

13.4 优化超参数与代码入口

标准两阶段都配置 AdamW:基础学习率 2e-4,weight decay 0.01;梯度最大范数 35;前 500 iterations 线性 warmup,从 1/3 倍基础学习率起步;之后 cosine annealing,最低比例 1e-3。warmup 是起初逐步提高学习率,避免突然的大更新;weight decay 用来约束参数幅度;梯度裁剪限制一次更新前的梯度规模。base_e2e.py:672

数据每 GPU 的样本数为 1,但每个样本内部含 L 帧×6 相机。使用 G 张 GPU 时,默认每次全局处理约 G 个这样的样本,而不是把时间维或相机维当 batch。--autoscale-lr 是可选参数;不能假定换 GPU 数时学习率必然自动改变。

启动链:tools/uniad_dist_train.sh → tools/train.py → custom_train_model → custom_train_detector → EpochBasedRunner/OptimizerHook。模型和数据构造 train.py:209;optimizer 与 hook mmdet_train.py:90。

当前说明来自本地标准配置与源码,没有实际运行训练、检查梯度或验证收敛。mini 配置会覆盖 epoch、warmup 和评测周期,不能套用本节标准轮数。

14.1 二分图匹配到底配什么

“二分图”就是左右两组对象及它们之间可能的连线:左边是网络的 N 个预测,右边是 M 个 GT(真实标注)。匹配要选出互不冲突的连线,使总代价最小。每个真实目标最多配一个预测,每个预测最多配一个真实目标;通常 N 远大于 M。

例如有三个 Query 产生预测 P0、P1、P2,真实标注是汽车 G0、行人 G1。下表仅是演示代价,越小越像:

预测汽车 G0行人 G1
P019
P128
P271

最小总代价配法为 P0→G0、P2→G1,总代价 2;P1 未配对,按背景/无目标监督。即使 P1 也很像汽车,也不能和 P0 同时占用同一个 GT。这样训练会鼓励多个 Query 分工,而不是全部重复预测同一辆车。

当前 Track matcher 的真正步骤:

  1. 从分类 logits 和 GT 类别计算分类匹配代价。
  2. 从编码框的前 8 维计算 L1 匹配代价;这里不把速度两维用于配对成本。
  3. 按配置,代价约写成 C=2.0*C_focal+0.25*C_bbox_L1;权重已在 cost 对象内部应用,源码直接相加,不应再重复乘一遍。
  4. cost.detach().cpu(),把成本矩阵从计算图取出。
  5. scipy.optimize.linear_sum_assignment(cost) 返回所选行、列,即预测下标与 GT 下标。
  6. 按这些对应关系计算分类/框等训练 loss。配对动作不反传,但配好对后的预测 loss 仍反传。

实现 hungarian_assigner_3d_track.py:40。注意类注释提到 IoU、示例二维框等内容是沿用旧注释,实际这段 Track assigner 只有分类和前 8 维 L1 cost,应以函数体为准。“Hungarian matching”是通用的最优一对一指派称呼;这里调用的是 SciPy 的线性指派接口,不是在仓库手写 Hungarian 求解循环。

UniAD 跟踪还加了一步:有已知目标 ID 的历史 tracks,先按真实 ID 继承匹配;只把剩余空槽与尚未被跟踪的 GT 送入上述矩阵匹配。自车槽单独处理。实现 track_loss.py:352。

这是训练监督时配预测与 GT。推理没有 GT,不进行这套 GT 匹配;推理通过跨帧 Query 和状态管理维持 ID。

14.2 QA、QM 是什么,为什么 Query 很多

论文通常写 $Q_A$、$Q_M$:Q 表示 Query 特征;A 表示 Agent(交通参与者),M 表示 Map(地图)。这里的 QA 不是 Question Answering。它们通常代表一组向量组成的矩阵,不是一个数字,也不是类别标签。

  • $Q_A∈R^{A×256}$:A 个有效交通参与者,每个一个 256 维信息向量;batch 维省略。
  • $Q_M∈R^{M×256}$:M 个地图 Query,每个也是 256 维;当前定位解码器初始有 300 个槽。
  • 自车 Ego/SDC Query:单独一张表示自车的信息卡片。SDC 是 Self-Driving Car。
  • 不同模块的 Query 服务不同问题;数量多,不代表场景里有那么多真目标。
Query数量为什么这样设怎样生成怎样变成有意义的信息
BEV40000,每个地面格一张卡Embedding(200*200,256)读取图像/历史,装入该格的场景信息
初始检测900 个候选槽,用来覆盖未知数量的目标学习表前 900 行经 6 层解码、训练匹配后逐渐学会表达目标或背景
Track / QA只保留有效目标,数量动态变化上一帧有效目标 Query 更新,再在当前帧解码筛选每个向量携带目标外观、类别、位置与运动相关信息
Ego1 个,始终专门表示自车同一学习表第 901 行,下标 900自车框/类别与轨迹监督帮助其学成自车表示
Map / QM300 个候选地图实例槽独立 Embedding(300,512)经地图编码/定位解码与地图 GT 监督,得到地图实例特征
Map Stuff1 个可行驶区域槽独立学习表用区域 mask 监督,表达连续区域

“学习表”不是人工写入 900 个物体的坐标,而是一块初始化后可训练的参数。训练不断更新它,部署时加载 checkpoint 中学好的值;推理每帧不会重新随机生成这些 Query。初始学习表每行 512 维,拆为 256 维的位置相关部分与 256 维内容部分;解码后的目标特征为 256 维。

关键流程:初始参数向量 → 读取 BEV → 解码后的目标特征 → 筛选有效目标 → QA。因此“900 个初始检测槽”和“最终 QA 有多少目标”不是同一个数量。900/300 是此模型的容量超参数,不是 Hungarian 算法规定的数量;也不是每行永久绑定一个具体类别,比如第 1 行只能检测汽车。

代码:检测/自车学习表 uniad_track.py:117;有效 QA uniad_track.py:476;地图学习表 panseg_head.py:152;地图输出特征 panseg_head.py:182。

14.3 “终点附近采样”到底是什么意思

这里指 MotionFormer 的 agent-goal 交互从 BEV 特征中取信息,不是在真实地面随机选落脚点,也不是拿真实未来终点作为输入。

例子:某个目标的“向右转”模态当前预测,6s 后会到 (10,5)m。网络要判断那附近有没有道路、路口或有用的场景线索,于是从 BEV 中 (10,5) 附近读取少量特征,帮助细化这条模态。

源码中:

  1. sample_index=-1 选参考轨迹的最后一个点。
  2. 第一轮参考来自轨迹锚点;后续轮参考来自上一轮预测并 detach 后的轨迹。
  3. 把相对目标的终点转换到当前场景坐标,再按 BEV 范围归一化。
  4. Query 经过线性层产生学习的采样偏移 sampling_offsets 和采样权重。
  5. 采样位置约为 归一化终点 + offset/[BEV宽,BEV高],插值读取特征,再加权融合。

例子中的附近位置可以想象为 (9.5,5)、(10,5.5) 等,但这只是示意:偏移由网络预测,不保证严格限制在终点某个固定半径内。当前配置 8 个注意力 head、每 head 每未来 step 4 个点、1 个 BEV level。由于代码为 12 个 step 分别预测 offsets/weights,不能把整个模块简化成“总共只取 4 个点”;这些组围绕所选终点参考展开。

实现 motion_deformable_attn.py:431、motion_deformable_attn.py:417;逐轮更新轨迹 modules.py:20。

该目标某一轨迹模态 Query

预测采样偏移与权重

轨迹锚点或上一轮预测轨迹

取最后一点;转到 BEV 坐标

终点参考 + 学习的偏移

BEV 场景特征

按采样位置插值读取并加权

更新模态特征,细化下一轮轨迹

14.4 “推理时牛顿法避障”应怎样理解

前文称“牛顿法避障”过于简化,准确说法是:推理时用 CasADi 建模、调用 IPOPT 做非线性轨迹优化。 IPOPT 官方定义其为内点法求解器;它内部的数值步骤与 Newton 类方法有关,但此源码不是一个手写的朴素牛顿更新循环。IPOPT 官方说明

网络先给 6 个轨迹点 p̂_t。假如某点靠近占用预测中的一辆车,就把这些点作为待调整变量,最小化:

J ( p ) = ∑ t ∥ p t − p ^ t ∥ 2 2 + α ∑ t , o ∈ O t c exp ⁡ ( − ∥ p t − o ∥ 2 2 2 σ 2 ) . J(p)=\sum_t\|p_t-\hat p_t\|_2^2+\alpha\sum_{t,o\in O_t}c\exp\left(-\frac{\|p_t-o\|_2^2}{2\sigma^2}\right). J(p)=t∑​∥pt​−p^​t​∥22​+αt,o∈Ot​∑​cexp(−2σ2∥pt​−o∥22​​).

第一项让轨迹不要随意远离网络结果;第二项让轨迹远离预测车辆占用格子的中心。靠近占用点时指数惩罚大,远离时惩罚小。用网络轨迹作为初始解,调用求解器得到修改后的点。

例如原始点在 (10,0),附近占用在 (10,0.3),求解器可能把轨迹稍向另一侧调整;这是说明优化意图的例子,不是本次执行出的数值结果。

当前实现只挑原轨迹点附近 5m 内的占用点;若一个也没有则直接返回原轨迹。它是软惩罚后处理,不是证明绝不碰撞的约束规划器。此碰撞优化器主要优化 XY 点,不能把训练用 smoother 的完整运动学约束套给它。默认只在测试时开启,不对这一求解过程做常规网络反传。代码 collision_optimization.py:16、planning_head.py:203。

14.5 “平滑真值轨迹”为什么要修改正确答案

GT 记录的是真实车辆从真实起点、真实朝向出发的未来运动。但网络感知的当前位置/朝向有误差;如果硬要求从偏了的起点立即接上原始未来点,就可能出现瞬间横移、急转或不合理加速度。

所以训练 MotionHead 时,会在适用样本上求一条从预测状态出发、尽量接近真实未来、又满足运动约束的参考轨迹,用它替代原始轨迹作为监督。这更准确叫“适配预测起始状态的非线性平滑”,不只是把锯齿做均值滤波。

  • 状态变量包括位置、朝向、速度;控制变量包括曲率和加速度。
  • 约束描述车如何随速度、朝向前进,并限制曲率、加速度、速度、转向角速度、横向加速度。
  • 目标包含贴近原 GT 的位置/朝向、保留终点,同时减少急转、加减速突变等。
  • jerk 指加速度变化率;直觉就是“加速/刹车是否突然”。曲率表示拐弯有多急。
  • 当前训练默认对非自车目标做此处理,自车 GT 单独保留。

并非每条 GT 都平滑:代码要求有效未来点足够、目标位移超过 2m、预测与 GT 初始位置相差不超过 2m、朝向相差不超过 30°。若优化结果与原参考平均偏离达到 1.5m,也退回原始参考的相对位移表示。

两种优化不要混淆:

项目训练真值平滑推理避碰优化
修改谁非自车运动预测的监督参考网络输出的自车规划轨迹
什么时候训练时推理时
依据什么预测初始状态 + GT 未来轨迹 + 运动约束网络轨迹 + 预测占用
目的让监督与感知误差、可行运动相容调整规划轨迹,减少接近占用
部署是否需要未来 GT不执行此训练步骤不需要未来 GT

代码入口 motion_head.py:445、motion_utils.py:13;优化约束和目标 motion_optimization.py:17。这些是源码行为说明,未在本次运行求解器。

14.6 导航指令是什么,从哪里来

导航指令是在路口等歧义场景告诉模型“希望右转/左转/直行”的高层意图。看见一张十字路口照片,并不能唯一决定目的地方向;感知负责看路,指令负责选大方向,规划头决定具体轨迹。

它不是方向盘角度、不是目标坐标,也不是自然语言文本。当前实现是整数:

command意义
0右转 RIGHT
1左转 LEFT
2直行 FORWARD

本地 nuScenes 离线数据集里,训练与评测输入的 command 由 get_sdc_planning_label() 根据记录的真实未来自动生成:把未来自车位置转换到当前局部坐标,取最后一个有效点的横向 x;x≥2m 记右转,x≤-2m 记左转,中间记直行;没有有效点也默认直行。这是坐标约定下的简单标签规则,不是实时路线规划算法。trajectory_api.py:226

模型内部用 nn.Embedding(3,256) 把整数变为一个 256 维指令向量,再与 Ego Track/Trajectory Query 融合生成 Planning Query。planning_head.py:139

离线评测的指令来源带有未来记录信息,这是一项已提供的输入条件。 实际部署不能读取未发生的未来,应由外部导航/路线规划系统根据目的地与路线提供类似指令。这个外部目的地导航器不是当前 PlanningHead 自动完成的部分。

14.7 一段话串起来

模型先用初始可学习 Query 从 BEV 中提取目标和地图。训练时通过最优一对一匹配,知道每张目标卡片应向哪个 GT 学习;推理时保留有效目标形成 QA,并用跨帧状态延续身份。QA、QM 和轨迹锚点帮助 MotionFormer预测多种未来,再到候选终点附近读 BEV 细化。自车 Query 与导航指令融合,生成自车轨迹;测试时根据预测占用再优化。训练中的 GT 平滑则解决另一件事:让运动监督适配有误差的感知起点。

15.1 匹配表的数字不是“汽车数”或“行人数”

前面的表是人为举例的匹配代价矩阵。每个格子的含义是:“把这一行预测,认作这一列真实目标,要付出多少不匹配代价?”

例如 P0/真实汽车这一格为 1,意思是 P0 的类别和框与这辆真实汽车比较接近;P0/真实行人为 9,意思是把 P0 当作那个行人很不合适。1、9 都不是米,不是概率,也不是物体数量。 它们是分类成本和编码框 L1 成本加权后的评分;表中 1/9/2/8/7/1 只是帮助理解的演示数值,不是这次从模型运行中读出的值。

列标题“真实汽车”表示数据集已经标注的某个具体目标,包含类别和真实框;不是泛指所有汽车的类别集合。如果有两辆真车,应分别有“汽车 A”和“汽车 B”两列。

代价由网络输出和 GT 自动算,不是人工给每辆车填写。真实代码成本也不必为正整数,不能把表格数值当成固定取值范围。越小表示按照这套成本更适合匹配。

15.2 六个相机究竟怎样共同形成一张 BEV

理解方向很关键:先问“BEV 这个空间位置应该看哪张图、看图哪里”,再取对应特征。不先把六张原始 RGB 图片拼成一张俯视照片。

以地面 (x=10m,y=2m) 的一个 BEV 格为例:

  1. 六张图片分别经过同一套 backbone/FPN,得到每路相机的多尺度特征。
  2. 沿这格上方的虚拟竖柱取 4 个高度点 (10,2,z_i)。因为地面上方可能有车身、行人或建筑,不应只取地面高度。
  3. 对每个相机,用已知内外参组成的 lidar2img 矩阵,把这些 3D 点投影到像素。内参描述镜头怎样成像,外参描述相机装在车的哪里、朝哪边。
  4. 投影在相机后方或画面外就跳过;例如这个格可能只被前相机和前右相机看到,其他四路不参与该格融合。
  5. 每个可见相机内部,BEV Query 产生学习偏移/权重,在投影点附近、各尺度图像特征上插值采样并加权汇总。
  6. 将相机各自得到的 256 维结果相加,再除以可见相机数量,随后线性变换、残差更新 BEV Query。如果两个相机都看到,粗略写作 f=(f_front+f_front_right)/2;相机内部的采样权重仍是学习的。
  7. 结合历史 BEV 的时间融合,在 6 层编码中逐步形成更好的共享 BEV 表征。

几何公式可以写为:[u',v',d',*]ᵀ = lidar2img · [x,y,z,1]ᵀ,再 u=u'/d'、v=v'/d'。这里 d’ 是投影计算中的深度,不是模型直接得到的真实物体深度。

参考点与标定把采样引向相关位置,任务训练让网络学会如何使用这些特征。遮挡并非投影矩阵自动解决:源码主要检查图像范围和正深度,多视角与学习特征帮助处理歧义。

投影代码 encoder.py,第 91 行;相机汇聚实际代码 spatial_cross_attention.py,第 171 行。

BEV 某一格:x,y

沿高度取 4 个参考点

投影到相机 1;检查是否在画面内

投影到相机 2;检查是否在画面内

其余相机同样处理

相机 1 多尺度特征

投影附近学习采样与加权

相机 2 多尺度特征

投影附近学习采样与加权

可见相机结果累加 / 可见相机数

该格的 256 维 BEV 特征

15.3 “1 个区域 Query”如何输出整张图

当前 Stuff 类只有 1 类:可行驶区域,所以有 1 个专门负责它的 Query。它不是 1 个格子、1 个矩形,也不限制只能有一块连通区域。

可以把这个 256 维向量理解成一个查询:“整张图里哪些位置属于可行驶区域?” 它读取地图特征后,与全图每个空间位置的特征建立关系,输出 200×200 个区域分数。最终阈值化得到可行驶区域 mask。

形状上:

1 个 Query       [1,256]
地图空间特征     [40000,256](示意,省略 batch)
Query 与各位置的关系
→ 40000 个区域分数
→ reshape 成    [1,200,200]

本代码中 mask 由自定义 attention 的各位置相关分数经过小型网络聚合得到,所以上面是结构示意,不是说源码只做一个未经变换的裸点积。学习表初始化仍为位置/内容合计 512 维,拆分后内容 Query 为 256 维。panseg_head.py,第 154 行、seg_mask_head.py,第 310 行

一张输出 mask 可以同时包含左边、前面等多块互不连接的可行驶区域。Query 数量、mask 像素数量和区域连通块数量,是三个不同概念。

15.4 Thing 4 层、Stuff 6 层里的“层”是什么

Thing 表示需要区分实例的地图元素:当前有分隔线、斑马线、道路边界等类型;Stuff 表示按类别整体分割的连续语义区域:当前为可行驶区域。这里说的是 MapFormer 的两个 mask 分支,不是在数检测到几辆车。

  • Thing 4 层:地图实例 Query 连续经过 4 个不同参数的 mask block,逐层读取地图特征、更新表示并生成 mask。
  • Stuff 6 层:区域 Query 连续经过 6 个不同参数的 mask block,逐层更新区域表示和 mask。
  • “4/6”不是 Query 个数、地图类别数、相机数或历史帧数,而是网络的计算深度。
  • 本地每个 block 主要有 cross-attention、前馈网络、残差与归一化;Stuff 分支还开启了 self-attention。
  • 两个分支在这些 block 后还各接一个末尾 AttentionTail,所以 4/6 特指配置的 num_decoder_layers,不是穷尽全部 attention 算子。
Thing:多个实例 Query → block1 → block2 → block3 → block4 → 实例 masks
Stuff:1 个区域 Query → block1 → block2 → … → block6 → 区域 mask

这是同一次前向里顺序经过的层,不是同一个模型训练 4/6 次。4 和 6 是作者选定的结构超参数;仅凭源码不能推断它们一定是最优值。配置 base_e2e.py,第 314 行;block 定义 seg_mask_head.py,第 214 行。

15.5 每个目标的 6 模态轨迹 Query 怎么做出来

“模态”在这里表示一种可能的未来走法,不是相机、雷达这种传感器模态。6 模态是同时给出 6 种未来猜测,不是 6 个未来时间点,也不是固定六类动作标签。

假设 QA 中一个向量表示前车。现在为同一前车建立 6 个不同的未来意图槽:它们共享同一辆车的观测信息,但有不同的运动先验,因此可能学成不同的候选轨迹。直行、减速、左右偏移等只是帮助理解的例子,不能把 6 个槽硬对应到固定动作名。

构造步骤:

  1. 读取离线轨迹锚点。 从训练轨迹聚类得到 4 个类别组各 6 条典型轨迹,每条 12 个未来点;运行时从 pkl 加载。
  2. 按目标类别选组。 汽车选机动车组的 6 条,行人选行人组的 6 条;不是运行时每辆车重新做 K-means。
  3. 读取 6 个不同的可学习模态向量。 学习表整体大小 4×6=24 行,每行 256 维。按类别组选择后,一辆车使用其中 6 行。
  4. 加入几何意图。 将轨迹锚点的终点在目标局部、场景等坐标表达下编码为 256 维向量;同一类别的锚点放到不同目标位置/朝向后,场景终点也不同。
  5. 融合构造 6 个初始模态特征。 代码把上述位置先验、学习向量和迭代特征经加法/拼接/MLP 融合。内部 query_embed 最初为零,再由意图特征填充,不是只把 QA 复制 6 份就完成。
  6. 与实际场景交互。 6 个模态分别读取 QA 中的其他目标、QM 地图特征与 BEV 终点附近特征;共享这辆车的 Track 信息,做 3 轮细化。
  7. 输出 6 条轨迹和 6 个分数。 每条有 12 个未来点及分布参数;训练用真实未来选择应鼓励的模式,推理返回所有候选和预测分数。

因此可简写为:

该目标 Track 信息
+ 六种典型轨迹的几何先验
+ 六个不同的学习模态向量
+ 当前道路与周围目标信息
→ [6,256] 的轨迹 Query
→ 6 条不同未来轨迹及概率

第一轮开始时,并不是仅凭随机向量就已知道前车会左转。不同先验使六个槽有区别,场景交互与训练监督让这些槽学出有用候选。base_motion_head.py,第 58 行、motion_head.py,第 252 行、modules.py,第 101 行

自车也参与这套六模态特征建模,但 PlanningHead 随后结合导航指令融合自车的六个模态特征,再回归一条最终自车轨迹。它并非一定直接挑选六条预测中的原封不动一条。planning_head.py,第 170 行

15.6 为什么代码链接打不开,还像要新建路径

此前代码定位采用 。某些代码聊天界面能解析这种写法,但 Obsidian 或其他 Markdown 客户端可能把它当作普通文件/笔记路径;:行号 并不是实际文件名的一部分,所以可能找不到文件,甚至弹出创建笔记/路径的界面。这不能直接归因于源码不存在,也不是 UniAD 正在重新生成代码。

本次把本文已有代码定位统一改成 GitHub 固定 commit 的 HTTPS 链接 + #L行号,适合从 Obsidian 浏览器打开;保留本地根目录文字 方便自行打开。没有复制或重新生成一份源码,也没有上传本地代码。

本地阅读可在 VS Code 已打开 UniAD 目录时,用 Ctrl+P 输入相对文件路径,再 Ctrl+G 输入行号。Linux 系统路径和 Obsidian 笔记链接应分开使用。

若“重新生成路径”指的是车辆规划轨迹,则每次收到新帧,模型确实会根据新观测重新输出未来 3s 的局部轨迹,测试避碰优化还可能调整它。它不是每帧自动重新计算整条目的地全局路线;高层路线和导航指令由外部系统负责。

16. 中间监督:哪些地方有答案,哪些没有

有中间监督,不是只用最后的自车轨迹训练。 监督指把某个预测与 GT(真实标注)比较得到 loss。应分两层:各任务的显式监督,以及同一解码器内部不同层的辅助监督。

16.1 各任务输出上的监督

位置预测什么对照的真实标注主要 loss
Track类别、3D 框、方向/速度、辅助轨迹每帧框、类别、跨帧 ID、辅助轨迹Focal、框 L1、轨迹 L1;ID 用于对应关系继承
Map Thing各地图实例类别、包围框、mask分隔线、斑马线、道路边界等 GTFocal、L1、GIoU、Dice
Map Stuff整张可行驶区域 mask每格是否属于可行驶区域Dice 等 mask 监督与区域分类
Motion每个目标六种未来及分数匹配目标的真实未来轨迹、有效时间 mask最佳模态分类、NLL、minFDE
Occ当前+未来各车辆实例占用真实未来车辆框栅格化的 mask、实例 ID二分类 BCE、Dice,含辅助 mask
Planning自车未来六个位置点真实自车未来轨迹、未来目标框轨迹距离、配置中的碰撞项

例如 Stuff 只有 1 个 Query,但它输出 200×200 个区域分数;可以用整张 GT mask 监督这一个 Query 产生的输出,并不是“一个 Query 只有一个标注数”。

QA/QM 本身是 256 维隐藏特征,通常没有人工给出“正确的 256 个数”。先把 Query 解码成框、类别、mask 或轨迹,再比较 GT;梯度让 Query 学成对任务有用的特征。

16.2 解码器中间层的辅助监督

  • Track 的 6 层 Decoder:每层都输出类别、框和辅助轨迹,按各层匹配计算 loss;当前训练在每个 dec level 调用 ClipMatcher。监督不只落在最后一层。
  • Map 的 6 层 Location Decoder:中间层有分类、框 L1/GIoU 辅助监督,最后层还连接 mask 分支。Thing 4 层、Stuff 6 层的 mask block 都有中间预测,计算对应辅助 mask loss;Thing 还带对应分类/框项,Stuff 有区域分类项。
  • Motion 的 3 轮:每轮输出轨迹和模态分数,都计算轨迹 loss,不只是第三轮。
  • Occ:用于构建注意力掩码的辅助 mask 预测和最终实例占用预测都计算 BCE/Dice;当前及四个未来时刻都参与有效标签范围内监督。这里不是每层都要人为提供注意力权重。
  • Planning 的 3 层:当前实现只拿最后输出回归轨迹、算规划 loss,没有给三层分别挂独立轨迹辅助 loss。

“辅助监督”表示中途也要求先给一个可检查的答案,帮助训练更早的层;不表示增加新的传感器或人工为每层画一份不同标签。同一套任务 GT 可以监督不同层的预测。

16.3 相机融合、BEV 和注意力有没有直接监督

当前这条标准路径没有单独的“正确 BEV 特征向量”、深度图 loss、相机融合权重 GT,也没有给每个采样偏移一个正确位置标签。相机标定是已知几何输入,不是让网络通过标定 loss 自己猜。

这些特征/注意力主要从任务 loss 获得间接监督:比如地图 mask 预测错了,地图 loss 的梯度会要求参与训练的前面特征提取步骤改变。但要区分阶段:stage1 当前 BEV 编码路径参与训练;stage2 默认冻结视觉和 BEV 编码路径,因此这些参数不会因为下游 loss 再更新。历史 BEV 生成本身也使用 no_grad。

图像与标定

BEV 特征;无独立特征 GT

Track:每层类别/框预测

Map:定位与 Thing/Stuff mask

每帧真实类别/框/ID → Track loss

真实地图 → Map loss

Motion:每轮六模态轨迹

真实未来轨迹 → Motion loss

Occ:辅助 mask + 最终占用

真实实例占用 → BCE/Dice

Planning:最终自车轨迹

真实自车轨迹/未来框 → Planning loss

代码(固定本地对应 commit):

本节为源码核对,未运行训练或梯度检查。

17. Dice:预测区域与真实区域重合多少

Dice(常译为戴斯系数)衡量两个区域的重合程度。这里名字来源于统计学家的姓氏,不是说训练中掷骰子。

设 P 是预测为前景的格子集合,G 是真实前景格子集合:

D i c e ( P , G ) = 2 ∣ P ∩ G ∣ ∣ P ∣ + ∣ G ∣ . Dice(P,G)=\frac{2|P\cap G|}{|P|+|G|}. Dice(P,G)=∣P∣+∣G∣2∣P∩G∣​.

情况Dice含义
两个非空区域完全一致1重合最好
两个非空区域完全没有交集0重合最差
只部分重合0 到 1 之间越大越接近

例如真实车辆占 10 格,预测占 8 格,其中 6 格重合:Dice=2×6/(10+8)=2/3≈0.667。这里的 10、8、6 是区域格子数量,与前面的匹配代价数字不同。

训练要最小化误差,所以常使用 DiceLoss=1-Dice,本例约为 0.333。Dice 分数越大越好,Dice loss 越小越好;不要把两者方向弄反。

17.1 训练时使用概率,避免先硬阈值化

训练中的预测 p_i 通常是每格 0~1 的前景概率,真实标签 g_i 是 0 或 1。经典 soft Dice 可写为:

D s o f t = 2 ∑ i p i g i + ϵ ∑ i p i + ∑ i g i + ϵ , L = 1 − D s o f t . D_{soft}=\frac{2\sum_i p_i g_i+\epsilon}{\sum_i p_i+\sum_i g_i+\epsilon},\qquad L=1-D_{soft}. Dsoft​=∑i​pi​+∑i​gi​+ϵ2∑i​pi​gi​+ϵ​,L=1−Dsoft​.

用连续概率可以反向传播;若先硬切成 0/1,会丢失用于优化的连续变化。epsilon 是很小的平滑常数(具体实现不一定很小),用于数值稳定和处理空区域;不同实现中放的位置也可能不同,所以应区分概念公式与源码。

17.2 为什么 UniAD 使用它

在 200×200 地图上,一条细分隔线或一辆远处车辆可能只占很少的格子。只看整体分类准确率,大量背景容易掩盖前景完全没学好的问题。Dice 直接比较目标前景的整体重合,而不是主要由正确背景数量主导。它仍会惩罚漏掉前景及预测过大的区域;并非只看交集、不管预测区域大小。

在 UniAD:Map 的道路/区域 mask、Occ 的实例占用 mask 使用 Dice。Occ 同时配合 BCE(二分类交叉熵):BCE 逐格判断对不对,Dice 衡量整体区域重合;两者互补,不是所有分割任务只用 Dice 就足够。

17.3 当前代码的两种写法

  • Map 自定义 DiceLoss 使用平方分母:1-2*sum(p*g)/(sum(p*p)+sum(g*g)+2*eps);默认此类 eps=1e-6。对二值非空区域、忽略 epsilon 时,和集合公式一致;软概率时数值与经典线性分母不同。
  • Occ 的 DiceLossWithMasks 在当前配置开启 naive_dice=True,使用线性分母和分子/分母加 epsilon,配置 eps=1.0;还过滤无效标签/帧。

代码:Map dice_loss.py、Occ occflow_loss.py、Occ 配置。

Dice 与 IoU 的区别:IoU=交集/并集;Dice=2×交集/(预测面积+真实面积)。二值情况下二者相关,但数值不一样,不应直接混用指标。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐