跳转至

第74章:RL 全身控制基础——IsaacLab 多肢体环境搭建

74.0 前置自测

答不出两题以上,建议先回到 足式/190_腿足RL训练栈足式/70_腿足简化模型理论复合/20_浮动基座臂统一动力学 复习。

  1. 腿足 RL 中为什么通常输出关节位置目标,而不是直接输出关节力矩?
  2. model.nqmodel.nv 在含浮动基座机器人中为什么不相等?
  3. Domain Randomization 的目标是让仿真更逼真,还是让策略更鲁棒?
  4. Teacher-Student 训练中,哪些信息可以给 Critic 或 Teacher,哪些信息不能给部署策略?
  5. 当机械臂末端追踪目标与基座稳定性冲突时,为什么不能简单提高末端跟踪奖励权重?

本章目标

本章把足式 RL 训练栈扩展到四足+臂与人形全身控制。

学完后,你应能设计一个含机械臂或上半身的 IsaacLab RL 环境,解释观测、动作、奖励和随机化背后的物理含义,并能用最小代码验证训练信号是否与理论一致。

本章不是追求某个开源仓库的一键复现,而是建立可迁移的设计框架:先理解为什么这样建,再把它落实到 IsaacLab、rsl_rl、MuJoCo 或自研训练栈。

知识树

复合 RL 全身控制的根问题是:如何让一个高维浮动基座系统在接触不确定、感知不完整和操作目标变化的情况下输出可部署的全身动作。

RL 全身控制
├── 问题建模
│   ├── MDP 与 POMDP
│   ├── 多时间尺度
│   └── locomotion 与 manipulation 的目标冲突
├── 环境搭建
│   ├── IsaacLab scene
│   ├── action manager
│   ├── observation manager
│   ├── reward manager
│   └── event manager
├── 观测设计
│   ├── 本体感知
│   ├── 任务命令
│   ├── 末端目标
│   ├── 历史窗口
│   └── 特权信息
├── 动作设计
│   ├── 关节位置目标
│   ├── 残差动作
│   ├── 夹爪动作
│   └── 多头动作
├── 奖励设计
│   ├── 末端跟踪
│   ├── 基座稳定
│   ├── 接触安全
│   ├── 能耗与平滑
│   └── 因果解耦
├── 随机化与蒸馏
│   ├── 负载与惯量随机化
│   ├── 延迟与噪声
│   ├── 外力与接触扰动
│   ├── Teacher-Student
│   └── 适应模块
└── 训练与部署
    ├── curriculum
    ├── 指标监控
    ├── ablation
    ├── sim2sim
    └── sim2real 安全门槛

这棵树的根不是“怎样把动作维度从 12 改成 18”,而是“怎样让学习系统理解全身耦合”。

如果只改维度,不改奖励、观测和随机化,得到的往往不是全身控制,而是一个更容易摔倒的腿足策略。


74.1 从纯腿足 RL 到复合 RL:问题为什么变难 ⭐⭐

这一节解决的问题是:四足加臂或人形上半身以后,RL 环境到底新增了什么难度。

动机:为什么不能照搬 Go2 velocity tracking

回顾足式 RL:典型 Go2 平地行走环境的任务很清楚。

策略输入本体感知和速度命令,输出 12 个腿关节的位置偏移。

奖励主要由速度跟踪、姿态稳定、足端接触、能耗和平滑组成。

这个问题已经很难,但目标之间的冲突相对单一。

当我们把 Z1、ARX5、WidowX 或人形上半身接进来时,任务从“身体走得稳”变成“身体走得稳,同时手要到指定位置,必要时还要施力”。

这不是简单加一个末端目标。

机械臂的每一次加速都会通过浮动基座改变全身动量。

末端接触物体时,物体反力又会通过雅可比转化为基座扰动和足端载荷变化。

如果策略只从奖励中看到“手离目标越近越好”,它可能牺牲支撑裕度去追手。

如果策略只从奖励中看到“基座越稳越好”,它又可能把手臂冻结在安全姿态附近,完全不操作。

这就是复合 RL 的核心矛盾:末端精度与全身稳定并不是天然一致的目标。

反面案例:末端奖励过强导致摔倒

假设我们在 Go2 velocity tracking 环境中新增一项:

\[ r_{ee} = \exp \left(-\frac{\|\mathbf{p}_{ee} - \mathbf{p}_{ee}^{cmd}\|^2}{\sigma_{ee}^2}\right) \]

然后把它的权重设为 \(w_{ee}=10\),高于速度跟踪和姿态稳定。

训练早期策略会发现一个捷径:大幅扭动基座可以更快把末端推向目标。

短期内 \(r_{ee}\) 上升,长期却导致 roll/pitch 角速度变大。

一旦足端接触相位与臂摆动叠加,支撑多边形边缘的法向力下降,策略就会摔倒。

如果 episode 终止惩罚不够强,策略甚至会学到“冲过去碰一下目标再摔倒也划算”。

这不是 PPO 的问题,而是任务目标被错误表达的结果。

历史来源:从 Deep-WBC 到视觉全身控制

早期腿足 RL 主要关注速度跟踪和地形鲁棒性。

RMA、Walk These Ways、Miki 的感知行走证明了本体感知、历史窗口和特权信息可以训练出可部署的行走策略。

四足+臂的 Deep Whole-Body Control 把这个范式扩展到统一动作空间:腿和臂由同一个策略一起输出。

Visual Whole-Body Control 又把高层视觉目标规划和低层全身目标追踪分成两个训练阶段。

UMI-on-Legs 进一步把固定臂操作策略输出的末端轨迹交给腿足全身控制器执行。

这些方法表面不同,本质都在回答同一个问题:高层任务只想要手到达目标,低层策略必须自己处理基座、腿、臂和接触之间的耦合。

人形方向也走向类似路径。

ExBody 强调上半身追踪与下半身平衡解耦。

FALCON 使用上下体双策略头分别处理末端跟踪和行走稳定。

SoFTA 进一步把上体和下体的控制频率分开。

从这些工作可以看到一个趋势:越接近真实操作,越不能把全身策略看成一个普通高维 MLP。

理论建模:从 MDP 到 POMDP

标准 RL 把问题写成马尔可夫决策过程:

\[ \mathcal{M} = (\mathcal{S}, \mathcal{A}, P, r, \gamma) \]

其中 \(s_t \in \mathcal{S}\) 是完整状态,\(a_t \in \mathcal{A}\) 是动作,\(P(s_{t+1}|s_t,a_t)\) 是转移概率。

在仿真中,完整状态可以包含基座位姿、所有关节、所有连杆速度、接触力、摩擦系数、负载质量和物体真实位姿。

但部署策略看不到完整状态。

它能看到的是观测:

\[ o_t = \Omega(s_t, \epsilon_t) \]

这里 \(\Omega\) 是传感器映射,\(\epsilon_t\) 是噪声和延迟。

因此真实问题更接近 POMDP。

POMDP 的策略严格来说应依赖历史:

\[ a_t \sim \pi(a_t | o_{0:t}, a_{0:t-1}) \]

工程上通常用固定长度历史、RNN、TCN 或适应模块近似这个历史依赖。

对于复合机器人,历史信息比纯腿足更重要。

原因是机械臂和物体交互引入了更多不可直接观测的变量。

负载质量改变了同样动作下的基座加速度。

夹爪接触状态改变了末端受力。

电机延迟使得动作和响应之间出现时差。

单帧观测无法区分这些情况,历史窗口却能从“我刚才怎么动,身体怎样响应”中推断出来。

本质洞察:复合 RL 的难点不是动作维度从 12 增到 18 或 29,而是观测中的缺失信息变多了。 策略必须从历史中推断负载、接触、延迟和扰动,这才是全身控制比普通速度跟踪更难的根本原因。

多时间尺度:腿、臂、任务命令并不等频

复合系统中至少存在四种时间尺度。

层级 典型频率 内容 RL 环境中的体现
物理仿真 200-1000 Hz 接触、关节动力学 sim_dt
低层控制 100-500 Hz PD、WBC、策略输出保持 decimation
操作命令 5-50 Hz 末端轨迹、抓取阶段 command buffer
任务规划 0.5-5 Hz 目标选择、物体状态 high-level command

纯腿足速度命令通常变化较慢。

末端目标却可能在 10 Hz 左右给出一段未来轨迹。

如果每个控制步都随机改变末端目标,策略会学到追逐噪声。

如果末端目标太久不更新,策略又会滞后。

因此复合 RL 环境必须明确命令频率和保持方式。

一种常见做法是让策略在 50-100 Hz 输出关节目标,但给它一个未来末端轨迹窗口。

这相当于告诉策略“接下来一小段时间手要怎么走”。

策略可以提前调整基座和腿,而不是等误差出现后再补救。

双重解读:统一策略与分层策略

统一策略可以理解为“一个大脑同时控制腿和臂”。

它的优点是耦合可以自然被学习。

它的缺点是探索空间大,奖励冲突强,数据需求高。

分层策略可以理解为“高层只说手要去哪,低层负责全身执行”。

它的优点是接口清晰,固定臂操作策略更容易复用。

它的缺点是高层如果给出不可达或不安全的末端轨迹,低层只能尽力补救。

两者不是谁替代谁,而是适合不同任务阶段。

任务类型 推荐结构 理由
平地边走边伸手 统一低层策略 耦合较强但目标简单
视觉抓取与移动操作 高层策略 + 全身低层 视觉和控制时间尺度不同
人形上体遥操作 上下体解耦 上肢精度和下肢平衡目标不同
接触丰富推拉 混合 MPC/RL 或双策略头 力与稳定性冲突明显

小型推导:动作维度增加为什么会改变探索难度

设策略输出为 \(a \in \mathbb{R}^{n_a}\)

若每个动作维度初始近似独立高斯:

\[ a_i \sim \mathcal{N}(0,\sigma^2) \]

则动作向量范数期望满足:

