这篇文章尝试从代码实现的角度系统梳理这个四足极限跑酷项目的整体结构、数据流动、训练过程、奖励函数与域随机化设计。与其把它理解为一个“单纯用深度图控制四足机器人”的项目不如更准确地说它是一套先利用仿真特权信息训练强教师策略再逐步蒸馏到可部署视觉策略的两阶段框架。项目的主线可以压缩成一句话训练时完整的753维观测会被拆成多路分别经过scan_encoder / priv_encoder / history_encoder / depth_encoder / estimator等模块最后被压缩到一个114维动作决策输入部署时则只保留可感知、可估计的那部分信息由depth_actor输出最终的12维动作。1. 项目总览从 753 维训练观测到 114 维动作输入在训练阶段环境完整观测的拼接顺序是obs proprio(53) scan(132) priv_explicit(9) priv_latent_raw(29) history(10x53530) 753其中proprio(53)本体感觉包括机身角速度、姿态相关量、yaw 误差、速度命令、关节位置、关节速度、上一时刻动作、足端接触等。scan(132)机器人前方地形高度采样也就是所谓的地形扫描特权信息。priv_explicit(9)显式特权信息。当前代码中这 9 维里真正有信息的主要是前 3 维机身线速度后 6 维基本是占位。priv_latent_raw(29)隐藏参数真值包括质量参数、摩擦系数和电机强度等域随机化变量。history(10x53)最近 10 帧本体感觉历史。虽然训练时观测是 753 维但 actor 真正决策时并不直接使用全部原始维度而是将不同来源的信息编码成低维表示最终拼成114 proprio(53) terrain_latent(32) priv_explicit(9) latent(20)部署阶段更进一步不再输入 753 维完整观测而是直接构造这 114 维输入。2. 模块关系每个编码器到底在做什么整个项目最容易混淆的地方是不同 encoder 的职责边界。实际上它们分工非常明确。2.1scan_encoder把地形扫描压成低维地形特征scan_encoder接收obs_scan(132)输出scan_latent(32)。它的作用是把高维地形高度采样压缩成策略可用的低维地形表示。这个模块没有显式监督标签而是作为 actor 的一部分通过 PPO 端到端学习。换句话说网络会自动找到“怎样的 32 维地形表示最有利于产生高回报动作”。2.2priv_encoder把隐藏参数真值编码成适应 latentpriv_encoder接收priv_latent_raw(29)输出priv_latent(20)。这里的 29 维并不是地形而是仿真中的隐藏参数真值包括质量参数 4 维摩擦系数 1 维电机强度 12 维电机强度 12 维这个模块也不是监督学习而是把这些原始隐藏参数编码成策略可用的 20 维低维表示。它一方面通过 PPO 梯度更新另一方面还会受到priv_reg_loss的额外约束。2.3history_encoder用历史本体感觉恢复隐藏信息history_encoder接收最近10帧proprio(10x53)输出hist_latent(20)。它学的不是地形也不是动作而是试图从历史运动信息中恢复出与priv_latent对齐的低维隐藏参数表示。可以把它理解为“部署时用来代替隐藏真值的一条适应分支”。需要特别强调的是history_encoder的目标不是拟合priv_latent_raw(29)而是拟合经过priv_encoder压缩后的priv_latent(20)。2.4estimator从本体感觉预测显式特权信息estimator接收proprio(53)输出priv_explicit_est(9)。它是标准监督学习模块利用obs中环境直接提供的priv_explicit_gt(9)作为标签学习从本体感觉估计显式特权量。这里有一个实现细节非常重要虽然写成了 9 维但当前代码里真正有物理意义的主要是前 3 维线速度后 6 维基本是零占位。因此从语义上看这一路本质上更接近“线速度估计器”。2.5depth_encoder从深度图提取视觉地形特征与 yawdepth_encoder接收一张58x87的深度图和一个被抹去 yaw 相关量的proprio(53)输出depth_latent(32)视觉版地形特征pred_yaw(2)预测出来的 yaw 相关量它的作用是用深度视觉去替代原本依赖扫描得到的scan_latent同时补回被屏蔽掉的 yaw 信息。2.6actor与depth_actor同一个主干两种地形特征来源actor是教师策略depth_actor是学生策略。二者在结构上并没有本质区别depth_actor本身就是actor的拷贝。它们的唯一区别在于输入中的 32 维地形特征来自哪里actor使用scan_encoder(obs_scan)得到的scan_latentdepth_actor使用depth_encoder(depth)得到的depth_latent因此depth_actor并不是“多了一个视觉 head 的 actor”而是同一个 actor 主干只是地形特征来源从扫描换成了视觉。3. 数据流动训练与部署时信息是怎么传的3.1 第一阶段Teacher RL第一阶段的核心目标是在拥有特权信息的条件下先训练出一个强教师策略。数据流大致如下obs_scan(132)经过scan_encoder得到scan_latent(32)proprio(53)经过estimator得到priv_explicit_est(9)priv_latent_raw(29)经过priv_encoder得到priv_latent(20)最近 10 帧proprio经过history_encoder得到hist_latent(20)之后 actor 接收proprio(53) scan_latent(32) priv_explicit_est(9) latent(20)这里最后这个latent(20)并不是同时放入priv_latent和hist_latent两份而是同一个 20 维槽位在两者之间切换大多数时候是priv_latent做 adaptation / DAgger 更新时会切换成hist_latent这一点很重要。它说明 actor 不是同时吃两个 20 维适应向量而是在训练过程中逐步从“依赖真值隐变量”过渡到“依赖历史估计隐变量”。3.2 第二阶段Vision Distillation第二阶段的目标是用视觉分支取代扫描分支。具体流程如下输入一张58x87深度图再输入一个被 mask 掉 yaw 相关维度的proprio(53)depth_encoder输出depth_latent(32)和pred_yaw(2)再把pred_yaw(2)回填到被 mask 的 yaw 位置然后构造学生输入proprio(53) depth_latent(32) priv_explicit_gt(9) hist_latent(20)然后把它送进depth_actor输出学生动作。这里有两个非常关键的实现细节第一第二阶段训练时depth_actor用的是priv_explicit_gt而不是estimator的预测值。这意味着第二阶段存在一个轻微的 train-deploy mismatch训练时学生还在“偷看”显式特权真值而部署时必须改成 estimator 输出。第二代码里虽然保留了“让depth_latent直接拟合scan_latent”的 latent 对齐接口但在当前主流程里这一项实际上没有启用。真正生效的是depth_actor_loss || action_teacher_mean - action_student_mean ||yaw_loss || yaw_teacher - yaw_student ||因此第二阶段本质上做的不是显式 latent 回归而是用视觉特征替代扫描特征用行为蒸馏让学生模仿教师动作用 yaw 监督帮助视觉分支恢复方向相关信息3.3 第三阶段Deployment / JIT部署时真正运行的是depth_encoder提供depth_latentestimator提供priv_explicit_esthistory_encoder提供hist_latentdepth_actor输出最终 12 维动作所以部署态真正依赖的是proprio depth_latent priv_explicit_est hist_latent这一点非常能体现整个项目的设计思想训练时充分利用仿真里的特权信息部署时则尽可能只依赖真实可得的感知和历史状态。4. 梯度更新每个模块到底怎么学4.1 第一阶段强化学习与辅助监督共同进行第一阶段并不是单纯的 PPO而是“PPO 主目标 多个辅助学习目标”共同作用。estimator是最标准的监督学习模块。它用proprio(53)预测priv_explicit_gt(9)损失是预测值和真值之间的均方误差。history_encoder主要通过单独的update_dagger()更新其目标是让hist_latent(20)拟合priv_latent(20)。这一步从损失形式上看是监督学习但因为训练样本来自当前策略不断 rollout 出来的新状态所以代码里采用了DAgger这个名字。这里的DAgger指Dataset Aggregation中文常译为“数据聚合式模仿学习”。它和普通监督学习的区别不在于 loss而在于训练数据不是固定离线数据而是随着当前策略不断重新收集和聚合的。actor / critic / scan_encoder主要通过 PPO 更新。具体包括策略截断损失价值函数损失熵正则项priv_encoder稍微特殊。它不直接拟合某个真值标签而是把priv_latent_raw(29)编码成策略可用的priv_latent(20)。它一方面会受到 PPO 梯度更新另一方面还会受到priv_reg_loss的共同塑形。这里必须把priv_reg_loss说清楚因为这是整个项目里很容易被理解反的地方。priv_reg_loss发生在 PPO 的更新过程中本质上是一个 latent 对齐正则。代码实现时hist_latent被detach()成常量梯度只会回到priv_latent这一支也就是说它不是“把 history_encoder 拉向 priv_encoder”而是“固定 hist_latent把 priv_latent 往 hist_latent 的方向拉近”这样做的目的是缩小训练时 privileged 分支和未来部署时 history 分支之间的表示差距避免 actor 只适应“带真值隐藏参数”的输入分布而在切换到历史估计 latent 时性能骤降。4.2 第二阶段行为蒸馏 yaw 监督第二阶段开始时会先复制教师 actor初始化出depth_actor。之后教师 actor 在这一阶段只是作为固定老师使用它负责产生教师动作不参与反向传播不更新参数学生侧则由depth_actor depth_encoder组成。训练目标有两个depth_actor_loss || action_teacher_mean - action_student_mean ||yaw_loss || yaw_teacher - yaw_student ||这里的depth_actor_loss比较的是 teacher 和 student 的确定性动作输出而不是 PPO 中从高斯分布里 sample 出来的随机动作。teacher 和 student 看到的是同一时刻、同一环境状态但两者的地形信息来源不同teacher 用真实扫描特征scan_latentstudent 用视觉替代特征depth_latent这就是典型的行为蒸馏。最终第二阶段真正被联合更新的是depth_actordepth_encoder而不是教师 actor。5. 奖励函数这个项目到底在鼓励什么从代码实现上看奖励项虽然很多但完全可以归纳成四类。5.1 任务跟踪类奖励这一类奖励直接决定机器人是否在“完成跑酷任务”。最核心的两个项是tracking_goal_veltracking_yawtracking_goal_vel鼓励机器人沿着当前目标点方向移动。tracking_yaw鼓励机器人让自身朝向对准目标方向。这两项构成了最直接的任务驱动力既要朝目标走也要朝向目标。5.2 机体稳定类奖励这一类奖励负责限制身体姿态过于激烈、避免翻车。主要包括lin_vel_z惩罚 z 向速度过大抑制过激跳动ang_vel_xy惩罚滚转和俯仰角速度过大orientation约束身体姿态保持合理它们的作用是告诉机器人可以高速跑、可以跳跃但不能把身体搞成失控状态。5.3 能耗与动作平滑类奖励如果没有这类正则策略很容易学出“能跑但很暴力”的动作模式。主要包括torques惩罚扭矩过大delta_torques惩罚相邻时刻扭矩变化过快action_rate惩罚动作变化过快dof_acc惩罚关节加速度过大hip_pos、dof_error鼓励关节不要偏离默认姿态太远这一类奖励的本质是让机器人不仅要跑得快还要跑得像一个真正可控、可执行的机器人。5.4 跑酷安全与地形交互类奖励跑酷任务对接触安全要求更高因此代码里还加入了几项专门和地形交互有关的惩罚。主要包括collision惩罚身体不该碰撞的部位发生接触feet_stumble惩罚脚撞到近似垂直障碍feet_edge惩罚脚踩在地形边缘这些奖励的目标很明确机器人不仅要过去而且要“安全地过去”。如果把整个奖励系统压缩成一句话它鼓励的是朝目标方向稳定前进保持合理朝向和姿态用平滑、低代价的动作完成运动避免危险碰撞和错误落脚6. 域随机化项目里到底随机了什么本项目中的域随机化主要可以归纳成四类动力学参数随机化、外部扰动随机化、执行器随机化和视觉相关随机化。除此之外代码里还保留了一些可选的鲁棒性增强项但当前默认配置下没有全部启用。6.1 动力学参数随机化这是最核心的一类随机化主要包括摩擦系数随机化机体质量随机化质心偏移随机化摩擦系数随机化让不同环境实例落在不同摩擦条件下从而提升策略对接触条件变化的适应能力。机体质量随机化和质心偏移随机化则用于模拟真实机器人质量建模误差、安装件变化、电池与传感器负载差异等问题。这三项共同作用的结果是让策略不要过拟合某一个精确动力学模型。6.2 外部扰动随机化项目中启用了随机推搡机制。实现上并不是施加真实的外力脉冲而是每隔一段时间直接随机修改机器人 base 的水平速度。虽然这种实现方式比较简化但目的很明确让机器人在训练中反复遭遇扰动并学会从扰动中恢复。对跑酷任务来说这一点尤其重要因为真实世界中的落地误差、碰撞反弹和地形细节偏差都可以近似理解为某种外部扰动。6.3 执行器随机化项目还对电机强度做了随机化范围是[0.8, 1.2]。在控制实现里这会影响 PD 控制中的刚度与阻尼通道相当于让不同环境中的电机“强一点或弱一点”。这项设计非常合理因为现实中的执行器性能不可能永远和仿真标称值完全一致。通过训练时引入这一随机化可以显著降低策略对理想执行器模型的依赖。6.4 视觉相关随机化对于视觉学生策略来说传感器本身也不能过于理想。项目中对相机参数做了轻量级随机化主要包括相机俯仰角随机化水平视场角随机化可选的深度噪声这类随机化的意义在于现实相机的安装角度、视场和成像噪声都不可能与仿真完全一致。通过在训练中轻微扰动这些参数可以减少视觉策略对某一套理想相机配置的过拟合。6.5 尚未默认启用的可选随机化除了已经启用的项代码里还保留了初始状态随机化动作延迟随机化观测噪声地形测量噪声这些更像是“鲁棒性工具箱”。当前主配置没有全部打开但从工程角度看它们为进一步增强 sim-to-real 泛化提供了扩展空间。7. 这个项目最值得注意的实现细节最后总结几条最容易混淆、但也最值得读代码时记住的点。第一history_encoder学的不是地形而是隐藏参数的低维适应表示。第二depth_actor不是一个全新网络而是教师 actor 的结构拷贝区别只在于地形特征来源从扫描切换成了视觉。第三第二阶段并没有真正启用scan_latent - depth_latent的显式 latent 对齐损失真正生效的是动作蒸馏和 yaw 监督。第四训练与部署之间存在一个小的不一致第二阶段蒸馏时学生仍然使用priv_explicit_gt而部署时则必须使用priv_explicit_est。8. 总结从代码角度看这个项目的设计并不是“直接把深度图喂进 actor”这么简单而是一套很清晰的逐步替换流程第一阶段先借助仿真特权信息训练强教师策略同时训练 estimator 和 history adaptation 分支第二阶段再用视觉特征去替代扫描特征用行为蒸馏训练学生策略最终部署时只保留真实可得的信息proprio depth history这套框架最有价值的地方在于它非常务实地利用了仿真的优势第一阶段先利用地形特权信息、显式特权信息和隐式特权信息训练出强教师策略同时让历史编码器学习如何用历史本体信息去逼近隐式特权表示第二阶段再用深度图提取的视觉地形特征替代原来的地形扫描特征并结合本体信息、显式信息和历史信息训练学生策略最终逼近真实部署条件。对高动态四足跑酷这种任务来说这种“先在仿真里把问题学明白再向现实约束收缩”的思路往往比一开始就强行追求纯端到端部署输入更容易做出真正可用的系统。