深度图端到端穿越门竞速

对应作业任务(4)端到端模型(输入图像、输出控制指令)。 对比实验②:同一 RL 算法、不同网络结构。

三段式"感知→规划→控制"的延迟放不下高速连续穿门。本模块把 48×64 的深度图 直接喂给网络,输出速度指令,中间没有手工特征、没有地图、没有路径搜索。 训练分两段:先用专家示范做 DAgger(策略自己飞,专家在它飞到的状态上纠错), 再用 PPO 在 MuJoCo 真动力学里继续优化。网络、反向传播、PPO、DAgger 全部纯 numpy 手写,不依赖 torch / stable-baselines3。


1. 模块定位

课程内已有的 air_learning 就是端到端视觉行为克隆,本模块在五个维度上与 它不同:

维度 air_learning 本模块
输入模态 RGB 224×224 深度图 48×64 + 机体系速度
数据来源 人工遥操作一次性录制 DAgger 在线聚合(策略跑、专家纠错)
训练范式 纯监督 MSE 行为克隆 + PPO 微调
任务 通用导航 竞速:连续穿越 8 道门,间距 5 m,开口 ±1.1 m
框架 torch 纯 numpy(含手写卷积反向)

与同为强化学习导航的 uav_ppo_nav 相比:

uav_ppo_nav 本模块
观测 22 维人工特征(16 线雷达直方图 + 目标相对位置 + 世界系速度) 原始深度图 + 机体系速度
训练环境 质点 + 一阶滞后模型 MuJoCo 刚体动力学(转子饱和、混控、姿态环)
算法 PPO DAgger + PPO
对比实验 README 注明"见后续 PR" 本模块即对比实验②

2. 架构

深度图 48×64 ─┐
              ├─→ 主干(mlp/cnn/gru/attn) ─→ 128 维 ─┬─→ 策略头 μ ─→ tanh ─→ 动作 a
机体系速度 3  ─┘                                    └─→ 价值头 V
                                                            │
                              a(航向系速度)─→ 参考轨迹 ─→ 几何控制器 ─→ 四桨推力

四种主干共用同一套输入输出契约,只换中间结构(对比实验②):

arch 主干 参数量 说明
mlp 深度图拉平 → 全连接 410756 无空间先验的基线
cnn 3 层 stride-2 卷积 → 展平 126684 有平移等变性
gru CNN → GRU(128) 208860 带记忆,可抑制单帧噪声
attn CNN → 单头自注意力(32 维) 130780 全局感受野(24 个 token)

卷积主干展平而非全局平均池化:竞速需要门在画面中的位置,池化会抹掉空间 信息(实测池化版横向输出塌到 ±0.05 m/s)。

策略头输出高斯均值,标准差是状态无关的可学习参数;动作经 tanh 压到 [−1,1],再按环境速度上限缩放。对数概率含 tanh 的雅可比修正 ;缺这一项时动作长期贴边界。


3. 环境

项 值
场景 gate_track:8 道门沿 +x 排开,间距 5 m,开口 2.2×2.2 m
门心高度 0.8 ~ 3.0 m(相邻门高差最大 2.2 m)
策略频率 20 Hz;控制器 100 Hz
观测 深度图 48×64(u8,量程 0.2~12 m)+ 机体系速度
动作 航向系速度,水平 ±4 m/s、垂直 ±2 m/s
奖励 前进速度 + 10×干净过门 − 0.01×能耗 − 0.05×动作变化 − 2×倾角

擦框不发奖励、也不额外扣分:passed 只数干净穿过开口的门,擦框时它不增加, 等于相对少拿 10 分。另设一项负奖励会重复扣。

3.1 动作语义为什么是"航向系"而不是"机体系"

底座的 VelocityCommander 用实时全姿态把机体系速度旋到世界系。这在 平缓轨迹上没问题,但竞速里机身为了前进本来就要俯仰 20~35°,映射会跟着转:

  • 机身一倾斜,R @ a 就把前向指令的一部分旋到竖直轴上
  • 参考点被拽向侧面,姿态环为了追上又倾得更多
  • 正反馈,几个门下来直接翻转(实测倾斜到 170°、掉到 −1.5 km)

