17 机械臂与灵巧手操作
本章定位:Part IV 的前几章已经让你掌握了足式机器人的速度跟踪(Ch13-14)和全身动作模仿(Ch15-16)。本章切换到一个完全不同的任务族:固定基座操作——让机械臂和灵巧手与外部物体交互。虽然同样使用 PPO、同样在 manager-based 架构上运行,但操作任务的 MDP 设计与 locomotion 存在深刻的结构性差异:奖励信号从连续变为阶段性,动作空间从全身协调变为末端精确,成功判据从"持续运动"变为"改变世界状态"。本章以 mjlab 的 YAM lift cube 为主线深度实战,以 Isaac Lab 的 DexSuite(Kuka+Allegro)为辅助对照,系统讲解如何把 locomotion 中积累的工程能力迁移到操作领域。
前置依赖:Ch05(Observation/Action 接口设计)、Ch06(Reward 与 Curriculum)、Ch07(RSL-RL 训练管线)、Ch08(Domain Randomization)
关键文献:DexPBT — DexPBT: Scaling up Dexterous Manipulation for Hand-Arm Systems with PBT(RSS'23,Isaac Lab DexSuite 内置) 关键框架:mjlab
Mjlab-Lift-Cube-Yam(平行夹爪入门),Isaac LabIsaac-Repose-Cube-Allegro-Direct-v0(灵巧手进阶),Isaac-Lift-Cube-Franka-v0(三种动作空间变体) 累积项目:D(固定基座操作模块——从 state 到视觉的完整训练管线) 阅读时间估计:精读约 4-6 小时(含动手实验),快速浏览约 1.5-2 小时。建议按小节顺序阅读,§17.6 的实验部分需要 GPU 环境。参考项目:
🔧 mjlablift_cube_env_cfg(YAM 机械臂抬方块四种变体)
🔧 Isaac Lab DexSuite + Franka Lift(灵巧手操作内置任务)
✅ DexPBT(github.com/NVIDIA-Omniverse/IsaacGymEnvs中的 DexPBT 配置)
前置自测
📋 答不出 ≥ 3 题 → 先回前置章节复习
- [Ch05] mjlab 的
ObservationManager如何将多个 observation term 拼接为一个 tensor?actor 和 critic 的 observation group 可以不同吗?为什么操作任务中这种不对称更重要? - [Ch06] 指数型奖励 \(r = \exp(-\|e\|^2 / \sigma^2)\) 中 \(\sigma\) 的作用是什么?\(\sigma\) 太小和太大分别导致什么问题?
- [Ch07] RSL-RL 的 PPO 训练中
num_steps_per_env和num_envs如何共同决定 batch size?操作任务的 episode 通常比 locomotion 短还是长? - [物理] 库仑摩擦模型 \(f_t \le \mu f_n\) 中,当法向力 \(f_n\) 接近零时会发生什么?为什么这对夹爪抓取尤其关键?
- [MuJoCo] MuJoCo 中
equality约束的joint类型如何实现两个关节的反向耦合?在夹爪建模中这有什么用途? - [Ch08] Domain Randomization 的四种事件模式(startup / reset / interval / step)分别在什么时机触发?操作任务中物体位姿随机化应该使用哪种模式?为什么?
- [Ch09] Teacher-student 蒸馏中 teacher 看到了 student 看不到的什么信息?在操作任务中这种信息不对称比 locomotion 更严重的原因是什么?
本章目标
学完本章后,你应该能够:
- 解释为什么固定基座操作中 RL 面临的核心挑战不是自由度数量,而是几何窗口的狭窄性和接触的二元性,以及这如何影响奖励、动作空间和训练超参数的设计
- 推导 staged reward 的数学形式,理解 reaching-bringing 的乘法门控为什么优于简单加权,并在 mjlab 源码中定位其实现
- 区分
JointPositionActionCfg和DifferentialIKActionCfg的工程特性,知道操作任务中何时用关节空间、何时用任务空间 - 对比 mjlab YAM lift cube 和 Isaac Lab DexSuite 在场景构建、奖励设计、动作空间和 Domain Randomization 上的设计差异
- 独立完成 YAM 四种任务变体(state / RGB / Depth / multi-cube)的注册验证、zero/random play 和短训练
- 诊断操作任务常见的训练失败模式——从 staged reward 卡在第一阶段到夹爪接触震荡
- 配置操作任务中的接触参数(condim、friction、solref),理解不同配置对抓取稳定性的影响
- 设计从 state 到 depth/RGB 的视觉操作管线,避免 privileged 信息泄漏,正确配置 CNN 编码器和 SpatialSoftmax
本章知识全景图
操作 RL 是一棵知识树——本章覆盖其中"固定基座"这根主干,后续章节(Ch19-Ch21)覆盖各个分支。在开始之前,先看清楚整棵树的结构,知道你现在在哪里:
操作 RL 知识树
├── 【本章】固定基座操作(Ch17)
│ ├── 平行夹爪(YAM lift cube → §17.2)
│ │ ├── State 版 → §17.6
│ │ ├── Depth 版 → §17.10
│ │ └── RGB 版 → §17.10
│ ├── 灵巧手(DexSuite → §17.3)
│ │ ├── In-hand reorientation
│ │ ├── Regrasping
│ │ └── Grasp-and-throw
│ ├── 动作空间(§17.8)
│ │ ├── JointPositionAction(默认)
│ │ ├── DiffIK(运动学层)
│ │ └── OSC(动力学层)
│ └── 接触建模(§17.4)
│ ├── MuJoCo Warp(solref/solimp)
│ └── PhysX(TGS + contact_offset)
├── 四足 + 臂 Loco-Manipulation(Ch19)
│ └── 分层控制(低层 joint + 高层 velocity/EE)
├── 人形全身操作(Ch20)
│ └── 上下体解耦 / 接触阶段分解 / 掩码条件
├── 轮式移动操作(Ch21)
│ └── 非完整约束 + 底盘漂移补偿
└── 前沿方法
├── Diffusion Policy(多模态动作,Ch16)
├── ADR / PBT(自动化 DR + 种群训练)
└── 视觉 Sim-to-Real(VIRAL,大规模蒸馏)
本章的教学路线:先理解"操作和 locomotion 有什么不同"(§17.1),再在 mjlab 中从零到一走通一个完整的操作任务(§17.2),然后看 Isaac Lab 如何处理更复杂的灵巧手(§17.3),深入接触物理的工程细节(§17.4),做系统性的跨域对比(§17.5),跑通最小实验(§17.6),学会诊断操作训练的常见失败(§17.7),理解三种动作空间的选型(§17.8),掌握双框架迁移(§17.9),最后扩展到视觉操作(§17.10)。
17.1 从 Locomotion 到 Manipulation:跨域迁移的结构性差异 ⭐⭐
这一节解决什么问题:建立"操作不是简化版 locomotion"的认知。虽然双框架的 manager-based 架构通用于两类任务,但 MDP 的内在结构完全不同——直接搬用 locomotion 的设计经验会导致系统性的失败。
动机:共享架构,不同灵魂
如果你刚读完 Ch13-14 的 locomotion 实战,自然会想:操作任务不过是换一个机器人、换一组 reward term,框架是一样的嘛。这个想法对了一半——框架确实一样。mjlab 的 ManagerBasedRlEnv、Isaac Lab 的 ManagerBasedRLEnv 都提供 ObservationManager、ActionManager、RewardManager、TerminationManager 和 EventManager。无论是四足还是机械臂,配置文件的骨架结构相同:scene → actions → observations → rewards → terminations → events。
但对了一半意味着错了另一半。MDP 设计哲学在两类任务之间有深刻的结构性差异,不是换几个参数就能桥接的。
六维设计差异对比
| 设计维度 | Locomotion(四足/人形) | Manipulation(机械臂/灵巧手) |
|---|---|---|
| 成功定义 | 持续运动(没有终点) | 状态改变(物体到达目标) |
| 奖励信号 | 连续密集(每步都有速度跟踪误差) | 阶段性(接触前信号弱,接触后信号突变) |
| 关键物理 | 地面接触(大面积、持续、宽容) | 夹爪接触(小面积、瞬时、敏感) |
| observation 核心 | 自身状态(base velocity、joint pos/vel) | 物体状态(object pose、relative position) |
| 动作空间 | 全身协调(12-25 DOF,容错高) | 末端精确(6-7 DOF,容错极低) |
| episode 结构 | 摔倒才终止,通常很长 | 成功/超时终止,通常较短 |
这六个差异不是独立的——它们构成一个因果链。因为成功定义是"改变世界状态",所以策略必须先接触物体;因为接触是瞬时的、敏感的,所以奖励信号在接触前后存在不连续跳变;因为跳变存在,所以需要 staged reward 而非简单加权;因为 staged reward 编码了阶段顺序,所以训练超参数(学习率、batch size)需要适配更尖锐的 reward landscape。
一个跨领域类比:locomotion 的 RL 像学骑自行车——即使骑得歪歪扭扭,只要车在动就在获得平衡反馈,信号是连续的。Manipulation 的 RL 像学投飞镖——在飞镖命中靶心之前,你几乎得不到有用的反馈,信号是阶段性的。不过这个类比在一个关键点上不完全成立:飞镖只有命中和未命中两种状态,而操作任务通常可以被分解为多个有序阶段(接近、接触、夹持、搬运),每个阶段都可以提供密集信号——这正是 staged reward 的设计动机。
如果直接搬用 locomotion 经验会怎样
反事实推理 A:直接复制 locomotion 的 PPO 超参数。 在 locomotion 中,learning rate = 1e-3、num_envs = 4096、num_steps_per_env = 24 是常见配置,reward landscape 相对平滑,大 batch + 大 lr 有利于快速收敛。把这组参数直接用于操作任务,现象是:训练前 100 个 iteration reward 不涨,然后突然跳到一个错误的局部极值——策略学会了把末端移到方块附近但不抓取。根本原因是操作的 reward landscape 在接近→夹取的边界处有尖锐的阶段跳变,大 lr 容易跳过从接近到抓取的窄通道。正确做法:操作任务通常需要更小的 learning rate(1e-4 或更低)和更多的 iteration。
反事实推理 B:放大 action scale 加速探索。 在 locomotion 中,增大 action scale 通常能加速探索——更大的步幅、更快的转向。在操作中,放大 action scale 会让末端在抓取窗口附近剧烈震荡,因为抓取窗口的空间精度要求是毫米级的,而 locomotion 的容错是厘米级的。action scale 放大 2 倍,末端位置精度降低 2 倍,成功率可能降低一个数量级。
反事实推理 C:把 observation 都放在 actor 中。 在 locomotion 中,actor 和 critic 的 observation 差异主要在 privileged terrain 信息。在操作中,状态版和视觉版的 observation 差异更大——状态版 actor 直接看到 ee_to_cube 向量,视觉版 actor 只看到 depth/RGB 图像。如果视觉版 actor 仍然保留了 ee_to_cube(privileged 泄漏),CNN 会被完全忽略,策略不会学到从图像中提取空间信息。这个泄漏在 locomotion 中影响较小(因为 terrain 信息主要给 critic),但在操作中是致命的。
本质洞察:Locomotion 和 manipulation 共享框架但不共享设计直觉。框架的 manager-based 架构是一个通用骨架,真正定义任务特性的是这个骨架上挂载的 observation terms、reward terms、action terms 和 event terms。跨域迁移的关键不是"换一个 robot asset",而是重新思考 MDP 的每一个组件。
操作任务的四阶段因果结构
成功的操作任务通常遵循一个固定的阶段序列,每个阶段是下一阶段的因果前提:
| 阶段 | 物理过程 | RL 学习难点 | 奖励设计要点 |
|---|---|---|---|
| 接近(Reaching) | 末端从初始位置移向目标物体 | 方块位置随机化后搜索空间大 | 末端到物体距离的密集信号 |
| 预抓取(Pre-grasp) | 末端调整到抓取姿态 | 腕部姿态对抓取成功率影响大但梯度弱 | 姿态对齐奖励(可选) |
| 夹持(Grasping) | 夹爪闭合并建立稳定接触 | 接触力从零到非零的不连续跳变 | 夹爪闭合信号或接触力反馈 |
| 搬运(Bringing) | 持握物体移向目标位置 | 必须同时维持抓取力和移动精度 | 物体到目标的距离 + 抬升高度 |
这个阶段分解反映了操作任务的因果结构:接近是夹持的前提,夹持是搬运的前提。如果把四个阶段的奖励简单相加,优化器会在早期完全忽略搬运项(因为物体还没被抓住时搬运误差恒定),只关注接近项。在数学上等价于:策略被吸引到一个"站在方块旁边但不抓取"的局部极值。
接触的二元性:操作的核心物理困难
Locomotion 中的地面接触是持续的、大面积的、力学上宽容的。脚的位置偏差几厘米通常不会导致灾难性失败。操作中的夹爪接触截然不同:它是瞬时的、小面积的、对位姿高度敏感的。
考虑夹爪闭合抓取方块的过程。在夹爪碰到方块之前,接触力为零,方块不会移动——无论夹爪多接近。在夹爪碰到方块的瞬间,接触力从零跳变到非零——物理状态发生了不连续变化。这种不连续性对 RL 的值函数估计造成困难:critic 必须在接触边界两侧给出截然不同的价值预测,而这个边界在状态空间中是一个低维流形(即一个"薄壳"而非一个"区域")。
为什么这种二元性如此重要? 从梯度的角度理解:值函数 \(V(s)\) 在接触边界处有一个近似不连续的跳变。PPO 的 critic 是一个 MLP,它用平滑函数去拟合这个不连续的值景观——在边界附近不可避免地产生大的近似误差。这个误差会通过 GAE 传播到 advantage 估计中,导致 actor 在接触边界附近收到不准确的梯度信号。
用一个日常类比来理解:想象你在用筷子夹花生。在筷子碰到花生之前,你的手指力量完全不影响花生的状态。在筷子碰到花生的瞬间,你突然需要精确控制力的大小和方向——太大会弹飞花生,太小会夹不住。这个"碰到"的瞬间是一个离散事件,不是一个渐进过程。机器人操作面临完全相同的物理困难,只是场景从筷子换成了夹爪。
接触二元性对不同组件的影响:
| 组件 | Locomotion 中的接触 | Manipulation 中的接触 | 工程后果 |
|---|---|---|---|
| 值函数 | 平滑(地面接触持续存在) | 不连续(接触/不接触边界) | 需要更小的 lr、更大的 mini-batch |
| 策略梯度 | 稳定(动作对 reward 的影响连续) | 尖锐(微小动作差异导致接触/不接触) | 需要更保守的 clip ratio |
| 探索 | 安全(随机动作很少致命) | 危险(随机夹爪动作可能弹飞物体) | 需要结构化的分阶段 reward |
| Domain Randomization | 对接触参数不敏感 | 对摩擦/刚度高度敏感 | 需要更保守的 DR 范围 |
从力/位置混合控制到 RL 的历史演进。 接触二元性不是 RL 时代才发现的问题——经典机器人学在 1980 年代就认识到了这个困难。Raibert & Craig (1981) 提出力/位置混合控制,核心思想是在接触方向用力控制、在自由方向用位置控制——手动将二元性分解为两个独立的子问题。Hogan (1985) 的阻抗控制进一步统一了力和位置控制,用虚拟弹簧-阻尼器把接触的不连续性"软化"为连续的阻抗行为。RL 的方法则完全不同——它不显式建模接触,而是让策略通过 staged reward 的梯度信号隐式学习何时接触、如何控制接触力。这种隐式学习的代价是需要大量的样本(4096 并行环境 × 数千 iteration),但优势是不需要精确的接触模型——策略自动适应仿真器的接触特性。
本质洞察:固定基座操作的难点不在于底座不动——难点在于所有成功都发生在一个极窄的几何窗口里。末端位置必须到位(毫米级),夹爪必须在正确时刻闭合(时序敏感),摩擦必须足够大(物理约束),目标必须在可达空间内(运动学约束)。这四个条件中任何一个不满足,整个抓取就失败。这与 locomotion "只要不摔倒就有 reward" 的宽容性形成鲜明对比。
GPU 仿真让操作 RL 从不可行变为分钟级
理解操作 RL 的工程背景必须认识到一个历史性转折:GPU 并行仿真将操作任务的训练时间从"天级"压缩到了"分钟级"。这不是渐进式改善——而是使这类任务从学术实验变为工程可行的根本性变化。
具体数据(来自 NVIDIA 和 mjlab 的公开发布):
| 仿真后端 | 操作任务吞吐 | 相对加速 | 来源 |
|---|---|---|---|
| MJX (JAX) | 基准 | 1× | MuJoCo 官方 |
| MuJoCo Warp (RTX 4090) | ~313× MJX | 313× | NVIDIA Developer Blog |
| MuJoCo Warp (RTX PRO 6000 Blackwell) | ~475× MJX | 475× | NVIDIA GTC 2026 |
| Newton Beta (MuJoCo Warp vs PhysX) | 65% faster in-hand dex manipulation | — | NVIDIA Isaac Lab + Newton |
实际效果:mjlab 作者 Kevin Zakka 报告,YAM 机械臂 lift cube 任务"can solve cube lifting with the YAM arm from 32×32 RGB frames in about <5 minutes of wall-clock time"。这意味着一个包含视觉输入的操作任务可以在一杯咖啡的时间内完成——这使得快速迭代 reward 设计和超参数调优成为可能。
操作 RL 中的算法选型:PPO vs SAC
在 locomotion 中,PPO + 大规模并行几乎没有竞争对手(回顾 Ch07)。但操作任务的情况更微妙——在某些场景下,off-policy 算法有理论上的优势。
PPO 在操作中的工程优势:4096 个并行环境每步产生 4096 个转移样本,on-policy 的 PPO 可以直接利用这些样本更新策略。这与 GPU 仿真的高吞吐天然匹配——用更多的环境换取更大的有效 batch size,而不是用 replay buffer 重复利用旧样本。mjlab 和 Isaac Lab 的所有内置操作任务都默认使用 PPO + RSL-RL。
SAC 在操作中的理论优势:操作任务的 reward 景观更尖锐(接触二元性),SAC 的最大熵目标鼓励更均匀的探索,理论上有助于发现窄通道中的高 reward 区域。此外,HER(Hindsight Experience Replay)只能在 off-policy 框架中使用——如果你想用稀疏 reward + HER 来避免 reward 工程,必须选 SAC。
工程结论:本章选择 PPO + staged reward,因为这是双框架(mjlab + Isaac Lab)的默认配置,工程复杂度最低。如果你有特定理由(如稀疏 reward 不可避免),SAC + HER 是一个合理的替代方案,但需要自行集成 off-policy 后端——这不在 RSL-RL 的内置支持范围内(回顾 Ch07 的多算法对比)。
操作任务中的 Reward Shaping 穷举分类
操作任务的 reward 设计可以从四个维度系统分类,而非随意列举几种方案:
| 维度 | 选项 | 典型例子 | 适合场景 |
|---|---|---|---|
| 信号密度 | 稀疏 / 密集 / 分阶段 | 最终成功 / 距离衰减 / staged | 稀疏适合简单任务,密集适合复杂任务 |
| 组合方式 | 加法 / 乘法门控 / 最大值 | \(\sum w_i r_i\) / \(r_1(1+r_2)\) / \(\max(r_1, r_2)\) | 加法简单但无优先级,乘法有因果门控 |
| 坐标系 | 世界系 / 物体系 / 末端系 | 全局距离 / 物体帧相对位置 / 末端帧偏差 | 世界系泛化差,物体系泛化好 |
| 时间结构 | 即时 / 累积 / 终端 | 每步距离 / episode 总距离 / 最终成功 | 即时利于 credit assignment |
mjlab 的 staged reward 选择了"密集 + 乘法门控 + 世界系 + 即时"的组合。这不是唯一正确的选择——hindsight experience replay(HER)使用"稀疏 + 加法 + 物体系 + 终端"的组合,在某些任务上也很有效。但 staged reward 的优势在于它不需要额外的 replay buffer 机制,直接在 on-policy PPO 中工作。
如果使用 HER 而非 staged reward 会怎样?HER 需要 off-policy 算法(如 SAC),因为它通过重新标注已完成 episode 的 goal 来增加成功样本。这意味着你不能直接用 RSL-RL 的 PPO——需要切换到 SAC 后端或自定义 replay buffer。工程复杂度大幅增加,且失去了 GPU 大规模并行仿真的吞吐优势(回顾 Ch07:SAC 的 replay buffer 是 off-policy 的天然匹配但不天然适合大规模并行 on-policy 数据收集模式)。
从工业机器人到学习型操作的三代演进
固定基座操作的方法论经历了三代演进,理解这个历史有助于理解 YAM 配置中每个设计决策的来源:
第一代(1980-2010):基于模型的规划与控制。 给定精确的物体模型、摩擦系数、抓取几何,用解析方法计算抓取姿态和力闭合条件。代表工作包括 Ferrari & Canny 1992(力闭合指标)和 Miller et al. 2004(GraspIt! 规划器)。优势:可解释、可验证、对已知物体精度高。局限:需要精确模型、无法泛化到未见物体、对不确定性敏感。
第二代(2015-2020):端到端深度学习。 用大量数据训练卷积网络直接从图像预测抓取位置或动作。代表工作包括 Levine et al. 2016(visuomotor policy)和 Mahler et al. 2017(Dex-Net)。优势:能处理未见物体、泛化能力强。局限:需要大量数据、缺乏可解释性、对分布外输入脆弱。
第三代(2020-至今):结构化学习。 结合经典方法的结构先验(如动作空间设计、奖励分解、课程学习)和深度 RL 的适应能力。代表工作包括 robosuite(Zhu et al. 2020)和 mjlab 的 YAM 配置。优势:训练更高效、行为更可解释。局限:需要领域专家设计结构先验。
YAM lift cube 属于第三代。staged reward 是结构先验(编码了阶段顺序),JointPositionAction 是动作空间先验(利用了关节控制的物理结构),vision cfg 的 privileged removal 是信息边界先验(强制视觉学习)。理解这一点很重要——你不是在"从零让 RL 学会抓取",而是在"用结构先验引导 RL 高效学会抓取"。
正在崛起的第四代:基础模型 + RL。 2024-2026 年,操作领域正在经历一个新的范式转换——将大语言模型/视觉-语言模型的知识作为先验,用 RL 做物理层的 fine-tuning。代表工作包括 RT-2 (Google DeepMind 2023)、π0 (Physical Intelligence 2025) 和 LeVERB (OpenDriveLab 2026)。LeVERB 的架构特别有代表性:高层使用 VLM(Vision-Language Model)从自然语言指令生成 latent action tokens,低层使用 RL 训练的 whole-body controller 把 latent tokens 转化为实际关节动作。这种"高层语义规划 + 低层物理控制"的分层架构,本质上是第三代"结构先验 + RL"思想的自然延伸——只是结构先验从人工设计的 reward 变成了从互联网规模数据中学到的 semantic knowledge。
本章不深入第四代方法(它们需要 VLM 和大规模预训练的背景知识),但需要理解:第三代方法(本章的内容)是第四代的基础。不管高层规划多么 fancy,低层的关节控制、接触建模、reward 设计的工程挑战不会消失——它们只是被封装在低层 controller 中。学好本章,你就拥有了构建第四代系统低层部分的工程能力。
操作 RL 方法演进的关键时间节点:
1987 — Khatib: Operational Space Control (OSC)
1992 — Ferrari & Canny: Force Closure 指标
2016 — Levine et al.: End-to-end visuomotor policy
2017 — Mahler et al.: Dex-Net (data-driven grasping)
2018 — Andrychowicz et al.: Dexterous In-Hand Manipulation (OpenAI)
2019 — OpenAI: Solving Rubik's Cube (ADR 首次提出)
2020 — Zhu et al.: robosuite benchmark
2021 — Makoviychuk et al.: Isaac Gym (GPU-accelerated RL)
2022 — Handa et al.: DeXtreme (灵巧手 sim-to-real)
2023 — Petrenko et al.: DexPBT (Population-Based Training)
2023 — Chi et al.: Diffusion Policy (多模态操作策略)
2025 — NVIDIA: VIRAL (大规模视觉 sim-to-real)
2026 — mjlab: YAM lift cube (<5 min 从 RGB 训练)
这条时间线清晰地展示了"计算"如何推动操作 RL 的进步:从 2016 年的单机 CPU 训练到 2026 年的单 GPU 五分钟收敛,训练效率提升了约 10,000 倍。这个加速不是来自算法本身(PPO 从 2017 年到现在几乎没变),而是来自物理引擎的 GPU 并行化(MuJoCo Warp、PhysX GPU)和框架的工程优化(mjlab 的 zero-copy TorchArray、Isaac Lab 的 tiled rendering)。
⚠️ 常见陷阱
⚠️ 编程陷阱:直接复制 locomotion 的 PPO 超参数
错误做法:把 locomotion 中 learning rate = 1e-3、batch size = 4096×24 的配置直接用于操作任务。
现象:训练前 200 iteration reward 不涨,然后跳到局部极值(只学会接近但不抓取)。
根本原因:操作的 reward landscape 有尖锐的阶段边界,大 lr 容易跳过接近→抓取的窄通道。
正确做法:操作任务通常需要更小的 learning rate(1e-4)和更多 iteration。先确认 reaching 阶段能学会,再关注 grasping 和 bringing。
💡 概念误区:认为固定基座比浮动基座"更简单"
新手想法:固定基座只有 6-7 个自由度,比 18-DOF 四足简单多了。
实际上:自由度数量只影响优化问题的维度,不影响问题的结构性困难。6-DOF 臂在 5mm 精度下抓取 2cm 方块,比 18-DOF 四足在 5cm 精度下踩粗糙地面更难——前者的成功集在状态空间中的体积远小于后者。
🧠 思维陷阱:只看总 reward 不看分项
新手想法:总 reward 在涨,说明训练在进步。
实际上:staged reward 中 reaching 项的快速上升可能掩盖 bringing 项的停滞。如果 reaching 从 0 涨到 0.9 而 bringing 始终为 0,总 reward \(0.9 \times (1 + 0) = 0.9\) 看起来不错,但策略只学会了接近而没学会搬运。必须把 reach、grasp、bring 三项指标分开记录。
⚠️ 编程陷阱:object pose 随机化的范围设置不对称
一个容易忽略的问题:如果方块初始位置只在机械臂正前方随机化,策略会学到一个偏向前方的运动模式。当方块出现在侧方时策略完全失效。正确做法是在工作空间的半球范围内均匀随机化物体位置,覆盖机械臂的全部可达空间。
练习
- [估算题] 一个 6-DOF 机械臂的关节空间是 \(\mathbb{R}^6\),每个关节有效范围约 4 rad。假设方块的抓取窗口在关节空间中约 \(0.1^6\) rad\(^6\)。随机探索一次命中抓取窗口的概率大约是多少?这解释了为什么稀疏奖励下操作任务的探索如此困难。
- [设计题] 如果你要把 locomotion 的 velocity tracking 任务和 manipulation 的 lift cube 任务合并为一个 loco-manipulation 任务(Ch19 的主题),列出至少 4 个需要重新设计的 MDP 组件,并解释为什么不能直接拼接。
- [跨章综合题] 回顾 Ch06 的 reward curriculum 和 Ch08 的 Domain Randomization。设计一个"先学接近、后学搬运"的课程方案,说明如何用 curriculum 逐步开启搬运阶段的 reward 权重,以及 DR 中物体质量的随机化范围应该如何随训练进度调整。
上节建立了"操作不是简化版 locomotion"的认知。但知道差异还不够——我们需要看到这些差异如何具体体现在框架的配置文件中。这正是下一节的主题:通过 mjlab 的 YAM lift cube 任务,从源码级别理解操作任务的 MDP 工程实现。
17.2 mjlab 机械臂操作:YAM Lift Cube 全流程 ⭐⭐
这一节解决什么问题:以 mjlab 的 YAM(Yet Another Manipulator)lift cube 为例,从 MJCF 资产到训练管线,走通固定基座操作的完整工程闭环。
动机:为什么选 YAM 作为入门
mjlab 的 manipulation 入口是 YAM lift cube 任务,它提供了四种变体:
| 变体 | Task ID | Actor 输入 | 特点 |
|---|---|---|---|
| State | Mjlab-Lift-Cube-Yam |
关节状态 + 物体位姿 + 目标位姿 | 最基础,纯 MLP |
| RGB | Mjlab-Lift-Cube-Yam-Rgb |
关节状态 + RGB 图像 | CNN 视觉 |
| Depth | Mjlab-Lift-Cube-Yam-Depth |
关节状态 + 深度图 | CNN 视觉 |
| Multi-Cube | Mjlab-Multi-Cube-Seg-Yam |
关节状态 + 深度图 + 分割掩码 | 多目标选择 |
四种变体共享同一个 MJCF 资产和 staged reward 框架,差异仅在 observation group 和视觉输入配置。这种"一个任务四种切面"的设计是理解操作 MDP 的理想起点——你可以在固定其他变量的前提下,单独观察输入模态对训练的影响。
YAM MJCF 资产解析
YAM 是 mjlab 内置的 7-DOF 机械臂(6 个臂关节 + 1 个主动夹爪关节),其 MJCF 定义位于 i2rt_yam/xmls/yam.xml。MJCF 中几个关键元素与操作任务直接相关:
grasp_site:末端执行器的抓取参考点。 这是一个 MuJoCo site(不产生碰撞和惯性的虚拟标记),定义了"末端在哪"。所有 reaching reward 和 DiffIK 计算都以这个 site 为参考。如果 grasp_site 的位置不在夹爪的物理抓取中心,reaching reward 会给出错误的梯度方向——策略会把 grasp_site 移到方块位置,但夹爪实际并没有对准方块。
joint equality:夹爪耦合约束。 YAM 的夹爪有两个手指,但只有一个主动关节。第二个手指通过 MuJoCo 的 equality/joint 约束与第一个手指反向耦合(polycoef="0 -1"),实现"一个 action 控制双指同步开合"。这把 action 维度从 8 降到 7。
freejoint 方块:可自由运动的操作对象。 方块用 freejoint 挂在 worldbody 下,有 7 个自由度(3 平移 + 4 四元数)。这与 locomotion 中所有 body 都在同一个 kinematic tree 上不同——方块是一个独立的动力学实体,只通过接触力与机械臂交互。
碰撞几何与视觉几何的分离。 MJCF 允许同一个 body 有不同的碰撞几何(geom with contype/conaffinity)和视觉几何(geom with rgba)。对 YAM 夹爪,碰撞几何通常用简单的 box 近似(快速碰撞检测),视觉几何用更精细的 mesh(好看但不参与物理)。一个常见错误是修改了视觉几何但没同步修改碰撞几何——在可视化中夹爪看起来对准了方块,但碰撞几何(看不到的 box)其实没有接触到方块。自检方法:在 viser 中切换到碰撞几何可视化模式,确认碰撞体和视觉体一致。
桌面与工作空间。 MJCF 中的桌面通常是一个带 contype=1 的静态 geom(不可移动),它定义了方块的支撑面。桌面高度和方块初始位置的关系决定了 reset 时是否会出现穿透。一个工程经验:在 MJCF 中,方块底面到桌面顶面应至少有 margin(默认 0.001m)的间距,否则接触求解器可能产生不合理的初始力。
这个类比可以帮助理解 MJCF 结构:MJCF 之于操作任务,就像舞台布景之于戏剧。articulation(机械臂)是演员,freejoint(方块)是道具,site(grasp_site、target)是灯光标记。布景的物理位置决定了演员的表演空间——grasp_site 偏了,"演员"再努力也碰不到"道具"。
验证 MJCF 资产的三步检查
在开始任何训练之前,用以下步骤确认 MJCF 资产的物理行为正确。这三步共花 5 分钟,能避免数小时的调试:
Step 1:确认 grasp_site 位置。 在 viser 中加载 YAM 资产,把机械臂移到默认姿态。确认 grasp_site 的可视化标记(通常是一个小球)位于两个夹爪指尖之间的中心。如果偏移超过 1cm,reaching reward 会给出错误的梯度方向。
Step 2:确认夹爪耦合。 在 viser 中手动发送 action,只改变夹爪关节(第 7 维)。确认两个手指同步运动(一个开时另一个也开)。如果不同步,检查 MJCF 中的 equality/joint 约束是否正确。
Step 3:确认方块物理。 把方块放在桌面上方 10cm 处,释放。确认方块自由落体后在桌面上弹跳然后静止,而不是穿透桌面或无限弹跳。自由落体 10cm 应耗时约 \(\sqrt{2 \times 0.1 / 9.81} \approx 0.14\) 秒——如果明显偏离这个值,检查 timestep 和方块质量设置。
Observation 设计:状态版 MDP 定义
YAM state 版的 observation 包含以下 term:
# actor observation group
arm_joint_pos # [num_envs, 7] 关节位置(含夹爪)
arm_joint_vel # [num_envs, 7] 关节速度
ee_pos_w # [num_envs, 3] 末端世界位置
ee_quat_w # [num_envs, 4] 末端世界姿态
ee_to_cube # [num_envs, 3] 末端到方块向量
cube_pos_w # [num_envs, 3] 方块世界位置
cube_quat_w # [num_envs, 4] 方块世界姿态
cube_to_goal # [num_envs, 3] 方块到目标向量
last_action # [num_envs, 7] 上一步动作
对比 locomotion 的 observation(回顾 Ch05:locomotion 的 actor observation 核心是 base_lin_vel、base_ang_vel、projected_gravity、joint_pos、joint_vel、commands),操作的 observation 有三个关键差异:
差异一:无 base velocity。 固定基座不动,不需要 base_lin_vel 和 base_ang_vel。基座姿态是常量,不进入 observation。
差异二:有 object pose。 Locomotion 中策略只需要感知自身状态和地形。操作中策略必须感知物体状态——方块在哪、目标在哪、末端和方块的相对关系。这些"世界状态"信息在 locomotion 中不存在。
差异三:relative vector 的重要性更高。 ee_to_cube 和 cube_to_goal 是两个相对向量,它们编码了任务的阶段信息:ee_to_cube 范数小说明 reaching 完成,cube_to_goal 范数小说明 bringing 完成。这两个 term 对训练效率至关重要——如果只给绝对位置而不给相对向量,策略需要自己学会做减法,收敛会慢很多。
如果不在 observation 中包含 ee_to_cube 会怎样?策略仍然可以从 ee_pos_w 和 cube_pos_w 中计算出这个向量,但 MLP 需要额外的网络容量来学习这个线性关系。实验表明,缺少 relative vector 会把 reaching 阶段的收敛时间增加 2-3 倍。这不是理论问题——这是 MLP 的归纳偏置限制。MLP 不天然擅长精确的向量减法运算。
Actor Observation 维度计算
计算总维度是工程验证的第一步——如果你预期的维度和实际创建的网络输入维度不一致,说明某个 term 的 shape 有问题:
| Term | 维度 | 说明 |
|---|---|---|
arm_joint_pos |
7 | 6 臂关节 + 1 夹爪 |
arm_joint_vel |
7 | 同上 |
ee_pos_w |
3 | 末端世界位置 |
ee_quat_w |
4 | 末端世界姿态(四元数) |
ee_to_cube |
3 | 末端到方块向量 |
cube_pos_w |
3 | 方块世界位置 |
cube_quat_w |
4 | 方块世界姿态 |
cube_to_goal |
3 | 方块到目标向量 |
last_action |
7 | 上一步动作 |
| 合计 | 41 |
验证方法:在训练开始前打印 actor_obs.shape,确认第二维是 41。如果不是,检查 obs group 配置中是否有遗漏或多余的 term。
Observation 归一化的工程考量。 操作任务的 observation 各 term 值域差异很大:joint_pos 在 [-2, 2] rad 范围,cube_pos_w 可能在 [0, 0.5] m 范围,cube_quat_w 在 [-1, 1] 范围。如果不做归一化,MLP 的不同输入神经元接收到的值域不均匀——大值域的 term 会主导梯度方向,小值域的 term 被"忽略"。
RSL-RL 提供了 EmpiricalNormalization——在训练过程中维护每个 observation 维度的 running mean 和 running std,对输入做 (obs - mean) / std 归一化。对操作任务,归一化通常帮助加速收敛 20-50%。但需要注意:deploy 时必须把训练期间的 mean/std 一起导出(它们被 bake 进 ONNX 文件),否则推理时的 observation 值域与训练时不一致。
Critic Observation 的 privileged 信息。 Asymmetric actor-critic 架构(回顾 Ch07)让 critic 看到比 actor 更多的信息。对操作任务,典型的 critic 额外 observation 包括:
| Privileged Term | 维度 | 来源 | actor 版为什么不能用 |
|---|---|---|---|
contact_force |
6 | 接触力传感器(仿真器) | 真实夹爪可能没有力传感器 |
object_velocity |
6 | 仿真器状态 | 真实场景中难以实时估计 |
true_friction |
1 | DR 参数 | 未知物体的摩擦系数不可测 |
true_mass |
1 | DR 参数 | 未知物体的质量不可测 |
Critic 通过这些 privileged 信息可以更准确地估计值函数,从而为 actor 提供更好的 advantage 估计。但 actor 绝不能接触这些信息——否则在真实部署中策略会失效(因为这些信息不可用)。这是操作任务中 privileged 泄漏 bug 的根源。
对比 locomotion 的 Go1 actor observation(约 48 维,包含 12 个关节 × 2 + base states + commands),操作的 41 维虽然略少,但信息结构更复杂——包含了自身、物体和目标三个实体的状态。
critic observation 通常在 actor observation 基础上增加 privileged 信息:物体速度、接触力、目标精确位置等。这种 asymmetric actor-critic 架构(回顾 Ch09:teacher-student 的核心理念)在操作任务中尤其重要——因为操作中 privileged 信息(如接触力大小)对值函数估计的帮助比 locomotion 中的 terrain heights 更大。
操作 Observation 的信息论视角
状态版 observation 的设计隐含了一个信息论问题:策略需要多少比特的有效信息来做出好的决策?在操作任务中,关键信息可以分为三个层次:
自身状态(约 20 维):关节位置、速度、夹爪状态。这些信息告诉策略"我的手臂在哪里、在做什么"。没有这些,策略甚至不知道当前的关节配置——任何有意义的动作都无法计算。
任务相关几何(约 6 维):ee_to_cube(3D)和 cube_to_goal(3D)。这是操作任务的核心信息——它告诉策略"物体在哪里、目标在哪里"。注意这 6 维信息的信息密度极高:它压缩了物体和目标的全部空间信息到最小必要表示。
历史上下文(约 7 维):上一步动作 \(a_{t-1}\)。帮助策略推断当前的运动趋势(类似于速度估计),避免动作跳变。
总共约 33 维的低维状态已经足以完全描述操作的决策问题。相比之下,一张 \(64 \times 64\) 的 RGB 图像有 12288 维——但其中与操作决策相关的有效信息可能只有几十比特(物体的位置、大小、颜色)。视觉版操作策略(§17.10)的核心挑战正是从这 12288 维的高维冗余输入中提取那几十比特的有效信息。这也解释了为什么 SpatialSoftmax 在操作视觉中如此有效——它把整张图像压缩为每个通道的"注意力中心"坐标,这本质上就是从高维图像中提取低维的空间位置信息。
命令系统:LiftingCommand
Locomotion 的命令是 velocity(vx、vy、yaw_rate),策略目标是跟踪这些速度。操作的命令是 goal position(目标抬升高度或目标位姿),策略目标是把物体移到指定位置。
mjlab 的 LiftingCommandCfg 定义了目标位置的采样方式:
# LiftingCommandCfg 的关键参数
goal_pos_range = {
"x": (-0.1, 0.1), # 相对于物体初始位置
"y": (-0.1, 0.1),
"z": (0.1, 0.3), # 抬升高度范围
}
resampling_time = 5.0 # 每 5 秒重新采样目标
注意 resampling_time 的作用:它控制多久更换一次目标位置。如果 resampling_time 太短(如 1 秒),策略可能来不及完成一次完整的 reach-grasp-bring 序列就被更换目标——这会干扰学习。如果太长(如 20 秒),episode 内只有一个目标,泛化性不足。5 秒是一个常见的平衡值。
一个与 locomotion 命令系统的关键差异:locomotion 的命令是持续的(速度目标一直存在),操作的命令是目标性的(达到目标后任务可以终止或重采样)。这种差异影响 termination 设计——操作任务通常有"成功终止"条件(物体距目标小于阈值),而 locomotion 没有。
Command → Reward → Termination 三角契约。 在 manager-based 架构中,这三个组件必须在语义上一致:
LiftingCommand 采样 goal_pos
↓ 传递给
Reward 计算 cube_to_goal = cube_pos - goal_pos
↓ 使用同一个
Termination 检查 ||cube_to_goal|| < success_threshold
如果 Command 的采样坐标系和 Reward 的坐标系不一致(如 Command 在世界系采样,但 Reward 在环境局部坐标系计算),三者的"目标"就不是同一个点。这个 bug 在单环境中不会暴露(世界系和局部系相同),但在多环境中会导致"策略在某些环境中成功、在另一些环境中失败"的诡异行为。
成功终止的工程影响。 当物体到达目标附近时,success_termination 触发 episode 结束。这与 locomotion 的超时终止有根本区别——成功终止会缩短 episode 长度,减少每个 episode 的步数。这对 PPO 的 rollout 采集有影响:如果策略学得很好,episode 变得很短(如 50 步就成功),4096 个环境的 rollout 长度可能远小于 num_steps_per_env——导致 rollout buffer 中大量样本来自"已成功的短 episode",缺乏"还在探索的长 episode"的多样性。一种常见的解决方案是成功后自动重采样目标(而非终止 episode),让策略在一个 episode 内连续完成多次 pick-and-place。
默认关节位姿(default_joint_pos)的选择。 JointPositionAction 的 offset 使用 MJCF 中定义的 keyframe 作为默认关节位姿。这个位姿应该是机械臂的"悬空准备姿态"——末端大致在工作空间中心、夹爪张开、关节远离极限。如果默认位姿让机械臂"瘫倒"在桌面上,策略的第一步就需要学会"站起来",浪费大量训练样本。好的默认位姿让策略从一开始就在合理的工作区间内探索。
Action 设计:关节空间 vs 任务空间
YAM lift cube 默认使用 JointPositionActionCfg,action 维度为 7(6 个臂关节目标 + 1 个夹爪开合目标)。策略输出 \(a \in [-1, 1]^7\),经过 scale 和 offset 映射到关节目标位置,然后由 PD 控制器跟踪。
具体的 action 处理流程:
策略输出 a ∈ [-1,1]^7
↓ × action_scale (默认 0.1)
关节增量 Δq ∈ R^7
↓ + default_joint_pos(关节默认位置)
关节目标 q_target ∈ R^7
↓ PD 控制器
τ = Kp × (q_target - q_current) + Kd × (0 - dq_current)
↓ 执行力矩
物理仿真更新
action_scale = 0.1 意味着策略的满量程输出(±1)对应 ±0.1 弧度的关节增量。对 YAM 的典型关节范围(约 ±2 弧度),这意味着每步最多移动关节范围的 5%。这个保守的设计是有意为之的——操作任务的末端定位精度要求高,大幅度的关节运动会导致末端轨迹不稳定。
夹爪 action 的特殊处理。 虽然夹爪和臂关节在 action tensor 中共享同一个向量,但它们的物理意义完全不同。臂关节控制空间位置,夹爪关节控制"开"还是"合"。策略需要学会在正确时刻输出夹爪关节的"合"信号(action 的第 7 维接近 -1 表示闭合,接近 +1 表示张开,具体取决于关节定义方向)。这个离散式决策(开/合)被连续动作空间"软化"了——策略可以输出中间值表示"半合"。但实际上,成功的抓取通常需要夹爪完全闭合,因此策略最终会学到在第 7 维输出接近 ±1 的值。
mjlab 还提供了 DifferentialIKActionCfg(DiffIK)作为可替换的动作抽象。DiffIK 让策略在末端笛卡尔空间输出 \((\Delta x, \Delta y, \Delta z, \Delta \text{roll}, \Delta \text{pitch}, \Delta \text{yaw})\),然后用 Jacobian 伪逆将其转换为关节速度。
| 特性 | JointPositionAction | DifferentialIKAction |
|---|---|---|
| action 维度 | 7(关节目标 + 夹爪) | 6+1(笛卡尔增量 + 夹爪) |
| 学习难度 | 需要隐式学习逆运动学 | IK 由框架处理,策略只需规划末端轨迹 |
| 灵活性 | 可利用冗余自由度(如肘部姿态) | 末端优先,冗余自由度被伪逆解的最小范数性质约束 |
| 适用场景 | 需要全身协调或冗余利用 | 末端位姿精度优先 |
| 工程复杂度 | 简单——直接关节目标 | 需要 Jacobian 计算 + 奇异值处理 |
当前 YAM lift cube 选择 JointPositionAction 的原因是:抬方块不需要精确的末端轨迹控制,关节空间动作足够;JointPositionAction 的工程复杂度更低,适合作为操作入门。
本质洞察:动作空间的选择不是"哪个更先进"的问题,而是"任务需要什么层次的抽象"的问题。JointPositionAction 让策略在关节空间自由探索——好处是灵活,坏处是搜索空间大。DiffIK 把搜索空间压缩到末端笛卡尔空间——好处是效率高,坏处是丧失了冗余自由度的利用能力。这与编程中"高级语言 vs 汇编"的选择类似:高级语言(DiffIK)更易用但限制更多,汇编(关节空间)更灵活但更难驾驭。
Regularization Reward Terms
除了 staged reward 这个核心 reward,操作任务还需要几个 regularization terms 来防止策略学到不合理的行为:
| Regularization Term | 数学形式 | 典型权重 | 作用 |
|---|---|---|---|
action_rate_l2 |
\(-\|a_t - a_{t-1}\|^2\) | 0.01-0.05 | 防止动作震荡 |
joint_acc_l2 |
\(-\|\ddot{q}\|^2\) | 0.001-0.01 | 防止关节加速度过大 |
joint_pos_limits |
\(-\sum \max(0, q - q_{\max})^2\) | 0.1 | 惩罚接近关节极限 |
这些 regularization 与 locomotion 中的完全相同(回顾 Ch06),是 manager-based 架构的可复用组件。唯一的差异是权重:操作任务的 action_rate_l2 权重通常比 locomotion 大 2-5 倍,因为操作对动作平滑性更敏感——夹爪的快速开合会导致物体在手中滑动。
Staged Reward 数学形式与源码精读
回顾 17.1 节的讨论:操作任务不能用简单加权求和。mjlab 的 staged_position_reward 用乘法门控解决这个问题。
数学形式:
其中 \(p_{\text{ee}}\) 是 grasp_site 位置,\(p_{\text{obj}}\) 是方块质心,\(p_{\text{goal}}\) 是目标位置。\(\sigma_{\text{reach}}\) 和 \(\sigma_{\text{bring}}\) 是高斯核标准差,控制奖励的"软半径"。
分析其在不同阶段的行为:
| 阶段 | \(r_{\text{reach}}\) | \(r_{\text{bring}}\) | \(r_{\text{staged}}\) | 梯度方向 |
|---|---|---|---|---|
| 末端远离方块 | ≈ 0 | ≈ 0 | ≈ 0 | 减小 \(\|p_{\text{ee}} - p_{\text{obj}}\|\) |
| 末端靠近方块 | ≈ 1 | ≈ 0 | ≈ 1 | 减小 \(\|p_{\text{goal}} - p_{\text{obj}}\|\) |
| 方块接近目标 | ≈ 1 | ≈ 1 | ≈ 2 | 维持两者 |
关键机制在第二行:当 \(r_{\text{reach}} \approx 1\) 时,总 reward 变为 \(1 + r_{\text{bring}}\),bringing 的梯度被完整传递。而在第一行,即使 \(r_{\text{bring}}\) 有微弱梯度,它被接近零的 \(r_{\text{reach}}\) 乘法抑制——策略只关注接近。这就是门控的核心:先满足前序条件,后续阶段的信号才"开门"。
如果不用 staged reward 而用等权加和?同一个"末端靠近方块但不抓取"的状态,加和 reward 约 \(0.8 + 0.2 = 1.0\);而抓起并搬运的 reward 为 \(1.0 + 0.9 = 1.9\)。看似差异存在,但实际训练中 reaching 的梯度远强于 bringing(因为 reaching 误差变化快),策略会卡在"接近但不抓取"。用 staged reward,同一状态只有 \(0.8 \times (1+0.2) = 0.96\),而抓起搬运为 \(1.0 \times (1+0.9) = 1.9\)——乘法门控放大了两种策略的 reward 差距,打破了局部极值。
源码中的实现(staged_position_reward 函数的核心逻辑):
# 计算末端到物体的距离奖励(mjlab 源码:平方距离 + 高斯核)
reach_error = torch.sum(torch.square(ee_pos - obj_pos), dim=-1)
reach_reward = torch.exp(-reach_error / reaching_std**2)
# 计算物体到目标的距离奖励
bring_error = torch.sum(torch.square(goal_pos - obj_pos), dim=-1)
bring_reward = torch.exp(-bring_error / bringing_std**2)
# 门控组合
reward = reach_reward * (1.0 + bring_reward)
注意 mjlab 当前 staged_position_reward 用的是平方距离高斯核 exp(-sum(square(Δ)) / std**2),不是 L1 版本。高斯核的梯度性质:误差大时 reward 接近 0 且梯度也很小,误差趋近 0 时 reward 趋近 1、梯度同样趋近 0(在零误差处梯度消失)。这意味着"最后一公分"的精修主要靠 std(核宽)控制——std 越小,核越尖锐、近距离梯度越强。把 std 调小可以加强接近阶段的驱动,但过小会让远处梯度过弱、早期难以收敛。
为什么选择 exponential kernel 而非 linear 或 quadratic? 三种常见的距离→reward 映射各有特性:
| 映射函数 | 数学形式 | 梯度行为 | 适合场景 |
|---|---|---|---|
| Linear | \(r = \max(0, 1 - d/d_{\max})\) | 恒定梯度,\(d > d_{\max}\) 时梯度为零 | 简单任务,已知工作空间范围 |
| Quadratic | \(r = 1 - (d/d_{\max})^2\) | 远处梯度大、近处梯度小 | 需要远距离强引导 |
| Exponential | \(r = \exp(-d/\sigma)\) | 处处非零梯度,近处 reward 变化快 | 操作任务(需要精确到位) |
Exponential 的关键优势是它在任意距离下都提供非零梯度——策略无论从多远开始都有信号。Linear 在 \(d > d_{\max}\) 外没有梯度(策略"看不到"目标),Quadratic 在远处梯度虽大但 reward 值为负(违反直觉)。
σ 参数的标定方法论。 σ 不是随意设定的——它应该与任务的空间尺度匹配。以下是标定 σ 的工程方法:
-
确定特征距离 \(d_{\text{char}}\):对 reaching,\(d_{\text{char}}\) 是机械臂工作空间半径的 1/3-1/2(末端到方块的典型初始距离)。对 bringing,\(d_{\text{char}}\) 是目标位置到方块初始位置的典型距离。
-
设置 σ 使 \(\exp(-d_{\text{char}}/\sigma) \approx 0.3-0.5\):这确保在特征距离处策略能获得适中的 reward 值(既不为零也不饱和)。由此推出 \(\sigma \approx d_{\text{char}} / \ln(2)\) 到 \(d_{\text{char}} / \ln(3)\)。
-
实际示例:YAM 工作空间半径 ≈ 0.5m,初始末端-方块距离 ≈ 0.15m。因此 \(\sigma_{\text{reach}} \approx 0.15 / 1.0 \approx 0.15\)。方块初始位置到目标距离 ≈ 0.2m,因此 \(\sigma_{\text{bring}} \approx 0.2 / 1.0 \approx 0.2\)。
-
验证方法:训练 500 iteration 后检查 reaching reward 的均值。如果 < 0.1(σ 太小,信号太弱),增大 σ;如果 > 0.9(σ 太大,已饱和无分辨力),减小 σ。
关键配置文件结构
YAM lift cube 的配置文件组织如下(以 state 版为例):
src/mjlab/tasks/manipulation/
├── config/
│ └── yam/
│ ├── __init__.py # task id 注册
│ ├── lift_cube_env_cfg.py # scene + action + obs + reward + term + event
│ └── rl_cfg.py # PPO 超参数
└── rewards/
└── manipulation_rewards.py # staged_position_reward 等
lift_cube_env_cfg.py 中的 make_lift_cube_env_cfg 函数返回完整的环境配置。与 locomotion 的 velocity_env_cfg 相比,关键差异包括:
| 配置项 | Locomotion (Go1) | Manipulation (YAM) |
|---|---|---|
| scene | 机器人 + terrain | 机器人 + 方块 + 目标可视化 |
| actions | JointPositionActionCfg(12 leg joints) |
JointPositionActionCfg(6 arm + 1 gripper) |
| observations | base_vel, joint, gravity, commands | joint, ee, object, goal, relative |
| rewards | tracking + regularization | staged + regularization |
| terminations | 摔倒、超时 | 成功、超时 |
| events | push, mass_rand, friction_rand | object_pose_rand, goal_rand |
| commands | velocity command (vx, vy, yaw) | lifting command (goal height) |
⚠️ 常见陷阱
⚠️ 编程陷阱:grasp_site 命名或位置错误
如果在 MJCF 中 grasp_site 的 pos 不在夹爪物理抓取中心,ee_to_cube 的计算结果会系统性偏移。策略可能学会让 grasp_site 到达方块位置,但夹爪实际没有对准。自检方法:在 viser 中用 zero agent 查看 grasp_site 标记(通常是一个小球),确认它在两个夹爪指尖之间。
💡 概念误区:把 DiffIK 当成 YAM 的默认动作空间
看到仓库有 DifferentialIKActionCfg,不代表当前 YAM lift cube 默认使用它。当前默认是 JointPositionActionCfg。把 DiffIK 的超参数修改应用于默认任务不会有任何效果。先读 lift_cube_env_cfg.py 中的 action cfg 类型。
⚠️ 编程陷阱:sigma 参数设置不当
\(\sigma_{\text{reach}}\) 太小(如 0.01)会让 reaching reward 在离方块 1cm 外就衰减到零——策略得不到足够的远距离梯度信号来驱动末端移动。\(\sigma_{\text{reach}}\) 太大(如 1.0)会让 reaching reward 在离方块 10cm 处就接近 1——策略觉得"差不多到了"就停止接近。典型值在 0.05-0.2 之间。
顺带一提,高斯核 \(\exp(-x^2/\sigma^2)\) 不是唯一的距离衰减选择——它有一个微妙的特性值得了解:在 \(x=0\) 处梯度恰好为零。这意味着末端精确到达物体位置时 reaching 梯度消失。实践中 bringing 项会接管,但如果你发现策略在方块正上方"犹豫不动",可能是这个零梯度导致的。其他候选方案:
| 衰减函数 | 表达式 | 零点梯度 | 何时替换高斯 |
|---|---|---|---|
| 线性衰减 | \(\max(0, 1 - x/d_{\max})\) | 恒定非零 | 工作空间 > 1 m,需要远距离信号 |
| tanh | \(1 - \tanh(x/\beta)\) | 非零 | 需要在目标点持续力控精度 |
| 逆距离 | \(1/(1 + x/\alpha)\) | 非零 | 长距离引导(结合近距离高斯) |
对大多数操作任务,高斯核仍是最佳默认——它在 \(x \approx \sigma\) 附近梯度最大,恰好在"快到了但不够精确"的关键区域提供最强信号。
🧠 思维陷阱:认为方块初始位置固定会"更容易"
固定初始位置确实让单一配置的收敛更快,但策略会过拟合到这个位置。一旦方块位置稍有变化,策略完全失效。在操作中,object pose randomization 不是"锦上添花"而是"基本要求"。
练习
- [源码阅读题] 在
lift_cube_env_cfg.py中找到staged_position_reward的调用位置,列出它使用的三个 sensor/state 数据源。画出数据从 MJCF 资产到 reward 计算的完整流向图。 - [设计题] 如果你想在 YAM 的 staged reward 中增加一个"夹爪对齐"奖励(奖励夹爪法向量与方块表面法向量的对齐度),它应该放在哪个阶段?以乘法还是加法方式组合?给出数学公式。
上节走通了 mjlab 机械臂操作的完整流程。但 mjlab 的 YAM 是一个相对简单的平行夹爪——只有一个主动夹爪关节。如果任务需要更复杂的末端(如五指灵巧手),工程挑战会完全不同。Isaac Lab 的 DexSuite 正是为这类高 DOF 灵巧手操作而设计的——这是下一节的主题。
17.3 Isaac Lab 灵巧手操作:DexSuite 与 DexPBT ⭐⭐⭐
这一节解决什么问题:从平行夹爪扩展到多指灵巧手,理解高 DOF 末端的特殊工程挑战,以及 Isaac Lab 的 DexSuite 如何通过 ADR 和 PBT 解决灵巧操作的训练稳定性问题。
动机:为什么灵巧手比平行夹爪难得多
平行夹爪的 action 维度是 1(开/合),抓取策略是"移到位然后闭合"——本质上是一个二元决策。灵巧手(如 Allegro Hand,16 个关节)的 action 维度是 16,抓取不再是二元决策而是一个连续的协调过程:每个手指需要在正确的时间移到正确的位置,施加正确方向和大小的力。
一个类比:平行夹爪像筷子——只有一个自由度(张合),使用简单但能力有限。灵巧手像人类手掌——20 个自由度,可以做各种精细操作,但协调控制的难度指数级增长。这个类比在一个地方不成立:筷子的使用不需要接触力反馈(你靠视觉对齐就够了),而灵巧手的操作高度依赖触觉反馈——哪个手指滑了、哪个手指力太大,这些信号在 observation 中至关重要。
从工程角度看,灵巧手操作引入了三个平行夹爪不存在的挑战:
| 挑战 | 平行夹爪 | 灵巧手 |
|---|---|---|
| 接触点数量 | 2(两个指尖) | 10-20(每个指节都可能接触) |
| 接触建模精度要求 | 中等(只需确保夹持力) | 极高(手指滑动、滚动、枢转都需要精确建模) |
| 动作协调维度 | 1(张合同步) | 16+(每个关节独立但必须协调) |
| 训练不稳定性来源 | 主要是 reaching 精度 | 接触力震荡 + 多指协调 + 物体旋转失控 |
Isaac Lab DexSuite 架构概览
Isaac Lab 的 DexSuite 提供了多个灵巧手操作任务,核心配置基于 Kuka 机械臂 + Allegro Hand 的组合:
| Task | 目标 | 难度 | 动作维度 |
|---|---|---|---|
Isaac-Repose-Cube-Allegro-Direct-v0 |
用灵巧手在掌中旋转方块到目标姿态 | ⭐⭐⭐ | 23 (7 arm + 16 hand) |
Isaac-Lift-Cube-Franka-v0 |
Franka Panda 平行夹爪抬方块(Manager-based) | ⭐⭐ | 7 |
Isaac-Lift-Cube-Franka-IK-Abs-v0 |
Franka 用绝对 DiffIK 动作空间抬方块 | ⭐⭐ | 6+1 |
Isaac-Lift-Cube-Franka-IK-Rel-v0 |
Franka 用相对 DiffIK 动作空间抬方块 | ⭐⭐ | 6+1 |
Isaac-Dexsuite-Kuka-Allegro-Lift-v0 |
Kuka+Allegro 灵巧手抬取(2.3 新增) | ⭐⭐⭐⭐ | 23 |
Isaac-Dexsuite-Kuka-Allegro-Reorient-v0 |
Kuka+Allegro 灵巧手在手旋转(2.3 新增) | ⭐⭐⭐⭐ | 23 |
注意 Franka Lift 系列提供了三种动作空间变体(JointPos -v0 / IK-Abs / IK-Rel),这与 §17.8 的动作空间选型讨论直接对应——你可以在同一个 lift cube 任务上对比三种动作空间的训练效果,这是一个非常有教育价值的消融实验。(OSC 动作空间在官方环境表中对应的是 Franka Reach 任务 Isaac-Reach-Franka-OSC-v0,不是 Lift。)
Isaac Lab 2.3 新增的 Dexsuite 环境(Lift 和 Reorient)是 DexPBT 论文原始实现的 Isaac Lab 重构版,同时引入了 ADR 和 PBT 的 first-class 支持。入门建议:先跑通 Isaac-Lift-Cube-Franka-v0(与 mjlab YAM 复杂度相当),确认训练管线正常后,再尝试灵巧手任务 Isaac-Repose-Cube-Allegro-Direct-v0——前者用默认配置通常能收敛,后者几乎必须调参。
DexSuite 的设计哲学与 mjlab 的 YAM 有显著差异:
物理后端差异。 Isaac Lab 使用 PhysX 作为物理引擎,其 TGS(Temporal Gauss-Seidel)迭代求解器在处理大量接触点时的特性与 MuJoCo 的凸优化求解器不同。PhysX 的接触稳定性更依赖 solver iteration count——迭代次数不够时,手指可能"穿过"物体。MuJoCo Warp 的凸优化接触通常对小物体抓取更稳定,但灵巧手的大量接触点会增加求解时间。
动作空间差异。 DexSuite 的 Allegro 任务默认使用关节目标(类似 mjlab 的 JointPositionAction),但 action 维度为 16(Allegro)+ 7(Kuka)= 23。这比 YAM 的 7 维高出三倍多。
奖励设计差异。 灵巧手的 repose(掌中旋转)任务不使用 staged reward——因为物体始终在手中,不存在"接近"阶段。奖励直接基于物体当前姿态与目标姿态的旋转距离:\(r = \exp(-\alpha \cdot d_{\text{rot}})\),其中 \(d_{\text{rot}}\) 是四元数距离。
DexPBT:Population-Based Training 的工程实现
DexPBT(DexPBT: Scaling up Dexterous Manipulation for Hand-Arm Systems with PBT,RSS'23)是 NVIDIA 在 Isaac Gym 中提出的灵巧手训练方法论,其核心思想已被集成到 Isaac Lab 的 DexSuite 中。
DexPBT 的具体任务谱。 原始论文在两个基础环境上验证:
| 环境 | 末端执行器 | 任务变体 |
|---|---|---|
AllegroKukaLSTM(单臂) |
Kuka iiwa + Allegro Hand | Reorientation, Regrasping, Grasp-and-Throw |
AllegroKukaTwoArmsLSTM(双臂) |
2× (Kuka + Allegro) | Reorientation, Regrasping |
其中 Grasp-and-Throw 是最难的变体——策略需要先用灵巧手抓住物体,然后精确地"投掷"到目标位置。这个任务同时需要力控制(抓取阶段)和弹道规划(投掷阶段),是对 PBT 探索能力的极端测试。
Isaac Lab 2.3 DexSuite 的工程细节。 从 DexPBT 到 Isaac Lab 2.3,工程实现有以下变化:
- 动作配置:DexSuite 的 Allegro reorient 任务使用
RelativeJointPositionActionCfg(asset_name="robot", joint_names=[".*"], scale=0.1)——注意是 Relative 而非 Absolute 关节位置,即策略输出当前位置的增量而非绝对目标。 - Dictionary observation space:Isaac Lab 2.3 引入了 dictionary observation space,允许 perception(视觉输入)和 proprioception(关节状态)在不同分支处理。这比把所有 obs 拼成一个 flat vector 更灵活。
- ADR + PBT 一键配置:Isaac Lab 2.3 的 DexSuite 将 ADR 和 PBT 做成了 first-class 配置选项(NVIDIA Developer Blog:"Isaac Lab 2.3 offers new features that support dexterous manipulation tasks, including dictionary observation space for perception and proprioception, and Automatic Domain Randomization (ADR) and Population Based Training (PBT) techniques"),不再需要从 IsaacGymEnvs 手动移植。
为什么灵巧手需要 PBT? 传统超参数搜索(grid search、random search)的问题是:每组超参数需要从头训练到收敛,wall-clock time 极长。灵巧手任务的超参数空间特别大(reward weights、action scale、PD gains、DR 范围都需要调),且不同超参数组合之间存在强交互效应——单独调某一个参数可能看不到效果,需要同时调多个。
PBT 的核心机制:同时训练一个"种群"(population)的策略(如 8-16 个),每个使用不同的超参数。每隔固定 iteration,性能差的策略"继承"性能好的策略的网络权重(exploit),然后对超参数做小幅随机扰动(explore)。这样,超参数搜索和策略训练同步进行,不需要从头重训。
PBT 训练流程:
t=0: 初始化 N 个策略,每个随机超参数
│
每 K iterations:
│
├─ 评估所有策略的性能
│
├─ 排名后 25% 的策略 → exploit(复制前 25% 的权重)
│ → explore(扰动超参数)
│
└─ 所有策略继续训练 K iterations
│
重复直到收敛
PBT 调优的超参数通常包括:reward 各项权重、action scale、entropy coefficient、learning rate。注意 PBT 不调 observation 或 reward 的结构——它只调数值参数。
PBT 的 exploit 机制细节。 exploit 不仅仅是"复制权重"——它需要处理 optimizer 状态(Adam 的 momentum 和 variance)。如果只复制 policy 权重而不处理 optimizer 状态,Adam 的 momentum 仍然"记得"旧参数的梯度方向,可能与新权重不兼容,导致 exploit 后性能骤降然后恢复。两种常见处理方式:(1) exploit 后重置 optimizer 状态(简单但损失动量信息),(2) 同时复制 optimizer 状态(保持一致性但工程复杂度更高)。DexPBT 论文使用方式 (1),因为灵巧手任务的 reward 景观变化快,旧的 momentum 信息对新参数配置参考价值有限。
PBT 的 explore 机制。 explore 是对超参数做随机扰动——但扰动幅度很关键。太大的扰动相当于重新随机搜索(失去了 exploit 带来的好起点优势),太小则搜索效率低。DexPBT 的经验值:对 reward weight 做 ±20% 的乘性扰动,对 learning rate 做 ±0.5× 的乘性扰动(即 lr 可能变为原来的 0.5-1.5 倍)。Isaac Lab 2.3 的 PBT 实现(PR #3399)将这些扰动范围作为可配置参数暴露。
PBT 在 Isaac Lab 中的工程实现
在 Isaac Lab 中使用 PBT 的典型配置结构如下。PBT 的核心逻辑是在一个"元循环"中管理多个并行训练的策略:
# PBT 伪代码——展示核心机制而非可运行代码
class PBTTrainer:
def __init__(self, population_size=8):
self.agents = [PPOAgent(random_hyperparams()) for _ in range(population_size)]
self.performance = [0.0] * population_size
def train_step(self, K=500):
# 所有 agent 各训练 K 个 iteration
for agent in self.agents:
agent.train(num_iterations=K)
# 评估所有 agent
self.performance = [agent.evaluate() for agent in self.agents]
# Exploit + Explore
ranked = sorted(range(len(self.agents)), key=lambda i: self.performance[i])
bottom_25 = ranked[:len(ranked)//4]
top_25 = ranked[-len(ranked)//4:]
for bad_idx in bottom_25:
good_idx = random.choice(top_25)
# Exploit: 复制网络权重
self.agents[bad_idx].load_weights(self.agents[good_idx].get_weights())
# Explore: 扰动超参数
self.agents[bad_idx].perturb_hyperparams(factor=0.2)
PBT 的一个微妙工程细节:exploit 复制的是网络权重,但不复制 optimizer 状态(momentum、Adam 的一阶/二阶矩估计)。这意味着继承了好权重的 agent 需要几个 iteration 来"热身"optimizer——前几个 iteration 的梯度可能不稳定。一个常见的做法是在 exploit 后重置 optimizer 或使用 warm-up learning rate。
DexSuite 与 Franka Lift 的代码结构对照
Isaac Lab 的操作任务代码结构与 mjlab 类似但术语不同:
Isaac Lab DexSuite 目录结构(示意):
source/extensions/omni.isaac.lab_tasks/
├── omni/isaac/lab_tasks/
│ └── manager_based/
│ └── manipulation/
│ ├── lift/ # Franka lift cube
│ │ ├── config/
│ │ │ └── franka/
│ │ │ ├── __init__.py
│ │ │ ├── lift_env_cfg.py # 环境配置
│ │ │ └── agents/
│ │ │ └── rsl_rl_ppo_cfg.py
│ │ └── lift_env.py
│ └── repose/ # Allegro repose
│ ├── config/
│ │ └── allegro/
│ │ ├── __init__.py
│ │ └── repose_env_cfg.py
│ └── repose_env.py
注意 Isaac Lab 的 lift_env_cfg.py 和 mjlab 的 lift_cube_env_cfg.py 在语义上高度对应,但 API 语法不同。一个关键差异是 Isaac Lab 使用 USD(Universal Scene Description)资产而非 MJCF——USD 支持更丰富的材质和渲染属性,这对视觉操作任务有优势。
Isaac Lab 中 Franka Lift 的启动命令
# Isaac Lab 中的 Franka lift cube 训练(参考命令,具体路径取决于安装方式)
python source/standalone/workflows/rsl_rl/train.py \
--task Isaac-Lift-Cube-Franka-v0 \
--num_envs 4096 \
--headless
对比 mjlab 的 uv run train Mjlab-Lift-Cube-Yam,Isaac Lab 的训练入口是一个 Python 脚本而非 CLI 命令。Isaac Lab 支持 --headless 模式(无渲染)来最大化训练吞吐——这在大规模灵巧手训练中尤其重要。
对初学者而言,建议先在 Isaac Lab 中跑通 Isaac-Lift-Cube-Franka-v0(平行夹爪,与 mjlab YAM 复杂度相当),确认训练管线可以工作后,再尝试灵巧手任务 Isaac-Repose-Cube-Allegro-Direct-v0。两个任务的代码结构相似,但调参难度差异显著:Franka lift 用默认配置通常能收敛,Allegro repose 几乎必须调参。
从 mjlab 到 Isaac Lab 的心智模型切换。 在 mjlab 中,一切以 MJCF + MuJoCo Warp 为中心,配置文件直接引用 .xml 文件中的 joint/body/site 名称。在 Isaac Lab 中,一切以 USD + Omniverse 为中心,配置文件引用 USD 中的 prim path(如 /World/Robot/panda_link7)。心智模型的不同表现在调试方式上:mjlab 中 debug 物理行为用 viser 的实时渲染;Isaac Lab 中 debug 用 Isaac Sim 的 GUI(功能更强但启动更慢)或 --headless + TensorBoard 日志。
如果不用 PBT 而手动调参会怎样?灵巧手任务的超参数空间有 20+ 维,手动调参通常只能覆盖 3-5 个关键参数,其余使用默认值。DexPBT 论文报告,PBT 在 Allegro repose 任务上比手动调参的最终性能高 30-50%——这不是因为 PBT 找到了"更好的算法",而是因为它在超参数空间中搜索得更充分。
Automatic Domain Randomization (ADR)
DexSuite 的另一个关键技术是 ADR(Automatic Domain Randomization)。回顾 Ch08:标准 DR 的随机化范围是手动设定的(如物体质量 ±20%)。ADR 让随机化范围自动扩展——当策略在当前范围内达到一定成功率时,自动扩大范围。
ADR 的工程起源:OpenAI Rubik's Cube (2019)。 ADR 最早由 OpenAI 在 Solving Rubik's Cube with a Robot Hand(Akkaya et al., 2019)中提出和工程验证。在那个工作中,Shadow Hand 上的 ADR 从单一的非随机化环境开始,最终自动扩展到覆盖物体质量、摩擦、惯性、观测噪声、动作延迟等数十个参数——其中任何一个参数的范围都超过了人类工程师敢于手动设定的值。最终训练出的策略在 LSTM 的隐藏状态中展现出了 emergent meta-learning 行为——隐藏状态编码了"当前环境是什么"的隐式系统辨识,让策略在不同物理参数的环境中自动调整行为。
需要注意:这种 emergent meta-learning 效应在原始 OpenAI 工作之外尚未被独立复现。这是一个需要 LSTM/GRU 策略网络 + 极大规模训练的现象——MLP 策略不具备这种能力。对本书的读者,将其作为"理解 ADR 价值"的直觉性洞察即可,不应视为 ADR 的必然结果。
ADR 的核心工程机制:
ADR 循环:
初始:物体质量 ∈ [0.05, 0.1] kg
│
策略成功率 > 80% → 扩大:物体质量 ∈ [0.04, 0.12] kg
│
策略成功率 > 80% → 继续扩大:物体质量 ∈ [0.03, 0.15] kg
│
策略成功率 < 50% → 停止扩大,保持当前范围
ADR 解决了一个经典的 DR 困境:范围太小,sim-to-real gap 大;范围太大,训练不收敛。ADR 让策略自己决定"能承受多少随机化",避免了人工设定范围的猜测。
ADR 的工程陷阱。 per-parameter independent expansion 可能产生不可训练的 corner cases——当多个参数同时取极端值时(如质量最大 + 摩擦最小 + 惯性最大),组合效应可能超出策略的学习能力。OpenAI 的工程解决方案是使用 rapid randomization(每一步都重新采样 DR 参数,而非每个 episode)+ behavioral cloning "warm-restart"(ADR 扩展范围后用当前策略的 rollout 数据 warm-start PPO,避免从头探索)。这些工程细节在 DexSuite 的 ADR 实现中部分被简化——Isaac Lab 2.3 的 ADR 使用 per-episode 采样而非 per-step,减少了工程复杂度但也降低了适应速度。
在灵巧手任务中,ADR 通常随机化以下参数:
| 参数 | 初始范围 | ADR 可能扩展到 | 对策略的影响 |
|---|---|---|---|
| 物体质量 | ±10% | ±50% | 力控制鲁棒性 |
| 物体摩擦 | ±10% | ±40% | 滑动检测与补偿 |
| 手指 PD gain | ±5% | ±20% | 关节刚度适应 |
| 物体尺寸 | ±5% | ±15% | 抓取姿态泛化 |
| 观测噪声 | 0-0.01 | 0-0.05 | 感知鲁棒性 |
| 动作延迟 | 0-1 step | 0-5 step | 通信延迟适应 |
双框架操作任务对比表
| 设计维度 | mjlab YAM Lift Cube | Isaac Lab DexSuite |
|---|---|---|
| 物理后端 | MuJoCo Warp(凸优化接触) | PhysX(TGS 迭代接触) |
| 末端类型 | 平行夹爪(1 DOF) | Allegro 灵巧手(16 DOF) |
| action 维度 | 7 | 23 |
| reward 结构 | staged(乘法门控) | 旋转距离(直接指数衰减) |
| DR 策略 | 手动设定范围 | ADR(自动扩展) |
| 超参数调优 | 手动 / grid search | PBT(种群训练) |
| 视觉支持 | 四种变体(state/RGB/Depth/multi-cube) | state 为主 |
| 训练规模 | 数千 envs | 数千-数万 envs |
| 入门难度 | ⭐⭐(适合操作入门) | ⭐⭐⭐⭐(需要更多调参经验) |
⚠️ 常见陷阱
⚠️ 编程陷阱:PhysX solver iteration 不足导致接触穿透
在 Isaac Lab 中,如果 solver_position_iteration_count 设置过低(如默认的 4),灵巧手的手指可能穿过物体。对于灵巧手任务,通常需要 16-32 次迭代。自检方法:用 zero agent 播放,检查物体是否在手中保持稳定而不穿透。
💡 概念误区:认为 PBT 只是"自动调参"
PBT 不只是调参——它的 exploit 机制(复制好的网络权重)让不同超参数配置之间共享训练进度。这意味着一个因为 reward weight 不合适而训练慢的策略,可以继承一个好参数策略的网络,然后在新参数下继续训练。纯调参方法(如 grid search)没有这种权重共享机制。
🧠 思维陷阱:认为"MuJoCo 接触更好所以灵巧手一定用 MuJoCo"
MuJoCo 的凸优化接触确实对小物体精细抓取有优势,但 PhysX 在大量接触点场景下的 GPU 并行效率更高。灵巧手可能同时有 20+ 个接触点,PhysX 的 TGS 求解器在 GPU 上的并行度比 MuJoCo Warp 的接触求解更好。选型不是"哪个更好",而是"哪个在你的任务规模和精度要求下更合适"。
⚠️ 编程陷阱:ADR 的边界条件设置不当
ADR 的扩展策略通常有一个 boundary_sampling_prob 参数——以一定概率采样边界值(范围的最大/最小值)而非均匀采样。如果这个概率太低,策略对边界情况的鲁棒性不足;太高,训练会不稳定。典型值 0.1-0.3。
练习
- [对比题] 比较 mjlab 的 YAM(7 DOF)和 Isaac Lab 的 Kuka+Allegro(23 DOF)在以下维度的差异:action 维度、observation 维度、reward 结构、典型训练 iteration 数。为什么 action 维度增加 3 倍但训练时间可能增加 10 倍以上?
- [设计题] 如果你要在 mjlab 中实现一个类似 ADR 的机制,需要在 EventManager 的哪个位置插入逻辑?画出数据流:成功率统计 → 范围更新 → EventManager 参数修改。
- [跨章综合题] 结合 Ch07(PPO 训练管线)和本节的 PBT,设计一个实验:在 YAM lift cube 上用 4 个不同 reward weight 配置的 PPO 策略做 PBT。写出种群初始化、exploit/explore 逻辑和评估指标的伪代码。
上节对比了平行夹爪与灵巧手的工程差异。但无论是 YAM 还是 Allegro,操作任务都面临一个共同的底层挑战:手指接触建模。接触力的精度直接决定了抓取策略能否在仿真中学到有效的力控制行为。这是下一节的主题。
17.4 手指接触建模的特殊挑战 ⭐⭐⭐
这一节解决什么问题:理解操作任务中接触建模为什么比 locomotion 更难、更敏感,以及在 MuJoCo Warp 和 PhysX 中如何配置接触参数以获得稳定的抓取行为。
动机:为什么接触在操作中更关键
在 locomotion 中,地面接触主要提供支撑力。脚底的接触面积大(几十平方厘米),接触力方向近似法向,摩擦要求不苛刻(只要不滑就行)。接触模型的小误差(如摩擦系数差 10%)通常不会导致灾难性失败——机器人可能走得不那么稳,但不会"突然不会走"。
操作中的手指接触完全不同。夹爪指尖的接触面积小(几平方毫米),接触力方向包含切向分量(摩擦力用于维持抓取),摩擦要求极其苛刻(摩擦不够物体会滑落)。接触模型的小误差可能导致策略在仿真中学会的抓取行为在真机上完全失效。
一个类比可以帮助理解:locomotion 的接触像穿运动鞋走路——鞋底大、摩擦好,偶尔滑一下也不会摔倒。操作的接触像用筷子夹花生——接触面积小、力学平衡精微,夹紧了花生碎、夹松了花生滑。仿真中的接触模型是否能准确复现这种精微平衡,决定了策略能否迁移到真机。
接触点密度与仿真精度的权衡
灵巧手(如 Allegro)的每个指节都可能与物体产生接触。一个五指灵巧手抓取一个方块,可能同时存在 10-20 个接触点。接触点越多,接触求解器的计算负担越大。
在 MuJoCo Warp 中,接触点数量由碰撞几何体的精度决定。精细的指尖碰撞几何(多个小 geom 拼接)可以产生更真实的接触分布,但增加碰撞检测的计算量。粗糙的指尖碰撞几何(一个大 geom)计算快但接触不真实——可能导致物体在手中"弹跳"。
| 接触建模精度 | geom 数量 | 接触效果 | 计算成本 | 适用场景 |
|---|---|---|---|---|
| 粗糙(每指 1 geom) | 5-10 | 物体可能穿透或弹跳 | 低 | 快速原型、大规模并行 |
| 中等(每指节 1 geom) | 15-20 | 抓取基本稳定 | 中 | 平行夹爪、简单灵巧操作 |
| 精细(指尖多 geom) | 30-50 | 接触分布真实 | 高 | 精细灵巧操作、sim-to-real |
如果接触几何过于粗糙会怎样?一个典型的失败案例:指尖用单个球体近似(常见于快速建模),但实际指尖是平面。球体接触产生的法向力方向总是径向的——这意味着两个球体指尖夹持方块时,法向力方向可能并不指向方块中心,导致方块受到"被挤出去"的侧向力。在真机上,平面指尖的法向力方向与接触面垂直,不会产生侧向力。这种几何级别的差异会让仿真中学到的力控制策略在真机上完全失效。
MuJoCo Warp 接触参数调优
mjlab 使用 MuJoCo Warp 作为物理后端,接触相关的关键参数包括:
condim(接触维度)。 决定接触力的维度:condim=1 只有法向力(无摩擦),condim=3 有法向 + 切向摩擦(点接触),condim=6 有法向 + 切向 + 扭转摩擦(面接触)。操作任务通常需要至少 condim=3,灵巧手可能需要 condim=6。
如果在操作任务中使用 condim=1(无摩擦)会怎样?夹爪只能提供法向压力,没有切向摩擦力来阻止物体滑动。即使夹爪从两侧紧紧夹住方块,方块也会因为重力的切向分量而滑落——因为没有摩擦力来抵消重力。策略会学到用极大的夹持力试图"嵌入"方块中,这在物理上是不合理的。
friction(摩擦系数)。 MuJoCo 使用各向同性摩擦,两个接触面的摩擦系数取较大值(而非常见的取均值)。操作任务中手指-物体的摩擦系数通常设为 0.5-1.0。如果仿真中摩擦太低,物体会在手中滑落;太高则策略不会学到精细的力控制——"怎么夹都不滑"会让策略变得粗暴。这就像在冰面上练习走路(摩擦太低)vs 在沥青上练习走路(摩擦正常)vs 在胶面上练习走路(摩擦太高)——你希望摩擦处于一个让策略需要学习平衡但不至于无法成功的范围。
solref 和 solimp(求解器参数)。 这是操作任务中最关键也最难理解的两组参数。要理解它们,首先需要理解 MuJoCo 的核心设计哲学。
MuJoCo 的软约束哲学。 MuJoCo 的官方文档明确指出:"This soft model... pushing harder against a constraint will always result in larger acceleration, and so the inverse dynamics can be uniquely defined."这意味着 MuJoCo 中的所有接触都是"软"的——物体总是有微小穿透,接触力与穿透深度成正比(类似弹簧)。这与 PhysX、Bullet 等使用硬约束求解器的引擎在哲学上完全不同。
这种设计对操作的影响是双面的。好处是:软约束避免了硬约束求解器在接触切换时的不连续力跳变,使得仿真更稳定、梯度更平滑。坏处是:如果参数设置不当,物体会出现可见的穿透——夹爪"嵌入"方块中。
solref = [time_constant, damping_ratio] 控制接触处虚拟弹簧-阻尼器的行为:
time_constant(默认 0.02,单位:秒):控制接触的"刚度"。值越小 → 弹簧越硬 → 穿透越小但数值越不稳定。物理直觉:它定义了"物体从穿透状态恢复到表面需要多长时间"。对操作任务,time_constant 应该与仿真 timestep 保持合理比例——MuJoCo 内部有安全机制,实际使用max(solref[0], 2*timestep)来防止过硬的接触导致仿真不稳定。damping_ratio(默认 1.0,无量纲):控制接触是否震荡。值 = 1.0 是临界阻尼(不震荡地最快恢复),< 1.0 是欠阻尼(弹跳),> 1.0 是过阻尼(缓慢恢复不弹跳)。对操作:使用默认 1.0 即可;如果模拟弹性球等需要弹跳的物体,降低到 0.1-0.5。
solimp = [dmin, dmax, width, midpoint, power] 控制位置相关的接触阻抗 \(d(r)\),其中 \(r\) 是穿透深度。大部分操作任务不需要修改 solimp 的默认值 [0.9, 0.95, 0.001, 0.5, 2]。只有在模拟非常柔软的材料(如硅胶指尖)时才需要调整 dmin 和 dmax。
操作任务的 solref 材料参照表(来源:RoboLaweb 实践指南,结合作者经验标注适用场景):
| 材料/场景 | solref 推荐值 | solimp 推荐值 | 物理描述 | 操作适用场景 |
|---|---|---|---|---|
| 刚性金属/混凝土 | [0.002, 1.0] | [0.9, 0.95, 0.001, 0.5, 2] | 极硬,几乎不变形 | 桌面、机械臂连杆 |
| 硬塑料/硬木 | [0.006, 1.0] | [0.9, 0.95, 0.001, 0.5, 2] | 硬但比金属略"软" | 操作物体(方块、盒子) |
| 硬橡胶/轮胎 | [0.015, 1.0] | [0.9, 0.95, 0.001, 0.5, 2] | 略有弹性,提供抓地力 | 夹爪衬垫、脚底 |
| 软硅胶/指尖 | [0.025, 1.0] | [0.95, 0.99, 0.001, 0.5, 2] | 明显可压缩变形 | 灵巧手指尖(增大接触面积) |
| 高弹性球 | [0.02, 0.1] | [0.9, 0.95, 0.001, 0.5, 2] | 高弹性,碰撞后弹飞 | 球类操作(如乒乓球) |
| 海绵/减震泡沫 | [0.04, 4.0] | [0.8, 0.99, 0.001, 0.5, 2] | 吸能,过阻尼不弹跳 | 缓冲垫、防碰撞条 |
impratio(摩擦-法向阻抗比)。 这是一个容易被忽略但对抓取稳定性影响显著的全局参数。impratio > 1 使摩擦力比法向力"更硬"——效果是在不增加摩擦系数的情况下减少滑移。对抓取任务,如果物体在手中缓慢滑落(但摩擦系数已经合理),尝试增大 impratio 到 5-10。MuJoCo 文档原文:"Settings larger than 1 cause friction forces to be 'harder' than normal forces, having the general effect of preventing slip, without increasing the actual friction coefficient."
如果不调整 impratio 而只增大摩擦系数会怎样?摩擦系数太大(>2.0)会让仿真在接触切换时产生不合理的大力——夹爪碰到方块时产生瞬间的巨大切向力,导致方块被"弹射"而非被平稳推动。impratio 提供了一种更温和的方式来增强抓取稳定性。
碰撞几何简化:CoACD vs V-HACD
操作任务中,物体的碰撞几何(用于物理引擎计算接触)和视觉几何(用于渲染)通常不同。碰撞几何需要是凸的(或凸的组合),因为凸碰撞检测算法(GJK/EPA)比通用三角网格碰撞检测快几个数量级。将复杂 mesh 分解为凸包集合有两种主流方法:
V-HACD(Voxel-based Hierarchical Approximate Convex Decomposition)。 传统方法,已集成在 Isaac Gym、PyBullet 和 MuJoCo Menagerie 的 pipeline 中。V-HACD 基于体素化,倾向于"填充"凹腔——这对 locomotion 的机体碰撞没问题(腿的形状本来就接近凸),但对操作物体可能产生物理错误。
CoACD(Collision-Aware Convex Decomposition, Wei et al., SIGGRAPH 2022)。 CoACD 是"碰撞感知"的,使用树搜索保留对接触重要的凹腔。其论文原文:"we propose a method that is better to preserve collision conditions of the input shape with fewer components"。
操作物体中的关键区别:考虑一个杯子。V-HACD 可能把杯子的内部空间填充为实心凸包——夹爪无法伸入杯内抓取杯沿。CoACD 会保留杯子的内部凹腔——夹爪可以正确地伸入杯内。对操作任务中常见的容器类物体(杯子、碗、盒子)、工具类物体(锤子把手的弯曲部分、扳手的开口)、和机构类物体(抽屉的滑轨、门把手),CoACD 是更正确的选择。
工程建议:locomotion 的机体碰撞用 V-HACD(更快,精度足够);操作物体碰撞用 CoACD(更慢但保留物理关键的凹腔)。MuJoCo Menagerie 的 pipeline(obj2mjcf + V-HACD)对复杂 link 如 Franka 的 link5 使用 V-HACD 分解;如果你的操作物体有明显凹腔,替换为 CoACD。
对手指接触,参数调优有以下经验规则:
| 场景 | solref 推荐值 | 原因 |
|---|---|---|
| 平行夹爪抓取 | [0.02, 1.0](默认) | 接触不需要极硬 |
| 灵巧手精细操作 | [0.005, 1.0] | 需要更硬的接触以减少穿透 |
| 柔性物体操作 | [0.05, 0.8] | 需要更软的接触以模拟柔性 |
margin 和 gap(接触检测参数)。 margin 定义了在物体实际接触之前多远开始检测潜在接触——这对操作任务很重要,因为夹爪接近物体的过程中需要提前"预感"到接触。gap 定义了不活跃接触的距离阈值。对精细抓取,通常需要减小 margin(如 0.001)以避免过早触发虚假接触。
接触稳定性的三步调优流程
面对一个新的操作任务时,接触参数的调优可以按以下三步进行:
Step 1:验证无接触时的物理行为。 用 zero agent 播放,确认方块在重力下自由落体的行为正常(速度、加速度符合 \(g=9.81\)),不出现不合理的弹跳或穿透。
Step 2:验证接触稳定性。 手动设置方块在夹爪之间的位置(通过修改初始状态),确认夹爪闭合时方块被稳定夹持而不穿透。如果方块穿透,增大 solver 精度(更小的 solref[0] 或更多的 solver iteration)。
Step 3:验证动态接触。 用 random agent 播放,确认方块在被随机碰撞时的行为合理——被推动而非穿透,被弹开而非粘住。如果方块表现"粘性"(碰到后黏在机械臂上),检查 friction 是否过高或 solimp 的 width 参数是否过大。
PhysX 接触配置(Isaac Lab)
Isaac Lab 的 PhysX 后端处理接触的方式不同。关键参数包括:
solver_position_iteration_count。 PhysX 的 TGS 求解器通过迭代求解接触约束。迭代次数越多,接触越稳定但计算越慢。灵巧手任务需要 16-32 次迭代(默认 4 次对 locomotion 够用,但对精细操作不够)。
一个实用的标定方法:逐步增加 iteration count(4 → 8 → 16 → 32),每次用 zero agent 测试物体在手中的稳定性。当增加 iteration count 不再改善接触稳定性时停止——这是性能和精度的平衡点。通常灵巧手在 16 次迭代时达到"够用"水平。
contact_offset 和 rest_offset。 PhysX 在物体实际接触之前就开始计算接触力(contact_offset > 0),用于提前稳定接触。rest_offset 定义物体"静止"时的穿透深度。这两个参数影响接触的"软硬"程度。对灵巧手,contact_offset 建议设为 0.005-0.01m(比 locomotion 的默认值 0.02 小),因为手指的几何尺度更小——过大的 contact_offset 会让手指在还没真正碰到物体时就感受到接触力,导致"隔空操作"的虚假行为。
GPU 接触缓冲区大小。 PhysX 的 GPU 接触求解器需要预分配缓冲区。如果灵巧手产生的接触点超过缓冲区大小,仿真会 crash。需要根据最大可能接触点数设置 PhysX scene 的 GPU contact/patch buffer 参数(如 gpu_max_rigid_contact_count、gpu_max_rigid_patch_count 等)。注意 max_depenetration_velocity 是限制求解器消除穿透时的最大修正速度,与接触缓冲区容量无关,不要混用。
MuJoCo Warp vs PhysX 接触模型的工程差异总结。 两个引擎处理同一个抓取场景的行为可能不同:MuJoCo Warp 的凸优化求解器在少量接触点时更精确,但计算量随接触点数增长快;PhysX 的 TGS 迭代求解器在大量接触点时有更好的 GPU 并行效率,但单个接触的精度依赖迭代次数。对平行夹爪(2-4 个接触点),两者差异不大;对灵巧手(10-20 个接触点),PhysX 在训练吞吐上通常有优势。
灵巧手特有的 Domain Randomization 策略
操作任务的 DR 与 locomotion 有所不同。Locomotion 的 DR 重点在底盘质量、摩擦系数和外部扰动。灵巧手操作的 DR 需要额外关注以下参数:
| DR 参数 | 随机化方式 | 对策略的影响 |
|---|---|---|
| 物体质量 | startup/reset 均匀采样 | 力控制鲁棒性——质量变大需要更大抓取力 |
| 物体摩擦 | startup/reset 均匀采样 | 滑动补偿——摩擦变小需要更精确的法向力 |
| 物体尺寸 | startup 均匀采样 | 手指张合幅度适应 |
| 手指 PD gain | startup/reset 均匀采样 | 关节刚度适应——gain 变小时动作需要更大 |
| 手指关节阻尼 | startup 均匀采样 | 运动速度适应 |
| 物体初始姿态 | reset 随机采样 | 起始位置泛化 |
| 重力方向 | startup 微扰 | 对重力方向的鲁棒性 |
注意:灵巧手的 DR 范围通常比 locomotion 更保守。Locomotion 中质量 ±30% 是常见配置;灵巧手中物体质量 ±30% 可能导致策略无法收敛——因为力控制对质量变化更敏感。初始 DR 范围建议从 ±10% 开始,逐步扩大(ADR 的思路)。
⚠️ 常见陷阱
⚠️ 编程陷阱:物体质量太小导致被"弹飞"
MuJoCo 中如果方块质量极小(如 0.001 kg)而手指施加的力有限(PD controller 输出的关节力矩),接触力可能产生极大的加速度,方块在一个 timestep 内飞出画面。自检方法:zero agent 播放时,方块应该在重力下缓慢下落,不应出现高速弹射。
💡 概念误区:认为"更多接触点 = 更好的仿真"
接触点数量并非越多越好。过多的接触点会增加求解器的计算负担,且可能引入数值不稳定(多个接触点之间的力分配可能震荡)。正确做法是用最少的接触点来获得足够的抓取稳定性。一个平行夹爪 2-4 个接触点通常足够;灵巧手 10-15 个接触点是合理范围。
🧠 思维陷阱:在仿真中追求完美接触而忽视 sim-to-real 的本质差异
无论接触模型多精确,仿真中的接触力永远是对真实接触的近似。与其花 90% 精力在精细的接触建模上,不如用 70% 精力做合理的建模 + 30% 精力做 Domain Randomization。DR 可以部分补偿接触模型的误差,因为它让策略对摩擦和刚度变化产生鲁棒性。
练习
- [实验题] 在 mjlab 的 YAM lift cube 中,分别测试
friction=0.3、friction=0.7和friction=1.5三种配置。用 trained agent 评估抓取成功率,记录差异。解释为什么中间值可能是最优的。 - [分析题] 如果灵巧手在仿真中训练的策略在真机上"抓不住物体",列出三个可能原因,并为每个原因给出对应的排查方法。
前几节分别深入了 mjlab 和 Isaac Lab 的操作实现。现在需要退后一步,从 MDP 设计的全局视角系统对比操作与足式任务的差异——不是笼统地说"不一样",而是逐个组件精确对照。这是下一节的主题。
17.5 操作 vs 足式:MDP 设计全维度对比 ⭐⭐
这一节解决什么问题:提供一张可查阅的"跨任务翻译表",让你在从 locomotion 切换到 manipulation(或反过来)时,快速定位每个 MDP 组件的设计差异。
动机:为什么需要系统性对比
前面的讨论散落在不同段落中。这里把所有差异汇总为一张完整的对照表,供日常开发参考。这张表的目标不是重复已经讲过的内容,而是提供一个"翻译手册"——当你从一个领域切换到另一个领域时,可以快速查阅每个组件的对应关系和注意事项。
MDP 全组件对照表
| MDP 组件 | 四足 Velocity Tracking (Go1) | 机械臂 Lift Cube (YAM) | 灵巧手 Repose (Allegro) | 设计原因 |
|---|---|---|---|---|
| observation (actor) | base_vel, gravity, joint_pos/vel, commands | joint_pos/vel, ee_pos, ee_to_cube, cube_pos, cube_to_goal | joint_pos/vel, fingertip_pos, obj_pose, goal_quat | 操作需要物体状态,locomotion 不需要 |
| observation (critic) | 上 + terrain heights + contact forces | 上 + privileged state | 上 + contact forces per finger | critic 看到更多真值信息 |
| action 维度 | 12(4×3 leg joints) | 7(6 arm + 1 gripper) | 23(7 arm + 16 hand) | 末端复杂度决定 |
| action 类型 | JointPositionAction | JointPositionAction / DiffIK | JointPositionAction | 关节空间是通用默认 |
| action scale | 0.25(较大,容错高) | 0.1-0.15(较小,精度高) | 0.05-0.1(最小,精度最高) | 任务精度要求反比 |
| reward 结构 | 加权求和(tracking + regularization) | 乘法门控(staged) | 旋转距离指数衰减 | 操作有因果阶段,locomotion 是持续优化 |
| termination | 摔倒(base 高度或姿态阈值) | 成功 + 超时 | 物体掉落 + 超时 | 操作有明确成功定义 |
| episode 长度 | 20s(1000 steps @ 50Hz) | 5-10s(短 episode) | 5-15s | 操作不需要长时间运行 |
| commands | velocity (vx, vy, yaw_rate) | goal height / goal position | goal quaternion | 任务定义不同 |
| DR 重点 | body mass, friction, push | object mass, friction, size, pose | 上 + finger gains, contact params | 操作对物体参数敏感 |
| 物理敏感点 | 地面接触、关节限位 | 夹爪接触、物体滑动 | 多指接触、物体旋转 | 接触类型不同 |
从对照表中读出的三个设计原则
原则一:observation 随任务对象扩展。 Locomotion 只需感知自身。操作需要感知自身 + 物体 + 目标。如果未来做 loco-manipulation(Ch19),则需要感知自身运动 + 末端 + 物体 + 目标 + 地形——observation 维度可能是 locomotion 的 3 倍。
原则二:action scale 与任务精度反相关。 精度要求越高,action scale 越小。这不是因为"动作小才好",而是因为 RL 策略的输出范围固定在 \([-1, 1]\),action scale 决定了单位策略输出对应多大的物理运动。scale 太大会让策略无法做出精细调整;scale 太小会让探索速度过慢。
原则三:reward 结构反映任务的因果拓扑。 Locomotion 的各 reward 项之间是平行关系(tracking + smoothness + energy 同时优化),用加法组合即可。操作的 reward 项之间有因果顺序(先 reaching 才能 grasping),用乘法门控编码因果关系。
从对照表到工程实践:跨域迁移的检查清单
对照表不是用来看的——它是用来指导实际配置迁移的。当你从 locomotion 切换到 manipulation(或反过来)时,以下检查清单可以避免最常见的遗漏:
第一步:调整 observation 维度。 Locomotion 的标准 obs 大约 48 维(Ch12)。操作任务需要增加物体相关的 obs(位置、朝向、末端到物体距离),同时可能不再需要某些 locomotion 特有的 obs(如 base velocity、gravity projection)。在 env_cfg.py 中,逐项核查 obs group 的每个 term,确认语义正确。
第二步:重写 reward 结构。 不要试图在 locomotion 的加法 reward 上"修补"——操作的 staged reward 是完全不同的架构。建议从空白开始,只保留 action_rate_l2 和 joint_acc_l2 这两个通用的 regularization 项,其余全部重写。
第三步:重新标定 action scale。 Locomotion 的 action scale 通常在 0.2-0.5 之间;操作任务需要 0.05-0.15。如果照搬 locomotion 的 action scale,机械臂的关节运动会过于剧烈——想象一下油门灵敏度为赛车级别的精密外科工具。
第四步:替换 termination 和 event 配置。 Locomotion 的 termination 是"摔倒",操作的 termination 是"成功/超时/物体掉落"。Locomotion 的 event 是"外部推力"和"地形变化",操作的 event 是"物体随机化"和"目标位置随机化"。这两组配置没有任何可复用的部分。
第五步:验证 episode 长度。 Locomotion 通常 20-30s,操作通常 5-10s。过长的 episode 会让操作任务的训练低效——方块一旦掉落,剩余时间都是无效的。但过短的 episode 不给策略足够的时间完成多阶段任务。经验法则:episode 长度 = 完成任务的合理时间 × 2。
⚠️ 常见陷阱
⚠️ 编程陷阱:跨任务迁移时忘记修改 termination 条件
Locomotion 的 termination 是"摔倒"(base 高度低于阈值)。如果在操作任务中保留了这个 termination,由于机械臂固定在桌面上,base 高度永远不变,这个 termination 永远不触发——看起来没问题,但它可能在 TerminationManager 中占据一个 slot,影响代码可读性。更危险的是,如果后来把操作任务迁移到移动底座上(Ch19),这个"遗留"的 termination 可能突然开始触发。
💡 概念误区:认为 locomotion 的超参数经验可以按比例缩放
"操作任务只有 7 DOF 而 locomotion 有 12 DOF,所以网络可以更小、训练可以更快。"这个推理是错的。难度不取决于 DOF 数量,而取决于 reward landscape 的形状。操作的 staged reward 有尖锐的阶段边界,可能比 locomotion 平滑的 tracking reward 更难优化——即使维度更低。
练习
- [翻译题] 假设你有一个在 Go1 上训练好的 velocity tracking 配置。现在要为 YAM lift cube 创建初始配置。逐个列出需要修改的配置项(scene、actions、observations、rewards、terminations、events),对每一项说明修改内容和原因。
- [设计题] 设计一个"操作版 curriculum":初始只有 reaching reward(目标是让末端碰到方块),在 reaching 成功率超过 80% 后开启 bringing reward。用
EventManager的interval模式实现成功率统计和 reward weight 切换。写出伪代码。
向 Loco-Manipulation 的过渡:本章知识如何扩展到 Ch19
本章的 MDP 对照表只涉及两个极端:纯 locomotion 和纯 manipulation(固定基座)。Ch19 将引入两者的融合——loco-manipulation(如四足机器人带机械臂执行抓取任务)。在那之前,值得预告几个关键的工程挑战,这些挑战直接源于本章建立的对比框架:
混合动作空间的尺度问题。 当一个策略同时控制底盘(速度 m/s)、臂关节(角度 rad)和末端增量(m),不同动作维度的物理量纲和典型值范围差异巨大。VBC (Visual Whole-Body Control, CoRL 2024) 的解决方案是分层:低层策略 50Hz 输出关节位置(统一量纲),高层策略 10Hz 输出底盘速度 + 末端目标位姿。这个分层模式直接来源于本章的两个设计原则——observation 随任务对象扩展(高层看到更多信息),action scale 与任务精度反相关(低层 scale 小以控制精度)。
操作阶段的底盘稳定性。 当机械臂执行抓取时,臂的反作用力会通过底盘传递到轮子/腿上,导致底盘漂移。Ch19 将详细讨论解决方案(stop reward、brake action、WoCoCo 式阶段切换),但核心思想在本章已经出现——乘法门控可以在操作阶段抑制底盘运动的 reward,强制底盘保持静止。
频率分层。 ETH 的羽毛球机器人 (Science Robotics 2025) 使用 400Hz 状态估计、100Hz 控制策略、60Hz 视觉感知的三级频率分层。这种分层在固定基座操作中不明显(一切都在 50Hz),但在 loco-manipulation 中成为核心工程决策。本章的 action 设计(PD 控制器在 physics step 频率执行、策略在 control frequency 输出)已经体现了这种分层思想的原型。
接触的稀疏性 vs 连续性。 本章的 staged reward 处理的是"一次性接触建立"——夹爪闭合后保持抓取。Ch19-20 的 WoCoCo 将这个概念扩展到"接触序列"——一系列离散的接触建立和释放。本章建立的乘法门控思想将在 WoCoCo 中演化为 contact-stage-aware reward——每个接触阶段有独立的 reward 组件,阶段之间的切换由接触模式变化驱动。如果你理解了本章的"reaching reward × bringing reward"门控逻辑,WoCoCo 的"approach reward → contact reward → lift reward → carry reward"就是自然的扩展。
操作 MDP 设计的通用模板
基于本章对操作 MDP 的系统分析,以下是一个可直接复用的 MDP 设计模板——当你面对任何新的操作任务时,可以从这个模板开始:
# 通用操作任务 MDP 设计模板
class ManipulationTaskTemplate:
"""操作任务的 MDP 设计模板。
使用方法:
1. 替换 [TASK-SPECIFIC] 标记为你的任务信息
2. 调整 reward 权重
3. 验证物理行为(zero play)
4. 训练并迭代
"""
# Observation
obs = {
# 通用 proprio(所有操作任务都需要)
"joint_pos": "[N_JOINTS]",
"joint_vel": "[N_JOINTS]",
"ee_pos_local": 3, # 末端位置(base 或 world frame)
"ee_quat_local": 4, # 末端姿态
"gripper_state": 1, # 夹爪开合
"last_action": "[ACTION_DIM]",
# [TASK-SPECIFIC] 任务相关 obs
"object_pos_relative": 3, # 物体相对位置
"object_quat_relative": 4, # 物体相对姿态
"target_pos_relative": 3, # 目标相对位置
}
# Action
action = {
"type": "JointPosition", # 或 "DiffIK"、"OSC"
"dim": "[N_JOINTS + 1]", # arm joints + gripper
"scale": {
"arm": 0.1, # [TASK-SPECIFIC] 精度要求越高 scale 越小
"gripper": 0.5,
},
}
# Staged Reward(核心设计)
reward = {
"stage_1_reach": {
"type": "exponential_distance",
"target": "object",
"sigma": 0.15, # [TASK-SPECIFIC]
"weight": 1.0,
},
"stage_2_grasp_gate": {
"type": "multiplicative_gate",
"condition": "gripper_closed AND object_contact",
},
"stage_3_bring": {
"type": "exponential_distance",
"target": "goal",
"sigma": 0.2,
"weight": 2.0, # 终极目标权重更大
"gate_by": "stage_2", # 被 grasp 门控
},
# 通用正则化
"action_rate": {"weight": -0.02},
"joint_limit": {"weight": -5.0},
"alive": {"weight": 0.5},
}
# Termination
termination = {
"success": "object_at_goal",
"timeout": "[EPISODE_LENGTH]", # 通常 5-15s
"failure": "object_dropped OR joint_limit_exceeded",
}
这个模板编码了本章的核心工程经验——staged reward 的乘法门控结构、action scale 的保守设置、regularization 的通用组合。对任何新任务,从这个模板开始修改比从零设计更高效。
填写模板的 reward 部分时,四个正交维度的选择可以指导你的设计:
| 维度 | 选项 | 模板中的默认选择 | 何时偏离默认 |
|---|---|---|---|
| 信号密度 | 稀疏 / 密集 / 分阶段 | 分阶段(staged) | HER 等 off-policy 方法可用稀疏 |
| 组合方式 | 加法 / 乘法门控 | 乘法门控 | 无明确因果顺序时用加法 |
| 坐标系 | 世界系 / 物体系 / 末端系 | 世界系 | 需要泛化到不同工作空间布局时用物体系 |
| 时间结构 | 即时 / 累积 / 终端 | 即时 | 只关心最终结果时用终端(需配合 HER) |
mjlab 的 staged reward 选择了"密集 + 乘法门控 + 世界系 + 即时"——这在 on-policy PPO 上最可靠。如果你使用 off-policy 算法(如 SAC)或有 HER 支持,可以考虑"稀疏 + 终端"组合——reward 设计更简单,但对 replay buffer 机制有依赖。
前面四节覆盖了操作任务的理论差异(17.1)、mjlab 实战(17.2)、Isaac Lab 对照(17.3)、接触建模(17.4)和全维度对比(17.5)。但理论和对比最终要落地为"能跑起来的命令"。下一节的任务是:从 uv run list-envs 到短训练,走通 YAM 四种变体的最小实验。
17.6 最小 uv run 实验 ⭐
这一节解决什么问题:如何用最少的命令验证 YAM 四种任务变体的基本功能?这是开始任何操作任务开发前的必经步骤。
任务注册验证
uv run list-envs | grep -i yam
通过标准:输出包含 Mjlab-Lift-Cube-Yam、Mjlab-Lift-Cube-Yam-Rgb、Mjlab-Lift-Cube-Yam-Depth 和 Mjlab-Multi-Cube-Seg-Yam 四个 task id。如果缺少任何一个,检查 src/mjlab/tasks/manipulation/config/yam/__init__.py 中的注册代码。
State Zero Play
uv run play Mjlab-Lift-Cube-Yam \
--agent zero \
--num-envs 4 \
--viewer viser \
--no-terminations
通过标准:YAM 机械臂、方块和目标点可视化都能正确创建。zero agent 输出全零,机械臂应保持初始姿态不动。方块在重力下落到桌面(如果有桌面),或持续加速下落(如果没有桌面碰撞体——无空气阻力的仿真中重力使其加速,而非匀速)。
观察重点:
- grasp_site 标记位于夹爪指尖之间
- 方块不穿透桌面或机械臂
- 目标位置可视化在合理高度
State Random Play
uv run play Mjlab-Lift-Cube-Yam \
--agent random \
--num-envs 4 \
--viewer viser \
--no-terminations
通过标准:机械臂剧烈运动但不"爆炸"。方块可能被偶尔碰到但不会以不合理的速度飞出。这验证了 action scale 设置合理。
如果 random agent 导致机器人"爆炸"(关节飞到极限位置或方块以极速弹射),问题通常在 action scale 过大。对操作任务,action scale 通常在 0.1-0.15,远小于 locomotion 的 0.25。
State Smoke Train
uv run train Mjlab-Lift-Cube-Yam \
--env.scene.num-envs 128 \
--agent.max-iterations 10 \
--agent.logger tensorboard \
--gpu-ids None
通过标准:state actor/critic 创建成功,rollout 不因 observation 或 action shape 崩溃。10 个 iteration 不代表任务学会了抓取——操作任务通常需要数百到数千 iteration 才能看到阶段性进步。
Depth Smoke Train
uv run train Mjlab-Lift-Cube-Yam-Depth \
--env.scene.num-envs 64 \
--agent.max-iterations 10 \
--agent.logger tensorboard \
--gpu-ids None
通过标准:camera observation group 和 CNN model 创建成功。如果报 shape 错误,检查 camera_name 和 obs_groups 配置。
Multi-Cube Smoke Train
uv run train Mjlab-Multi-Cube-Seg-Yam \
--env.scene.num-envs 64 \
--agent.max-iterations 10 \
--agent.logger tensorboard \
--gpu-ids None
通过标准:depth 和 target_mask 同时进入 camera group,target selection 不报错。
完整训练(State 版参考配置)
uv run train Mjlab-Lift-Cube-Yam \
--env.scene.num-envs 4096 \
--agent.max-iterations 5000 \
--agent.logger wandb
关键 PPO 超参数(操作任务推荐值 vs locomotion 默认值):
| 超参数 | Locomotion 默认 | 操作推荐 | 修改原因 |
|---|---|---|---|
| learning_rate | 1e-3 | 3e-4 ~ 5e-4 | 操作的 reward 景观更尖锐,需要更小的步长 |
| mini_batch_size | 4096×24/4 | 4096×16/4 | 操作 episode 更短,rollout 数据更少 |
| num_epochs | 5 | 8 | 每条轨迹更有信息价值,多扫几遍 |
| clip_param | 0.2 | 0.1~0.2 | 更保守的策略更新避免跳过接触边界 |
| gamma | 0.99 | 0.99 | 通常不需要改 |
| entropy_coef | 0.01 | 0.005~0.01 | 操作探索不需要太多随机性 |
| value_loss_coef | 1.0 | 0.5~1.0 | value function 在操作中更难学 |
Wall-clock 时间参考(单 RTX 4090,4096 envs):
| 任务变体 | 5000 iteration | 收敛所需 iteration | 预期 wall-clock |
|---|---|---|---|
| State 版 | ~30 min | ~2000-3000 | ~15-20 min |
| Depth 32×32 版 | ~2 h | ~3000-5000 | ~1-2 h |
| RGB 32×32 版 | ~3 h | ~5000-10000 | ~2-4 h |
回顾本节开头的关键数据:mjlab 作者报告 RGB 32×32 版可以在 <5 分钟 内收敛——这个数字可能是在更高端的 GPU(如 H100)或更少的 iteration 要求下测得。上表的数字是保守估计,基于"reaching + bringing reward 都收敛"的标准。
训练期望: - 前 200 iteration:reaching reward 快速上升(末端学会移向方块) - 200-1000 iteration:grasping 阶段,reward 增长放缓(策略学习在正确时刻闭合夹爪) - 1000-3000 iteration:bringing reward 上升(策略学会抬起并搬运方块) - 3000+ iteration:精度优化和稳定性提升
如果 reaching 阶段的 reward 在 500 iteration 后仍不上升,参考 17.2 节的诊断流程:先检查 ee_to_cube 是否正确进入 actor observation,再检查 \(\sigma_{\text{reach}}\) 是否合理。
如何读操作任务的 TensorBoard
操作任务的 TensorBoard 与 locomotion 有不同的阅读重点。Locomotion 主要看 tracking_reward 和 velocity_error;操作任务需要关注更多分项:
必看指标:(1) reward/reach 和 reward/bring 分开看——如果只看总 reward,你可能错过"策略卡在某个阶段"的问题;(2) reward/action_rate_l2——如果这个值突然增大,说明策略出现了动作震荡;(3) episode/success_rate——操作任务有明确的成功判据(方块到达目标),这个指标比总 reward 更能反映策略的实际能力。
次看指标:(1) action/gripper_std——夹爪动作的标准差反映策略的"决断力"。如果夹爪 action std 始终很大,说明策略对"何时闭合夹爪"犹豫不决;(2) obs/ee_to_cube_mean——末端到方块的平均距离,应该在 reaching 阶段快速下降。
报警指标:(1) value loss 持续上升(而非收敛);(2) KL divergence 出现周期性 spike(策略在不同行为之间反复切换);(3) entropy 在 500 iteration 内降到接近零(过早坍缩到确定性策略,可能卡在局部极值)。
与 locomotion 的一个关键区别:locomotion 的 reward 通常单调上升,操作任务的 reward 可能出现"平台期"——reaching 收敛后,策略需要时间"发现"夹爪闭合与 bringing reward 之间的因果关系。这个平台期可能持续数百 iteration,不要误以为训练卡住而过早终止。
⚠️ 常见陷阱
🧠 思维陷阱:把 smoke test 当成训练收敛的证据
新手想法:跑了 10 个 iteration,看到 reward 有变化,就认为"训练成功"。
实际上:短训练的 reward 变化只反映策略从完全随机到稍有方向性的转变。操作任务的阶段性收敛需要数百 iteration 才能观察到。Smoke test 只验证管线的入口正确性。
⚠️ 编程陷阱:视觉 smoke test 失败时先调 CNN
新手想法:Depth 训练报错,一定是 CNN 架构或超参数的问题。
实际上:视觉训练 90% 的错误在输入层面——camera_name 错、obs_groups 配置错、HWC/CHW 转换错、图像未归一化。先保存一帧图像 tensor,确认 shape 和值范围。只有在输入确认正确后才考虑 CNN 架构。
练习
- [实践题] 依次运行上述 5 个 smoke test 命令,记录每个步骤的通过/失败情况。如果有失败,根据错误信息定位原因。
- [分析题] 在 smoke train 的 TensorBoard 中找到 reward 分项(reach、bring)。画出它们随 iteration 变化的曲线。解释为什么 reach 先上升而 bring 后上升。
Smoke test 验证了管线能跑。但"能跑"和"跑得好"之间还有很长的路。操作任务的训练诊断比 locomotion 更复杂——因为 staged reward 的阶段性意味着失败模式也是阶段性的。下一节专门讨论操作任务的训练诊断方法。
17.7 操作任务的训练诊断与常见失败模式 ⭐⭐⭐
这一节解决什么问题:当操作任务的训练出了问题(reward 不涨、策略卡在某个阶段、夹爪震荡),如何系统性定位根因?
操作任务的三层诊断框架
回顾 Ch07 的训练诊断三层模型(物理层 → MDP 层 → 优化层),操作任务在每一层都有特殊的失败模式:
| 诊断层 | Locomotion 常见问题 | Manipulation 特殊问题 |
|---|---|---|
| 物理层 | 穿透地面、关节过限 | 方块穿透手指、接触力震荡、方块被弹飞 |
| MDP 层 | observation 维度错、reward weight 不平衡 | staged reward 卡阶段、privileged 泄漏、object pose 坐标系错 |
| 优化层 | KL spike、entropy 塌缩 | 因阶段性 reward 导致的 value function 估计困难 |
失败模式一:策略卡在 Reaching 阶段
症状:reaching reward 接近 1 但 bringing reward 始终为 0。总 reward 约 1.0 不再上升。
可能原因清单:
| 原因 | 排查方法 | 修复方案 |
|---|---|---|
| 夹爪 action 无效 | 检查 action 的第 7 维是否映射到正确的关节 | 修正 action 到关节的映射 |
| \(\sigma_{\text{reach}}\) 太大 | 打印 reaching 距离,检查是否还有 5cm+ | 减小 \(\sigma_{\text{reach}}\) |
| 物体太重/摩擦太低 | zero agent 下观察方块是否能被稳定夹持 | 调整质量或摩擦系数 |
| learning rate 太大 | 检查 KL divergence 是否有 spike | 降低 lr 到 1e-4 |
失败模式二:夹爪持续震荡
症状:机械臂在方块附近持续快速张合夹爪,不做出稳定的抓取决策。
根因分析:这通常是 action smoothness penalty 不足。策略发现"反复闭合-打开"可以获得瞬间的接触奖励——每次闭合都短暂地让 ee_to_cube 变小,但又立刻松开。这就像一个人反复拿起又放下物品——看似在"做事",实际没有进展。
修复方案:增加 action_rate_l2 的 reward 权重。这个 regularization term 惩罚连续两步动作之间的差异,迫使策略做出平滑、有承诺的决策。典型权重 0.01-0.05。
失败模式三:方块在手中旋转失控(灵巧手)
症状:灵巧手抓住方块后,方块开始快速旋转,最终飞出。
根因分析:灵巧手的多指接触力可能在某些配置下产生净力矩——五个手指的切向力不平衡,导致方块旋转。这在平行夹爪中不存在,因为两个手指的力天然对称。
修复方案:(1) 增加旋转惩罚(物体角速度的 L2 范数);(2) 增加物体摩擦系数使旋转更难发生;(3) 减小 action scale 让手指运动更精细。
失败模式四:视觉版 actor 不使用图像
症状:Depth 版训练的 reward 曲线与 State 版几乎相同,但 play 时关闭 privileged 状态后策略完全失效。
根因分析:actor observation 中仍然包含了 ee_to_cube 或 cube_pos_w 等 privileged 状态信息。CNN 分支的权重几乎未更新——策略完全依赖 state 信息,图像只是一个没用的额外输入。
修复方案:严格检查视觉版 actor 的 observation group。视觉版 actor 应该只包含 joint_pos、joint_vel、last_action 和 camera 图像——不应包含任何 object/goal 的显式状态。Object 信息应仅在 critic observation 中出现。
失败模式五:物体在 reset 后"嵌入"桌面
症状:部分环境在 reset 后方块位于桌面内部,导致接触力瞬间爆炸,observation 出现 NaN。
根因分析:reset 时物体位置的 z 坐标采样范围太低,加上桌面高度和物体半径的计算没有考虑 geom margin,导致物体初始位置在桌面碰撞几何体内部。MuJoCo 的接触求解器会产生极大的穿透力试图把物体"推出"桌面。
修复方案:在 reset 的 object pose randomization 中,确保 z 坐标的下限至少为 table_height + cube_half_size + margin。自检方法:用 1000 个 env 做 zero play,统计是否有 env 的 observation 出现异常大值。
失败模式六:训练初期 value loss 极大且不收敛
症状:value loss 从第一个 iteration 开始就在 \(10^3\) 量级,且不随训练下降。
根因分析:staged reward 在训练初期几乎为零(策略还没学会 reaching),导致 return 分布高度集中在零附近。但 value network 的初始预测可能在非零值附近,导致巨大的 value loss。这在 locomotion 中不太常见,因为 locomotion 的 alive reward 从第一步就提供了非零的 baseline。
修复方案:(1) 增加一个小的 alive reward(每步 +0.001)作为 baseline,减小 value function 的初始估计误差。(2) 使用 value function 的 warm-up(前 50 iteration 只训练 critic 不更新 policy)。(3) 减小 value function 的 learning rate。
失败模式七:策略学会"推"而非"抓"(Reward Hacking)
症状:bringing reward 在涨,play 可视化却发现策略没有真正抓起方块——而是用末端把方块推向目标位置。当目标位置在桌面上方时,pushing 策略在目标高度处失效。
根因分析:staged reward 的 \(r_{\text{bring}} = \exp(-\|p_{\text{goal}} - p_{\text{obj}}\| / \sigma_{\text{bring}})\) 不区分物体是被"抬起"还是被"推过去"。如果目标位置在桌面上(只有 xy 偏移无 z 偏移),pushing 是一种合法的高效策略——它不需要夹爪闭合,成功率可能反而更高。只有当目标位置有高度差时,pushing 才会失效。
修复方案:(1) 增加 grasping_reward:当物体高度超过桌面一定阈值且方块在两个夹爪指尖之间时给予额外奖励,迫使策略学会真正抬起物体。(2) 把目标位置的 z 坐标设置为始终高于桌面(如 10-30cm),让 pushing 策略无法成功。(3) 增加 is_grasped 的检测逻辑作为 reward 前置条件——只有在物体被夹持的情况下 bringing reward 才生效。
这是操作任务中最常见的 reward hacking:策略找到了一条 reward landscape 中的"捷径",绕过了设计者期望的行为。每当你发现 reward 曲线看起来正常但 play 行为不对,第一个怀疑对象就是 reward hacking。
失败模式八:多环境间的 reward 方差极大
症状:4096 个并行环境中,某些环境的 episode return 持续为零,而其他环境正常。
根因分析:object pose randomization 的范围太大,导致部分环境中方块的初始位置超出了机械臂的可达工作空间。这些环境中策略从物理上就不可能完成任务——但训练器不知道这一点,会把这些零 return 计入 advantage 估计,拖慢整体训练。
修复方案:检查 object pose randomization 的 xy 范围是否在机械臂的工作空间内。简单的验证方法:计算 YAM 的工作空间近似半径(arm link 长度之和 × 0.8),确保 object xy 采样范围在此半径内。如果无法缩小采样范围(任务需要),可以在 reset 时增加可达性检查——对不可达的初始位置重新采样。
操作任务诊断检查清单
Stage 1:物理检查(5 分钟)
□ zero agent:方块不穿透、不弹飞
□ random agent:机械臂不爆炸、action scale 合理
□ grasp_site 位置在夹爪中心
Stage 2:MDP 检查(10 分钟)
□ actor obs 不含 privileged 信息(视觉版)
□ reward 分项独立记录(reach / bring 分开看)
□ object pose 使用正确的坐标系(世界系 vs 环境 origin)
□ termination 条件正确(成功/超时/物体掉落)
□ event 随机化范围合理(物体质量/摩擦/位置)
Stage 3:优化检查(训练中)
□ reaching reward 在 200 iteration 内上升
□ KL divergence 不持续偏高
□ 夹爪 action 不震荡(action_rate 分项)
□ value loss 不爆炸
□ task success rate 独立于 reward 记录并上升
Stage 4:行为验证(训练后)
□ play 可视化确认策略行为符合预期
□ 遮蔽测试(视觉版:全零图像输入时策略失效)
□ 成功率在不同初始条件下一致(无某类 env 持续为零)
黄金诊断原则:Reward Up, Task Metric Flat → Reward Hacking
这是操作任务诊断中最重要的一条原则,值得单独强调。
原则:永远独立记录 task metric(如 grasp success rate、lift height)和 reward。如果 reward 曲线在涨但 task metric 持平或下降,几乎可以确定策略在 reward hacking——它找到了一种获取 reward 但不完成实际任务的"捷径"。
为什么这在操作中特别常见? 因为 staged reward 有多个分项,策略可能学会最大化某个分项而忽略其他分项。例如:reaching reward 已经很高(末端很接近方块),但策略从不闭合夹爪——因为闭合夹爪有 action cost,而不闭合夹爪也不会被 reaching reward 惩罚。
实施方法:在 env_cfg.py 的 termination 或 observation manager 中添加一个不参与 reward 计算的 success_rate 指标。在 mjlab 中:
# 在 reward function 之外独立记录 task metric
def _compute_success(self) -> torch.Tensor:
"""Success = object within 5cm of goal AND above table."""
dist = torch.norm(self.cube_pos - self.goal_pos, dim=-1)
above_table = self.cube_pos[:, 2] > self.table_height + 0.05
return (dist < 0.05) & above_table
将此指标通过 extras["success_rate"] 传递给 logger,在 TensorBoard 中独立查看。
操作任务的 NaN 调试专题
操作任务比 locomotion 更容易出现 NaN(Not a Number),因为接触力的不连续性和物体的自由运动增加了数值不稳定的风险。
NaN 的三种常见来源:
来源一:物体嵌入桌面。 如果 reset 时方块的 z 坐标低于桌面高度,MuJoCo 的接触求解器会在第一步计算出极大的"反穿透"力,可能导致速度溢出到 NaN。自检方法:打印 reset 后第一步的 cube_vel,如果绝对值超过 100 m/s,说明初始穿透过深。
来源二:DiffIK 在奇异配置附近。 当 Jacobian 条件数 > \(10^6\) 时,DLS 伪逆的数值精度下降到 float32 的极限。如果此时策略输出了一个较大的笛卡尔增量,计算出的关节速度可能溢出。修复:增大 lambda_dls(如 0.01 → 0.1)或在 DiffIK 输出后加 clip。
来源三:reward 计算中的除零。 exp(-d/σ) 中如果 σ=0 或 d 包含 NaN(来自物体位置溢出),整个 reward 计算链会传播 NaN。mjlab 的一个工程亮点是内置了 NaN/Inf 检测——如果检测到 NaN,会自动报告产生 NaN 的具体 term 名称,极大地加速了调试。
NaN 定位的标准流程:
1. 确认 NaN 出现在哪一步
→ 在 env.step() 前后检查 obs、reward、done
2. 如果 obs 中有 NaN
→ 逐项检查 obs term:joint_pos, joint_vel, cube_pos, ee_to_cube
→ 通常是 cube_pos 中最先出现 NaN(物体物理状态溢出)
3. 如果 reward 中有 NaN
→ 检查 reward function 的数学运算(除零、log(0)、sqrt(负数))
4. 定位后
→ 对物理 NaN:修复初始状态或减小 timestep
→ 对计算 NaN:添加 clamp 或 epsilon 保护
跨框架训练结果不一致的诊断
当你在 mjlab 中训练成功但迁移到 Isaac Lab(或反过来)后训练不收敛,诊断需要系统性地排除物理引擎差异带来的影响。
诊断步骤:
-
确认 MDP 语义一致:逐项核对 obs、action、reward、termination 的定义是否在两个框架中完全等价。最常见的差异是坐标系(MuJoCo 是 z-up,PhysX 也是 z-up,但 asset 的本地坐标系可能不同)和关节顺序(MJCF 和 USD 中关节的排列顺序可能不同)。
-
确认物理行为一致:在两个框架中都运行 zero agent,比较:
- 方块自由落体的时间是否一致(应为 ~0.14s 从 10cm 高度)
- 夹爪闭合时的接触力大小是否在同一量级
-
方块在桌面上的摩擦行为是否一致(斜面滑动速度)
-
如果物理行为一致但训练不收敛:检查 PPO 超参数和采样口径是否对齐。注意 steps/s 影响的是 wall-clock(同样时间内采样量不同),在
num_envs、num_steps_per_env、episode 长度/终止语义相同时,相同 iteration 数的采样步数是一样的,不会因 steps/s 不同而改变;若固定 iteration 比较,应重点核对物理语义、终止条件和 batch 设置是否一致,而不是 steps/s。 -
如果物理行为不一致:这是根本原因——两个引擎对同一个接触场景的求解结果不同。这种情况下,不应期望相同的 reward function 在两个框架中诱导出相同的策略。需要分别调参,迁移的是"设计思路"而非"具体参数值"。
本质洞察:跨框架迁移的核心困难不在语法(改配置名称)而在物理(两个引擎的接触求解行为不同)。因此,迁移后必须重新训练而非直接加载 checkpoint,重新调参而非照搬 σ 值。
⚠️ 常见陷阱
⚠️ 编程陷阱:object pose 的坐标系不一致
mjlab 中 object 的世界位置可能需要减去 env origin 才能得到局部坐标。如果 cube_pos_w 给的是全局位置但 goal_pos 给的是相对于 env origin 的位置,cube_to_goal 的计算就会系统性偏差。在多 env 场景中,不同 env 的 origin 不同,这个错误在单 env 中不会暴露(因为 origin 是零)。
💡 概念误区:认为"reward 分项都在涨就没问题"
所有分项都在涨不代表策略行为正确。一个常见的隐蔽问题是:策略学会了一种"作弊"方式来获取 bring reward——不是抓起方块搬运,而是用末端推方块滑向目标。推和搬在 reward 数值上可能相近,但推的行为在真机上远不如搬可靠。需要通过 play 可视化确认策略的实际行为。
练习
- [诊断题] 你的 YAM lift cube 训练在 1000 iteration 后,reaching reward = 0.95 但 bringing reward = 0.02。列出排查步骤(至少 3 步),每步说明你要检查什么、可能的发现和对应的修复方案。
- [跨章综合题] 结合 Ch05(observation 设计)、Ch06(reward 设计)和本章的知识,分析以下场景:你的 YAM Depth 版训练 reward 很高,但 play 时把 camera 遮住(全黑图像),策略仍然能抓取。写出从这个现象到根因的完整诊断链,包括至少 3 个检查步骤和对应的源码锚点。
17.8 DiffIK 动作空间:从关节空间到笛卡尔空间 ⭐⭐⭐
这一节解决什么问题:理解
DifferentialIKActionCfg的工程原理,知道何时从默认的关节空间动作切换到任务空间动作,以及切换时的工程代价。
动机:为什么需要任务空间动作
回顾 17.2 节的讨论:JointPositionAction 让策略在关节空间输出目标,策略需要隐式学习逆运动学。对 YAM lift cube 这种简单任务,这不是问题——MLP 可以通过大量数据学会关节到末端的映射。
但对更精确的操作任务(如轨迹跟踪、装配),关节空间动作的学习效率低下。想象一个需要末端沿直线移动 5cm 的任务:在关节空间中,这条直线可能对应一条复杂的非线性关节轨迹——策略需要学习这种非线性映射。在笛卡尔空间中,这只是一个简单的 \(\Delta x = 0.05\) 命令。
这就是 DiffIK 的价值:它把逆运动学的计算从"策略需要学的东西"变成"框架自动处理的东西",让策略可以在更自然的任务空间中规划。
DiffIK 原理简述
Differential Inverse Kinematics 的核心思想是利用 Jacobian 矩阵将笛卡尔空间的期望速度映射为关节空间的速度:
其中 \(J^\dagger\) 是 Jacobian 伪逆(通常使用 DLS——Damped Least Squares 正则化以避免奇异点附近的数值不稳定),\(v_{\text{desired}} = [\Delta x, \Delta y, \Delta z, \Delta \text{roll}, \Delta \text{pitch}, \Delta \text{yaw}]\) 是策略输出的笛卡尔增量。
DLS 伪逆的数学推导:
标准伪逆 \(J^\dagger = J^T(JJ^T)^{-1}\) 在 \(J\) 接近奇异时(\(JJ^T\) 几乎不可逆)会产生极大的关节速度——物理上就是"为了微小的末端移动,某些关节需要疯狂转动"。DLS 在分母中加一个正则化项来缓解这个问题:
\(\lambda\) 是阻尼参数——越大越"保守"(关节速度更小但末端跟踪精度下降),越小越"激进"(末端跟踪精确但奇异点附近不稳定)。典型值:\(\lambda = 0.01\)(Isaac Lab 默认)到 \(\lambda = 0.05\)(保守场景)。
为什么需要理解 DLS 的工程实现? 因为 \(\lambda\) 的选择直接影响操作任务的精度——太大导致末端移动"迟钝"(策略输出的笛卡尔增量被过度衰减),太小导致奇异点附近"抖动"。你可能需要在不同任务或不同工作空间区域使用不同的 \(\lambda\)——这在 mjlab/Isaac Lab 的默认配置中只是一个参数,但知道它的含义才能正确调参。
DiffIK 的完整 Python 实现(用于理解原理,不是直接运行的代码):
import torch
def diff_ik_step(
jacobian: torch.Tensor, # [B, 6, n_joints] 末端 Jacobian
delta_pose: torch.Tensor, # [B, 6] 策略输出的笛卡尔增量
lambda_dls: float = 0.01,
q_current: torch.Tensor = None, # [B, n_joints] 当前关节角度
dt: float = 0.02,
) -> torch.Tensor:
"""DiffIK 单步计算:笛卡尔增量 → 关节角度增量。
Args:
jacobian: 机械臂在当前配置下的末端 Jacobian 矩阵
在 MuJoCo 中通过 mj_jac() 计算
在 PhysX 中通过 get_jacobian() 计算
delta_pose: 策略网络输出 × action_scale 后的笛卡尔增量
前 3 维 = 位置增量 (m)
后 3 维 = 旋转增量 (rad, axis-angle)
lambda_dls: DLS 阻尼参数
q_current: 当前关节角度(用于加增量得到目标角度)
Returns:
q_target: [B, n_joints] 关节目标角度
"""
B, m, n = jacobian.shape # m=6 (末端自由度), n=关节数
# Step 1: 计算 DLS 伪逆
# J†_dls = J^T (J J^T + λ²I)^{-1}
JJT = torch.bmm(jacobian, jacobian.transpose(1, 2)) # [B, 6, 6]
damping = lambda_dls ** 2 * torch.eye(m, device=jacobian.device)
JJT_damped = JJT + damping.unsqueeze(0) # [B, 6, 6]
# 求逆(对 6×6 矩阵,直接求逆比 SVD 更快)
JJT_inv = torch.linalg.inv(JJT_damped) # [B, 6, 6]
J_pinv = torch.bmm(jacobian.transpose(1, 2), JJT_inv) # [B, n, 6]
# Step 2: 把笛卡尔位姿增量映射到关节位移增量
# Δq = J† · Δpose(策略直接输出位姿增量,这里不涉及 / dt)
delta_q = torch.bmm(J_pinv, delta_pose.unsqueeze(-1)).squeeze(-1) # [B, n]
# Step 3: 加到当前关节角度得到目标角度
q_target = q_current + delta_q # 位姿增量版本:直接相加,无需再乘 dt
return q_target
def compute_manipulability(jacobian: torch.Tensor) -> torch.Tensor:
"""计算操作性指标(manipulability measure)。
w = sqrt(det(J J^T))
w 越大说明末端在当前配置下的运动"灵活性"越好。
w 接近 0 说明接近奇异配置——某些方向几乎无法移动。
可以作为 reward 的惩罚项:鼓励策略避开低 manipulability 配置。
"""
JJT = torch.bmm(jacobian, jacobian.transpose(1, 2)) # [B, 6, 6]
det = torch.linalg.det(JJT) # [B]
return torch.sqrt(torch.clamp(det, min=1e-10)) # 避免负数
Manipulability 作为 reward 项:在操作任务中,可以加一个 manipulability_reward = w / w_max 项(权重 ~0.1),鼓励策略保持在高 manipulability 的配置中——这自动避免了奇异配置,减少 DiffIK 的数值问题。
在 mjlab 中,DiffIK 的实现位于 differential_ik.py,其核心流程:
策略输出 a ∈ [-1,1]^6
↓ × action_scale
笛卡尔增量 Δpose ∈ R^6
↓ Jacobian 计算
J(q_current) ∈ R^{6×n_joints}
↓ DLS 伪逆
Δq = J†(q) · Δpose
↓ 加到当前关节角度(位姿增量版本,不乘 dt)
q_target = q_current + Δq
↓ PD 控制器
τ = Kp(q_target - q) + Kd(dq_target - dq)
关节空间 vs 笛卡尔空间的选型决策
| 任务特征 | 推荐动作空间 | 工程原因 |
|---|---|---|
| 简单抓取(lift cube) | 关节空间 | 不需要精确末端轨迹,MLP 足够 |
| 精确轨迹跟踪(画直线) | 笛卡尔空间 | 直线在笛卡尔空间是线性的 |
| 需要冗余自由度利用 | 关节空间 | DiffIK 的最小范数解不利用冗余 |
| 高 DOF 灵巧手 | 关节空间 | Jacobian 计算量大,且手指没有简单的笛卡尔描述 |
| 多阶段操作(pick-place) | 笛卡尔空间 | 阶段之间的过渡在笛卡尔空间更直观 |
如果对 lift cube 使用 DiffIK 会怎样?性能差距通常不大——因为抓取不需要精确的末端轨迹。但 DiffIK 引入了额外的计算(Jacobian 计算和伪逆求解),且在奇异点附近需要特殊处理。对简单任务,这是不必要的工程复杂度。
双框架中的 DiffIK 配置对比
在 mjlab 中切换到 DiffIK,需要在 env_cfg.py 中替换 action 配置:
# mjlab: 从关节位置动作切换到 DiffIK(字段以当前 mjlab 源码为准)
# 修改前(关节位置动作)
actions=JointPositionActionCfg(
entity_name="robot",
actuator_names=("arm_.*", "gripper_.*"),
scale=0.1,
)
# 修改后(DifferentialIKActionCfg:直接吃笛卡尔位姿增量)
actions=DifferentialIKActionCfg(
entity_name="robot",
actuator_names=("arm_.*",), # 注意:只包含 arm actuators,不含 gripper
frame_type="site", # IK 目标 frame 类型
frame_name="grasp_site", # IK 目标 frame
use_relative_mode=True, # 动作是相对当前位姿的增量
delta_pos_scale=0.05, # 位置增量缩放
delta_ori_scale=0.05, # 朝向增量缩放
damping=0.05, # DLS 伪逆阻尼 λ
)
注意几个关键点:(1) mjlab 当前 DifferentialIKActionCfg 没有 controller=DiffIKControllerCfg(...) 这一层,DLS 求解参数(damping、position_weight/orientation_weight、joint_limit_weight、posture_weight 等)直接是 action cfg 的字段。(2) DiffIK 只作用于 arm actuators,gripper 仍需要单独的关节动作 term——IK 控制末端姿态,不控制夹爪开合。(3) frame_name 指定 IK 参考 frame,必须与 observation 中计算 ee 位姿使用的 frame 一致,否则策略的空间认知和实际 IK 控制会不匹配。
在 Isaac Lab 中,DiffIK 的配置方式类似但命名略有不同:
# Isaac Lab: DiffIK 配置
actions=DifferentialInverseKinematicsActionCfg(
asset_name="robot",
joint_names=["panda_joint.*"],
body_name="panda_hand",
controller=DifferentialIKControllerCfg(
command_type="pose",
use_relative_mode=True, # Isaac Lab 特有:相对 vs 绝对
ik_method="dls",
ik_params={"lambda_val": 0.01},
),
)
Isaac Lab 额外提供了 use_relative_mode 选项:True 表示策略输出的是相对于当前末端姿态的增量,False 表示输出绝对目标姿态。相对模式更常用——它让策略不需要知道当前末端的绝对位置,只需要输出"向哪个方向移动多远"。
本质洞察:DiffIK 的抽象层次选择就像编程语言的选择——关节空间是"汇编语言"(灵活但需要手动管理底层映射),笛卡尔空间是"高级语言"(表达力强但抽象有代价)。选择取决于任务复杂度和对底层控制的需求程度,而不是"越高级越好"。
第三种选择:Operational Space Control (OSC)
除了 JointPosition 和 DiffIK,Isaac Lab 还提供了 OperationalSpaceControllerActionCfg——基于 Oussama Khatib (1987) 提出的操作空间控制理论。三种动作空间代表三种不同的控制层次:
| 层次 | 动作空间 | 控制量 | 计算复杂度 | 适用场景 |
|---|---|---|---|---|
| 关节层 | JointPositionAction | 关节角度 (rad) | 低(直接 PD) | 大多数任务的默认选择 |
| 运动学层 | DiffIKAction | 末端位姿增量 (m, rad) | 中(Jacobian 伪逆) | 需要精确末端轨迹 |
| 动力学层 | OSCAction | 末端力/力矩 (N, N·m) | 高(质量矩阵 + Jacobian) | 需要精确力控制 |
OSC 的核心计算:
其中 \(F_{\text{desired}}\) 是任务空间的期望力(包括位置保持力和接触力),\(N(q)\) 是零空间投影矩阵,\(\tau_{\text{null}}\) 是零空间的姿态保持力矩。OSC 需要的动力学信息(质量矩阵 \(M(q)\)、科氏力 \(C(q, \dot{q})\)、重力补偿 \(g(q)\))由 PhysX 或 MuJoCo Warp 实时计算。
为什么操作任务可能需要 OSC? DiffIK 控制末端位置(运动学),但不直接控制接触力。在装配任务(如销钉插孔)中,当销钉碰到孔壁时,位置控制会产生极大的接触力——因为 DiffIK 试图让末端到达"穿过孔壁"的目标位置。OSC 可以在接触方向切换为力控制(如保持 5N 的法向接触力),在自由方向保持位置控制——这正是经典的力/位置混合控制思想在 RL 框架中的工程实现。
Isaac Lab 的 OSC 配置示例:
# Isaac Lab: OSC 配置(用于接触丰富的操作任务)
actions=OperationalSpaceControllerActionCfg(
asset_name="robot",
joint_names=["panda_joint.*"],
body_name="panda_hand",
# 注意 Isaac Lab 用的是 controller_cfg(不是 controller)
controller_cfg=OperationalSpaceControllerCfg(
target_types=["pose_abs"], # 必填:任务空间目标类型(pose/wrench × abs/rel)
impedance_mode="fixed", # "fixed" 或 "variable"(策略可调)
motion_stiffness_task=300.0, # 位置保持刚度 (N/m)
motion_damping_ratio_task=1.0, # 临界阻尼
contact_wrench_stiffness_task=None, # None = 开环力控
nullspace_control="position", # 零空间姿态保持
),
)
如果使用 impedance_mode="variable",策略输出不仅包含末端期望位姿,还包含刚度参数——这让策略可以自适应地调整"多硬地推"和"多柔软地抬"。这是当前操作 RL 的前沿方向之一。
mjlab 目前是否支持 OSC? mjlab 内置了 DiffIK(带 null-space posture regularization + soft joint-limit avoidance),但截至本书撰写时尚未内置 OSC 的完整 ActionTerm。这是因为 OSC 需要的动力学信息(质量矩阵、科氏力)在 MuJoCo Warp 中的 GPU 批量计算尚未完全优化。如果你需要 OSC,Isaac Lab + PhysX 是当前更成熟的选择。
mjlab DiffIK 的独有特色。 值得强调的是,mjlab 的 DiffIK 实现(arXiv:2601.22074 §3.3)内置了两个 Isaac Lab DiffIK 不具备的特性:(1) null-space posture regularization——在 IK 解的零空间中加入姿态正则化项,让冗余自由度(如 7-DOF 臂的肘部)自动趋向一个"舒适"的默认姿态,避免奇异配置;(2) soft joint-limit avoidance——当关节接近极限时自动在零空间中施加排斥力,降低到达关节极限的概率。这两个特性在 locomotion 中不重要(关节极少触限),但在操作任务中非常实用——机械臂在执行复杂轨迹时容易进入关节极限附近的不良配置。
Isaac Lab 的 Pink IK 控制器。 Isaac Lab 最近还引入了 PinkIKController——基于 Pink IK solver 的集成,支持带权重的多任务 IK(如同时跟踪末端位置 + 保持直立姿态 + 避免关节极限),通过加权最小二乘求解。这在 G1 LocoManip 环境(§17.9 中提到的 Isaac-PickPlace-Locomanipulation-G1-Abs-v0)中使用,用于驱动 29-DOF G1 的上半身。Pink IK 比简单的 DLS DiffIK 更灵活,但计算成本也更高——对操作入门来说,DLS DiffIK 已经足够。
三种动作空间的选型决策流程
以下是基于任务特征的动作空间选型决策流程:
你的操作任务需要什么?
├── 只需要位置控制(大多数抓取任务)
│ ├── 需要精确末端轨迹?
│ │ ├── 是 → DiffIK(笛卡尔空间直觉好)
│ │ └── 否 → JointPosition(工程最简单)
│ └── 需要利用冗余自由度(如 7-DOF 臂的肘部避障)?
│ └── 是 → JointPosition(DiffIK 的最小范数解不利用冗余)
├── 需要力控制(装配、擦拭、打磨)
│ └── OSC(唯一直接控制接触力的选项)
└── 需要自适应刚度(接触阶段柔软、搬运阶段刚硬)
└── OSC variable impedance(策略输出包含刚度参数)
对本章的 YAM lift cube 任务:选择 JointPosition。不需要精确轨迹(抓取路径不重要),不需要力控制(PD 控制器隐式处理接触力),不需要自适应刚度(始终抓紧即可)。
⚠️ 常见陷阱
⚠️ 编程陷阱:DiffIK 在奇异配置附近的数值问题
当机械臂接近完全伸直(奇异配置)时,Jacobian 的条件数暴增,伪逆计算不稳定。DLS 正则化(lambda_dls 参数)可以缓解但不能消除——在极端奇异配置下,DiffIK 的输出可能是不合理的大关节速度。自检方法:在 play 中观察机械臂是否在某些姿态突然剧烈抖动。
💡 概念误区:认为 DiffIK 是"更高级"的动作空间
DiffIK 不是关节空间的"升级版"——它是一种不同的抽象层次,有自己的优势和代价。关节空间灵活但学习效率低;笛卡尔空间直观但丧失冗余自由度利用能力。选择取决于任务需求,不是"先进程度"。
⚠️ 编程陷阱:DiffIK 的 body_name 和 observation 的参考 body 不一致
DiffIK 的 body_name 指定了 IK 计算的目标参考点。如果这个 body 与 observation 中计算 ee_pos_w 的 body 不同,策略的空间认知和 IK 控制会在不同坐标系中操作——策略认为末端在位置 A(基于 obs 中的 body),但 IK 控制器把目标映射到位置 B(基于 DiffIK 的 body)。表现为策略学到了正确的笛卡尔增量方向,但末端的实际移动量有系统性偏差。自检方法:在 play 中打印 IK 目标位置和 obs 中的 ee_pos,确认两者追踪同一个物理点。
练习
- [计算题] 假设 YAM 有 6 个关节,Jacobian 矩阵为 \(6 \times 6\)。如果 DLS 参数 \(\lambda = 0.01\),计算 DLS 伪逆 \(J^\dagger = J^T(JJ^T + \lambda^2 I)^{-1}\) 在 \(J\) 为单位矩阵时的结果。\(\lambda\) 增大对结果有什么影响?
- [实验题] 在 mjlab 中把 YAM lift cube 的 action 从
JointPositionActionCfg切换到DifferentialIKActionCfg。比较两种配置在 2000 iteration 后的 reaching reward。解释差异(如果有的话)的原因。
17.9 从 YAM 到 DexSuite 的双框架配置映射 ⭐⭐
这一节解决什么问题:提供一张 mjlab 和 Isaac Lab 操作任务配置的精确映射表,让你在一个框架中学到的知识可以快速迁移到另一个框架。
配置对应关系
| 配置组件 | mjlab (YAM Lift Cube) | Isaac Lab (DexSuite / Franka) |
|---|---|---|
| 场景定义 | SceneCfg + MJCF assets |
InteractiveSceneCfg + USD assets |
| 机器人资产 | i2rt_yam/xmls/yam.xml |
franka_panda.usd / allegro.usd |
| 物体资产 | cube.xml(freejoint) |
cuboid.usd(RigidObject) |
| 动作配置 | JointPositionActionCfg |
JointPositionActionCfg |
| 观测管理 | ObservationManager + obs_groups |
ObservationGroupCfg |
| 奖励管理 | RewardManager + reward terms |
RewardTermCfg |
| 终止管理 | TerminationManager |
TerminationTermCfg |
| 事件管理 | EventManager + 4 modes |
EventTermCfg + randomization |
| 命令 | LiftingCommandCfg |
goal position via CommandTermCfg |
| RL 后端 | RSL-RL(PPO) | rl_games / RSL-RL / SKRL |
| 可视化 | viser (web-based) | Isaac Sim Viewer / USD |
关键差异点
差异一:物体建模方式。 mjlab 用 MJCF 的 freejoint 定义可自由移动的物体。Isaac Lab 用 USD 的 RigidObject + PhysX 刚体属性。两者语义等价但语法不同——迁移时需要注意 PhysX 的惯性参数使用的是 principal axes 而非 full tensor。
差异二:观测分组语法。 mjlab 用 obs_groups = {"actor": ("actor",), "critic": ("critic",)} 在 RL 配置中定义哪些 observation group 给 actor/critic。Isaac Lab 的 ObservationGroupCfg 直接在环境配置中定义,语法更显式。
差异三:RL 后端选择。 mjlab 绑定 RSL-RL(PPO only)。Isaac Lab 支持多后端:rl_games(PPO + SHAC)、RSL-RL(PPO)、SKRL(多算法)、stable-baselines3。对操作任务,如果需要 SAC 或扩散策略(回顾 Ch07 对算法选型的讨论),Isaac Lab 的多后端支持是优势——操作任务的多模态动作分布可能更适合 SAC 或扩散策略。
差异四:视觉渲染能力。 Isaac Lab 基于 Omniverse 的 RTX 渲染器可以产生接近真实的 RGB 图像(光线追踪、PBR 材质、真实光照),这对需要 RGB 输入的操作 sim-to-real 至关重要。mjlab 的 MuJoCo Warp 渲染器功能更有限,但 depth 图的质量对大多数操作任务足够。
差异五:配置哲学。 mjlab 选择了 instance-based 配置风格——你创建一个配置对象实例,直接修改其属性。Isaac Lab 使用 class-based 配置(dataclass 继承 + __post_init__ mutation)——你通过继承基类并覆盖字段来定制配置。这种设计哲学差异影响了代码的可读性和可调试性:mjlab 的方式更直观("所见即所得"),但不容易做多级继承;Isaac Lab 的方式更灵活(可以在继承链中逐级覆盖),但 __post_init__ 的隐式修改容易造成"配置值不符合预期"的调试困难。
Isaac Lab 2.3 的操作任务新增。 Isaac Lab 2.3 引入了两个值得关注的新操作环境:
-
Isaac-PickPlace-Locomanipulation-G1-Abs-v0:G1 人形机器人的 loco-manipulation 任务,使用学习的 RL 低层策略追踪速度 + 骨盆高度命令,上层使用 Pink IK 控制器驱动 29-DOF G1 的躯干和手臂。这是当前 Isaac Lab 中最接近真实 humanoid manipulation 的内置环境,可以作为 Ch20 的前瞻参考。 -
Isaac-PickPlace-FixedBaseUpperBodyIK-G1-Abs-v0:固定底盘版——只训练上半身的 IK 操作,底盘不动。这与本章的 YAM 在任务结构上最接近,但机器人是 G1 而非 YAM。如果你想在 Isaac Lab 中复现类似本章的固定基座操作实验,这个环境是最直接的起点。
从 mjlab 到 Isaac Lab 的迁移检查清单
当你在 mjlab 中完成了操作任务的开发(reward 设计、action space 选择、超参数调优),需要迁移到 Isaac Lab 时(如需要 RTX 渲染或多后端),以下检查清单确保迁移的正确性:
迁移检查清单(mjlab → Isaac Lab):
□ 资产转换:
- MJCF → USD(使用 Isaac Sim 的 MJCF Importer 或手动 URDF→USD)
- 检查关节数量和名称是否一致
- 检查惯性参数是否自动正确转换
- 检查碰撞几何是否与视觉几何匹配
□ Actuator 配置:
- mjlab 的 kp/kd → Isaac Lab 的 stiffness/damping
- 注意:数值不能直接照搬(物理引擎求解器不同)
- 需要在 zero play 中验证关节响应一致
□ Observation 迁移:
- 逐项对比 obs term 名称和维度
- 特别注意坐标系差异(MuJoCo 使用全局坐标,PhysX 可能使用局部坐标)
- 打印两个框架的 obs 向量,在同一场景下对比数值
□ Reward 迁移:
- reward function 的数学表达式相同
- 但 sigma 参数可能需要重新调——因为物理行为差异导致特征距离不同
- 先用 random agent 打印两个框架的 reward 值域,确认量级一致
□ Action 迁移:
- action_scale 可能需要调整(不同求解器对相同位置命令的响应不同)
- DiffIK 的 lambda_dls 在两个框架中意义相同但最优值可能不同
□ 训练验证:
- 在两个框架中分别训练 2000 iter
- 对比 reaching_reward 和 success_rate
- 允许 ±20% 的性能差异(来自物理引擎差异)
- 如果差异 > 50%,逐项排查以上检查项
一个实际的迁移陷阱:关节顺序差异。 mjlab 的 MJCF 中关节按 XML 出现顺序排列。Isaac Lab 的 USD 中关节按 USD prim 的字母顺序排列(除非显式指定)。如果两者的关节顺序不同,action_scale 向量中的每个元素会对应到不同的关节——底盘 kp 被应用到夹爪上(或反过来)。自检方法:在两个框架中打印关节名称列表,确认顺序一致;如果不一致,在 Isaac Lab 的 ArticulationCfg 中用 joint_names_expr 显式指定顺序。
⚠️ 常见陷阱
⚠️ 编程陷阱:直接照搬 mjlab 的 kp/kd 到 Isaac Lab
MuJoCo 的 PD 控制器和 PhysX 的 PD 控制器使用不同的求解器——相同的 kp 值在两个框架中产生不同的力矩。Isaac Lab 的 kp 通常需要比 mjlab 大 2-5 倍才能达到类似的关节刚度。经验法则:在 zero play 中给一个小角度命令(如 0.1 rad),观察关节响应速度——调整 kp 直到两个框架的响应速度接近。
💡 概念误区:认为"两个框架训练的策略应该完全一样"
不同物理引擎的接触模型、求解器精度和数值积分方式都有差异。即使配置"完全一样",两个框架训练出的策略在行为细节上也会不同。重要的是 task metric(success rate、tracking error)一致——而非 reward 曲线的完全重合。
练习
- [对比题] 在 mjlab 和 Isaac Lab 中分别配置 YAM lift cube 的等价环境。打印两者的 obs 向量(在同一初始条件下),对比哪些维度的数值相同、哪些不同。解释差异的来源。
- [迁移题] 按上述迁移检查清单,将 mjlab 中训练好的 YAM lift cube 策略的 reward 配置迁移到 Isaac Lab。在 Isaac Lab 中训练 2000 iter,对比 success rate。
这两个 G1 pick-place 环境(Isaac-PickPlace-Locomanipulation-G1-Abs-v0 与 Isaac-PickPlace-FixedBaseUpperBodyIK-G1-Abs-v0)由 Isaac Lab 2.3 的 PR #3150 引入;面向 Unitree G1 Inspire 五指手的遥操作支持则来自 PR #3242。其上半身 IK 控制器使用 null-space posture regularization 来保持直立偏好姿态——这与 §17.8 讨论的 DiffIK null-space 控制是同一思想在人形平台上的应用。
迁移工作流:从 mjlab Lift Cube 到 Isaac Lab Franka Lift
具体到从 mjlab YAM lift cube 迁移到 Isaac Lab Franka lift,需要修改的配置可以分为三个层级:
层级一:必须修改(否则无法运行)。 (1) 资产文件从 MJCF .xml 改为 USD .usd;(2) 场景配置类从 SceneCfg 改为 InteractiveSceneCfg;(3) action 中的 joint names 从 YAM 的命名改为 Franka 的命名(如 panda_joint1..7);(4) observation 中引用的 body names 和 joint names 全部替换;(5) 物理引擎参数从 MuJoCo 的 solref/solimp 改为 PhysX 的 contact_offset/solver_iteration_count。
层级二:建议修改(否则性能受影响)。 (1) reward 的 \(\sigma\) 参数需要根据 Franka 的工作空间重新标定——YAM 和 Franka 的机械臂长度不同,reaching 的典型距离不同;(2) action scale 需要根据 Franka 的关节角度范围重新调整;(3) observation 中的 normalization 参数需要更新。
层级三:可选修改(根据需求)。 (1) 切换 RL 后端(如从 RSL-RL 切到 rl_games 以使用 SHAC 算法);(2) 启用 Isaac Lab 特有的 RTX 渲染做 RGB 版本;(3) 利用 Isaac Lab 的 Replicator 做更丰富的视觉 DR。
本质洞察:框架迁移的核心困难不在语法——任何有经验的工程师都能查文档改配置。困难在于物理引擎的行为差异:同一个 reward function 在 MuJoCo 和 PhysX 中可能诱导出不同的策略,因为两个引擎对同一接触力的求解结果不同。因此,迁移后必须重新训练而非直接加载 checkpoint。
⚠️ 常见陷阱
⚠️ 编程陷阱:跨框架迁移时坐标系约定不同
mjlab 使用 MuJoCo 的 z-up 坐标系,Isaac Lab 默认也是 z-up,但 USD 资产可能使用 y-up。如果从 Blender(y-up)导出 USD 到 Isaac Lab 而没有坐标系转换,物体可能"躺着"加载。
💡 概念误区:认为一个框架的训练结果可以直接迁移到另一个框架
即使任务逻辑相同,MuJoCo Warp 和 PhysX 的物理行为不同(接触模型、求解器特性)。在一个框架中训练好的策略在另一个框架中可能表现不同——这不是 bug,而是物理引擎差异的正常表现。跨框架验证的正确做法是在两个框架中分别训练并比较。
练习
- [迁移题] 列出将 mjlab YAM lift cube 迁移到 Isaac Lab Franka lift 需要修改的 5 个核心配置项。对每一项,写出 mjlab 的配置语法和 Isaac Lab 的对应语法。
- [设计题] 如果你想在 Isaac Lab 中使用 SAC 而非 PPO 训练 Franka lift cube,列出需要修改的配置项和需要注意的数据流差异(提示:回顾 Ch07 中 on-policy vs off-policy 的 rollout storage 差异)。
上节建立了双框架的配置映射。但配置映射只解决了"怎么迁移"的问题。操作任务还有一个 locomotion 中较少涉及的维度:视觉输入。YAM 的四种变体中,三种涉及视觉——这是下一节的主题。
17.10 视觉操作变体:从 State 到 RGB/Depth ⭐⭐⭐
这一节解决什么问题:理解操作任务从 state 输入切换到视觉输入时的工程变化,以及如何避免视觉版中常见的 privileged 信息泄漏。
动机:为什么视觉对操作任务尤其重要
Locomotion 任务中,策略主要依赖本体感知(关节位置/速度、IMU)来控制运动——地面的触觉反馈和本体感受提供了足够的信息。视觉在 locomotion 中主要用于感知地形(Ch18 的主题)。
操作任务中,策略必须感知外部物体的位置和形状。在 state 版中,这些信息通过 privileged 的 cube_pos_w、ee_to_cube 等向量直接提供。但在真实部署中,这些信息必须从视觉传感器(RGB 相机或深度传感器)中提取。因此,视觉不是操作任务的"进阶选项"——它是 sim-to-real 的必经之路。
State → Depth → RGB 的渐进管线
操作任务的视觉开发通常遵循一个渐进策略,而非一步到位:
| 阶段 | 输入 | 目标 | 工程重点 |
|---|---|---|---|
| Stage 0 | 无(zero/random play) | 验证物理资产正确 | 碰撞几何、接触参数、方块自由落体 |
| Stage 1 | State (privileged) | 验证 MDP 设计正确 | reward、action、termination 的正确性 |
| Stage 2 | Depth | 验证 CNN 能从深度图提取物体信息 | CNN 架构、图像预处理、obs group 配置 |
| Stage 3 | RGB | 处理材质、光照、颜色变化 | 纹理随机化、光照随机化、颜色 augmentation |
为什么不直接从 RGB 开始?因为 RGB 引入了大量视觉无关的变化源(光照、材质、背景颜色),如果 MDP 本身有问题(reward 设计错误、action scale 不当),你无法区分失败是来自视觉还是 MDP。先用 state 验证 MDP 正确性,再用 depth 验证空间感知能力,最后用 RGB 处理视觉外观变化。这种渐进策略与软件工程中"先让功能正确再优化性能"的原则一致。
视觉版 Observation Group 配置
YAM Depth 版的 observation group 配置结构如下:
# 视觉版的 obs_groups 配置(关键差异)
obs_groups = {
"actor": ("actor", "camera"), # actor 看到 state + 图像
"critic": ("critic", "camera"), # critic 看到 privileged state + 图像
}
其中 "actor" group 中不应包含 ee_to_cube、cube_pos_w 等物体状态 term。这些信息应该只在 "critic" group 中出现。"camera" group 包含深度图(或 RGB 图),同时给 actor 和 critic。
一个关键的工程细节:RSL-RL 的 obs_groups 名字必须在 env cfg 和 rl_cfg 中完全一致。如果 env cfg 定义了 "camera" group 但 rl_cfg 中写成了 "Camera"(大小写不同),网络创建时会找不到对应的 group,报一个不太直观的 shape 错误。
如何检测 privileged 信息泄漏? 这是视觉操作中最隐蔽的 bug——策略看起来"学会了",但实际上完全不使用视觉输入。检测方法有三个层级:
(1) 静态检查:在代码中逐项核查 actor obs group 包含的 term,确认没有 ee_to_cube、cube_pos_w、cube_quat_w、cube_to_goal 等物体/目标状态。这是最简单但最有效的检查。
(2) 遮蔽测试:训练完成后,用 play 模式评估策略,但把 camera 图像替换为全零 tensor(相当于"遮住相机")。如果策略仍然能成功抓取,说明视觉输入没有被使用——一定存在 privileged 泄漏。正确的视觉策略在遮蔽测试中应该完全失效。
(3) 梯度检查:在训练过程中,定期检查 CNN 第一层权重的梯度范数。如果梯度范数接近零(相对于 MLP 部分),说明 CNN 没有被有效训练。这通常比遮蔽测试更早发现问题——不需要等训练完成。
这三个检查应该形成习惯:每次启动视觉版训练前做静态检查,训练初期做梯度检查,训练完成后做遮蔽测试。
CNN 架构与 SpatialSoftmax
视觉版使用 CNN 处理图像输入。mjlab YAM 视觉 PPO 的默认 CNN(_VISION_CNN_CFG)实际是两层 + SpatialSoftmax:output_channels=[16, 32]、kernel_size=[5, 3]、stride=[2, 2]、spatial_softmax=True:
Depth Image [1, H, W]
↓ Conv2d(1, 16, 5, stride=2) + ReLU # 默认第一层 16 通道
↓ Conv2d(16, 32, 3, stride=2) + ReLU # 默认第二层 32 通道
↓ SpatialSoftmax(默认开启)
↓ Linear → feature vector
↓ 与 state features 拼接
↓ MLP → action
注意:下面"完整 CNN 实现"给出的是一个三层教学示例(32/64/64),通道数比 mjlab 默认更大,用于演示更深的视觉编码器;它不是 mjlab YAM 的默认配置。
操作视觉的完整 CNN 实现:
import torch
import torch.nn as nn
import torch.nn.functional as F
class ManipulationVisualEncoder(nn.Module):
"""操作任务的视觉编码器。
与 Ch18 的 locomotion 编码器类似,但有以下差异:
1. SpatialSoftmax 是默认选择(操作需要物体定位)
2. 输入可以是 depth (1ch) 或 RGB (3ch)
3. 分辨率通常更高(64×64 或 128×128)——因为物体细节比地形更重要
"""
def __init__(
self,
in_channels=1, # 1 for depth, 3 for RGB
base_channels=32,
num_layers=3,
use_spatial_softmax=True,
temperature=1.0,
output_dim=128,
):
super().__init__()
layers = []
ch_in = in_channels
for i in range(num_layers):
ch_out = base_channels * (2 ** min(i, 1)) # 32, 64, 64
kernel = 5 if i == 0 else 3
stride = 2 if i < 2 else 1
layers.extend([
nn.Conv2d(ch_in, ch_out, kernel, stride=stride, padding=kernel // 2),
nn.ReLU(inplace=True),
])
ch_in = ch_out
self.backbone = nn.Sequential(*layers)
self.use_spatial_softmax = use_spatial_softmax
if use_spatial_softmax:
self.pool = SpatialSoftmax(ch_out, temperature=temperature)
feature_dim = ch_out * 2 # 每个 channel 输出 (x, y)
else:
self.pool = nn.AdaptiveAvgPool2d(1)
feature_dim = ch_out
self.fc = nn.Linear(feature_dim, output_dim)
def forward(self, image):
"""
Args:
image: [B, C, H, W] 归一化后的图像
Returns:
features: [B, output_dim]
"""
x = self.backbone(image)
if self.use_spatial_softmax:
x = self.pool(x)
else:
x = self.pool(x).flatten(1)
return self.fc(x)
class SpatialSoftmax(nn.Module):
"""SpatialSoftmax 池化层。
对每个 feature map 计算"注意力中心"的 (x, y) 坐标。
这让 CNN 的输出直接表达物体的空间位置——
比 Global Pooling 保留了更多的几何信息。
"""
def __init__(self, num_channels, temperature=1.0):
super().__init__()
self.temperature = temperature
self.num_channels = num_channels
def forward(self, feature_maps):
# feature_maps: [B, C, H, W]
B, C, H, W = feature_maps.shape
# 创建归一化的坐标网格
x_coords = torch.linspace(0, 1, W, device=feature_maps.device)
y_coords = torch.linspace(0, 1, H, device=feature_maps.device)
# 对每个 channel 做 spatial softmax
flat = feature_maps.reshape(B, C, -1) # [B, C, H*W]
weights = F.softmax(flat / self.temperature, dim=-1) # [B, C, H*W]
weights = weights.reshape(B, C, H, W)
# 计算期望坐标
expected_x = (weights.sum(dim=2) * x_coords).sum(dim=-1) # [B, C]
expected_y = (weights.sum(dim=3) * y_coords).sum(dim=-1) # [B, C]
# 拼接输出
return torch.cat([expected_x, expected_y], dim=-1) # [B, 2*C]
操作视觉的完整策略网络:
class VisualManipulationPolicy(nn.Module):
"""视觉操作的完整策略网络。
数据流:
depth/RGB → CNN Encoder → visual_features (128D)
proprio (joint_pos, joint_vel, ee_pos, gripper) → concat
→ MLP → action (joint positions or DiffIK)
"""
def __init__(
self,
visual_encoder: ManipulationVisualEncoder,
proprio_dim: int = 24, # 关节位置 + 速度 + EE 位姿 + 夹爪
action_dim: int = 7, # YAM: 6 arm joints + 1 gripper(若用 Franka 则 7+1=8)
hidden_dims: tuple = (256, 128),
):
super().__init__()
self.visual_encoder = visual_encoder
input_dim = visual_encoder.fc.out_features + proprio_dim
layers = []
prev = input_dim
for h in hidden_dims:
layers.extend([nn.Linear(prev, h), nn.ELU()])
prev = h
layers.append(nn.Linear(prev, action_dim))
self.mlp = nn.Sequential(*layers)
def forward(self, image, proprio):
vis_feat = self.visual_encoder(image)
combined = torch.cat([vis_feat, proprio], dim=-1)
return self.mlp(combined)
视觉版相机配置(mjlab + Isaac Lab)
# mjlab: YAM 视觉任务的相机配置(字段与默认值以当前 mjlab 源码为准)
# MJCF 中已定义相机 robot/camera_d405
from mjlab.entity.sensor import CameraSensorCfg
cam_cfg = CameraSensorCfg(
name="camera_d405", # 取自 camera_name 末段
camera_name="robot/camera_d405", # MJCF 中的相机全名
height=32, # YAM 默认 32x32(小分辨率以提吞吐)
width=32,
data_types=("depth",), # 也可用 ("rgb",)
cutoff_distance=0.5, # depth 截断距离(操作任务范围比 locomotion 小)
)
# 注意:mjlab 用的是 CameraSensorCfg;没有 CameraObsCfg/normalize/clip_range 这些字段。
# 64x64 等更高分辨率属于可选改造,不是 YAM 默认。
# Isaac Lab: 操作任务的 TiledCamera 配置
from isaaclab.sensors import TiledCameraCfg
wrist_camera = TiledCameraCfg(
prim_path="{ENV_REGEX_NS}/Robot/wrist_link/wrist_camera",
offset=TiledCameraCfg.OffsetCfg(
pos=(0.05, 0.0, 0.02),
rot=(1.0, 0.0, 0.0, 0.0), # (w,x,y,z) 朝前
convention="ros",
),
data_types=["distance_to_image_plane"],
spawn=PinholeCameraCfg(
focal_length=24.0,
horizontal_aperture=20.955,
clipping_range=(0.01, 2.0),
),
width=64,
height=64,
)
操作 vs locomotion 相机配置的关键差异:
| 参数 | Locomotion (Ch18) | Manipulation (本章) | 原因 |
|---|---|---|---|
| 安装位置 | 前视(base 前方) | 手腕或俯视(看工作台) | 需要看到末端和物体 |
| 裁剪范围 | 0.01-5.0 m | 0.01-2.0 m | 操作在近距离 |
| 分辨率 | 64×64 | 64×64 或 128×128 | 物体细节比地形更重要 |
| FOV | 80-120° | 60-90° | 操作不需要广角 |
| 帧率 | 15-30 Hz | 与控制同频(50 Hz) | 物体运动更快 |
视觉版的完整训练配方
# 操作视觉版的完整训练流程
# Step 1: 确认 state 版 MDP 正确(参考 §17.6)
uv run train YAM-Lift-State --env.scene.num-envs 4096 --agent.max-iterations 3000
# 验证 bringing_reward > 0.5
# Step 2: 切换到 depth 版
uv run train YAM-Lift-Depth \
--env.scene.num-envs 512 \
--env.cameras.wrist_cam.enabled True \
--agent.max-iterations 5000 \
--agent.learning-rate 1e-4 \
--agent.cnn-lr-scale 0.3 \
--agent.logger wandb
# Step 3 (可选): 切换到 RGB 版 + 视觉 DR
uv run train YAM-Lift-RGB \
--env.scene.num-envs 256 \
--env.cameras.wrist_cam.data-type rgb \
--dr.texture-randomization True \
--dr.lighting-randomization True \
--agent.max-iterations 8000
视觉版训练的 TensorBoard 关键指标:
需要额外监控的指标(state 版不需要):
cnn_grad_norm:
正常:> 0 且稳定(CNN 在学习)
异常:≈ 0 → CNN 未连接到 obs / privileged 泄漏
visual_feature_std:
正常:逐步增大到 0.1-1.0
异常:始终 ≈ 0 → CNN 输出全零(未学到任何特征)
reaching_reward (对比 state 版):
正常:达到 state 版的 60-80%
异常:< state 版的 30% → 视觉输入可能有问题
rendering_time_ms:
正常:< 5 ms per step (64×64)
异常:> 20 ms → 渲染成为瓶颈,减少 num_envs
SpatialSoftmax 是一种特殊的池化操作,它输出每个特征图的"期望空间位置"(x, y 坐标),而非 global pooling 的统计量。这对操作任务有天然优势——因为物体的空间位置正是策略需要的核心信息。SpatialSoftmax 输出 2 × num_channels 个值(每个通道一个 x 和一个 y),比 global average pooling 保留了更多的空间信息。
其计算过程如下:对每个特征图 \(F_k \in \mathbb{R}^{H \times W}\),首先计算空间 softmax 权重 \(w_k(i,j) = \frac{\exp(F_k(i,j))}{\sum_{i',j'} \exp(F_k(i',j'))}\),然后计算期望坐标 \(x_k = \sum_{i,j} w_k(i,j) \cdot j / W\) 和 \(y_k = \sum_{i,j} w_k(i,j) \cdot i / H\)。输出 \((x_k, y_k)\) 就是第 \(k\) 个特征图的"注意力中心"。当特征图对方块的边缘响应最强时,SpatialSoftmax 的输出就是方块边缘在图像中的位置——这正是策略需要的定位信息。
如果用 global average pooling 替代 SpatialSoftmax 会怎样?global pooling 丢弃了空间位置信息,只保留了"特征图中有没有特定 pattern"的信息。对分类任务这够了("图中有没有猫"),但对操作任务不够("方块在图像的哪个位置"直接决定了 reaching 的方向)。实验表明,SpatialSoftmax 在 manipulation 任务上通常比 global pooling 收敛快 2-5 倍。
视觉版训练的工程注意事项
从 state 版切换到视觉版时,除了 obs 配置的变化,还有几个工程层面的注意事项会显著影响训练效率:
渲染开销与环境数量的权衡。 State 版可以轻松并行 4096 个环境,因为不需要渲染。Depth 版每个环境每步都需要渲染一帧深度图——渲染成本远高于物理仿真。因此视觉版的并行环境数通常需要降低到 512-1024。减少的环境数意味着每次 rollout 的样本量减少,需要相应调整 mini-batch size 和 PPO 的 epoch 数。
图像归一化。 MuJoCo Warp 输出的深度图是以米为单位的浮点数(如近处 0.3m,远处 2.0m)。不同于 ImageNet 预训练模型期望的 [0, 1] 或标准化输入,操作任务的深度图通常用简单的线性归一化:\(d_{\text{norm}} = (d - d_{\text{min}}) / (d_{\text{max}} - d_{\text{min}})\),其中 \(d_{\text{min}}\) 和 \(d_{\text{max}}\) 是相机的近裁剪面和远裁剪面距离。如果用了不正确的归一化(如均值方差标准化),CNN 看到的值域会随场景变化——一个只有方块的场景和一个有方块加桌面的场景,相同深度对应不同的归一化值。
梯度回传路径。 在 PPO 中,CNN 和 MLP 是端到端训练的——CNN 的梯度来自 policy loss 和 value loss。如果 CNN 的学习率与 MLP 相同,可能出现两种问题:CNN 更新太快(视觉特征不稳定,策略抖动)或 CNN 更新太慢(视觉特征没有学到有用信息)。一种常见做法是给 CNN 分支设置独立的、更小的学习率(通常是 MLP 的 0.1-0.5 倍)。
帧堆叠 vs 帧差分。 单帧深度图只提供了空间信息,没有时间信息(物体在移动还是静止?)。两种常见方案:帧堆叠(stack 最近 2-4 帧作为多通道输入)或帧差分(当前帧减去前一帧,显式编码运动)。对 lift cube 这种物体初始静止的任务,帧差分通常效果更好——因为"方块是否在被搬运"可以直接从差分图中看出(移动区域有非零值)。对灵巧手的 in-hand rotation,帧堆叠通常更好——因为旋转的方向和速度需要多帧的空间信息来推断。
从测量模型理解操作视觉
上面讨论的所有工程细节——图像归一化、CNN lr 缩放、帧堆叠——都是在回答"如何处理视觉输入"。但要理解为什么这些细节如此重要,需要后退一步:视觉不是"更多的数字"——它是一个有投影、遮挡和光照的测量系统。
可以把相机测量过程写成一个函数:
其中 \(x_t\) 是仿真状态(物体位姿),\(g\) 是场景几何,\(K\) 是相机内参,\(T_{\text{cam}}\) 是外参,lighting 和 material 决定了 RGB 的外观。这个函数的输出可以是 RGB、depth 或 segmentation。
这个视角从根本上改变了我们设计视觉策略的方式。如果视觉只是"更多的数字",我们只需要更大的 MLP。但因为视觉是一个有投影、遮挡、光照的测量系统,我们必须关心测量过程本身——相机放在哪里、看到了什么、看不到什么、看到的东西在什么条件下会变。Visual DR 的本质是在 \(\text{Render}\) 函数的参数空间中做随机化——改变 lighting 和 material 但不改变 \(x_t\)——迫使 CNN 学到对测量条件鲁棒的特征。
用一个跨领域类比来理解:操作视觉面临的问题与卫星遥感惊人地相似。卫星从高空拍图识别地面物体,但图像外观会随季节(光照角度)、天气(云层遮挡)和传感器老化(噪声变化)剧烈变化。遥感领域的 domain adaptation 和机器人视觉的 domain randomization 本质上是同一思想——只不过我们可以在仿真中主动控制"成像条件"的变化范围。但两者有一个关键区别:遥感是开环的(识别结果不影响下一张图像),视觉控制是闭环的(动作改变场景,进而改变下一帧图像)——分布漂移因此更加严重。
多模态融合策略:Early vs Late Fusion
视觉策略的 observation 包含高维图像和低维 proprioception 两种模态。如何融合它们是一个重要的设计决策:
Early Fusion(像素级拼接):把低维状态广播到图像尺寸,与图像在 channel 维拼接后一起送入 CNN。每一层都能同时看到视觉和状态信息——但低维状态被不必要地重复了 \(H \times W\) 次,浪费计算且干扰 CNN 的空间特征提取。
Late Fusion(特征级拼接):图像单独经过 CNN 编码成低维特征,然后与 proprioception 拼接后送入 MLP。这是 mjlab 和大多数视觉 RL 框架采用的方式——CNN 专注于提取视觉特征,MLP 负责融合。
为什么 Late Fusion 更合理? CNN 的归纳偏置(局部性、平移等变性)是为空间数据设计的。把一个标量(如 gripper 开合度)广播到 \(64 \times 64\) 的 spatial map 上,破坏了 CNN "同样的 pattern 出现在任何位置都应被同样检测" 的假设——因为 gripper 状态在每个位置都一样,它变成全局偏置而非有用的空间信息。
用一个类比来理解:Late Fusion 就像交响乐团的配器——小提琴擅长旋律(CNN 擅长空间特征),定音鼓擅长节奏(proprioception 擅长精确状态)。好的配器不是让定音鼓也演奏旋律,而是让每种乐器发挥特长后在适当的时机融合。Late Fusion 让 CNN "演奏视觉旋律",MLP "整合节奏和旋律"来产生最终的动作。
视觉版的 Domain Randomization
视觉版需要额外的视觉 DR 来弥补 sim-to-real 的外观差异:
| DR 类型 | 参数 | 典型范围 | 作用 |
|---|---|---|---|
| 纹理随机化 | 方块颜色/材质 | 均匀采样 RGB | 对颜色变化鲁棒 |
| 光照随机化 | 光源位置/强度 | ±30% 位置、±50% 强度 | 对光照条件鲁棒 |
| 相机噪声 | 深度/RGB 噪声 | Gaussian σ=0.01 | 对传感器噪声鲁棒 |
| 背景随机化 | 桌面颜色/纹理 | 随机采样 | 对背景变化鲁棒 |
| 相机位姿 | 外参微扰 | ±2° 旋转、±1cm 平移 | 对安装误差鲁棒 |
注意:Depth 版通常不需要纹理和光照随机化(深度图不受这些因素影响),但需要深度噪声随机化。RGB 版则需要全套视觉 DR。这是 Depth 版开发效率更高的原因之一——需要调的 DR 参数更少。
视觉操作的 Sim-to-Real 前沿:VIRAL
当你完成了 depth 版的训练并准备部署到真实机器人时,sim-to-real gap 是最大的挑战。VIRAL(NVIDIA, 2025)是当前视觉操作 sim-to-real 最大规模的开源工作——在 64 GPUs 上训练、在 Unitree G1 上实现 54/59 连续 loco-manipulation 循环。虽然 VIRAL 超出了本章的固定基座操作范围(它是 humanoid loco-manipulation),但其视觉 sim-to-real 的工程方法论完全适用于固定基座操作:
Delta-action space for long-horizon tasks。 VIRAL 的 teacher 策略使用 delta-action(输出相对于当前位置的增量而非绝对位置),这在长序列任务中比绝对位置 action 更稳定——因为误差不会随时间累积。这与本章 §17.8 讨论的 DiffIK use_relative_mode=True 是同一思想的应用。
Mixed online DAgger + BC distillation。 VIRAL 的 vision student 不是纯 BC 蒸馏——它混合了在线 DAgger(student 执行、teacher 标注)和离线 BC(teacher 执行数据上训练)。这比纯 BC 更鲁棒,因为 student 在自己的分布上获得了 teacher 的修正信号。
大规模 tiled rendering。 VIRAL 使用 Isaac Lab 的 tiled rendering 在单个 GPU 上同时渲染数百个环境的图像。Isaac Lab 的 TiledCamera 将多个环境的相机输出拼接到一张大的 GPU framebuffer 中,避免了 per-environment 渲染的开销。文档建议:在 RTX 4090 上,512 个相机是上限;每个 tile 的分辨率建议 ≥100×100 以确保 DLSS 有效。
对本章的固定基座操作,VIRAL 的工程方法论意味着:(1) 先在 state 版验证 MDP,(2) 用 teacher-student 架构蒸馏视觉策略,(3) 用大规模视觉 DR 弥补 sim-to-real gap。这与 §17.10 的渐进管线完全一致——VIRAL 只是把每一步的工程质量推到了工业级水准。
操作策略的前沿替代:Diffusion Policy
到目前为止,本章的策略都是标准的 MLP + PPO 方案。Diffusion Policy(Chi et al., IJRR 2025)提出了一种基于扩散模型的操作策略表示——策略不直接输出动作,而是通过迭代去噪从噪声中"生成"动作序列。
Diffusion Policy 的核心工程特性:
| 特性 | MLP (PPO) | Diffusion Policy |
|---|---|---|
| 动作分布 | 单高斯(unimodal) | 多模态(multimodal) |
| 推理速度 | ~0.1ms | ~10-50ms(需要多步去噪) |
| 数据需求 | 在线 RL(大量环境并行) | 离线 IL(需要 demonstration) |
| 适合任务 | 单一明确策略的任务 | 多种有效策略的任务 |
| 训练框架 | PPO + RSL-RL | BC + 自定义 diffusion loop |
什么时候 Diffusion Policy 优于 MLP? 当任务有多种等效的成功策略时——例如装配任务中螺丝可以从左边拧也可以从右边拧,MLP 的单高斯输出会"折中"两种策略导致失败(输出两种策略的均值,而均值不是有效的动作)。Diffusion Policy 可以在不同的去噪轨迹中生成不同的策略,每一条都是有效的。
什么时候 MLP 足够? 对 lift cube 这种只有一种合理抓取策略的任务(从上方接近、闭合夹爪、抬起),MLP 和 Diffusion Policy 的性能差异很小——因为动作分布本身就是单模态的。论文的实验数据也证实了这一点:Diffusion Policy 在多模态任务(如 T 形块插入)上显著优于 MLP,但在 lift 类任务上差异不大。
推理速度的工程代价。 标准 Diffusion Policy 需要 100 步去噪(DDPM),推理耗时 ~50ms——对 50Hz 控制的操作任务来说过慢。最近的加速方法包括 DDIM(25 步)、Consistency Distillation(1 步,ManiCM 实现 10× 加速),使推理时间降低到可接受的范围。但这些加速方法增加了工程复杂度——对本章的教学目的,MLP + PPO 是更合适的起点。
向前预告:Diffusion Policy 在 Ch16(模仿学习)中会作为主要方法之一深入讨论。本章只需理解其在操作任务中的定位——它是 MLP 的高级替代方案,适合多模态、长序列、需要 demonstration 的操作任务。
⚠️ 常见陷阱
⚠️ 编程陷阱:图像的 HWC/CHW 格式未正确转换
MuJoCo Warp 渲染器输出的图像通常是 HWC 格式(Height × Width × Channels),但 PyTorch 的 Conv2d 期望 CHW 格式。如果忘记 permute(0, 3, 1, 2) 转换,CNN 会把空间维度当成通道维度,输出完全是噪声。自检方法:保存一帧 tensor,用 matplotlib 可视化,确认图像内容正确。
💡 概念误区:认为"分辨率越高视觉越好"
高分辨率(如 256×256)确实保留了更多细节,但 CNN 的计算量随分辨率平方增长。对 lift cube 这种只需知道"方块在哪"的任务,32×32 或 64×64 的深度图已经足够——方块在图像中只占几个像素,更高的分辨率提供的额外信息不值其计算代价。先从 32×32 开始验证管线,再按需增加分辨率。
🧠 思维陷阱:把视觉策略的失败归因于 CNN 架构
当视觉版训练失效时,90% 的问题在输入层面而非模型层面——camera_name 错误、obs_groups 配置不匹配、图像未归一化到 [0,1]、privileged 信息泄漏。只有在确认输入完全正确后,才应该考虑修改 CNN 架构(层数、通道数、池化方式)。
四个操作视觉工程案例
以下案例来自实际开发中反复出现的问题模式。每个案例提供具体的诊断步骤和修复路径。
案例一:单方块 Depth 抓取——从低维到视觉的迁移
背景:state 版策略能成功抓取,换成 depth 后训练不稳定。这是视觉操作最常见的起步问题。
诊断步骤:(1) 保存一帧 depth tensor,打印 min、max、NaN ratio 和 shape。(2) 确认裁剪后的 depth max < cutoff_distance。(3) 在 viewer 中确认方块在视野中心且占据合理的像素面积(5%-30%)。(4) 检查 CNN feature norm 是否持续增长——为零说明梯度消失。
健康指标参考:
| 信号 | 健康范围 | 问题指示 |
|---|---|---|
| depth min | > 0.05 m | 太小→near plane 太近或有穿模 |
| depth max(裁剪后) | < cutoff_distance | 等于 cutoff→裁剪在工作 |
| NaN ratio | 0% | 有 NaN→渲染配置有误 |
| 方块像素覆盖率 | 5%-30% | 太小→目标不可见;太大→超出视野 |
| CNN feature norm | 持续增长后稳定 | 持续为零→梯度消失 |
通过标准:depth 视觉策略的 success rate 达到 state 基线的 60% 以上。如果差距过大(<40%),应先排查视觉管线问题而非调 RL 超参。
案例二:RGB 抓取的纹理过拟合
背景:训练时方块颜色固定为红色,颜色随机化后策略失败。这揭示了 RGB 策略最核心的问题——颜色捷径。
诊断步骤:(1) play 时只改方块颜色(绿/蓝/白),不改位置和光照。(2) 保存 SpatialSoftmax 关键点可视化——如果颜色改变后关键点从方块跳到背景的相似颜色区域,就是颜色捷径的证据。(3) 对比 depth-only 在同条件下是否不受影响——如果是,说明 RGB 策略依赖颜色而非几何。
阶段化修复路径:先加方块颜色随机化(色相 ±60°)→ 再加桌面纹理随机化 → 再加光照随机化 → 每步都重新检查 feature 可视化。修复方法不是加更大的 CNN——而是加受控的 DR 迫使 CNN 学基于几何的特征。
案例三:多方块目标选择——Target Contract 验证
背景:场景里有多个方块,策略必须抓指定目标而不是最近的。核心不是视觉编码而是 target mask、command 和 reward 之间的一致性。
Target Contract 的三个一致性要求:(1) Command 中的 target_id 必须与 reward 评估的目标一致。(2) Mask 的非零区域必须覆盖且仅覆盖 command 指定的目标。(3) Reward 只奖励抓取正确目标,抓 distractor 不被奖励。任何一个不一致都会导致策略学到"抓最近的物体"而非"抓指定的目标"。
案例四:腕部相机遮挡处理
背景:腕部相机在接近方块时视角好,但抓取时末端遮挡了目标。这是 partial observability 的典型场景。
核心问题:夹爪闭合时遮挡了目标,策略此刻必须依靠"记忆"(之前看到目标的位置)来完成闭合。如果策略没有记忆机制(纯单帧 MLP),遮挡瞬间它会"失忆",输出变随机。
设计决策:对桌面抓取,遮挡窗口通常 2-5 帧(100-250 ms @ 20 Hz)。2 帧 history(concatenate 最近 2 帧)通常足够覆盖。如果遮挡更长(如手臂完全挡住目标),可能需要 RNN——但工程上更好的方案是调整相机位置以减少遮挡,而不是用复杂网络"硬学"遮挡下的行为。
视觉版验收清单
在视觉操作策略的每个开发阶段,使用以下清单验收:
Stage 1(state 基线):
□ State 版 success rate > 60%(证明 MDP 设计正确)
□ reward 分项(reach/bring)收敛趋势正常
Stage 2(depth 切换):
□ depth 图可视化:方块清晰可见,值域在 [near_clip, far_clip] 内
□ actor obs group 不含 ee_to_cube / cube_to_goal(无 privileged 泄漏)
□ CNN 第一层梯度 > 0(CNN 在学习)
□ 遮蔽测试:图像全零时策略失效(确认依赖视觉)
□ depth 版 success rate > state 版 × 60%
Stage 3(RGB 切换,可选):
□ 颜色随机化后 success rate 不降 > 20%
□ SpatialSoftmax 关键点在方块区域(不在背景)
□ 光照/纹理 DR 开启后收敛
全阶段通过后进入 sim-to-real 准备。
练习
- [实验题] 在 mjlab 中分别训练 YAM state 版和 depth 版 2000 iteration。比较 reaching reward 的收敛速度差异。如果 depth 版慢 3 倍以上,检查 CNN 输入是否正确。
- [设计题] 设计一个实验来验证 SpatialSoftmax 对 depth 版操作任务的贡献:训练两个版本(SpatialSoftmax vs Global Average Pooling),用相同的超参数和种子,比较 2000 iteration 后的 reaching + bringing reward。预测哪个更好并解释原因。
本章小结
| 知识点 | 核心结论 | 重要程度 |
|---|---|---|
| 操作 vs locomotion 的 MDP 差异 | 共享框架但不共享设计直觉,六维结构性差异 | ⭐⭐ |
| Staged Reward 门控机制 | 乘法门控编码因果顺序,打破"接近但不抓取"的局部极值 | ⭐⭐⭐ |
| YAM MJCF 关键元素 | grasp_site、joint equality、freejoint 决定物理行为 | ⭐⭐ |
| Staged Reward 的 sigma 设置 | σ ≈ mean distance / ln(2),使特征距离处 exp(-d/σ) ≈ 0.5(要 ≈0.3 则用 / ln(3.3)) | ⭐⭐⭐ |
| 关节空间 vs DiffIK vs OSC | 三层抽象选择取决于任务精度/力控需求,不是"先进程度" | ⭐⭐⭐ |
| DLS 伪逆和 lambda 参数 | lambda 太小→奇异点抖动,太大→末端迟钝;典型 0.01 | ⭐⭐ |
| Manipulability 指标 | w = sqrt(det(JJ^T)),可作为 reward 惩罚避开奇异配置 | ⭐⭐ |
| DexSuite 灵巧手工程 | 接触点密度、solver iteration、ADR、PBT | ⭐⭐⭐ |
| 手指接触建模 | 接触几何精度、摩擦参数、求解器参数三者共同决定抓取稳定性 | ⭐⭐⭐ |
| MuJoCo solref/condim/impratio | solref 控制接触刚度,condim 控制摩擦自由度,impratio 控制摩擦稳定性 | ⭐⭐⭐ |
| PhysX solver_iteration | 灵巧手需 16-32 iter,平行夹爪 4-8 iter 够用 | ⭐⭐ |
| 灵巧手 DR 策略 | 比 locomotion 更保守的初始范围 + ADR 逐步扩展 | ⭐⭐⭐ |
| 操作训练诊断 | 阶段性失败模式需要分项检查,不能只看总 reward | ⭐⭐ |
| Reward hacking 检测 | "Reward Up, Task Metric Flat"——独立记录 success rate | ⭐⭐⭐ |
| 双框架配置映射 | 语义等价但语法不同,坐标系和物理引擎是关键差异 | ⭐⭐ |
| 视觉操作管线 | State → Depth → RGB 渐进路线,SpatialSoftmax 优于 Global Pooling | ⭐⭐⭐ |
| SpatialSoftmax 原理 | 输出每个 feature map 的"注意力中心"(x,y),保留空间位置信息 | ⭐⭐⭐ |
| 测量模型视角 | 视觉是有投影/遮挡/光照的测量系统,不是"更大的 obs 向量" | ⭐⭐ |
| 多模态融合 | Late Fusion(特征级拼接)优于 Early Fusion(像素级拼接) | ⭐⭐ |
| 高斯核 vs 其他衰减 | 高斯在零点梯度为零;线性远距离更强;tanh 零点非零 | ⭐⭐ |
| Reward 4维分类 | 信号密度×组合方式×坐标系×时间结构,系统选择设计方案 | ⭐⭐ |
| 操作 obs 信息论 | 33 维状态 = 几十比特有效信息 vs 12288 维 RGB 图像 | ⭐⭐ |
| 视觉 privileged 泄漏检测 | 静态检查 + 遮蔽测试 + 梯度检查三层防线 | ⭐⭐⭐ |
| CNN lr 缩放 | CNN lr 通常为 MLP lr 的 0.1-0.5×,防止视觉特征不稳定 | ⭐⭐ |
| Diffusion Policy 定位 | MLP 的高级替代方案,适合多模态/长序列任务,推理较慢 | ⭐⭐ |
| 从零创建操作任务 | Phase 1 资产→Phase 2 MDP→Phase 3 迭代调试的系统流程 | ⭐⭐⭐ |
回顾本章的主线:我们从 locomotion 的工程直觉出发,系统性地发现了操作任务在 observation(需要感知外部物体)、action(需要精细控制末端)、reward(需要阶段性门控)、termination(有明确的成功判据)、DR(对接触参数更敏感)和 contact modeling(精度要求远高于地面接触)六个维度上的结构性差异。这六个差异不是孤立的——它们共同源于操作任务的一个根本特征:策略必须在一个极其狭窄的几何窗口内精确控制力的大小和方向,才能与外部物体建立稳定的接触关系。
操作任务工程快查表
以下汇总本章的核心工程参数,供日常开发快速参考:
| 参数类别 | 参数名 | 推荐值(平行夹爪) | 推荐值(灵巧手) | 调参方向 |
|---|---|---|---|---|
| Action | action_scale | 0.1-0.15 | 0.05-0.1 | 震荡→减小,探索慢→增大 |
| Reward | σ_reach | 0.1-0.2 | 0.05-0.1 | 信号太弱→增大,饱和→减小 |
| Reward | σ_bring | 0.15-0.25 | 0.1-0.15 | 同上 |
| Reward | action_rate_l2 weight | 0.02-0.05 | 0.05-0.1 | 动作震荡→增大 |
| Contact (MuJoCo) | solref | [0.02, 1.0] | [0.005-0.01, 1.0] | 穿透→减小 solref[0] |
| Contact (MuJoCo) | condim | 3 | 3-6 | 滑动严重→增大到 6 |
| Contact (MuJoCo) | impratio | 1-5 | 5-10 | 物体滑落→增大 |
| Contact (PhysX) | solver_iter | 4-8 | 16-32 | 穿透→增加 |
| Contact (PhysX) | contact_offset | 0.01-0.02 | 0.005-0.01 | "隔空操作"→减小 |
| Training | num_envs | 4096 | 2048-4096 | 内存不足→减半 |
| Training | learning_rate | 3e-4~5e-4 | 1e-4~3e-4 | 不收敛→减小 |
| Training | episode length | 5-10s | 5-15s | 超时多→增大 |
| DR | object mass | ±15% | ±10%(ADR 扩展) | 收敛不了→缩小范围 |
| DR | object friction | ±20% | ±10%(ADR 扩展) | sim-to-real gap→扩大 |
| Vision | resolution | 32×32~64×64 | 32×32 | 收敛慢→降分辨率 |
| Vision | CNN lr ratio | 0.1-0.5× MLP lr | 0.1-0.5× MLP lr | CNN 权重不更新→增大 |
本章建立的四个核心心智模型
学完本章后,你应该在脑中建立了四个可复用的心智模型:
模型一:阶段因果结构 → 乘法门控 reward。 当你面对任何可以分解为"先做 A 才能做 B"的任务(不仅限于操作),乘法门控是编码这种因果顺序的通用工具。加法组合没有优先级,乘法门控有——前序阶段为零时后续阶段的信号被自动屏蔽。
模型二:接触二元性 → 保守的超参数。 当任务涉及接触切换(接触/不接触的离散事件),reward 景观天然更尖锐,PPO 的超参数(lr、clip、entropy)需要比连续控制任务更保守。这不仅适用于操作——locomotion 中跳跃任务(空中→着地的接触切换)同样受此影响。
模型三:三层抽象 → 三种动作空间。 关节层(JointPosition)、运动学层(DiffIK)、动力学层(OSC)分别适用于不同精度和力控需求的任务。这个选型框架在面对新操作任务时可以直接套用——根据任务是否需要精确轨迹或力控制来选择。
模型四:State → Depth → RGB 的渐进验证。 这个管线不仅适用于操作视觉——任何涉及视觉输入的 RL 任务都应该先验证 state 版 MDP 的正确性,再引入视觉复杂度。在 locomotion 的视觉地形感知(Ch18)中也遵循完全相同的管线。
本章聚焦于固定基座的操作。下一章(Ch18)将引入视觉地形感知——当机器人需要在复杂地形上移动时,如何融合视觉与本体感知?这个问题与本章的视觉操作有一个深刻的相似之处:两者都需要策略从高维感知输入中提取与控制直接相关的空间信息。在操作中,这是"方块在哪里";在地形感知中,这是"脚下的地形是什么样"。SpatialSoftmax、CNN 特征提取、visual DR——这些技术在两个领域中的工程实现惊人地相似。
累积项目:本章新增模块
累积项目 D 在本章增加"固定基座操作"模块。你应该能够:
- 在 mjlab 中跑通 YAM lift cube 四种变体的完整 smoke test(list-envs → zero play → random play → short train),并把结果写入实验日志
- 解读 staged reward 的分项指标(reach、bring),诊断策略是否卡在某个阶段
- 在 Isaac Lab 中运行 DexSuite 的至少一个灵巧手任务(如 Kuka+Allegro reorientation),对比与 mjlab 的训练速度
- 把 YAM lift cube 从 state 版切换到 depth 版,确认 CNN 梯度非零,遮蔽测试验证无 privileged 泄漏
- 使用 DiffIK 动作空间训练一个 reaching 策略,对比与 JointPosition 的收敛速度
完成标准:
| 验收项 | 预期结果 | 验证方法 |
|---|---|---|
| YAM smoke test | 全部 4 步通过无报错 | 截图 + 日志 |
| State 版训练 | bringing_reward > 0.3 (2000 iter) | TensorBoard |
| Depth 版训练 | reaching_reward > state 版的 60% | TensorBoard |
| 遮蔽测试 | 遮蔽后 success rate ≈ 0 | 评估脚本 |
| DiffIK reaching | EE-to-target < 3 cm | TensorBoard |
推荐的工程执行顺序(2 周):
Week 1: State 版全流程
Day 1: 配置 mjlab 环境 → list-envs → zero play → random play
Day 2-3: 训练 state 版 lift cube(3000 iter)
调参 sigma_reach / sigma_bring / action_scale
Day 4: 切换到 DiffIK 动作空间(§17.8)
对比 JointPosition 和 DiffIK 的 reaching 精度
Day 5: 在 Isaac Lab 中运行 DexSuite 的一个任务
记录训练吞吐和收敛速度
Week 2: 视觉版 + 诊断
Day 1: 配置 depth 版 obs group(移除 privileged terms)
Day 2-3: 训练 depth 版(5000 iter,512 envs)
监控 CNN 梯度和 visual_feature_std
Day 4: 遮蔽测试验证 + 梯度检查
Day 5: 撰写实验报告,对比 state vs depth 的性能差异
常见的 Week 1 困难及解决方案:
| 困难 | 典型原因 | 快速解决 |
|---|---|---|
| zero play 物体穿透桌面 | solref 参数不对 | 减小 solref[0] 到 0.01 |
| 训练 500 iter 后 reaching 仍为零 | ee_to_cube 未进入 actor obs | 打印 actor obs 维度确认 |
| bringing 始终为零 | 夹爪 action 映射错误 | 检查 action 第 7 维是否控制夹爪 |
| DiffIK 在某些位置抖动 | 接近奇异配置 | 增大 lambda_dls 到 0.05 |
| Isaac Lab DexSuite 无法启动 | 缺少依赖或版本不对 | 按 Isaac Lab 文档安装匹配版本 |
常见的 Week 2 困难及解决方案:
| 困难 | 典型原因 | 快速解决 |
|---|---|---|
| depth 版完全不学 | camera obs 未加入 actor group | 检查 obs_groups 配置 |
| CNN 梯度为零 | obs_groups 名称大小写不匹配 | 对比 env_cfg 和 rl_cfg 的名称 |
| 遮蔽测试通过(不该通过) | actor obs 中有 ee_to_cube 等 | 移除所有物体状态 term |
| 渲染速度极慢 | num_envs 太多(>1024) | 减到 256-512 |
| depth 图全黑 | 相机朝向错误 | 保存一帧可视化检查 |
完成标准(细化验收):
| 验收项 | 预期结果 | 验证命令 |
|---|---|---|
| YAM State 版 2000 iter | reaching reward 上升、bringing reward > 0.3 | tensorboard 查看 reach/bring 分项 |
| YAM Depth 版 2000 iter | reaching reward 约 state 版 60% 量级 | 确认 CNN 分支权重有更新(非全零) |
| Zero play 物理验证 | 方块自由落体正常,无穿透/弹飞 | viser 可视化检查 |
| Staged reward 分析 | 能画出 reach、bring、total 三条曲线的时序图 | 自定义 tensorboard 分项记录 |
| 配置映射报告 | 完成 mjlab→Isaac Lab 的 5 项核心配置对照 | 文档形式提交 |
项目扩展方向(可选):(1) 实现一个 multi-cube 版本,同时抬起两个方块到不同目标——这需要修改 observation(增加第二个方块的状态)、reward(两个方块的 staged reward 如何组合?乘法还是加法?)和 termination(两个方块都到达才算成功?还是任意一个?)。(2) 尝试把 YAM 的 reward function 迁移到 Franka,记录需要调整的 \(\sigma\) 参数和收敛速度差异。(3) 实现帧堆叠和帧差分两种视觉输入方案,比较 depth 版的训练效果。
项目代码保存在独立目录。请在实验日志中标注"累积项目 D:Ch17 新增固定基座操作模块"。
累积项目进度总览
| 章节 | 新增模块 | 累积能力 |
|---|---|---|
| Ch04-07 | RL 工程基础 | 理解 MDP、PPO、训练管线 |
| Ch08-09 | DR + Teacher-Student | 鲁棒性工程 + 蒸馏管线 |
| Ch13 | 四足速度跟踪 | locomotion 完整闭环 |
| Ch14 | 人形 locomotion | 高 DOF locomotion |
| Ch15-16 | Motion Imitation + 多模态动作 | 动作跟踪与生成 |
| Ch17 | 固定基座操作 | 操作 MDP + staged reward + 双框架对照 |
附:从零创建一个新操作任务的检查清单
当你需要为一个新的操作场景(如用 YAM 拧瓶盖、用灵巧手折纸)创建任务时,以下检查清单提供了每一步需要做什么和可能出错的地方:
Phase 1:资产准备(耗时最长但最容易被低估的阶段)
| 步骤 | 具体行动 | 出错信号 |
|---|---|---|
| 1a. 获取物体模型 | 从 CAD 或 3D 扫描获取 mesh,转换为 MJCF(mjlab)或 USD(Isaac Lab) | mesh 导入后物体尺寸不对(单位错误:mm vs m) |
| 1b. 设置碰撞几何 | 用简单凸包近似碰撞体,不要直接用高精度 mesh | zero play 时物体穿透或 sim 极慢 |
| 1c. 标定物理属性 | 设置质量、惯性矩、摩擦系数 | 质量太小物体被弹飞,摩擦太低物体从手中滑落 |
| 1d. 验证物理行为 | 物体自由落体到桌面,检查弹跳和静止 | 无限弹跳(restitution 太高)或穿透(solver 不足) |
Phase 2:MDP 设计(决定训练成败的核心阶段)
| 步骤 | 具体行动 | 出错信号 |
|---|---|---|
| 2a. 定义 observation | 列出策略需要知道的全部信息,计算总维度 | 维度不匹配导致网络创建失败 |
| 2b. 定义 action | 选择关节空间或 DiffIK,设置 scale | scale 太大→末端震荡,太小→探索慢 |
| 2c. 设计 staged reward | 分解任务阶段,每阶段一个密集信号,乘法门控 | 策略卡在某阶段→检查门控逻辑 |
| 2d. 设定 termination | 成功条件 + 超时 + 异常检测 | 无成功终止→episode 总是超时,浪费训练样本 |
| 2e. 配置 events | 物体位姿随机化 + DR | 随机化范围过大→部分 env 不可达 |
Phase 3:迭代调试(本章所有诊断技术的综合应用)
| 步骤 | 具体行动 | 出错信号 |
|---|---|---|
| 3a. Zero play | 确认物理正常 | 不正常则回 Phase 1 |
| 3b. Random play | 确认 action scale 和 observation 正常 | NaN 或异常值则检查 MDP 定义 |
| 3c. 短训练(500 iter) | 确认 reaching reward 上升 | 不上升→检查 obs 和 sigma |
| 3d. 中训练(2000 iter) | 观察阶段性收敛 | 卡阶段→检查门控逻辑和接触参数 |
| 3e. Play 验证 | 确认策略行为符合预期 | Reward hacking→增加约束 reward |
这个流程看起来繁琐,但每一步都有明确的"出错信号"和回退点。跳过 Phase 1 直接写 reward 是操作任务开发中最常见的错误——物理资产的问题会以各种隐蔽方式影响训练,而你可能误以为是 reward 或超参数的问题。
延伸阅读
| 资料 | 难度 | 推荐原因 |
|---|---|---|
| DexPBT: Scaling up Dexterous Manipulation for Hand-Arm Systems with PBT (RSS'23) | ⭐⭐⭐ | PBT 在灵巧手操作中的完整方法论 |
| Zhu et al. 2020, "robosuite: A Modular Simulation Framework and Benchmark for Robot Learning" | ⭐⭐ | 操作任务的标准 benchmark 设计 |
| Chi et al. 2025, "Diffusion Policy: Visuomotor Policy Learning via Action Diffusion" (IJRR) | ⭐⭐⭐ | 扩散策略在操作中的应用,与 MLP 策略的对比 |
| Handa et al. 2023, "DeXtreme: Transfer of Agile In-Hand Manipulation from Simulation to Reality" | ⭐⭐⭐⭐ | 灵巧手 sim-to-real 的完整工程链,ADR 的实际应用 |
| He et al. 2025, "VIRAL: Visual Sim-to-Real at Scale for Humanoid Loco-Manipulation" | ⭐⭐⭐⭐ | 视觉操作 sim-to-real 最大规模工作(64 GPUs,54/59 连续循环) |
| Sferrazza et al. 2024, "HumanoidBench: Simulated Humanoid Benchmark for Whole-Body Locomotion and Manipulation" (RSS'24) | ⭐⭐⭐ | 27 个全身操作 benchmark 任务,层级 RL vs 端到端 RL 的对比 |
| Wei et al. 2022, "CoACD: Collision-Aware Convex Decomposition" (SIGGRAPH) | ⭐⭐ | 操作物体碰撞几何简化的最佳实践 |
| Khatib 1987, "A Unified Approach for Motion and Force Control of Robot Manipulators" | ⭐⭐⭐ | OSC 的原始论文——理解操作空间控制的理论基础 |
| Makoviychuk et al. 2021, "Isaac Gym: High Performance GPU-Based Physics Simulation for Robot Learning" | ⭐⭐ | 理解 Isaac Lab DexSuite 的物理后端基础 |
| MuJoCo Modeling Documentation — Constraint Model | ⭐ | solref/solimp 的完整参数解释和调参指南 |
| mjlab manipulation tasks 源码 | ⭐⭐ | src/mjlab/tasks/manipulation/ 完整代码阅读 |
| Isaac Lab DexSuite 文档 + PR #3378 / #3399 | ⭐⭐ | DexSuite 任务配置和 PBT 集成的工程实现 |
阅读顺序建议:先读 robosuite(理解操作任务的标准 MDP 设计传统),再读 DexPBT(理解灵巧手的工程挑战和 PBT 解决方案),然后按需选读 Diffusion Policy(如果计划用扩散策略做操作)或 DeXtreme/VIRAL(如果计划做 sim-to-real)。mjlab 源码和 Isaac Lab 文档作为持续参考。对需要理解 OSC 的读者,Khatib 1987 是必读——它只有 10 页,但定义了整个操作空间控制领域。
DexPBT 论文精读建议:重点关注 Section 3(PBT 在灵巧手中的工程实现细节)和 Section 4.2(ablation study——哪些 PBT 组件对最终性能贡献最大)。论文的 supplementary material 包含完整的超参数范围和 ADR 配置,可以作为自己实现 PBT 时的参考。DexPBT 的代码已集成到 IsaacGymEnvs,两个基础环境:AllegroKukaLSTM(单臂)和 AllegroKukaTwoArmsLSTM(双臂),任务包括 reorientation, regrasping, grasp-and-throw。
Diffusion Policy 论文精读建议:重点关注 Section 4(action diffusion 的工程实现——如何在 rollout 中高效推理 diffusion model),以及与 MLP/GMM 策略的对比实验。Diffusion Policy 在多模态动作分布的任务(如装配、推拉门)上有显著优势,但在单模态任务(如 lift cube)上与 MLP 策略差异不大——这说明方法选择应基于任务特性而非"新颖程度"。
VIRAL 论文精读建议:重点关注视觉 sim-to-real 的工程管线——delta-action space、mixed DAgger + BC、大规模 tiled rendering、visual DR 参数设置。虽然 VIRAL 针对的是 humanoid loco-manipulation,但其视觉管线工程对任何操作任务的 sim-to-real 都有直接参考价值。
从论文到工程的桥梁:上述论文描述的方法论都可以在 mjlab 或 Isaac Lab 中实现。但论文通常省略了大量工程细节(如 reward 的具体 \(\sigma\) 值、DR 的具体范围、PBT 的 exploit/explore 频率)。这些细节对复现至关重要。建议在读论文的同时,对照框架的源码——论文中一句"we use staged reward"在源码中对应数十行的具体实现。
故障排查手册
| 症状 | 可能原因 | 排查步骤 | 相关章节 |
|---|---|---|---|
| Reaching reward 不上升 | ee_to_cube 未进入 actor obs / sigma 设置不当 | 1. 打印 actor obs 维度 2. 检查 sigma_reach 值 3. 确认 grasp_site 位置 | 17.2 |
| Bringing reward 始终为零 | 夹爪 action 无效 / 物体太重 / 摩擦太低 | 1. 检查 action 第 7 维映射 2. zero agent 测试方块抬起可行性 3. 调整质量/摩擦 | 17.2, 17.4 |
| 夹爪持续震荡 | action_rate penalty 不足 / action scale 过大 | 1. 增加 action_rate_l2 权重 2. 减小 action scale 3. 可视化 action 时序 | 17.7 |
| 方块被弹飞 | 质量太小 / 接触刚度过高 / 物理 timestep 太大 | 1. 检查方块质量 2. 调整 solref 参数 3. 减小 timestep | 17.4 |
| 视觉版策略不使用图像 | actor obs 中含 privileged state | 1. 列出 actor obs group 的所有 term 2. 移除 ee_to_cube 等 3. 重新训练 | 17.7, 17.10 |
| DiffIK 在特定姿态抖动 | 接近奇异配置 | 1. 记录抖动时的关节角 2. 检查 Jacobian 条件数 3. 增大 lambda_dls | 17.8 |
| PhysX 手指穿透物体 | solver iteration 不足 | 1. 增加 solver_position_iteration_count 到 16-32 2. 减小 timestep | 17.3 |
| Smoke train 报 obs shape 错误 | camera group 配置不匹配 | 1. 检查 env cfg 的 obs_groups 2. 检查 rl_cfg 的 obs_groups 3. 确认 camera_name | 17.6 |
| 多 env 下 object pose 计算错误 | 未减去 env origin | 1. 单 env 测试是否正确 2. 检查 cube_pos 是否减了 env_origins 3. 打印多 env 的 cube_to_goal 分布 | 17.7 |
| 策略学会推而不是抓 | reward 不区分推和抓 | 1. play 可视化确认行为 2. 增加 grasping reward 3. 把目标高度设为桌面以上 | 17.7 |
| 部分 env 的 return 持续为零 | 物体初始位置超出工作空间 | 1. 统计各 env 的 return 分布 2. 检查 object pose 随机化范围 3. 增加可达性检查 | 17.7 |
| Value loss 爆炸 | 训练初期 return 全为零 | 1. 增加小的 alive reward 2. 减小 value lr 3. value warm-up | 17.7 |
| Depth 版收敛极慢 | 图像归一化错误 / CNN lr 不合适 | 1. 可视化一帧 depth 确认值域 2. 检查归一化参数 3. 降低 CNN lr | 17.10 |
| 训练中出现 NaN | 物体初始嵌入桌面 / action scale 过大 | 1. 检查 reset 时 object z 坐标下限 2. 减小 action scale 3. 增加 gradient clipping | 17.7 |
| 夹爪 action 恒为零 | action 第 7 维的 scale/offset 配置错误 | 1. 打印 raw action 的第 7 维统计量 2. 检查 joint 到 action 的 index 映射 3. 确认 equality 约束是否生效 | 17.2 |
| Isaac Lab 训练吞吐远低于预期 | solver iteration 过高 / 环境数不足 | 1. profiler 检查物理仿真 vs 渲染 vs 网络的时间占比 2. 减少不必要的 solver iteration 3. 增加 num_envs | 17.3, 17.4 |
| PBT exploit 后性能骤降 | optimizer 状态未重置 / lr 不匹配 | 1. exploit 后重置 optimizer 2. 使用 warm-up lr 3. 减小 explore 的扰动幅度 | 17.3 |
| ADR 范围扩展过快 | 成功率阈值太低 / 评估 episode 数不足 | 1. 提高成功率阈值(如 80%→90%) 2. 增加评估 episode 数 3. 增加 ADR 评估间隔 | 17.3 |
| 遮蔽测试通过(视觉策略不该通过) | privileged 信息泄漏到 actor obs | 1. 逐项核查 actor obs group 2. 移除所有 object/goal state term 3. 重新训练 | 17.10 |
| OSC 在接触时力跳变 | impedance_mode 配置不当 | 1. 检查 motion_stiffness_task 是否过高 2. 使用 variable impedance 让策略自适应 3. 降低 contact_wrench_stiffness_task | 17.8 |
| σ_reach 调了很多次仍不收敛 | 工作空间尺度估计错误 | 1. 打印 ee_to_cube 的初始分布 2. 设 σ ≈ mean(||ee_to_cube||) / ln(2) 3. 验证 reward 在特征距离处 exp(-d/σ) ≈ 0.5(用 / ln(3.3) 则 ≈ 0.3) | 17.2 |
| 跨框架训练结果不一致 | 物理引擎行为差异 | 1. 对比两框架的 zero agent 物理行为 2. 检查关节顺序是否一致 3. 重新调参而非照搬 | 17.9 |
| 碰撞几何不匹配视觉几何 | MJCF 碰撞体和视觉体分离 | 1. viser 切换到碰撞几何可视化 2. 确认碰撞 box 包围视觉 mesh 3. 用 CoACD 生成更精确的碰撞体 | 17.2, 17.4 |
| 训练初期 NaN 崩溃 | 物体穿透/DiffIK 奇异/reward 除零 | 1. 检查 reset 时物体 z 坐标 2. 增大 DLS lambda 3. 添加 epsilon 保护 | 17.7 |
写在最后:本章从 locomotion 出发,系统性地建立了操作任务的工程直觉。操作的核心困难——狭窄的几何窗口、接触的二元性、阶段性的 reward landscape——决定了它与 locomotion 共享框架但不共享设计哲学。掌握了这个哲学差异后,你在面对任何新的操作场景时都有了一个起点:先分析任务的阶段因果结构,再设计对应的 staged reward,然后选择合适的动作抽象层次,最后用系统性的诊断流程迭代调优。这套方法论将在后续章节中继续发挥作用——Ch19 的移动操作需要同时处理 locomotion 和 manipulation 的 MDP 设计挑战,而本章建立的操作工程直觉是那个融合的基础。
本章与全书其他章节的联系:
向后回顾:Ch07 的 PPO 诊断三层模型在本章扩展为操作特有的"物理层→接触层→MDP 层→优化层"四层诊断。Ch05 的 observation 设计原则在本章扩展为"privileged vs non-privileged"的操作版——物体状态是 critic 的 privileged 信息,actor 只能通过视觉获取。Ch06 的 reward shaping 在本章发展为 staged reward 的乘法门控——这是 reward 设计从"单一连续信号"到"阶段因果结构"的质变。
向前预告:Ch18(视觉地形感知)将从另一个角度展开视觉与控制的融合——当机器人的"眼睛"不是看物体而是看地形时,CNN 编码器、visual DR 和 teacher-student 蒸馏的工程实现有哪些相似之处和关键差异?本章建立的 SpatialSoftmax、visual DR、privileged 泄漏检测三大视觉工程技术在 Ch18 中将被直接复用。Ch19(四足+臂 loco-manipulation)将把本章的 staged reward 扩展为 navigation→reaching→grasping 的多阶段融合,本章的 DiffIK 动作空间将成为 VBC 分层架构中低层策略的核心组件。Ch20(人形全身控制)将把操作能力从固定基座扩展到不稳定的双足平台——本章建立的所有操作工程技术都将面对"上肢操作 vs 下肢平衡"的新约束。
一个总结性的工程经验法则:操作任务的 80% 工作量不在"训练策略"上,而在"确保物理资产和 MDP 正确"上。一个物理正确的 lift cube 环境,用默认的 PPO 超参数和最简单的 staged reward 就能在 2000 iteration 内收敛。一个物理有问题的环境(碰撞不准确、摩擦太低、solref 不对),无论你怎么调 reward 和超参数都不会工作。在开始任何操作任务训练之前,完成以下"物理健康检查":
text □ Zero play:物体自由落到桌面,弹跳 1-2 次后稳定静止(不穿透、不无限弹跳) □ 关节:打印所有关节名称和范围,确认与 MJCF/USD 一致 □ 夹爪:手动发送闭合命令,确认两个手指同时闭合(equality 约束生效) □ 接触:夹爪闭合时物体不穿透、不被弹飞、不从手中滑落 □ Obs:打印 actor obs 维度和各 term,确认无 privileged 泄漏 □ Reward:random agent 下打印各 reward 项的均值,确认值域合理 □ Action:打印 action 的关节映射顺序,确认与 action_scale 一致这 7 项检查能在训练开始前排除 90% 的操作任务配置错误。做了这些检查后再开始训练——这是本章最重要的工程建议。