跳转至

第 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 完整训练配置参考与调参策略 ⭐⭐

动机:理解配置之间的耦合关系是高效调参的前提

轮足 RL 的训练配置不是一组独立的参数——它是一个互相耦合的系统。修改一个参数常常需要连带修改其他参数。不理解这些耦合关系,调参就变成了低效的随机搜索。

类比:调参就像调吉他弦——调紧一根弦会影响其他弦的张力,导致音准整体偏移。经验丰富的吉他手知道调弦的正确顺序(先调低音弦再调高音弦),并且知道调完一轮后需要回头检查。同样,轮足 RL 的参数调整也有正确的顺序:先确定环境参数(dt、num_envs),再调奖励权重,再调 PPO 超参数,最后调 DR 范围。每轮调完后回头检查之前调过的参数是否仍然合适。这个类比的边界在于:吉他弦的物理耦合是已知的(弦之间通过琴桥传递力),而 RL 参数的耦合关系往往需要通过实验才能发现。

一次成功的训练依赖于数十个配置参数的协同。改变其中一个参数(如学习率)可能需要相应调整其他参数(如 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.11B 轮足 RL 的能量效率分析与模式选择机制 ⭐⭐⭐

动机:为什么轮足机器人的能效问题比纯足式更重要

轮足机器人的核心优势之一是能量效率——在平坦路面上滚动的能耗远低于行走。但这个优势只有在策略正确选择运动模式时才能体现。如果策略在所有地形上都采用行走模式,轮足机器人就退化为一个"脚上多了四个累赘轮子"的纯足式机器人。

理解能量效率的物理基础对于设计正确的奖励函数至关重要——不是简单地"加一个能耗惩罚",而是理解不同运动模式的能耗特性如何驱动策略的模式选择。

三种运动模式的能耗对比

纯滚动模式的能耗主要来自滚动阻力和空气阻力:

\[ P_{roll} = (c_r m g + \frac{1}{2} \rho C_d A v^2) \cdot v \tag{78.33} \]

其中 \(c_r\) 是滚动阻力系数(橡胶轮在硬地面上约 0.01-0.03),\(v\) 是行驶速度。在低速(\(v < 2\) m/s)时,空气阻力可忽略,能耗主要由 \(c_r m g v\) 决定。对于 20 kg 的轮足机器人以 1 m/s 滚动,功率约为 \(0.02 \times 20 \times 9.8 \times 1 \approx 4\) W。

纯行走模式的能耗主要来自关节力矩和摆腿加速:

\[ P_{walk} = \sum_{j=1}^{12} |\tau_j \dot{q}_j| + P_{swing} \tag{78.34} \]

典型四足机器人行走时的关节功率约为 50-200 W,是纯滚动的 10-50 倍。这个巨大的差异正是轮足机器人存在的意义。

混合模式的能耗介于两者之间。腿部维持姿态的功率与地形复杂度成正比,轮部提供推进力的功率与速度成正比:

\[ P_{hybrid} = P_{posture}(\theta_{terrain}) + P_{propulsion}(v) \tag{78.35} \]

基于能耗的模式选择分析

在无显式模式切换规则的 RL 训练中,能耗惩罚通过梯度隐式驱动模式选择。当能耗惩罚权重 \(w_E\) 足够大时,策略会在不同地形上自发地选择能耗最低的模式。

考虑一个简单的决策场景:机器人面前有一段平路和一个 10 cm 高的台阶。

策略选择 平路段能耗 台阶段能耗 总能耗
全程行走 高(关节摩擦) 中(正常跨越) 很高
全程滚动 低(滚动阻力) 极高(卡住或摔倒) 极高或失败
滚动→行走→滚动 低+中+低 中(跨越后恢复滚动) 最低

本质洞察:能耗惩罚在 RL 训练中扮演了"自然选择压力"的角色。策略不是被显式告知"平路要滚、台阶要走",而是通过累积奖励的数值差异自发学会这种行为——因为模式不对的策略能耗更高,在进化(梯度更新)中会被淘汰。这就是为什么能耗惩罚不能太小——太小的话选择压力不够,策略会停留在"万金油"模式。

能耗指标的工程测量

在实践中,如何从仿真中提取能耗信息用于分析和调试:

def compute_energy_metrics(env, episode_data):
    """计算一个 episode 的能量指标"""
    total_energy = 0.0
    leg_energy = 0.0
    wheel_energy = 0.0

    for t in range(len(episode_data)):
        torques = episode_data[t]["torques"]       # [16]
        joint_vel = episode_data[t]["joint_vel"]    # [16]
        dt = episode_data[t]["dt"]

        # 腿部能耗(关节 0-11)
        leg_power = torch.sum(torch.abs(torques[:12] * joint_vel[:12]))
        leg_energy += leg_power * dt

        # 轮部能耗(关节 12-15)
        wheel_power = torch.sum(torch.abs(torques[12:] * joint_vel[12:]))
        wheel_energy += wheel_power * dt

        total_energy += (leg_power + wheel_power) * dt

    distance = compute_total_distance(episode_data)

    return {
        "total_energy_J": total_energy,
        "leg_energy_J": leg_energy,
        "wheel_energy_J": wheel_energy,
        "leg_ratio": leg_energy / (total_energy + 1e-6),
        "cost_of_transport": total_energy / (env.robot_mass * 9.8 * distance + 1e-6),
    }

Cost of Transport (CoT) 是评价运动效率的标准化指标:

\[ CoT = \frac{E_{total}}{m g d} \tag{78.36} \]
运动方式 典型 CoT 范围 参考
轮式滚动 0.01-0.05 最高效
轮足混合 0.1-0.5 地形依赖
四足行走 0.5-2.0 步态依赖
人类行走 0.05-0.1 自然界基准

如果不监控 CoT 会怎样? 策略可能学到一种"看起来在走但实际在滑"的行为——在仿真中利用不精确的接触模型获得低能耗,但到真机上完全不工作。CoT 是检测这种"取巧"行为的重要指标——如果 CoT 低于物理极限(如低于纯滚动的理论值),说明策略在利用仿真漏洞。

⚠️ 常见陷阱

⚠️ 编程陷阱:能耗计算只用力矩的绝对值 - 错误做法:\(P = \sum |\tau_j| \cdot |\dot{q}_j|\) - 这个公式计算的是"正功+负功的总和"——即使关节在做再生制动(电机反向发电),也算成正功耗 - 正确做法:如果机器人有再生制动能力,应用 \(P = \sum \max(0, \tau_j \dot{q}_j)\);如果没有,\(|\tau_j \dot{q}_j|\) 是合理的

💡 概念误区:认为 CoT 越低策略越好 - 低 CoT 可能意味着策略在"滑行"或"利用仿真漏洞" - 正确评估:CoT 应在物理合理范围内,同时其他指标(速度跟踪、滑移率)也合格

练习

  1. ⭐⭐ 计算一个 20 kg 轮足机器人以 1 m/s 在平地滚动 100 m 的理论最低能耗(假设 \(c_r = 0.02\))。与同距离行走的能耗估计对比。
  2. ⭐⭐⭐ 设计一个实验来验证能耗惩罚对模式选择的影响:用 \(w_E \in \{0, 0.001, 0.005, 0.01, 0.05\}\) 五组权重训练,记录每组在平地上的滚动时间比例和 CoT。
  3. ⭐⭐⭐ 如果轮足机器人配备了超级电容器(可以回收制动能量),奖励函数中的能耗项应该如何修改?

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

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

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

轮足 RL 的全球研究格局

2024-2025 年轮足 RL 的主要研究力量分布在以下实验室和公司:

机构 代表工作 核心贡献 平台
ETH RSL / Swiss-Mile 城市自主导航 公里级部署、RL+导航 ANYmal-W
SUSTech clearlab Wheel-Legged-Gym, CTS 开源训练栈、并发蒸馏 自研平台
CMU FLORES 可重构轮足、MuJoCo 训练 FLORES
Frontiers 2026 双足轮足 MoE 稀疏专家混合模式切换 双足轮足
Stanford / Columbia UMI-on-Legs 相关 操作集成 Go2 系列

这个格局表明:轮足 RL 不再只是学术探索——Swiss-Mile 的商业化部署证明了技术的成熟度。但学术研究仍有大量开放问题,尤其是在安全约束、长时部署稳定性和多模态协调方面。

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

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

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

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

前沿方向一B:CTS 方法的数学直觉

CTS 的核心思想可以用以下直觉理解:传统两阶段方法中,教师是一个"已经毕业的导师"——他已经学完了所有知识,然后教学生。问题是导师可能学到了一些"只有导师才能用"的知识(依赖特权信息的技巧),学生永远无法复现这些技巧。

CTS 让教师和学生同时上课(concurrent training)。教师在学习过程中就意识到"我的学生看不到这个信息",因此自动避免依赖学生不可获取的信息。

数学上,CTS 通过在教师的 Critic 中融入学生的信息约束来实现这一点:

\[ V_T(o_T) \approx V_S(o_S) + \text{residual}(o_{priv}) \]

当 residual 很大时,说明教师严重依赖特权信息——这个信号会回传给教师的 Actor,鼓励教师学习不依赖特权信息的行为。

CTS 在轮足场景中的优势特别明显。轮足机器人的模式切换行为高度依赖地形信息——教师可以"看到"前方地形并提前准备切换,但学生从本体感知中推断地形的能力有限。CTS 让教师在学习模式切换时就考虑学生的推断能力,从而学到"即使地形推断不准确也能安全切换"的鲁棒行为。

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

目前大多数轮足 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 迁移。

前沿方向二B:稀疏专家混合(MoE)模式切换

2026 年的一项工作提出了使用稀疏混合专家(Sparse Mixture-of-Experts, MoE)来解决轮足模式切换问题。核心思想是:训练多个"专家"网络,每个专家负责一种运动模式(如纯滚动、纯行走、混合),一个门控网络根据当前观测自动选择激活哪个专家。

与传统的单一策略网络相比,MoE 有两个优势:

  1. 模式特化:每个专家可以针对特定模式深度优化,不需要在多种模式之间折中
  2. 可解释性:门控权重直接显示当前激活的是哪种模式,比单一网络的隐含模式选择更可调试

训练过程使用分阶段课程:Phase 1 在特定地形上分别训练各专家(滚动专家只在平地训练,行走专家只在台阶训练),Phase 2 在混合地形上训练门控网络和联合微调。

反事实推理:如果不用 MoE 而是用更大的单一网络,能否达到同样的模式切换质量?实验表明不能——单一网络在模式边界处的行为是"混合"(同时半走半滚),而 MoE 在边界处是"切换"(从一个专家完整切换到另一个)。"切换"行为在真机上更稳定,因为每种模式内部的控制是一致的。

前沿方向三:安全约束的 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
模式切换可解释性 黑箱切换 安全认证需要 MoE、后验分析
能耗优化 固定步态 电池续航关键 CoT 优化、步态自适应
与操作的整合 纯运动 需要 Loco-Manipulation 轮足臂 RL

反事实推理:如果轮足 RL 领域在未来 3 年内解决了上述所有开放问题,会发生什么?轮足机器人将从"需要工程师监督的实验平台"变成"可以自主执行城市配送和巡检的产品"。解决安全约束和长时稳定性是商业化的最后瓶颈——Swiss-Mile 已经展示了技术可行性,但要大规模部署还需要解决 corner case 的鲁棒性。

前沿方向四:轮足机器人的 Loco-Manipulation

轮足机器人搭配机械臂后,成为一种独特的移动操作平台。2024 年的 Arm-Constrained Curriculum Learning 工作展示了在轮足底盘上训练 Loco-Manipulation 策略的方法。

核心挑战是滚动模式下臂操作的耦合更强——滚动时基座的支撑面积(四个轮子接触点)远小于行走时(四个足底),臂运动引起的质心偏移更容易导致倾覆。解决方案是在训练早期用课程学习限制臂的动作幅度,让策略先学会稳定的轮足运动,再逐步放开臂的自由度。

这种 Arm-Constrained 课程学习的数学表达:

\[ \alpha_{arm}(epoch) = \alpha_{max} \cdot \sigma\left(\frac{epoch - epoch_0}{epoch_{scale}}\right) \]

其中 \(\sigma\) 是 sigmoid 函数,确保臂动作幅度从 0 平滑增长到最大值。\(epoch_0\) 通常设为总训练的 20%(等策略学会走/滚后),\(epoch_{scale}\) 控制增长速率。

⚠️ 常见陷阱

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

练习

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

78.12B 从单体到群体:多轮足机器人协同的 RL 视角 ⭐⭐⭐⭐

动机:单体 RL 的成功不意味着多体的简单复制

当单个轮足机器人的 RL 策略已经足够成熟时,自然的下一步是将多个机器人部署在同一环境中协同工作。但多体 RL 不是"复制 N 个单体策略"这么简单——机器人之间的交互引入了全新的学习挑战。

Multi-Agent RL 的核心困难

困难 单体 RL 多体 RL
环境稳定性 环境是马尔可夫的 其他 agent 的策略在变,环境非稳态
信用分配 奖励完全由自己的动作决定 团队奖励无法确定每个 agent 的贡献
通信 不需要 有限带宽、延迟、丢包
碰撞避免 只避静态障碍 需要避开运动中的队友
可扩展性 固定 从 2 个扩展到 N 个需要策略泛化

类比:单体 RL 像训练一个独奏钢琴家——他只需要关注自己的演奏。多体 RL 像训练一个交响乐团——每个乐手不仅要演奏自己的部分,还要听其他乐手的声音并据此调整。如果每个乐手只练独奏然后拼在一起(独立策略),结果通常是灾难性的——不是节奏对不上,就是音量不协调。这个类比的边界在于:交响乐有指挥(集中式协调),而分布式多机器人系统通常没有全局协调器。

轮足场景下的多机 RL 架构

架构 通信需求 可扩展性 适用场景
集中式 Critic + 分布式 Actor (CTDE) 训练时全局,部署时局部 中等 2-4 个机器人的紧耦合任务
独立 PPO + 通信 低(只传意图) 松耦合编队
参数共享 无通信需求 同构机器人
图神经网络策略 邻居信息 可变数量机器人

对于轮足机器人的编队巡逻任务,参数共享 + 局部通信是最实用的方案:所有机器人共享同一套策略参数(因为它们的物理结构相同),但每个机器人的观测中包含邻居的相对位置和速度信息。

多机轮足的特有挑战

模式不同步:两个轮足机器人在编队行驶时,可能一个在滚动模式、另一个在行走模式(因为它们感知到的地形不同)。模式不同步会导致编队速度不一致和间距波动。

解决方案:在多机奖励中增加模式一致性项

\[ r_{mode\_sync} = -w_{sync} \cdot \sum_{i \neq j} \|\text{mode}_i - \text{mode}_j\|^2 \]

但这个惩罚项需要权衡——强制模式一致会让所有机器人在遇到障碍时都切换到行走,即使只有一个机器人的路径上有障碍。更好的做法是鼓励速度一致而非模式一致——让每个机器人自由选择模式,但要求编队速度匹配。

⚠️ 常见陷阱

🧠 思维陷阱:认为多机 RL 只需要增加碰撞避免奖励 - 新手想法:在单体奖励上加一个 \(-w_{coll} \cdot \max(0, d_{min} - d_{ij})\) 就行 - 实际上:碰撞避免只是多机 RL 的冰山一角。更核心的问题是任务分配、速度协调和通信延迟处理 - 正确思维:多机 RL 需要从任务定义开始重新设计——环境、观测、动作、奖励都需要适配多体场景

练习

  1. ⭐⭐ 设计一个 2 轮足机器人编队行驶的奖励函数,包含速度跟踪、间距保持和碰撞避免三项。
  2. ⭐⭐⭐ 比较 CTDE 和独立 PPO 在双机搬运长杆任务中的表现预期差异。
  3. ⭐⭐⭐⭐ 如果通信有 100ms 延迟,编队控制的观测空间应如何设计来补偿这个延迟?

78.13 常见误解汇总

误解 正确理解
轮足 RL 就是足式 RL 加轮子 观测、动作、奖励和训练课程都需要从轮足特性重新设计
策略自动学会最优模式切换 需要能耗惩罚和滑移惩罚显式引导模式选择
仿真中表现好等于真机能用 必须有 DR、sim2sim 验证和灰度部署流程
更大的网络一定更好 过大网络在 RL 中容易过拟合非稳态数据分布
总奖励上升说明训练正常 必须按分项看,可能某项在取巧而其他项在恶化
力矩控制比位置控制更好 位置控制更安全、更适合 sim2real,主流工作都用位置控制
DR 范围越大迁移越好 过大范围导致保守策略,应根据真机参数校准范围
RL+MPC 混合总是优于单独使用 混合引入额外工程复杂度,简单场景不必要

本章小结

模块 核心问题 应掌握输出
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.12C 轮足 RL 的 Sim2Sim 验证方法论 ⭐⭐⭐

为什么 Sim2Sim 是必要的中间步骤

Sim2Sim(从一个仿真器迁移到另一个仿真器)是 Sim2Real 之前的关键验证步骤。如果策略在 IsaacLab 中训练后,在 MuJoCo 中行为完全不同,那么它几乎肯定在真机上也不会工作——因为两个仿真器之间的差异远小于仿真与真实之间的差异。

Sim2Sim 验证的核心指标:

指标 通过标准 不通过的含义
速度跟踪误差 MuJoCo 误差 < IsaacLab 误差的 2 倍 策略依赖 IsaacLab 特有的接触模型
步态模式 定性一致(平地滚动、台阶行走) 模式切换依赖仿真器特定的物理行为
CoT MuJoCo 和 IsaacLab 在同一数量级 能耗特性不可迁移
摔倒率 MuJoCo 摔倒率 < 2 倍 稳定性依赖仿真器特定的接触
动作分布 关节角度范围和频谱相似 策略可能过拟合仿真器的数值噪声

Sim2Sim 失败时的常见原因和解决方案:

失败模式 原因 解决方案
接触行为不同 两个仿真器的接触模型差异 增加摩擦和恢复系数的 DR 范围
时间步敏感 策略依赖特定的 dt 在不同 dt 下做 DR
关节阻尼不同 仿真器的阻尼模型差异 增加关节阻尼的 DR 范围
数值积分差异 积分器精度不同 使用更小的子步来减少误差

不是 X 而是 Y:Sim2Sim 验证的目的不是让策略在两个仿真器中表现完全一样——这几乎不可能。目的是确认策略的核心行为模式(模式切换、姿态恢复、速度跟踪)在不同物理引擎下都成立。数值上的差异是可接受的,定性的行为差异(如在一个仿真器中滚动、另一个中行走)是不可接受的。

Sim2Sim 的工程实现

def sim2sim_validation(policy_path, isaac_config, mujoco_config, n_episodes=100):
    """执行 Sim2Sim 验证"""
    # 在 IsaacLab 环境中评估
    isaac_metrics = evaluate_policy(policy_path, isaac_config, n_episodes)

    # 在 MuJoCo 环境中评估(相同的观测 schema)
    mujoco_metrics = evaluate_policy(policy_path, mujoco_config, n_episodes)

    # 对比关键指标
    report = {
        "vel_tracking_ratio": mujoco_metrics["vel_error"] / (isaac_metrics["vel_error"] + 1e-6),
        "fall_rate_ratio": mujoco_metrics["fall_rate"] / (isaac_metrics["fall_rate"] + 1e-6),
        "cot_ratio": mujoco_metrics["cot"] / (isaac_metrics["cot"] + 1e-6),
        "mode_consistency": compute_mode_agreement(isaac_metrics["modes"], mujoco_metrics["modes"]),
    }

    # 判定是否通过
    passed = (report["vel_tracking_ratio"] < 2.0 
              and report["fall_rate_ratio"] < 2.0
              and report["mode_consistency"] > 0.8)

    return report, passed

练习

  1. ⭐⭐ 在 IsaacLab 中训练一个轮足策略后,导出到 MuJoCo 做 Sim2Sim 验证。记录五个关键指标的对比。
  2. ⭐⭐⭐ 如果 Sim2Sim 失败(模式不一致),分析最可能的原因并提出修复方案。

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 个被继承覆盖的参数,说明覆盖后的值与默认值有什么不同。

78.14 轮足 RL 与 Loco-Manipulation 的交叉 ⭐⭐⭐

动机:轮足不只是"走"的工具

当轮足机器人搭配机械臂时,它成为一种高效的移动操作平台——轮子提供高速移动,腿提供地形适应,臂提供操作能力。这个"轮足 + 臂"的组合在仓储物流、最后一英里配送和建筑巡检中有明确的应用场景。

轮足臂 RL 的额外难点

在本章讨论的 16-DOF(12 腿 + 4 轮)基础上,增加 6-DOF 机械臂后,总动作维度达到 22。这不是简单地"加 6 维"——臂的运动会通过角动量耦合影响底盘稳定性,特别是在滚动模式下这种耦合更强。

耦合场景 滚动模式 行走模式 原因
臂快速移动 强耦合(基座晃动大) 中等耦合 滚动时支撑面积小
臂负重 强耦合(重心偏移) 中等耦合 滚动对重心更敏感
臂伸展 强耦合(力矩臂大) 中等耦合 远端质量在滚动时影响更大

2024 年的一项工作(Arm-Constrained Curriculum Learning)提出了臂约束课程学习:在训练早期限制臂的动作幅度,让策略先学会稳定的轮足运动,再逐步放大臂动作范围。这与 Deep-WBC(复合/180)中的动作缩放衰减思想一致——先学会走/滚,再学会边走边操作。

安全约束对轮足臂的特殊意义

轮足臂的操作场景通常在人类环境中(仓库、办公室),安全约束比纯足式更重要。

Friction-Aware Safety Locomotion(2024)结合视觉语言模型和 RL 来实现摩擦感知的安全运动——VLM 从图像中判断地面材质(如瓷砖、地毯、湿地),RL 策略根据材质估计调整步态和速度。这种"感知 → 理解 → 适配"的管线比纯 DR 更有针对性——DR 让策略对所有摩擦都鲁棒,但可能过于保守;VLM 让策略针对当前摩擦做出最优选择。

类比:纯 DR 就像一个穿着万能鞋的人——在所有地面上都还行,但在任何地面上都不是最优。VLM + RL 就像一个会换鞋的人——看到冰面换防滑鞋,看到跑道换跑鞋。每种地面都有最优表现。这个类比的边界在于:VLM 的材质判断可能出错(如误判干燥地面为湿滑),而穿万能鞋虽然保守但不会出错。

练习

  1. ⭐⭐ 分析轮足臂在滚动模式下臂运动对基座的扰动量级:假设 1 kg 的臂末端以 0.5 m/s 移动,对 20 kg 基座产生多大的角动量变化?
  2. ⭐⭐⭐ 设计一个轮足臂 RL 的课程学习方案,包含轮足运动、臂操作、负载变化三个维度的难度进阶。
  3. ⭐⭐⭐ 比较 DR 鲁棒策略和 VLM 自适应策略在低摩擦地面上的预期表现差异。

累积项目:本章新增模块

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

项目概述:本项目将本章讨论的所有技术组件串联成一个可执行的闭环流程。从 URDF 模型加载到 ONNX 导出部署,每一步都有明确的交付物和验收标准。

项目难度:⭐⭐⭐(约 60-80 人时,假设有 IsaacLab 使用经验)

先决条件:完成 足式/190_腿足RL训练栈 的累积项目,能独立训练纯足式策略。

本章新增以下模块:

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

项目验收标准

阶段 验收标准
环境搭建 轮关节可连续旋转,腿关节有限位,课程地形可切换
奖励设计 12 项奖励均有合理量级,消融表完整
训练 速度跟踪误差 < 0.3 m/s,摔倒率 < 5%,平地 CoT < 0.5
蒸馏 学生策略在评估环境中性能 > 教师的 80%
导出 ONNX 与 PyTorch 输出最大差异 < 1e-5

延伸阅读

  • 基础 ⭐⭐ 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 蒸馏框架,用于人形机器人感知运动
  • 前沿 ⭐⭐⭐⭐ Sparse MoE for wheel-legged (Frontiers 2026):稀疏专家混合模式切换
  • 前沿 ⭐⭐⭐ Friction-Aware Safety Locomotion (2024):VLM + RL 摩擦感知安全运动
  • 前沿 ⭐⭐⭐ Arm-Constrained Curriculum Learning (2024):轮足臂课程学习
  • 前沿 ⭐⭐⭐ FLORES (2025):可重构轮足机器人,MuJoCo 训练

78.17 本章与后续章节的关系

后续章节 与本章的关系 本章哪个知识点为其铺垫
复合/90 Swiss-Mile 商业化 Swiss-Mile 是轮足 RL 的最成功商业案例 78.7 蒸馏、78.8 DR、78.9 部署
复合/110 轮足 SimToReal 与硬件 深化本章 78.9 的部署内容 78.8 DR、78.9 Sim-to-Real
复合/70 轮足混合 MPC MPC 方案与本章 RL 方案的对比 78.10 RL+MPC 混合架构
复合/60 轮式运动学与 Pfaffian 轮式运动学约束影响动作空间设计 78.4 动作空间设计

78.15 版本信息速查

工具/框架 推荐版本 用途 备注
IsaacLab 最新 stable GPU 并行 RL 训练 替代 IsaacGym
rsl_rl 最新 PPO 算法实现 与 IsaacLab 配套
PyTorch 2.x 神经网络训练 需要 CUDA 支持
ONNX Runtime 1.16+ 策略部署推理 C++ 或 Python
MuJoCo 3.x Sim2Sim 验证 交叉验证用
WandB / TensorBoard 最新 训练曲线记录 推荐 WandB
Unitree SDK 最新 Go2/B2-W 硬件接口 关注 API 变更

78.15B 轮足 RL 的调试工具箱 ⭐⭐

可视化调试方法

训练过程中最有效的调试方法不是看数字,而是看策略的行为。以下可视化工具对轮足 RL 的调试至关重要:

可视化内容 观察目的 实现方式
关节角度轨迹 检查是否打限位、是否有高频振荡 12+4 维关节角度的时间序列图
步态相位图 检查步态是否对称、轮/腿模式是否合理 足端接触力的时间-空间图
基座轨迹 检查行走是否直、转弯是否平滑 俯视图上的 xy 轨迹
能耗分布 检查能耗是否来自预期部位 各关节能耗的饼图
奖励地图 检查不同区域的奖励分布 地形上覆盖奖励热力图
def visualize_gait_pattern(contact_forces, dt, episode_length):
    """绘制步态相位图——直观显示轮/腿模式"""
    import matplotlib.pyplot as plt

    fig, axes = plt.subplots(4, 1, figsize=(12, 8), sharex=True)
    leg_names = ["FR", "FL", "RR", "RL"]

    for i, (ax, name) in enumerate(zip(axes, leg_names)):
        # 足端接触力的时间序列
        times = np.arange(episode_length) * dt
        foot_contact = contact_forces[:, i] > 1.0  # 接触阈值
        wheel_spinning = np.abs(contact_forces[:, i + 4]) > 0.1  # 轮转动

        ax.fill_between(times, 0, foot_contact, alpha=0.5, label="足端接触")
        ax.fill_between(times, 0, wheel_spinning * 0.5, alpha=0.5, label="轮转动")
        ax.set_ylabel(name)
        ax.legend(loc="upper right")

    axes[-1].set_xlabel("Time (s)")
    plt.suptitle("步态相位图:蓝=足端接触 / 橙=轮转动")
    plt.tight_layout()
    return fig

本质洞察:好的调试工具不是"找 bug"用的——它是"理解策略"用的。通过步态相位图,你可以直观看到策略在何时选择滚动、何时选择行走、切换是否平滑。这比看 reward 数字更能建立对策略行为的直觉。


78.16 研究实践建议

给新手的建议

  1. 先在纯足式上练手。在开始轮足 RL 之前,先用标准的 legged_gym 训练一个 Go2 的行走策略。理解纯足式 RL 的全部流程(观测、动作、奖励、DR、蒸馏、部署)后,再转向轮足。
  2. 观测 Schema 是生命线。从第一天就用结构化的 Schema 定义观测空间,包含变量名、维度、单位和缩放系数。导出 ONNX 后再改观测顺序是灾难性的。
  3. 每改一个 reward 就做一次消融。不要积累多个 reward 修改后一起测试——你无法分辨是哪个修改带来的变化。
  4. 用视频记录策略行为。数字指标(reward、tracking error)不能替代视觉直觉。每次训练后录制 30 秒视频,看策略是否"看起来对"。

给有经验者的建议

  1. 投资 Sim2Sim 验证。在 IsaacLab 训练后,在 MuJoCo 中测试。如果两个仿真器的行为一致,说明策略没有过拟合特定仿真器。
  2. 关注 CoT 而非只看速度。高速度不等于好策略——如果 CoT 异常低,策略可能在利用仿真漏洞。
  3. 延迟随机化是 Sim2Real 的第一优先级。在所有 DR 参数中,执行器延迟对真机行为的影响最大。先加延迟随机化,再加其他 DR。
  4. 灰度部署不可跳过。即使仿真表现完美,真机首测也必须从悬空测试开始,逐步增加速度和地形复杂度。

🔧 故障排查手册

症状 可能原因 排查步骤 相关章节
训练奖励一直不涨 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