环境因此改用参考航向 R_z(yaw) 做映射,把姿态从回路里摘出去。ROS 节点 里发布前先做 v_body = Rᵀ · R_z(yaw) · a,让底座的全姿态映射正好复原出 R_z(yaw)·a。

3.2 门通过判定

门是位于 x = 5(i+1) 的竖直矩形框。质心穿过该平面且落在开口内算通过, 否则记一次擦框。判定是几何的,不依赖感知——避免"感知对了但判定错了"污染奖励。

一次只结算一道门,无论成功还是擦框都推进门序:擦了框却不推进,下一步会因 "已越过平面"再记一次碰撞,一次失误记两笔。

3.3 奖励权重的量级

项 一轮的量级(270 帧 @3 m/s)
前进奖励 × 系数 1.0 +810
8 道门的奖励(系数 10) +80
倾角惩罚(系数 2.0,巡航 20°) −108

系数初值下,"直着飞不穿门"能拿 700 分,"穿齐 8 道门"才 780 分——PPO 没有 穿门的动机。改成前进 0.2、过门 50、倾角 0.5 之后:直飞 +100,全过门 +500。


4. 专家

赛道是一条沿 +x 排开的门序列,"走哪条路"没有搜索空间,所以专家不用 A—— 真正需要决策的是怎么在门之间保持平飞并压住速度*。用航向系比例控制:

瞄准点取门中心再往前 2.5 m(look_ahead)。这个值不是随便定的:相邻门 高差最大 2.2 m、间距 5 m,只有 1.7 s 去补偿。瞄准点 1.5 m 时无人机进门前才 开始补偿高度,实测平均擦框 1.25 次/轮;2.5 m 时 0 次。

专家只吃真值位姿(不吃深度图):深度图留给策略学,专家给出"该怎么动"。 两者共用同一套动作语义,DAgger 的对齐前提。

实测(10 轮,起点抖动 0.6 m):8.00/8 门全过、0 擦框、13.49 s、完成率 100%。


5. 训练

5.1 DAgger

纯行为克隆的问题在闭环里会放大:策略一旦偏离专家轨迹,就进入了训练集里没见过的 状态,误差随步数累积。DAgger 的做法是在策略自己的状态分布上持续向专家要标签:

π₀ ← 专家示范
for i in 1..N:
    用 πᵢ₋₁ 跑,并在每一步同时问专家该怎么做
    把新数据并进数据集
    πᵢ ← 在聚合数据上重训

聚合数据按"轮次越新权重越高"采样(decay):早期专家轨迹的样本量会淹没 后几轮的纠错样本。

采集策略轨迹不加探索噪声。 初版直接按策略分布采样,log_std_init=-0.5 对应 σ=0.6(归一化尺度),折算成物理量是 2.4 m/s 的随机抖动——无人机出门 就被甩飞,采到的全是失控状态,第 1 轮后闭环掉到 0/8(第 0 轮 1.7/8)。 DAgger 用策略自己的状态分布,探索留给后面的 PPO。

策略漂出专家包络时截断本轮采集。 失控状态下的专家标签残差很大,而行为 克隆用的是 MSE(平方加权),几千条这种样本会把专家数据反向压过去:实测 不截断时三轮 DAgger 把闭环从 1.7/8 拖到 0/8,连专家分布上的验证 MSE 都从 0.00058 涨到 0.00835。截断条件取专家自己从不越过的边界(倾角 < 50°、 高度 > 0.4 m、|y| < 4 m)。

5.1.1 每轮的闭环评测

只看开环 MSE 会误判:训练/验证集都是专家数据时 MSE 一路降,策略可能已退化。 run_dagger 每轮跑一次闭环评测,把通过门数记进训练曲线。