\[ \mathbb{E}\|a\|^2 = n_a \sigma^2 \]

从 12 维腿关节增加到 18 维腿臂关节,初始随机动作能量增加 50%。

如果 action scale 不变,训练早期的随机动作更激烈。

如果机械臂关节也使用腿关节同样的 scale,末端会产生不合理的大幅摆动。

所以复合 RL 中 action scale 必须分组设置。

腿关节偏移可以较大,因为腿需要产生步态。

臂关节偏移通常较小,因为臂末端位置对关节角非常敏感。

夹爪动作还可能是低频离散或准连续信号,不能和髋关节同样处理。

代码验证:动作维度与初始动作能量

import torch

def expected_action_energy(dim: int, sigma: float = 1.0, samples: int = 100000):
    """估计随机高斯动作的平均平方范数。"""
    a = sigma * torch.randn(samples, dim)
    return torch.mean(torch.sum(a * a, dim=-1)).item()

for dim in [12, 18, 29]:
    energy = expected_action_energy(dim, sigma=0.2)
    print(f"动作维度 {dim:2d}: E[||a||^2] = {energy:.4f}")

# 结论:
# 维度越高,初始随机动作的总能量越大。
# 因此复合机器人不能直接沿用纯腿足的 action scale。

这段代码没有控制机器人,却验证了一个训练设计原则。

动作空间越大,随机探索越容易产生强扰动。

这就是为什么复合 RL 通常需要更保守的 action scale、更强的终止条件和更细的 curriculum。

⚠️ 常见陷阱

⚠️ 编程陷阱:所有关节共用一个 action scale

错误做法:腿、臂、腰、夹爪全部使用 scale=0.25

现象:训练早期机械臂快速乱甩,基座 roll/pitch 剧烈波动。

根本原因:不同关节对末端和质心的灵敏度不同,统一 scale 会让高杠杆关节过度探索。

正确做法:按关节组设置 scale,例如腿 0.25 rad、臂肩 0.12 rad、腕部 0.08 rad、夹爪单独限幅。

💡 概念误区:把多肢体 RL 理解成纯腿足 RL 加末端奖励

新手想法:只要把动作维度扩展,再加一个末端误差奖励即可。

实际情况:臂改变质量分布、动量和接触反力,奖励和随机化都要重新设计。

正确理解:复合 RL 是一个耦合控制问题,末端目标必须通过全身稳定性约束来实现。

🧠 思维陷阱:看到统一策略成功就放弃结构化接口

新手想法:端到端策略既然能学,就不需要高层轨迹、WBC 或 MPC。

实际情况:统一策略在固定任务上可能很好,但任务变化、真机安全和故障定位都需要接口化设计。

正确做法:能端到端训练,也要保留清晰的命令、观测、奖励和评估边界。

小节练习

  1. 以 Go2+Z1 为例,列出纯腿足策略和复合策略在动作空间、观测空间、奖励项、随机化参数上的差异。
  2. 推导如果动作维度从 18 增到 29,但高斯探索标准差不变,初始动作平方范数期望增加多少。
  3. 设计一个训练失败案例:只奖励末端位置、不奖励基座姿态,预测策略可能学到的三种异常行为。

74.2 IsaacLab 多肢体环境的结构化搭建 ⭐⭐

这一节解决的问题是:如何把复合机器人环境拆成可调试的 IsaacLab 模块。

动机:为什么环境配置比网络结构更重要

在腿足 RL 中,很多项目使用相似的 PPO 网络结构。

差异真正大的地方往往是环境。

复合机器人更是如此。

同样一个 PPO,如果环境定义错误,训练曲线可能看似上升,但学到的是不可部署行为。

如果观测里泄漏仿真真值,策略在仿真中会非常强,真机却无法运行。

如果奖励项单位不一致,末端厘米级误差和基座弧度误差会被错误权重混合。

如果随机化只随机腿不随机臂,策略在空载时很好,一拿物体就失稳。

环境配置决定了策略“认为世界是什么样子”。

网络只是学习这个世界中的映射。

反面案例:环境能跑但观测顺序错

复合环境最常见的 bug 不是程序崩溃,而是程序能跑但语义错。

例如观测向量约定为:

\[ o = [\omega_b, g_b, q_{leg}, \dot q_{leg}, q_{arm}, \dot q_{arm}, p_{ee}^{cmd}, a_{last}] \]

部署代码却按另一种顺序解析:

