🔄 sim2real 部署步骤拆解

从"仿真里会走"到"真机上能走" —— 十步完整流程,每步给出目标、做法、坑点与验收标准
域随机化 策略导出 真机部署 数据闭环 十步流程
🎯 本页学习目标
1. 能说清"仿真策略直接上真机为什么会失败"(sim2real gap 的 5 个来源)
2. 能按十步流程把训练好的策略部署到真机,并说出每步的验收标准
3. 理解域随机化、延迟注入、策略导出(ONNX/C++)、数据闭环各自的作用
4. 与 04-04 仿真与 RL 配合:那页讲"怎么训",本页讲"怎么上真机"
建议用时:约 30 分钟

1 什么是 sim2real gap?

在仿真里训练出一个"满分策略",直接拷到真机往往立刻摔倒或根本站不起来。原因是仿真和真机之间存在系统性差异,这些差异被统称为 sim2real gap。gap 越大,策略迁移越容易失败;而 sim2real 的全部工作,就是把 gap 缩小到策略能容忍的范围

差异来源仿真里常被简化成真机上的真实情况
物理精度理想刚体、库伦摩擦常数、零背隙柔性、变摩擦、减速器背隙、接触非线性
电机/执行器理想力矩源、无延迟力矩带宽有限、响应延迟、饱和、发热降额
通信/控制延迟同步无延迟控制周期 1-5ms、通信抖动、量化
传感器噪声真值直接给编码器/IMU 噪声、漂移、遮挡
算力桌面 GPU 无限制机载算力受限,策略推理要实时(常见 50Hz)
💡 一句话原则:策略能容忍的"随机性"越大,迁移成功率越高。所以 sim2real 的核心手段不是"把仿真做得多逼真",而是"把策略练得多鲁棒"(域随机化),再配合真机侧的工程化对齐(延迟、频率、保护)。

2 十步部署流程

以下十步按顺序执行;每一步都有"验收标准",达标再进下一步。可对照 04-04 的训练流水线一起看。

1
真机建模(URDF / MJCF)
目标:在仿真里搭一个与真机结构一致的模型。做法:量测真实质量/惯量/连杆长度/关节限位,写进 URDF;不要用"默认参数"糊弄。坑:惯量张量(尤其转子和末端)误差大。验收:仿真里自由摆动/摔倒的姿态与真机肉眼一致。
2
参数辨识(电机 / 摩擦 / 阻尼)
目标:把电机 KV、相电阻、库伦+粘滞摩擦、减速器效率实测出来。做法:开环阶跃/斜坡实验 + 最小二乘拟合(或用 SimpleFOC/ODrive 的辨识例程)。坑:摩擦随温度/速度变化,取"工作区间"的均值。验收:同一指令下仿真与真机的电流/转速曲线趋势一致。
3
域随机化(让策略"见过世面")
目标:训练时随机扰动物理参数,让策略对参数变化不敏感。做法:每个 episode 随机采样质量(±10-20%)、摩擦、力矩缩放、地面摩擦/坡度、推搡力等。坑:范围太小=没效果,范围太大=学不会;先小后大逐步加。验收:策略在随机化后的环境里仍能稳定完成任务(成功率 ≥ 90%)。
4
动作延迟注入
目标:让策略适应真实控制延迟。做法:训练时把动作延迟 1-3 个控制周期(如 20-60ms)再作用到环境。坑:只延迟观测不延迟动作、或延迟随机化缺失,都会让策略上真机"手抖"。验收:策略在 40ms 延迟下不掉分(或掉分 < 5%)。
5
观测噪声注入
目标:让策略适应传感器噪声。做法:给角度/角速度/线速度/IMU 加高斯噪声(幅值取真机实测的 1-1.5 倍)。坑:角度直接用"真值"会让策略依赖不存在的信号。验收:策略在噪声下仍稳定,且不发生高频抖动。
6
奖励塑形与防 hack
目标:设计让策略"真的会走"而非"钻空子"的奖励。做法:速度跟踪 + 能量惩罚 + 姿态惩罚 + 存活奖励的组合;加入"课程"(先站立→再行走)。坑:常见 hack:腿高频抽动、原地滑步、靠"摔倒前一刻"刷分。验收:回放 rollout 视频,确认动作自然、无异常抖动。
7
PPO 训练(与 04-04 衔接)
目标:训练出收敛策略。做法:Isaac Lab / legged-gym 跑 PPO;观察回报曲线与"提前终止"占比。坑:奖励曲线涨但实测表现差 → 奖励有问题。验收:训练收敛、rollout 成功率达标、策略确定性(无随机动作方差发散)。
8
策略导出(ONNX / TorchScript → C++ / Rust)
目标:把 PyTorch 策略转成真机能跑的推理。做法:导出 ONNX,用 onnxruntime(CPU/GPU)或转 C++ 在机载电脑推理;固定输入/输出张量形状。坑:忘记把"随机动作的均值"当最终输出(推理时不要采样);数值精度(float32→float16)可能抖动。验收:同一输入下,ONNX 输出与 PyTorch 输出误差 < 1e-3。