5.2 损失

行为克隆用 MSE,不用 Huber:

Huber 在残差大于 后退化成 L1,梯度是常数,回归目标变成条件中位数。 本任务里横向指令在 ±3 m/s 之间来回摆动(相邻门 y 差最大 1.4 m),中位数接近 0 ——实测 Huber(δ=0.1) 把策略的横向输出压在 [−0.08, +0.08] m/s,专家是 [−2.8, +3.0],闭环 3.7/8;换 MSE 后输出范围 [−0.34, +0.55],闭环 5.0/8。

5.2.1 水平镜像增广

深度图左右翻转 + 取反是严格成立的样本:相机与机体左右对称、重力沿 、专家律 在 下正好变号。增广把数据 翻倍,并固定"画面左 ↔ "的符号约定;不增广时左右两种情形各自从数据里学, 横向输出被压向中间。

验证集先切再增广:镜像件与原件是同一帧的两种视角,混进验证集会泄漏。

消融(同一份 2176 条专家数据、40 轮、同一批评测种子):

mirror 验证 MSE 开环 RMS(, ) 横向输出范围 闭环
关 0.00475 0.235 / 0.121 [−2.00, 2.68] 4.2/8,擦框 3.8
开 0.00404 0.218 / 0.129 [−2.04, 2.07] 5.0/8,擦框 3.0

5.3 PPO

裁剪式代理目标 + GAE。两处符号约定:

  • (高斯得分函数,符号为正)。 写成负号会让更新把高回报动作的概率往下压——单测里奖励偏向 +1 的动作, 10 轮 PPO 后策略输出收敛到 −1。
  • 熵 ,最大化熵是让 变大。

6. 对比实验②:同一 PPO、不同网络结构

四种主干在完全相同的环境、超参、随机种子下各训一遍,比较:

指标 含义
参数量 网络规模
BC 完成率 / 通过门数 纯行为克隆阶段(未加 PPO)
PPO 完成率 / 通过门数 加 PPO 之后
推理延迟 单帧前向耗时(20 Hz 要求 < 50 ms)

7. 实测结果

平台:Windows 11 + Intel Core 5 220H(核显,无独显)。策略推理 0.7 ms/帧 (20 Hz 预算 50 ms,余量 70 倍)。ROS2 侧在 Ubuntu 20.04 + Humble 虚拟机中 colcon build 通过。

7.1 专家

10 轮、起点抖动 0.6 m:8.00/8 门全过、0 擦框、13.49 s、完成率 100%。 专家是策略的上界参照。

7.2 DAgger 没有跑赢"只做一轮行为克隆"

每轮都做闭环评测(通过门数 / 8):

第 0 轮后 第 1 轮后 第 2 轮后 第 3 轮后 专家分布上的验证 MSE
不截断 1.7 0.0 0.0 0.3 0.00058 → 0.00620 → 0.00835
加漂移截断 1.7 0.3 0.0 0.7 0.00073 → 0.00167 → 0.00110
加漂移截断(复跑) 1.0 1.0 0.3 1.0 0.00093 → 0.00104 → 0.00097

两次独立的截断跑都停在 1 门上下,说明这不是噪声——DAgger 在这里确实没有 正收益。

两个观察:

  • 不截断时,加进去的策略样本会把专家拟合一起拖坏(验证 MSE 涨 14 倍)。 策略跑飞后的状态残差很大,MSE 又是平方加权,几千条这种样本压过了专家数据。 加漂移截断后验证 MSE 稳住了(0.0007~0.0017),新增样本数也从 2000 掉到 1486 → 673 → 189——后几轮策略一出门就出包络,几乎采不到新数据。
  • 闭环始终没有超过第 0 轮。瓶颈在第 0 轮的行为克隆本身(抖动 1.0 m 时 1.7/8),不在 DAgger。

7.3 最终策略

起点条件对成绩影响很大,两种都报:

条件 通过门数 用时 擦框 推理延迟
无起点抖动(seed 0) 7/8 13.6 s 1 0.69 ms
起点抖动 0.6 m,10 轮平均 3.1/8 — 0.90 0.69 ms
专家(上界) 8.0/8 13.5 s 0 —

无抖动那一轮,策略用时与专家只差 0.1 s——速度不是瓶颈,鲁棒性才是。 下图就是这一轮:横向压在 ±1 m 内,高度跟着门走,只在第 2 道门擦了一下框。

起点一偏到 0.6 m,平均就掉到 3.1/8。精度预算:门半宽 1.1 m、间距 5 m、 巡航 3 m/s,每个门只有 1.67 s 修正时间。横向速度误差 RMS 0.22(归一化)= 0.88 m/s,在一个门距内累积 1.47 m,超过门半宽。稳定过关需把横向 RMS 压到 0.6 m/s 以下,是现在精度的 1.5 倍。

数据量是当前唯一的瓶颈。 训练数据从 8 轮示范(2176 帧)加到 30 轮 (约 8100 帧),抖动 0.6 m 下的平均通过门数从 1.8 涨到 3.1,擦框下降,趋势 未到平台。训练集 MSE 0.0006,比验证 MSE 低一个数量级,瓶颈在泛化: 相邻帧只差 0.05 s,一条轨迹里独立场景只有两位数,不足以训练 48×64 卷积网络。

PPO 在这点预算下帮不上忙:6 × 1024 步约 6100 帧,训练回报一路从 251.8 掉到 156.9。要让它起作用需要几十倍的在策略采样量。

7.4 对比实验②:同一 PPO、不同网络结构

2176 条专家演示只采一次,四个结构在同一份数据、同一套超参(BC 40 轮 MSE + 镜像、PPO 6 × 1024 步)、同一个种子下各训一遍,评测 5 轮、起点抖动 1.0 m:

结构 参数量 推理延迟 BC 通过门数 BC 擦框 PPO 通过门数 PPO 擦框
MLP(拉平) 410756 0.55 ms 2.00 1.00 0.00 0.20
CNN 126684 0.66 ms 1.80 1.20 0.40 0.80
CNN+GRU 208860 0.87 ms 0.00 0.00 0.00 0.00
CNN+注意力 130780 0.87 ms 1.60 1.40 0.00 0.00

能读出来的:

  • 参数量与成绩不成比例。 MLP 用 3.2 倍于 CNN 的参数拿到相近(甚至略好) 的行为克隆成绩,但推理更快——48×64 的深度图拉平后只有 3075 维,卷积在这里 没有体现出优势。
  • CNN+GRU 完全没学出来(0 门)。行为克隆按帧打乱训练,每帧的隐状态都是 零;闭环里隐状态却是一路累积下来的。训练与推理喂给 GRU 的隐状态分布不一致, 且回合越长差得越远。要修就得按定长片段切数据、片段内隐状态连续(VIO 前端 就是这么做的),本模块没做。
  • PPO 在这点预算下没有正收益。 6 × 1024 步(约 6100 帧)对 PPO 来说太少, 四个结构里三个被它拖低。要让它起作用需要几十倍的在策略采样量。
  • 四种结构的推理延迟都在 1 ms 以内,20 Hz 的 50 ms 预算完全用不满。


8. 源码解析

uav_depth_gate_racing_e2e/core/
├── nn.py         手写 Conv2d / Linear / GRUCell / SelfAttention / LayerNorm / Adam
├── gate_env.py   竞速环境(深度图观测、航向系动作、门通过判定)
├── expert.py     比例控制专家
├── policy.py     四种主干 + 高斯策略头 + 价值头
├── bc.py         行为克隆(MSE / Huber)
├── dagger.py     轮次加权聚合的 DAgger
└── ppo.py        GAE + 裁剪代理目标 + tanh 雅可比

8.1 nn.py:im2col 的维度顺序

