EXTREME-PARKOUR项目学习记录

这篇文章尝试从代码实现的角度,系统梳理这个四足极限跑酷项目的整体结构、数据流动、训练过程、奖励函数与域随机化设计。与其把它理解为一个"单纯用深度图控制四足机器人"的项目,不如更准确地说:它是一套先利用仿真特权信息训练强教师策略,再逐步蒸馏到可部署视觉策略的两阶段框架。

项目的主线可以压缩成一句话:训练时,完整的 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(10x53=530) = 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.1 scan_encoder:把地形扫描压成低维地形特征

scan_encoder 接收 obs_scan(132),输出 scan_latent(32)

它的作用是把高维地形高度采样压缩成策略可用的低维地形表示。这个模块没有显式监督标签,而是作为 actor 的一部分,通过 PPO 端到端学习。换句话说,网络会自动找到"怎样的 32 维地形表示最有利于产生高回报动作"。

2.2 priv_encoder:把隐藏参数真值编码成适应 latent

priv_encoder 接收 priv_latent_raw(29),输出 priv_latent(20)

这里的 29 维并不是地形,而是仿真中的隐藏参数真值,包括:

  • 质量参数 4 维
  • 摩擦系数 1 维
  • 电机强度 12 维
  • 电机强度 12 维

这个模块也不是监督学习,而是把这些原始隐藏参数编码成策略可用的 20 维低维表示。它一方面通过 PPO 梯度更新,另一方面还会受到 priv_reg_loss 的额外约束。

2.3 history_encoder:用历史本体感觉恢复隐藏信息

history_encoder 接收最近 10proprio(10x53),输出 hist_latent(20)

它学的不是地形,也不是动作,而是试图从历史运动信息中恢复出与 priv_latent 对齐的低维隐藏参数表示。可以把它理解为"部署时用来代替隐藏真值的一条适应分支"。

需要特别强调的是:
history_encoder 的目标不是拟合 priv_latent_raw(29),而是拟合经过 priv_encoder 压缩后的 priv_latent(20)

2.4 estimator:从本体感觉预测显式特权信息

estimator 接收 proprio(53),输出 priv_explicit_est(9)

它是标准监督学习模块,利用 obs 中环境直接提供的 priv_explicit_gt(9) 作为标签,学习从本体感觉估计显式特权量。

这里有一个实现细节非常重要:虽然写成了 9 维,但当前代码里真正有物理意义的主要是前 3 维线速度,后 6 维基本是零占位。因此从语义上看,这一路本质上更接近"线速度估计器"。

2.5 depth_encoder:从深度图提取视觉地形特征与 yaw

depth_encoder 接收一张 58x87 的深度图和一个被抹去 yaw 相关量的 proprio(53),输出:

  • depth_latent(32):视觉版地形特征
  • pred_yaw(2):预测出来的 yaw 相关量

它的作用是用深度视觉去替代原本依赖扫描得到的 scan_latent,同时补回被屏蔽掉的 yaw 信息。

2.6 actordepth_actor:同一个主干,两种地形特征来源

actor 是教师策略,depth_actor 是学生策略。二者在结构上并没有本质区别,depth_actor 本身就是 actor 的拷贝。它们的唯一区别在于输入中的 32 维地形特征来自哪里:

  • actor 使用 scan_encoder(obs_scan) 得到的 scan_latent
  • depth_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_latenthist_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_latent
  • estimator 提供 priv_explicit_est
  • history_encoder 提供 hist_latent
  • depth_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 这个名字。这里的 DAggerDataset 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_latentdetach() 成常量,梯度只会回到 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_latent
  • student 用视觉替代特征 depth_latent

这就是典型的行为蒸馏。

最终第二阶段真正被联合更新的是:

  • depth_actor
  • depth_encoder

而不是教师 actor。

5. 奖励函数:这个项目到底在鼓励什么

从代码实现上看,奖励项虽然很多,但完全可以归纳成四类。

5.1 任务跟踪类奖励

这一类奖励直接决定机器人是否在"完成跑酷任务"。

最核心的两个项是:

  • tracking_goal_vel
  • tracking_yaw

tracking_goal_vel 鼓励机器人沿着当前目标点方向移动。
tracking_yaw 鼓励机器人让自身朝向对准目标方向。

这两项构成了最直接的任务驱动力:既要朝目标走,也要朝向目标。

5.2 机体稳定类奖励

这一类奖励负责限制身体姿态过于激烈、避免翻车。

主要包括:

  • lin_vel_z:惩罚 z 向速度过大,抑制过激跳动
  • ang_vel_xy:惩罚滚转和俯仰角速度过大
  • orientation:约束身体姿态保持合理

它们的作用是告诉机器人:可以高速跑、可以跳跃,但不能把身体搞成失控状态。

5.3 能耗与动作平滑类奖励

如果没有这类正则,策略很容易学出"能跑但很暴力"的动作模式。

主要包括:

  • torques:惩罚扭矩过大
  • delta_torques:惩罚相邻时刻扭矩变化过快
  • action_rate:惩罚动作变化过快
  • dof_acc:惩罚关节加速度过大
  • hip_posdof_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

这套框架最有价值的地方,在于它非常务实地利用了仿真的优势:第一阶段先利用地形特权信息、显式特权信息和隐式特权信息训练出强教师策略,同时让历史编码器学习如何用历史本体信息去逼近隐式特权表示;第二阶段再用深度图提取的视觉地形特征替代原来的地形扫描特征,并结合本体信息、显式信息和历史信息训练学生策略,最终逼近真实部署条件。

对高动态四足跑酷这种任务来说,这种"先在仿真里把问题学明白,再向现实约束收缩"的思路,往往比一开始就强行追求纯端到端部署输入,更容易做出真正可用的系统。

相关推荐
QYR_112 小时前
乙二醇汽车冷却液市场深度分析:热管理技术如何重塑行业格局?
大数据·人工智能
sp_fyf_20242 小时前
【大语言模型】 WizardLM:赋能大型预训练语言模型以遵循复杂指令
人工智能·深度学习·神经网络·语言模型·自然语言处理
深度学习lover2 小时前
<数据集>yolo 瓶盖识别<目标检测>
人工智能·python·yolo·计算机视觉·瓶盖识别
人机与认知实验室2 小时前
《人-机器人交互导论》第二版译者序
机器人
煜bart2 小时前
多智能体系统破解AI幻觉难题
人工智能·机器人·ai编程
跟着珅聪学java2 小时前
Java AI 开发完全教程
java·开发语言·人工智能
kyle~2 小时前
FANUC机器人与3D相机网络连接冲突错误排查报告(Linux默认路由冲突)
数码相机·3d·机器人
Ztopcloud极拓云视角2 小时前
实战:GPT-6 + Gemma 4 端云混合 AI 调用架构设计
大数据·人工智能·gpt
测绘第一深情2 小时前
MapQR:自动驾驶在线矢量化高精地图构建的端到端 SOTA 方法
数据结构·人工智能·python·神经网络·算法·机器学习·自动驾驶