XYT·星野图
首页项目归档照片墙音乐灵境说说杂谈友链关于
封面
返回上一级

机器人姿态输入对比:DrivoR 的 11 维与 TD25A 的 4 维

写作时间:2026-08-26 15:30
# 端到端
# 自动驾驶
# DrivoR
# 输入契约
# TD25A

状态:源码解读 + 契约定稿。 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.py
  • navsim/common/dataclasses.py::EgoStatus(L138 定义 · L178 相对化)
  • navsim/agents/ego_status_mlp_agent.py::EgoStatusFeatureBuilder
  • navsim/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 轴怎么编码
avatar

XYT

以文字为星,以思考为野,绘一幅属于自己的星野图。

RECOMMENDED

DrivoR 怎么处理相机:4 张图到 64 个 token

2026-08-25 18:10

想法博客

2026-08-27 23:50

覆盖染色图:输入契约、逐层解析与在 DrivoR 里的接法

2026-08-25 00:40

Table of Contents