Ch20 · 人形全身控制:上肢操作 + 下肢平衡
定位:Part V 第二章。将 Ch14(人形基础 locomotion)和 Ch19(loco-manipulation 方法论)扩展到人形机器人——当双足行走的机器人需要同时用上肢操作时,平衡约束如何与操作目标融合? 关键文献:ExBody/ExBody2 — Expressive Whole-Body Control(RSS'24 / RSS'25 Workshop Spotlight);WoCoCo — Whole-Body Humanoid Control with Sequential Contacts(CoRL'24);KungfuBot(NeurIPS'25);HumanoidBench(RSS'24);HoST — Humanoid Standing-up Control(RSS'25 Best Systems Paper Finalist) 关键框架:mjlab + Isaac Lab 双框架 · HOVER Isaac Lab 扩展模板 · RSL-RL 机器人:Unitree G1(23-43 DOF)· Unitree H1(19 DOF)· Booster T1 累积项目:F(人形全身控制模块——从上下肢解耦到接触阶段分解的工程管线) 前置要求:Ch14(人形 Locomotion)、Ch15(动作模仿)、Ch19(loco-manipulation 方法论) 本章定位:人形全身控制的"方法论入口"。Ch14 教了人形怎么走,Ch15 教了怎么模仿动作,Ch19 教了底盘+臂如何协调——本章把这些能力融合到人形平台上:23-43 个自由度同时服务于行走平衡和上肢操作。 阅读时间估计:精读约 7-9 小时(含动手实验),快速浏览约 3 小时。§20.5-20.7 的三个案例精读需要较深的 Ch14+Ch15+Ch19 基础。
前置自测
📋 答不出 \(\ge\) 3 题 → 先回前置章节复习
- [Ch14] G1 人形的 DOF 地图是什么?哪些关节属于下肢(locomotion),哪些属于上肢(manipulation)?
- [Ch14] 双足行走与四足行走在稳定性上的根本区别是什么?ZMP(Zero Moment Point)的物理含义是什么?
- [Ch15] 动作模仿的 tracking reward 使用什么距离度量?关节角度 tracking 和 keypoint tracking 各有什么优缺点?
- [Ch19] 四阶段课程(arm-constrained curriculum)的每个阶段解决什么问题?hot-start 技巧如何加速训练?
- [Ch19] 多目标 reward 的乘法门控编码了什么因果关系?什么时候该用乘法门控、什么时候该用加权组合?
- [RL 基础] Teacher-student 蒸馏中,DAgger 与 offline BC 的核心区别是什么?为什么人形全身控制通常需要 DAgger?
- [物理] 当人形机器人双脚站立、单手举起 2 kg 物体时,CoM 偏移对 ZMP 有什么影响?底盘(下肢)需要如何补偿?
本章目标
学完本章后,你应该能够:
- 分析人形全身操作的核心挑战——DOF 爆炸(29+ 关节)、上下肢动力学耦合、接触丰富的长时程任务
- 对比三条技术路线——上下肢解耦(ExBody)、接触阶段分解(WoCoCo)、mask-conditioned 多模态(HOVER)的工程优劣
- 精读 ExBody2 的 velocity-landmark 解耦 tracking、teacher-data 自动过滤和 specialist fine-tuning
- 精读 WoCoCo 的 contact stage 定义、stage-aware reward 和 three-stage DR 课程
- 精读 KungfuBot 的 physical motion filter 和 bi-level adaptive tracking
- 理解 HumanoidBench 的 27-task 评估体系,知道分层 RL 在哪些任务上优于端到端 RL
- 集成跌倒恢复模块(HoST)到 locomotion + manipulation 策略切换管线
- 配置 Isaac Lab G1 LocoManip 环境和 HOVER 扩展模板
本章知识全景图
人形全身控制知识树
├── 【本章核心】上肢操作 + 下肢平衡
│ ├── 核心挑战(§20.1)
│ │ ├── DOF 爆炸(29+ 关节 + 灵巧手)
│ │ ├── 上下肢动力学耦合(角动量守恒)
│ │ └── 接触序列的长时程探索
│ ├── 三条技术路线(§20.2)
│ │ ├── 上下肢解耦(ExBody/ExBody2)
│ │ ├── 接触阶段分解(WoCoCo)
│ │ └── mask-conditioned 多模态(HOVER)
│ ├── 评估体系(§20.4)
│ │ └── HumanoidBench 27-task benchmark
│ ├── 精读案例
│ │ ├── WoCoCo(CoRL'24,§20.5)
│ │ ├── ExBody/ExBody2(RSS'24,§20.6)
│ │ └── KungfuBot(NeurIPS'25,§20.7)
│ └── 跌倒恢复集成(§20.8)
│ └── HoST(RSS'25 Best Systems)
├── 【回顾桥】Ch14 人形 locomotion · Ch15 动作模仿 · Ch19 loco-manipulation
├── 【前瞻】Ch21 轮式+双臂 · Ch22 DIY 自定义机器人
└── 平台参考
├── Unitree G1(23-43 DOF)
├── Unitree H1(19 DOF)
└── Booster T1(23 DOF)
20.1 人形全身操作的核心挑战 ⭐⭐
这一节解决什么问题:建立"人形全身控制远比四足 loco-manipulation 难"的认知。Ch19 的四足+臂已经很有挑战了,人形为什么更难?多难?
从四足 Loco-Manipulation 到人形全身控制
Ch19 的四足+臂(如 Go2-Arx)有 19 个自由度——12 个腿关节 + 6 个臂关节 + 1 个夹爪。人形全身控制(如 G1)本体就有 23 个自由度——12 个下肢关节 + 10 个上肢关节 + 1 个腰部关节,再加灵巧手(每只 6-7 DOF)可达 35-43 DOF。这不仅仅是"更多关节"——而是质的变化。
用一个日常类比:Ch19 的四足+臂就像一个人趴在滑板上伸手拿东西——四个轮子(四条腿)提供了极好的稳定性,即使手臂使劲伸出去也不太会翻倒。人形全身控制就像一个人站在平衡板上搬箱子——两条腿(双足)提供的稳定性远不如四条腿,上半身的任何动作都会剧烈影响平衡。你必须同时"控制平衡"和"搬箱子",两者不能分开。
挑战一:DOF 爆炸与探索空间
| 平台 | 总 DOF | 动作空间维度 | 探索空间(粗略) | 典型收敛时间 |
|---|---|---|---|---|
| Go1 locomotion (Ch13) | 12 | 12 | \(10^{12}\) | 2-8 h |
| Go2-Arx loco-manip (Ch19) | 19 | 19 | \(10^{19}\) | 10-20 h |
| G1 全身 (无手) | 23 | 23 | \(10^{23}\) | 20-50 h |
| G1 全身 + 灵巧手 | 35-43 | 35-43 | \(10^{43}\) | 50-200 h |
| H1 + Shadow Hand | 101 | 61 (position) | \(10^{61}\) | 极难收敛 |
PPO 在高维动作空间中的探索效率随维度指数下降。从 12D(四足 locomotion)到 19D(四足 loco-manipulation)的增长已经让训练时间翻倍;从 19D 到 23D(人形全身)的增长更加剧了探索困难——策略需要在一个巨大的空间中找到"同时保持平衡且完成操作"的极小区域。
这就是为什么端到端训练在人形全身控制中比在四足 loco-manipulation 中更难成功——四足有四个支撑点提供的"安全网",策略即使乱动也不容易摔倒;人形只有两个支撑点,随机动作几乎 100% 导致摔倒。HumanoidBench 的实验数据证实了这一点:标准 PPO 在大部分全身操作任务上"still unsolved"。
挑战二:上下肢动力学耦合
人形的上下肢耦合比四足+臂更严重——原因是双足的支撑多边形极小。
定量对比:
| 维度 | Go2(四足) | G1(人形) | 差异 |
|---|---|---|---|
| 支撑面积 | ~800 cm²(四脚矩形) | ~200 cm²(两脚矩形) | 4\(\times\) 更小 |
| CoM 到支撑边缘 | ~20 cm | ~5-8 cm | 3\(\times\) 更小余量 |
| 单臂举物 2kg 的 CoM 偏移 | ~4 cm(占余量 20%) | ~2.7 cm(占余量 ~27%,单腿支撑相可达 50%+) | 人形余量紧张 |
| 角动量传递的敏感度 | 低(4 个支撑点抑制旋转) | 高(2 个支撑点无法有效抑制) | 质变 |
当 G1 单手举起 2 kg 物体(臂长 ~0.5 m)时,CoM 在侧向的偏移约为 \(2 \times 0.5 / (35 + 2) \approx 2.7\) cm。G1 站立时双脚之间的宽度约 20 cm,CoM 从中心到侧边缘约 10 cm——2.7 cm 的偏移占余量的 27%。如果同时在走路(单腿支撑 phase),支撑面积减半,CoM 余量进一步收缩——此时 2.7 cm 的偏移可能占余量的 50% 以上。
这意味着人形的任何上肢动作都必须被下肢立即补偿。如果上肢快速挥动(如 KungfuBot 的功夫动作),产生的角动量必须被下肢通过步态调整来消除——否则机器人会像陀螺一样旋转。这种"上肢动→下肢必须跟着补偿"的耦合是人形全身控制的核心工程挑战。
本质洞察:四足的 loco-manipulation 中,底盘提供了一个"稳定的移动平台"——臂的动作虽然影响 CoM,但四条腿的大支撑面积提供了足够的缓冲。人形全身控制中没有这个"稳定平台"——下肢本身就是一个需要主动控制的不稳定系统,上肢的每一个动作都在"干扰"这个不稳定系统。这就是为什么人形全身控制需要更强的上下肢协调——不是可选的优化,而是不协调就摔倒的硬约束。
用一个更精确的日常类比来理解这个差异:四足+臂就像在宽大的办公桌上做手工——桌面(四条腿的支撑面积)足够大、足够稳,你可以专注于手上的工作,不用担心桌子翻倒。人形全身控制就像在独木桥上做手工——桥面(双脚的支撑面积)极窄,每一个手臂动作都可能让你失去平衡。你必须时刻"分心"来维持平衡——你的"注意力"(策略的容量)被迫同时服务于两个目标。这就是为什么人形全身控制的 MLP 通常需要比四足更大的网络(如 [512, 256, 128] vs [256, 256, 128])——更多的参数来同时编码"如何平衡"和"如何操作"。
挑战三:接触丰富的长时程任务
人形的全身操作任务通常涉及复杂的接触序列——哪些身体部位在什么时候与什么物体/环境接触。例如:
| 任务 | 接触序列 | 接触体 |
|---|---|---|
| 搬箱子 | 双脚站立 → 弯腰 → 双手接触箱子 → 抬起 → 行走 | 脚、手、箱子 |
| 开门 | 双脚站立 → 单手接触门把 → 转动 → 推开 → 走过 | 脚、手、门把、门 |
| 跑酷跳跃 | 双脚 → 单脚蹬地 → 腾空 → 双手抓壁 → 翻上 | 脚、手、墙壁 |
| 攀岩 | 交替手脚接触岩壁,4 点支撑 → 3 点 → 移动 → 4 点 | 手、脚、岩壁 |
每一次接触模式的变化(如从双脚站立变为单脚)都改变了系统的约束条件——可行的动作空间在每次切换时突变。传统 RL 在处理这种"接触模式切换"时非常困难——因为策略需要在不同的接触模式之间"探索",而不正确的接触模式切换几乎 100% 导致摔倒。
WoCoCo 的核心贡献就是解决这个问题——通过将长时程任务分解为一系列"接触阶段"(contact stage),每个阶段内的接触模式固定,策略只需要在阶段内探索,阶段之间的切换由人工指定的接触计划驱动。
⚠️ 常见陷阱
⚠️ 编程陷阱:把 Ch19 的四足 loco-manipulation 配置直接移植到人形
四足的 action scale(如腿关节 \(\pm\)0.25 rad)在人形上可能不适用——人形的髋关节和膝关节需要更大的活动范围(行走步幅更大),但踝关节需要更小的活动范围(平衡更敏感)。不能用一个 scale 覆盖所有下肢关节。
💡 概念误区:认为"人形就是两条腿的四足"
许多慢速四足步态(如 crawl/walk)是静态稳定的——任何时刻至少 3 脚着地,CoM 始终在支撑多边形内(注意四足的高速步态如 trot/bound/pace 同样依赖动态稳定,并非任何时刻都有 3 脚着地)。人形的行走则普遍是动态稳定的——在每一步的 swing phase 只有 1 脚着地,CoM 可能暂时超出支撑面积,靠下一步的落脚来恢复;总体上四足比双足拥有更大的支撑冗余。这意味着人形的 locomotion 策略不能简单地追求"CoM 在支撑多边形内"——而需要追求"CoM 的轨迹在一个步周期内的平均是稳定的"。这个动态稳定性的概念在四足中不存在。
🧠 思维陷阱:认为"DOF 多就一定需要分层"
HumanoidBench 的实验确实显示分层 RL 在大部分任务上优于端到端。但 WoCoCo 证明了端到端 RL 在特定条件下(有明确的接触计划 + 合适的 reward 分解)也能工作。分层不是银弹——它引入了层间信息瓶颈,可能限制上下肢的精细协调。选择分层 vs 端到端应基于任务特性,而非 DOF 数量。
练习
- [计算题] G1 的总质量约 35 kg,双臂质量各约 3 kg,臂长约 0.5 m。当双臂同时向前伸出(平举)时,CoM 的前向偏移是多少?如果 G1 站立时 CoM 到前脚趾的距离约 8 cm,双臂平举后 CoM 是否仍在支撑面积内?
- [分析题] 为什么 HumanoidBench 中分层 RL 比端到端 RL 表现好?从探索效率和信用分配(credit assignment)两个角度分析。
- [设计题] 列出 3 个"接触序列简单"(适合端到端训练)和 3 个"接触序列复杂"(需要接触阶段分解或分层)的人形全身操作任务。
20.2 三条技术路线对比 ⭐⭐⭐
这一节解决什么问题:面对人形全身控制的三大挑战,学术界提出了三种截然不同的技术路线。它们各自解决什么问题?工程上各有什么优劣?什么时候选择哪种?
路线一:上下肢解耦(ExBody / ExBody2)
核心思想:把人形机器人"劈成两半"——上半身负责跟踪参考动作(操作/表达),下半身负责保持行走稳定(locomotion)。两部分使用不同的 reward 目标,由一个统一的策略网络同时优化。
ExBody 解耦模式:
上半身 reward = 关键点/关节角度 tracking(参考来自 MoCap)
下半身 reward = velocity tracking(跟踪根节点速度命令)
合并:total_reward = w_upper * r_upper + w_lower * r_lower
ExBody2 的三个关键改进(相比 ExBody):
-
解耦 velocity tracking 与 landmark tracking:ExBody 中 velocity tracking 和 body landmark tracking 耦合在一起——如果参考动作中包含大幅度的根节点移动(如跑步),velocity tracking 会与 landmark tracking 冲突("是跟着跑还是保持姿态?")。ExBody2 将两者完全分离——velocity tracking 独立控制根节点移动,landmark tracking 在局部坐标系中跟踪身体关键点。这消除了 drift-pose 耦合问题。
-
Teacher-data 自动过滤:人类 MoCap 数据中包含大量机器人物理上无法执行的动作(如后空翻、劈叉)。ExBody 使用语言标签手动过滤("dance" → 保留,"backflip" → 删除),但标签不精确。ExBody2 训练一个 privileged teacher,凡是 teacher 执行不了的动作(reward 低于阈值或 early termination)自动过滤——这比人工过滤更可靠。
-
Specialist fine-tuning:ExBody2 先训练一个 generalist policy(在所有 MoCap 数据上),然后对特定动作组(如"舞蹈")做 fine-tuning。Fine-tuning 提高了目标动作组的 tracking 精度,但降低了其他动作组的性能——这是 versatility vs fidelity 的 trade-off。
ExBody2 的工程优势:训练简单、收敛稳定、sim-to-real 可靠。因为下半身 velocity tracking 已经在 Ch14 中充分验证,上半身 tracking 只是在稳定行走基础上的"叠加"——不会破坏行走稳定性。
ExBody2 的工程劣势:上下肢协调性受限。因为解耦了 reward,策略难以学会"上肢动作需要下肢配合"的复杂协调——如搬重物时下肢需要更宽的步幅来补偿 CoM 偏移。
路线二:接触阶段分解(WoCoCo)
核心思想:将长时程任务分解为一系列"接触阶段"——每个阶段有明确的接触模式(哪些身体部位与什么接触),策略在每个阶段内优化一组 stage-aware reward,阶段之间的切换由人工指定的接触计划驱动。
WoCoCo 接触分解模式(以搬箱子为例):
Stage 0: 双脚站立,接近箱子 → nav reward
Stage 1: 双脚站立,双手触碰箱子 → hand-contact reward
Stage 2: 双脚站立,双手抬起箱子 → lift reward
Stage 3: 行走,同时保持箱子不掉 → walk + hold reward
WoCoCo 的三个核心设计:
-
Contact stage 定义:每个 stage 由一组"期望接触"定义——哪些身体部位应该与环境/物体有接触力。这个定义是任务无关的(task-agnostic)——对任何涉及接触序列的任务,只需要指定"什么时候哪里应该有接触"。
-
Stage-aware reward:每个 stage 有自己的 dense reward。WoCoCo 的 reward 由三部分组成:
- Contact reward:鼓励正确的接触模式(如 Stage 1 中鼓励双手接触箱子)
- Stage-count reward:鼓励策略推进到下一个 stage
-
Curiosity reward(可选):鼓励探索新的状态(防止策略卡在某个 stage)
-
Three-stage DR 课程:
- (i) 无 DR,先学会接触序列
- (ii) 开启 DR + 增大 smoothness weight
- (iii) 更严格的 regularization,为 sim-to-real 做准备
WoCoCo 的工程优势:通用性强——同一个 reward 模板可以应用于跑酷、搬箱、舞蹈、攀岩等完全不同的任务。端到端训练,上下肢可以学到精细协调。
WoCoCo 的工程劣势:需要人工指定接触计划——"什么时候哪里应该有接触"需要任务知识。对于接触序列不明确的任务(如自由探索),WoCoCo 无法应用。
路线三:Mask-Conditioned 多模态(HOVER / MaskedManipulator)
核心思想:训练一个 oracle teacher 策略(看到全部 privileged 信息),然后用 DAgger 蒸馏到不同的"command mask"下的 student。不同的 mask 激活不同的控制模态——如头+手追踪(teleoperation 模式)、全身关节追踪(motion imitation 模式)、根节点追踪(navigation 模式)。
HOVER mask-conditioned 模式:
Oracle Teacher: full privileged state → action
Student (mask = HEAD+HANDS): depth + proprio → action(遥操作)
Student (mask = FULL_BODY): joint_ref + proprio → action(模仿)
Student (mask = ROOT_ONLY): root_vel + proprio → action(导航)
HOVER 的 Mask 机制详解:
HOVER 的关键创新是用"command mask"来控制 student 接收什么信息。Training 时 oracle teacher 始终看到完整状态,但 student 的输入被 mask 选择性地遮蔽——被遮蔽的维度设为零或随机噪声。不同的 mask 定义了不同的"控制模态":
# HOVER 的 Mask 定义
class HoverMasks:
"""定义不同控制模态的 command mask。
mask 是一个 binary 向量,1 = student 可以看到,0 = 被遮蔽。
"""
# 遥操作模式:只看到头部和手部的目标位置
MASK_HEAD_HANDS = {
"head_target_pos": True,
"left_hand_target_pos": True,
"right_hand_target_pos": True,
"joint_ref": False, # 关节参考被遮蔽
"root_vel_cmd": False, # 根速度命令被遮蔽
}
# 动作模仿模式:看到全身关节参考
MASK_FULL_BODY = {
"head_target_pos": False,
"left_hand_target_pos": False,
"right_hand_target_pos": False,
"joint_ref": True, # 关节参考可见
"root_vel_cmd": True, # 根速度命令可见
}
# 导航模式:只看到根速度命令
MASK_ROOT_ONLY = {
"head_target_pos": False,
"left_hand_target_pos": False,
"right_hand_target_pos": False,
"joint_ref": False,
"root_vel_cmd": True,
}
# 蒸馏时使用多个 mask 交替训练
distill_mask_modes = [
HoverMasks.MASK_HEAD_HANDS, # 遥操作 specialist
HoverMasks.MASK_FULL_BODY, # 动作模仿 specialist
HoverMasks.MASK_ROOT_ONLY, # 导航 specialist
]
# HOVER generalist 使用所有 mask 的混合:
# DISTILL_MASK_MODES_ALL 在每个 DAgger step 随机选择一个 mask
HOVER 的 DAgger 蒸馏过程:
# HOVER Student 蒸馏的核心循环
for iteration in range(num_student_iterations):
# 1. 随机选择一个 mask mode
mask = random.choice(distill_mask_modes)
# 2. Student rollout(使用 masked obs)
obs_full = env.get_obs() # 完整 obs
obs_masked = apply_mask(obs_full, mask) # 遮蔽后的 obs
student_action = student_policy(obs_masked)
# 3. Oracle 标注(使用完整 obs)
oracle_action = oracle_policy(obs_full)
# 4. 用 student 动作执行(DAgger 核心)
env.step(student_action)
# 5. MSE loss 更新 student
loss = F.mse_loss(student_action, oracle_action)
loss.backward()
student_optimizer.step()
HOVER 的工程优势:一个策略网络服务多种控制模态——不需要为每种任务训练独立的策略。对多任务部署场景(如同一个机器人需要时而导航、时而遥操作、时而模仿动作)非常方便。部署时只需改变输入 mask 就能切换模态,无需加载不同的策略权重。
HOVER 的工程劣势:依赖于高质量的 oracle teacher——teacher 的性能上限就是 student 的上限。蒸馏过程中信息损失不可避免。对于 teacher 无法解决的任务,HOVER 也无法解决。此外,generalist student(在所有 mask 上训练)的每个模态性能通常不如 specialist student(只在一个 mask 上训练)。
HOVER 的训练时间参考(RTX 4090):
| 阶段 | num_envs | Iterations | Wall-clock |
|---|---|---|---|
| Oracle Teacher | 4096 | 50k-80k | 12-23 h |
| Specialist Student | 4096 | ~10k | ~16 min |
| Generalist Student | 4096 | ~20k | ~32 min |
注意 oracle teacher 的训练时间远长于 student——这与 Ch18 的三阶段蒸馏管线一致(Stage 1 teacher 最耗时)。HOVER 的 student 蒸馏极快(16 分钟),因为 DAgger 的监督学习比 RL 的 reward 信号更高效。
三条路线的工程决策树
你的人形全身控制任务是什么类型?
├── 上半身跟踪参考动作 + 下半身行走
│ → ExBody2(最简单、最可靠的 sim-to-real)
│
├── 任务涉及明确的接触序列变化
│ ├── 接触计划可以人工指定
│ │ → WoCoCo(端到端训练,自动上下肢协调)
│ └── 接触计划不明确
│ → 分层 RL(VBC 模式 from Ch19)
│
├── 需要多种控制模态(导航 + 遥操作 + 模仿)
│ → HOVER(一个策略服务多种需求)
│
└── 高动态动作(功夫/舞蹈/体操)
→ KungfuBot(adaptive tracking + physical filter)
⚠️ 常见陷阱
💡 概念误区:认为"三条路线互斥——选了一条就不能用另一条"
实际项目中经常混合使用:用 ExBody2 的 velocity-landmark 解耦 reward 作为基础 + WoCoCo 的 contact stage 分解处理接触丰富的阶段 + HOVER 的 mask-conditioned 蒸馏实现多模态部署。三条路线是工具箱中的不同工具,不是互斥的哲学。
⚠️ 编程陷阱:ExBody2 的 velocity tracking 和 locomotion velocity tracking 混淆
ExBody2 的 velocity tracking 跟踪的是参考动作中的根节点速度(MoCap 数据给出),不是用户命令的速度(Ch14 中的 velocity command)。如果你在 ExBody2 的基础上加入用户速度命令控制,需要修改 velocity tracking 的 source——从 MoCap reference 切换到用户 command。
练习
- [分析题] 对比 ExBody2 和 WoCoCo 在以下任务上的适用性:(a) 人形机器人跳舞(上半身跟踪 MoCap),(b) 人形机器人搬箱子走到指定位置,(c) 人形机器人攀岩。给出每个任务选择哪条路线及原因。
- [设计题] 如果你要设计一个"人形机器人在办公室中巡逻 + 发现异常物体时拾取"的系统,需要结合哪些路线的哪些元素?
- [跨章综合题] Ch19 的 VBC 分层架构和 HOVER 的 mask-conditioned 蒸馏都使用了 teacher-student 范式。两者的 teacher 看到的信息有什么区别?student 学到的能力有什么区别?
20.3 Isaac Lab G1 LocoManip 环境与 HOVER 模板 ⭐⭐
这一节解决什么问题:如何在 Isaac Lab 和 mjlab 中搭建人形全身控制的训练环境?HOVER 的 Isaac Lab 扩展模板提供了什么工程基础设施?
Isaac Lab 的 G1 人形环境
Isaac Lab 2.3 提供了 Unitree G1 的内置环境,包括基础 locomotion 和实验性的 loco-manipulation。G1 的 DOF 配置:
Unitree G1 DOF 地图(基础 23-DOF 配置):
下肢(12 DOF):
左腿:hip_pitch, hip_roll, hip_yaw, knee, ankle_pitch, ankle_roll (6)
右腿:hip_pitch, hip_roll, hip_yaw, knee, ankle_pitch, ankle_roll (6)
上肢(10 DOF):
左臂:shoulder_pitch, shoulder_roll, shoulder_yaw, elbow, wrist_roll (5)
右臂:shoulder_pitch, shoulder_roll, shoulder_yaw, elbow, wrist_roll (5)
躯干(1 DOF):
waist_yaw (1)
总计:23 DOF(不含手指;扩展版加腰部 roll/pitch 与腕部 pitch/yaw 可达 29 DOF)
可选灵巧手(每只 6 DOF,如五指手;官方 Dex3-1 三指手为每只 7 DOF):
thumb, index, middle, ring, pinky, thumb_rotation
含手总计:35 DOF(23 + 2×6)
Isaac Lab 的 G1 全身控制配置示例:
# Isaac Lab: G1 全身控制环境配置
from isaaclab.envs import ManagerBasedRLEnvCfg
class G1WholeBodyEnvCfg(ManagerBasedRLEnvCfg):
scene = SceneCfg(
num_envs=4096,
env_spacing=3.0,
robot=ArticulationCfg(
prim_path="{ENV_REGEX_NS}/Robot",
spawn=UsdFileCfg(usd_path="assets/unitree_g1.usd"),
actuators={
# 下肢——大力矩,粗精度
"legs": ImplicitActuatorCfg(
joint_names_expr=[".*hip.*", ".*knee.*", ".*ankle.*"],
stiffness={
".*hip_roll": 150.0,
".*hip_yaw": 150.0,
".*hip_pitch": 200.0,
".*knee": 200.0,
".*ankle": 40.0, # 踝关节需要精细控制
},
damping=5.0,
),
# 上肢——小力矩,高精度
"arms": ImplicitActuatorCfg(
joint_names_expr=[".*shoulder.*", ".*elbow.*", ".*wrist.*"],
stiffness=80.0,
damping=2.0,
),
# 躯干
"waist": ImplicitActuatorCfg(
joint_names_expr=["waist_yaw"],
stiffness=200.0,
damping=5.0,
),
},
),
)
PD gain 分组的物理原理:人形的不同关节对力矩的需求差异极大。髋关节需要支撑整个上半身的重量(~25 kg \(\times\) 9.81 m/s² = ~245 N 的重力负载),kp 需要 150-200。踝关节在平衡中起关键作用但力矩需求相对小(只需要微调 CoP 位置),kp 设为 40 就够了——太大会导致踝关节"太硬",无法做出平衡所需的精细调整。肩关节的力矩需求取决于是否需要举重物——空手时 kp=80 足够,举 2 kg 物体时可能需要增大到 120。
HOVER 扩展模板
HOVER(NVlabs, 2024)提供了一个完整的 Isaac Lab 扩展模板,可以直接用于人形全身控制的训练和部署。其代码结构:
HOVER 仓库结构:
neural_wbc/
├── core/ # 环境核心逻辑
│ ├── reference_motion.py # 参考动作处理
│ ├── mask.py # Mask 机制核心实现
│ └── modes.py # mask modes(exbody/humanplus/h2o/omnih2o)
├── isaac_lab_wrapper/ # Isaac Lab 适配层
│ ├── neural_wbc_env_cfg_h1.py # H1 环境配置
│ └── neural_wbc_env_cfg_g1.py # G1 环境配置(如有)
├── mujoco_wrapper/ # MuJoCo sim-to-sim 验证
├── hw_wrappers/ # 真机部署适配
│ └── unitree_h1/ # H1 硬件接口
├── data/
│ └── motions/ # AMASS MoCap 数据
│ └── stable_punch.pkl # 示例:稳定的出拳动作
└── inference_env/ # 部署推理环境
scripts/rsl_rl/
├── train_teacher_policy.py # Stage 1: 训练 Oracle Teacher
├── train_student_policy.py # Stage 2: DAgger 蒸馏 Student
├── play.py # 可视化回放
└── eval.py # 评估
使用 HOVER 模板的典型工作流:
# Step 1: 训练 Oracle Teacher(特权信息)
python scripts/rsl_rl/train_teacher_policy.py \
--num_envs 4096 \
--reference_motion_path data/motions/stable_punch.pkl \
--max_iter 80000
# Step 2: DAgger 蒸馏 Student(masked obs)
python scripts/rsl_rl/train_student_policy.py \
--num_envs 4096 \
--teacher_ckpt logs/teacher/best.pt \
--distill_mask_modes MASK_HEAD_HANDS \
--max_iter 10000
# Step 3: 评估
python scripts/rsl_rl/eval.py --ckpt logs/student/best.pt
# Step 4: Sim-to-sim 验证(MuJoCo)
python neural_wbc/mujoco_wrapper/play_mujoco.py --ckpt logs/student/best.pt
训练时间参考(RTX 4090):
| 阶段 | num_envs | Iterations | Wall-clock |
|---|---|---|---|
| Oracle Teacher | 4096 | 50k-80k | 12-23 h |
| Student (DAgger) | 4096 | ~10k | ~16 min |
HOVER 的关键配置参数:
# HOVER 的核心配置(从 custom_config.yaml 提取)
config = {
"sim": {
"dt": 0.02, # 物理仿真步长 50 Hz
},
"ctrl_delay_step_range": [0, 5], # 控制延迟 DR(0-5 步)
"reference_motion_path": "stable_punch.pkl",
"distill_mask_modes": [
"MASK_HEAD_HANDS", # 遥操作模式
"DISTILL_MASK_MODES_ALL", # 通用模式
],
"teacher": {
"num_envs": 4096,
"max_iter": 80000,
},
"student": {
"num_envs": 4096,
"max_iter": 10000,
},
}
mjlab 的人形全身控制环境
mjlab 的人形环境基于 MuJoCo Warp,使用 MJCF 模型。典型的 G1 训练命令:
# mjlab: G1 velocity tracking(Ch14 的基础任务)
uv run train Mjlab-Velocity-Flat-Unitree-G1 \
--env.scene.num-envs 4096
# mjlab: G1 motion tracking(Ch15 的模仿任务)
uv run train Mjlab-Tracking-Flat-Unitree-G1 \
--registry-name your-org/motions/dance-motion \
--env.scene.num-envs 4096
# mjlab: G1 spinkick(高动态全身控制)
uv run train Mjlab-Spinkick-Unitree-G1 \
--env.scene.num-envs 4096
mjlab vs Isaac Lab 的人形全身控制对比:
| 维度 | mjlab (MuJoCo Warp) | Isaac Lab (PhysX/Omniverse) |
|---|---|---|
| 模型格式 | MJCF | USD |
| 接触精度 | 高(软接触、PGS 求解器) | 高(GPU PhysX) |
| 渲染 | MuJoCo Warp Batch Renderer | RTX(如需视觉输入) |
| MoCap 数据管理 | WandB registry | 本地 .pkl / AMASS |
| 推荐场景 | 快速迭代、动作模仿 | 视觉全身、multi-mode 蒸馏 |
| 典型吞吐 | ~50k steps/s (4096 envs) | ~40k steps/s (4096 envs) |
工程建议:先在 mjlab 中验证 reward 设计和 action scale(更快迭代),确认策略在仿真中工作后,如果需要视觉输入或 multi-mode 蒸馏(HOVER 模式),再迁移到 Isaac Lab。
⚠️ 常见陷阱
⚠️ 编程陷阱:HOVER 的 Isaac Lab 版本依赖
HOVER 依赖 Isaac Lab v2.0.0 + Isaac Sim 4.5。更新版本的 Isaac Lab(v2.3+)重命名了 rsl_rl → rsl_rl_lib,直接安装会报错。解决方案:按 HOVER README 的版本要求安装,或手动修改 import 路径。
⚠️ 编程陷阱:人形的 default_joint_pos 设置不当
如果 default_joint_pos 是完全伸直(所有关节角度=0),人形一开始就会"直挺挺地"摔倒——因为膝关节没有弯曲来吸收冲击。正确的 default_joint_pos 应该是一个微曲的站立姿态(膝关节 ~0.2 rad、髋关节 ~-0.1 rad)。
练习
- [工程题] 在 Isaac Lab 中加载 G1 USD 模型,配置全身 23 DOF 的 actuator 分组(下肢/上肢/躯干),运行 zero agent 100 步确认站立稳定。
- [对比题] 使用 HOVER 模板,分别用 1024 和 4096 num_envs 训练 Oracle Teacher 5000 iteration。比较训练吞吐和 reward 收敛速度。
- [迁移题] 列出将 HOVER 的 H1 配置修改为 G1 配置需要改动的 5 个关键参数。
20.4 HumanoidBench 评估体系 ⭐⭐
这一节解决什么问题:人形全身控制的任务千差万别——搬箱子、开门、跳跃、舞蹈——如何系统性评估一个策略的能力?HumanoidBench 提供了什么标准化的评估框架?
HumanoidBench 概览
HumanoidBench(Sferrazza et al., RSS 2024)是当前最全面的人形全身操作 benchmark——在 Unitree H1 + 双 Shadow-Hand 风格灵巧手上定义了 27 个任务,涵盖纯 locomotion、全身 manipulation 和混合任务。
平台参数:
| 参数 | 值 |
|---|---|
| 机器人 | Unitree H1 + 21-DOF \(\times\) 2 灵巧手 |
| 总 DOF | 101 |
| Action 维度 | 61(position control) |
| Proprio obs 维度 | 151 |
| 相机 | 2\(\times\) 头部 RGB(448 分辨率) |
| 触觉 | 448-taxel 全身触觉 |
| 控制频率 | 50 Hz |
27 任务分类:
| 类别 | 任务示例 | 核心挑战 | 典型基线性能 |
|---|---|---|---|
| locomotion(12 tasks) | 行走、跑步、站立、爬楼梯 | 平衡 + 步态 | PPO 可解决大部分 |
| whole-body manip(15+ tasks) | 搬箱子、开门、推车、投篮、擦窗 | 平衡 + 接触 + 长时程 | PPO 大部分"still unsolved" |
HumanoidBench 的核心发现:
"Hierarchical learning decisively outperforms end-to-end RL on long-horizon manipulation."
这句话是 HumanoidBench 论文中最重要的经验结论。在所有全身 manipulation 任务上,分层 RL(先训练低层 locomotion policy,再在其上训练高层 task policy)的成功率比端到端 RL(PPO/SAC/TD-MPC2/DreamerV3 直接在全 DOF 上训练)高出数倍。这为 ExBody2 和 HOVER 的分层架构提供了实证支持。
为什么分层比端到端好? 两个原因:
-
探索效率:61D 动作空间中,随机探索几乎 100% 导致摔倒。分层中低层策略已经学会了稳定行走——高层策略在"已经会走路的机器人"上探索操作任务,不用担心摔倒。
-
信用分配:端到端策略在摔倒后得到大的负 reward,但无法区分"因为走路不稳摔倒"还是"因为抓取动作导致摔倒"——信用分配混乱。分层中,摔倒的责任明确归属于低层策略(如果它的 velocity tracking 出了问题)。
HumanoidBench 作为你自己工作的 baseline 参照:
如果你要发表人形全身控制的研究,HumanoidBench 提供了标准化的对比基线。使用方法:
# 安装 HumanoidBench
git clone https://github.com/carlosferrazza/humanoid-bench
cd humanoid-bench && pip install -e .
# 运行你的策略在指定任务上
python evaluate.py --task push --policy your_policy.pt
# 对比标准基线
# HumanoidBench 论文中报告了 PPO, SAC, TD-MPC2, DreamerV3 的性能
⚠️ 常见陷阱
💡 概念误区:认为"HumanoidBench 用 H1 + Shadow Hand,跟我的 G1 没关系"
HumanoidBench 的核心贡献不是特定的机器人配置——而是任务定义和评估方法论。即使你使用 G1(没有灵巧手),HumanoidBench 中不需要灵巧手的任务(如搬箱子、推车)的 obs/action/reward 设计可以直接参考。
HumanoidBench 中代表性任务的 MDP 分析
以下详细分析三个具有代表性的任务——它们不需要灵巧手,可以直接迁移到 G1:
注意:以下任务 MDP 是用于教学说明的概念表达(字典格式的伪代码),不是可直接运行的 mjlab/Isaac Lab 配置。实际实现时需要将每个字段映射到对应框架的
ObservationTermCfg、RewardTermCfg等 API。但 obs 维度、reward 表达式和 success metric 的设计逻辑是通用的——可以直接指导你的实际配置。
任务一:Push(推箱子到目标位置)
# Push 任务的 MDP 设计
push_task = {
"obs": {
# 本体感知(与 Ch14 一致)
"joint_pos": 23, # G1 关节角度
"joint_vel": 23, # G1 关节速度
"base_ang_vel": 3, # 角速度
"projected_gravity": 3, # 重力方向
# 任务相关
"box_pos_relative": 3, # 箱子相对于 base 的位置
"box_vel": 3, # 箱子速度
"target_pos_relative": 3, # 目标位置相对于 base
"hand_to_box_dist": 2, # 左右手到箱子的距离
},
"action": 23, # G1 全身关节位置
"reward": {
"approach_box": "exp(-||base - box|| / 0.5)",
"push_progress": "exp(-||box - target|| / 0.3)",
"upright": "exp(-pitch² / 0.1²)",
"alive": "1.0",
"action_rate": "-0.01 * ||a_t - a_{t-1}||²",
},
"episode_length": 500, # 10 秒 @ 50 Hz
"success_metric": "box_to_target_dist < 0.1 m",
}
任务二:Carry(搬起箱子并走到目标位置)
# Carry 任务的 MDP 设计——比 Push 多了 grasp + locomotion 阶段
carry_task = {
"obs": {
# 本体感知
"joint_pos": 23,
"joint_vel": 23,
"base_ang_vel": 3,
"projected_gravity": 3,
# 任务相关
"box_pos_relative": 3,
"box_quat_relative": 4,
"target_pos_relative": 3,
"hand_contact_force": 6, # 左右手的接触力(3D × 2)
"box_height": 1, # 箱子离地高度
},
"action": 23,
"reward": {
"approach": "exp(-||base - box|| / 0.5) * (1 - box_grasped)",
"grasp": "box_grasped * 5.0",
"carry": "box_grasped * exp(-||box - target|| / 0.3)",
"box_height": "box_grasped * exp(-|box_h - 0.5|² / 0.1²)",
"upright": "exp(-pitch² / 0.1²)",
"action_rate": "-0.02 * ||a_t - a_{t-1}||²",
},
"episode_length": 750, # 15 秒(需要更长时间完成)
"success_metric": "box_grasped AND box_to_target_dist < 0.15 m",
}
任务三:Door(开门并走过去)
# Door 任务的 MDP 设计——接触序列最复杂
door_task = {
"obs": {
"joint_pos": 23,
"joint_vel": 23,
"base_ang_vel": 3,
"projected_gravity": 3,
# 任务相关
"door_handle_pos_relative": 3, # 门把手位置
"door_angle": 1, # 门的开合角度
"hand_to_handle_dist": 2, # 手到门把手距离
"door_contact_force": 3, # 手与门的接触力
},
"action": 23,
"reward": {
"approach_door": "exp(-||base - door|| / 0.5)",
"reach_handle": "exp(-||hand - handle|| / 0.1)",
"turn_handle": "door_angle / max_angle * 3.0",
"push_door": "door_angle * 2.0",
"walk_through": "(base_x > door_x) * 5.0",
"upright": "exp(-pitch² / 0.1²)",
},
"episode_length": 1000, # 20 秒(长时程任务)
"success_metric": "base passed through door AND door_angle > 60°",
}
从这三个任务中提取的通用设计原则:
| 原则 | Push | Carry | Door | 通用建议 |
|---|---|---|---|---|
| obs 使用相对坐标 | ✅ box_pos_relative | ✅ | ✅ handle_pos_relative | 始终使用 base frame 相对坐标 |
| reward 有明确的阶段性 | 弱(直接推) | 强(接近→抓→搬) | 强(接近→抓把手→开→走过) | 阶段越多用乘法门控或 WoCoCo |
| episode 长度 | 500 | 750 | 1000 | 阶段越多 episode 越长 |
| 接触力作为 obs | 否 | ✅ hand_contact | ✅ door_contact | 涉及力控任务时加入 |
| 关键 success metric | 距离 | 距离+高度 | 位置+角度 | 用任务语义而非 reward 值评估 |
分层 RL 的工程实现模式
HumanoidBench 证实了分层 RL 的优势。以下是标准的两层架构实现:
# 分层 RL 的标准两层架构
class HierarchicalHumanoidPolicy:
"""分层全身控制策略。
低层:23-DOF locomotion policy(跟踪 base velocity command)
高层:task policy(根据任务 obs 输出 velocity command + 上肢目标)
低层 50 Hz,高层 10 Hz(每 5 个低层 step 更新一次高层命令)
"""
def __init__(self, low_level_ckpt, high_level_policy):
# 低层:预训练的 locomotion policy(Ch14 的成果)
self.low_level = torch.load(low_level_ckpt)
self.low_level.eval()
for p in self.low_level.parameters():
p.requires_grad = False
# 高层:可训练的 task policy
self.high_level = high_level_policy
self.high_level_freq_ratio = 5 # 低层:高层 = 5:1
self.step_counter = 0
self.current_high_level_cmd = None
def step(self, obs):
"""每个仿真步调用。"""
# 高层策略每 5 步更新一次
if self.step_counter % self.high_level_freq_ratio == 0:
task_obs = self._extract_task_obs(obs)
self.current_high_level_cmd = self.high_level(task_obs)
# 高层输出:
# - base_vel_cmd (3D): 底盘速度命令
# - upper_body_target (8D): 上肢目标关节角度
# 低层策略每步执行
low_level_obs = self._compose_low_level_obs(
obs, self.current_high_level_cmd
)
joint_action = self.low_level(low_level_obs)
self.step_counter += 1
return joint_action
def _extract_task_obs(self, obs):
"""提取高层策略需要的任务相关 obs。"""
return torch.cat([
obs["joint_pos"],
obs["joint_vel"],
obs["base_ang_vel"],
obs["projected_gravity"],
obs["task_specific_obs"], # 如 box_pos_relative
], dim=-1)
def _compose_low_level_obs(self, obs, high_level_cmd):
"""组合低层策略的 obs(本体感知 + 高层命令)。"""
return torch.cat([
obs["joint_pos"],
obs["joint_vel"],
obs["base_ang_vel"],
obs["projected_gravity"],
high_level_cmd["base_vel_cmd"], # 高层给出的速度命令
high_level_cmd["upper_body_target"], # 高层给出的上肢目标
obs["last_action"],
], dim=-1)
分层 RL 训练流程:
Step 1: 训练低层 locomotion policy(Ch14)
→ 纯 velocity tracking,全身 23 DOF
→ 冻结低层权重
Step 2: 训练高层 task policy
→ 输入:task obs(物体/目标位置等)
→ 输出:base_vel_cmd + upper_body_target
→ 低层作为"环境的一部分"——高层的动作通过低层转化为关节角度
→ PPO 训练高层,reward 是任务 reward
Step 3 (可选): 端到端 fine-tune
→ 解冻低层权重
→ 用极小 lr (1e-5) 同时更新高层和低层
→ 目标:让低层适应高层可能给出的"不寻常"命令
分层 RL 的 curriculum 建议:高层训练初期应该只给简单的任务(如物体在附近、不需要长距离行走),逐步增加任务难度。这与 Ch19 的四阶段课程思想一致——先学子技能(低层已有),再在简单场景中学组合(高层+低层),最后加 DR。
练习
- [调研题] 从 HumanoidBench 的 27 个任务中,选出 5 个不需要灵巧手的任务。对每个任务,列出核心的 obs term 和 reward term。
- [分析题] HumanoidBench 中 locomotion 任务 PPO 可以解决但 manipulation 任务 PPO 无法解决——从动作空间维度和 episode 长度两个角度解释这个差异。
20.5 精读:WoCoCo 接触阶段分解(CoRL'24)⭐⭐⭐
这一节解决什么问题:通过 WoCoCo 的精读,理解如何将复杂的全身操作任务分解为可学习的接触阶段,以及 stage-aware reward 如何工程实现。
WoCoCo 的核心思想
WoCoCo(Whole-Body Control with Sequential Contacts,Zhang et al., CoRL 2024)的核心洞察是:大多数复杂的全身操作任务可以被描述为一系列接触模式的切换。
"接触模式"定义了在某一时刻,哪些身体部位应该与环境有接触力。例如:
双脚站立 = {left_foot: ground, right_foot: ground}
单脚支撑 = {left_foot: ground}
双手抓墙 = {left_foot: ground, right_foot: ground, left_hand: wall, right_hand: wall}
任务就是这些接触模式的有序序列——"先双脚站立 → 再单脚蹬地 → 再腾空 → 再双手抓墙"。每次接触模式的切换定义了一个"阶段边界"。
Contact Stage 的工程定义
# WoCoCo: Contact Stage 定义(以搬箱子为例)
class BoxCarryContactPlan:
"""搬箱子任务的接触阶段计划。
每个阶段定义:
1. 期望的接触集合(哪些 body 应该有接触力)
2. 进入条件(什么时候从上一个阶段切换过来)
3. 退出条件(什么时候切换到下一个阶段)
"""
stages = [
# Stage 0: 接近箱子
ContactStage(
name="approach",
expected_contacts={"left_foot", "right_foot"},
entry_condition=lambda: True, # 初始阶段
exit_condition=lambda env: env.hand_to_box_dist < 0.1, # 手接近箱子
),
# Stage 1: 双手触碰箱子
ContactStage(
name="grasp",
expected_contacts={"left_foot", "right_foot", "left_hand", "right_hand"},
entry_condition=lambda env: env.hand_to_box_dist < 0.1,
exit_condition=lambda env: env.box_lifted > 0.05, # 箱子被抬起
),
# Stage 2: 搬运行走
ContactStage(
name="carry_walk",
expected_contacts={"left_foot", "right_foot", "left_hand", "right_hand"},
entry_condition=lambda env: env.box_lifted > 0.05,
exit_condition=lambda env: env.box_at_target, # 箱子到达目标位置
),
# Stage 3: 放下箱子
ContactStage(
name="place",
expected_contacts={"left_foot", "right_foot"},
entry_condition=lambda env: env.box_at_target,
exit_condition=lambda env: env.hands_released,
),
]
Stage-Aware Reward 的实现
WoCoCo 的 reward 由三个组件组成,每个组件都是 stage-aware 的:
组件一:Contact Reward
def contact_reward(env, current_stage):
"""鼓励当前阶段的期望接触模式。
不是 0/1 的二值判断——而是 dense 的接触力大小。
正确接触给正 reward,错误接触给负 reward。
"""
expected = current_stage.expected_contacts
actual = env.get_active_contacts()
reward = 0.0
# 鼓励期望的接触
for body in expected:
contact_force = env.get_contact_force(body)
reward += torch.clamp(contact_force, 0, 100) / 100 # 归一化
# 惩罚意外的接触(如膝盖撞地)
unexpected = actual - expected
for body in unexpected:
contact_force = env.get_contact_force(body)
reward -= torch.clamp(contact_force, 0, 100) / 50 # 更大的惩罚
return reward
组件二:Stage-Count Reward
def stage_count_reward(env, current_stage_idx, total_stages):
"""鼓励策略推进到更高的阶段。
每进入一个新阶段给一次性的 bonus reward。
防止策略"卡在"某个阶段不前进。
"""
return current_stage_idx / total_stages # 线性奖励
组件三:Task-Specific Term(每个任务只需 1-2 项)
# 搬箱子任务的 task-specific reward
def box_carry_task_reward(env, current_stage):
if current_stage.name == "approach":
# 导航 reward——与 Ch19 §19.5 相同
return gaussian_distance(env.base_pos, env.box_pos, sigma=0.5)
elif current_stage.name == "carry_walk":
# 搬运 reward——箱子到目标位置的距离
return gaussian_distance(env.box_pos, env.target_pos, sigma=0.3)
else:
return 0.0
WoCoCo 的 reward 设计哲学:contact reward 和 stage-count reward 是 task-agnostic 的——对任何涉及接触序列的任务都一样。每个具体任务只需要指定 1-2 个 task-specific reward term。这种"通用模板 + 少量定制"的设计使 WoCoCo 能快速适应新任务。
WoCoCo 的 Three-Stage DR 课程
# WoCoCo 的三阶段 DR 课程
dr_curriculum = {
# 阶段 (i): 无 DR,先学会接触序列
"phase_1": {
"dr_enabled": False,
"smoothness_weight": 0.01,
"iterations": 5000,
},
# 阶段 (ii): 开启 DR + 增大 smoothness weight
"phase_2": {
"dr_enabled": True,
"dr_mass_range": (-0.5, 0.5),
"dr_friction_range": (0.5, 1.5),
"smoothness_weight": 0.05,
"iterations": 5000,
},
# 阶段 (iii): 更严格的 regularization,准备 sim-to-real
"phase_3": {
"dr_enabled": True,
"dr_mass_range": (-1.0, 1.0),
"dr_friction_range": (0.3, 2.0),
"smoothness_weight": 0.1,
"action_rate_weight": 0.05,
"iterations": 5000,
},
}
为什么 WoCoCo 在 Phase 1 不加 DR? 接触序列的学习本身就很难——如果同时加 DR,物理参数的变化会让接触判断变得不可靠(如摩擦系数低的时候手可能抓不住箱子),策略会因为"接触不可靠"而学不会正确的接触序列。先在确定的物理参数下学会接触序列,再逐步引入不确定性。
用一个学乐器的类比:Phase 1 就像在安静的练习室里练新曲子——先把音符和节奏搞对(接触序列)。Phase 2 就像在嘈杂的环境中练习——加入各种干扰(DR)但你已经会弹了,不会因为干扰就忘记曲子。Phase 3 就像在舞台上排练——最严格的条件(强正则化),确保现场演出时不会出问题(sim-to-real)。如果一上来就在嘈杂环境中学新曲子(Phase 1 就加 DR),你可能连基本的音符都记不住。
本质洞察:WoCoCo 的接触阶段分解本质上是一种"把长时程探索问题分解为短时程探索问题"的策略。一个 20 秒的搬箱子任务(1000 步)的探索空间是 \(10^{1000 \times 23}\)——不可能通过随机探索解决。但分成 4 个阶段后,每个阶段只有 ~5 秒(250 步),探索空间缩小到 \(10^{250 \times 23}\)——仍然巨大,但 dense reward 让 PPO 能在每个阶段内高效学习。更重要的是,阶段之间的切换由人工接触计划驱动——策略不需要自己"发现"正确的接触序列,只需要在给定的接触模式下学会最优动作。
WoCoCo 的四个示范任务
| 任务 | 接触阶段数 | 核心挑战 | 真机验证 |
|---|---|---|---|
| 跑酷跳跃 | 3-5 | 腾空阶段无接触,落地冲击大 | ✅ |
| 箱子搬运 | 4 | 搬运时需保持箱子不掉 | ✅ |
| 拍手跺脚舞蹈 | 6-8 | 接触切换频繁,时序精确 | ✅ |
| 攀岩 | 8-12 | 3D 接触规划,负重悬挂 | ✅ |
论文还证明了 WoCoCo 的通用性——将同样的框架应用于 22-DOF 恐龙机器人的 loco-manipulation,无需修改核心 reward 设计。
WoCoCo 的完整 Reward 配置(mjlab 风格)
# mjlab: WoCoCo 风格的全身控制 Reward 配置
class WoCoCoRewardsCfg:
"""基于接触阶段的全身操作 reward 配置。
设计原则:
1. contact_reward 和 stage_progress 是 task-agnostic(通用)
2. 每个任务只需 1-2 个 task-specific term
3. regularization 全程生效
"""
# === Task-Agnostic Rewards ===
# 接触模式正确性(每个阶段自动检查)
contact_correctness = ContactCorrectnessRewardCfg(
func=dense_contact_reward,
params={
"correct_contact_scale": 1.0, # 正确接触的正 reward
"incorrect_contact_scale": -2.0, # 错误接触的负 reward
"force_clip": 100.0, # N, 接触力裁剪
},
weight=2.0,
)
# 阶段推进奖励
stage_progress = StageProgressRewardCfg(
func=linear_stage_reward,
params={"bonus_per_stage": 5.0},
weight=1.0,
)
# === Regularization (全程生效) ===
base_orientation = OrientationRewardCfg(
func=projected_gravity_penalty,
params={"threshold": 0.3}, # rad
weight=-1.0,
)
base_height = HeightRewardCfg(
func=height_tracking,
params={"target": 0.68, "sigma": 0.05}, # G1 站立高度
weight=0.5,
)
action_rate = ActionRateRewardCfg(weight=-0.02)
joint_acceleration = JointAccelerationRewardCfg(
func=joint_acc_penalty,
weight=-0.001, # 防止关节加速度过大
)
alive = AliveRewardCfg(weight=0.5)
# === Task-Specific (搬箱子示例, 只需 2 个额外项) ===
box_approach = NavigationRewardCfg(
func=gaussian_distance,
params={"sigma": 0.5},
body_name="base",
target_name="box",
active_stages=[0], # 只在 Stage 0 激活
weight=1.0,
)
box_at_target = GoalReachRewardCfg(
func=goal_distance,
params={"sigma": 0.2},
object_name="box",
goal_name="target_marker",
active_stages=[2, 3], # 只在搬运阶段激活
weight=3.0,
)
WoCoCo Reward 的工程亮点——active_stages 参数:每个 reward term 可以指定在哪些 stage 激活。这比 Ch19 的乘法门控更灵活——乘法门控只能编码线性顺序,active_stages 可以在任意 stage 组合上激活。例如 box_at_target 在 Stage 2 和 Stage 3 都激活(搬运和放下阶段都需要让箱子靠近目标)。
工程注意:上述
active_stages字段是概念性的伪代码——mjlab/Isaac Lab 的标准RewardTermCfg不原生支持active_stages参数。实际实现需要在自定义的 reward function 中检查当前 contact stage 并手动启用/禁用对应的 reward 项。一种工程实现方式是在 reward 函数内部加一个if current_stage in active_stages:的条件判断。
WoCoCo 的完整训练管线
# Phase 1: 无 DR,学接触序列
uv run train WoCoCo-BoxCarry-G1 \
--env.scene.num-envs 4096 \
--agent.max-iterations 5000 \
--agent.learning-rate 3e-4 \
--reward.smoothness-weight 0.01 \
--dr.enabled False
# Phase 2: 开 DR + 增大 smoothness
uv run train WoCoCo-BoxCarry-G1 \
--resume-from phase1_best.pt \
--env.scene.num-envs 4096 \
--agent.max-iterations 5000 \
--agent.learning-rate 1e-4 \
--reward.smoothness-weight 0.05 \
--dr.enabled True \
--dr.mass-range "[-0.5, 0.5]" \
--dr.friction-range "[0.5, 1.5]"
# Phase 3: 严格 regularization
uv run train WoCoCo-BoxCarry-G1 \
--resume-from phase2_best.pt \
--env.scene.num-envs 4096 \
--agent.max-iterations 5000 \
--agent.learning-rate 5e-5 \
--reward.smoothness-weight 0.1 \
--reward.action-rate-weight 0.05 \
--dr.enabled True \
--dr.mass-range "[-1.0, 1.0]" \
--dr.friction-range "[0.3, 2.0]"
WoCoCo 的训练收敛特征(搬箱子任务):
Phase 1 (无 DR, 5000 iter):
0-1000 iter: contact_reward 快速上升(学会正确的脚地接触)
1000-3000 iter: stage_progress 上升(学会按顺序推进阶段)
box_approach 上升(Stage 0 的导航)
3000-5000 iter: 开始出现完整的 Stage 0→1→2→3 序列
Phase 2 (DR on, 5000 iter):
初期: reward 短暂下降(DR 引入不确定性)
中期: reward 恢复并超过 Phase 1(策略学会了更鲁棒的接触)
后期: smoothness weight 增大导致动作更平滑但速度稍慢
Phase 3 (strict reg, 5000 iter):
reward 可能略低于 Phase 2(正则化代价)
但 action_rate 和 joint_acceleration 明显更小
sim-to-real 准备就绪
如果 Phase 1 中 stage_progress 始终为 0(策略卡在 Stage 0):最常见的原因是 exit_condition 太严格。例如"hand_to_box_dist < 0.05 m"要求手精确到 5cm 以内才进入 Stage 1——对人形来说太难了。放松到 0.15 m 通常就够了。另一个原因是 contact_reward 的"正确接触"定义太严格——如果只有完美的双手同时接触才给正 reward,策略可能无法先用一只手试探。改为"至少一只手接触就给部分 reward"。
⚠️ 常见陷阱
⚠️ 编程陷阱:Contact stage 的切换条件太"松"
如果 exit_condition 只检查"手是否接近箱子"而不检查"接触力是否存在",策略可能学到"把手伸到箱子附近但不真正触碰"就进入下一个阶段。正确做法:切换条件应同时检查几何接近和物理接触。
💡 概念误区:认为"WoCoCo 只适用于有明确接触计划的任务"
虽然 WoCoCo 需要人工指定接触计划,但这个计划不需要很精细——只需要"大概的接触顺序"。例如搬箱子的接触计划是"先走到箱子附近 → 手碰到箱子 → 抬起 → 走",不需要精确到"左手先碰还是右手先碰"。策略在每个阶段内的细节(如用什么手势抓取)完全由 RL 自动学习。
练习
- [设计题] 为"人形机器人开门"任务设计 WoCoCo 的 contact stage 计划。列出每个阶段的名称、expected_contacts 和 exit_condition。
- [实验题] 使用 WoCoCo 的 three-stage DR 课程训练一个简单任务。比较 Phase 1 结束和 Phase 3 结束的成功率。
- [分析题] WoCoCo 的 contact reward 使用 dense 的接触力大小(而非 binary 的"有/没有接触")。这对 PPO 的梯度信号有什么好处?
20.6 精读:ExBody/ExBody2 上下肢解耦(ExBody RSS'24 / ExBody2 RSS'25 Workshop Spotlight)⭐⭐⭐
这一节解决什么问题:通过 ExBody2 的精读,理解上下肢解耦控制的工程实现——velocity-landmark 解耦 tracking、teacher-data 自动过滤和 specialist fine-tuning。
ExBody 的原始设计
ExBody(Cheng et al., RSS 2024,chengxuxin/expressive-humanoid)的核心思想简单且有效:
ExBody 的 reward 分解:
r_upper = Σ_i w_i · exp(-||q_i^robot - q_i^ref||² / σ_i²)
→ 上半身关键点跟踪参考动作
r_lower = exp(-||v_root^robot - v_root^cmd||² / σ_v²)
→ 下半身跟踪速度命令
r_total = w_upper · r_upper + w_lower · r_lower + r_regularization
上半身的参考动作来自 MoCap 数据——CMU motion capture FBX → ASE poselib → YAML 格式。策略不需要"理解"动作的语义(是舞蹈还是挥手),只需要让关键点(肩、肘、手腕)的位置跟踪参考值。
下半身的速度命令与 Ch14 完全相同——策略跟踪外部给出的 \([v_x, v_y, \omega_z]\) 命令。这个设计的优雅之处在于:下半身的 locomotion 已经在 Ch14 中充分验证和调优,ExBody 只需要在"已经稳定行走的基础上"增加上半身的跟踪目标。
ExBody2 的三个关键改进
改进一:解耦 velocity tracking 与 landmark tracking
ExBody 的一个问题是 velocity tracking 和 landmark tracking 耦合——如果参考动作包含大幅度的前后移动(如跑步),velocity tracking 的"跟踪参考根速度"和 landmark tracking 的"跟踪参考姿态"之间会产生冲突。
ExBody2 的解决方案:
# ExBody2 的解耦 tracking
def exbody2_reward(env, ref_motion, velocity_cmd):
# 1. Velocity tracking——跟踪用户命令(不是参考动作的速度)
vel_error = env.root_velocity - velocity_cmd # 用户命令
r_velocity = torch.exp(-vel_error.norm(dim=-1) / 0.25)
# 2. Landmark tracking——在局部坐标系中跟踪
# 关键:使用机器人当前位置的局部坐标系,不是世界坐标系
for joint_name in upper_body_joints:
# 获取参考动作中关键点的局部位置
ref_pos_local = ref_motion.get_local_position(joint_name)
# 获取机器人当前关键点的局部位置
robot_pos_local = env.get_local_position(joint_name)
# 在局部坐标系中计算误差
error = (ref_pos_local - robot_pos_local).norm()
r_landmark += torch.exp(-error / sigma)
# 3. 合并
return w_vel * r_velocity + w_landmark * r_landmark
"局部坐标系"是关键——landmark 在机器人当前 base frame 中计算,而非世界坐标系。这消除了"参考动作在世界中移动了 5 米但机器人只走了 3 米导致 landmark 大幅偏离"的 drift 问题。
改进二:Teacher-Data 自动过滤
# ExBody2 的 teacher-data 过滤流程
def filter_motion_data(motion_dataset, teacher_policy, threshold=0.5):
"""用 privileged teacher 自动过滤不可行的动作。
原理:如果 teacher(拥有完美信息)都无法执行某个动作,
那 student(信息更少)更不可能——所以应该从训练数据中删除。
"""
feasible_motions = []
for motion in motion_dataset:
# 用 teacher 在仿真中执行这个参考动作
reward, episode_length = evaluate_teacher(teacher_policy, motion)
# 过滤条件:
# 1. 平均 reward 高于阈值(teacher 能跟踪)
# 2. episode 没有 early termination(没摔倒)
if reward > threshold and episode_length > min_length:
feasible_motions.append(motion)
else:
print(f"Filtered: {motion.name} (reward={reward:.3f})")
print(f"Kept {len(feasible_motions)}/{len(motion_dataset)} motions")
return feasible_motions
这种"用 teacher 作为过滤器"的方法比 ExBody 的语言标签过滤更可靠——"dance"标签可能包含前空翻(机器人做不了),但 teacher 评估可以精确判断。
改进三:Specialist Fine-Tuning
# ExBody2 的 generalist → specialist 流程
# Step 1: 在所有过滤后的 MoCap 数据上训练 generalist
generalist = train_policy(
motion_data=all_feasible_motions,
num_iterations=20000,
)
# Step 2: 对特定动作组 fine-tune
specialist_dance = fine_tune(
base_policy=generalist,
motion_data=dance_motions_only,
num_iterations=5000,
lr=1e-5, # 比 generalist 小 10×
)
Fine-tuning 的 trade-off:specialist 在目标动作组上的 tracking error 降低 30-50%,但在其他动作组上的性能下降 20-40%。这是一个"专精 vs 通用"的设计决策——如果你的部署场景只需要跳舞,specialist 更好;如果需要多种动作,generalist 更好。
本质洞察:ExBody2 的 generalist→specialist 范式揭示了一个深层的工程权衡——网络容量是有限的。一个 [256, 256, 128] 的 MLP 大约有 ~100K 参数。如果这些参数需要同时编码"走路"、"跳舞"、"挥手"、"蹲下"四种行为,每种行为分到 ~25K 参数的"容量"。如果 fine-tune 只保留"跳舞",全部 ~100K 参数都服务于跳舞——precision 自然更高。这与 Foundation Model 领域的"pretrain + fine-tune"范式完全一致——大模型先学通用能力,再在特定任务上微调。
用一个运动员的类比:generalist 像全能运动员(铁人三项)——游泳、骑车、跑步都会但都不是顶级。specialist 像专项运动员——只练一项但达到极限水平。ExBody2 的做法是"先当全能运动员打基础(generalist),再选一项做专项训练(specialist)"——这比从零开始做专项训练更高效,因为全能训练已经建立了通用的运动能力(稳定行走、基本协调)。
ExBody2 的 MoCap 数据处理管线
原始 MoCap 数据(CMU / AMASS)
→ FBX 格式
→ ASE poselib 转换为 poselib YAML
→ retarget 到 G1 的骨骼结构
→ 关节角度映射(人体 → G1)
→ 运动学可行性检查
→ Teacher 评估过滤(自动删除不可行动作)
→ 最终的训练数据集
Retargeting 的工程要点:人体有 ~24 个主要关节,G1 有 23 个——关节不是一一对应的。例如,人体的脊柱有多个自由度,G1 只有一个 waist_yaw。retarget 时需要将人体的多关节运动"投影"到 G1 的少关节空间中——这不可避免地丢失了一些动作细节(如脊柱侧弯在 G1 上无法表达)。
AMASS 到 G1 的 retargeting 代码(简化版):
# AMASS → G1 retargeting 的核心逻辑
import numpy as np
class AMASS2G1Retargeter:
"""将 AMASS (SMPL) 动作数据转换为 G1 关节角度。
SMPL 有 23 个 body joints + 全局旋转。
G1 有 23 个关节(此处为简化教学映射,只映射 SMPL 能提供的主关节,
其余 G1 关节如 ankle_roll/wrist_roll 保持默认姿态)。映射关系如下:
"""
# SMPL → G1 的关节映射
JOINT_MAP = {
# 下肢
"left_hip": "left_hip_pitch", # SMPL hip → G1 hip_pitch
"left_knee": "left_knee",
"left_ankle": "left_ankle_pitch",
"right_hip": "right_hip_pitch",
"right_knee": "right_knee",
"right_ankle": "right_ankle_pitch",
# 上肢
"left_shoulder": "left_shoulder_pitch",
"left_elbow": "left_elbow",
"right_shoulder": "right_shoulder_pitch",
"right_elbow": "right_elbow",
# 躯干
"spine": "waist_yaw", # 只取 yaw 分量
}
# G1 的关节限位(rad)
JOINT_LIMITS = {
"left_hip_pitch": (-1.57, 1.57),
"left_knee": (0.0, 2.09),
"left_ankle_pitch": (-0.87, 0.52),
"left_shoulder_pitch": (-3.14, 3.14),
"left_elbow": (-1.57, 1.57),
# ... 其他关节类似
}
def retarget(self, smpl_poses, fps_in=30, fps_out=50):
"""将 SMPL 动作转换为 G1 关节角度序列。
Args:
smpl_poses: [T, 23, 3] SMPL joint rotations (axis-angle)
fps_in: 原始 MoCap 帧率
fps_out: G1 控制频率
Returns:
g1_joints: [T', 23] G1 关节角度序列
"""
g1_joints_list = []
for frame in smpl_poses:
g1_frame = {}
for smpl_name, g1_name in self.JOINT_MAP.items():
smpl_idx = self.SMPL_JOINT_INDICES[smpl_name]
axis_angle = frame[smpl_idx]
# 提取对应轴的旋转角度
# G1 的关节是单轴的,需要从 SMPL 的 3-axis 旋转中提取
angle = self._extract_primary_axis(axis_angle, g1_name)
# 裁剪到关节限位内
low, high = self.JOINT_LIMITS[g1_name]
angle = np.clip(angle, low, high)
g1_frame[g1_name] = angle
g1_joints_list.append(g1_frame)
# 帧率转换(30 Hz → 50 Hz,线性插值)
g1_joints = self._resample(g1_joints_list, fps_in, fps_out)
return g1_joints
def _extract_primary_axis(self, axis_angle, joint_name):
"""从 axis-angle 旋转中提取 G1 关节对应轴的角度。
例如:hip_pitch 对应 pitch 轴(绕 Y 轴旋转),
所以从 axis-angle 中提取 Y 轴分量。
"""
if "pitch" in joint_name:
return axis_angle[1] # Y 轴
elif "roll" in joint_name:
return axis_angle[0] # X 轴
elif "yaw" in joint_name:
return axis_angle[2] # Z 轴
else:
return np.linalg.norm(axis_angle) # 使用旋转角度的范数
def _resample(self, joints_list, fps_in, fps_out):
"""帧率转换:线性插值。"""
T_in = len(joints_list)
T_out = int(T_in * fps_out / fps_in)
# ... scipy.interpolate.interp1d ...
return resampled
Retargeting 的三个常见问题:
-
骨骼长度不匹配:人体手臂长约 60cm,G1 手臂长约 50cm。如果只映射关节角度而不调整运动幅度,G1 的末端轨迹会比人体的短 ~17%。解决方案:对 target landmark 位置做缩放——而非对关节角度缩放。ExBody2 的 landmark tracking 在局部坐标系中直接跟踪位置,自动处理了这个问题。
-
自由度缺失:人体有 hip_roll, hip_yaw, hip_pitch 三个髋关节自由度,但某些人形机器人可能只有 hip_pitch + hip_roll(没有 hip_yaw)。缺失的 DOF 对应的参考动作分量会被丢弃——策略只能在可用的 DOF 上尽可能接近参考动作。KungfuBot 的 adaptive sigma 正好处理了这种"部分不可能"的情况——在缺失 DOF 导致的误差上自动放大 sigma。
-
帧率不匹配:AMASS 数据通常是 30 Hz 或 120 Hz,G1 的控制频率是 50 Hz。帧率转换需要插值——线性插值对关节角度可行,但对四元数需要使用 Slerp(球面线性插值)以避免旋转不自然。
mjlab 的参考动作加载方式:
# mjlab: 通过 WandB Registry 加载参考动作
# Step 1: 预处理 CSV → NPZ
# python scripts/tracking/csv_to_npz.py \
# --input motion_data.csv \
# --input-fps 30 --output-fps 50 \
# --render # 可选:生成预览视频
# Step 2: 上传到 WandB Registry
# wandb artifact put --name your-org/motions/dance-v1 motion_data.npz
# Step 3: 训练时引用
# uv run train Mjlab-Tracking-Flat-Unitree-G1 \
# --registry-name your-org/motions/dance-v1 \
# --env.scene.num-envs 4096
ExBody2 的完整 Tracking Reward 实现
# ExBody2 的 velocity-landmark 解耦 tracking reward
class ExBody2TrackingReward:
"""ExBody2 的核心 reward 实现。
两个独立的 tracking 目标:
1. velocity tracking——跟踪用户给定的根节点速度命令
2. landmark tracking——在局部坐标系中跟踪参考动作的关键点位置
两者完全解耦:
- velocity tracking 不看参考动作的根速度
- landmark tracking 不看用户的速度命令
"""
# 上半身需要跟踪的关键点
UPPER_BODY_KEYPOINTS = [
"left_shoulder", "left_elbow", "left_wrist",
"right_shoulder", "right_elbow", "right_wrist",
"head", "waist",
]
# 每个关键点的 tracking sigma(不同重要程度)
KEYPOINT_SIGMAS = {
"left_shoulder": 0.08,
"left_elbow": 0.06,
"left_wrist": 0.04, # 末端精度要求最高
"right_shoulder": 0.08,
"right_elbow": 0.06,
"right_wrist": 0.04,
"head": 0.10, # 头部精度要求较低
"waist": 0.08,
}
def compute(self, env, ref_motion, velocity_cmd):
# === Part 1: Velocity Tracking ===
root_vel = env.get_root_velocity() # [B, 3] 世界坐标系
vel_error = root_vel[:, :2] - velocity_cmd[:, :2] # xy 平面
yaw_error = root_vel[:, 2:3] - velocity_cmd[:, 2:3] # yaw
r_lin_vel = torch.exp(-vel_error.norm(dim=-1) / 0.25)
r_ang_vel = torch.exp(-yaw_error.abs().squeeze() / 0.25)
r_velocity = 0.7 * r_lin_vel + 0.3 * r_ang_vel
# === Part 2: Landmark Tracking (局部坐标系) ===
r_landmark = torch.zeros(env.num_envs, device=env.device)
for kp_name in self.UPPER_BODY_KEYPOINTS:
# 参考动作的关键点位置(在参考动作的 local frame 中)
ref_pos = ref_motion.get_keypoint_local(kp_name) # [B, 3]
# 机器人的关键点位置(在机器人当前的 local frame 中)
robot_pos = env.get_keypoint_local(kp_name) # [B, 3]
# 局部坐标系中的误差
error = (ref_pos - robot_pos).norm(dim=-1)
sigma = self.KEYPOINT_SIGMAS[kp_name]
r_landmark += torch.exp(-error / sigma)
r_landmark /= len(self.UPPER_BODY_KEYPOINTS) # 归一化
# === Part 3: Regularization ===
r_action_rate = -torch.sum(
(env.action - env.last_action)**2, dim=-1
) * 0.01
r_joint_limit = -torch.sum(
torch.clamp(torch.abs(env.joint_pos) - 0.9 * env.joint_limit, min=0),
dim=-1
) * 5.0
r_foot_contact = self._foot_contact_reward(env)
# === 合并 ===
total = (
1.0 * r_velocity +
1.5 * r_landmark +
r_action_rate +
r_joint_limit +
0.3 * r_foot_contact +
0.5 # alive reward
)
return total
def _foot_contact_reward(self, env):
"""鼓励脚与地面的正确接触——防止"飘浮"行走。"""
left_contact = (env.contact_forces["left_foot"].norm(dim=-1) > 1.0).float()
right_contact = (env.contact_forces["right_foot"].norm(dim=-1) > 1.0).float()
# 至少一只脚在地面上
return (left_contact + right_contact).clamp(0, 1)
Landmark tracking 中的"局部坐标系"关键细节:
ExBody2 使用的"局部坐标系"不是简单的"减去 base 位置"——而是一个"定期重置"的局部坐标系。具体做法:每隔 \(T_{\text{reset}}\) 帧(如 30 帧 = 0.6 秒),将局部坐标系的原点重置为当前 base 位置。在两次重置之间,参考动作的关键点位置相对于"上次重置时的 base 位置"计算——而非当前 base 位置。
为什么这样做?如果用当前 base 位置作为原点(每帧更新),根节点的移动速度差异会被完全消除——即使机器人走得比参考动作快/慢 2 倍,landmark error 也是零。这意味着策略不会被鼓励跟踪参考动作的移动速度——velocity tracking 成了唯一的速度信号。
如果用固定原点(全局坐标系),drift 会随时间累积——5 秒后参考动作可能已经"走远了"而机器人还在原地,landmark error 暴涨但策略对此无能为力。
"定期重置"是一个折中:在 0.6 秒的窗口内,策略被鼓励跟踪参考动作的相对位移(包括移动速度的变化),但不会被长期 drift 干扰。
# 定期重置局部坐标系的实现
class PeriodicLocalFrame:
def __init__(self, reset_interval=30):
"""
reset_interval: 每隔多少帧重置一次局部原点
"""
self.reset_interval = reset_interval
self.origin_pos = None
self.origin_quat = None
self.frame_counter = 0
def update(self, base_pos, base_quat):
"""每步调用:检查是否需要重置原点。"""
if self.frame_counter % self.reset_interval == 0:
self.origin_pos = base_pos.clone()
self.origin_quat = base_quat.clone()
self.frame_counter += 1
def to_local(self, world_pos):
"""将世界坐标转为当前局部坐标。"""
delta = world_pos - self.origin_pos
return quat_rotate(quat_conjugate(self.origin_quat), delta)
⚠️ 常见陷阱
⚠️ 编程陷阱:Landmark tracking 在世界坐标系而非局部坐标系中计算
这是 ExBody(原版)到 ExBody2 的核心改进。如果在世界坐标系中计算 landmark error,策略会尝试同时跟踪参考动作的绝对位置——这在参考动作有大幅度位移时是不可能的(机器人可能走得比参考动作快或慢)。局部坐标系消除了这个问题——策略只需要"姿态像参考动作",不需要"在同一个位置"。
💡 概念误区:认为"ExBody2 的 teacher 过滤会丢弃太多数据"
实际上,在 AMASS 这样的大规模 MoCap 数据集中,大部分动作都是日常活动(走路、坐下、站起、挥手),这些动作对 G1 来说是物理可行的。真正被过滤掉的是少数极端动作(后空翻、劈叉、单手倒立)。典型的过滤比例:保留 70-85% 的动作数据。
练习
- [实现题] 写出 ExBody2 的 landmark tracking reward 函数(在局部坐标系中)。需要跟踪哪些关键点?sigma 应该设为多少?
- [分析题] ExBody2 的 generalist → specialist fine-tuning 与 transfer learning 中的"预训练→微调"有什么异同?
- [跨章综合题] 对比 ExBody2 的 teacher-data 过滤和 KungfuBot 的 physical motion filter。两者过滤的依据有什么区别?哪个更自动化?
20.7 精读:KungfuBot——Physics-Based Humanoid Whole-Body Control for Learning Highly-Dynamic Skills(NeurIPS'25;机制:adaptive motion tracking / BLO)⭐⭐⭐
这一节解决什么问题:高动态动作(功夫、舞蹈、体操)对人形机器人的追踪精度要求极高——有些动作人形机器人物理上无法完美复制。KungfuBot 如何通过 adaptive tracking 解决这个"不可能完美"的问题?
动机:为什么高动态动作特别难
ExBody2 的 tracking reward 使用固定的 sigma(tracking tolerance):
如果 sigma 设得太小(如 0.05 rad),只有非常精确的跟踪才能获得正 reward——策略在难以跟踪的动作上会因为"怎么做都得不到 reward"而放弃。如果 sigma 设得太大(如 0.5 rad),粗略的跟踪就能获得高 reward——策略不会追求精细的动作质量。
KungfuBot 的核心洞察是:不同的动作关键帧需要不同的 sigma。慢速的站立姿态可以精确跟踪(sigma 小),高速的踢腿动作机器人物理上无法完美复制(sigma 应该大),过渡帧不需要精确跟踪(sigma 最大)。
Bi-Level Optimization
KungfuBot 使用双层优化(Bi-Level Optimization, BLO)自动调整每个关键帧的 sigma:
外层(slow loop):调整 per-keyframe sigma
目标:最大化整体 tracking reward + 惩罚 sigma 太大
更新频率:每 N 个 PPO epoch 更新一次
内层(fast loop):训练 tracking policy
目标:最大化 tracking reward(使用外层给定的 sigma)
更新频率:每个 PPO step 更新
# KungfuBot 的 Bi-Level Optimization 简化实现
class AdaptiveTrackingCurriculum:
def __init__(self, num_keyframes, initial_sigma=0.2):
self.sigmas = torch.full((num_keyframes,), initial_sigma)
self.sigma_lr = 0.01
def tracking_reward(self, joint_errors, keyframe_idx):
"""使用 per-keyframe sigma 计算 tracking reward。"""
sigma = self.sigmas[keyframe_idx]
return torch.exp(-joint_errors**2 / sigma**2)
def update_sigmas(self, tracking_errors_per_keyframe):
"""外层更新:根据 tracking 误差调整 sigma。
核心逻辑:
- 如果某个 keyframe 的 tracking error 远大于 sigma → sigma 太小
→ 增大 sigma(放松该帧的要求)
- 如果某个 keyframe 的 tracking error 远小于 sigma → sigma 太大
→ 减小 sigma(收紧该帧的要求)
"""
for i, error in enumerate(tracking_errors_per_keyframe):
gradient = error - self.sigmas[i] # error 偏差
self.sigmas[i] += self.sigma_lr * gradient
self.sigmas[i] = self.sigmas[i].clamp(0.05, 0.5) # 限制范围
BLO 的效果:训练过程中,简单帧的 sigma 自动收缩到 0.05-0.1 rad(精确跟踪),困难帧的 sigma 自动扩大到 0.3-0.5 rad(允许粗略近似),不可能帧的 sigma 扩大到最大值(基本不惩罚偏差)。策略因此能"在简单处精确、在困难处灵活"——这比固定 sigma 的全局策略表现更好。
Physical Motion Filter
在 BLO 之前,KungfuBot 先用 Physical Motion Filter 对 MoCap 数据做预筛选。与 ExBody2 的 teacher 过滤不同,KungfuBot 使用物理指标(而非 teacher 评估)来判断可行性:
# KungfuBot 的 Physical Motion Filter
def physical_motion_filter(motion_data, robot_model):
"""基于物理指标筛选可行的参考动作。
三个物理指标:
1. CoM-CoP proximity: CoM 投影是否在 CoP(Center of Pressure)附近
2. Contact-mask agreement: 脚部接触模式是否与参考一致
3. Joint-limit check: 参考动作是否在机器人关节范围内
"""
feasible = []
for motion in motion_data:
# 1. CoM-CoP proximity
com = compute_com_trajectory(motion, robot_model)
cop = compute_cop_trajectory(motion)
com_cop_dist = (com[:, :2] - cop[:, :2]).norm(dim=-1).mean()
# 2. Contact-mask agreement
ref_contacts = extract_foot_contacts(motion)
sim_contacts = simulate_contacts(motion, robot_model)
contact_agreement = (ref_contacts == sim_contacts).float().mean()
# 3. Joint-limit check
joint_angles = retarget_to_robot(motion, robot_model)
in_limits = (joint_angles.abs() < robot_model.joint_limits * 0.95).all()
# 综合判断
if com_cop_dist < 0.15 and contact_agreement > 0.8 and in_limits:
feasible.append(motion)
return feasible
Physical filter vs Teacher filter:Physical filter 更快(不需要训练 teacher,纯运动学/动力学计算),但可能过于保守(某些动力学上可行但运动学检查不通过的动作会被误删)。Teacher filter 更准确(通过实际执行判断),但需要先训练 teacher(几小时)。KungfuBot 使用 physical filter 作为第一道筛选(快速),如果数据集很大可以再加 teacher filter 作为第二道。
KungfuBot 的训练结果
KungfuBot 在 Unitree G1 上验证了功夫动作(踢腿、出拳、旋风腿)的跟踪效果。对比 ExBody2 和 OmniH2O:
| 方法 | MPBPE (easy) | MPBPE (medium) | MPBPE (hard) |
|---|---|---|---|
| OmniH2O | baseline | — | — |
| ExBody2 | 改进 | — | — |
| KungfuBot | 最优 | 最优 | 最优 |
KungfuBot 在所有难度级别上都优于 ExBody2——特别是在"hard"动作上(高速踢腿、旋风腿),BLO 的 adaptive sigma 让策略能更合理地分配 tracking 精度。
KungfuBot 的完整训练管线
KungfuBot 三阶段管线:
Stage 0: 动作数据预处理
→ GVHMR 从视频提取 SMPL 动作
→ Physical Motion Filter 筛选可行动作
→ Retarget 到 G1 骨骼
→ 最终训练数据集
Stage 1: Teacher 训练(asymmetric actor-critic)
→ Privileged teacher 使用 BLO adaptive sigma
→ 外层每 200 PPO epoch 更新一次 sigma
→ 内层标准 PPO 训练
→ 训练时间:~20k iterations
Stage 2: Student 蒸馏(DAgger)
→ Student 只使用 proprioception history
→ 从 teacher 动作标签蒸馏
→ 训练时间:~5k iterations
KungfuBot BLO 的完整实现:
# KungfuBot 的 Bi-Level Optimization 完整实现
class KungfuBotBLO:
"""Bi-Level Optimization for adaptive tracking tolerance.
外层(slow): 调整 per-keyframe sigma
内层(fast): 训练 tracking policy
外层更新规则:
sigma_new = sigma_old + lr * (tracking_error - sigma_old)
直觉:如果某帧的 tracking error 大于当前 sigma,
说明这一帧太难了——放大 sigma 放松要求。
如果 error 小于 sigma,说明策略已经学会了——
收紧 sigma 要求更高精度。
"""
def __init__(
self,
num_keyframes: int,
num_joints: int,
initial_sigma: float = 0.2,
sigma_lr: float = 0.005,
sigma_min: float = 0.05,
sigma_max: float = 0.5,
update_interval: int = 200, # PPO epochs
):
# per-keyframe, per-joint sigma
self.sigmas = torch.full(
(num_keyframes, num_joints), initial_sigma
)
self.sigma_lr = sigma_lr
self.sigma_min = sigma_min
self.sigma_max = sigma_max
self.update_interval = update_interval
self.error_accumulator = []
self.epoch_counter = 0
def tracking_reward(self, joint_errors, keyframe_idx):
"""计算 adaptive sigma 下的 tracking reward。
Args:
joint_errors: [B, num_joints] 每个关节的角度误差
keyframe_idx: [B] 当前帧在参考动作中的索引
Returns:
reward: [B] tracking reward
"""
# 收集误差用于外层更新
self.error_accumulator.append(
(keyframe_idx.clone(), joint_errors.detach().clone())
)
# 使用当前 sigma 计算 reward
sigma = self.sigmas[keyframe_idx] # [B, num_joints]
per_joint_reward = torch.exp(-joint_errors**2 / sigma**2)
return per_joint_reward.mean(dim=-1) # [B]
def maybe_update_sigmas(self):
"""外层更新:每 update_interval 个 epoch 执行一次。"""
self.epoch_counter += 1
if self.epoch_counter % self.update_interval != 0:
return
if not self.error_accumulator:
return
# 聚合所有累积的误差数据
# 计算每个 keyframe 的平均 tracking error
keyframe_errors = {} # keyframe_idx → list of error tensors
for kf_idx_batch, error_batch in self.error_accumulator:
for kf_idx, error in zip(kf_idx_batch, error_batch):
kf = kf_idx.item()
if kf not in keyframe_errors:
keyframe_errors[kf] = []
keyframe_errors[kf].append(error)
# 更新 sigma
for kf_idx, errors in keyframe_errors.items():
mean_error = torch.stack(errors).mean(dim=0) # [num_joints]
# 外层梯度:error - sigma
gradient = mean_error - self.sigmas[kf_idx]
self.sigmas[kf_idx] += self.sigma_lr * gradient
self.sigmas[kf_idx] = self.sigmas[kf_idx].clamp(
self.sigma_min, self.sigma_max
)
# 清空累积器
self.error_accumulator = []
# 打印 sigma 统计
print(f"[BLO] Sigma update at epoch {self.epoch_counter}:")
print(f" mean={self.sigmas.mean():.4f}, "
f"min={self.sigmas.min():.4f}, "
f"max={self.sigmas.max():.4f}")
BLO 的训练可视化:在 TensorBoard/WandB 中应该监控以下 BLO 特有指标:
BLO 特有指标:
sigma_mean: 所有 keyframe 的平均 sigma → 应在 0.1-0.3 范围内
sigma_min: 最小 sigma(最容易的帧)→ 应在 0.05-0.1
sigma_max: 最大 sigma(最难的帧)→ 应在 0.3-0.5
sigma_std: sigma 的标准差 → 越大说明帧间难度差异越大
easy_frame_ratio: sigma < 0.1 的帧比例 → 策略"已掌握"的帧
hard_frame_ratio: sigma > 0.3 的帧比例 → 策略"放弃精确"的帧
如果 sigma_mean 持续增大到接近 sigma_max,说明大部分帧都太难了——可能是参考动作整体不可行(physical filter 没有充分过滤),或者策略的容量不够(MLP 太小)。如果 sigma_mean 持续减小到接近 sigma_min,说明参考动作太简单——不需要 BLO,固定 sigma 就够了。
⚠️ 常见陷阱
⚠️ 编程陷阱:BLO 的外层更新频率太高
如果外层(sigma 更新)每个 PPO step 都更新,sigma 会因为策略还没来得及适应就被改变——导致"sigma 和策略互相追逐"的不稳定。正确做法:外层更新频率应远低于内层——每 100-500 个 PPO step 更新一次 sigma。
💡 概念误区:认为"adaptive sigma 一定比固定 sigma 好"
对简单的动作(如行走、站立),固定 sigma 就足够了——不需要 BLO 的额外复杂度。BLO 的价值在于"异质难度"的动作——有些帧简单、有些帧困难的动作序列。如果所有帧难度相似,BLO 的 sigma 会收敛到一个接近固定值的状态——增加了计算但没有收益。
练习
- [实现题] 实现 KungfuBot 的 AdaptiveTrackingCurriculum 类。在一个简单的行走参考动作上测试:观察哪些关键帧的 sigma 收缩(简单帧)、哪些扩大(困难帧)。
- [分析题] KungfuBot 的 BLO 与 Ch19 的四阶段课程在"渐进式训练"的理念上有什么异同?
20.8 跌倒恢复集成 ⭐⭐
这一节解决什么问题:人形机器人在全身操作中不可避免会摔倒——如何集成跌倒恢复模块,让机器人摔倒后自动站起来继续工作?
HoST:跌倒恢复的标杆
HoST(Learning Humanoid Standing-up Control across Diverse Postures,Huang et al., RSS 2025 Best Systems Paper Finalist)是当前最完整的跌倒恢复框架——能让 G1 从各种摔倒姿态(面朝下、面朝上、侧躺)恢复到站立。
HoST 的核心工程贡献:
- Pulling-force curriculum(虚拟安全绳课程):在训练初期,给机器人躯干施加一个向上的外力(约 60% 体重),让策略在"减重"条件下学会站起来。随着训练进行,这个力逐步衰减到零——策略被迫用自己的关节力矩站起来。
# HoST 的虚拟安全绳课程
class VirtualHarnessCurriculum:
def __init__(self, initial_force_ratio=0.6, decay_type="linear"):
"""
initial_force_ratio: 初始辅助力占体重的比例
decay_type: "linear" / "exponential" / "adaptive"
"""
self.force_ratio = initial_force_ratio
self.decay_type = decay_type
self.decay_iterations = 2000 # 默认 2000 iter 衰减到零
def get_assist_force(self, robot_mass, gravity, iteration):
"""计算当前 iteration 的辅助力。"""
if self.decay_type == "linear":
progress = min(iteration / self.decay_iterations, 1.0)
ratio = self.force_ratio * (1.0 - progress)
elif self.decay_type == "exponential":
ratio = self.force_ratio * math.exp(-3.0 * iteration / self.decay_iterations)
else: # adaptive
# 根据站起成功率自适应衰减
ratio = self.force_ratio # 需要外部更新
return ratio * robot_mass * gravity # N,向上的力
这种"虚拟安全绳"思想在后续工作中被反复采用——例如 AGILE(arXiv:2603.20147)称之为"Virtual Harness",用同样的 PD root forces + 衰减策略。注意时间线:HoST(arXiv:2502.08378,2025-02,RSS 2025)早于 AGILE(2026-03),因此这一辅助训练手段不是 HoST 从 AGILE 借鉴的;HoST 自身使用 vertical pulling force curriculum,并将其应用到 stand-up 任务。
-
Multi-critic 架构:HoST 使用四个独立的 critic(每个 critic 负责一组 reward),独立优化。这比单一 critic 更稳定——因为 stand-up 的 reward 组件之间有冲突("快速站起来"vs"平滑站起来")。
-
Smoothness regularization:stand-up 动作如果不加约束会非常暴力——关节角速度极大、力矩冲击严重。HoST 加入了三个 smoothness 约束:action rate(动作变化率)、acceleration(关节加速度)、joint-torque-rate(力矩变化率)。这确保站起来的动作在真机上不会损坏电机。
策略切换管线:locomotion → fall detect → HoST → standing → resume
人形全身控制的完整策略切换管线:
┌──────────────────────────────────────┐
│ Main Locomotion + │
│ Whole-Body Control Policy │
│ (行走 + 操作,正常运行) │
└─────────┬────────────────────────────┘
│
▼ 检测到摔倒?
┌──────────────────────────┐
│ Fall Detector │
│ base_height < threshold │
│ OR pitch/roll > limit │
└─────────┬────────────────┘
│ Yes
▼
┌──────────────────────────┐
│ HoST Stand-Up Policy │
│ (从摔倒姿态恢复站立) │
└─────────┬────────────────┘
│ 站稳了?
▼ base_height > threshold
│ AND pitch/roll < limit
│ AND stable for N steps
┌──────────────────────────────────────┐
│ Resume Main Policy │
│ (切回正常 locomotion + 操作) │
└──────────────────────────────────────┘
# 策略切换的实现
class PolicySwitcher:
def __init__(self, main_policy, standup_policy,
fall_height=0.3, stable_height=0.5, stable_steps=50):
self.main_policy = main_policy
self.standup_policy = standup_policy
self.fall_height = fall_height
self.stable_height = stable_height
self.stable_steps = stable_steps
self.mode = "main"
self.stable_counter = 0
def step(self, obs):
base_height = obs["base_height"]
base_pitch = obs["base_pitch_abs"]
if self.mode == "main":
# 检测摔倒
if base_height < self.fall_height or base_pitch > 1.0:
self.mode = "standup"
self.stable_counter = 0
print("FALL DETECTED → switching to stand-up policy")
return self.main_policy(obs)
elif self.mode == "standup":
# 检测是否站稳
if base_height > self.stable_height and base_pitch < 0.3:
self.stable_counter += 1
else:
self.stable_counter = 0
if self.stable_counter > self.stable_steps:
self.mode = "main"
print("STANDING STABLE → switching back to main policy")
return self.main_policy(obs)
return self.standup_policy(obs)
状态缓存(State Caching)的工程优化:HoST 训练 stand-up 策略时,需要大量的"摔倒初始状态"。但让机器人从站立开始随机摔倒来生成初始状态太慢——因为大部分时间策略在"站着"而非"摔倒后恢复"。HoST 的解决方案:一次性 rollout 收集大量的"摔倒姿态"(各种角度、位置、速度),缓存起来作为 stand-up 训练的初始状态分布。这大幅减少了训练中"等待摔倒"的无效时间。
State Caching 的实现:
# HoST 的 State Caching 实现
class FallenStateCache:
"""收集和缓存各种摔倒姿态作为 stand-up 训练的初始状态。
工作流:
1. 在仿真中让机器人从各种初始条件下自由摔倒(无策略控制)
2. 收集摔倒后的稳定状态(不再运动)
3. 按姿态分类(面朝下/面朝上/侧躺/蜷缩)
4. 缓存到文件,训练时直接加载
"""
def __init__(self, num_envs=4096, settle_steps=100):
self.num_envs = num_envs
self.settle_steps = settle_steps # 等待摔倒稳定的步数
self.cache = {
"face_down": [],
"face_up": [],
"side_left": [],
"side_right": [],
"curled": [],
}
def collect(self, env, num_batches=100):
"""收集摔倒状态。"""
for batch in range(num_batches):
# 随机初始化——各种高度、角度、速度
env.reset_with_random_orientation(
height_range=(0.3, 0.7),
pitch_range=(-3.14, 3.14),
roll_range=(-3.14, 3.14),
vel_range=(-2.0, 2.0),
)
# 让机器人自由摔倒(无控制输入)
for _ in range(self.settle_steps):
zero_action = torch.zeros(self.num_envs, env.action_dim)
env.step(zero_action)
# 收集稳定后的状态
states = env.get_full_state() # [num_envs, state_dim]
# 按姿态分类
pitch = states[:, 3] # 假设 index 3 是 pitch
roll = states[:, 4]
for i in range(self.num_envs):
if pitch[i] > 1.0:
self.cache["face_down"].append(states[i])
elif pitch[i] < -1.0:
self.cache["face_up"].append(states[i])
elif roll[i] > 1.0:
self.cache["side_left"].append(states[i])
elif roll[i] < -1.0:
self.cache["side_right"].append(states[i])
else:
self.cache["curled"].append(states[i])
total = sum(len(v) for v in self.cache.values())
print(f"Collected {total} fallen states:")
for name, states in self.cache.items():
print(f" {name}: {len(states)}")
def sample(self, batch_size):
"""从缓存中采样一批初始状态。
均匀采样不同姿态类别——确保策略在所有姿态上都训练。
"""
all_states = []
for category_states in self.cache.values():
if category_states:
all_states.extend(category_states)
indices = torch.randint(0, len(all_states), (batch_size,))
return torch.stack([all_states[i] for i in indices])
def save(self, path):
torch.save(self.cache, path)
def load(self, path):
self.cache = torch.load(path)
HoST 的完整训练配置:
# HoST stand-up 训练的完整配置
class HoSTStandUpEnvCfg(ManagerBasedRLEnvCfg):
# 虚拟安全绳课程
virtual_harness = VirtualHarnessCfg(
initial_force_ratio=0.6, # 初始辅助力 = 60% 体重
decay_type="exponential", # 指数衰减
decay_iterations=2000, # 2000 iter 衰减到接近零
force_direction=[0, 0, 1], # 向上的力
)
# Multi-Critic 架构
critics = MultiCriticCfg(
groups={
"stand_up": {
"rewards": ["base_height", "base_orientation"],
"weight": 1.0,
},
"smoothness": {
"rewards": ["action_rate", "joint_acc", "torque_rate"],
"weight": 0.5,
},
"contact": {
"rewards": ["foot_contact", "hand_push"],
"weight": 0.3,
},
"stability": {
"rewards": ["final_standing"],
"weight": 2.0,
},
},
)
# Reward 配置
rewards = RewardsCfg(
terms={
# 核心目标:站起来
"base_height": HeightRewardCfg(
target=0.68, # G1 站立高度
sigma=0.1,
weight=2.0,
),
"base_orientation": OrientationRewardCfg(
target_pitch=0.0,
target_roll=0.0,
sigma=0.2,
weight=1.5,
),
"final_standing": FinalStandingRewardCfg(
height_threshold=0.6,
orientation_threshold=0.3,
duration=20, # 保持站立 20 步才算成功
bonus=10.0,
weight=1.0,
),
# 平滑约束
"action_rate": ActionRateRewardCfg(weight=-0.03),
"joint_acc": JointAccelerationRewardCfg(weight=-0.002),
"torque_rate": TorqueRateRewardCfg(weight=-0.001),
# 接触辅助
"foot_contact": FootContactRewardCfg(
target_bodies=["left_foot", "right_foot"],
weight=0.5,
),
"hand_push": HandPushRewardCfg(
# 鼓励用手撑地站起(面朝下时)
weight=0.3,
),
},
)
# 初始化——从 FallenStateCache 加载
initialization = InitializationCfg(
mode="cached",
cache_path="fallen_states_cache.pt",
# 或者 mode="random" 从随机姿态开始
)
# Early termination
termination = TerminationsCfg(
terms={
"time_limit": TimeLimitTerminationCfg(max_steps=300), # 6 秒
"standing_achieved": StandingTerminationCfg(
height=0.6, duration=50, success=True,
),
},
)
HoST 的 Multi-Critic 为什么比 Single Critic 更好?
Standard PPO 使用单一 critic \(V(s)\) 估计所有 reward 的总和。但 stand-up 任务的不同 reward 组件有不同的时间尺度:
base_height在 stand-up 的最后阶段才能获得大 reward(长时间尺度)action_rate在每一步都有信号(短时间尺度)smoothness的梯度方向可能与stand_up的梯度方向冲突
Single critic 需要同时预测这些不同时间尺度的 reward 的混合 value——如果某个组件的 variance 远大于其他组件(如 final_standing 的 10.0 bonus),critic 会被这个高 variance 组件主导,忽略低 variance 组件(如 action_rate 的 -0.03)。
Multi-critic 为每组 reward 训练独立的 critic——每个 critic 只需要预测自己负责的 reward 组件的 value,不受其他组件的 variance 干扰。PPO 的 advantage 是各 critic 的 advantage 的加权和——梯度方向更稳定。
用一个类比:想象你同时在跟踪三个目标(站起来、不震荡、脚要着地)。一个"全知"的评委(single critic)需要同时评估所有三个方面的总分——很难做到公平。三个"专项"评委(multi-critic)各自只评估自己的专项——更精确、更稳定。最终总评取加权平均。
⚠️ 常见陷阱
⚠️ 编程陷阱:main policy 和 standup policy 的 obs 维度不同
Main policy 的 obs 可能包含操作相关的信息(object position 等),stand-up policy 的 obs 不需要这些。切换时需要对 obs 做适配——从 main policy 的 obs 中提取 standup policy 需要的子集。
⚠️ 编程陷阱:切换回 main policy 时 action 突变
如果 standup policy 的最后一帧 action 和 main policy 的第一帧 action 差异很大(如一个关节从 0.5 rad 突变到 -0.2 rad),真机上会产生大的力矩冲击。解决方案:切换时对 action 做平滑插值(如 5-10 步内线性过渡)。
练习
- [设计题] 为 G1 设计一个 fall detector。应该检测哪些 obs?阈值应该设为多少?如何避免"误检测"(正常蹲下被判为摔倒)?
- [工程题] 实现一个 PolicySwitcher,集成 main policy 和 HoST stand-up policy。在仿真中推一把机器人使其摔倒,验证切换是否正确工作。
- [分析题] 虚拟安全绳课程的三种 decay 类型(linear/exponential/adaptive)各有什么优缺点?哪种更适合 stand-up 任务?
20.9 全身控制的工程参数表与诊断 ⭐⭐
这一节解决什么问题:人形全身控制的训练参数比四足复杂得多。如何系统性地设置 PD gain、action scale、reward 权重?常见的训练失败如何诊断?
PD Gain 分组推荐(G1 平台)
| 关节组 | kp | kd | 物理依据 |
|---|---|---|---|
| hip_roll | 150 | 5.0 | 支撑体重 + 侧向稳定 |
| hip_yaw | 150 | 5.0 | 转向力矩 |
| hip_pitch | 200 | 5.0 | 主要承重关节 |
| knee | 200 | 5.0 | 主要承重 + 吸收冲击 |
| ankle | 40 | 2.0 | 平衡微调(不能太硬) |
| waist_yaw | 200 | 5.0 | 躯干旋转 |
| shoulder_pitch | 80 | 2.0 | 上肢举物 |
| shoulder_roll | 80 | 2.0 | 上肢侧展 |
| shoulder_yaw | 60 | 1.5 | 上肢旋转 |
| elbow | 60 | 1.5 | 前臂控制 |
踝关节 kp 为什么特别小? 踝关节是双足平衡的"精细调节器"——它通过微小的角度变化来移动 CoP(Center of Pressure)在脚底的位置。如果 kp 太大(如 200),微小的位置偏差就产生大力矩——踝关节变成"刚性铰链",无法做出平衡所需的柔性调整。真机上踝关节的电机也通常比膝关节小——kp 设置应该反映真实硬件的能力。
Action Scale 推荐(G1 全身 23 DOF)
| 关节组 | scale (rad) | 每步最大偏移 | 调参方向 |
|---|---|---|---|
| hip_roll | 0.15 | ~9° | 步态太保守→增大 |
| hip_yaw | 0.10 | ~6° | 同上 |
| hip_pitch | 0.25 | ~14° | 步幅太小→增大 |
| knee | 0.25 | ~14° | 同上 |
| ankle_pitch | 0.10 | ~6° | 平衡不稳→减小 |
| ankle_roll | 0.08 | ~5° | 侧向平衡→减小 |
| waist_yaw | 0.15 | ~9° | 躯干旋转不够→增大 |
| shoulder_pitch | 0.15 | ~9° | 上肢不够灵活→增大 |
| shoulder_roll | 0.15 | ~9° | 同上 |
| shoulder_yaw | 0.10 | ~6° | 同上 |
| elbow | 0.10 | ~6° | 精度不够→减小 |
| wrist_roll | 0.10 | ~6° | 末端定向→按需 |
与 Ch19 四足的对比:人形的 hip_pitch/knee 的 scale(0.25 rad)与四足腿关节相同——因为行走步幅的物理需求类似。但人形新增了 ankle_pitch/ankle_roll(~0.08-0.10 rad,更精细)和 waist_yaw(0.15 rad,躯干旋转)。上肢 scale 与 Ch19 的臂关节类似(0.10-0.15 rad)。
跨平台 PD Gain 与 Action Scale 对比
以下对比表将 Ch13(四足 locomotion)、Ch17(固定基座操作)、Ch19(四足 loco-manip)和 Ch20(人形全身)的参数放在一起,帮助建立"不同形态 → 不同力矩需求"的直觉:
| 关节类型 | Go1 locomotion (Ch13) | YAM arm (Ch17) | Go2+ARX (Ch19) | G1 humanoid (Ch20) |
|---|---|---|---|---|
| 底盘/腿 hip kp | 40 | — | 40 | 150-200 |
| 底盘/腿 knee kp | 40 | — | 40 | 200 |
| 踝关节 kp | — | — | — | 40(精细平衡) |
| 臂 shoulder kp | — | 20 | 15 | 80 |
| 臂 elbow kp | — | 15 | 10 | 60 |
| 夹爪 kp | — | 5 | 5 | — |
| 腿 action scale | 0.25 rad | — | 0.25 rad | 0.15-0.25 rad |
| 臂 action scale | — | 0.1 rad | 0.10-0.15 rad | 0.10-0.15 rad |
| 总 DOF | 12 | 7-8 | 19 | 23(无手) |
关键观察: 1. 人形 hip kp 是四足的 4-5 倍——因为人形的两条腿需要支撑整个体重(35 kg),而四足的每条腿只承担 1/4 体重(3 kg)。 2. 人形新增踝关节 kp 特别小——踝关节的作用是平衡微调,太硬会丧失柔性。 3. 臂的 kp 和 scale 在所有平台上相似——因为上肢操作的精度需求不依赖于底盘形态。 4. 从四足到人形,training 难度指数增长——不仅 DOF 增加,PD gain 的调参空间也显著增大。
Reward 设计参考(ExBody2 风格)
| Reward 项 | 表达式 | 推荐权重 | 适用路线 |
|---|---|---|---|
| velocity_tracking | $\exp(- | v - v_{\text{cmd}} | |
| landmark_tracking | $\exp(- | p_i^{\text{local}} - p_i^{\text{ref}} | |
| joint_tracking | $\exp(- | q - q_{\text{ref}} | |
| contact_reward | \(\sum_i \text{clamp}(f_i^{\text{correct}}, 0, 100)\) | 2.0 | WoCoCo |
| stage_progress | stage_idx / total_stages | 5.0 | WoCoCo |
| base_height | $\exp(- | h - h_{\text{target}} | ^2 / 0.05^2)$ |
| orientation | $\exp(- | \text{projected_gravity} | |
| action_rate | $- | a_t - a_{t-1} | |
| joint_limit | $-\sum \text{clamp}( | q | - 0.9 q_{\max}, 0)$ |
| alive | 1.0(每步存活) | 0.5 | 通用 |
| foot_contact_force | $-\text{clamp}( | f_{\text{foot}} | |
| angular_momentum | $- | L_{\text{subtree}} |
人形全身控制的诊断层次
Layer 1:基础 locomotion(5 min)
□ 不加上肢任务——纯行走是否稳定(Ch14 基线)
→ 不稳定:回到 Ch14 检查 locomotion 策略
Layer 2:上肢固定的行走(10 min)
□ 上肢保持默认姿态(不动)——行走是否稳定
→ 不稳定:上肢默认姿态的 CoM 影响太大,调整 default_arm_pos
Layer 3:上肢活动的行走(训练中期)
□ 上肢开始执行参考动作——是否频繁摔倒
□ 如果摔倒,是因为上肢动作的角动量还是 CoM 偏移?
→ 角动量:增大 angular_momentum 惩罚
→ CoM 偏移:增大 base_height/orientation reward
Layer 4:task metric(训练后期)
□ 独立评估任务指标(tracking error, grasp success 等)
□ reward 涨但 task metric 不涨 → reward hacking
Layer 5:sim-to-real(部署时)
□ 关节命令在安全范围内
□ action rate 足够平滑
□ foot contact force 不超过硬件极限
典型失败模式
| 症状 | 可能原因 | 快速修复 |
|---|---|---|
| 站立不稳、持续晃动 | ankle kp 太大或太小 | 调整 ankle kp 到 30-50 |
| 行走时上身倾斜 | upper body reward 和 lower body reward 失衡 | 增大 orientation reward |
| 上肢动作时摔倒 | 角动量惩罚不足 | 增大 angular_momentum weight |
| tracking error 不收敛 | sigma 太小(对高动态动作) | 增大 sigma 或使用 KungfuBot BLO |
| 策略学到"鹤立鸡群"姿态 | standing reward 太强 | 减小 alive reward,增大 motion reward |
| 真机上关节震荡 | action_rate/smoothness 不足 | 增大 action_rate weight 到 0.05 |
| 真机上脚部撞击过大 | foot_contact_force 惩罚缺失 | 添加 contact force 限制 reward |
| MoCap 跟踪时策略放弃困难帧 | 固定 sigma 对困难帧不公平 | 切换到 KungfuBot BLO |
人形特有的训练注意事项
1. 初始化姿态极其重要。 四足的初始化只要四脚着地就行,但人形的初始化必须是一个平衡的站立姿态——膝关节微曲(~0.2 rad)、髋关节微屈(~-0.1 rad)、重心在双脚中间。如果初始化是完全伸直的站立,第一步仿真就会因为刚度不够而"软腿"摔倒。
# G1 推荐的默认站立姿态
default_joint_pos_g1 = {
# 下肢——微曲站立(每腿 6 DOF)
"left_hip_roll": 0.0,
"left_hip_yaw": 0.0,
"left_hip_pitch": -0.15, # 微屈
"left_knee": 0.3, # 膝关节弯曲
"left_ankle_pitch": -0.15, # 踝关节补偿
"left_ankle_roll": 0.0, # 侧向保持水平
"right_hip_roll": 0.0,
"right_hip_yaw": 0.0,
"right_hip_pitch": -0.15,
"right_knee": 0.3,
"right_ankle_pitch": -0.15,
"right_ankle_roll": 0.0,
# 上肢——自然下垂(每臂 5 DOF)
"left_shoulder_pitch": 0.0,
"left_shoulder_roll": 0.2, # 微外展
"left_shoulder_yaw": 0.0,
"left_elbow": 0.3, # 微曲
"left_wrist_roll": 0.0,
"right_shoulder_pitch": 0.0,
"right_shoulder_roll": -0.2,
"right_shoulder_yaw": 0.0,
"right_elbow": 0.3,
"right_wrist_roll": 0.0,
# 躯干
"waist_yaw": 0.0,
} # 合计 12 + 10 + 1 = 23 DOF
2. 早期终止条件比四足更严格。 四足的 early termination 通常是"base 高度 < 0.2m"或"pitch > 60°"。人形需要更敏感的终止条件——"base 高度 < 0.4m"或"pitch > 45°"——因为一旦人形开始倾斜,恢复比四足难得多(只有两个支撑点)。
# 人形全身控制的 early termination 配置
termination_cfg = TerminationsCfg(
terms={
"base_height_low": HeightTerminationCfg(
threshold=0.4, # m(比四足的 0.2 高很多)
below=True,
),
"base_pitch_large": OrientationTerminationCfg(
threshold=0.78, # rad(~45°,比四足的 60° 小)
),
"base_roll_large": OrientationTerminationCfg(
threshold=0.78,
),
},
)
3. 对称性利用可以加速收敛。 人形是左右对称的——左腿的步态应该和右腿镜像。可以利用这个对称性做 data augmentation——对每个 rollout,生成一个左右镜像的"虚拟 rollout",PPO 同时在两个方向上学习。AGILE 提供了 config-driven 的对称性增强:
# 对称性增强配置
symmetry_cfg = SymmetryAugmentationCfg(
enabled=True,
mirror_pairs={
"left_hip_roll": "right_hip_roll",
"left_hip_pitch": "right_hip_pitch",
"left_knee": "right_knee",
"left_ankle": "right_ankle",
"left_shoulder_pitch": "right_shoulder_pitch",
"left_shoulder_roll": "right_shoulder_roll",
"left_elbow": "right_elbow",
},
mirror_obs_dims={
"base_ang_vel_y": -1, # y 轴角速度取反
"projected_gravity_y": -1,
},
)
对称性增强通常能把收敛速度提高 20-40%——相当于"免费"获得了 2\(\times\) 的训练数据。
⚠️ 常见陷阱
⚠️ 编程陷阱:对称性增强的 obs 镜像不完整
对称性增强不仅要镜像关节角度(交换左右),还要镜像 obs 中与方向相关的项——如 base_ang_vel 的 y 分量、projected_gravity 的 y 分量、velocity command 的 vy 分量。如果 obs 镜像不完整,"虚拟 rollout"的 obs-action 对是不一致的——PPO 会在这些噪声数据上训练,反而降低性能。
练习
- [配置题] 为 G1 配置完整的 23 DOF action space,包括每个关节的 scale。写出 mjlab 的 ActionCfg。
- [诊断题] 你的人形全身控制策略在训练 5000 iter 后仍然频繁摔倒。按诊断层次逐层排查——你首先应该检查什么?
本章小结
| 知识点 | 核心结论 | 重要程度 |
|---|---|---|
| 核心挑战 | DOF 爆炸 + 上下肢耦合 + 接触序列——比四足难一个数量级 | ⭐⭐⭐ |
| 支撑面积对比 | 人形 ~200 cm² vs 四足 ~800 cm²,CoM 余量小 3\(\times\) | ⭐⭐ |
| 动态稳定性 | 人形行走是动态稳定(swing phase 单脚)而非静态稳定 | ⭐⭐ |
| 三条技术路线 | ExBody2(解耦)、WoCoCo(接触分解)、HOVER(mask 蒸馏) | ⭐⭐⭐ |
| 路线选择决策树 | 上半身跟踪→ExBody2;接触丰富→WoCoCo;多模态→HOVER | ⭐⭐⭐ |
| HumanoidBench | 27-task benchmark,分层 RL 在 manipulation 上优于端到端 RL | ⭐⭐ |
| 分层 RL 实现 | 低层 locomotion(50Hz) + 高层 task(10Hz),低层冻结训练高层 | ⭐⭐⭐ |
| WoCoCo contact stage | 按接触模式分解任务,stage-aware reward,task-agnostic 模板 | ⭐⭐⭐ |
| WoCoCo DR 课程 | 无 DR→开 DR+平滑→严格正则化,三阶段渐进 | ⭐⭐ |
| WoCoCo active_stages | reward 项可指定在哪些 stage 激活,比乘法门控更灵活 | ⭐⭐ |
| ExBody2 velocity-landmark 解耦 | 局部坐标系 landmark tracking + 独立 velocity tracking | ⭐⭐⭐ |
| ExBody2 periodic local frame | 每 30 帧重置局部坐标系原点,消除 drift 但保留短期位移跟踪 | ⭐⭐⭐ |
| ExBody2 teacher 过滤 | 用 privileged teacher 评估自动删除不可行动作 | ⭐⭐⭐ |
| ExBody2 specialist fine-tuning | generalist→specialist 的 versatility-fidelity trade-off | ⭐⭐ |
| KungfuBot BLO | per-keyframe adaptive sigma,困难帧放松、简单帧收紧 | ⭐⭐⭐ |
| KungfuBot physical filter | CoM-CoP proximity + contact-mask agreement + joint-limit check | ⭐⭐ |
| HOVER mask mechanism | command mask 选择性遮蔽 student 输入,切换控制模态 | ⭐⭐⭐ |
| HOVER DAgger | oracle 全状态标注 + student masked-obs 执行 + MSE 蒸馏 | ⭐⭐ |
| HoST 跌倒恢复 | 虚拟安全绳课程 + multi-critic + smoothness regularization | ⭐⭐⭐ |
| HoST state caching | 预收集摔倒姿态缓存,加速 stand-up 训练 | ⭐⭐ |
| HoST multi-critic | 四组 reward 独立 critic,消除不同时间尺度的 variance 干扰 | ⭐⭐⭐ |
| 策略切换管线 | locomotion→fall detect→stand-up→resume,action 平滑过渡 | ⭐⭐ |
| PD gain 分组 | 踝关节 kp 30-50(平衡微调),膝/髋 kp 150-200(承重) | ⭐⭐⭐ |
| 对称性增强 | 镜像 obs+action,收敛加速 20-40%,需完整镜像方向相关 obs | ⭐⭐ |
| 默认站立姿态 | 膝曲 0.3 rad + 髋屈 -0.15 rad,不能完全伸直 | ⭐⭐ |
| Early termination | 人形比四足更严格(高度 0.4m vs 0.2m,角度 45° vs 60°) | ⭐⭐ |
| MoCap retargeting | AMASS→SMPL→G1 关节映射,骨骼长度不匹配需缩放 | ⭐⭐ |
本章建立的三个核心心智模型
模型一:双足的脆弱性。 人形全身控制的所有工程决策都源于一个物理事实——双足的支撑面积极小(~200 cm²),任何上肢动作都可能打破平衡。这不是可以通过"调 reward 权重"解决的——而是需要从架构层面(解耦/分层/接触分解)处理的根本约束。四足有四条腿的"安全网",人形没有——这是质的区别,不是量的区别。
模型二:任务驱动的路线选择。 三条技术路线不是"好坏"排序,而是"适用场景"不同。上半身跟踪参考动作(ExBody2 最简单且 sim-to-real 最可靠)、接触丰富的操作(WoCoCo 最通用且可端到端训练)、多模态控制(HOVER 最灵活且一个模型服务多需求)——选择应基于你的任务特性,而非论文的发表年份或引用次数。三条路线也可以组合使用——ExBody2 的 reward 设计 + WoCoCo 的接触阶段 + HOVER 的蒸馏模板。
模型三:渐进式训练的普遍性。 从 Ch19 的四阶段课程到 WoCoCo 的三阶段 DR 课程,从 KungfuBot 的 adaptive sigma 到 HoST 的虚拟安全绳——"先简单后复杂、先确定后随机、先辅助后独立"的渐进式训练哲学贯穿了所有成功的人形全身控制方法。这不是巧合——而是因为人形全身控制的探索空间极大(\(10^{23+}\)),直接在完整问题上训练几乎不可能收敛。渐进式训练通过"在每一步缩小搜索空间"来使问题可解——先固定一些维度(冻结臂/冻结底盘/用安全绳),在缩小的空间中找到好的解,再逐步放开约束。
累积项目:本章新增模块
累积项目 F 在本章增加"人形全身控制"模块。你应该能够:
- 在 mjlab 或 Isaac Lab 中配置 G1 的 23 DOF 全身控制环境
- 实现 ExBody2 风格的上下肢解耦 reward
- 在简单的 MoCap 参考动作上训练 tracking 策略,tracking error 达标
- 集成 HoST 风格的跌倒恢复模块
- 对比 ExBody2 和 WoCoCo 两种路线在"搬箱子"任务上的性能
完成标准:
| 验收项 | 预期结果 | 验证方法 |
|---|---|---|
| G1 模型加载 | 23 DOF 正确,默认姿态站立稳定 | 可视化 + zero play |
| 纯 locomotion | velocity tracking error < Ch14 基线 + 10% | TensorBoard |
| 上肢 tracking | MPJPE < 5 cm(简单动作如挥手) | 评估脚本 |
| 跌倒恢复 | 从侧躺恢复到站立 success rate > 70% | HoST 评估 |
| 策略切换 | 推倒后自动恢复并继续行走 | 仿真演示 |
推荐工程执行顺序(4 周):
Week 1: 基础搭建
Day 1-2: 配置 G1 环境(23 DOF actuator 分组 + default pose)
Day 3-5: 训练纯 locomotion velocity tracking(Ch14 复习)
确认行走稳定后进入 Week 2
Week 2: 上肢 tracking
Day 1-2: 准备 MoCap 数据(AMASS → retarget → G1)
Day 3-5: 实现 ExBody2 的 velocity-landmark 解耦 reward
训练上半身 tracking(挥手/跳舞简单动作)
Week 3: 全身操作
Day 1-3: 实现 WoCoCo contact stage 框架(搬箱子任务)
Day 4-5: 训练 + 调参
Week 4: 跌倒恢复 + 集成
Day 1-2: 训练 HoST stand-up policy
Day 3-4: 实现 PolicySwitcher
Day 5: 端到端演示(行走→操作→摔倒→恢复→继续)
常见的 Week 1 困难及解决方案:
| 困难 | 典型原因 | 快速解决 |
|---|---|---|
| G1 模型加载后腿软摔倒 | default_joint_pos 设置不对 | 膝曲 0.3 rad + 髋屈 -0.15 rad |
| 行走一步后摔倒 | ankle kp 太大(踝关节太硬) | 减小到 30-50 |
| 行走速度跟踪不准 | hip_pitch action scale 太小 | 增大到 0.25 rad |
| 训练 5k iter 仍不收敛 | MLP 太小 / lr 太高 | MLP [256,256,128], lr 3e-4 |
| 对称性增强后变差 | obs 镜像不完整 | 打印镜像前后 obs 对比 |
常见的 Week 2 困难及解决方案:
| 困难 | 典型原因 | 快速解决 |
|---|---|---|
| MoCap retarget 后关节超限 | SMPL→G1 映射不含 clamp | 加 np.clip 到关节限位 |
| 上半身 tracking 上半身不动 | landmark reward weight 太低 | 增大到 1.5-2.0 |
| 上半身动时下半身崩溃 | velocity-landmark 耦合(用了世界坐标系) | 切换到局部坐标系 + periodic reset |
| AMASS 数据加载报错 | 数据格式不匹配 | 检查 poselib YAML 格式 |
如果 Week 3 Day 3 后搬箱子 success rate 为零:按以下步骤排查:(1) 检查 WoCoCo 的 stage_progress 是否 > 0——如果为 0 说明策略卡在 Stage 0(接近阶段),减小 sigma_nav 或放松 exit_condition。(2) 如果 stage_progress > 0 但 grasp 为 0,检查 contact_reward 的"正确接触"定义是否太严格。(3) 如果前两项正常但搬运时掉落,增大 action_rate weight 使搬运动作更平滑。
延伸阅读
| 资料 | 难度 | 推荐原因 |
|---|---|---|
| Ji et al. 2024, "ExBody2: Advanced Expressive Humanoid Whole-Body Control" (arXiv:2412.13196) | ⭐⭐⭐ | 上下肢解耦的最新改进,velocity-landmark 解耦 + teacher 过滤 |
| Cheng et al. 2024, "ExBody: Expressive Whole-Body Control" (RSS'24) | ⭐⭐⭐ | ExBody2 的原始版本,代码开源 |
| Zhang et al. 2024, "WoCoCo: Learning Whole-Body Humanoid Control with Sequential Contacts" (CoRL) | ⭐⭐⭐⭐ | 接触阶段分解的标杆工作,4 个真机任务验证 |
| KungfuBot (NeurIPS'25) | ⭐⭐⭐ | Adaptive tracking curriculum,高动态动作 |
| Sferrazza et al. 2024, "HumanoidBench" (RSS) | ⭐⭐ | 27-task benchmark,分层 vs 端到端的实证对比 |
| Huang et al. 2025, "HoST" (RSS Best Systems Paper Finalist) | ⭐⭐⭐ | 跌倒恢复的标杆,虚拟安全绳课程 |
| He et al. 2024, "HOVER" (NVIDIA, arXiv:2410.21229) | ⭐⭐⭐ | Isaac Lab 扩展模板,mask-conditioned 多模态控制 |
| Fu et al. 2024, "HumanPlus" (CoRL'24) | ⭐⭐⭐ | 遥操作 + 模仿学习,33-DOF H1 + 灵巧手 |
| AMO (RSS'25) | ⭐⭐⭐ | Adaptive Motion Optimization,超灵巧手全身控制 |
| AGILE (arXiv:2603.20147) | ⭐⭐⭐ | 四阶段工作流 + 算法工具箱,含 Virtual Harness 训练辅助 |
| HOMIE (RSS'25) | ⭐⭐ | 半自主遥操作系统,RL locomotion + 外骨骼手臂 + 手套手指 |
| LeVERB (arXiv:2506.13751) | ⭐⭐⭐⭐ | Vision-Language 全身控制,前沿方向 |
| BeyondMimic (whole_body_tracking) | ⭐⭐ | mjlab 的 motion tracking 参考实现 |
阅读顺序建议:先读 ExBody(理解上下肢解耦的基本架构),再读 ExBody2(理解改进),然后读 WoCoCo(理解接触阶段分解的通用性),最后读 HOVER(理解 mask-conditioned 多模态的工程模板)。KungfuBot 和 HoST 作为具体技巧的补充参考。HumanoidBench 作为评估基准。
ExBody2 论文精读建议:重点关注 Section III-B(velocity-landmark 解耦的具体实现——如何在局部坐标系中计算 landmark error,以及 periodic local frame 的 reset interval 如何选择)和 Section IV(generalist→specialist fine-tuning 的 trade-off 分析——Figure 5 展示了不同 fine-tuning 数据量对 versatility 和 fidelity 的影响)。代码中 legged_gym/envs/ 的 reward 函数实现是理解 velocity-landmark 解耦的最直接途径。
WoCoCo 论文精读建议:重点关注 Section 3(contact stage 的形式化定义——如何从任务描述中提取接触计划)和 Section 4(四个示范任务的 reward 配置——每个任务只需要 1-2 个 task-specific term,其余全是 task-agnostic 的 contact reward)。特别注意 Table 1 中四个任务的接触阶段数量和 exit condition——这是你设计新任务时的直接参考模板。
KungfuBot 论文精读建议:重点关注 Section 3.2(Adaptive Motion Tracking——BLO 的数学推导和实现细节,即外层梯度的估计方法)和 Section 3.1(Motion Processing Pipeline——physics-based motion filter 的物理指标定义)。Figure 6 展示了 filter 的效果——被接受的动作在仿真中的 Episode Length Ratio 显著高于被拒绝的动作,证明了 filter 的有效性。
HoST 论文精读建议:重点关注 Section III(虚拟安全绳的三种 decay 类型的消融实验——exponential decay 在大多数姿态上表现最好)和 Section IV(multi-critic 架构的设计——四组 reward 如何分配到不同 critic)。部署相关的 smoothness regularization 细节在 Section V——这些参数值可以直接用于你的 sim-to-real。
HOVER 论文精读建议:重点关注 Section 3(oracle→student 的 DAgger 蒸馏流程——mask 如何在 DAgger 中使用)和 Section 4(specialist vs generalist 的性能对比)。代码中 neural_wbc/core/mask.py(配合 modes.py)是 mask 机制的核心实现。注意 HOVER 依赖 Isaac Lab v2.0.0——版本不匹配是最常见的安装问题。
如果时间有限只读一篇:对于上肢 tracking 任务读 ExBody/ExBody2——它是最简单、最可复现的方案。对于全身接触任务读 WoCoCo——它的 task-agnostic reward 模板可以快速适配新任务。对于多模态部署读 HOVER——它的 Isaac Lab 扩展模板是生产级的工程参考。
故障排查手册
| 症状 | 可能原因 | 排查步骤 | 相关章节 |
|---|---|---|---|
| 站立不稳、持续晃动 | ankle kp 不对 | 1. 调整 ankle kp 到 30-50 2. 检查 default_joint_pos | 20.9 |
| 一开始就摔倒 | default_joint_pos 腿伸直 | 1. 设置膝曲 0.3 rad + 髋屈 -0.15 rad 2. 检查初始化 | 20.9 |
| 行走时上身倾斜 | orientation reward 太弱 | 1. 增大 orientation weight 2. 检查上肢默认姿态的 CoM 影响 | 20.6 |
| 上肢动作时摔倒 | 角动量惩罚不足 | 1. 增大 angular_momentum weight 2. 减小 arm action scale | 20.1 |
| tracking error 不收敛 | sigma 太小 / 参考动作不可行 | 1. 增大 sigma 或用 BLO 2. 检查 teacher 过滤结果 | 20.6, 20.7 |
| WoCoCo stage 不推进 | exit_condition 太严格 | 1. 放松 exit condition 2. 检查 contact reward 是否给了正信号 | 20.5 |
| ExBody2 上半身不动 | landmark reward weight 太低 | 1. 增大 landmark weight 2. 检查参考动作是否正确加载 | 20.6 |
| ExBody2 下半身步态崩溃 | velocity tracking 与 landmark tracking 耦合 | 1. 确认在局部坐标系计算 landmark 2. 检查 periodic reset interval | 20.6 |
| HOVER teacher 不收敛 | num_envs 太少 | 1. 增大到 4096 2. 检查参考动作长度是否够 | 20.3 |
| HOVER student 各模态性能不均 | generalist 训练中某些 mask 出现频率低 | 1. 均匀采样 mask modes 2. 增大少见模态的采样权重 | 20.2 |
| 策略切换时 action 突变 | 切换时未做 action 平滑 | 1. 加 5-10 步线性过渡 2. 检查两个策略的 action range | 20.8 |
| 跌倒恢复策略不站起 | 虚拟安全绳衰减太快 | 1. 增大 decay_iterations 2. 检查初始力比例 | 20.8 |
| 跌倒恢复只从面朝上恢复 | FallenStateCache 姿态分布不均 | 1. 检查缓存中各姿态的比例 2. 增加稀少姿态的采样 | 20.8 |
| 对称性增强后性能下降 | obs 镜像不完整 | 1. 检查所有方向相关 obs 是否正确镜像 2. 用小 num_envs 对比有/无对称 | 20.9 |
| BLO sigma 全部收敛到 max | 参考动作整体不可行 / MLP 容量不足 | 1. 检查 physical filter 是否充分 2. 增大 MLP 到 [512,256,128] | 20.7 |
| BLO sigma 全部收敛到 min | 参考动作太简单 / 不需要 BLO | 1. 直接用固定 sigma 2. 尝试更难的参考动作 | 20.7 |
| 分层 RL 高层不学 | 低层 locomotion 不响应高层命令 | 1. 检查低层是否接收了高层的 vel cmd 2. 确认 obs 拼接正确 | 20.4 |
| MoCap retarget 后关节角度超限 | 骨骼映射不对 / 关节限位未裁剪 | 1. 打印 retarget 后的关节角度范围 2. 加 clamp 到关节限位 | 20.6 |
| 真机上行走震荡 | action_rate/smoothness 不足 | 1. 增大 action_rate weight 到 0.05 2. 增大 foot_contact_force 限制 | 20.9 |
| 真机上脚底冲击过大 | 训练中无 contact force 惩罚 | 1. 添加 foot_contact_force reward 2. 限制落地速度 | 20.9 |
| HumanoidBench 上 PPO 不收敛 | 端到端训练 101-DOF 太难 | 1. 切换到分层 RL 2. 使用预训练的低层策略 | 20.4 |
| Isaac Lab HOVER 安装报错 | rsl_rl 包名变更 | 1. 使用 Isaac Lab v2.0.0 2. 手动修改 import rsl_rl → rsl_rl_lib | 20.3 |
| 训练 20k iter 后 tracking error 停滞 | 探索不足 / lr 太小 | 1. 增大 entropy_coeff 到 0.01 2. 检查 lr 是否衰减过快 | 20.9 |
症状→路线选择的快速决策
| 你想做什么 | 推荐路线 | 第一个应该配置的东西 |
|---|---|---|
| 让机器人跟着 MoCap 跳舞 | ExBody2 | MoCap retarget + velocity-landmark reward |
| 让机器人搬箱子走到指定位置 | WoCoCo | 接触阶段计划(4 stages) |
| 让机器人既能导航又能遥操作 | HOVER | Oracle teacher + mask modes |
| 让机器人做高难度功夫动作 | KungfuBot BLO | Physical motion filter + adaptive sigma |
| 让机器人摔倒后自动站起 | HoST | FallenStateCache + 虚拟安全绳 |
| 评估策略在标准任务上的水平 | HumanoidBench | 安装 benchmark + 运行评估脚本 |
写在最后:本章将人形机器人从"会走路"(Ch14)提升到"边走边干活"——这是人形机器人从实验室走向实际应用的关键一步。三条技术路线(ExBody2 解耦、WoCoCo 接触分解、HOVER 多模态)各有适用场景,掌握了它们的工程核心后,你就能根据具体任务需求选择最合适的方案。
跌倒恢复(HoST)是一个经常被忽视但极其重要的工程模块——真实世界中的人形机器人不可避免会摔倒,能自动站起来是基本的安全要求。策略切换管线(locomotion → fall detect → stand-up → resume)是将实验室策略变成可部署系统的关键基础设施。
本章与后续章节的联系:
向后回顾:Ch14 的 velocity tracking 策略在本章成为 ExBody2 下半身控制和分层 RL 低层策略的基础。Ch15 的动作模仿技术在 ExBody2 和 KungfuBot 的 MoCap tracking 中被直接复用。Ch19 的多目标 reward 融合和四阶段课程在 WoCoCo 和 HoST 中以不同形式出现。
向前预告:Ch21(轮式底盘 + 双臂)将把全身控制的概念应用到另一种形态——底盘从双足变成轮式,取消了平衡约束但引入了非完整约束(differential drive 不能侧移)。本章建立的 reward 融合方法、接触阶段分解和 teacher-student 蒸馏管线将直接迁移。Ch22(DIY 自定义机器人)将提供从零搭建任意形态机器人的完整指南——包括如何为新形态选择合适的全身控制路线。Ch23(Sim-to-Real)将深入本章 §20.8 提到的 action smoothness、contact force 限制和 HoST 部署细节。
一个总结性的工程经验法则:人形全身控制的工程核心不是"让机器人做出炫酷的动作"——而是"在做动作的同时不摔倒"。所有成功的方法都在某种程度上处理了这个约束:ExBody2 通过解耦让下肢专注于稳定,WoCoCo 通过接触阶段确保每一步都有足够的支撑,HOVER 通过 oracle teacher 在 privileged 信息下学会稳定后再蒸馏。在开始任何人形全身控制的训练之前,完成以下"稳定性健康检查":
text □ 默认姿态:膝曲 0.3 rad + 髋屈 -0.15 rad,zero play 站立 > 5 秒 □ 纯 locomotion:velocity tracking error < Ch14 基线 + 10% □ 上肢默认姿态 CoM:打印 CoM 偏移,确认 < 支撑余量的 30% □ PD gain:踝关节 kp 30-50,膝/髋 kp 150-200,肩/肘 kp 60-80 □ Early termination:base_height < 0.4m 或 pitch > 45° 立即终止 □ Action scale:下肢 0.15-0.25 rad,上肢 0.10-0.15 rad □ Regularization:action_rate + angular_momentum + foot_contact_force 全部开启这 7 项检查能在训练开始前排除 80% 的人形全身控制配置错误。做了这些检查后再开始加入上肢操作或 MoCap tracking——这是本章最重要的工程建议。