Skip to content

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 题 → 先回前置章节复习

  1. [Ch14] G1 人形的 DOF 地图是什么?哪些关节属于下肢(locomotion),哪些属于上肢(manipulation)?
  2. [Ch14] 双足行走与四足行走在稳定性上的根本区别是什么?ZMP(Zero Moment Point)的物理含义是什么?
  3. [Ch15] 动作模仿的 tracking reward 使用什么距离度量?关节角度 tracking 和 keypoint tracking 各有什么优缺点?
  4. [Ch19] 四阶段课程(arm-constrained curriculum)的每个阶段解决什么问题?hot-start 技巧如何加速训练?
  5. [Ch19] 多目标 reward 的乘法门控编码了什么因果关系?什么时候该用乘法门控、什么时候该用加权组合?
  6. [RL 基础] Teacher-student 蒸馏中,DAgger 与 offline BC 的核心区别是什么?为什么人形全身控制通常需要 DAgger?
  7. [物理] 当人形机器人双脚站立、单手举起 2 kg 物体时,CoM 偏移对 ZMP 有什么影响?底盘(下肢)需要如何补偿?

本章目标

学完本章后,你应该能够:

  1. 分析人形全身操作的核心挑战——DOF 爆炸(29+ 关节)、上下肢动力学耦合、接触丰富的长时程任务
  2. 对比三条技术路线——上下肢解耦(ExBody)、接触阶段分解(WoCoCo)、mask-conditioned 多模态(HOVER)的工程优劣
  3. 精读 ExBody2 的 velocity-landmark 解耦 tracking、teacher-data 自动过滤和 specialist fine-tuning
  4. 精读 WoCoCo 的 contact stage 定义、stage-aware reward 和 three-stage DR 课程
  5. 精读 KungfuBot 的 physical motion filter 和 bi-level adaptive tracking
  6. 理解 HumanoidBench 的 27-task 评估体系,知道分层 RL 在哪些任务上优于端到端 RL
  7. 集成跌倒恢复模块(HoST)到 locomotion + manipulation 策略切换管线
  8. 配置 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 数量。

