第 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 复习)
- PPO 的 clipped surrogate objective 限制了什么?写出公式并解释 \(\epsilon\) 的物理意义。
- 在纯足式 RL 中,观测空间通常包含哪些量?为什么需要历史窗口?
- 轮足机器人相比纯足式多了哪些自由度?这些自由度的控制接口有何不同?
- 什么是特权学习(Privileged Learning)?教师网络比学生网络多看到什么信息?
- Domain Randomization 的核心假设是什么?如果随机化范围设得太宽会怎样?
本章目标¶
学完本章,你应能:
- 说清楚轮足 RL 相比纯足式 RL 的额外难点——额外自由度、模式切换、滑移问题
- 搭建 IsaacLab 轮足训练环境——URDF 加载、地形生成、传感器配置
- 设计完整的观测空间和动作空间——区分本体感知与特权信息、腿关节与轮速命令
- 逐项设计奖励函数——理解每项奖励的物理意义和权重平衡
- 配置 PPO 训练流程——网络架构、超参数选择、训练曲线诊断
- 实现 Teacher-Student 蒸馏——从特权教师到可部署学生的知识迁移
- 设计 Domain Randomization 方案——参数选择、范围确定、课程训练
- 完成 Sim-to-Real 部署——ONNX 导出、推理优化、安全包裹设计
- 理解 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 的关键公式¶
整个训练过程围绕一个核心优化问题:
其中策略 \(\pi_\theta\) 将观测映射到混合动作空间:
对于四足轮足机器人(如基于 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_scale和wheel_action_scale,在 config 中明确标注两者的物理单位💡 概念误区:认为轮足 RL 就是"足式 RL + 多 4 个轮子输出" - 新手想法:直接在足式策略上加 4 维输出,其他不变 - 实际上:观测空间需要增加轮速反馈和滑移估计,奖励函数需要完全重新设计(增加滚动奖励、滑移惩罚、模式平滑项),训练课程需要从平地滚动开始逐步增加地形复杂度。这不是"加几维"的问题,而是整个训练栈的重新设计 - 正确思路:把轮足 RL 当作一个全新问题来设计,虽然可以复用足式 RL 的框架代码,但观测、动作、奖励、课程都需要从轮足的物理特性出发重新思考
🧠 思维陷阱:认为 RL 策略会自动发现最优的模式切换边界 - 新手想法:只要给足够的训练时间,策略自然会学到什么时候该滚、什么时候该走 - 实际上:如果奖励函数没有正确引导,策略可能学到的是一种"万金油"模式——永远半走半滚,在任何地形上都不是最优的。更糟糕的是,策略可能学会利用仿真器的漏洞(如超低摩擦下的高速侧滑),获得高奖励但无法迁移到真机 - 正确思维:奖励函数必须显式包含能耗项和滑移惩罚,迫使策略在能滚动时选择滚动(因为更省能),在不能滚动时选择行走(因为滑移被惩罚)
练习¶
- ⭐ 列出纯足式 Go2 的观测空间(参考足式/190_腿足RL训练栈),然后逐项分析哪些需要为轮足修改、哪些需要新增。画一张表格对比。
- ⭐⭐ 假设一个轮足机器人在 \(15°\) 坡面上行驶。用能量分析说明:此时纯滚动模式还是混合模式更高效?提示:考虑重力分量、轮胎摩擦和腿部支撑力的关系。
- ⭐⭐ 设计一个最小实验来验证"策略利用侧滑取巧"的现象。提示:对比训练时和测试时摩擦系数不同的策略表现。
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)或三角网格生成。课程训练中,地形难度应随训练进度逐步提升:
其中 \(\eta\) 是晋级阈值(通常 0.7-0.8)。这个公式的含义是:当某个难度级别的成功率超过阈值后,自动进入下一个难度级别。
步骤三:执行器模型配置。 仿真中的电机模型直接影响策略的可迁移性。
理想电机假设瞬时力矩跟踪:\(\tau = \tau_{cmd}\)。但真实电机有三个关键非理想特性:
- 带宽限制:电机力矩响应不是瞬时的,有 1-5 ms 的延迟和 10-50 Hz 的带宽限制。可以用一阶低通滤波器近似:\(\tau(s) = \frac{1}{1 + s/\omega_c} \tau_{cmd}(s)\),其中 \(\omega_c\) 是截止频率
- 力矩饱和:真实电机在高转速下最大力矩降低(恒功率区),呈近似线性下降
- 齿轮间隙(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 弥补精度不足
练习¶
- ⭐ 打开 Wheel-Legged-Gym 仓库(clearlab-sustech/Wheel-Legged-Gym),找到环境配置文件,列出所有可配置的仿真参数及其默认值。
- ⭐⭐ 用 URDF 描述一个最简单的轮足机器人:1 条腿(2 个旋转关节)+ 1 个轮子(1 个 continuous 关节)。在 Isaac Sim 中加载并验证轮关节可以连续旋转。
- ⭐⭐ 设计一个实验来测量仿真中的执行器延迟:发送阶跃力矩命令,记录实际力矩的响应曲线,与真实电机的数据表对比。
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 的历史窗口)已足够。
或者更紧凑地,只保留一个学习的隐变量:
其中 \(\phi\) 可以是 TCN(时间卷积网络)或 MLP。
观测归一化:所有观测量在进入策略网络之前必须归一化。不同物理量的数值范围差异极大——关节角度在 \([-\pi, \pi]\),轮速在 \([-40, 40]\) rad/s,IMU 角速度在 \([-10, 10]\) rad/s。如果不归一化,数值大的量会主导梯度,数值小的量会被忽略。
其中 \(\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 拼接的顺序来"隐式"定义。因为:
- 训练端和部署端的代码通常由不同的人/团队维护
- 观测顺序在导出 ONNX 后被冻结,改不了
- 一旦顺序不一致,策略会接收到完全错误的输入(如把轮速当成关节角度),输出看似合理但完全混乱的动作
# 观测 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 迁移效果
练习¶
- ⭐ 计算上述 schema 中可部署观测的总维度。如果加入 5 帧历史(只对 joint_pos、joint_vel、wheel_vel 保留历史),总维度变为多少?
- ⭐⭐ 设计一个实验来验证"观测顺序错误"会导致什么后果:在训练好的策略上,人为交换 joint_pos 和 joint_vel 的顺序,观察策略行为变化。
- ⭐⭐⭐ 为什么基座线速度 \(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,位置目标模式是最常用的选择,原因有三:
- 安全性:PD 控制器天然具有弹簧-阻尼特性。即使策略输出了一个不合理的位置目标,PD 控制器也会以有限的力矩去执行,不会产生破坏性的力矩脉冲
- Sim-to-Real 友好:位置 PD 控制的行为对电机模型误差的敏感度远低于直接力矩控制。PD 增益可以在 sim 和 real 之间保持相同
- 学习效率:位置目标的变化范围小(\(\pm 0.3\)~\(0.5\) rad),网络输出分布紧凑,PPO 的 clipping 机制更有效
其中 \(q_{default}\) 是默认站姿角度,\(s_{leg}\) 是动作缩放系数(通常 0.25-0.5 rad),\(a_{leg} \in [-1, 1]^{12}\) 是策略的归一化输出。
轮关节的动作设计¶
轮关节的控制模式与腿关节有本质不同。轮子不存在"位置"概念——它可以连续旋转——因此只能控制速度或力矩。
其中 \(s_{wheel}\) 是轮速缩放系数(通常 10-30 rad/s),\(a_{wheel} \in [-1, 1]^4\) 是策略的归一化输出。
为什么用速度控制而不是力矩控制? 两个原因:
- 与滚动运动学的一致性:轮子的前进速度 \(v = r \cdot \omega\),速度控制直接对应运动学目标。力矩控制则需要策略自己学会从期望速度到期望力矩的映射,这等价于让策略学习一个内模型
- 稳态行为更可预测:速度控制在稳态时轮子会以目标速度旋转(由速度 PI 闭环保证),力矩控制在平地上会持续加速直到摩擦力平衡
本质洞察:动作空间设计的本质是在策略的表达能力和学习的效率之间做权衡。力矩控制给策略最大的自由度,但学习空间太大,策略容易在无关维度上浪费探索。位置/速度控制通过底层 PD/PI 闭环缩小了搜索空间,代价是无法表达某些需要精细力矩控制的动作。对于轮足 RL,位置+速度的混合控制已经足够表达所有需要的运动模式。
动作安全包裹¶
策略输出不应直接进入电机驱动器。中间需要经过三层安全处理:
第一层:低通滤波。 策略网络的输出可能在相邻时步之间有很大跳变(尤其是训练早期),直接执行会产生冲击力矩。
其中 \(\alpha \in [0.2, 0.8]\) 是滤波系数。\(\alpha\) 越小,平滑度越高但响应越慢。实践中 \(\alpha = 0.6\) 是常见选择。
第二层:限幅。 滤波后的动作仍需限制在物理可行范围内。
第三层:姿态保护。 当机器人姿态异常(如 roll/pitch 超过安全阈值)时,强制减小动作幅度:
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)都使用位置目标控制 - 正确思路:选择控制模式时考虑整个系统的鲁棒性,而不是单个模块的理论最优性
练习¶
- ⭐ 计算当 \(s_{leg} = 0.4\),\(q_{default}\) 对应的 hip/thigh/knee 分别为 \([0, 0.8, -1.6]\) rad 时,策略输出 \(a_{leg} = [1, 1, 1, ...]\) 对应的关节目标位置。这些位置是否在物理可行范围内?
- ⭐⭐ 设计一个对照实验:分别用 \(\alpha = 0.2, 0.5, 0.8\) 的低通滤波系数训练策略,记录训练曲线和最终性能。预测哪个 \(\alpha\) 值会导致训练不稳定?
- ⭐⭐⭐ (跨章综合题)结合 复合/60_轮式运动学与Pfaffian 的知识,如果轮足机器人的四个轮子并非全向轮,而是普通轮(只能沿轮轴方向滚动),那么动作空间的 4 维轮速 \(\omega_{wheel} \in \mathbb{R}^4\) 中实际有多少个独立自由度?提示:考虑 Pfaffian 约束。
78.5 奖励函数工程:从物理目标到可学习信号 ⭐⭐⭐⭐¶
动机:奖励是 RL 训练的"灵魂"¶
如果仿真环境是策略训练的"身体",那么奖励函数就是"灵魂"。同一个仿真环境、同一套 PPO 超参数,换一组奖励权重就可能训练出完全不同的行为——一组出优雅的模式切换,另一组出疯狂的侧滑取巧。
奖励函数设计的核心困难在于多目标冲突:你希望机器人跑得快(速度跟踪),又希望它省电(能耗惩罚),还希望它不打滑(滑移惩罚),同时保持姿态稳定(姿态奖励)、动作平滑(平滑惩罚)。这些目标之间存在根本性的矛盾——跑得快必然费电,省电就跑不快。
奖励工程的本质不是寻找"最好的"权重组合——没有全局最优解。它是在这些矛盾目标之间找到一个满足部署需求的折中点,并通过系统化的消融实验验证这个折中点的鲁棒性。
奖励分项的逐项设计¶
轮足 RL 的奖励函数通常包含 10-15 个分项。下面逐项解释每项的物理意义、数学形式和设计考量。
第一类:任务奖励(鼓励完成目标)
速度跟踪奖励。用高斯核而不是线性误差有两个好处:(1) 当跟踪误差很小时奖励接近 1,策略有明确的"做对了"信号;(2) 当误差很大时奖励接近 0(而不是负无穷),不会主导梯度。\(\sigma_v\) 控制宽容度——\(\sigma_v = 0.25\) m/s 意味着速度误差在 0.25 m/s 以内时奖励已经接近最大值。
航向角速度跟踪,形式与速度跟踪相同。
第二类:姿态和稳定性奖励
姿态保持奖励。\(g_{proj,z}\) 是重力在机体 z 轴的投影,完全直立时 \(g_{proj,z} = -1\)(指向地面),完全翻倒时 \(g_{proj,z} = 1\)(指向天空)。上式将其归一化到 \([0, 1]\),直立时奖励为 0(因为 \((-1+1)/2=0\))。
等等,这里有个问题——直立时奖励为 0 似乎没有鼓励作用。实际中通常用另一种形式:
这是一个惩罚项(负号),姿态偏离越大惩罚越重。二者等价但后者更直观。
第三类:滑移惩罚(轮足特有)
其中 \(\kappa_i\) 是第 \(i\) 个轮子的纵向滑移率,\(\alpha_i\) 是侧向滑移角。
纵向滑移率的定义:
其中 \(r\) 是轮半径,\(\omega_{wheel}\) 是轮转速,\(v_{contact}\) 是接触点的地面速度(沿轮子前进方向)。\(\kappa = 0\) 表示纯滚动(无滑移),\(|\kappa| = 1\) 表示完全滑移(轮子锁死拖行或空转)。
如果不加滑移惩罚会怎样? 策略会学到一种"侧滑漂移"行为——利用仿真中的低侧向摩擦,让轮子以一定角度切入地面,靠侧滑的摩擦力加速。在仿真中这看起来速度很快、奖励很高,但在真机上侧向摩擦远比仿真中复杂(取决于轮胎材质、地面材质、接触压力分布),导致真机直接翻车。
第四类:能耗惩罚
能耗惩罚鼓励策略选择低能耗的运动方式。这对轮足 RL 尤其重要——在平坦路面上,纯滚动的能耗远低于行走(轮子滚动摩擦远小于腿部关节摩擦),能耗惩罚会自然驱使策略在可滚动时选择滚动。
第五类:动作平滑惩罚
惩罚相邻时步之间动作的突变。过大的动作变化意味着关节加速度过高,不仅费电还会加速机械磨损。
动作 jerk(加加速度)惩罚,比一阶平滑更强地抑制高频振荡。
第六类:关节限位惩罚
软限位惩罚。\(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 | — | 非足/轮部位接触地面 |
⚠️ 注意:上表的权重是一个起点,不是最终值。每个机器人平台、每种任务场景都需要通过消融实验来微调。但权重的数量级关系应该保持:任务奖励 > 安全惩罚 > 能耗惩罚 > 平滑惩罚。
消融实验方法论¶
奖励消融是验证每一项奖励是否必要的系统化方法。步骤如下:
- 用完整奖励函数训练一个基线策略(baseline)
- 每次只关闭一项奖励(设其权重为 0),保持其他不变
- 训练到收敛,记录关键指标:最终 episode return、速度跟踪误差、滑移率、能耗、摔倒率
- 对比消融结果与基线
| 关闭项 | 预期现象 | 如果没有预期现象 |
|---|---|---|
| 滑移惩罚 | 侧滑率大幅上升,速度跟踪可能反而更好(取巧) | 说明滑移惩罚的权重可能过低 |
| 能耗惩罚 | 能耗上升 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 维空间,随机搜索效率极低。而且权重的效果不是独立的——改变滑移惩罚的权重会连带影响速度跟踪的行为。系统化的方法是先用物理直觉确定权重的数量级,再用消融实验验证每项的必要性,最后用小范围扫描微调关键项 - 正确思维:奖励工程是一门实验科学,必须有系统的记录、控制变量和可复现的实验
练习¶
- ⭐ 用 Python 实现滑移率公式 (78.17),输入是轮速 \(\omega\)、轮半径 \(r\)、接触点速度 \(v_{contact}\)。测试边界情况:\(\omega = 0, v = 0\) 时返回什么?
- ⭐⭐ 进行一次完整的消融实验:关闭滑移惩罚训练 1M 步,与完整奖励的基线对比。记录速度跟踪误差、平均滑移率和摔倒率。
- ⭐⭐⭐ (跨章综合题)结合 复合/70_轮足混合MPC 中的模式切换逻辑,设计一个奖励项来鼓励"在平地上优先滚动、在台阶前自动切换为行走"。写出数学公式并解释为什么这种设计能起作用。
78.6 PPO 训练细节:网络、超参数与曲线诊断 ⭐⭐⭐¶
动机:PPO 是手段,不是目标¶
PPO(Proximal Policy Optimization)是当前足式/轮足 RL 中使用最广泛的算法——不是因为它理论上最优,而是因为它在"训练稳定性"和"样本效率"之间取得了最佳的工程折中。理解 PPO 的超参数如何影响训练结果,是调试轮足 RL 的核心技能。
回顾 足式/190_腿足RL训练栈 中的 PPO 基础:PPO 的核心思想是用 clipped surrogate objective 限制策略更新步长,防止一次更新破坏已学到的好行为。
其中 \(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-2 ms 内完成推理,MLP 的推理时间可以轻松控制在 0.1 ms 以内
- 观测维度不大:即使加上历史窗口,观测维度也不超过 200-300,MLP 完全可以处理
- 训练稳定性:复杂架构在 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,只有当确认是表达能力不足时才增大网络
练习¶
- ⭐ 用 WandB 或 TensorBoard 画出一次完整训练的分项奖励曲线(至少 5 项),标注训练的三个阶段:探索期、快速提升期、收敛期。
- ⭐⭐ 做一个 learning rate 扫描实验:lr \(\in \{1e-4, 3e-4, 1e-3, 3e-3\}\),训练 2000 epoch,比较训练曲线的收敛速度和稳定性。
- ⭐⭐ 解释为什么 \(\gamma = 0.99\) 对应的有效时间视野是约 100 步(提示:\(\gamma^{100} \approx 0.366\))。如果控制频率是 50 Hz,这对应多长的物理时间?
78.7 Teacher-Student 蒸馏:从特权到可部署 ⭐⭐⭐¶
动机:信息不对称的优雅解决方案¶
上一节训练出的策略看到了特权信息(地形高度图、真实摩擦系数、接触力等),在仿真中表现优异。但这些信息在真机上不可获取——你不可能在每个轮子下面放一个精确的力传感器和滑移率计。
最直观的解决方案是:从一开始就只用可部署观测训练。但这样做的效果很差——因为策略看不到地形、不知道摩擦、无法感知滑移,它很难学到有效的模式切换策略。没有特权信息,策略只能学到一种保守的"万金油"行为。
Teacher-Student 框架是解决这个矛盾的标准方法。其核心思想是:
- Teacher 阶段:用完整的特权信息训练一个"全知"教师策略 \(\pi_T(o_T)\),它能看到一切,因此能学到最优行为
- Student 阶段:用行为克隆(Behavior Cloning)训练一个只看可部署观测的学生策略 \(\pi_S(o_S)\),让它模仿教师的动作
本质洞察:Teacher-Student 蒸馏的本质不是"用大模型蒸馏小模型"(这是 NLP 中蒸馏的含义),而是用完整信息蒸馏受限信息。教师和学生的网络大小可以完全相同——区别只在于输入的信息量。学生必须学会从有限的历史观测中推断出教师直接看到的特权信息。
两阶段训练流程¶
阶段一:训练教师策略
教师策略的观测包含所有可部署观测加上特权信息:
用标准 PPO 训练到收敛。教师策略的性能是学生的性能上界——学生不可能比教师做得更好(因为学生看到的信息是教师的子集)。
阶段二:蒸馏学生策略
学生策略只看可部署观测加历史窗口:
蒸馏损失有两种形式:
动作蒸馏:直接模仿教师的动作输出。
这是最简单的形式,但存在一个问题:如果教师的动作分布是多模态的(比如在某种情况下既可以走也可以滚),L2 损失会让学生学到两种模式的平均,而不是任何一种模式。
隐变量蒸馏:教师网络提取一个隐变量 \(z_T = \text{encoder}_T(o_{priv})\),学生网络学习从历史观测中预测这个隐变量。
其中 \(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 的表现
练习¶
- ⭐ 画出 Teacher-Student 训练流程的数据流图,标注每个模块的输入/输出维度。
- ⭐⭐ 设计一个实验来区分蒸馏失败的两种模式:先测量 \(L_z\) 和 \(L_{BC}\) 的收敛情况,然后分别尝试增大网络和增加传感器,看哪种改善更大。
- ⭐⭐⭐ 如果蒸馏中加入在线 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
课程训练:从易到难的渐进随机化¶
如果一开始就用最大范围的随机化,策略在训练早期会完全无法学习——环境变化太大,任何行为都得不到稳定的奖励信号。课程训练的思想是从小范围开始,随着策略能力增强逐步扩大:
其中 \(epoch_{full}\) 是达到完整随机化范围的 epoch 数(通常设为总训练 epoch 的 30%-50%)。
如果随机化范围太宽会怎样? 策略会变得过于保守——它学会了一种在"最差情况"下仍然安全的行为。例如,如果摩擦系数的范围包含了 \(\mu = 0.1\)(冰面),策略可能学会永远低速、小步行走,即使在高摩擦地面上也不敢高速滚动。这就是鲁棒性和性能的权衡——随机化范围越宽,策略越鲁棒但性能越保守。
⚠️ 常见陷阱¶
⚠️ 编程陷阱:随机化参数在 episode 内不变但 episode 间不重新采样 - 错误做法:在环境初始化时采样一次参数,之后不再改变 - 现象:每个环境实例只见过一组参数,策略学到的是针对特定参数的行为 - 根本原因:应该在每次环境 reset 时重新采样参数 - 正确做法:在
reset()函数中对每个环境独立重新采样所有随机化参数💡 概念误区:认为 Domain Randomization 可以替代准确的物理建模 - 新手想法:反正要随机化,物理引擎精不精确无所谓 - 实际上:DR 只能处理参数级别的不确定性("摩擦系数是多少"),不能处理模型结构级别的差异("接触力的计算方式完全不同")。如果仿真器的接触模型是刚体碰撞,但真实世界是柔性接触,即使摩擦系数被随机化覆盖了,接触力的时间特性仍然完全不同 - 正确思路:先确保仿真器的物理模型结构合理,再用 DR 覆盖参数不确定性
练习¶
- ⭐ 解释为什么轮半径的随机化范围只有 \(\pm 5\%\) 而不是更大。提示:轮半径误差会直接导致里程计偏置。
- ⭐⭐ 设计一个实验来确定执行器延迟随机化的最优范围:分别用 \(\{0\}\)、\(\{0,1\}\)、\(\{0,1,2\}\)、\(\{0,1,2,3\}\) 步训练,然后在固定 2 步延迟的评估环境中测试。画出性能-鲁棒性的权衡曲线。
- ⭐⭐⭐ 如果你有真机的传感器数据(1000 步的 IMU 和编码器记录),如何用它来校准 DR 的参数范围?描述一个系统化的方法。
78.9 Sim-to-Real 部署:从仿真到实机 ⭐⭐⭐¶
动机:部署不是"把模型拷贝到机器人上"¶
训练完成后,你有一个 PyTorch 模型和一组经过验证的权重。但从这个模型到真机上可靠运行的策略,中间还有一系列工程步骤——每一步都可能引入错误。
ONNX 导出¶
PyTorch 模型不能直接在嵌入式平台上运行(大多数机器人控制器没有 Python 环境和 CUDA)。需要先导出为 ONNX 格式,再用 ONNX Runtime 或 TensorRT 在 C++ 环境中推理。
导出时必须确保:
- 观测归一化参数被冻结:\(\mu\) 和 \(\sigma\) 作为常量嵌入模型
- 动作后处理被包含:低通滤波、限幅、缩放都在模型内部完成
- 数值精度一致:训练用 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 环境中的性能(而不只是标称环境)、动作平滑度、安全壳触发率等
练习¶
- ⭐ 完成一次 ONNX 导出和数值对齐验证。记录导出的模型文件大小和推理延迟。
- ⭐⭐ 设计一套完整的灰度部署检查清单(checklist),至少包含 15 个检查项,覆盖硬件、软件、通信和安全。
- ⭐⭐ 描述一个你见过或读过的 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(全身控制器)。
模式 B:MPC 底层安全壳 + RL 上层决策
RL 策略在上层输出"意图"(如期望的运动方向和速度),MPC 在底层将这个意图转化为满足安全约束的关节命令。
Lee et al. (Science Robotics 2024) 的 Swiss-Mile 工作采用的是接近模式 A 的方案:RL 策略负责底层的运动控制和模式切换,上层的导航规划负责提供目标速度和航向。
接口设计¶
RL 和 MPC 之间的接口设计是混合架构的核心。接口不当会导致两个模块互相对抗——MPC 试图修正 RL 的决策,RL 试图绕过 MPC 的约束。
好的接口应满足:
- 语义清晰:每个接口变量有明确的物理意义和量纲
- 频率匹配:MPC 和 RL 运行频率不同时,需要插值或保持器
- 安全边界:接口变量有合理的范围限制
本质洞察:RL+MPC 混合架构的本质是责任分工。RL 负责"学习做什么"(what to do),MPC 负责"保证怎么做"(how to do it safely)。这种分工让每个模块做自己最擅长的事——RL 不需要学习硬约束,MPC 不需要处理模式切换。
⚠️ 常见陷阱¶
🧠 思维陷阱:认为混合架构总是比纯 RL 或纯 MPC 好 - 新手想法:两者结合肯定优于单独使用 - 实际上:混合架构引入了新的工程复杂度——两个模块的调参空间叠加,调试时需要区分问题来自 RL 还是 MPC。如果应用场景相对简单(如平地移动),纯 MPC 可能更合适;如果约束不重要(如仿真中的实验),纯 RL 更简单高效 - 正确思维:根据应用需求选择架构——安全关键场景用混合架构,研究探索用纯 RL,约束明确的场景用纯 MPC
练习¶
- ⭐ 画出 RL+MPC 混合架构的模式 A 和模式 B 的系统框图,标注每个模块的输入/输出和运行频率。
- ⭐⭐ 讨论:如果 RL 策略输出的速度命令超出了 MPC 的可行域,应该怎么处理?设计一个"软约束"接口方案。
- ⭐⭐⭐ (跨章综合题)结合 足式/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 环境中的评估性能(而不是标称环境)来决定何时停止训练
练习¶
- ⭐ 在上述配置中,计算每个 PPO epoch 使用的总样本数:\(N = \text{num\_envs} \times \text{episode\_steps} \times \text{num\_learning\_epochs} / \text{num\_mini\_batches}\)。
- ⭐⭐ 修改配置中的 dt(从 0.02 变为 0.01),列出所有需要相应调整的参数及其新值。
- ⭐⭐ 设计一个自动化的超参数搜索脚本:选择 3 个最关键的超参数,各取 3 个值,用网格搜索训练 27 组实验,选出最佳组合。
78.11B 轮足 RL 的能量效率分析与模式选择机制 ⭐⭐⭐¶
动机:为什么轮足机器人的能效问题比纯足式更重要¶
轮足机器人的核心优势之一是能量效率——在平坦路面上滚动的能耗远低于行走。但这个优势只有在策略正确选择运动模式时才能体现。如果策略在所有地形上都采用行走模式,轮足机器人就退化为一个"脚上多了四个累赘轮子"的纯足式机器人。
理解能量效率的物理基础对于设计正确的奖励函数至关重要——不是简单地"加一个能耗惩罚",而是理解不同运动模式的能耗特性如何驱动策略的模式选择。
三种运动模式的能耗对比¶
纯滚动模式的能耗主要来自滚动阻力和空气阻力:
其中 \(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。
纯行走模式的能耗主要来自关节力矩和摆腿加速:
典型四足机器人行走时的关节功率约为 50-200 W,是纯滚动的 10-50 倍。这个巨大的差异正是轮足机器人存在的意义。
混合模式的能耗介于两者之间。腿部维持姿态的功率与地形复杂度成正比,轮部提供推进力的功率与速度成正比:
基于能耗的模式选择分析¶
在无显式模式切换规则的 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 范围 | 参考 |
|---|---|---|
| 轮式滚动 | 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 应在物理合理范围内,同时其他指标(速度跟踪、滑移率)也合格
练习¶
- ⭐⭐ 计算一个 20 kg 轮足机器人以 1 m/s 在平地滚动 100 m 的理论最低能耗(假设 \(c_r = 0.02\))。与同距离行走的能耗估计对比。
- ⭐⭐⭐ 设计一个实验来验证能耗惩罚对模式选择的影响:用 \(w_E \in \{0, 0.001, 0.005, 0.01, 0.05\}\) 五组权重训练,记录每组在平地上的滚动时间比例和 CoT。
- ⭐⭐⭐ 如果轮足机器人配备了超级电容器(可以回收制动能量),奖励函数中的能耗项应该如何修改?
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 中融入学生的信息约束来实现这一点:
当 residual 很大时,说明教师严重依赖特权信息——这个信号会回传给教师的 Actor,鼓励教师学习不依赖特权信息的行为。
CTS 在轮足场景中的优势特别明显。轮足机器人的模式切换行为高度依赖地形信息——教师可以"看到"前方地形并提前准备切换,但学生从本体感知中推断地形的能力有限。CTS 让教师在学习模式切换时就考虑学生的推断能力,从而学到"即使地形推断不准确也能安全切换"的鲁棒行为。
前沿方向二:基于感知的运动控制¶
目前大多数轮足 RL 工作使用本体感知(IMU + 编码器)作为学生策略的输入。但最新的研究开始将深度相机或 LiDAR 点云直接输入策略网络,实现视觉引导的运动控制。
这种方法的挑战在于:
- 观测维度爆炸:一帧深度图有数万个像素,直接输入 MLP 不可行
- sim-to-real gap 更大:视觉域的 sim-to-real gap 远大于本体感知——仿真渲染的纹理、光照、阴影都与真实不同
- 端到端 vs 模块化:是让策略直接从像素学习运动,还是先用传统方法提取地形特征再输入策略?
CoRL 2025 的 Omni-Perception 方法展示了直接处理 LiDAR 点云的端到端腿足运动控制,在 Isaac Gym 中实现了大规模并行训练和高效 sim-to-real 迁移。
前沿方向二B:稀疏专家混合(MoE)模式切换¶
2026 年的一项工作提出了使用稀疏混合专家(Sparse Mixture-of-Experts, MoE)来解决轮足模式切换问题。核心思想是:训练多个"专家"网络,每个专家负责一种运动模式(如纯滚动、纯行走、混合),一个门控网络根据当前观测自动选择激活哪个专家。
与传统的单一策略网络相比,MoE 有两个优势:
- 模式特化:每个专家可以针对特定模式深度优化,不需要在多种模式之间折中
- 可解释性:门控权重直接显示当前激活的是哪种模式,比单一网络的隐含模式选择更可调试
训练过程使用分阶段课程:Phase 1 在特定地形上分别训练各专家(滚动专家只在平地训练,行走专家只在台阶训练),Phase 2 在混合地形上训练门控网络和联合微调。
反事实推理:如果不用 MoE 而是用更大的单一网络,能否达到同样的模式切换质量?实验表明不能——单一网络在模式边界处的行为是"混合"(同时半走半滚),而 MoE 在边界处是"切换"(从一个专家完整切换到另一个)。"切换"行为在真机上更稳定,因为每种模式内部的控制是一致的。
前沿方向三:安全约束的 RL¶
标准 PPO 通过奖励中的惩罚项来约束不安全行为,但这是"软约束"——无法保证策略永远不会违反安全边界。在商业部署中,"几乎不会违反"和"绝对不会违反"之间的差距可能是生死攸关的。
Constrained RL(如 CPO, PPO-Lagrangian)将安全约束作为优化问题的硬约束:
其中 \(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 课程学习的数学表达:
其中 \(\sigma\) 是 sigmoid 函数,确保臂动作幅度从 0 平滑增长到最大值。\(epoch_0\) 通常设为总训练的 20%(等策略学会走/滚后),\(epoch_{scale}\) 控制增长速率。
⚠️ 常见陷阱¶
🧠 思维陷阱:认为解决了所有开放问题才能做产品 - 新手想法:这么多未解决的问题,轮足机器人还不能商用 - 实际上:RIVR 已经在街头送外卖了。产品不需要解决所有学术问题——它只需要在特定场景下足够可靠。学术上的"开放问题"在产品中可以通过工程手段绕过(如远程接管替代完全自主)。研究的价值是降低这些绕过手段的成本 - 正确思维:区分"学术上有趣"和"工程上紧迫"的问题。优先研究后者
练习¶
- ⭐⭐ 选择一个开放问题,写一个 1 页的研究提案:包括研究动机、拟用方法、实验设计和预期结果。
- ⭐⭐⭐ 阅读 CTS 论文(clearlab-sustech, 2024),回答:CTS 相比传统两阶段蒸馏在什么场景下优势最大?什么场景下可能不如两阶段方法?
- ⭐⭐⭐ 对比 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 + 通信 | 低(只传意图) | 高 | 松耦合编队 |
| 参数共享 | 无通信需求 | 高 | 同构机器人 |
| 图神经网络策略 | 邻居信息 | 高 | 可变数量机器人 |
对于轮足机器人的编队巡逻任务,参数共享 + 局部通信是最实用的方案:所有机器人共享同一套策略参数(因为它们的物理结构相同),但每个机器人的观测中包含邻居的相对位置和速度信息。
多机轮足的特有挑战¶
模式不同步:两个轮足机器人在编队行驶时,可能一个在滚动模式、另一个在行走模式(因为它们感知到的地形不同)。模式不同步会导致编队速度不一致和间距波动。
解决方案:在多机奖励中增加模式一致性项:
但这个惩罚项需要权衡——强制模式一致会让所有机器人在遇到障碍时都切换到行走,即使只有一个机器人的路径上有障碍。更好的做法是鼓励速度一致而非模式一致——让每个机器人自由选择模式,但要求编队速度匹配。
⚠️ 常见陷阱¶
🧠 思维陷阱:认为多机 RL 只需要增加碰撞避免奖励 - 新手想法:在单体奖励上加一个 \(-w_{coll} \cdot \max(0, d_{min} - d_{ij})\) 就行 - 实际上:碰撞避免只是多机 RL 的冰山一角。更核心的问题是任务分配、速度协调和通信延迟处理 - 正确思维:多机 RL 需要从任务定义开始重新设计——环境、观测、动作、奖励都需要适配多体场景
练习¶
- ⭐⭐ 设计一个 2 轮足机器人编队行驶的奖励函数,包含速度跟踪、间距保持和碰撞避免三项。
- ⭐⭐⭐ 比较 CTDE 和独立 PPO 在双机搬运长杆任务中的表现预期差异。
- ⭐⭐⭐⭐ 如果通信有 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
练习¶
- ⭐⭐ 在 IsaacLab 中训练一个轮足策略后,导出到 MuJoCo 做 Sim2Sim 验证。记录五个关键指标的对比。
- ⭐⭐⭐ 如果 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 时,它会查找对应的 WheelLeggedEnv 和 WheelLeggedCfg。
# 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
练习¶
- ⭐ 克隆 Wheel-Legged-Gym 仓库,找到
_compute_reward函数,列出所有奖励项及其默认权重。 - ⭐⭐ 在该仓库中新增一个奖励项:惩罚轮速的一阶差分(\(\|w_t - w_{t-1}\|^2\))。训练并对比加入前后的策略行为。
- ⭐⭐ 用
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 kg 的臂末端以 0.5 m/s 移动,对 20 kg 基座产生多大的角动量变化?
- ⭐⭐⭐ 设计一个轮足臂 RL 的课程学习方案,包含轮足运动、臂操作、负载变化三个维度的难度进阶。
- ⭐⭐⭐ 比较 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 研究实践建议¶
给新手的建议¶
- 先在纯足式上练手。在开始轮足 RL 之前,先用标准的 legged_gym 训练一个 Go2 的行走策略。理解纯足式 RL 的全部流程(观测、动作、奖励、DR、蒸馏、部署)后,再转向轮足。
- 观测 Schema 是生命线。从第一天就用结构化的 Schema 定义观测空间,包含变量名、维度、单位和缩放系数。导出 ONNX 后再改观测顺序是灾难性的。
- 每改一个 reward 就做一次消融。不要积累多个 reward 修改后一起测试——你无法分辨是哪个修改带来的变化。
- 用视频记录策略行为。数字指标(reward、tracking error)不能替代视觉直觉。每次训练后录制 30 秒视频,看策略是否"看起来对"。
给有经验者的建议¶
- 投资 Sim2Sim 验证。在 IsaacLab 训练后,在 MuJoCo 中测试。如果两个仿真器的行为一致,说明策略没有过拟合特定仿真器。
- 关注 CoT 而非只看速度。高速度不等于好策略——如果 CoT 异常低,策略可能在利用仿真漏洞。
- 延迟随机化是 Sim2Real 的第一优先级。在所有 DR 参数中,执行器延迟对真机行为的影响最大。先加延迟随机化,再加其他 DR。
- 灰度部署不可跳过。即使仿真表现完美,真机首测也必须从悬空测试开始,逐步增加速度和地形复杂度。
🔧 故障排查手册¶
| 症状 | 可能原因 | 排查步骤 | 相关章节 |
|---|---|---|---|
| 训练奖励一直不涨 | 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 |