状态:源码解读 + 契约定稿。 DrivoR 侧对
valeoai/DrivoR@main逐行核对; 设计依据来自 ICML 2024 覆盖路径规划工作的实验结论。
一眼版
| DrivoR | TD25A | |
|---|---|---|
| 维度 | 11(其中 3 维恒为 0) | 4 |
| 构成 | pose 3 + vel 2 + acc 2 + cmd 4 | v · ω · a · ω̇ |
| 角速度 | 没有 | 有(新增) |
| 归一化 | 无 | 待定 |
| 去向 | 加到 64 条 proposal query 上,加两次 | 同 |
| 参数量 | Linear(11→256) = 3,072 |
Linear(4→256) = 1,280 |
一、DrivoR 侧:11 维怎么拼出来的
# drivor_features.py::compute_features
ego_feature = torch.cat([pose, velocity, acceleration, driving_command], dim=-1)
按下标摊开(torch.cat 拼成一条一维向量,编号从 0 数):
下标 0 1 2 │ 3 4 │ 5 6 │ 7 8 9 10
内容 x y θ │ vx vy │ ax ay │ ←── one-hot 4 位 ──→
段 └─ ego_pose ─┘ └velocity┘ └accel─┘ └ driving_command ┘
| 段 | 维 | 内容 |
|---|---|---|
ego_pose |
3 | (x, y, heading),局部系(in_global_frame = False) |
ego_velocity |
2 | (vx, vy),vy 是侧向速度 |
ego_acceleration |
2 | (ax, ay) |
driving_command |
4 | one-hot 路线意图 |
vy 为什么存在——不是麦克纳姆轮
nuPlan/nuScenes 都是真实乘用车,没有横移能力。vy 来自侧滑:
车在转弯、轮胎打滑时,速度矢量和车头朝向不完全重合,中间有个侧滑角。
直线巡航时它接近 0。
说白了他们不承诺任何运动学模型,直接给平面速度矢量的两个分量,让网络自己学。
driving_command 是导航指令,不是避障信息
4 位 one-hot,表示路线希望我往哪走——左 / 直 / 右。
它解决的是一个纯粹的歧义:路口那一帧,左转和右转看到的画面一模一样, 没有这个信号网络无从知道该拐哪边。就像车机导航说「下个路口左转」。
官方文档强调:只由期望路线决定,不掺杂障碍物和交通信号;「左 / 右」涵盖转弯、变道和急弯。 它直接从数据集字段读,不是算出来的:
driving_command = scene_dict_list[frame_idx]["driving_command"]
第 4 位是什么官方没有明说,按惯例应该是「未知 / 无路线」。
历史帧加载了,但只用最后一帧
# scene_filter/navtrain.yaml
num_history_frames: 4 # frames are at 2Hz
四帧全部加载、拼成 (4, 11) 存进 feature。然后在模型里:
if self._config.full_history_status:
ego_status = features["ego_status"].flatten(-2) # (B, 44)
else:
ego_status = features["ego_status"][:, -1] # (B, 11) ← 默认走这条
前三帧算了、存了、然后扔掉。 这和相机 cameras[-1]、cam_*: [3] 一致:整个模型是单帧的。
二、DrivoR 侧的四个坑
2.1 那 3 维 pose 恒等于 (0, 0, 0)
NavSim 做相对化时,原点取的是最后一帧:
# navsim/common/dataclasses.py:178
local_ego_poses = convert_absolute_to_relative_se2_array(
StateSE2(*global_ego_poses[-1]), # ← 以末帧为原点
np.array(global_ego_poses, dtype=np.float64)
)
而模型又恰好只取末帧。末帧相对自己,永远是原点。
11 维里有 3 维是常数零。真正带信息的只有 8 维。
这 3 维本来是有用的——四帧连起来就是「我刚才是怎么开过来的」,一条短轨迹。 不是这个字段没用,是取法把它的信息取没了。
2.2 官方自己的另一个 agent 把这 3 维删了
# navsim/agents/ego_status_mlp_agent.py
ego_status_feature = torch.cat([velocity, acceleration, driving_command]) # 2 + 2 + 4 = 8
...
torch.nn.Linear(8, hidden_layer_dim) # ← 8,不是 11
同一个仓库,两个 agent,一个删了一个留着。 DrivoR 留着更像照抄 dataclass 时顺手 cat 进去的。
2.3 没有任何归一化
相机支路做了 ImageNet 标准化,激光栅格值域压到 [0,1]。到了 ego 这里——什么都没做。
原始 m/s 和 m/s² 直接进 Linear。速度量级 0–15、加速度 0–3、one-hot 是 0/1,
三种量纲混在一个线性层里各自缩放。能训出来是权重自己学到补偿,但这不是设计,是省略。
2.4 它不是「第 65 个上下文 token」,而且加了两次
ego_token = self.hist_encoding(ego_status)[:, None] # (B, 1, 256)
traj_tokens = ego_token + self.init_feature.weight[None] # (B, 64, 256) ← 广播相加
init_feature 是 nn.Embedding(64, 256),64 条可学习的 proposal query。
ego token 被加到每一条 query 上,不是拼进上下文。上下文自始至终只有 scene token。
而且 scorer 那边又来一遍:
tr_out = self.scorer_attention(embedded_traj, scene_features)
tr_out = tr_out + ego_token # 第二次
一次影响「生成什么轨迹」,一次影响「怎么给轨迹打分」。
三、TD25A 侧:为什么只剩 4 维
| DrivoR | TD25A | 理由 |
|---|---|---|
ego_pose (x,y,θ) |
删 | 图里隐式有,见第五节 |
vx |
v |
保留 |
vy(侧向速度) |
ω |
差速底盘结构上不能横移,vy 恒 0 |
ax |
a |
IMU 直接测,干净 |
ay |
ω̇ |
ay = v·ω,和已有的两维冗余 |
driving_command 4 位 |
闲置 | 见第七节 |
ω 是新增的——DrivoR 根本没有角速度
velocity 是 (vx, vy),acceleration 是 (ax, ay),从头到尾没有 yaw rate。
对汽车勉强能忍——它转向靠方向盘、而且不能原地转。
对能原地旋转的差速车这是硬缺失:原地转时 v = 0 但 ω ≠ 0,
没有 ω 的话状态向量完全看不出车在转。
而 53 次硬停全在折返 cusp——恰恰是原地转那个动作。
所以 vy → ω 不只是换个量纲合适的量,是把一个对本项目失效模式至关重要、
而 DrivoR 压根没有的量补进去。
ay 为什么换成 ω̇
差速车转弯时的横向加速度就是向心加速度:
ay = v · ω
而 v 和 ω 已经在向量里了。网络能算出来的东西,再喂一遍是浪费一维。
换成角加速度 ω̇,和 (v, ω) 保持导数关系,结构对称。
代价要认:IMU 天然给的是 a(加速度计)和 ω(陀螺仪),ω̇ 没有传感器直接给,
只能对 ω 差分,噪声会放大。而且策略跑 2–3 Hz、一次看 4 秒,
ω̇ 描述的是几十毫秒尺度的变化——真正需要它的是 15 Hz 那个跟踪器。
保留是权衡后的选择:它至少不是死值,每帧都在变。
四、两条被否掉的自研维度
设计稿里曾经想加,最后都砍了。
4.1 footprint 6 顶点——它是常量
一期机构不动、冻结为保守包络,那这 6 个顶点每一帧都是同一组数。
刚花一整节批评 DrivoR 那 3 维恒零的 pose,不能转头自己再造一个死输入。 等机构真的能动了再加回来。
4.2 定位置信度——改成训练时注噪
原想给网络一维「我现在信不信得过自己的位姿」。问题是: 这一维在训练数据里会变吗? 纯仿真训练的定位是完美的,那它永远是同一个数—— 和 DrivoR 的 pose 一个下场。
ICML 2024 的做法更扎实:不喂置信度,训练和评测时直接往位置和朝向上加高斯噪声。
| 噪声(位置 / 朝向 / 雷达) | T₉₀ | T₉₉ |
|---|---|---|
| 0.01 m / 0.05 rad / 0.05 m | 310 | 409 |
| 0.02 m / 0.1 rad / 0.1 m | 338 | 486 |
结论是高噪声下方法依然可用。与其加一维告诉网络「我不确定」,不如让它在带噪数据里学出鲁棒性。
还有一层:位姿飘的后果本身就是可观测的——静态图说这格是墙、occ_b 说这格是空,
那就是配准错了。这个矛盾已经在输入图里,不需要压成标量再喂一遍。
五、位置到底以什么形式进网络
这是整篇最容易误解的一点。
位置不是不给,是用图给、不用数字给。
所有 BEV 输入都是以车为中心、车头朝上裁出来的。 站在 A 点和 B 点,裁出来的图完全不同——「裁在哪儿」本身就唯一确定了你在哪儿。
ICML 2024 §3.3 明确讨论过这件事:
用自车坐标系的图来编码位姿,让智能体直接把观测对应到动作, 而不必去学一个从「单独的位置特征表示」到动作的额外映射—— 并点名对比了 Theile et al. 2020 用「2D one-hot 位置图」表示位置的做法。
不是没人想过,是比较过之后选择不给。 另外两条理由:
- 喂绝对坐标,网络会学成「到了 (12, 47) 就左转」,换一栋楼直接废
- 覆盖任务本来就是平移不变的
那「整层楼还剩哪片没扫」怎么办?靠多尺度——同一份地图裁两次, 远图 0.2 m/格覆盖 ±22.4 m,整层楼都在图上。详见 为什么不给机器人喂全局坐标。
六、盲基线:一个该抄的实验
EgoStatusMLPAgent 在 NavSim 里的定位是盲基线——只看自车运动学,
完全不接任何传感器。官方文档把它当参照系:仅靠外推运动学能拿到多少分。
完整模型 相机 + 激光 + ego → 分数 A
盲基线 只有 ego(8 维) → 分数 B
A − B 才是感知真正的贡献
在自动驾驶基准上这个差值往往小得让人尴尬——很多场景里「保持当前速度和航向」 本来就接近最优解。任何一个宣称「端到端感知有用」的工作,都应该先把 B 报出来。
覆盖清扫该照做一个:只吃 ego 状态、看不到地图和覆盖图的策略。 如果它分数跟完整模型差不多,说明 BEV 那 8 个通道白给了。
七、待定
ω从哪个话题来——还没核实机上实际发布的是什么- 归一化——DrivoR 一点没做,我们要不要做、怎么做
- 清扫意图——闲置中。原设想是「沿带进度 s、横向偏差 d、目标带方向」替换
driving_command,但三个量里只有「目标带方向」是真对应物: 它在 BEV 图里看不出来,必须上游告诉;另外两个是空间量,图里本来就有, 压成标量是有损的重复
参考
navsim/agents/drivoR/drivor_features.py::compute_features·drivor_model.pynavsim/common/dataclasses.py::EgoStatus(L138 定义 · L178 相对化)navsim/agents/ego_status_mlp_agent.py::EgoStatusFeatureBuildernavsim/planning/script/config/common/train_test_split/scene_filter/navtrain.yaml·docs/agents.md- Jonnarth, Zhao & Felsberg. Learning Coverage Paths in Unknown Environments with Deep RL. ICML 2024(§3.3 egocentric maps · §4.4 噪声鲁棒性)
- 相关:输入契约 v3 · 为什么不给全局坐标 · DrivoR 怎么处理相机 · 雷达 z 轴怎么编码