练习

  1. [计算题] G1 的总质量约 35 kg,双臂质量各约 3 kg,臂长约 0.5 m。当双臂同时向前伸出(平举)时,CoM 的前向偏移是多少?如果 G1 站立时 CoM 到前脚趾的距离约 8 cm,双臂平举后 CoM 是否仍在支撑面积内?
  2. [分析题] 为什么 HumanoidBench 中分层 RL 比端到端 RL 表现好?从探索效率和信用分配(credit assignment)两个角度分析。
  3. [设计题] 列出 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):

  1. 解耦 velocity tracking 与 landmark tracking:ExBody 中 velocity tracking 和 body landmark tracking 耦合在一起——如果参考动作中包含大幅度的根节点移动(如跑步),velocity tracking 会与 landmark tracking 冲突("是跟着跑还是保持姿态?")。ExBody2 将两者完全分离——velocity tracking 独立控制根节点移动,landmark tracking 在局部坐标系中跟踪身体关键点。这消除了 drift-pose 耦合问题。

  2. Teacher-data 自动过滤:人类 MoCap 数据中包含大量机器人物理上无法执行的动作(如后空翻、劈叉)。ExBody 使用语言标签手动过滤("dance" → 保留,"backflip" → 删除),但标签不精确。ExBody2 训练一个 privileged teacher,凡是 teacher 执行不了的动作(reward 低于阈值或 early termination)自动过滤——这比人工过滤更可靠。

  3. 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 的三个核心设计

  1. Contact stage 定义:每个 stage 由一组"期望接触"定义——哪些身体部位应该与环境/物体有接触力。这个定义是任务无关的(task-agnostic)——对任何涉及接触序列的任务,只需要指定"什么时候哪里应该有接触"。

  2. Stage-aware reward:每个 stage 有自己的 dense reward。WoCoCo 的 reward 由三部分组成:

  3. Contact reward:鼓励正确的接触模式(如 Stage 1 中鼓励双手接触箱子)
  4. Stage-count reward:鼓励策略推进到下一个 stage
  5. Curiosity reward(可选):鼓励探索新的状态(防止策略卡在某个 stage)

  6. Three-stage DR 课程

  7. (i) 无 DR,先学会接触序列
  8. (ii) 开启 DR + 增大 smoothness weight
  9. (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。

练习

  1. [分析题] 对比 ExBody2 和 WoCoCo 在以下任务上的适用性:(a) 人形机器人跳舞(上半身跟踪 MoCap),(b) 人形机器人搬箱子走到指定位置,(c) 人形机器人攀岩。给出每个任务选择哪条路线及原因。
  2. [设计题] 如果你要设计一个"人形机器人在办公室中巡逻 + 发现异常物体时拾取"的系统,需要结合哪些路线的哪些元素?
  3. [跨章综合题] 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_rlrsl_rl_lib,直接安装会报错。解决方案:按 HOVER README 的版本要求安装,或手动修改 import 路径。

⚠️ 编程陷阱:人形的 default_joint_pos 设置不当

如果 default_joint_pos 是完全伸直(所有关节角度=0),人形一开始就会"直挺挺地"摔倒——因为膝关节没有弯曲来吸收冲击。正确的 default_joint_pos 应该是一个微曲的站立姿态(膝关节 ~0.2 rad、髋关节 ~-0.1 rad)。

练习

  1. [工程题] 在 Isaac Lab 中加载 G1 USD 模型,配置全身 23 DOF 的 actuator 分组(下肢/上肢/躯干),运行 zero agent 100 步确认站立稳定。
  2. [对比题] 使用 HOVER 模板,分别用 1024 和 4096 num_envs 训练 Oracle Teacher 5000 iteration。比较训练吞吐和 reward 收敛速度。
  3. [迁移题] 列出将 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 的分层架构提供了实证支持。

为什么分层比端到端好? 两个原因:

  1. 探索效率:61D 动作空间中,随机探索几乎 100% 导致摔倒。分层中低层策略已经学会了稳定行走——高层策略在"已经会走路的机器人"上探索操作任务,不用担心摔倒。

  2. 信用分配:端到端策略在摔倒后得到大的负 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 配置。实际实现时需要将每个字段映射到对应框架的 ObservationTermCfgRewardTermCfg 等 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。

练习

  1. [调研题] 从 HumanoidBench 的 27 个任务中,选出 5 个不需要灵巧手的任务。对每个任务,列出核心的 obs term 和 reward term。
  2. [分析题] 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 自动学习。

练习

  1. [设计题] 为"人形机器人开门"任务设计 WoCoCo 的 contact stage 计划。列出每个阶段的名称、expected_contacts 和 exit_condition。
  2. [实验题] 使用 WoCoCo 的 three-stage DR 课程训练一个简单任务。比较 Phase 1 结束和 Phase 3 结束的成功率。
  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 的三个常见问题

  1. 骨骼长度不匹配:人体手臂长约 60cm,G1 手臂长约 50cm。如果只映射关节角度而不调整运动幅度,G1 的末端轨迹会比人体的短 ~17%。解决方案:对 target landmark 位置做缩放——而非对关节角度缩放。ExBody2 的 landmark tracking 在局部坐标系中直接跟踪位置,自动处理了这个问题。

  2. 自由度缺失:人体有 hip_roll, hip_yaw, hip_pitch 三个髋关节自由度,但某些人形机器人可能只有 hip_pitch + hip_roll(没有 hip_yaw)。缺失的 DOF 对应的参考动作分量会被丢弃——策略只能在可用的 DOF 上尽可能接近参考动作。KungfuBot 的 adaptive sigma 正好处理了这种"部分不可能"的情况——在缺失 DOF 导致的误差上自动放大 sigma。

  3. 帧率不匹配: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% 的动作数据。

练习

  1. [实现题] 写出 ExBody2 的 landmark tracking reward 函数(在局部坐标系中)。需要跟踪哪些关键点?sigma 应该设为多少?
  2. [分析题] ExBody2 的 generalist → specialist fine-tuning 与 transfer learning 中的"预训练→微调"有什么异同?
  3. [跨章综合题] 对比 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):

\[r_{\text{tracking}} = \exp\left(-\frac{||q_{\text{robot}} - q_{\text{ref}}||^2}{\sigma^2}\right)\]

如果 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 会收敛到一个接近固定值的状态——增加了计算但没有收益。

练习

  1. [实现题] 实现 KungfuBot 的 AdaptiveTrackingCurriculum 类。在一个简单的行走参考动作上测试:观察哪些关键帧的 sigma 收缩(简单帧)、哪些扩大(困难帧)。
  2. [分析题] 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 的核心工程贡献

  1. 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 任务。

  1. Multi-critic 架构:HoST 使用四个独立的 critic(每个 critic 负责一组 reward),独立优化。这比单一 critic 更稳定——因为 stand-up 的 reward 组件之间有冲突("快速站起来"vs"平滑站起来")。

  2. 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 步内线性过渡)。

练习

  1. [设计题] 为 G1 设计一个 fall detector。应该检测哪些 obs?阈值应该设为多少?如何避免"误检测"(正常蹲下被判为摔倒)?
  2. [工程题] 实现一个 PolicySwitcher,集成 main policy 和 HoST stand-up policy。在仿真中推一把机器人使其摔倒,验证切换是否正确工作。
  3. [分析题] 虚拟安全绳课程的三种 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 会在这些噪声数据上训练,反而降低性能。

练习

  1. [配置题] 为 G1 配置完整的 23 DOF action space,包括每个关节的 scale。写出 mjlab 的 ActionCfg。
  2. [诊断题] 你的人形全身控制策略在训练 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 在本章增加"人形全身控制"模块。你应该能够:

  1. 在 mjlab 或 Isaac Lab 中配置 G1 的 23 DOF 全身控制环境
  2. 实现 ExBody2 风格的上下肢解耦 reward
  3. 在简单的 MoCap 参考动作上训练 tracking 策略,tracking error 达标
  4. 集成 HoST 风格的跌倒恢复模块
  5. 对比 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——这是本章最重要的工程建议。