第 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 完整训练配置参考与调参策略 ⭐⭐¶
动机:配置文件是训练的"DNA"¶
一次成功的训练依赖于数十个配置参数的协同。改变其中一个参数(如学习率)可能需要相应调整其他参数(如 clip range 和 batch size)。本节给出一份经过实践验证的完整配置参考,并解释参数之间的耦合关系。
完整训练配置¶
class WheelLeggedTrainCfg:
"""轮足 RL 训练的完整配置"""
# === 环境参数 ===
num_envs = 4096 # GPU 并行环境数,A100 可以上 8192
episode_length_s = 20.0 # 每个 episode 的时长(秒)
dt = 0.02 # 仿真步长(s),对应 50 Hz 控制频率
decimation = 4 # 物理子步数:实际物理步长 = dt/decimation = 5ms
# === 观测配置 ===
obs_scales = {
"lin_vel": 2.0, # 线速度缩放
"ang_vel": 0.25, # 角速度缩放
"dof_pos": 1.0, # 关节位置缩放
"dof_vel": 0.05, # 关节速度缩放(数值大,需缩小)
"height_measurements": 5.0, # 高度测量缩放
}
clip_observations = 100.0 # 观测裁剪范围
# === 动作配置 ===
leg_action_scale = 0.4 # 腿关节动作缩放(rad)
wheel_action_scale = 15.0 # 轮速动作缩放(rad/s)
action_filter_alpha = 0.6 # 低通滤波系数
# === 奖励配置 ===
rewards = {
"tracking_lin_vel": {"weight": 1.5, "sigma": 0.25},
"tracking_ang_vel": {"weight": 0.5, "sigma": 0.25},
"orientation": {"weight": -5.0},
"base_height": {"weight": -1.0, "target": 0.52},
"slip_penalty": {"weight": -0.1},
"energy": {"weight": -0.005},
"action_rate": {"weight": -0.01},
"action_jerk": {"weight": -0.001},
"joint_limit": {"weight": -10.0, "margin": 0.1},
"collision": {"weight": -5.0},
"feet_air_time": {"weight": 0.1, "threshold": 0.5},
}
# === PPO 配置 ===
learning_rate = 3e-4
clip_param = 0.2
gamma = 0.99
lam = 0.95 # GAE lambda
num_mini_batches = 4
num_learning_epochs = 5
entropy_coef = 0.01
value_loss_coef = 1.0
max_grad_norm = 1.0
use_clipped_value_loss = True
# === 网络配置 ===
policy_hidden_dims = [512, 256, 128]
value_hidden_dims = [512, 256, 128]
activation = "elu"
init_noise_std = 1.0 # 初始探索噪声标准差
# === Domain Randomization ===
randomize_friction = True
friction_range = [0.3, 1.5]
randomize_base_mass = True
added_mass_range = [-3.0, 5.0] # kg
push_robots = True
push_interval_s = 15 # 每 15 秒随机推一下
max_push_vel = 1.0 # 最大推力产生的速度变化(m/s)
randomize_motor_strength = True
motor_strength_range = [0.8, 1.2]
# === 课程训练 ===
curriculum = True
terrain_curriculum = True
max_terrain_level = 7
terrain_proportions = [0.2, 0.2, 0.15, 0.15, 0.1, 0.1, 0.05, 0.05]
参数耦合关系¶
以下参数之间存在强耦合,调整一个时必须考虑其他:
| 参数 A | 参数 B | 耦合关系 | 调参建议 |
|---|---|---|---|
| learning_rate | clip_param | lr 增大时 clip 应减小 | 保持 lr/clip ≈ 1.5e-3 |
| num_envs | mini_batch_size | 总批量 = num_envs × episode_steps / num_mini_batches | 总批量应 > 10000 |
| action_scale | reward weights | 缩放改变后奖励的量级会变 | 改缩放后检查奖励分项量级 |
| dt | obs_history_length | dt 变小则历史窗口对应更短的物理时间 | 保持历史窗口≈25-50ms |
| friction_range | slip_penalty | 摩擦范围太宽时滑移惩罚需加大 | DR 扩大后检查滑移率 |
| episode_length | gamma | 长 episode 需要较大的 gamma | episode 20s → gamma ≥ 0.99 |
系统化调参流程¶
调参不应该是随机搜索。以下是一个经过实践验证的系统化流程:
第一轮:基线建立(2-4 小时) 1. 用上述默认配置训练 2000 epoch 2. 检查:策略是否学会了基本的前进运动?奖励是否在上升? 3. 如果完全不学习 → 检查观测归一化和奖励量级
第二轮:奖励调优(4-8 小时) 1. 做完整的奖励消融实验(逐项关闭) 2. 确认每项奖励的必要性 3. 调整权重使分项奖励的量级在同一数量级
第三轮:DR 调优(4-8 小时) 1. 从小范围 DR 开始训练到收敛 2. 逐步扩大 DR 范围(公式 78.27),观察性能下降 3. 找到"性能可接受的最大 DR 范围"
第四轮:精细调参(2-4 小时) 1. 在最终 DR 范围下做 lr 和 clip 的小范围扫描 2. 做 entropy_coef 和 init_noise_std 的扫描 3. 选择最佳组合做最终训练
本质洞察:调参的目标**不是**找到"全局最优参数组合"——这在 RL 的随机性下是不可能的。目标是找到一个**鲁棒的参数区域**——在这个区域内,参数的小幅扰动不会导致训练失败。如果你找到了一组"刚好能工作"的参数,那说明你站在悬崖边——生产环境中任何小的变化都可能让训练崩溃。
⚠️ 常见陷阱¶
⚠️ 编程陷阱:修改了 dt 但忘记相应调整 action_filter_alpha - 错误做法:把 dt 从 0.02 改为 0.01(控制频率翻倍),但 alpha 保持 0.6 - 现象:动作变得更"抖",因为滤波器的截止频率随 dt 变化 - 根本原因:低通滤波的等效截止频率 \(f_c = -\frac{\ln(1-\alpha)}{2\pi \cdot dt}\),dt 减半时 \(f_c\) 翻倍 - 正确做法:按 \(\alpha_{new} = 1 - (1-\alpha_{old})^{dt_{new}/dt_{old}}\) 调整
💡 概念误区:认为训练时间越长越好 - 新手想法:多训练一些总没坏处 - 实际上:RL 训练存在过拟合——不是对数据集的过拟合,而是对当前 DR 分布的过拟合。长时间训练后策略可能学会了"在 DR 分布边界上的最优行为",这种行为在真机上(真机参数在 DR 分布内部的某个点)可能不是最优的 - 正确思路:用 DR 环境中的评估性能(而不是标称环境)来决定何时停止训练
练习¶
- ⭐ 在上述配置中,计算每个 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.12 轮足 RL 的前沿进展与开放问题 ⭐⭐⭐⭐¶
动机:知道前沿在哪里才能找到研究方向¶
轮足 RL 虽然在工程上取得了显著进展(Swiss-Mile 的城市部署就是最好的证明),但仍有很多开放问题尚未解决。理解这些问题的边界,对于选择研究方向和设计实验至关重要。
前沿方向一:Concurrent Teacher-Student 训练¶
传统的 Teacher-Student 是两阶段的:先训练教师到收敛,再蒸馏学生。这有一个问题:教师学到的行为可能超出学生的表达能力——教师可以依赖某些特权信息做出精细决策,但学生无论如何也无法从历史观测中推断出这些信息。结果是蒸馏后的学生性能与教师有显著差距。
clearlab-sustech 提出的 CTS (Concurrent Teacher-Student) 方法(2024)解决了这个问题:教师和学生在同一个训练循环中同时训练。教师和学生共享相同的 Critic 网络和策略网络结构,同时在环境中采集数据。教师看到的数据分布和学生不同,但优化的目标函数是相同的。
这种方法的优势在于:教师在学习过程中就"知道"学生的能力边界——如果某个行为需要学生永远无法获取的信息,教师在训练过程中就会发现这种行为对 Critic 的价值贡献不高(因为学生无法复现),从而自动避免学习这种行为。
前沿方向二:基于感知的运动控制¶
目前大多数轮足 RL 工作使用本体感知(IMU + 编码器)作为学生策略的输入。但最新的研究开始将深度相机或 LiDAR 点云直接输入策略网络,实现视觉引导的运动控制。
这种方法的挑战在于:
- 观测维度爆炸:一帧深度图有数万个像素,直接输入 MLP 不可行
- sim-to-real gap 更大:视觉域的 sim-to-real gap 远大于本体感知——仿真渲染的纹理、光照、阴影都与真实不同
- 端到端 vs 模块化:是让策略直接从像素学习运动,还是先用传统方法提取地形特征再输入策略?
CoRL 2025 的 Omni-Perception 方法展示了直接处理 LiDAR 点云的端到端腿足运动控制,在 Isaac Gym 中实现了大规模并行训练和高效 sim-to-real 迁移。
前沿方向三:安全约束的 RL¶
标准 PPO 通过奖励中的惩罚项来约束不安全行为,但这是"软约束"——无法保证策略永远不会违反安全边界。在商业部署中,"几乎不会违反"和"绝对不会违反"之间的差距可能是生死攸关的。
Constrained RL(如 CPO, PPO-Lagrangian)将安全约束作为优化问题的硬约束:
其中 \(C_i\) 是第 \(i\) 个约束的代价函数,\(d_i\) 是约束阈值。这比在奖励中加惩罚项更有理论保证,但训练也更困难。
开放问题汇总¶
| 问题 | 现状 | 挑战 | 潜在方向 |
|---|---|---|---|
| 模式切换的可解释性 | RL 策略是黑盒 | 无法证明安全性 | 可解释 RL、后验分析 |
| 极端场景的鲁棒性 | DR 范围有限 | 覆盖不了所有长尾 | 自适应 DR、真机反馈 |
| 多机器人协同 | 单机策略 | 通信延迟、碰撞避免 | Multi-Agent RL |
| 长时间部署的稳定性 | 短时间测试 | 模型退化、传感器漂移 | 在线自适应 |
| 与人类的自然交互 | 简单避障 | 理解人类意图 | Social-aware RL |
⚠️ 常见陷阱¶
🧠 思维陷阱:认为解决了所有开放问题才能做产品 - 新手想法:这么多未解决的问题,轮足机器人还不能商用 - 实际上:RIVR 已经在街头送外卖了。产品不需要解决所有学术问题——它只需要在特定场景下足够可靠。学术上的"开放问题"在产品中可以通过工程手段绕过(如远程接管替代完全自主)。研究的价值是**降低这些绕过手段的成本** - 正确思维:区分"学术上有趣"和"工程上紧迫"的问题。优先研究后者
练习¶
- ⭐⭐ 选择一个开放问题,写一个 1 页的研究提案:包括研究动机、拟用方法、实验设计和预期结果。
- ⭐⭐⭐ 阅读 CTS 论文(clearlab-sustech, 2024),回答:CTS 相比传统两阶段蒸馏在什么场景下优势最大?什么场景下可能不如两阶段方法?
- ⭐⭐⭐ 对比 Constrained RL(CPO)和奖励惩罚法在安全约束方面的理论保证。在轮足场景中,哪种方法更实用?为什么?
本章小结¶
| 模块 | 核心问题 | 应掌握输出 |
|---|---|---|
| 78.1 轮足 RL 难点 | 额外自由度 + 模式切换 + 滑移 | 能说清楚为什么不能直接复用足式 RL |
| 78.2 仿真环境 | URDF + 地形 + 执行器模型 | 能搭建可训练的 IsaacLab 环境 |
| 78.3 观测空间 | 可部署 vs 特权 + 历史窗口 | 能写出完整的观测 schema |
| 78.4 动作空间 | 腿位置 + 轮速度 + 安全包裹 | 能设计混合动作空间 |
| 78.5 奖励设计 | 12+ 项逐项分析 + 消融方法 | 能独立设计并调试奖励函数 |
| 78.6 PPO 训练 | 网络架构 + 超参数 + 曲线诊断 | 能诊断训练问题并调参 |
| 78.7 蒸馏 | Teacher-Student + 隐变量重建 | 能实现特权→可部署的迁移 |
| 78.8 Domain Randomization | 参数表 + 课程训练 | 能设计覆盖真机的 DR 方案 |
| 78.9 Sim-to-Real | ONNX 导出 + 灰度部署 | 能完成从训练到真机的全流程 |
| 78.10 RL+MPC 混合 | 两种混合模式 + 接口设计 | 能设计混合架构的责任分工 |
78.13 Wheel-Legged-Gym 代码走读:从入口到训练循环 ⭐⭐¶
动机:读懂仓库是独立开发的前提¶
本节以 clearlab-sustech/Wheel-Legged-Gym 为参照,走读一个典型轮足 RL 训练栈的代码结构。读完本节后你应该能定位任何配置项、修改任何奖励函数、添加新的观测量。
仓库结构¶
典型的轮足 RL 仓库遵循 legged_gym 的结构模式:
wheel_legged_gym/
├── envs/
│ ├── base/ # 基类
│ │ ├── legged_robot.py # 环境基类(step, reset, compute_reward)
│ │ └── legged_robot_config.py # 配置基类
│ ├── wheel_legged/ # 轮足专用
│ │ ├── wheel_legged_env.py # 轮足环境(继承基类,添加轮相关逻辑)
│ │ └── wheel_legged_config.py # 轮足配置
│ └── __init__.py # 任务注册
├── scripts/
│ ├── train.py # 训练入口
│ └── play.py # 可视化评估
├── resources/
│ └── robots/
│ └── wheel_legged/
│ └── urdf/ # URDF 模型文件
└── rsl_rl/ # PPO 算法实现
├── algorithms/
│ └── ppo.py # PPO 核心
├── modules/
│ └── actor_critic.py # 策略和价值网络
└── runners/
└── on_policy_runner.py # 训练循环
关键:先找入口,再沿调用链读。 不要从第一个文件开始按顺序读——应该从 train.py 开始,沿着函数调用链一步步深入。
训练入口 → 环境创建 → 训练循环¶
# train.py 的核心逻辑(简化版)
def train():
# 1. 解析任务名称 → 找到对应的环境类和配置
env_cfg, train_cfg = task_registry.get_cfgs(task_name)
# 2. 创建环境
env = task_registry.make_env(task_name, env_cfg)
# 此时 4096 个并行环境已在 GPU 上创建完毕
# 3. 创建 PPO 训练器
runner = OnPolicyRunner(env, train_cfg, device="cuda")
# 4. 训练循环
runner.learn(num_learning_iterations=5000)
任务注册机制:task_registry 是一个全局字典,将任务名称映射到环境类和配置类。当你输入 --task wheel_legged 时,它会查找对应的 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 个被继承覆盖的参数,说明覆盖后的值与默认值有什么不同。
累积项目:本章新增模块¶
项目名称:从零构建轮足 RL 训练-部署闭环
本章新增以下模块:
| 阶段 | 任务 | 交付物 |
|---|---|---|
| 环境搭建 | 在 IsaacLab 中加载轮足 URDF,配置地形和执行器 | 可运行的训练环境 |
| 奖励设计 | 实现 12 项奖励函数,完成消融实验 | 奖励消融报告 |
| 训练 | PPO 训练到收敛,记录分项奖励曲线 | 训练日志 + 检查点 |
| 蒸馏 | 完成 Teacher-Student 蒸馏 | 学生策略检查点 |
| 导出 | ONNX 导出 + 数值对齐验证 | ONNX 文件 + 验证报告 |
延伸阅读¶
- 基础 ⭐⭐ clearlab-sustech/Wheel-Legged-Gym:轮足 RL 训练栈开源实现,入门首选
- 基础 ⭐⭐ Rudin et al., Learning to Walk in Minutes (2022):GPU 并行腿足 RL 的基石工作
- 进阶 ⭐⭐⭐ Lee et al., Learning Robust Autonomous Navigation and Locomotion for Wheeled-Legged Robots (Science Robotics, 2024):Swiss-Mile/ETH 的轮足 RL+导航系统,公里级城市部署验证
- 进阶 ⭐⭐⭐ Chamorro et al., Blind Stair Traversal via RL (2024):轮足/足式盲爬楼梯,关注观测设计和蒸馏
- 进阶 ⭐⭐⭐ CTS: Concurrent Teacher-Student RL (clearlab-sustech, 2024):教师和学生同时训练的新范式
- 工具 ⭐⭐ IsaacLab 文档 + rsl_rl 库:现代训练栈的标准工具
- 工具 ⭐⭐ Distillation-PPO (2025):两阶段 RL 蒸馏框架,用于人形机器人感知运动
🔧 故障排查手册¶
| 症状 | 可能原因 | 排查步骤 | 相关章节 |
|---|---|---|---|
| 训练奖励一直不涨 | 1. 奖励权重失衡(惩罚太重) 2. 观测未归一化 3. 学习率过低 |
1. 打印各奖励分项,检查惩罚项是否占主导 2. 打印观测的 mean/std 3. 扫描 lr={1e-4, 3e-4, 1e-3} |
§78.5, §78.6 |
| 策略只会站着不动 | 1. 课程太难(初始地形复杂) 2. 碰撞惩罚太重 3. 动作缩放太小 |
1. 从纯平地开始训练 2. 降低碰撞惩罚权重 3. 增大 action_scale |
§78.2, §78.5 |
| 仿真中好但真机摔倒 | 1. DR 范围未覆盖真机参数 2. 观测顺序训练/部署不一致 3. 缺少执行器延迟随机化 |
1. 测量真机参数,对比 DR 范围 2. 用 schema 交叉验证 3. 加入 1-3 步延迟随机化 |
§78.8, §78.9 |
| 模式切换时力矩跳变 | 1. 动作滤波不够 2. 奖励缺少平滑项 3. 模式边界处奖励冲突 |
1. 减小 \(\alpha\)(增强滤波) 2. 增加动作 jerk 惩罚 3. 检查消融实验中边界附近的行为 |
§78.4, §78.5 |
| ONNX 导出后行为异常 | 1. 归一化参数未冻结 2. 动作后处理未包含在导出中 3. float32/float16 精度问题 |
1. 检查导出前是否调用了 model.eval() 2. 对比导出前后的 100 组输出 3. 统一使用 float32 |
§78.9 |