配套代码 1 — Isaac Lab 域随机化配置(对应第 3 步,建议在训练前就配好):

# Isaac Lab:用 EventTerm 声明域随机化(ManagerBasedRLEnv 的 events 配置)
import isaaclab.envs.mdp.events as mdp_events
from isaaclab.envs import ManagerBasedRLEnvCfg
from isaaclab.managers import EventTermCfg as EventTerm
from isaaclab.utils import configclass

@configclass
class MyEnvCfg(ManagerBasedRLEnvCfg):
    events = ManagerBasedRLEnvCfg.events  # 在原有 events 上追加以下随机化项

    events.randomize_link_mass = EventTerm(
        func=mdp_events.randomize_rigid_body_mass,       # 随机化连杆质量
        mode="startup",                                   # 每次环境 reset 前抽样一次
        params={
            "asset_cfg": SceneEntityCfg("robot"),
            "mass_distribution_params": (0.8, 1.2),       # 质量缩放区间:标称值 ±20%
            "operation": "scale",
        },
    )
    events.randomize_friction = EventTerm(
        func=mdp_events.randomize_rigid_body_material,   # 随机化地面摩擦
        mode="startup",
        params={
            "asset_cfg": SceneEntityCfg("robot"),
            "static_friction_range": (0.4, 1.2),          # 静摩擦区间(覆盖瓷砖/草地/地毯)
            "dynamic_friction_range": (0.3, 1.0),         # 动摩擦区间
            "restitution_range": (0.0, 0.0),
        },
    )
    events.randomize_push = EventTerm(
        func=mdp_events.push_by_setting_velocity,         # 随机推力扰动(抗扰训练)
        mode="interval",                                  # 每隔 interval_s 秒推一次
        interval_range_s=(5.0, 10.0),
        params={
            "velocity_range": {"x": (-0.5, 0.5), "y": (-0.5, 0.5)},  # 推力等效速度 ±20% 量级
        },
    )

配套代码 2 — ONNX 导出与数值校验(对应第 8 步的验收项):

import torch, numpy as np, onnxruntime as ort

actor.eval()  # 推理导出:确定性前向,取均值不采样
obs = torch.randn(1, obs_dim)  # 固定形状的示例输入
torch.onnx.export(
    actor, obs, "policy.onnx",
    input_names=["obs"], output_names=["action"],
    opset_version=17, dynamo=False,
)
sess = ort.InferenceSession("policy.onnx", providers=["CPUExecutionProvider"])
onnx_out = sess.run(["action"], {"obs": obs.numpy()})[0]
pt_out = actor(obs).detach().numpy()
diff = np.abs(pt_out - onnx_out).max()
assert diff < 1e-3, f"ONNX 与 PyTorch 输出偏差过大: {diff}"
print(f"max|Δ| = {diff:.2e}  ✓ 校验通过")
9
真机部署(接口 / 频率 / 保护)
目标:策略推理 → 关节指令 的实时闭环。做法:上位机 50-100Hz 推理,输出目标关节角度/力矩 → 底层 FOC 环(见 04-02)执行;对齐频率、加死区、加力矩/速度限幅。坑:频率不齐(推理 50Hz vs 电机 10kHz)要经插值;缺限幅会烧电机/损坏关节。验收:上电后先用"悬吊/支撑"模式小力矩试运行,确认动作方向、限位、急停都正确。
10
数据闭环(真机数据 → 修正 → 再训练)
目标:用真机数据持续改进。做法:记录真机状态/动作/失败片段 → 回灌到仿真(做"重放"或修正参数)→ 针对性再训练。坑:只收集成功数据会掩盖失败模式,摔倒/异常数据更珍贵。验收:形成一个"仿真训练→真机试跑→数据回传→改进"的可重复闭环。

