跳转至

第 78 章 轮足机器人 RL 训练栈——从仿真环境到实机部署的完整流程

元信息
难度 ⭐⭐⭐(GPU 并行训练 + 奖励工程 + Sim-to-Real 部署)
预计时间 1.5 周(30-40 小时)
前置依赖 足式/190_腿足RL训练栈、复合/60_轮式运动学与Pfaffian、复合/70_轮足混合MPC
下游连接 复合/90_Swiss_Mile商业化、复合/110_轮足SimToReal与硬件

本章定位:MPC 用显式模型编码约束和模式切换规则,RL 从大规模仿真交互中学习"何时滚、何时迈、何时混合"的策略。本章以 Wheel-Legged-Gym 类训练栈为主线,从仿真环境搭建到实机安全部署,逐层拆解轮足 RL 的全部工程细节。


前置自测

📋 前置自测(答不出 2 题以上 → 先回 足式/190_腿足RL训练栈 和 复合/70_轮足混合MPC 复习)

  1. PPO 的 clipped surrogate objective 限制了什么?写出公式并解释 \(\epsilon\) 的物理意义。
  2. 在纯足式 RL 中,观测空间通常包含哪些量?为什么需要历史窗口?
  3. 轮足机器人相比纯足式多了哪些自由度?这些自由度的控制接口有何不同?
  4. 什么是特权学习(Privileged Learning)?教师网络比学生网络多看到什么信息?
  5. Domain Randomization 的核心假设是什么?如果随机化范围设得太宽会怎样?

本章目标

学完本章,你应能:

  1. 说清楚轮足 RL 相比纯足式 RL 的额外难点——额外自由度、模式切换、滑移问题
  2. 搭建 IsaacLab 轮足训练环境——URDF 加载、地形生成、传感器配置
  3. 设计完整的观测空间和动作空间——区分本体感知与特权信息、腿关节与轮速命令
  4. 逐项设计奖励函数——理解每项奖励的物理意义和权重平衡
  5. 配置 PPO 训练流程——网络架构、超参数选择、训练曲线诊断
  6. 实现 Teacher-Student 蒸馏——从特权教师到可部署学生的知识迁移
  7. 设计 Domain Randomization 方案——参数选择、范围确定、课程训练
  8. 完成 Sim-to-Real 部署——ONNX 导出、推理优化、安全包裹设计
  9. 理解 RL+MPC 混合架构——RL 底层策略与 MPC 上层规划的接口设计

78.1 为什么轮足 RL 比纯足式更难 ⭐⭐

动机:额外自由度带来的维度诅咒

假设你已经成功训练了一个纯足式四足机器人(如 Unitree Go2)的 RL 策略。这个策略学会了用 12 个关节在各种地形上行走。现在把机器人的四个脚掌替换为四个驱动轮——你的策略还能用吗?

答案是:完全不能。原因不仅仅是"多了 4 个轮子的自由度"这么简单。轮足 RL 的难度来自三个根本性的结构变化。

第一个难点:混合动作空间。 纯足式机器人的 12 个关节都是旋转关节,控制接口统一——要么都发位置目标,要么都发力矩。但轮足机器人的腿关节和轮关节有完全不同的物理特性。腿关节的行程有限(通常 \(\pm 60°\)\(\pm 180°\)),适合位置控制;轮关节可以连续旋转,没有"位置"概念,只能控制轮速或轮力矩。这意味着策略的动作空间不再是均匀的——一部分输出是位置增量,另一部分是速度命令,二者的量纲、范围、动力学响应时间都不同。

第二个难点:模式切换边界。 纯足式机器人只有一种运动模式——迈步行走。但轮足机器人有至少三种模式:纯滚动(轮子转、腿不动)、纯行走(腿迈步、轮子锁定)、混合模式(腿调姿态、轮子驱动前进)。更关键的是,这些模式之间的**切换边界**是连续的、依赖地形的、且难以用显式规则描述。平坦路面上应当纯滚动以节能,遇到台阶应当切换为行走或混合模式,遇到缓坡可能介于两者之间。RL 策略需要隐式地学会这些模式边界。

本质洞察:轮足 RL 的核心挑战**不是**让机器人学会走路或学会滚动——这两个子问题已经分别被纯足式 RL 和移动机器人控制解决了。真正的挑战是让策略学会**在两种模式之间无缝切换**,并且这种切换是地形自适应的、能耗最优的。

第三个难点:滑移的双刃剑效应。 纯足式机器人在训练中,脚底滑移总是坏事——它意味着接触力不足或地形太滑。但轮足机器人的轮子就是靠"滑移"(准确说是滚动接触)来前进的。RL 策略在训练过程中容易混淆"有益的轮子滚动"和"有害的侧向滑移"。如果奖励设计不当,策略会学会用侧滑来取巧——在仿真中看起来速度很快,但到了真机上就会因为侧向摩擦不够而摔倒。

回顾 足式/190_腿足RL训练栈 中的经验:纯足式 RL 的奖励函数通常包含速度跟踪、能耗惩罚和姿态保持。轮足 RL 需要在此基础上增加**滑移惩罚**和**模式平滑过渡**两类全新的奖励项,这大大增加了奖励工程的复杂度。

如果不用 RL,纯 MPC 能解决吗

回顾 复合/70_轮足混合MPC:MPC 可以处理轮足的模式切换,但需要显式定义模式边界和切换逻辑。在实践中,这些边界极其难以精确建模——地面摩擦系数、轮胎弹性、负载分布、地形坡度都会影响最优切换点。MPC 通常需要手动设定"当坡度超过 \(\theta_{max}\) 时切换为行走模式"这样的阈值规则。

如果不这样做——如果用固定阈值的 MPC 去处理连续变化的地形——会出现两个问题: 1. 切换瞬间产生力矩跳变,因为两种模式的参考轨迹不连续 2. 在模式边界附近反复振荡(刚切换到行走就发现坡度降低,又切回滚动)

RL 的优势正在于此:它可以从数据中学习**连续的、软的**模式切换策略,而不需要硬编码阈值。策略网络的输出天然是连续的——它不会输出"现在切换到模式 B",而是输出一组连续的关节位置和轮速,这些连续量自然地在不同地形上表现为不同的运动模式。

轮足 RL 的关键公式

整个训练过程围绕一个核心优化问题:

\[ \theta^* = \arg\max_\theta \; \mathbb{E}_{\tau \sim \pi_\theta} \left[ \sum_{t=0}^{T} \gamma^t r_t \right] \tag{78.1} \]

其中策略 \(\pi_\theta\) 将观测映射到混合动作空间:

\[ a_t = \pi_\theta(o_t) = \begin{bmatrix} \Delta q_{leg}^{des} \\ \omega_{wheel}^{des} \end{bmatrix} \in \mathbb{R}^{n_{leg} + n_{wheel}} \tag{78.2} \]

对于四足轮足机器人(如基于 ANYmal 的 Swiss-Mile 平台),\(n_{leg} = 12\)(每条腿 3 个关节),\(n_{wheel} = 4\)(每条腿末端一个轮子),总动作维度为 16。

💡 概念澄清:公式 (78.2) 中腿关节用的是**位置增量** \(\Delta q\),而不是绝对位置 \(q\)。这是因为 RL 策略输出的是围绕默认站姿 \(q_0\) 的残差:\(q_{leg}^{des} = q_0 + s_q \cdot \Delta q_{leg}^{des}\),其中 \(s_q\) 是动作缩放系数。这样做有两个好处:(1) 策略输出的零值对应默认站姿,初始化时机器人不会乱动;(2) 动作范围更对称,有利于神经网络学习。

对比维度 纯足式 RL 轮足 RL
动作维度 12(关节位置) 16(12 关节 + 4 轮速)
动作类型 均匀(全部位置控制) 混合(位置 + 速度)
运动模式 1 种(行走) 3+ 种(滚动/行走/混合)
滑移处理 纯惩罚 需区分滚动与滑移
奖励项数 5-8 项 10-15 项
训练难度 中等 高(奖励工程关键)

⚠️ 常见陷阱

⚠️ 编程陷阱:腿关节和轮关节用同一套动作缩放 - 错误做法:action_scale = 0.5 对所有 16 维统一缩放 - 现象:轮速命令被压缩到 \(\pm 0.5\) rad/s,机器人爬行般缓慢;或者腿关节被放大到 \(\pm 5\) rad,关节打到限位 - 根本原因:腿关节的合理范围是 \(\pm 0.3\)~\(0.5\) rad(围绕默认站姿的小偏移),而轮速的合理范围是 \(\pm 10\)~\(30\) rad/s - 正确做法:分别设置 leg_action_scalewheel_action_scale,在 config 中明确标注两者的物理单位

💡 概念误区:认为轮足 RL 就是"足式 RL + 多 4 个轮子输出" - 新手想法:直接在足式策略上加 4 维输出,其他不变 - 实际上:观测空间需要增加轮速反馈和滑移估计,奖励函数需要完全重新设计(增加滚动奖励、滑移惩罚、模式平滑项),训练课程需要从平地滚动开始逐步增加地形复杂度。这不是"加几维"的问题,而是整个训练栈的重新设计 - 正确思路:把轮足 RL 当作一个全新问题来设计,虽然可以复用足式 RL 的框架代码,但观测、动作、奖励、课程都需要从轮足的物理特性出发重新思考

🧠 思维陷阱:认为 RL 策略会自动发现最优的模式切换边界 - 新手想法:只要给足够的训练时间,策略自然会学到什么时候该滚、什么时候该走 - 实际上:如果奖励函数没有正确引导,策略可能学到的是一种"万金油"模式——永远半走半滚,在任何地形上都不是最优的。更糟糕的是,策略可能学会利用仿真器的漏洞(如超低摩擦下的高速侧滑),获得高奖励但无法迁移到真机 - 正确思维:奖励函数必须显式包含能耗项和滑移惩罚,迫使策略在能滚动时选择滚动(因为更省能),在不能滚动时选择行走(因为滑移被惩罚)

练习

  1. ⭐ 列出纯足式 Go2 的观测空间(参考足式/190_腿足RL训练栈),然后逐项分析哪些需要为轮足修改、哪些需要新增。画一张表格对比。
  2. ⭐⭐ 假设一个轮足机器人在 \(15°\) 坡面上行驶。用能量分析说明:此时纯滚动模式还是混合模式更高效?提示:考虑重力分量、轮胎摩擦和腿部支撑力的关系。
  3. ⭐⭐ 设计一个最小实验来验证"策略利用侧滑取巧"的现象。提示:对比训练时和测试时摩擦系数不同的策略表现。

78.2 仿真环境搭建:IsaacLab 配置与 URDF 模型 ⭐⭐

动机:为什么仿真环境是训练成功的前提

RL 策略的质量上限由仿真环境决定。如果仿真环境的物理不够真实、地形不够多样、传感器模型不够准确,那么无论奖励函数设计得多精巧、PPO 超参数调得多细,训练出的策略都无法迁移到真机。

回顾 足式/190_腿足RL训练栈:纯足式 RL 使用 IsaacGym 进行 GPU 并行训练,单卡可同时仿真数千个机器人实例。这种大规模并行是 RL 训练效率的关键——PPO 每次更新需要数十万步交互数据,如果用单个仿真器串行采集,一次训练可能需要数天;而 GPU 并行可以在数小时内完成。

IsaacLab(IsaacGym 的继任者,基于 NVIDIA Isaac Sim)继承了 GPU 并行仿真的核心能力,同时提供了更模块化的环境配置接口。截至 2025 年,IsaacLab 已成为足式和轮足 RL 训练的主流平台。

环境搭建的三个核心步骤

步骤一:URDF/MJCF 模型加载。 轮足机器人的模型文件需要正确描述腿关节和轮关节的物理属性。关键区别在于:

属性 腿关节(revolute) 轮关节(continuous)
类型 revolute,有限行程 continuous,无限旋转
位置限位 \([q_{min}, q_{max}]\) 无限位
控制模式 位置 PD 控制 速度 PI 控制
摩擦模型 关节粘性摩擦 轮胎-地面滚动摩擦
惯性影响 影响腿部摆动频率 影响加减速响应

在 URDF 中,轮关节应定义为 continuous 类型,其 <limit> 标签不设 lower/upper,只设 effort(最大力矩)和 velocity(最大转速):