\[ o' = [\omega_b, g_b, q_{arm}, \dot q_{arm}, q_{leg}, \dot q_{leg}, p_{ee}^{cmd}, a_{last}] \]

维度完全相同,所以不会报错。

但策略看到的“髋关节角”实际是“肩关节角”。

这种错误会表现为训练中能收敛、导出后行为荒谬。

因此 IsaacLab 中应把观测项命名化、分组化,而不是手写一个巨大拼接函数后不再检查。

历史来源:Manager-Based 环境的意义

早期 legged_gym 环境通常在一个类里写完 reset、step、reward 和 randomization。

这种方式对于单一四足行走足够高效。

但一旦加入机械臂、视觉、夹爪、物体和外部任务,单个类会迅速膨胀。

IsaacLab 的 Manager-Based 设计把环境拆成多个管理器。

ObservationManager 负责观测。

ActionManager 负责动作解释。

RewardManager 负责奖励项。

TerminationManager 负责终止条件。

CommandManager 负责命令采样。

EventManager 负责随机化和扰动。

这种拆分不是为了形式美观,而是为了定位错误。

当末端跟踪失败时,你可以单独打印命令、末端观测、奖励分项和动作输出。

当真机部署异常时,你可以逐项核对部署观测是否与训练观测一致。

复合环境的推荐模块

模块 复合机器人新增内容 必查问题
Scene 统一 USD/URDF、臂、夹爪、物体 坐标系和惯量是否正确
Action 腿臂分组 scale、夹爪动作 动作维度和关节顺序是否一致
Observation 末端目标、目标历史、臂关节 是否泄漏仿真真值
Reward 末端位姿、基座稳定、接触力 权重是否冲突
Termination 跌倒、超限、自碰撞、力矩饱和 是否过早或过晚终止
Command EE 轨迹、速度命令、模式命令 命令是否可达
Event 负载、延迟、摩擦、外扰 随机化是否覆盖真机

理论:环境是一个可组合的状态转移包装器

物理引擎给出基本转移:

\[ s_{t+1} = f_{\text{physics}}(s_t, u_t, \xi) \]

这里 \(u_t\) 是底层关节命令,\(\xi\) 是环境参数。

RL 策略输出的不是直接物理输入,而是高层动作 \(a_t\)

ActionManager 实现:

\[ u_t = \mathcal{A}(a_t, q_t, \dot q_t) \]

ObservationManager 实现:

\[ o_t = \mathcal{O}(s_t, c_t, \eta_t) \]

RewardManager 实现:

\[ r_t = \sum_i w_i r_i(s_t, a_t, c_t) \]

CommandManager 实现:

\[ c_{t+1} = \mathcal{C}(c_t, s_t, \zeta_t) \]

EventManager 改变参数:

\[ \xi_{k+1} \sim p(\xi) \]

把这些函数分开,等价于把一个复杂 MDP 显式分解。

这让我们能逐个验证每个函数。

本质洞察:IsaacLab 管理器不是工程包装,而是把 RL 问题的数学构件拆出来。 Action 对应策略输出到物理输入的映射,Observation 对应部分可观测传感器,Reward 对应优化目标,Event 对应环境参数分布。

环境骨架代码

from dataclasses import dataclass

@dataclass
class WholeBodyEnvShape:
    """用一个小结构体记录维度,防止训练和部署各写各的。"""

    num_leg_joints: int = 12
    num_arm_joints: int = 6
    num_gripper_joints: int = 1
    ee_pose_dim: int = 6
    base_cmd_dim: int = 3
    history_steps: int = 4

    @property
    def action_dim(self) -> int:
        # 腿、臂、夹爪都由策略输出目标偏移
        return self.num_leg_joints + self.num_arm_joints + self.num_gripper_joints

    @property
    def proprio_dim(self) -> int:
        # base_ang_vel(3) + projected_gravity(3)
        # joint_pos + joint_vel + last_action
        joint_dim = self.num_leg_joints + self.num_arm_joints + self.num_gripper_joints
        return 3 + 3 + joint_dim + joint_dim + joint_dim

    @property
    def command_dim(self) -> int:
        # 底盘速度命令 + 末端 SE(3) 命令
        return self.base_cmd_dim + self.ee_pose_dim

    @property
    def policy_obs_dim(self) -> int:
        # 当前观测 + 若干步历史
        return (self.proprio_dim + self.command_dim) * self.history_steps


shape = WholeBodyEnvShape()
print("动作维度:", shape.action_dim)
print("策略观测维度:", shape.policy_obs_dim)

这类代码看似简单,却能避免很多隐藏错误。

当你换机器人时,第一件事不是改网络,而是重新计算这些维度并写成可测试的配置。

IsaacLab 配置伪代码

class Go2ArmWholeBodyEnvCfg:
    """Go2+Arm 全身 RL 环境配置示意。"""

    # 场景:机器人、地面、目标物体、传感器
    scene = {
        "num_envs": 4096,
        "env_spacing": 3.0,
        "robot_asset": "go2_arx5.usd",
        "terrain": "plane_or_rough",
    }

    # 动作:腿和臂分组缩放
    actions = {
        "leg_joint_pos": {
            "joint_names": ["FL_.*", "FR_.*", "RL_.*", "RR_.*"],
            "scale": 0.25,
        },
        "arm_joint_pos": {
            "joint_names": ["arm_joint.*"],
            "scale": 0.10,
        },
        "gripper": {
            "joint_names": ["gripper_joint"],
            "scale": 0.02,
        },
    }

    # 观测:策略只能看部署可获得的信息
    observations = {
        "policy": [
            "base_ang_vel",
            "projected_gravity",
            "joint_pos_rel",
            "joint_vel_rel",
            "last_action",
            "base_velocity_command",
            "ee_target_in_task_frame",
            "ee_error_in_base_frame",
        ],
        # critic 可以使用特权信息,但 actor 不可以
        "critic": [
            "payload_mass",
            "friction_coeff",
            "external_force",
            "contact_force_truth",
        ],
    }

    # 奖励:所有项必须能解释物理意义
    rewards = {
        "ee_position_tracking": 2.0,
        "ee_orientation_tracking": 0.5,
        "base_orientation": -1.0,
        "base_ang_vel_xy": -0.05,
        "joint_torque": -1.0e-5,
        "action_rate": -0.02,
        "self_collision": -2.0,
        "survival": 0.5,
    }

这段代码不是可以直接运行的完整 IsaacLab 配置。

它的作用是展示结构和命名。

真正的项目中应把每个字符串绑定到 IsaacLab 的 observation term、reward term 和 event term。

验证环境的最小闭环

复合环境建好后,不要立刻训练 4096 个环境。

先做单环境检查。

检查项 方法 合格现象
关节顺序 打印 action index 到 joint name 顺序与部署约定一致
默认姿态 reset 后可视化 站立稳定,臂不穿模
末端坐标 手动给臂关节角,比较 FK 末端移动方向符合预期
奖励分项 随机动作下打印均值 无 NaN,量级可解释
终止条件 人为倾倒或超限 及时终止
随机化 每个 episode 打印样本 范围与配置一致

单环境冒烟测试代码

def smoke_test_env(env, steps: int = 200):
    """用随机小动作检查环境是否存在维度、NaN 和终止异常。"""
    obs, info = env.reset()
    print("初始观测 shape:", obs.shape)

    for t in range(steps):
        # 使用小幅随机动作,避免一开始就把机器人打翻
        action = 0.05 * torch.randn(env.num_envs, env.action_space.shape[0], device=env.device)
        obs, reward, terminated, truncated, info = env.step(action)

        if torch.isnan(obs).any():
            raise RuntimeError(f"第 {t} 步观测出现 NaN")

        if torch.isnan(reward).any():
            raise RuntimeError(f"第 {t} 步奖励出现 NaN")

        if t % 20 == 0:
            # 打印奖励分项,比只看 total reward 更有诊断价值
            reward_terms = info.get("reward_terms", {})
            print(t, {k: float(v.mean()) for k, v in reward_terms.items()})

    print("冒烟测试结束")

如果环境连小随机动作都不能稳定执行几十步,通常是模型、关节顺序、PD 增益或终止条件有问题。

这时不要启动长训练。

先把单环境调通。

⚠️ 常见陷阱

⚠️ 编程陷阱:训练观测和部署观测分别手写

错误做法:训练环境中拼接一套观测,部署脚本中重新拼接一套观测。

现象:仿真训练曲线很好,导出后 sim2sim 行为完全不一致。

根本原因:观测顺序、归一化、坐标系或单位出现微小差异。

正确做法:把观测规格写成单一配置,训练、回放、导出和部署共用同一份顺序定义。

💡 概念误区:把 Critic 能看到的信息也给 Actor

错误想法:反正训练在仿真中,给策略多看一点真值能学得更快。

现象:训练性能虚高,部署时缺少这些输入导致策略不可用。

根本原因:Actor 输入必须是部署可获得的信息,Critic 或 Teacher 才能使用特权信息。

正确做法:严格区分 policy 观测组和 critic 观测组。

🧠 思维陷阱:环境一能跑就开始大规模训练

错误想法:4096 并行很快,先训一轮看看。

现象:几个小时后才发现奖励符号错、末端坐标系错或随机化没有生效。

正确做法:先单环境可视化,再小批量随机动作,再短训练,再完整训练。

小节练习

  1. 为 Go2+Z1 写出动作维度、策略观测维度和 Critic 特权观测维度的计算表。
  2. 设计一个检查观测顺序的单元测试:给每个关节一个唯一角度,验证拼接向量中对应位置正确。
  3. 解释为什么 Critic 可以看接触力真值,但 Actor 不应该直接看仿真的接触力数组。

74.3 观测空间设计:策略到底应该看见什么 ⭐⭐⭐

这一节解决的问题是:如何设计复合机器人的观测,使策略既能学习耦合,又不依赖真机不可得信息。

动机:观测不是越多越好

很多初学者面对复杂任务时会自然想到:把所有状态都喂给策略。

在仿真中,这样做确实会提高训练速度。

但部署时,很多状态无法获得。

例如真实摩擦系数、物体质量、精确接触力、无延迟的基座线速度、仿真中的目标物体真值,都不是低层策略天然可用的输入。

如果 Actor 依赖这些信息,策略学到的是“读答案做题”。

一旦答案消失,行为就崩溃。

复合 RL 的观测设计必须遵守一个原则:Actor 只能看部署闭环中真实可提供的信息。

可以由状态估计器提供的信息要建模噪声和延迟。

可以由高层规划器提供的信息要明确坐标系和更新频率。

不可部署的信息只能给 Critic、Teacher 或用于离线诊断。

反面案例:把世界系末端误差直接喂给 Actor

假设训练时给 Actor 输入:

\[ \mathbf{e}_{ee}^{world} = \mathbf{p}_{ee}^{world} - \mathbf{p}_{target}^{world} \]

这看似合理。

但真机上 \(\mathbf{p}_{ee}^{world}\) 需要外部定位、SLAM 或 VIO。

如果部署系统只有本体感知和基座里程计,这个量会带噪声、漂移和延迟。

训练中无噪声的世界系误差会让策略依赖厘米级精度。

真机中 5 cm 漂移就可能导致末端来回补偿,引起基座晃动。

更稳健的做法是输入任务帧或基座帧中的目标,训练时注入延迟和噪声,并让低层只承担短时跟踪。

观测类别总表

类别 例子 Actor 可用性 说明
本体感知 关节角、关节速度、IMU、上一动作 部署核心输入
基座估计 线速度、姿态、高度 需状态估计器,必须加噪声
任务命令 速度命令、末端目标、模式命令 来自高层或操作者
末端状态 末端位姿、末端速度 可由 FK 算得,但依赖基座估计
接触信息 足端触地、夹爪接触 可估计,真值不能直接用
历史信息 过去观测、过去动作 用于近似 POMDP 信念
特权信息 摩擦、负载、外力真值、地形真值 只给 Critic 或 Teacher

坐标系选择:世界系、基座系、任务帧

观测中的同一个向量,用不同坐标系表达会改变学习难度。

世界系表达直观,但依赖全局定位。

基座系表达部署友好,但基座晃动会把目标也变成晃动信号。

任务帧表达适合操作,因为目标相对桌面、门把手或物体定义,而不是相对机器人身体定义。

表达方式 形式 优点 风险
世界系 \(\mathbf{p}_{ee}^{W}\) 与地图和任务一致 依赖全局定位
基座系 \(\mathbf{p}_{ee}^{B}\) 本体感知可计算 基座抖动会污染目标
任务帧 \(\mathbf{p}_{ee}^{T}\) 与操作对象绑定 需要任务帧估计

在四足臂任务中,常见做法是低层策略输入未来末端目标在任务帧或基座帧中的表示。

同时输入基座姿态和角速度,让策略知道身体正在怎样晃动。

高层视觉策略负责慢速更新任务帧。

低层全身策略负责短时补偿。

理论:观测历史如何近似隐变量

设不可见环境参数为 \(z_t\),例如负载质量、摩擦、延迟和接触状态。

策略真正需要的是:

\[ a_t = \pi(o_t, z_t) \]

但部署时看不到 \(z_t\)

历史窗口提供了一个估计:

\[ \hat z_t = \phi(o_{t-H:t}, a_{t-H:t-1}) \]

于是策略变成:

\[ a_t = \pi(o_t, \hat z_t) \]

这就是 RMA、Teacher-Student 和许多历史堆叠方法的共同结构。

历史窗口不是“给网络更多输入”这么简单。

它提供了辨识动力学的证据。

同样的关节命令,如果负载更重,基座响应会更慢。

同样的脚端速度,如果摩擦更低,身体会出现滑移。

同样的末端命令,如果夹爪碰到物体,臂关节力矩和速度会出现不同模式。

这些差异都需要时间序列才能看出来。

观测维度设计示例

以 Go2+6DOF 臂+夹爪为例,使用 19 维动作。

观测项 维度 坐标系 说明
base angular velocity 3 body IMU 角速度
projected gravity 3 body 姿态的紧凑表示
velocity command 3 body/task \(v_x,v_y,\omega_z\)
joint position relative 19 joint 相对默认姿态
joint velocity 19 joint 编码器速度
last action 19 action 动作平滑和延迟补偿
ee target position 3 task/base 末端位置命令
ee target orientation 3 task/base 轴角或 log SO(3)
ee current error 6 base/task 由 FK 得到
mode command 2 scalar 行走/站立/接触模式
estimated contact 4 foot 足端触地估计

单帧合计为 81 维。

如果堆叠 4 帧,策略输入为 324 维。

如果使用 TCN,则可以保留形状 [batch, time, feature],让卷积处理时间维。

归一化原则

观测归一化不是小细节。

PPO 对输入尺度很敏感。

复合机器人中,不同观测项的自然尺度差异很大。

关节角通常在弧度量级。

关节速度可能达到十几 rad/s。

末端位置误差在米级或厘米级。

动作历史在归一化动作空间中通常是 \([-1,1]\)

如果不归一化,网络会优先关注数值大的通道。

信号 推荐尺度处理 原因
关节角 相对默认姿态并除以关节范围 避免绝对零位差异
关节速度 clip 后乘以缩放系数 限制异常尖峰
末端位置误差 以米为单位,clip 到任务范围 防止早期误差过大
姿态误差 用 SO(3) log,clip 到 \(\pi\) 避免欧拉角跳变
接触估计 0/1 或平滑概率 避免硬真值依赖
历史动作 保持归一化动作 与策略输出空间一致

代码:构造可检查的观测字典

import torch

class WholeBodyObservationBuilder:
    """把命名观测转换为策略向量,同时保留调试字典。"""

    def __init__(self):
        self.order = [
            "base_ang_vel",
            "projected_gravity",
            "velocity_cmd",
            "joint_pos_rel",
            "joint_vel",
            "last_action",
            "ee_target",
            "ee_error",
            "mode_cmd",
            "contact_est",
        ]

    def build(self, terms: dict):
        pieces = []
        debug_shapes = {}
        for name in self.order:
            value = terms[name]
            if torch.isnan(value).any():
                raise RuntimeError(f"{name} 出现 NaN")
            pieces.append(value)
            debug_shapes[name] = tuple(value.shape)
        obs = torch.cat(pieces, dim=-1)
        return obs, debug_shapes


builder = WholeBodyObservationBuilder()

# 示例:batch=4096
terms = {
    "base_ang_vel": torch.zeros(4096, 3),
    "projected_gravity": torch.zeros(4096, 3),
    "velocity_cmd": torch.zeros(4096, 3),
    "joint_pos_rel": torch.zeros(4096, 19),
    "joint_vel": torch.zeros(4096, 19),
    "last_action": torch.zeros(4096, 19),
    "ee_target": torch.zeros(4096, 6),
    "ee_error": torch.zeros(4096, 6),
    "mode_cmd": torch.zeros(4096, 2),
    "contact_est": torch.zeros(4096, 4),
}

obs, shapes = builder.build(terms)
print(obs.shape)
print(shapes)

这段代码的关键不是 torch.cat

关键是 order 成为唯一观测顺序来源。

训练、回放和部署都应使用同一顺序。

⚠️ 常见陷阱

⚠️ 编程陷阱:末端姿态用欧拉角差

错误做法:直接用 roll - roll_ref, pitch - pitch_ref, yaw - yaw_ref

现象:接近 \(\pi\)\(-\pi\) 时误差跳变,奖励突然变差。

根本原因:SO(3) 不是欧氏空间,欧拉角有周期和奇异性。

正确做法:用旋转矩阵或四元数计算相对旋转,再用李代数 log 得到 3 维误差。

💡 概念误区:历史越长越好

错误想法:历史窗口越长,策略知道的信息越多。

现象:输入维度过大,训练变慢,旧信息干扰当前控制。

根本原因:历史窗口应覆盖系统辨识所需时间尺度,而不是无限增长。

正确做法:从 3-5 帧短历史开始;若要辨识负载或地形,再用 0.5-1.0 秒窗口配合 TCN。

🧠 思维陷阱:把特权信息泄漏当成性能提升

错误想法:仿真里能拿到接触力和摩擦系数,给策略看可以更快训练。

现象:仿真成功率很高,真机或带噪 sim2sim 崩溃。

正确做法:特权信息只能用于 Critic、Teacher、诊断指标或蒸馏标签。

小节练习

  1. 为人形 G1 上体操作任务设计一个 Actor 观测表,明确每个观测项的维度、坐标系和部署来源。
  2. 写出用四元数计算末端姿态误差的伪代码,并说明为什么比欧拉角差更稳定。
  3. 设计一个实验比较单帧观测、4 帧历史和 TCN 历史编码在随机负载任务中的差异。

74.4 动作空间:让策略输出什么才安全 ⭐⭐

这一节解决的问题是:复合机器人策略的动作应该定义为关节位置、速度、力矩,还是更高层的残差。

动机:动作空间决定探索边界

RL 训练早期的策略几乎是随机的。

如果动作直接是力矩,随机输出会直接作用到机器人动力学上。

对于四足臂或人形,这非常危险。

腿的随机力矩可能让脚打滑。

臂的随机力矩可能让末端撞到身体。

腰部的随机力矩可能把基座快速扭倒。

所以多数可部署腿足 RL 采用关节位置目标。

策略输出归一化动作 \(a_t \in [-1,1]^n\)

ActionManager 将其映射为:

\[ q_t^{des} = q^{default} + s \odot a_t \]

底层 PD 再输出力矩:

\[ \tau = K_p(q^{des} - q) - K_d \dot q \]

这样策略探索被关节目标和 PD 增益包裹起来。

反面案例:直接力矩输出导致高频抖动

直接力矩输出最大的问题是没有自然平滑约束。

PPO 的动作噪声会让相邻控制步的力矩变化很大。

即使平均力矩不大,高频力矩也会造成电机发热、齿轮冲击和接触抖动。

在复合机器人中,机械臂惯量更集中在上方,高频扭动会通过基座放大到足端接触。

如果仿真器的电机模型过于理想,策略甚至会学会利用高频力矩“震动”末端接近目标。

这种行为在真机上通常不可接受。

动作设计选项

动作形式 输出 优点 风险 推荐阶段
关节位置目标 \(q^{des}\) 稳定,易部署 依赖 PD 增益 入门与大多数任务
关节位置残差 \(\Delta q\) 可叠加参考轨迹 参考质量影响结果 模仿和操作
关节速度目标 \(\dot q^{des}\) 平滑轨迹方便 积分漂移 运动学低层
关节力矩 \(\tau\) 控制自由度最大 探索危险 研究级力控
末端速度 \(\dot x_{ee}\) 接口直观 需要 IK/WBC 分层控制
混合动作 \(q^{des}\) + 臂 residual 利用结构先验 接口复杂 复杂任务

分组 scale:腿、臂、腰、夹爪不能同权

动作映射应写成分块形式:

\[ \begin{aligned} q_{\ell}^{des} &= q_{\ell}^{default} + s_{\ell} \odot a_{\ell} \\ q_{a}^{des} &= q_{a}^{default} + s_{a} \odot a_{a} \\ q_{g}^{des} &= q_{g}^{default} + s_{g} \odot a_{g} \end{aligned} \]

其中 \(s_{\ell}\)\(s_a\)\(s_g\) 应由关节功能和安全边界决定。

腿关节要覆盖步态运动,因此 scale 可以较大。

臂肩关节具有大力臂,对基座影响明显,因此 scale 应较小。

腕关节对末端姿态灵敏,也应谨慎。

夹爪动作常常需要慢速、限位和滞回,不能像普通关节那样每步剧烈改变。

控制降采样

在 IsaacLab 中,物理仿真频率通常高于策略频率。

例如仿真步长 0.005 s,策略每 4 个仿真步更新一次,则策略频率为 50 Hz。

动作在 4 个仿真步内保持不变。

这称为 decimation。

控制降采样有两个作用。

第一,降低策略推理频率,减少计算。

第二,给 PD 和物理系统时间响应,避免策略每个物理步都改变目标。

复合机器人中,decimation 还影响末端平滑性。

如果策略频率太低,末端轨迹会阶梯化。

如果策略频率太高,动作噪声会直接进入机械臂。

常见选择是仿真 200 Hz、策略 50 Hz 或 100 Hz。

上体精细稳定任务可以采用更高频上体头,但要明确和下体头的同步方式。

残差动作:参考轨迹上的安全探索

很多操作任务已有参考轨迹。

例如末端从当前位姿移动到目标位姿,可以由 IK 或高层策略给出 nominal 关节轨迹。

此时 RL 不必从零学习所有动作。

可以让策略输出残差:

\[ q^{des} = q^{ref} + s \odot a_t \]

残差动作好比自行车的辅助轮。

参考轨迹提供基本方向。

RL 只学习补偿基座扰动、接触误差和模型误差。

这种设计能显著降低探索难度。

但它也会限制策略多样性。

如果参考轨迹本身不可行,残差策略很难救回来。

因此残差动作适合有明确任务轨迹的操作,不适合从零发现新步态。

代码:分组动作映射

import torch

class GroupedJointPositionAction:
    """把归一化动作映射到分组关节目标。"""

    def __init__(self, default_q, leg_ids, arm_ids, gripper_ids):
        self.default_q = default_q
        self.leg_ids = leg_ids
        self.arm_ids = arm_ids
        self.gripper_ids = gripper_ids

        self.leg_scale = 0.25
        self.arm_scale = 0.10
        self.gripper_scale = 0.02

    def __call__(self, action):
        q_des = self.default_q.clone()

        n_leg = len(self.leg_ids)
        n_arm = len(self.arm_ids)
        n_gripper = len(self.gripper_ids)

        a_leg = action[:, :n_leg]
        a_arm = action[:, n_leg:n_leg + n_arm]
        a_gripper = action[:, n_leg + n_arm:n_leg + n_arm + n_gripper]

        q_des[:, self.leg_ids] += self.leg_scale * torch.clamp(a_leg, -1.0, 1.0)
        q_des[:, self.arm_ids] += self.arm_scale * torch.clamp(a_arm, -1.0, 1.0)
        q_des[:, self.gripper_ids] += self.gripper_scale * torch.clamp(a_gripper, -1.0, 1.0)

        # 最后再做一次关节限位裁剪,避免默认姿态加残差后越界
        return q_des

这段代码体现了两个原则。

动作裁剪发生在归一化空间。

关节限位裁剪应发生在物理关节空间。

两者都需要。

⚠️ 常见陷阱

⚠️ 编程陷阱:只裁剪 action,不裁剪关节目标

错误做法:action = clip(action, -1, 1) 后直接映射。

现象:默认姿态靠近关节限位时,q_default + scale * action 仍然越界。

根本原因:action 边界不等于关节物理边界。

正确做法:归一化动作裁剪后,还要对 q_des 做关节限位裁剪。

💡 概念误区:直接力矩一定更高级

错误想法:力矩控制更接近真实动力学,所以一定更好。

实际情况:直接力矩探索更危险,对执行器模型、奖励和安全约束要求更高。

正确理解:关节位置 PD 不是低级方案,而是把安全先验注入动作空间。

🧠 思维陷阱:把夹爪当成普通关节

错误想法:夹爪也只是一个 DOF,放进动作向量即可。

实际情况:夹爪有接触、滞回、力限制和任务阶段逻辑。

正确做法:夹爪动作低频更新,并在奖励中区分接近、闭合、持握和释放阶段。

小节练习

  1. 设计 Go2+ARX5 的分组 action scale,并说明每个 scale 的物理依据。
  2. 比较 q_des = q_default + scale * actionq_des = q_ref + scale * action 的适用任务。
  3. 在仿真中固定策略为随机动作,扫描不同 arm scale,记录基座 roll/pitch 的标准差。

74.5 奖励设计:末端跟踪与基座稳定的冲突 ⭐⭐⭐

这一节解决的问题是:如何把“手要准”和“身体要稳”写成不会互相破坏的奖励。

动机:奖励函数是目标排序的语言

复合 RL 的奖励设计比纯腿足更难。

纯腿足速度跟踪的主目标通常是速度命令。

复合任务至少有两个主目标:行走稳定和末端任务。

这两个目标经常冲突。

手要够远,身体可能需要前倾。

手要够快,基座可能被反作用力矩扰动。

手要施力,足端法向力和摩擦裕度会变化。

奖励函数必须表达优先级,而不是把所有误差简单相加。

反面案例:奖励项量纲混乱

设奖励为:

\[ r = -\|\mathbf{p}_{ee}-\mathbf{p}_{ref}\|^2 - \|\boldsymbol{\theta}_{base}\|^2 \]

末端误差单位是米。

基座姿态单位是弧度。

如果末端误差 0.1 m,平方为 0.01。

如果基座 pitch 0.2 rad,平方为 0.04。

看似基座项更大。

但实际不同任务中误差范围、梯度和安全含义完全不同。

如果不做权重和归一化,奖励会隐含一个不透明的目标排序。

这就是为什么奖励项必须先做尺度分析,再谈权重。

奖励总体结构

推荐把复合奖励分成四层。

层级 内容 作用 示例
任务奖励 末端位置、姿态、速度、抓取成功 驱动操作目标 \(r_{ee}\)
稳定奖励 姿态、角速度、支撑裕度、速度跟踪 防止摔倒 \(r_{base}\)
安全惩罚 自碰撞、关节限位、力矩饱和 阻止危险行为 \(r_{safe}\)
品质正则 能耗、动作平滑、关节居中 改善可部署性 \(r_{reg}\)

最终奖励:

\[ r_t = w_{task}r_{task} + w_{stable}r_{stable} + w_{safe}r_{safe} + w_{reg}r_{reg} \]

注意安全项通常还应进入 termination,而不只是奖励。

自碰撞、严重跌倒、关节超限不能只靠一个负奖励慢慢学习。

它们应当立即终止 episode。

末端跟踪奖励

位置误差:

\[ e_p = \|\mathbf{p}_{ee} - \mathbf{p}_{ref}\| \]

姿态误差:

\[ \mathbf{e}_R = \log(\mathbf{R}_{ee}^{T}\mathbf{R}_{ref}) \in \mathbb{R}^3 \]

组合奖励:

\[ r_{ee} = \exp\left(-\frac{e_p^2}{\sigma_p^2}\right) + \alpha_R \exp\left(-\frac{\|\mathbf{e}_R\|^2}{\sigma_R^2}\right) \]

为什么用指数形式?

它把奖励限制在有界范围。

它让不同奖励项更容易平衡。

它在误差很大时不会产生巨大梯度。

但指数也有风险。

如果 \(\sigma_p\) 太小,训练早期误差很大,奖励接近 0,策略收不到信号。

所以复合任务中常用课程学习:先给大 \(\sigma_p\),让策略学会靠近;再逐步缩小 \(\sigma_p\),提高精度。

基座稳定奖励

基座稳定至少包括四类信号。

奖励项 公式 目的
姿态水平 \(-\|g_{xy}^{body}\|^2\) 限制 roll/pitch
横滚俯仰角速度 \(-\|\omega_{xy}\|^2\) 抑制晃动
垂直速度 \(-v_z^2\) 避免跳跃和跌落
支撑裕度 \(d(\text{CoM}, \partial \mathcal{P})\) 保持质心投影在支撑多边形内

纯腿足策略常用前三项。

复合操作建议加入支撑裕度或其近似。

因为机械臂外伸时,roll/pitch 可能还不大,但 CoM 投影已经接近支撑边界。

如果只看姿态,策略会低估危险。

操作与稳定的动态权重

复合任务不能使用固定权重解决所有阶段。

站立抓取时,可以提高末端精度。

快速行走时,应降低末端权重,优先保持稳定。

接触施力时,应把力控和基座稳定放在高优先级。

可以定义阶段权重:

\[ w_{ee}(t) = \begin{cases} w_{stand}, & \text{站立操作}\\ w_{walk}, & \text{行走中操作}\\ w_{contact}, & \text{接触施力} \end{cases} \]

其中通常:

\[ w_{stand} > w_{contact} > w_{walk} \]

也可以用速度命令连续调节:

\[ w_{ee} = w_{max}\exp(-\beta \|\mathbf{v}_{cmd}\|) \]

速度越大,末端权重越低。

这种设计把“走稳优先”写进奖励函数。

Advantage Mixing 的直觉

Deep-WBC 中提出的 Advantage Mixing 试图缓解腿和臂奖励互相污染。

普通 PPO 使用同一个 advantage 评估整条动作向量。

但腿动作主要影响行走稳定,臂动作主要影响末端跟踪。

如果末端误差很大,整条动作都会被惩罚,腿策略也会收到不该承担的负反馈。

如果基座摔倒,臂动作也会被同样惩罚,策略难以判断到底是哪部分出了问题。

Advantage Mixing 的思想是把不同奖励来源对应到不同动作子空间。

腿动作更关注 locomotion advantage。

臂动作更关注 manipulation advantage。

这不是完全解耦动力学,而是让信用分配更符合因果结构。

类比多人项目中的成绩分配:如果只给团队总分,每个人不知道自己哪里做错;如果把腿的稳定指标和臂的末端指标分开反馈,学习信号更清晰。

代码:奖励分项计算

import torch

def exp_tracking_reward(error, sigma):
    """有界指数追踪奖励。"""
    error_sq = torch.sum(error * error, dim=-1)
    return torch.exp(-error_sq / (sigma * sigma))

def whole_body_rewards(data):
    """计算 Go2+Arm 的核心奖励分项。"""

    # 末端位置和姿态误差
    r_ee_pos = exp_tracking_reward(data["ee_pos_err"], sigma=0.15)
    r_ee_rot = exp_tracking_reward(data["ee_rot_err"], sigma=0.50)

    # 基座稳定
    r_base_level = -torch.sum(data["projected_gravity_xy"] ** 2, dim=-1)
    r_base_ang = -torch.sum(data["base_ang_vel_xy"] ** 2, dim=-1)
    r_z_vel = -(data["base_lin_vel_z"] ** 2)

    # 能耗与动作平滑
    r_torque = -torch.sum(data["joint_torque"] ** 2, dim=-1)
    r_action_rate = -torch.sum((data["action"] - data["last_action"]) ** 2, dim=-1)

    # 安全项
    r_collision = -data["self_collision_count"].float()
    r_joint_limit = -data["joint_limit_violation"].float()

    # 速度越大,末端权重越低
    speed = torch.linalg.norm(data["velocity_cmd"][:, :2], dim=-1)
    w_ee = 2.0 * torch.exp(-1.5 * speed)

    reward = (
        w_ee * r_ee_pos
        + 0.5 * w_ee * r_ee_rot
        + 1.0 * r_base_level
        + 0.05 * r_base_ang
        + 0.5 * r_z_vel
        + 1.0e-5 * r_torque
        + 0.02 * r_action_rate
        + 2.0 * r_collision
        + 5.0 * r_joint_limit
    )

    terms = {
        "ee_pos": r_ee_pos,
        "ee_rot": r_ee_rot,
        "base_level": r_base_level,
        "base_ang": r_base_ang,
        "z_vel": r_z_vel,
        "torque": r_torque,
        "action_rate": r_action_rate,
        "collision": r_collision,
        "joint_limit": r_joint_limit,
        "w_ee": w_ee,
    }
    return reward, terms

注意这里的权重符号。

r_torquer_action_rater_collision 已经是负数。

最终乘正权重。

很多项目的 bug 来自“负奖励又乘了负权重”,结果变成鼓励错误行为。

奖励调试指标

只看总 reward 没有意义。

必须同时记录分项。

指标 正常趋势 异常含义
ee_pos 逐渐上升 末端目标不可达或权重太低
base_level 绝对值下降 基座在晃或臂扰动过强
torque 初期高,后期下降 动作粗糙或能耗惩罚太弱
action_rate 逐渐平滑 策略高频抖动
collision 接近 0 自碰撞几何或动作 scale 有问题
w_ee 随命令变化 动态权重未生效

⚠️ 常见陷阱

⚠️ 编程陷阱:奖励符号双重取负

错误做法:r_torque = -||tau||^2,配置中又写 torque = -1e-5

现象:策略被奖励使用大扭矩。

根本原因:负项和权重符号没有统一约定。

正确做法:统一约定“函数返回有符号项,配置权重为正”或“函数返回正代价,配置权重为负”,不要混用。

💡 概念误区:末端误差越小一定越好

错误想法:只要手更准,策略就更好。

实际情况:行走中末端 1 cm 精度可能换来基座大幅晃动和足端打滑。

正确理解:操作精度要在稳定性和安全约束内优化。

🧠 思维陷阱:一次性打开所有奖励项

错误想法:论文里有的奖励项都加上,训练会更完整。

现象:策略学不到主任务,或卡在不动的局部最优。

正确做法:先训最小任务奖励和关键安全项,再逐步加入正则,并做消融实验。

小节练习

  1. 设计一个站立抓取、慢走抓取、接触推门三阶段的动态末端权重函数。
  2. 写出一个奖励分项表,要求每一项都说明单位、典型数值范围和期望趋势。
  3. 做一个消融实验:删除 action_rate、放大 ee_pos、删除 base_ang_vel_xy,预测各自的行为变化。

74.6 Domain Randomization:含臂系统的鲁棒性设计 ⭐⭐⭐

这一节解决的问题是:复合机器人需要随机化哪些比纯腿足更多的因素。

动机:机械臂让 sim2real gap 放大

纯腿足策略已经需要随机化质量、摩擦、电机强度、延迟和传感器噪声。

加入机械臂后,gap 的来源更多。

臂末端可能拿起未知负载。

夹爪可能接触软物体。

腕部相机或 iPhone VIO 有延迟。

臂的惯量标定比腿更容易偏差。

末端接触力通过长力臂放大为基座力矩。

这些因素使同一个末端命令在仿真和真机上产生不同的全身响应。

如果训练中没有覆盖这些变化,策略会过度适应空载、理想臂、无延迟和刚性接触。

反面案例:只随机腿不随机负载

假设训练时随机化地面摩擦和腿部电机强度,却始终让机械臂空载。

策略会学到一种比较激进的臂摆动方式。

真机上夹爪拿起 1 kg 物体后,臂的等效惯量和重力力矩明显增加。

同样的动作导致基座前倾。

腿部策略虽然见过摩擦变化,却没见过“上方质量突然改变”的情况。

结果是行走仍然能走,但末端跟踪和支撑裕度同时恶化。

这说明复合 RL 的 DR 不能只沿用腿足参数表。

DR 的贝叶斯视角回顾

Domain Randomization 的训练目标是:

\[ \max_\theta \mathbb{E}_{\xi \sim p(\xi)} [J(\theta,\xi)] \]

其中 \(\xi\) 是环境参数。

真实参数 \(\xi_{real}\) 未知。

随机化分布 \(p(\xi)\) 应覆盖真实可能值。

如果范围太窄,真机落在分布外。

如果范围太宽,策略变得过度保守。

复合机器人中,\(p(\xi)\) 的维度更高。

因此更需要按物理来源分类,而不是随意列几个参数。

复合机器人随机化分类

类别 参数 典型范围 物理意义
全身质量 base/link mass \(\pm 10\%-20\%\) CAD 与装配误差
臂负载 payload mass 0-2 kg 或按任务 抓取物体质量
负载质心 payload CoM 夹爪附近几厘米 物体握持偏心
惯量 link inertia 随质量缩放 旋转响应
地面摩擦 foot friction 0.3-1.2 足端打滑
末端摩擦 gripper/object friction 0.2-1.0 抓取与推拉
电机强度 motor scale 0.85-1.15 执行器能力
控制延迟 action delay 0-40 ms 通信和执行器带宽
感知延迟 target delay 0-150 ms VIO/视觉高层延迟
IMU 噪声 angular velocity noise 按传感器标定 姿态估计
关节噪声 encoder noise 0.002-0.02 rad 编码器误差
外部推力 base push 0-100 N 碰撞与扰动
末端外力 ee wrench 0-50 N 推门、拉车、按压

负载随机化的物理约束

负载不是简单给总质量加一个随机数。

负载加在夹爪附近,会改变全身质心:

\[ \mathbf{p}_{com}' = \frac{m\mathbf{p}_{com} + m_L\mathbf{p}_L}{m + m_L} \]

其中 \(m_L\) 是负载质量,\(\mathbf{p}_L\) 是负载质心。

负载还改变臂末端的重力力矩:

\[ \boldsymbol{\tau}_{load} = \mathbf{J}_{ee}^{T} \begin{bmatrix} \mathbf{0} \\ m_L\mathbf{g} \end{bmatrix} \]

这意味着负载越远离基座,扰动越大。

随机化时应同时改变质量、质心和惯量。

只改质量不改惯量会产生非物理系统。

末端外力随机化

推门、拉抽屉、搬运和按压都涉及末端外力。

末端外力通过雅可比进入关节空间:

\[ \boldsymbol{\tau}_{ee} = \mathbf{J}_{ee}^{T}\mathbf{f}_{ee} \]

也通过整体动力学影响浮动基座。

训练中可以施加随机外力:

\[ \mathbf{f}_{ee} \sim \mathcal{U}(\mathbf{f}_{min}, \mathbf{f}_{max}) \]

但要注意可行性。

如果外力超过电机或摩擦极限,策略不可能稳定。

因此外力 curriculum 很重要。

先从 0-5 N 开始。

策略稳定后逐步增加到任务需要范围。

人形力敏感操作中常见 0-40 N、甚至更高的课程力。

四足臂小平台则要根据重量和电机能力保守设置。

延迟随机化

复合系统有两类延迟。

第一类是低层动作延迟。

策略输出到电机响应之间存在延迟。

第二类是任务目标延迟。

高层视觉、VIO 或操作策略输出的末端目标可能滞后 50-150 ms。

这两类延迟对策略影响不同。

动作延迟让所有关节命令滞后。

目标延迟让策略追逐旧目标。

因此应分开建模。

延迟类型 作用对象 训练实现 典型症状
action delay 关节命令 动作环形缓冲 动作相位滞后
observation delay 传感器 观测环形缓冲 状态估计慢
command delay 末端目标 目标轨迹缓冲 手追旧目标
random packet drop 高层目标 保持上一命令 末端卡顿

代码:动作和目标延迟缓冲

class DelayBuffer:
    """固定最大长度的延迟缓冲,训练时随机读取旧值。"""

    def __init__(self, max_delay_steps: int, initial_value):
        self.max_delay_steps = max_delay_steps
        self.buffer = [initial_value.clone() for _ in range(max_delay_steps + 1)]

    def push(self, value):
        self.buffer.insert(0, value.clone())
        self.buffer.pop()

    def sample(self, delay_steps):
        delay_steps = int(delay_steps)
        delay_steps = max(0, min(delay_steps, self.max_delay_steps))
        return self.buffer[delay_steps]


def apply_random_delay(current_action, current_target, action_buffer, target_buffer):
    """分别对动作和目标施加随机延迟。"""
    action_buffer.push(current_action)
    target_buffer.push(current_target)

    action_delay = torch.randint(low=0, high=3, size=()).item()
    target_delay = torch.randint(low=0, high=8, size=()).item()

    delayed_action = action_buffer.sample(action_delay)
    delayed_target = target_buffer.sample(target_delay)
    return delayed_action, delayed_target

真实系统中延迟不是常数。

随机延迟比固定延迟更接近通信和视觉管线。

DR 逐步加严

复合任务建议分阶段扩大随机化。

阶段 随机化内容 目标
S0 无随机化或极小噪声 验证任务和奖励正确
S1 腿足标准 DR 学会基本行走
S2 臂负载和惯量 DR 学会负载补偿
S3 末端外力 DR 学会接触扰动
S4 目标延迟和丢包 学会部署鲁棒性
S5 混合扰动 接近最终部署分布

这不是说正式训练必须从无 DR 开始。

而是调试时可以分阶段定位。

一旦确认环境正确,正式训练应尽早加入适度 DR。

⚠️ 常见陷阱

⚠️ 编程陷阱:随机化负载质量但不改变重力力矩

错误做法:只在观测或奖励里记录 payload,不把负载附加到物理模型。

现象:策略以为见过负载,实际动力学没有变化。

根本原因:随机变量没有进入物理转移函数。

正确做法:将负载作为刚体、固定关节或外力真正作用到仿真动力学中。

💡 概念误区:DR 范围越大越安全

错误想法:既然不知道真实参数,就把范围设得特别宽。

现象:策略保守、动作迟钝、末端跟踪精度低。

根本原因:策略在过宽分布上优化平均性能,需要牺牲单点性能。

正确做法:用硬件标定、系统辨识和实验日志收窄范围。

🧠 思维陷阱:只随机低层物理,不随机高层接口

错误想法:只要随机质量和摩擦,sim2real 就足够。

实际情况:操作任务常常被视觉/VIO 延迟、目标漂移和丢包击垮。

正确做法:对高层命令的延迟、噪声和保持策略也做训练建模。

小节练习

  1. 为“Go2+ARX5 拿起 0.5 kg 物体慢走”设计 DR 参数表,并说明每个范围如何从硬件估计得到。
  2. 推导负载质心前移 10 cm 对全身 CoM 的影响,假设机器人质量 15 kg,负载 1 kg。
  3. 实现目标延迟缓冲,对比无延迟训练和随机延迟训练在 100 ms 部署延迟下的末端误差。

74.7 Teacher-Student 与特权学习:让部署策略少看但不少懂 ⭐⭐⭐

这一节解决的问题是:如何利用仿真真值提高训练效率,同时不让部署策略依赖真值。

动机:训练时的信息优势不能浪费

仿真中有很多真机难以获得的信息。

摩擦系数、负载质量、精确接触力、外部扰动力、地形高度、物体真实位姿都可以直接读取。

如果完全不用这些信息,训练会更慢。

如果直接给 Actor,部署会失败。

Teacher-Student 和不对称 Actor-Critic 的价值就在这里。

它们让训练过程使用信息优势,而部署策略保持输入可行。

三种常见结构

结构 Actor 输入 Critic/Teacher 输入 训练复杂度 部署形式
对称 PPO 本体 + 命令 同 Actor 单策略
不对称 Actor-Critic 本体 + 命令 本体 + 特权 单 Actor
Teacher-Student Student 看本体历史 Teacher 看特权 Student
RMA 风格适应 Base + latent Encoder 看特权 Base + 适应模块

不对称 Actor-Critic

不对称 Actor-Critic 中,Actor 的输入保持部署可用。

Critic 可以看到特权信息。

PPO 的策略更新依赖 advantage。

Critic 更准确,advantage 噪声更低,Actor 学习更稳定。

部署时只保留 Actor。

这种方法实现简单,适合作为复合 RL 的第一版。

但它没有显式训练 Actor 从历史中推断隐变量。

如果任务强依赖负载、摩擦或接触状态,Teacher-Student 或 RMA 更有效。

Teacher-Student 蒸馏

Teacher 输入:

\[ o_T = [o_{proprio}, o_{cmd}, o_{priv}] \]

Student 输入:

\[ o_S = [o_{proprio}^{t-H:t}, a^{t-H:t-1}, o_{cmd}] \]

Teacher 通过 PPO 训练。

Student 用监督学习模仿 Teacher 动作:

\[ L_{BC} = \|\pi_S(o_S) - \pi_T(o_T)\|^2 \]

如果只用 Teacher 轨迹蒸馏,会出现分布偏移。

Student 有小误差后,会到达 Teacher 轨迹中没有的状态。

因此更稳健的方法是让 Student 参与采样,再用 Teacher 给这些状态标注动作。

这和 DAgger 的思想一致。

RMA 风格 latent 蒸馏

RMA 不直接蒸馏动作,而是蒸馏环境 latent。

Teacher Encoder 把特权信息编码为:

\[ z_t = E(o_{priv}) \]

Base policy 使用:

\[ a_t = \pi(o_{proprio}, z_t) \]

部署时 Adaptation Module 从历史估计:

\[ \hat z_t = \phi(o_{t-H:t}, a_{t-H:t-1}) \]

然后:

\[ a_t = \pi(o_{proprio}, \hat z_t) \]

这种结构的优点是职责清晰。

Base policy 学控制。

Adaptation Module 学辨识。

latent 不必等于真实物理参数。

它只需要包含“控制应该如何改变”的信息。

复合机器人中的特权信息

特权信息 用途 不应直接给 Actor 的原因
payload mass 负载适应 真机抓取物质量未知
payload CoM 惯量补偿 夹持位置难精确估计
foot friction 打滑适应 真实摩擦不可直接测
ee contact force truth 接触力学习 无力传感器时不可得
external push 抗扰学习 外力不可直接读取
object pose truth 操作目标 视觉估计有噪声延迟
terrain height truth 感知行走 真机只能由传感器估计

这些信息可以进入 Critic、Teacher 或用于 reward 计算。

进入 reward 不等于进入 Actor。

例如末端真实误差可以用于奖励,因为训练时需要评价任务完成度。

但 Actor 输入的末端目标和误差必须模拟部署可获得的估计。

代码:冻结 Teacher 的动作蒸馏

import torch
import torch.nn.functional as F

def distill_step(student, teacher, batch, optimizer):
    """Teacher-Student 动作蒸馏的一步。"""

    obs_student = batch["obs_student"]
    obs_teacher = batch["obs_teacher"]

    # Teacher 只提供标签,不参与梯度更新
    with torch.no_grad():
        target_action = teacher(obs_teacher)

    pred_action = student(obs_student)
    loss = F.mse_loss(pred_action, target_action)

    optimizer.zero_grad()
    loss.backward()
    torch.nn.utils.clip_grad_norm_(student.parameters(), 1.0)
    optimizer.step()

    return {"distill_loss": float(loss.detach())}

蒸馏阶段最重要的是冻结 Teacher。

如果 Teacher 也更新,Student 会追逐一个移动目标。

训练会变得不稳定。

代码:latent 蒸馏

def latent_distill_step(adaptation, privileged_encoder, batch, optimizer):
    """让历史编码器预测特权编码 latent。"""

    history = batch["history_obs_action"]
    priv = batch["privileged_obs"]

    with torch.no_grad():
        z_target = privileged_encoder(priv)

    z_pred = adaptation(history)
    loss = F.mse_loss(z_pred, z_target)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    return {"latent_loss": float(loss.detach())}

latent 蒸馏比动作蒸馏更模块化。

但它要求 Base policy 在训练时已经学会使用 latent。

工程实现上更复杂。

⚠️ 常见陷阱

⚠️ 编程陷阱:蒸馏时忘记关闭 Teacher 梯度

错误做法:Teacher 和 Student 同时进入优化器。

现象:loss 震荡,Student 总追不上 Teacher。

根本原因:标签本身在变化。

正确做法:Teacher eval(),参数 requires_grad_(False),并在 torch.no_grad() 中产生标签。

💡 概念误区:Student 性能差是因为信息少,无法避免

错误想法:Teacher 看真值,Student 不看真值,所以 Student 必然很差。

实际情况:很多隐变量能从历史响应中推断,例如负载、摩擦和接触。

正确理解:Student 的关键是历史窗口和训练分布,而不是单帧输入。

🧠 思维陷阱:所有任务都要两阶段蒸馏

错误想法:Teacher-Student 更高级,所以默认使用。

实际情况:平地短时末端跟踪可能用不对称 Actor-Critic 就足够。

正确做法:先用简单结构建立基线,再为感知、负载或接触复杂任务加入蒸馏。

小节练习

  1. 设计一个不对称 Actor-Critic 观测配置:Actor 看哪些量,Critic 看哪些量。
  2. 为随机负载任务设计 RMA latent,说明历史窗口应包含哪些信号。
  3. 分析动作蒸馏和 latent 蒸馏在四足臂推门任务中的优缺点。

74.8 训练流程:从能动到能用 ⭐⭐

这一节解决的问题是:如何把复合 RL 训练拆成可诊断、可复现的阶段。

动机:不要从最难任务开始

一个常见失败路径是:一开始就让机器人在随机地形上边走边抓取随机物体,还打开全部 DR。

这会让失败原因不可定位。

策略不收敛时,你不知道是末端目标不可达、奖励冲突、动作 scale 过大、随机化过强,还是模型坐标系错。

复合 RL 训练应像调试控制器一样分阶段。

先验证站立。

再验证臂跟踪。

再加入慢速行走。

再加入负载和延迟。

最后再做复杂任务。

推荐训练阶段

阶段 任务 主要目标 通过标准
P0 单环境随机动作 环境无 NaN 200 步无异常
P1 站立保持 基座稳定 roll/pitch 小
P2 站立末端跟踪 臂能到目标 EE 误差下降
P3 慢速行走 腿能稳走 速度跟踪可用
P4 行走中末端跟踪 耦合学习 EE 与 base 同时稳定
P5 随机负载 鲁棒操作 不同负载成功
P6 延迟与外力 部署鲁棒 sim2sim 稳定
P7 任务流水线 高层接口 端到端完成

Curriculum 设计

课程学习不是只调地形难度。

复合任务至少有五个课程轴。

课程轴 从简单到复杂
末端目标距离 近距离小范围到远距离大范围
目标速度 静态点到连续轨迹
行走速度 站立到慢走到正常走
负载质量 空载到轻载到任务负载
扰动强度 无外力到小外力到任务外力

这些课程轴不要同时快速增加。

每次只增加一个主要难度。

否则训练曲线变化无法解释。

评估指标

复合 RL 的评估不能只看 episode reward。

至少应记录以下指标。

指标 单位 意义
EE position RMSE m 末端位置精度
EE orientation RMSE rad 末端姿态精度
base roll/pitch RMS rad 基座稳定性
base angular velocity RMS rad/s 晃动程度
CoM margin m 支撑裕度
torque RMS Nm 能耗和安全
action rate RMS normalized 平滑度
fall rate % 安全性
collision rate % 自碰撞风险
success rate % 任务完成度

训练日志解读

现象 可能原因 优先检查
reward 上升但成功率不升 reward hacking 成功条件和分项奖励
EE 误差下降但摔倒率上升 末端权重过高 base 奖励和动态权重
torque 很低但不动 能耗惩罚过强 torque/action 正则
action_rate 很高 动作噪声或频率过高 decimation 和平滑奖励
训练早期大量终止 action scale 或终止过严 随机动作回放
加 DR 后崩溃 随机化范围过大 单项 DR 扫描

代码:训练前配置检查

def validate_whole_body_cfg(cfg):
    """训练前做静态检查,尽早发现维度和权重错误。"""

    assert cfg.action_dim == cfg.num_leg_joints + cfg.num_arm_joints + cfg.num_gripper_joints

    assert cfg.leg_action_scale > cfg.arm_action_scale
    assert cfg.arm_action_scale > 0.0
    assert cfg.gripper_action_scale >= 0.0

    assert "ee_position_tracking" in cfg.reward_weights
    assert "base_orientation" in cfg.reward_weights
    assert "action_rate" in cfg.reward_weights

    for name, weight in cfg.reward_weights.items():
        if not isinstance(weight, float):
            raise TypeError(f"{name} 的权重必须是 float")

    if cfg.domain_rand.payload_mass_max > cfg.robot_mass * 0.2:
        print("警告:负载质量超过机器人质量 20%,需要确认硬件可行性")

    print("配置检查完成")

这类检查不能替代仿真,但能防止低级配置错误进入长训练。

sim2sim 测试

在真机部署前,应先做 sim2sim。

训练可能在 IsaacLab 中完成。

部署前可以把策略放到 MuJoCo、Gazebo 或另一个 IsaacLab 场景中测试。

sim2sim 的目的不是证明真机一定成功。

它用于暴露对仿真器细节的过拟合。

如果策略在 IsaacLab 中很好,在 MuJoCo 中完全失败,说明它可能依赖了 PhysX 的接触细节、关节阻尼或观测真值。

sim2sim 中应保留同样的观测接口和动作接口。

只替换物理后端和模型细节。

⚠️ 常见陷阱

⚠️ 编程陷阱:训练日志只记录总 reward

错误做法:只看 TensorBoard 的 episode reward。

现象:reward 上升但行为变差,无法定位原因。

正确做法:记录所有奖励分项、关键物理指标和成功率。

💡 概念误区:训练成功等于任务成功

错误想法:平均 reward 足够高就可以部署。

实际情况:reward 是代理指标,可能被策略钻空子。

正确理解:必须用任务成功率、物理安全指标和 sim2sim 稳定性共同判断。

🧠 思维陷阱:失败后立刻改算法

错误想法:PPO 不收敛就换 SAC、换网络、换更大模型。

实际情况:多数失败来自环境、奖励、随机化和动作接口。

正确做法:先做单环境、分项奖励、随机动作、短训练和消融,再考虑算法。

小节练习

  1. 为 Go2+Z1 设计 P0 到 P6 的训练课程,每阶段写出通过标准。
  2. 选三个指标说明为什么总 reward 无法替代它们。
  3. 设计一个 sim2sim 验证方案,把 IsaacLab 训练策略迁移到 MuJoCo 回放。

74.9 综合项目:从零搭建 Go2+Arm EE Tracking 环境 ⭐⭐

本章的综合项目是搭建一个最小可训练的四足臂全身 RL 环境。

目标不是一次做出完整抓取系统。

目标是建立一个可扩展的低层全身策略:输入本体感知和末端目标,输出腿臂关节位置目标,在站立和慢走中跟踪末端。

项目目标

  1. 加载 Go2+6DOF 臂统一模型。
  2. 定义 18 或 19 维关节位置动作。
  3. 构造部署可用 Actor 观测。
  4. 构造含特权信息的 Critic 观测。
  5. 实现末端位置与姿态跟踪奖励。
  6. 实现基座稳定、能耗和平滑正则。
  7. 加入负载、延迟和外力随机化。
  8. 完成站立末端跟踪与慢走末端跟踪两阶段训练。

项目目录建议

whole_body_ee_tracking/
├── assets/
│   └── go2_arm.usd
├── cfg/
│   ├── env_cfg.py
│   ├── obs_cfg.py
│   ├── reward_cfg.py
│   └── rand_cfg.py
├── tasks/
│   ├── commands.py
│   ├── observations.py
│   ├── rewards.py
│   ├── terminations.py
│   └── events.py
├── train.py
├── play.py
└── export.py

目录拆分要与环境概念一致。

观测、奖励和随机化不要混在一个大文件里。

里程碑 1:站立末端跟踪

固定基座速度命令为 0。

末端目标在机器人前方小范围采样。

只要求末端位置跟踪,不要求姿态精度。

奖励权重以基座稳定为主。

通过标准:

指标 阈值
fall rate < 5%
EE position RMSE < 8 cm
base roll/pitch RMS < 0.08 rad
self collision 接近 0

里程碑 2:慢走末端跟踪

加入小速度命令。

例如 \(v_x \in [0, 0.4]\) m/s。

末端目标仍在较小范围。

动态降低末端权重,避免走路时追手过激。

通过标准:

指标 阈值
velocity tracking RMSE < 0.15 m/s
EE position RMSE < 12 cm
fall rate < 10%
action rate RMS 持续下降

里程碑 3:随机负载

在夹爪处附加 0-0.8 kg 负载。

Actor 不直接看负载真值。

Critic 可以看负载质量。

通过标准:

负载 目标
0 kg 保持基线性能
0.3 kg EE RMSE 增加不超过 30%
0.8 kg 不摔倒,允许精度下降

里程碑 4:目标延迟

对末端目标加入 0-100 ms 随机延迟。

训练时记录目标时间戳。

评估时固定延迟 100 ms。

通过标准:

情况 目标
无延迟 基线精度
50 ms 精度轻微下降
100 ms 不出现持续振荡

项目中的关键检查点

检查点 检查方法
FK 正确 关节手动扫描,末端移动方向可视化
目标可达 采样目标用 IK 或几何范围过滤
奖励量级 训练前随机策略统计分项均值
动作安全 随机动作 roll/pitch 不爆炸
DR 生效 每个 episode 打印随机参数抽样
导出一致 导出策略与训练回放动作一致

综合练习

  1. 完成站立末端跟踪训练,并画出 EE RMSE、base roll/pitch、torque RMS 三条曲线。
  2. 在同一策略上分别测试空载、0.3 kg、0.8 kg,写出负载对基座稳定性的影响。
  3. 把 body-frame 末端目标改成 task-frame 末端目标,比较目标延迟下的世界系 EE 抖动。
  4. 将 Critic 特权信息中的 payload mass 删除,观察训练速度和最终鲁棒性变化。
  5. 结合 复合/30_多模态MPC,解释如果把 EE tracking 从 reward 移到 MPC cost,会改变哪些接口。

74.10 延伸阅读

主题 推荐材料 难度 阅读目的
腿足 RL 训练栈 足式/190_腿足RL训练栈 ⭐⭐ 回顾 IsaacLab、PPO、DR、Teacher-Student
简化模型 足式/70_腿足简化模型理论 ⭐⭐ 理解 CoM、ZMP、SRBD 与稳定性
统一动力学 复合/20_浮动基座臂统一动力学 ⭐⭐⭐ 理解臂对基座的耦合
多模态 MPC 复合/30_多模态MPC ⭐⭐⭐ 理解 EE cost、自碰撞与力控
Deep-WBC Fu et al. CoRL 2022 ⭐⭐⭐ 统一四足臂策略
Visual WBC Liu et al. CoRL 2024 ⭐⭐⭐ 视觉高层与低层全身策略
UMI-on-Legs Ha et al. CoRL 2024 ⭐⭐⭐ task-frame EE tracking 与跨平台操作
RMA Kumar et al. RSS 2021 ⭐⭐⭐ 历史适应模块

延伸阅读的重点不是记住每篇方法名称。

阅读时应追问四个问题。

第一,它把哪些信息给 Actor,哪些信息给 Teacher 或 Critic。

第二,它的动作空间是统一关节目标、残差动作,还是分层接口。

第三,它如何平衡末端任务和基座稳定。

第四,它如何处理 sim2real gap。


🔧 故障排查手册

症状 可能原因 排查步骤 相关知识
训练一开始大量摔倒 action scale 过大或 PD 增益不合适 1. 随机小动作回放 2. 降低臂 scale 3. 检查默认姿态 74.4
EE reward 不上升 目标不可达或坐标系错误 1. 可视化目标 2. 手动 FK 验证 3. 暂时固定基座 74.3/74.5
EE 准但基座很晃 末端权重过高 1. 打印 base 指标 2. 降低行走时 EE 权重 3. 加动作平滑 74.5
策略不动 能耗或动作惩罚过强 1. 关闭部分正则 2. 提高任务奖励 3. 检查 alive 奖励 74.5
加负载后失败 payload DR 不足 1. 增加负载随机化 2. 检查负载是否作用到物理模型 3. 加历史观测 74.6/74.7
导出后行为异常 观测顺序或归一化不一致 1. 打印训练和部署观测 2. 用固定状态比对 3. 共用观测配置 74.2/74.3
sim2sim 失败 过拟合仿真器接触或电机模型 1. 增加 DR 2. 调整电机延迟 3. 比较接触力和力矩 74.6/74.8
自碰撞频繁 臂目标采样不安全或缺少碰撞惩罚 1. 可视化目标范围 2. 加自碰撞终止 3. 缩小腕部 scale 74.4/74.5
末端高频抖动 策略频率过高或 action_rate 太弱 1. 提高 decimation 2. 加动作二阶平滑 3. 检查目标延迟 74.4/74.6
Critic loss 很低但 Actor 部署失败 特权信息泄漏到 Actor 或训练部署观测不一致 1. 检查 policy 观测组 2. 删除不可部署输入 3. 做导出回放 74.3/74.7

本章小结

知识点 核心结论 工程动作
问题建模 复合 RL 难在耦合和部分可观测 用历史和特权学习处理隐变量
IsaacLab 环境 管理器对应 MDP 构件 分离 action、observation、reward、event
观测空间 Actor 只能看部署可得信息 命名观测顺序并统一训练部署
动作空间 关节位置目标是安全先验 腿臂夹爪分组 scale
奖励设计 末端精度必须服从稳定安全 分项记录并动态调权
随机化 含臂系统要随机负载、延迟和末端外力 分阶段扩大 DR
特权学习 训练可用真值,部署不能依赖真值 使用不对称 Critic、Teacher-Student 或 RMA
训练流程 从能动到能用需要阶段化 P0 到 P7 逐步验证

本章回答了“如何把腿足 RL 扩展成全身 RL”的基础问题。

下一章将把低层全身策略放进更完整的操作技能接口中,讨论高层 Diffusion Policy、ACT、VLA 或遥操作系统如何向低层发送 task-frame 末端命令,并让机器人在移动中执行抓取、推拉和工具使用。

章末统一练习与故障排查

⚠️ 易错点一:只看单个指标。 40_RL全身控制基础 中的任何结论都应同时检查任务指标、物理约束和软件接口。只看总误差或总奖励,容易把模型错误误判为参数问题。

💡 易错点二:忽略坐标系和时间戳。 复合机器人控制链很长,坐标系、采样频率和延迟一旦没有显式记录,后续所有优化和学习结果都会失去解释力。

🧠 易错点三:把演示成功当成系统可靠。 教学实验应至少包含一次扰动、一次异常输入和一次日志复盘,才能说明方法的边界。

练习

  1. 选择本章一个核心公式,写出每一项的单位、坐标系和数据来源。
  2. 选择本章一个代码片段,说明它依赖哪些配置项;如果配置错一个符号,会出现什么日志现象?
  3. 设计一个只改变单个因素的实验,用来验证本章最关键的工程判断。

本质洞察:复合机器人文档中的公式、代码和项目不是三块孤立内容。公式定义可行边界,代码实现边界,项目用日志证明边界是否真实存在。

故障排查

症状 优先怀疑 验证动作
仿真正常但部署异常 观测、坐标系或时间戳不一致 用同一段日志离线回放训练端和部署端
指标突然变差 模式切换、限幅或安全壳触发 画出模式、保护标志和控制命令
调参没有效果 根因不是权重而是模型假设错误 回到最小实验,关闭无关模块
结果难以复现 配置没有版本化 保存模型哈希、配置哈希和随机种子