as_strided 的维度顺序与 reshape 的目标一致。顺序不一致时 reshape 按内存序 重新分组;此时前向与反向仍互为转置、数值梯度校验通过,只有伴随恒等式 能发现(单测含这一项)。

8.2 混控饱和会吃掉姿态控制

mixer 的力矩预算是 :期望总推力 顶到 时预算归零,四个桨都满转、没有任何差动余量,姿态控制彻底消失。

几何控制器的位置增益 只需要 0.6 m 的位置误差就能把期望推力算到 上限——实测竞速场景里 峰值 2269 N,是上限 24 N 的 95 倍,全程 54.5% 的控制步力矩预算不足 5%,姿态不可控。

修法是给控制器加输出限幅(GeometricController.thrust_alloc = 0.7), 等比缩小期望推力矢量而不是逐个 clip。加上之后专家从 0/8 变成 8/8。

8.3 观测里的深度图非空

GateRacingEnv 早期沿用底座的 headless 开关(底座里是"不渲染视觉传感器", 对键盘遥控/规划适用)。训练全用 --headless 时策略吃到全零图,退化成 "照平均速度平飞"——横向输出 ±0.05 m/s,而训练损失还在下降。现在环境永远 渲染,自检加了"深度图非空"一项(形状断言拦不住)。

8.4 ppo.py:熵项的符号与高斯得分函数

两处符号写反时训练照常进行、更新方向相反:

  • (高斯对数密度的得分函数, 符号为正)。写成负号的意思是"降低高回报动作的概率"——单测里奖励偏向 的动作,10 轮 PPO 后策略输出收敛到 。
  • 熵 ,最大化熵是让 变大。

这两处不靠肉眼看,靠两个单测:test_ppo_pushes_toward_high_reward_action (回报只与动作有关的赌博机,PPO 必须把动作推向高回报侧)和 test_entropy_raises_log_std(两份逐位相同的策略、同一批数据,只差 entropy_coef, 的差必须为正)。

8.5 整网梯度的验证方式

扰动 会让几千个 ReLU 里的某几个跨过折点,每次翻转对数值梯度的 贡献与真实梯度同量级:实测 、逐元素比值落在 到 之间,真梯度和噪声混在一起,怎么调容差都验不出问题。

所以逐元素的数值梯度只在层一级做(Conv2d / GRU / 自注意力三个测试, 外加 im2col 的伴随恒等式);整网用下降量验——沿解析梯度走一步,损失 下降量应与 同量级(实测只有 0.25 倍,折点噪声吃掉七成, 所以带宽放到 0.05~5 倍)。这一步抓得住符号写反与量级写错。

8.6 出图路径的动作尺度

策略输出是 tanh 的 [−1,1],要乘上速度上限才是物理指令。plotting._rollout 早期漏了这一步,把 [−1,1] 直接喂给环境——无人机以四分之一速度飞,出图看着 "通过 4/8、超时收场",像是策略不行,实际是出图路径的错。同一份权重在 评测里是 7/8、13.6 s。评测与出图走的是两套调用,只测一套不够。

8.7 resize_area 的缩放方式

深度图先按米做面积平均再量化,不是先量化再缩放:量化是 1/254 的定点,先量化 会把远处的细节直接抹平。无效像素不参与平均,按有效像素数加权——否则空洞会把 周围的有效深度拉浅。放大时退回最近邻:分箱宽度不足一个源像素,箱内计数会 取到 0,除零出 NaN。


9. 已知限制

  • 策略频率 20 Hz、控制器 100 Hz。深度图 48×64 是训练速度与空间分辨率的折中。
  • 专家只吃真值位姿,上界为"干净通过全部门"。
  • 单精度不参与训练:全部用 float64 numpy,CPU 推理约 0.7 ms/帧,余量很大。
  • 奖励里没有惩罚"绕路",前进速度本身就给正奖励,所以策略可能学会"贴着门框 蹭过去"——擦框拿不到 +10 是唯一的约束。要做严格竞速应该按时间给奖励。