<!-- 轮关节定义示例 -->
<joint name="FL_wheel_joint" type="continuous">
  <parent link="FL_shank_link"/>
  <child link="FL_wheel_link"/>
  <axis xyz="0 1 0"/>  <!-- 绕 y 轴旋转 -->
  <limit effort="10.0" velocity="40.0"/>  <!-- 最大 10 Nm, 40 rad/s -->
  <dynamics damping="0.01" friction="0.005"/>
</joint>

为什么不能把轮关节也定义为 revolute? 因为 revolute 类型在 Isaac Sim 中会强制启用位置限位检查。当轮子连续旋转超过 \(2\pi\) 时,仿真器会尝试将其拉回限位范围,导致突然的力矩跳变,策略训练会变得不稳定。

步骤二:地形生成。 轮足机器人需要的地形比纯足式更多样。纯足式 RL 的地形通常包括平地、台阶、随机坡面和碎石。轮足 RL 需要额外增加:

地形类型 轮足的独特需求 训练目的
长距离平地 学习高效纯滚动 建立滚动基线
缓坡(\(5°\)-\(15°\) 学习坡面滚动+姿态调节 测试模式选择
台阶(单级/多级) 学习模式切换(滚动→行走→滚动) 训练切换能力
门槛/减速带 学习小障碍越过 城市场景适应
低摩擦表面 学习应对滑溜地面 鲁棒性训练
混合地形 在同一 episode 中经历多种地形 端到端整合

IsaacLab 中的地形通常用高度图(height field)或三角网格生成。课程训练中,地形难度应随训练进度逐步提升:

\[ \text{terrain\_level}_{k+1} = \text{terrain\_level}_k + \mathbf{1}[\text{success\_rate} > \eta] \tag{78.3} \]

其中 \(\eta\) 是晋级阈值(通常 0.7-0.8)。这个公式的含义是:当某个难度级别的成功率超过阈值后,自动进入下一个难度级别。

步骤三:执行器模型配置。 仿真中的电机模型直接影响策略的可迁移性。

理想电机假设瞬时力矩跟踪:\(\tau = \tau_{cmd}\)。但真实电机有三个关键非理想特性:

  1. 带宽限制:电机力矩响应不是瞬时的,有 1-5 ms 的延迟和 10-50 Hz 的带宽限制。可以用一阶低通滤波器近似:\(\tau(s) = \frac{1}{1 + s/\omega_c} \tau_{cmd}(s)\),其中 \(\omega_c\) 是截止频率
  2. 力矩饱和:真实电机在高转速下最大力矩降低(恒功率区),呈近似线性下降
  3. 齿轮间隙(backlash):减速器的齿轮间隙在方向反转时产生死区

如果仿真中不建模这些非理想特性,策略会学到依赖"完美电机"的高频振荡动作,到真机上电机跟不上就会摔倒。

# IsaacLab 中的执行器模型配置示例
class WheelLeggedActuatorCfg:
    # 腿部关节:位置 PD 控制
    leg_actuator = ActuatorNetMLPCfg(
        joint_names_expr=[".*hip.*", ".*thigh.*", ".*shank.*"],
        stiffness={".*hip.*": 80.0, ".*thigh.*": 80.0, ".*shank.*": 80.0},
        damping={".*hip.*": 2.0, ".*thigh.*": 2.0, ".*shank.*": 2.0},
        # 力矩限制(Nm)
        effort_limit=33.5,
        # 速度限制(rad/s)
        velocity_limit=21.0,
    )
    # 轮部关节:速度控制
    wheel_actuator = ActuatorNetMLPCfg(
        joint_names_expr=[".*wheel.*"],
        # 轮关节用速度 PI 控制,不需要 stiffness
        stiffness=0.0,
        damping=5.0,  # 这里的 damping 充当速度 PI 的 P 增益
        effort_limit=10.0,
        velocity_limit=40.0,
    )

本质洞察:仿真环境搭建**不是**让仿真"看起来像真实",而是让仿真"覆盖真实可能出现的情况"。真实世界是一个具体的参数点(某个摩擦系数、某个电机延迟、某个负载质量),仿真通过 Domain Randomization 覆盖一个包含该点的参数分布。这就是为什么步骤三(执行器模型)如此重要——它定义了"真实参数点"的哪些维度会被覆盖。

⚠️ 常见陷阱

⚠️ 编程陷阱:轮关节用 revolute 类型定义 - 错误做法:URDF 中 <joint type="revolute">,设 lower="-3.14" upper="3.14" - 现象:轮子转到 \(\pm \pi\) 时突然反弹,训练中出现间歇性力矩尖峰 - 根本原因:revolute 类型的限位检查在每个仿真步生效,连续旋转必然触发 - 正确做法:改为 <joint type="continuous">,去掉 lower/upper 限位

💡 概念误区:认为地形越复杂越好 - 新手想法:一开始就用最复杂的混合地形训练 - 实际上:课程训练(Curriculum Learning)的核心是从简单到复杂。如果一开始地形太难,策略只会学到"站在原地不动"——因为任何运动都会摔倒。应该从纯平地开始,让策略先学会滚动,再逐步增加台阶和坡面 - 正确思路:设计 5-8 个难度级别,每个级别有明确的地形参数范围,用公式 (78.3) 自动晋级

🧠 思维陷阱:只关注物理引擎精度,忽略并行仿真的数值一致性 - 新手想法:用最高精度的物理引擎设置(最小时间步、最多求解迭代) - 实际上:GPU 并行仿真中数千个环境共享同一个物理步进,过高的精度设置会导致仿真速度大幅下降。而且由于 GPU 浮点运算的非确定性,提高精度设置并不一定能提高策略质量。关键是让仿真的物理行为在**统计意义上**覆盖真实情况 - 正确思维:用中等精度(dt=0.005s,4-8 次接触求解迭代)作为起点,通过 Domain Randomization 弥补精度不足

练习

  1. ⭐ 打开 Wheel-Legged-Gym 仓库(clearlab-sustech/Wheel-Legged-Gym),找到环境配置文件,列出所有可配置的仿真参数及其默认值。
  2. ⭐⭐ 用 URDF 描述一个最简单的轮足机器人:1 条腿(2 个旋转关节)+ 1 个轮子(1 个 continuous 关节)。在 Isaac Sim 中加载并验证轮关节可以连续旋转。
  3. ⭐⭐ 设计一个实验来测量仿真中的执行器延迟:发送阶跃力矩命令,记录实际力矩的响应曲线,与真实电机的数据表对比。

78.3 观测空间设计:策略能看到什么 ⭐⭐⭐

动机:观测决定策略的信息边界

RL 策略只能基于观测 \(o_t\) 来决策。如果一个信息没有进入观测空间,策略就完全不知道它的存在——无论这个信息对任务多么重要。观测空间设计的核心决策是:哪些信息是真机上可获取的(可部署观测),哪些是仿真中独有的(特权信息)

这个区分之所以如此关键,是因为训练和部署的信息不对称。在仿真中,你可以获取地形的完整高度图、每个接触点的精确法向力、轮子的真实滑移率——这些都是仿真器内部状态的直接读出。但在真机上,你只有 IMU(噪声、偏置、漂移)、关节编码器(量化误差)、轮速传感器(延迟)和可能的深度相机(遮挡、光照变化)。

如果训练时策略看到了特权信息,部署时这些信息突然消失,策略的性能会断崖式下降。这就是为什么需要 Teacher-Student 两阶段训练(§78.7 详述)。

可部署观测:真机传感器能给什么

轮足机器人的可部署观测分为以下几类:

本体感知(Proprioception):来自机器人自身传感器的信息

观测量 维度 来源 物理意义
基座角速度 \(\omega_{base}\) 3 IMU 陀螺仪 机器人旋转速度
重力方向投影 \(g_{proj}\) 3 IMU 加速度计 + 姿态估计 机器人相对重力的倾斜
关节位置 \(q_{leg}\) 12 编码器 各腿关节当前角度
关节速度 \(\dot{q}_{leg}\) 12 编码器差分 各腿关节角速度
轮速 \(\omega_{wheel}\) 4 轮速传感器 各轮当前转速
速度命令 \(v_{cmd}\) 3 上层规划 目标线速度 \(v_x, v_y\) + 角速度 \(\omega_z\)
上一时刻动作 \(a_{t-1}\) 16 策略输出缓存 动作平滑参考

为什么需要重力方向投影而不是欧拉角? 欧拉角存在万向锁问题——当 pitch 接近 \(\pm 90°\) 时,roll 和 yaw 无法区分,导致数值跳变。重力方向投影 \(g_{proj} = R^T_{body} [0, 0, -1]^T\) 是机体坐标系下的重力向量,三个分量连续变化,不存在奇异性。

历史窗口:为什么需要过去几步的观测?

单帧观测无法提供速度估计和延迟补偿的信息。考虑以下场景:机器人正在一个坡面上滚动,IMU 报告 pitch 角为 \(10°\),关节速度为零(轮子在滚动,腿不动)。仅从这一帧观测,策略无法判断:(a) 机器人是在上坡还是下坡?(b) 轮子是在正常滚动还是在打滑?(c) 地面摩擦系数是多少?

如果加入前 \(k\) 帧的观测历史,策略可以通过时间差分推断出速度变化趋势、加速度方向、以及摩擦力是否足够。实践中 \(k = 3\)~\(10\) 帧(对应 15-50 ms 的历史窗口)已足够。

\[ o_t^{history} = [o_{t-k}, o_{t-k+1}, \ldots, o_{t-1}, o_t] \tag{78.4} \]

或者更紧凑地,只保留一个学习的隐变量:

\[ z_t = \phi(o_{t-k:t}) \tag{78.5} \]

其中 \(\phi\) 可以是 TCN(时间卷积网络)或 MLP。

观测归一化:所有观测量在进入策略网络之前必须归一化。不同物理量的数值范围差异极大——关节角度在 \([-\pi, \pi]\),轮速在 \([-40, 40]\) rad/s,IMU 角速度在 \([-10, 10]\) rad/s。如果不归一化,数值大的量会主导梯度,数值小的量会被忽略。

\[ o_{norm} = \text{clip}\left(\frac{o - \mu}{\sigma}, -c, c\right) \tag{78.6} \]

其中 \(\mu, \sigma\) 是运行统计量(running mean/std),\(c\) 是裁剪范围(通常 5-10)。裁剪是必要的,因为 RL 训练早期环境重置频繁,统计量不稳定,不裁剪会出现极端的归一化值。

特权信息:教师网络的"上帝视角"

教师网络在训练时可以额外获取以下信息(学生网络在部署时无法获取):

特权信息 维度 物理意义 为什么学生看不到
地形高度图 \(H \times W\) 机器人周围的地形高度 需要高精度深度传感器 + 地图构建
地面摩擦系数 \(\mu\) 1 或 \(n_c\) 各接触点的摩擦 无传感器可直接测量
接触法向力 \(f_n\) \(n_c\) 各足/轮的接触力 需要力传感器(昂贵/脆弱)
轮滑移率 \(\kappa\) \(n_{wheel}\) 轮子的纵向滑移 需要精确的地面真实速度
基座线速度 \(v_{base}\) 3 机器人在世界坐标系的速度 IMU 积分有漂移,GPS 不精确
物理参数(质量、惯性等) \(n_p\) 域随机化参数 真机参数未知

本质洞察:特权信息的选择**不是**"仿真中能获取什么就给什么"。应该只选择那些**对策略决策有因果影响**的信息。例如,机器人后方 10 米处的地形高度对当前步态决策没有影响,给教师这个信息只会增加学习难度。通常只给机器人正前方 1-2 米范围的地形信息。

观测 Schema:版本化的工程必需品

观测空间的定义必须用结构化的 schema 记录,而不是靠代码中 tensor 拼接的顺序来"隐式"定义。因为:

  1. 训练端和部署端的代码通常由不同的人/团队维护
  2. 观测顺序在导出 ONNX 后被冻结,改不了
  3. 一旦顺序不一致,策略会接收到完全错误的输入(如把轮速当成关节角度),输出看似合理但完全混乱的动作
# 观测 Schema 示例
OBSERVATION_SCHEMA = {
    "version": "1.3.0",
    "deploy_obs": [
        {"name": "base_ang_vel",    "dim": 3,  "unit": "rad/s", "scale": 0.25},
        {"name": "gravity_proj",    "dim": 3,  "unit": "1",     "scale": 1.0},
        {"name": "joint_pos",       "dim": 12, "unit": "rad",   "scale": 1.0},
        {"name": "joint_vel",       "dim": 12, "unit": "rad/s", "scale": 0.05},
        {"name": "wheel_vel",       "dim": 4,  "unit": "rad/s", "scale": 0.1},
        {"name": "velocity_cmd",    "dim": 3,  "unit": "m/s,rad/s", "scale": 1.0},
        {"name": "last_action",     "dim": 16, "unit": "mixed", "scale": 1.0},
    ],  # 总维度: 53
    "privileged_obs": [
        {"name": "terrain_heights", "dim": 187, "unit": "m",    "scale": 1.0},
        {"name": "friction_coeff",  "dim": 1,   "unit": "1",    "scale": 1.0},
        {"name": "base_lin_vel",    "dim": 3,   "unit": "m/s",  "scale": 2.0},
        {"name": "contact_forces",  "dim": 12,  "unit": "N",    "scale": 0.02},
        {"name": "slip_ratio",      "dim": 4,   "unit": "1",    "scale": 1.0},
    ],  # 总维度: 207
}

⚠️ 常见陷阱

⚠️ 编程陷阱:导出 ONNX 时归一化参数与训练不一致 - 错误做法:训练时用 running normalization,导出时忘记冻结 \(\mu, \sigma\) - 现象:部署后策略前几秒正常,之后越来越偏——因为部署端在用初始化的 \(\mu=0, \sigma=1\) - 根本原因:training 模式下的 RunningMeanStd 在每步更新统计量,eval 模式下应该冻结 - 正确做法:导出前调用 model.eval(),并手动将 \(\mu, \sigma\) 存入模型参数 - 自检方法:导出前后用同一批观测数据跑推理,对比输出差异,\(L_\infty\) 误差应 \(< 10^{-5}\)

💡 概念误区:认为更多的历史帧数一定更好 - 新手想法:历史窗口越长,策略能推断的信息越多 - 实际上:过长的历史窗口增加了网络输入维度,使得训练更困难。而且超过 50 ms 的历史信息对当前步态决策的因果影响已经很弱——关节 PD 控制器的响应时间只有 5-10 ms。实践中 3-5 帧(15-25 ms)通常是最优的平衡点 - 正确思路:用消融实验确定最优历史长度——固定其他超参数,扫描 \(k \in \{1, 3, 5, 10, 20\}\),看训练奖励和 sim-to-real 迁移效果

练习

  1. ⭐ 计算上述 schema 中可部署观测的总维度。如果加入 5 帧历史(只对 joint_pos、joint_vel、wheel_vel 保留历史),总维度变为多少?
  2. ⭐⭐ 设计一个实验来验证"观测顺序错误"会导致什么后果:在训练好的策略上,人为交换 joint_pos 和 joint_vel 的顺序,观察策略行为变化。
  3. ⭐⭐⭐ 为什么基座线速度 \(v_{base}\) 被归为特权信息?如果真机有一个精确的外部定位系统(如 motion capture),是否可以将其加入可部署观测?讨论利弊。

78.4 动作空间设计:策略输出什么 ⭐⭐⭐

动机:动作空间的设计决定了策略的表达能力边界

动作空间不是"策略输出多少维"这么简单的问题。它涉及三个层面的设计决策:(1) 输出什么物理量(位置?速度?力矩?),(2) 输出的范围多大(动作缩放),(3) 输出如何被执行(低层控制器接口)。这三个层面的选择直接影响策略的学习难度、表达能力和 sim-to-real 迁移性。

腿关节的动作设计

腿关节有三种常见的控制模式,各有优劣:

控制模式 公式 优点 缺点
位置目标 \(q^{des} = q_0 + s_q \cdot a_q\) 隐含 PD 稳定性,安全 无法输出任意力矩
力矩直接控制 \(\tau = s_\tau \cdot a_\tau\) 最大灵活性 训练困难,不安全
残差位置 + 反馈力矩 \(\tau = K_p(q^{des} - q) + K_d(\dot{q}^{des} - \dot{q}) + s_\tau \cdot a_{ff}\) 兼顾安全和灵活 维度翻倍

对于轮足 RL,**位置目标模式**是最常用的选择,原因有三:

  1. 安全性:PD 控制器天然具有弹簧-阻尼特性。即使策略输出了一个不合理的位置目标,PD 控制器也会以有限的力矩去执行,不会产生破坏性的力矩脉冲
  2. Sim-to-Real 友好:位置 PD 控制的行为对电机模型误差的敏感度远低于直接力矩控制。PD 增益可以在 sim 和 real 之间保持相同
  3. 学习效率:位置目标的变化范围小(\(\pm 0.3\)~\(0.5\) rad),网络输出分布紧凑,PPO 的 clipping 机制更有效
\[ q_{leg}^{des} = q_{default} + s_{leg} \cdot a_{leg} \tag{78.7} \]

其中 \(q_{default}\) 是默认站姿角度,\(s_{leg}\) 是动作缩放系数(通常 0.25-0.5 rad),\(a_{leg} \in [-1, 1]^{12}\) 是策略的归一化输出。

轮关节的动作设计

轮关节的控制模式与腿关节有本质不同。轮子不存在"位置"概念——它可以连续旋转——因此只能控制**速度**或**力矩**。

\[ \omega_{wheel}^{des} = s_{wheel} \cdot a_{wheel} \tag{78.8} \]

其中 \(s_{wheel}\) 是轮速缩放系数(通常 10-30 rad/s),\(a_{wheel} \in [-1, 1]^4\) 是策略的归一化输出。

为什么用速度控制而不是力矩控制? 两个原因:

  1. 与滚动运动学的一致性:轮子的前进速度 \(v = r \cdot \omega\),速度控制直接对应运动学目标。力矩控制则需要策略自己学会从期望速度到期望力矩的映射,这等价于让策略学习一个内模型
  2. 稳态行为更可预测:速度控制在稳态时轮子会以目标速度旋转(由速度 PI 闭环保证),力矩控制在平地上会持续加速直到摩擦力平衡

本质洞察:动作空间设计的本质是在**策略的表达能力**和**学习的效率**之间做权衡。力矩控制给策略最大的自由度,但学习空间太大,策略容易在无关维度上浪费探索。位置/速度控制通过底层 PD/PI 闭环缩小了搜索空间,代价是无法表达某些需要精细力矩控制的动作。对于轮足 RL,位置+速度的混合控制已经足够表达所有需要的运动模式。

动作安全包裹

策略输出不应直接进入电机驱动器。中间需要经过三层安全处理:

第一层:低通滤波。 策略网络的输出可能在相邻时步之间有很大跳变(尤其是训练早期),直接执行会产生冲击力矩。

\[ a_{filtered} = \alpha \cdot a_{raw} + (1 - \alpha) \cdot a_{prev} \tag{78.9} \]

其中 \(\alpha \in [0.2, 0.8]\) 是滤波系数。\(\alpha\) 越小,平滑度越高但响应越慢。实践中 \(\alpha = 0.6\) 是常见选择。

第二层:限幅。 滤波后的动作仍需限制在物理可行范围内。

\[ a_{clipped} = \text{clip}(a_{filtered}, a_{min}, a_{max}) \tag{78.10} \]

第三层:姿态保护。 当机器人姿态异常(如 roll/pitch 超过安全阈值)时,强制减小动作幅度:

\[ a_{safe} = a_{clipped} \cdot \max\left(0, 1 - \frac{\|[\text{roll}, \text{pitch}]\|}{\theta_{max}}\right) \tag{78.11} \]
def process_action(raw_action, last_action, robot_state, config):
    """三层动作安全处理"""
    # 分离腿关节和轮关节动作
    leg_action = raw_action[:12]
    wheel_action = raw_action[12:]

    # 第一层:低通滤波
    leg_filtered = config.alpha_leg * leg_action + (1 - config.alpha_leg) * last_action[:12]
    wheel_filtered = config.alpha_wheel * wheel_action + (1 - config.alpha_wheel) * last_action[12:]

    # 第二层:限幅
    leg_clipped = torch.clamp(leg_filtered, -1.0, 1.0)
    wheel_clipped = torch.clamp(wheel_filtered, -1.0, 1.0)

    # 第三层:姿态保护
    rp_norm = torch.norm(robot_state.rp, dim=-1, keepdim=True)  # roll-pitch 范数
    safety_scale = torch.clamp(1.0 - rp_norm / config.rp_max, min=0.0)

    # 应用缩放到物理量
    q_des = config.q_default + config.leg_scale * leg_clipped * safety_scale
    omega_des = config.wheel_scale * wheel_clipped * safety_scale

    return torch.cat([q_des, omega_des], dim=-1)

⚠️ 常见陷阱

⚠️ 编程陷阱:训练时加了低通滤波但导出时忘记 - 错误做法:训练代码中 apply_action(filter(policy_output)),但 ONNX 只导出 policy_output - 现象:真机动作剧烈抖动,关节嗡嗡响 - 根本原因:策略已经学会了"依赖滤波器来平滑输出",没有滤波器它不知道需要自己输出平滑的值 - 正确做法:将低通滤波和限幅逻辑写在策略网络的 forward 方法中,一起导出

💡 概念误区:认为位置控制比力矩控制"差" - 新手想法:力矩控制更"高级",应该用力矩控制来获得更好的性能 - 实际上:MIT Mini Cheetah(Kim et al. 2019)用力矩控制在 MPC+WBC 框架中取得了出色效果,但那是在有精确模型的前提下。RL 策略的输出噪声远大于 MPC,用力矩控制非常容易在真机上造成危险。学术界主流的足式 RL 工作(Rudin et al. 2022, Lee et al. 2024)都使用位置目标控制 - 正确思路:选择控制模式时考虑整个系统的鲁棒性,而不是单个模块的理论最优性

练习

  1. ⭐ 计算当 \(s_{leg} = 0.4\)\(q_{default}\) 对应的 hip/thigh/knee 分别为 \([0, 0.8, -1.6]\) rad 时,策略输出 \(a_{leg} = [1, 1, 1, ...]\) 对应的关节目标位置。这些位置是否在物理可行范围内?
  2. ⭐⭐ 设计一个对照实验:分别用 \(\alpha = 0.2, 0.5, 0.8\) 的低通滤波系数训练策略,记录训练曲线和最终性能。预测哪个 \(\alpha\) 值会导致训练不稳定?
  3. ⭐⭐⭐ (跨章综合题)结合 复合/60_轮式运动学与Pfaffian 的知识,如果轮足机器人的四个轮子并非全向轮,而是普通轮(只能沿轮轴方向滚动),那么动作空间的 4 维轮速 \(\omega_{wheel} \in \mathbb{R}^4\) 中实际有多少个独立自由度?提示:考虑 Pfaffian 约束。

78.5 奖励函数工程:从物理目标到可学习信号 ⭐⭐⭐⭐

动机:奖励是 RL 训练的"灵魂"

如果仿真环境是策略训练的"身体",那么奖励函数就是"灵魂"。同一个仿真环境、同一套 PPO 超参数,换一组奖励权重就可能训练出完全不同的行为——一组出优雅的模式切换,另一组出疯狂的侧滑取巧。

奖励函数设计的核心困难在于**多目标冲突**:你希望机器人跑得快(速度跟踪),又希望它省电(能耗惩罚),还希望它不打滑(滑移惩罚),同时保持姿态稳定(姿态奖励)、动作平滑(平滑惩罚)。这些目标之间存在根本性的矛盾——跑得快必然费电,省电就跑不快。

奖励工程的本质不是寻找"最好的"权重组合——没有全局最优解。它是在这些矛盾目标之间找到一个**满足部署需求的折中点**,并通过系统化的消融实验验证这个折中点的鲁棒性。

奖励分项的逐项设计

轮足 RL 的奖励函数通常包含 10-15 个分项。下面逐项解释每项的物理意义、数学形式和设计考量。

第一类:任务奖励(鼓励完成目标)

\[ r_{vel} = w_{vel} \cdot \exp\left(-\frac{\|v_{xy} - v_{xy}^{cmd}\|^2}{\sigma_v^2}\right) \tag{78.12} \]

速度跟踪奖励。用高斯核而不是线性误差有两个好处:(1) 当跟踪误差很小时奖励接近 1,策略有明确的"做对了"信号;(2) 当误差很大时奖励接近 0(而不是负无穷),不会主导梯度。\(\sigma_v\) 控制宽容度——\(\sigma_v = 0.25\) m/s 意味着速度误差在 0.25 m/s 以内时奖励已经接近最大值。

\[ r_{yaw} = w_{yaw} \cdot \exp\left(-\frac{(\omega_z - \omega_z^{cmd})^2}{\sigma_\omega^2}\right) \tag{78.13} \]

航向角速度跟踪,形式与速度跟踪相同。

第二类:姿态和稳定性奖励

\[ r_{upright} = w_{up} \cdot (g_{proj,z} + 1) / 2 \tag{78.14} \]

姿态保持奖励。\(g_{proj,z}\) 是重力在机体 z 轴的投影,完全直立时 \(g_{proj,z} = -1\)(指向地面),完全翻倒时 \(g_{proj,z} = 1\)(指向天空)。上式将其归一化到 \([0, 1]\),直立时奖励为 0(因为 \((-1+1)/2=0\))。

等等,这里有个问题——直立时奖励为 0 似乎没有鼓励作用。实际中通常用另一种形式:

\[ r_{upright} = -w_{up} \cdot \|[\text{roll}, \text{pitch}]\|^2 \tag{78.15} \]

这是一个惩罚项(负号),姿态偏离越大惩罚越重。二者等价但后者更直观。

第三类:滑移惩罚(轮足特有)

\[ c_{slip} = w_{slip} \cdot \sum_{i=1}^{4} (|\kappa_i| + |\alpha_i|) \tag{78.16} \]

其中 \(\kappa_i\) 是第 \(i\) 个轮子的纵向滑移率,\(\alpha_i\) 是侧向滑移角。

纵向滑移率的定义:

\[ \kappa = \frac{r\omega_{wheel} - v_{contact}}{max(|r\omega_{wheel}|, |v_{contact}|, \epsilon)} \tag{78.17} \]

其中 \(r\) 是轮半径,\(\omega_{wheel}\) 是轮转速,\(v_{contact}\) 是接触点的地面速度(沿轮子前进方向)。\(\kappa = 0\) 表示纯滚动(无滑移),\(|\kappa| = 1\) 表示完全滑移(轮子锁死拖行或空转)。

如果不加滑移惩罚会怎样? 策略会学到一种"侧滑漂移"行为——利用仿真中的低侧向摩擦,让轮子以一定角度切入地面,靠侧滑的摩擦力加速。在仿真中这看起来速度很快、奖励很高,但在真机上侧向摩擦远比仿真中复杂(取决于轮胎材质、地面材质、接触压力分布),导致真机直接翻车。

第四类:能耗惩罚

\[ c_{energy} = w_E \cdot \sum_{j=1}^{n_a} |\tau_j \dot{q}_j| \tag{78.18} \]

能耗惩罚鼓励策略选择低能耗的运动方式。这对轮足 RL 尤其重要——在平坦路面上,纯滚动的能耗远低于行走(轮子滚动摩擦远小于腿部关节摩擦),能耗惩罚会自然驱使策略在可滚动时选择滚动。

第五类:动作平滑惩罚

\[ c_{smooth} = w_{sm} \cdot \|a_t - a_{t-1}\|^2 \tag{78.19} \]

惩罚相邻时步之间动作的突变。过大的动作变化意味着关节加速度过高,不仅费电还会加速机械磨损。

\[ c_{jerk} = w_{jk} \cdot \|a_t - 2a_{t-1} + a_{t-2}\|^2 \tag{78.20} \]

动作 jerk(加加速度)惩罚,比一阶平滑更强地抑制高频振荡。

第六类:关节限位惩罚

\[ c_{limit} = w_{lim} \cdot \sum_{j} \max(0, |q_j| - q_j^{soft\_limit})^2 \tag{78.21} \]

软限位惩罚。\(q_j^{soft\_limit}\) 设在硬件限位的 80%-90%,留出安全裕度。

超参数表

奖励项 符号 权重 \(w\) \(\sigma\) 或其他参数 备注
线速度跟踪 \(r_{vel}\) 1.5 \(\sigma_v = 0.25\) 主要任务奖励
角速度跟踪 \(r_{yaw}\) 0.5 \(\sigma_\omega = 0.25\) 航向控制
姿态惩罚 \(c_{rp}\) -5.0 roll+pitch 平方和
滑移惩罚 \(c_{slip}\) -0.1 纵向+侧向
能耗惩罚 \(c_{energy}\) -0.005 $
动作平滑 \(c_{smooth}\) -0.01 一阶差分
动作 jerk \(c_{jerk}\) -0.001 二阶差分
关节限位 \(c_{limit}\) -10.0 soft=0.9·hard 超限严厉惩罚
基座高度 \(r_{height}\) -1.0 \(h_{target}\) 鼓励保持站姿高度
足端空速 \(c_{air}\) -0.5 惩罚摆动腿速度过大
碰撞惩罚 \(c_{coll}\) -5.0 非足/轮部位接触地面

⚠️ 注意:上表的权重是一个**起点**,不是最终值。每个机器人平台、每种任务场景都需要通过消融实验来微调。但权重的**数量级关系**应该保持:任务奖励 > 安全惩罚 > 能耗惩罚 > 平滑惩罚。

消融实验方法论

奖励消融是验证每一项奖励是否必要的系统化方法。步骤如下:

  1. 用完整奖励函数训练一个基线策略(baseline)
  2. 每次只关闭一项奖励(设其权重为 0),保持其他不变
  3. 训练到收敛,记录关键指标:最终 episode return、速度跟踪误差、滑移率、能耗、摔倒率
  4. 对比消融结果与基线
关闭项 预期现象 如果没有预期现象
滑移惩罚 侧滑率大幅上升,速度跟踪可能反而更好(取巧) 说明滑移惩罚的权重可能过低
能耗惩罚 能耗上升 30-100%,模式选择偏向行走 说明能耗惩罚正在驱动模式选择
动作平滑 动作高频振荡,真机部署时电机嗡嗡响 说明策略本身已经足够平滑
姿态惩罚 机器人大幅倾斜但仍能前进 说明物理约束已经足够限制姿态

⚠️ 常见陷阱

⚠️ 编程陷阱:所有奖励项用 sum 而不是分别记录 - 错误做法:total_reward = sum([r1, r2, c1, c2, ...]),只记录 total_reward - 现象:训练曲线看起来在涨,但不知道是哪项在涨——可能速度跟踪在涨但滑移也在涨 - 根本原因:复合奖励的总值掩盖了分项的变化趋势 - 正确做法:每个奖励分项都单独记录到 TensorBoard/WandB,用 env.extras["rewards/vel_tracking"] = r_vel.mean() 保存

💡 概念误区:认为高斯核奖励 \(\exp(-e^2/\sigma^2)\) 等价于 L2 惩罚 \(-e^2\) - 新手想法:二者都是误差越大奖励/惩罚越大,效果一样 - 实际上:高斯核在误差很大时奖励趋近于 0,梯度也趋近于 0——策略不会因为离目标很远而受到大的梯度推动。L2 惩罚在误差很大时梯度很大,可能导致训练不稳定。高斯核的另一个优势是输出范围固定在 \([0, 1]\),不需要额外的归一化 - 正确思路:任务奖励用高斯核(有明确的"做对了"信号),惩罚项用 L2(不需要上界,越违规惩罚越大)

🧠 思维陷阱:用试错法调奖励权重 - 新手想法:这组权重不行就换一组,多试几次总能找到好的 - 实际上:12 个奖励项的权重构成一个 12 维空间,随机搜索效率极低。而且权重的效果不是独立的——改变滑移惩罚的权重会连带影响速度跟踪的行为。系统化的方法是先用物理直觉确定权重的数量级,再用消融实验验证每项的必要性,最后用小范围扫描微调关键项 - 正确思维:奖励工程是一门实验科学,必须有系统的记录、控制变量和可复现的实验

练习

  1. ⭐ 用 Python 实现滑移率公式 (78.17),输入是轮速 \(\omega\)、轮半径 \(r\)、接触点速度 \(v_{contact}\)。测试边界情况:\(\omega = 0, v = 0\) 时返回什么?
  2. ⭐⭐ 进行一次完整的消融实验:关闭滑移惩罚训练 1M 步,与完整奖励的基线对比。记录速度跟踪误差、平均滑移率和摔倒率。
  3. ⭐⭐⭐ (跨章综合题)结合 复合/70_轮足混合MPC 中的模式切换逻辑,设计一个奖励项来鼓励"在平地上优先滚动、在台阶前自动切换为行走"。写出数学公式并解释为什么这种设计能起作用。

78.6 PPO 训练细节:网络、超参数与曲线诊断 ⭐⭐⭐

动机:PPO 是手段,不是目标

PPO(Proximal Policy Optimization)是当前足式/轮足 RL 中使用最广泛的算法——不是因为它理论上最优,而是因为它在"训练稳定性"和"样本效率"之间取得了最佳的工程折中。理解 PPO 的超参数如何影响训练结果,是调试轮足 RL 的核心技能。

回顾 足式/190_腿足RL训练栈 中的 PPO 基础:PPO 的核心思想是用 clipped surrogate objective 限制策略更新步长,防止一次更新破坏已学到的好行为。

\[ L^{CLIP}(\theta) = \mathbb{E}_t \left[ \min\left( r_t(\theta) \hat{A}_t, \; \text{clip}(r_t(\theta), 1-\epsilon, 1+\epsilon) \hat{A}_t \right) \right] \tag{78.22} \]

其中 \(r_t(\theta) = \pi_\theta(a_t|s_t) / \pi_{\theta_{old}}(a_t|s_t)\) 是新旧策略的概率比,\(\hat{A}_t\) 是优势函数估计,\(\epsilon\) 是 clipping 范围。

网络架构

轮足 RL 使用的网络架构通常是简单的 MLP(多层感知机),而不是更复杂的 Transformer 或 GNN。原因是:

  1. 推理延迟约束:部署时策略需要在 1-2 ms 内完成推理,MLP 的推理时间可以轻松控制在 0.1 ms 以内
  2. 观测维度不大:即使加上历史窗口,观测维度也不超过 200-300,MLP 完全可以处理
  3. 训练稳定性:复杂架构在 RL 训练中更容易出现梯度问题

典型的网络架构如下:

组件 架构 参数
Actor(策略网络) MLP: \(n_{obs}\) → 512 → 256 → 128 → \(n_{act}\) 激活函数:ELU
Critic(价值网络) MLP: \(n_{obs}\) → 512 → 256 → 128 → 1 激活函数:ELU
隐变量编码器(可选) TCN/MLP: \(n_{hist} \times n_{obs}\) → 64 → 32 只在学生网络使用

为什么用 ELU 而不是 ReLU? ReLU 在负半轴梯度为零,可能导致神经元"死亡"——一旦某个神经元的输入长期为负,它再也不会被激活。ELU 在负半轴有一个小的非零梯度 \(\alpha(e^x - 1)\),避免了这个问题。对于 RL 这种训练信号噪声很大的场景,ELU 的稳定性优势更加明显。

关键超参数

超参数 典型值 物理意义 调参方向
learning_rate 3e-4 梯度步长 训练初期奖励不涨 → 增大;训练后期振荡 → 减小
\(\epsilon\) (clip range) 0.2 策略更新幅度上限 策略更新过激 → 减小;更新太慢 → 增大
\(\gamma\) (discount) 0.99 未来奖励的衰减率 策略太短视 → 增大(至 0.999);训练不稳定 → 减小
\(\lambda\) (GAE) 0.95 bias-variance 权衡 高方差 → 减小;高偏差 → 增大
mini_batch_size 4096 每次梯度更新的样本数 梯度估计方差大 → 增大
num_epochs 5 每批数据重复训练次数 过拟合当前数据 → 减小
entropy_coef 0.01 鼓励探索的力度 策略过早收敛 → 增大;策略混乱 → 减小
max_grad_norm 1.0 梯度裁剪阈值 训练 NaN → 减小

训练曲线诊断

训练曲线是诊断训练问题的最重要工具。以下是常见曲线模式及其含义:

曲线模式 可能原因 处理方法
奖励持续上升但缓慢 学习率过低或 \(\epsilon\) 过小 增大 lr 或 \(\epsilon\)
奖励快速上升后突然崩塌 策略更新过激,破坏了好行为 减小 lr 和 \(\epsilon\)
奖励振荡不收敛 奖励分项冲突或 GAE \(\lambda\) 不当 检查奖励消融,调整 \(\lambda\)
奖励卡在某个值不动 策略陷入局部最优(如"站着不动") 增大 entropy_coef,调整课程
Policy loss 急剧增大 梯度爆炸 减小 max_grad_norm 和 lr
KL divergence 持续 > 0.1 策略更新步长过大 减小 \(\epsilon\) 或使用自适应 KL

🧠 思维陷阱:只看总奖励曲线 - 新手想法:总奖励在涨就说明训练在进步 - 实际上:总奖励 = 任务奖励 + 各种惩罚的加和。可能出现"速度跟踪奖励在涨但滑移惩罚也在涨"的情况——策略在学会跑得更快的同时也在学会取巧。更危险的是"姿态惩罚在降(姿态更稳)但能耗惩罚在涨(策略变得更费电)"——总奖励看起来持平,实际上策略特性在变 - 正确思维:永远按分项看奖励曲线。如果分项有 12 个,就画 12 条曲线

⚠️ 常见陷阱

⚠️ 编程陷阱:observation normalization 的 running stats 在多 GPU 间不同步 - 错误做法:每个 GPU worker 独立更新自己的 running mean/std - 现象:不同 worker 的策略行为差异越来越大,训练效率下降 - 根本原因:观测归一化的统计量应该在所有 worker 间共享 - 正确做法:使用 torch.distributed.all_reduce 在每个 epoch 同步统计量

💡 概念误区:认为更大的网络一定学得更好 - 新手想法:512-512-512 比 256-128 好 - 实际上:过大的网络在 RL 中容易过拟合当前的 replay buffer。RL 的数据分布是非稳态的(策略在变,采集到的数据分布也在变),大网络的记忆能力反而可能固化旧的(错误的)行为模式。实验表明 512-256-128 的递减结构(每层递减一半)是一个鲁棒的选择 - 正确思路:先用小网络(256-128)建立 baseline,只有当确认是表达能力不足时才增大网络

练习

  1. ⭐ 用 WandB 或 TensorBoard 画出一次完整训练的分项奖励曲线(至少 5 项),标注训练的三个阶段:探索期、快速提升期、收敛期。
  2. ⭐⭐ 做一个 learning rate 扫描实验:lr \(\in \{1e-4, 3e-4, 1e-3, 3e-3\}\),训练 2000 epoch,比较训练曲线的收敛速度和稳定性。
  3. ⭐⭐ 解释为什么 \(\gamma = 0.99\) 对应的有效时间视野是约 100 步(提示:\(\gamma^{100} \approx 0.366\))。如果控制频率是 50 Hz,这对应多长的物理时间?

78.7 Teacher-Student 蒸馏:从特权到可部署 ⭐⭐⭐

动机:信息不对称的优雅解决方案

上一节训练出的策略看到了特权信息(地形高度图、真实摩擦系数、接触力等),在仿真中表现优异。但这些信息在真机上不可获取——你不可能在每个轮子下面放一个精确的力传感器和滑移率计。

最直观的解决方案是:从一开始就只用可部署观测训练。但这样做的效果很差——因为策略看不到地形、不知道摩擦、无法感知滑移,它很难学到有效的模式切换策略。没有特权信息,策略只能学到一种保守的"万金油"行为。

Teacher-Student 框架是解决这个矛盾的标准方法。其核心思想是:

  1. Teacher 阶段:用完整的特权信息训练一个"全知"教师策略 \(\pi_T(o_T)\),它能看到一切,因此能学到最优行为
  2. Student 阶段:用行为克隆(Behavior Cloning)训练一个只看可部署观测的学生策略 \(\pi_S(o_S)\),让它模仿教师的动作

本质洞察:Teacher-Student 蒸馏的本质**不是**"用大模型蒸馏小模型"(这是 NLP 中蒸馏的含义),而是**用完整信息蒸馏受限信息**。教师和学生的网络大小可以完全相同——区别只在于输入的信息量。学生必须学会从有限的历史观测中推断出教师直接看到的特权信息。

两阶段训练流程

阶段一:训练教师策略

教师策略的观测包含所有可部署观测加上特权信息:

\[ o_T = [o_{deploy}, o_{priv}] \tag{78.23} \]

用标准 PPO 训练到收敛。教师策略的性能是学生的性能上界——学生不可能比教师做得更好(因为学生看到的信息是教师的子集)。

阶段二:蒸馏学生策略

学生策略只看可部署观测加历史窗口:

\[ o_S = [o_{deploy,t-k}, \ldots, o_{deploy,t}] \tag{78.24} \]

蒸馏损失有两种形式:

动作蒸馏:直接模仿教师的动作输出。

\[ L_{BC} = \mathbb{E} \left[ \|\pi_S(o_S) - \pi_T(o_T)\|^2 \right] \tag{78.25} \]

这是最简单的形式,但存在一个问题:如果教师的动作分布是多模态的(比如在某种情况下既可以走也可以滚),L2 损失会让学生学到两种模式的平均,而不是任何一种模式。

隐变量蒸馏:教师网络提取一个隐变量 \(z_T = \text{encoder}_T(o_{priv})\),学生网络学习从历史观测中预测这个隐变量。

\[ L_z = \mathbb{E} \left[ \|z_S - z_T\|^2 \right] + \beta \cdot L_{BC} \tag{78.26} \]

其中 \(z_S = \text{encoder}_S(o_{deploy,t-k:t})\)。这种方式的优势是:学生不是在模仿教师的具体动作,而是在学习**推断教师所依赖的环境状态**。一旦学生能准确推断环境状态,即使教师的动作策略是多模态的,学生也能做出正确的选择。

Lee et al. (Science Robotics 2024) 在 Swiss-Mile 的工作中使用了这种隐变量蒸馏方法,学生策略通过观测历史学习推断地形类型和摩擦特性,在苏黎世和塞维利亚的城市环境中完成了公里级别的自主导航。

蒸馏训练的实践细节

# 蒸馏训练伪代码
teacher = load_pretrained_teacher("teacher_checkpoint.pt")
teacher.eval()  # 冻结教师

student = StudentPolicy(obs_dim=53, history_len=5, latent_dim=32, act_dim=16)
optimizer = torch.optim.Adam(student.parameters(), lr=1e-3)

for epoch in range(num_epochs):
    # 在环境中用教师采集数据
    obs_full, obs_deploy, actions_teacher = collect_rollout(teacher, env)

    # 教师的隐变量
    z_teacher = teacher.extract_latent(obs_full[:, 53:])  # 特权信息部分

    # 学生的隐变量(从历史推断)
    z_student = student.history_encoder(obs_deploy_history)

    # 学生的动作
    actions_student = student.actor(obs_deploy[:, :53], z_student)

    # 损失:隐变量重建 + 动作模仿
    loss_z = F.mse_loss(z_student, z_teacher.detach())
    loss_bc = F.mse_loss(actions_student, actions_teacher.detach())
    loss = loss_z + 0.5 * loss_bc

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

蒸馏失败的两种模式及其诊断

失败模式 症状 原因 解决方法
表达能力不足 \(L_z\)\(L_{BC}\) 都不降 学生网络太小或历史太短 增大隐变量维度或历史长度
信息不可观测 \(L_z\) 降了但 \(L_{BC}\) 不降 某些特权信息从历史中确实推断不出来 检查哪些特权信息对动作影响最大,考虑增加传感器

⚠️ 常见陷阱

⚠️ 编程陷阱:蒸馏时用学生策略采集数据 - 错误做法:让学生策略与环境交互,然后用教师的动作作为标签 - 现象:蒸馏早期效果好,后期性能下降 - 根本原因:学生采集的状态分布与教师不同。学生犯错后进入教师从未见过的状态,教师的标签不再有意义(Distribution Shift) - 正确做法:用教师策略采集数据(DAgger 方式),或者在蒸馏过程中混合使用教师和学生采集的数据

💡 概念误区:认为蒸馏后学生一定比教师差 - 新手想法:学生看到的信息少,一定不如教师 - 实际上:在某些情况下学生可能**在真机上**表现得比教师在**仿真中**更好。原因是教师可能过拟合仿真中的某些特权信息(如精确的接触力),而学生被迫学习更鲁棒的特征(如从关节反馈推断接触状态) - 正确思路:评估学生和教师的性能时,应在相同的评估环境中对比,并同时评估 sim 和 real 的表现

练习

  1. ⭐ 画出 Teacher-Student 训练流程的数据流图,标注每个模块的输入/输出维度。
  2. ⭐⭐ 设计一个实验来区分蒸馏失败的两种模式:先测量 \(L_z\)\(L_{BC}\) 的收敛情况,然后分别尝试增大网络和增加传感器,看哪种改善更大。
  3. ⭐⭐⭐ 如果蒸馏中加入在线 RL 微调(student 在蒸馏的同时也接收环境奖励),会有什么好处和风险?这种方法叫什么?(提示:参考 clearlab-sustech 的 CTS 方法)

78.8 Domain Randomization:覆盖真实世界的参数分布 ⭐⭐⭐

动机:为什么仿真中训练好的策略到真机就"废了"

即使仿真物理引擎再精确,仿真和真实之间总存在差异(Sim-to-Real Gap)。这些差异来自四个维度:

维度 具体来源 影响
动力学参数 质量分布、关节摩擦、接触弹性 力矩响应不同
执行器特性 电机延迟、力矩饱和、齿轮间隙 动作执行偏差
传感器噪声 IMU 偏置/漂移、编码器量化、轮速计误差 观测不准确
接触模型 地面摩擦、轮胎弹性、接触几何 滑移行为差异

Domain Randomization 的核心思想是:在训练时随机化这些参数,迫使策略学会在参数变化范围内都能工作的鲁棒行为

本质洞察:Domain Randomization 不是**让仿真更接近真实——它不关心真实世界的参数具体是多少。它是让策略对参数变化**不敏感。如果策略在摩擦系数 \(\mu \in [0.3, 1.5]\) 的范围内都能工作,那么真实世界的 \(\mu = 0.8\)(或者任何在这个范围内的值)自然就能应对。这就是为什么 DR 的关键不是"精确匹配真实参数",而是"让随机化范围覆盖真实参数"。

参数随机化表

参数 默认值 随机化范围 分布 理由
基座质量 \(m_0\) \([0.8m_0, 1.2m_0]\) 均匀 负载变化
质心偏移 \((0,0,0)\) \(\pm 0.05\) m 各轴 均匀 负载不对称
关节摩擦 \(f_0\) \([0.5f_0, 2.0f_0]\) 均匀 磨损和温度
地面摩擦 \(\mu\) 1.0 \([0.3, 1.5]\) 均匀 不同地面材质
轮半径 \(r_0\) \([0.95r_0, 1.05r_0]\) 均匀 磨损和充气量
电机力矩偏置 0 \(\pm 0.5\) Nm 高斯 标定误差
执行器延迟 0 步 \(\{0, 1, 2, 3\}\) 离散均匀 通信和计算延迟
IMU 噪声 0 \(\sigma = 0.05\) rad/s 高斯 传感器噪声
编码器噪声 0 \(\sigma = 0.01\) rad 高斯 量化误差
重力方向偏移 \((0,0,-g)\) \(\pm 0.5°\) 各轴 高斯 IMU 标定误差
滚动阻力 \(c_r = 0.01\) \([0.005, 0.03]\) 均匀 轮胎和地面材质
PD 增益 \((K_p, K_d)\) \([0.8, 1.2] \times\) 标称值 均匀 执行器一致性

执行器延迟的随机化:这是最容易被忽略但对 Sim-to-Real 影响最大的参数。真机的控制链路通常有 1-3 个时步(5-15 ms)的延迟(来自通信、传感器处理和计算时间)。如果仿真中不模拟这个延迟,策略会学到一种"即时响应"的行为——它依赖于"发出命令后下一个时步立刻看到效果"。到真机上延迟一出现,策略的闭环就断了。

# 延迟随机化实现
class ActionDelayBuffer:
    def __init__(self, num_envs, act_dim, max_delay=3):
        self.buffer = torch.zeros(num_envs, max_delay + 1, act_dim)
        # 每个环境的延迟步数独立随机
        self.delays = torch.randint(0, max_delay + 1, (num_envs,))

    def apply(self, action):
        """将当前动作存入缓冲区,返回延迟后的动作"""
        self.buffer = torch.roll(self.buffer, 1, dims=1)
        self.buffer[:, 0] = action
        # 每个环境取对应延迟的历史动作
        delayed = self.buffer[torch.arange(len(self.delays)), self.delays]
        return delayed

课程训练:从易到难的渐进随机化

如果一开始就用最大范围的随机化,策略在训练早期会完全无法学习——环境变化太大,任何行为都得不到稳定的奖励信号。课程训练的思想是从小范围开始,随着策略能力增强逐步扩大:

\[ p_{range}(epoch) = p_{min} + (p_{max} - p_{min}) \cdot \min\left(1, \frac{epoch}{epoch_{full}}\right) \tag{78.27} \]

其中 \(epoch_{full}\) 是达到完整随机化范围的 epoch 数(通常设为总训练 epoch 的 30%-50%)。

如果随机化范围太宽会怎样? 策略会变得过于保守——它学会了一种在"最差情况"下仍然安全的行为。例如,如果摩擦系数的范围包含了 \(\mu = 0.1\)(冰面),策略可能学会永远低速、小步行走,即使在高摩擦地面上也不敢高速滚动。这就是鲁棒性和性能的权衡——随机化范围越宽,策略越鲁棒但性能越保守。

⚠️ 常见陷阱

⚠️ 编程陷阱:随机化参数在 episode 内不变但 episode 间不重新采样 - 错误做法:在环境初始化时采样一次参数,之后不再改变 - 现象:每个环境实例只见过一组参数,策略学到的是针对特定参数的行为 - 根本原因:应该在每次环境 reset 时重新采样参数 - 正确做法:在 reset() 函数中对每个环境独立重新采样所有随机化参数

💡 概念误区:认为 Domain Randomization 可以替代准确的物理建模 - 新手想法:反正要随机化,物理引擎精不精确无所谓 - 实际上:DR 只能处理参数级别的不确定性("摩擦系数是多少"),不能处理模型结构级别的差异("接触力的计算方式完全不同")。如果仿真器的接触模型是刚体碰撞,但真实世界是柔性接触,即使摩擦系数被随机化覆盖了,接触力的时间特性仍然完全不同 - 正确思路:先确保仿真器的物理模型结构合理,再用 DR 覆盖参数不确定性

练习

  1. ⭐ 解释为什么轮半径的随机化范围只有 \(\pm 5\%\) 而不是更大。提示:轮半径误差会直接导致里程计偏置。
  2. ⭐⭐ 设计一个实验来确定执行器延迟随机化的最优范围:分别用 \(\{0\}\)\(\{0,1\}\)\(\{0,1,2\}\)\(\{0,1,2,3\}\) 步训练,然后在固定 2 步延迟的评估环境中测试。画出性能-鲁棒性的权衡曲线。
  3. ⭐⭐⭐ 如果你有真机的传感器数据(1000 步的 IMU 和编码器记录),如何用它来校准 DR 的参数范围?描述一个系统化的方法。

78.9 Sim-to-Real 部署:从仿真到实机 ⭐⭐⭐

动机:部署不是"把模型拷贝到机器人上"

训练完成后,你有一个 PyTorch 模型和一组经过验证的权重。但从这个模型到真机上可靠运行的策略,中间还有一系列工程步骤——每一步都可能引入错误。

ONNX 导出

PyTorch 模型不能直接在嵌入式平台上运行(大多数机器人控制器没有 Python 环境和 CUDA)。需要先导出为 ONNX 格式,再用 ONNX Runtime 或 TensorRT 在 C++ 环境中推理。

导出时必须确保:

  1. 观测归一化参数被冻结\(\mu\)\(\sigma\) 作为常量嵌入模型
  2. 动作后处理被包含:低通滤波、限幅、缩放都在模型内部完成
  3. 数值精度一致:训练用 float32,导出也必须用 float32(不要为了推理速度换 float16,精度损失可能导致策略行为变化)
def export_to_onnx(model, obs_dim, output_path):
    """导出策略到 ONNX"""
    model.eval()
    dummy_input = torch.randn(1, obs_dim)

    torch.onnx.export(
        model,
        dummy_input,
        output_path,
        input_names=["observation"],
        output_names=["action"],
        opset_version=11,
        do_constant_folding=True,
    )

    # 导出后数值对齐验证
    import onnxruntime as ort
    session = ort.InferenceSession(output_path)

    # 用 100 组随机输入验证
    for _ in range(100):
        test_input = torch.randn(1, obs_dim)
        pytorch_output = model(test_input).detach().numpy()
        onnx_output = session.run(None, {"observation": test_input.numpy()})[0]
        max_diff = np.abs(pytorch_output - onnx_output).max()
        assert max_diff < 1e-5, f"数值不一致: max_diff={max_diff}"

推理延迟优化

在控制频率为 50-200 Hz 的轮足机器人上,每个控制周期只有 5-20 ms。策略推理必须在这个时间内完成。

平台 推理时间(512-256-128 MLP) 备注
NVIDIA Jetson Orin ~0.3 ms GPU 推理
Intel NUC i7 ~0.5 ms CPU,ONNX Runtime
Raspberry Pi 4 ~2.0 ms CPU,需要量化

推理延迟远小于控制周期,通常不是瓶颈。但要注意:推理延迟的**方差**比**均值**更重要——偶尔的延迟尖峰(由操作系统调度、内存分配等引起)可能导致控制周期被跳过。

实机首测的灰度部署流程

绝对不要一上来就让机器人在复杂地形上全速奔跑。正确的首测流程是:

阶段 环境 速度 检查项
1. 悬空测试 机器人被吊起,腿脚悬空 关节运动方向和幅度是否合理
2. 台架测试 机器人站在台架上,有外部支撑 0 站姿是否稳定,轮子是否空转
3. 平地低速 平坦地面 0.3 m/s 能否直线前进,转弯是否正常
4. 平地中速 平坦地面 1.0 m/s 速度跟踪精度,能耗是否合理
5. 简单障碍 低矮台阶(3-5 cm) 0.5 m/s 模式切换是否平滑
6. 复杂地形 台阶、坡面、门槛 自适应 综合性能评估

每个阶段都必须有人手持急停开关,随时准备断电保护。

⚠️ 常见陷阱

⚠️ 编程陷阱:训练端和部署端的关节顺序不一致 - 错误做法:训练时关节顺序是 [FR_hip, FR_thigh, FR_calf, FL_hip, ...],部署时 SDK 返回 [FL_hip, FL_thigh, FL_calf, FR_hip, ...] - 现象:机器人前腿和后腿动作互换,或者左右腿互换——立刻摔倒 - 根本原因:不同的 SDK 和仿真器对关节的编号顺序不同 - 正确做法:建立一个关节映射表,在训练和部署两端都显式定义关节顺序,用一个单元测试验证映射正确

💡 概念误区:认为 sim 中表现好 = real 中表现好 - 新手想法:sim 中 reward 很高,部署应该没问题 - 实际上:sim 中最后 10% 的性能提升往往来自过拟合仿真器——比如利用了仿真器特有的接触弹跳模型,或者依赖了精确到不切实际的传感器信息。sim 中 reward 从 95% 提升到 100% 的那 5%,在 real 中可能完全无法复现 - 正确思路:用一组"sim-to-real 友好度"指标来评估策略质量——包括 DR 环境中的性能(而不只是标称环境)、动作平滑度、安全壳触发率等

练习

  1. ⭐ 完成一次 ONNX 导出和数值对齐验证。记录导出的模型文件大小和推理延迟。
  2. ⭐⭐ 设计一套完整的灰度部署检查清单(checklist),至少包含 15 个检查项,覆盖硬件、软件、通信和安全。
  3. ⭐⭐ 描述一个你见过或读过的 Sim-to-Real 部署失败案例,分析失败的根本原因属于上述四个维度(动力学/执行器/传感器/接触)中的哪一个。

78.10 RL+MPC 混合架构:两种范式的最佳组合 ⭐⭐⭐

动机:RL 和 MPC 不是竞争关系

到目前为止,我们讨论的是"纯 RL"方案——策略直接输出关节命令。但在工业实践中,越来越多的系统采用 RL+MPC 的混合架构。为什么?

纯 RL 的优势是学习能力强,可以处理模型不确定性和复杂的模式切换。纯 MPC 的优势是约束处理能力强,可以精确保证安全边界。两者的短板恰好互补:

能力 纯 RL 纯 MPC RL+MPC
模式切换 强(隐式学习) 弱(需显式规则)
约束满足 弱(软惩罚) 强(硬约束)
模型依赖 无(model-free) 强(需精确模型)
可解释性
安全保证

混合架构的两种模式

模式 A:RL 底层 + MPC 上层

MPC 在上层做轨迹规划(输出目标速度、步态参数),RL 策略在底层做运动控制(输出关节命令)。这种模式中 RL 替代了传统的 WBC(全身控制器)。

\[ v_{cmd}, \text{gait\_params} = \text{MPC}(x_{state}, x_{goal}) \tag{78.28} $$ $$ a_t = \pi_\theta(o_t, v_{cmd}, \text{gait\_params}) \tag{78.29} \]

模式 B:MPC 底层安全壳 + RL 上层决策

RL 策略在上层输出"意图"(如期望的运动方向和速度),MPC 在底层将这个意图转化为满足安全约束的关节命令。

\[ \text{intent} = \pi_\theta(o_t) \tag{78.30} $$ $$ a_t = \text{MPC}(\text{intent}, \text{constraints}) \tag{78.31} \]

Lee et al. (Science Robotics 2024) 的 Swiss-Mile 工作采用的是接近模式 A 的方案:RL 策略负责底层的运动控制和模式切换,上层的导航规划负责提供目标速度和航向。

接口设计

RL 和 MPC 之间的接口设计是混合架构的核心。接口不当会导致两个模块互相对抗——MPC 试图修正 RL 的决策,RL 试图绕过 MPC 的约束。

好的接口应满足:

  1. 语义清晰:每个接口变量有明确的物理意义和量纲
  2. 频率匹配:MPC 和 RL 运行频率不同时,需要插值或保持器
  3. 安全边界:接口变量有合理的范围限制

本质洞察:RL+MPC 混合架构的本质是**责任分工**。RL 负责"学习做什么"(what to do),MPC 负责"保证怎么做"(how to do it safely)。这种分工让每个模块做自己最擅长的事——RL 不需要学习硬约束,MPC 不需要处理模式切换。

⚠️ 常见陷阱

🧠 思维陷阱:认为混合架构总是比纯 RL 或纯 MPC 好 - 新手想法:两者结合肯定优于单独使用 - 实际上:混合架构引入了新的工程复杂度——两个模块的调参空间叠加,调试时需要区分问题来自 RL 还是 MPC。如果应用场景相对简单(如平地移动),纯 MPC 可能更合适;如果约束不重要(如仿真中的实验),纯 RL 更简单高效 - 正确思维:根据应用需求选择架构——安全关键场景用混合架构,研究探索用纯 RL,约束明确的场景用纯 MPC

练习

  1. ⭐ 画出 RL+MPC 混合架构的模式 A 和模式 B 的系统框图,标注每个模块的输入/输出和运行频率。
  2. ⭐⭐ 讨论:如果 RL 策略输出的速度命令超出了 MPC 的可行域,应该怎么处理?设计一个"软约束"接口方案。
  3. ⭐⭐⭐ (跨章综合题)结合 足式/190_腿足RL训练栈 和 复合/70_轮足混合MPC 的知识,设计一个完整的 RL+MPC 轮足控制系统的训练和部署方案。指定 RL 和 MPC 各自负责什么,接口变量有哪些,如何处理频率不匹配。

78.11 完整训练配置参考与调参策略 ⭐⭐

动机:配置文件是训练的"DNA"

一次成功的训练依赖于数十个配置参数的协同。改变其中一个参数(如学习率)可能需要相应调整其他参数(如 clip range 和 batch size)。本节给出一份经过实践验证的完整配置参考,并解释参数之间的耦合关系。

完整训练配置

class WheelLeggedTrainCfg:
    """轮足 RL 训练的完整配置"""

    # === 环境参数 ===
    num_envs = 4096            # GPU 并行环境数,A100 可以上 8192
    episode_length_s = 20.0    # 每个 episode 的时长(秒)
    dt = 0.02                  # 仿真步长(s),对应 50 Hz 控制频率
    decimation = 4             # 物理子步数:实际物理步长 = dt/decimation = 5ms

    # === 观测配置 ===
    obs_scales = {
        "lin_vel": 2.0,        # 线速度缩放
        "ang_vel": 0.25,       # 角速度缩放
        "dof_pos": 1.0,        # 关节位置缩放
        "dof_vel": 0.05,       # 关节速度缩放(数值大,需缩小)
        "height_measurements": 5.0,  # 高度测量缩放
    }
    clip_observations = 100.0  # 观测裁剪范围

    # === 动作配置 ===
    leg_action_scale = 0.4     # 腿关节动作缩放(rad)
    wheel_action_scale = 15.0  # 轮速动作缩放(rad/s)
    action_filter_alpha = 0.6  # 低通滤波系数

    # === 奖励配置 ===
    rewards = {
        "tracking_lin_vel":  {"weight": 1.5,  "sigma": 0.25},
        "tracking_ang_vel":  {"weight": 0.5,  "sigma": 0.25},
        "orientation":       {"weight": -5.0},
        "base_height":       {"weight": -1.0, "target": 0.52},
        "slip_penalty":      {"weight": -0.1},
        "energy":            {"weight": -0.005},
        "action_rate":       {"weight": -0.01},
        "action_jerk":       {"weight": -0.001},
        "joint_limit":       {"weight": -10.0, "margin": 0.1},
        "collision":         {"weight": -5.0},
        "feet_air_time":     {"weight": 0.1,  "threshold": 0.5},
    }

    # === PPO 配置 ===
    learning_rate = 3e-4
    clip_param = 0.2
    gamma = 0.99
    lam = 0.95                 # GAE lambda
    num_mini_batches = 4
    num_learning_epochs = 5
    entropy_coef = 0.01
    value_loss_coef = 1.0
    max_grad_norm = 1.0
    use_clipped_value_loss = True

    # === 网络配置 ===
    policy_hidden_dims = [512, 256, 128]
    value_hidden_dims = [512, 256, 128]
    activation = "elu"
    init_noise_std = 1.0       # 初始探索噪声标准差

    # === Domain Randomization ===
    randomize_friction = True
    friction_range = [0.3, 1.5]
    randomize_base_mass = True
    added_mass_range = [-3.0, 5.0]  # kg
    push_robots = True
    push_interval_s = 15       # 每 15 秒随机推一下
    max_push_vel = 1.0         # 最大推力产生的速度变化(m/s)
    randomize_motor_strength = True
    motor_strength_range = [0.8, 1.2]

    # === 课程训练 ===
    curriculum = True
    terrain_curriculum = True
    max_terrain_level = 7
    terrain_proportions = [0.2, 0.2, 0.15, 0.15, 0.1, 0.1, 0.05, 0.05]

参数耦合关系

以下参数之间存在强耦合,调整一个时必须考虑其他:

参数 A 参数 B 耦合关系 调参建议
learning_rate clip_param lr 增大时 clip 应减小 保持 lr/clip ≈ 1.5e-3
num_envs mini_batch_size 总批量 = num_envs × episode_steps / num_mini_batches 总批量应 > 10000
action_scale reward weights 缩放改变后奖励的量级会变 改缩放后检查奖励分项量级
dt obs_history_length dt 变小则历史窗口对应更短的物理时间 保持历史窗口≈25-50ms
friction_range slip_penalty 摩擦范围太宽时滑移惩罚需加大 DR 扩大后检查滑移率
episode_length gamma 长 episode 需要较大的 gamma episode 20s → gamma ≥ 0.99

系统化调参流程

调参不应该是随机搜索。以下是一个经过实践验证的系统化流程:

第一轮:基线建立(2-4 小时) 1. 用上述默认配置训练 2000 epoch 2. 检查:策略是否学会了基本的前进运动?奖励是否在上升? 3. 如果完全不学习 → 检查观测归一化和奖励量级

第二轮:奖励调优(4-8 小时) 1. 做完整的奖励消融实验(逐项关闭) 2. 确认每项奖励的必要性 3. 调整权重使分项奖励的量级在同一数量级

第三轮:DR 调优(4-8 小时) 1. 从小范围 DR 开始训练到收敛 2. 逐步扩大 DR 范围(公式 78.27),观察性能下降 3. 找到"性能可接受的最大 DR 范围"

第四轮:精细调参(2-4 小时) 1. 在最终 DR 范围下做 lr 和 clip 的小范围扫描 2. 做 entropy_coef 和 init_noise_std 的扫描 3. 选择最佳组合做最终训练

本质洞察:调参的目标**不是**找到"全局最优参数组合"——这在 RL 的随机性下是不可能的。目标是找到一个**鲁棒的参数区域**——在这个区域内,参数的小幅扰动不会导致训练失败。如果你找到了一组"刚好能工作"的参数,那说明你站在悬崖边——生产环境中任何小的变化都可能让训练崩溃。

⚠️ 常见陷阱

⚠️ 编程陷阱:修改了 dt 但忘记相应调整 action_filter_alpha - 错误做法:把 dt 从 0.02 改为 0.01(控制频率翻倍),但 alpha 保持 0.6 - 现象:动作变得更"抖",因为滤波器的截止频率随 dt 变化 - 根本原因:低通滤波的等效截止频率 \(f_c = -\frac{\ln(1-\alpha)}{2\pi \cdot dt}\),dt 减半时 \(f_c\) 翻倍 - 正确做法:按 \(\alpha_{new} = 1 - (1-\alpha_{old})^{dt_{new}/dt_{old}}\) 调整

💡 概念误区:认为训练时间越长越好 - 新手想法:多训练一些总没坏处 - 实际上:RL 训练存在过拟合——不是对数据集的过拟合,而是对当前 DR 分布的过拟合。长时间训练后策略可能学会了"在 DR 分布边界上的最优行为",这种行为在真机上(真机参数在 DR 分布内部的某个点)可能不是最优的 - 正确思路:用 DR 环境中的评估性能(而不是标称环境)来决定何时停止训练

练习

  1. ⭐ 在上述配置中,计算每个 PPO epoch 使用的总样本数:\(N = \text{num\_envs} \times \text{episode\_steps} \times \text{num\_learning\_epochs} / \text{num\_mini\_batches}\)
  2. ⭐⭐ 修改配置中的 dt(从 0.02 变为 0.01),列出所有需要相应调整的参数及其新值。
  3. ⭐⭐ 设计一个自动化的超参数搜索脚本:选择 3 个最关键的超参数,各取 3 个值,用网格搜索训练 27 组实验,选出最佳组合。

78.12 轮足 RL 的前沿进展与开放问题 ⭐⭐⭐⭐

动机:知道前沿在哪里才能找到研究方向

轮足 RL 虽然在工程上取得了显著进展(Swiss-Mile 的城市部署就是最好的证明),但仍有很多开放问题尚未解决。理解这些问题的边界,对于选择研究方向和设计实验至关重要。

前沿方向一:Concurrent Teacher-Student 训练

传统的 Teacher-Student 是两阶段的:先训练教师到收敛,再蒸馏学生。这有一个问题:教师学到的行为可能超出学生的表达能力——教师可以依赖某些特权信息做出精细决策,但学生无论如何也无法从历史观测中推断出这些信息。结果是蒸馏后的学生性能与教师有显著差距。

clearlab-sustech 提出的 CTS (Concurrent Teacher-Student) 方法(2024)解决了这个问题:教师和学生在同一个训练循环中同时训练。教师和学生共享相同的 Critic 网络和策略网络结构,同时在环境中采集数据。教师看到的数据分布和学生不同,但优化的目标函数是相同的。

这种方法的优势在于:教师在学习过程中就"知道"学生的能力边界——如果某个行为需要学生永远无法获取的信息,教师在训练过程中就会发现这种行为对 Critic 的价值贡献不高(因为学生无法复现),从而自动避免学习这种行为。

前沿方向二:基于感知的运动控制

目前大多数轮足 RL 工作使用本体感知(IMU + 编码器)作为学生策略的输入。但最新的研究开始将深度相机或 LiDAR 点云直接输入策略网络,实现视觉引导的运动控制。

这种方法的挑战在于:

  1. 观测维度爆炸:一帧深度图有数万个像素,直接输入 MLP 不可行
  2. sim-to-real gap 更大:视觉域的 sim-to-real gap 远大于本体感知——仿真渲染的纹理、光照、阴影都与真实不同
  3. 端到端 vs 模块化:是让策略直接从像素学习运动,还是先用传统方法提取地形特征再输入策略?

CoRL 2025 的 Omni-Perception 方法展示了直接处理 LiDAR 点云的端到端腿足运动控制,在 Isaac Gym 中实现了大规模并行训练和高效 sim-to-real 迁移。

前沿方向三:安全约束的 RL

标准 PPO 通过奖励中的惩罚项来约束不安全行为,但这是"软约束"——无法保证策略永远不会违反安全边界。在商业部署中,"几乎不会违反"和"绝对不会违反"之间的差距可能是生死攸关的。

Constrained RL(如 CPO, PPO-Lagrangian)将安全约束作为优化问题的硬约束:

\[ \max_\theta \; \mathbb{E}[R(\tau)] \quad \text{s.t.} \quad \mathbb{E}[C_i(\tau)] \leq d_i, \quad \forall i \tag{78.32} \]

其中 \(C_i\) 是第 \(i\) 个约束的代价函数,\(d_i\) 是约束阈值。这比在奖励中加惩罚项更有理论保证,但训练也更困难。

开放问题汇总

问题 现状 挑战 潜在方向
模式切换的可解释性 RL 策略是黑盒 无法证明安全性 可解释 RL、后验分析
极端场景的鲁棒性 DR 范围有限 覆盖不了所有长尾 自适应 DR、真机反馈
多机器人协同 单机策略 通信延迟、碰撞避免 Multi-Agent RL
长时间部署的稳定性 短时间测试 模型退化、传感器漂移 在线自适应
与人类的自然交互 简单避障 理解人类意图 Social-aware RL

⚠️ 常见陷阱

🧠 思维陷阱:认为解决了所有开放问题才能做产品 - 新手想法:这么多未解决的问题,轮足机器人还不能商用 - 实际上:RIVR 已经在街头送外卖了。产品不需要解决所有学术问题——它只需要在特定场景下足够可靠。学术上的"开放问题"在产品中可以通过工程手段绕过(如远程接管替代完全自主)。研究的价值是**降低这些绕过手段的成本** - 正确思维:区分"学术上有趣"和"工程上紧迫"的问题。优先研究后者

练习

  1. ⭐⭐ 选择一个开放问题,写一个 1 页的研究提案:包括研究动机、拟用方法、实验设计和预期结果。
  2. ⭐⭐⭐ 阅读 CTS 论文(clearlab-sustech, 2024),回答:CTS 相比传统两阶段蒸馏在什么场景下优势最大?什么场景下可能不如两阶段方法?
  3. ⭐⭐⭐ 对比 Constrained RL(CPO)和奖励惩罚法在安全约束方面的理论保证。在轮足场景中,哪种方法更实用?为什么?

本章小结

模块 核心问题 应掌握输出
78.1 轮足 RL 难点 额外自由度 + 模式切换 + 滑移 能说清楚为什么不能直接复用足式 RL
78.2 仿真环境 URDF + 地形 + 执行器模型 能搭建可训练的 IsaacLab 环境
78.3 观测空间 可部署 vs 特权 + 历史窗口 能写出完整的观测 schema
78.4 动作空间 腿位置 + 轮速度 + 安全包裹 能设计混合动作空间
78.5 奖励设计 12+ 项逐项分析 + 消融方法 能独立设计并调试奖励函数
78.6 PPO 训练 网络架构 + 超参数 + 曲线诊断 能诊断训练问题并调参
78.7 蒸馏 Teacher-Student + 隐变量重建 能实现特权→可部署的迁移
78.8 Domain Randomization 参数表 + 课程训练 能设计覆盖真机的 DR 方案
78.9 Sim-to-Real ONNX 导出 + 灰度部署 能完成从训练到真机的全流程
78.10 RL+MPC 混合 两种混合模式 + 接口设计 能设计混合架构的责任分工

78.13 Wheel-Legged-Gym 代码走读:从入口到训练循环 ⭐⭐

动机:读懂仓库是独立开发的前提

本节以 clearlab-sustech/Wheel-Legged-Gym 为参照,走读一个典型轮足 RL 训练栈的代码结构。读完本节后你应该能定位任何配置项、修改任何奖励函数、添加新的观测量。

仓库结构

典型的轮足 RL 仓库遵循 legged_gym 的结构模式:

wheel_legged_gym/
├── envs/
│   ├── base/                    # 基类
│   │   ├── legged_robot.py      # 环境基类(step, reset, compute_reward)
│   │   └── legged_robot_config.py  # 配置基类
│   ├── wheel_legged/            # 轮足专用
│   │   ├── wheel_legged_env.py  # 轮足环境(继承基类,添加轮相关逻辑)
│   │   └── wheel_legged_config.py  # 轮足配置
│   └── __init__.py              # 任务注册
├── scripts/
│   ├── train.py                 # 训练入口
│   └── play.py                  # 可视化评估
├── resources/
│   └── robots/
│       └── wheel_legged/
│           └── urdf/            # URDF 模型文件
└── rsl_rl/                      # PPO 算法实现
    ├── algorithms/
    │   └── ppo.py               # PPO 核心
    ├── modules/
    │   └── actor_critic.py      # 策略和价值网络
    └── runners/
        └── on_policy_runner.py  # 训练循环

关键:先找入口,再沿调用链读。 不要从第一个文件开始按顺序读——应该从 train.py 开始,沿着函数调用链一步步深入。

训练入口 → 环境创建 → 训练循环

# train.py 的核心逻辑(简化版)
def train():
    # 1. 解析任务名称 → 找到对应的环境类和配置
    env_cfg, train_cfg = task_registry.get_cfgs(task_name)

    # 2. 创建环境
    env = task_registry.make_env(task_name, env_cfg)
    # 此时 4096 个并行环境已在 GPU 上创建完毕

    # 3. 创建 PPO 训练器
    runner = OnPolicyRunner(env, train_cfg, device="cuda")

    # 4. 训练循环
    runner.learn(num_learning_iterations=5000)

任务注册机制task_registry 是一个全局字典,将任务名称映射到环境类和配置类。当你输入 --task wheel_legged 时,它会查找对应的 WheelLeggedEnvWheelLeggedCfg

# envs/__init__.py 中的注册
task_registry.register(
    "wheel_legged",
    WheelLeggedEnv,
    WheelLeggedCfg(),
    WheelLeggedCfgPPO()
)

环境的核心方法

环境类中最重要的四个方法,每个控制步调用一次:

class WheelLeggedEnv(LeggedRobot):
    def step(self, actions):
        """1. 接收策略输出,执行一步仿真"""
        # 处理动作:分离腿和轮、低通滤波、限幅
        self.actions = actions.clip(-1, 1)
        leg_actions = self.actions[:, :12]
        wheel_actions = self.actions[:, 12:]

        # 计算关节目标
        self.leg_targets = self.default_dof_pos[:, :12] + \
                          self.cfg.control.leg_action_scale * leg_actions
        self.wheel_targets = self.cfg.control.wheel_action_scale * wheel_actions

        # 执行物理仿真(decimation 个子步)
        for _ in range(self.cfg.control.decimation):
            torques = self._compute_torques(self.leg_targets, self.wheel_targets)
            self.gym.set_dof_actuation_force_tensor(self.sim, torques)
            self.gym.simulate(self.sim)

        # 更新观测
        self.obs_buf = self._compute_observations()

        # 计算奖励
        self.rew_buf = self._compute_reward()

        # 检查是否需要重置
        self.reset_buf = self._check_termination()

        return self.obs_buf, self.rew_buf, self.reset_buf, self.extras

    def _compute_observations(self):
        """2. 构建观测向量"""
        obs = torch.cat([
            self.base_ang_vel * self.obs_scales["ang_vel"],
            self.projected_gravity,
            (self.dof_pos[:, :12] - self.default_dof_pos[:, :12]) * self.obs_scales["dof_pos"],
            self.dof_vel[:, :12] * self.obs_scales["dof_vel"],
            self.dof_vel[:, 12:] * self.obs_scales["wheel_vel"],  # 轮速
            self.commands[:, :3],
            self.actions,
        ], dim=-1)

        obs = torch.clip(obs, -self.cfg.normalization.clip_observations,
                              self.cfg.normalization.clip_observations)
        return obs

    def _compute_reward(self):
        """3. 计算各奖励分项并求和"""
        # 速度跟踪
        lin_vel_error = torch.sum(
            torch.square(self.commands[:, :2] - self.base_lin_vel[:, :2]), dim=1)
        r_vel = torch.exp(-lin_vel_error / self.cfg.rewards.tracking_sigma)

        # 滑移惩罚(轮足专有)
        wheel_vel = self.dof_vel[:, 12:]  # 轮关节角速度
        contact_vel = self._get_wheel_contact_velocity()  # 接触点地面速度
        slip = torch.abs(wheel_vel * self.wheel_radius - contact_vel)
        c_slip = torch.sum(slip, dim=-1)

        # ... 其他奖励项 ...

        total_reward = (self.cfg.rewards.tracking_lin_vel * r_vel
                       + self.cfg.rewards.slip_penalty * c_slip
                       + ...)  # 所有分项加权求和

        # 记录分项到 extras(用于 TensorBoard)
        self.extras["rewards/vel_tracking"] = r_vel.mean().item()
        self.extras["rewards/slip"] = c_slip.mean().item()

        return total_reward

    def _check_termination(self):
        """4. 检查终止条件"""
        # 翻倒终止
        body_contact = torch.any(self.contact_forces[:, self.body_indices, :] > 1.0, dim=-1)

        # 超时终止
        timeout = self.episode_length_buf >= self.max_episode_length

        return body_contact.any(dim=-1) | timeout

为什么要读这些代码? 因为当你需要修改训练行为时,必须知道在哪里改。例如: - 想增加一种新的奖励项 → 修改 _compute_reward - 想增加一种新的观测量 → 修改 _compute_observations 和 schema - 想修改动作处理逻辑 → 修改 step 中的动作预处理部分 - 想改变终止条件 → 修改 _check_termination

配置继承机制

轮足配置通常继承自足式基类配置,只覆盖轮足相关的参数:

class WheelLeggedCfg(LeggedRobotCfg):
    class env(LeggedRobotCfg.env):
        num_observations = 53      # 覆盖观测维度
        num_actions = 16           # 覆盖动作维度(12腿 + 4轮)

    class control(LeggedRobotCfg.control):
        leg_action_scale = 0.4     # 新增:腿动作缩放
        wheel_action_scale = 15.0  # 新增:轮速缩放

    class rewards(LeggedRobotCfg.rewards):
        # 新增轮足相关奖励
        slip_penalty = -0.1
        wheel_energy = -0.002

注意:配置继承会隐藏很多默认值。在训练前,务必打印最终展开的配置(所有继承都解析完毕),确认没有遗漏的默认值在影响训练行为。

# 打印最终配置的实用函数
def print_final_config(cfg, prefix=""):
    for key, value in vars(cfg).items():
        if isinstance(value, type):
            print_final_config(value, prefix=f"{prefix}{key}.")
        else:
            print(f"{prefix}{key} = {value}")

⚠️ 常见陷阱

⚠️ 编程陷阱:继承覆盖时只改了配置类,忘了改环境类 - 错误做法:在配置中增加了 num_actions = 16,但环境类的 step 方法仍按 12 维处理动作 - 现象:训练时报 tensor shape 不匹配的错误,或者后 4 维动作被忽略 - 正确做法:配置和环境类必须同步修改。增加轮动作维度时,step 中的动作拆分、_compute_reward 中的能耗计算、_compute_observations 中的 last_action 维度都需要相应更新

💡 概念误区:认为读仓库就是读 README - 新手想法:README 说了怎么用就够了 - 实际上:README 只告诉你怎么跑起来,不告诉你怎么修改和调试。对于研究者来说,"跑起来"只是起点——你需要理解环境的物理行为、奖励的数学形式、配置的继承关系,才能有效地做实验 - 正确思路:按照本节的调用链顺序读代码:train.py → env.step → compute_observations → compute_reward → check_termination → config inheritance

练习

  1. ⭐ 克隆 Wheel-Legged-Gym 仓库,找到 _compute_reward 函数,列出所有奖励项及其默认权重。
  2. ⭐⭐ 在该仓库中新增一个奖励项:惩罚轮速的一阶差分(\(\|w_t - w_{t-1}\|^2\))。训练并对比加入前后的策略行为。
  3. ⭐⭐ 用 print_final_config 打印默认配置的所有参数,找出至少 3 个被继承覆盖的参数,说明覆盖后的值与默认值有什么不同。

累积项目:本章新增模块

项目名称:从零构建轮足 RL 训练-部署闭环

本章新增以下模块:

阶段 任务 交付物
环境搭建 在 IsaacLab 中加载轮足 URDF,配置地形和执行器 可运行的训练环境
奖励设计 实现 12 项奖励函数,完成消融实验 奖励消融报告
训练 PPO 训练到收敛,记录分项奖励曲线 训练日志 + 检查点
蒸馏 完成 Teacher-Student 蒸馏 学生策略检查点
导出 ONNX 导出 + 数值对齐验证 ONNX 文件 + 验证报告

延伸阅读

  • 基础 ⭐⭐ clearlab-sustech/Wheel-Legged-Gym:轮足 RL 训练栈开源实现,入门首选
  • 基础 ⭐⭐ Rudin et al., Learning to Walk in Minutes (2022):GPU 并行腿足 RL 的基石工作
  • 进阶 ⭐⭐⭐ Lee et al., Learning Robust Autonomous Navigation and Locomotion for Wheeled-Legged Robots (Science Robotics, 2024):Swiss-Mile/ETH 的轮足 RL+导航系统,公里级城市部署验证
  • 进阶 ⭐⭐⭐ Chamorro et al., Blind Stair Traversal via RL (2024):轮足/足式盲爬楼梯,关注观测设计和蒸馏
  • 进阶 ⭐⭐⭐ CTS: Concurrent Teacher-Student RL (clearlab-sustech, 2024):教师和学生同时训练的新范式
  • 工具 ⭐⭐ IsaacLab 文档 + rsl_rl 库:现代训练栈的标准工具
  • 工具 ⭐⭐ Distillation-PPO (2025):两阶段 RL 蒸馏框架,用于人形机器人感知运动

🔧 故障排查手册

症状 可能原因 排查步骤 相关章节
训练奖励一直不涨 1. 奖励权重失衡(惩罚太重)
2. 观测未归一化
3. 学习率过低
1. 打印各奖励分项,检查惩罚项是否占主导
2. 打印观测的 mean/std
3. 扫描 lr={1e-4, 3e-4, 1e-3}
§78.5, §78.6
策略只会站着不动 1. 课程太难(初始地形复杂)
2. 碰撞惩罚太重
3. 动作缩放太小
1. 从纯平地开始训练
2. 降低碰撞惩罚权重
3. 增大 action_scale
§78.2, §78.5
仿真中好但真机摔倒 1. DR 范围未覆盖真机参数
2. 观测顺序训练/部署不一致
3. 缺少执行器延迟随机化
1. 测量真机参数,对比 DR 范围
2. 用 schema 交叉验证
3. 加入 1-3 步延迟随机化
§78.8, §78.9
模式切换时力矩跳变 1. 动作滤波不够
2. 奖励缺少平滑项
3. 模式边界处奖励冲突
1. 减小 \(\alpha\)(增强滤波)
2. 增加动作 jerk 惩罚
3. 检查消融实验中边界附近的行为
§78.4, §78.5
ONNX 导出后行为异常 1. 归一化参数未冻结
2. 动作后处理未包含在导出中
3. float32/float16 精度问题
1. 检查导出前是否调用了 model.eval()
2. 对比导出前后的 100 组输出
3. 统一使用 float32
§78.9