3 三个最关键的工程细节

① 控制频率对齐:策略(如 50Hz)与电机控制环(如 10kHz)频率不同。策略输出的目标要经插值/平滑后再下发;否则真机会"一格一格"抖动。
② 力矩限制与急停:真机第一原则是"不伤人、不摔坏"。务必加力矩上限、速度上限、倒地自动断电/抱闸,并设物理急停。
③ 先软后硬、先支撑后自由:首次上真机一定在"悬吊支架"或"人工扶住"下小力矩试跑,确认无误再逐步放开 —— 这是无数人形/四足项目的标准安全流程。

4 常见误区

✗ "仿真跑分高 = 真机能跑":仿真 100 分,真机可能 0 分 —— gap 没缩小前,分数没有迁移意义。
✗ "跳过域随机化":不做域随机化的策略,几乎必然在真机上因一点点参数偏差而失败。
✗ "推理时还在采样动作":训练时动作是"高斯采样",推理时必须取均值;继续采样 = 真机乱抖。

5 本节自测

1. 训练策略时,让仿真器随机扰动质量/摩擦/地面参数,目的是什么?
💡 这就是"域随机化"的核心:让策略适应参数不确定性,从而在真机(参数未知/有偏差)上也能工作。
2. 策略从 PyTorch 部署到真机时,推理阶段应该输出什么?
💡 训练时动作来自"均值+噪声"的高斯分布;推理(部署)时必须取均值(确定性),否则真机动作会随机抖动。
3. 首次把策略上真机时,最安全的做法是什么?
💡 安全第一:先软后硬、先支撑后自由,配合力矩/速度限幅与急停,是行业标准流程。
4. 下面哪项是"sim2real gap"的来源?
💡 电机不是理想力矩源,有延迟、带宽、饱和、发热降额 —— 这是仿真常忽略、真机必存在的 gap 来源之一。
5. 在第 10 步「数据闭环」中,真机试跑产生的数据里,哪种最值得回灌到仿真?
💡 见第 2 节第 10 步:只收集成功数据会掩盖失败模式,摔倒/异常数据更珍贵,它们能暴露 sim2real gap 的真实分布。
6. 关于「仿真跑分高 = 真机能跑」这个想法,正确的理解是?
💡 见第 4 节常见误区:仿真 100 分、真机可能 0 分。关键是缩小 sim2real gap,而不是盲目追求仿真分数或画面逼真。
7. 策略推理是 50Hz,而电机 FOC 控制环是 10kHz,正确做法是?
💡 见第 3 节工程细节①:策略(如 50Hz)与电机环(如 10kHz)频率不同,策略输出的目标要经插值/平滑后再下发,否则真机会「一格一格」抖动。
📌 本节要点速查:①sim2real gap 来自物理、执行器、延迟、噪声、算力五处差异;②十步流程:建模→辨识→域随机化→延迟注入→噪声注入→奖励塑形→PPO 训练→导出(ONNX/C++)→真机部署→数据闭环;③推理时取动作均值而非采样,导出后校验输出误差小于 1e-3;④三个工程铁律:频率对齐(插值)、限幅与急停、先支撑后自由。
📌 本节小结:sim2real gap 来自物理、执行器、延迟、噪声、算力五处差异;十步流程(建模→辨识→域随机化→延迟注入→噪声注入→奖励塑形→训练→导出→部署→数据闭环)把 gap 逐步缩小;三个工程铁律是"频率对齐、限幅急停、先支撑后自由"。
🤔 思考题: 1. 如果你的策略在仿真里能走、上真机却"高频抖腿",最可能漏了哪一步?
2. 域随机化范围设得太大或太小,分别会发生什么?
3. 真机上只有 50Hz 的推理能力,你会怎么设计观测/动作与控制频率?

6 参考来源