sim2real 配方
最后更新
策略在仿真里走得很好、上机就摔,是这类项目最常见的失败模式。microduck_rl 把整套 sim2real 配方编码进了仓库本身,而不是留在某个人的经验里。
这一页梳理它做对了哪几件事——理解这些,比照抄参数更有用。
一、执行器物理不能用理想模型
用的是 BAM 的 M6 模型来建模 Dynamixel XL330 舵机,包含:
- 电压控制律
- 反电动势(back-EMF)
- 库仑摩擦、Stribeck 摩擦、负载相关摩擦
这一层是 sim2real 的地基。把舵机当成理想力矩源,仿真里学到的步态在真机上几乎必然失效——真实舵机的摩擦和电压特性会吃掉策略依赖的那部分响应。
二、随机化的是物理参数,不是噪声
域随机化逐环境施加在这四个量上:
| 随机化对象 | 为什么是它 |
|---|---|
| 电池电压 | 满电和快没电的鸭子,舵机能出的力矩不一样 |
| 负载下的电压跌落 | 多个舵机同时发力时电压会塌,这直接影响步态 |
| 指令延迟 | 真实系统的控制链路有延迟,策略必须对此鲁棒 |
| 摩擦力大小 | 个体差异与磨损 |
实现是 src/mjlab_microduck/actuator/ 里的 FrictionDRBamActuator。
注意这四个都是物理参数,不是往观测里加高斯噪声。这是两种完全不同的随机化——前者让策略学会应对真实世界的参数分布,后者只是让它对观测噪声不敏感。
三、齿隙必须建模在正确的一侧
这是最容易做错的一环,也是 microduck_rl 里最值得学的实现细节。
每个主任务都有 Backlash 变体,在 14 个舵机关节上各串联 ±1°(总计 2°) 的齿轮间隙。
关键在于编码器的位置:
真实机器人的编码器装在齿隙的输出侧。
所以仿真里必须匹配这个结构:
- 每个舵机加一个非驱动的
passive_<joint>_backlash铰链; - 固件 PD 模拟(
BacklashEncoderBamActuator)和joint_pos/joint_vel观测都透过齿隙读取——即qpos[servo] + qpos[backlash]。
如果把编码器建模在齿隙的输入侧,策略会以为自己精确知道关节位置,而真机上它读到的是齿隙之后的值。这个差异在静态时看不出来,在换向时集中爆发。
还有一个工程上的好处:观测维度和动作维度都不变,所以 ONNX 导出和机上运行时完全不需要改动。带齿隙训练的策略和不带齿隙的是可以直接互换的。
四、多个策略共享一份观测契约
部署时运行时会在行走 / 恢复 / 特技策略之间热切换,背后是一份共享的 61 维观测契约。
这个设计的意义是:任何一个策略都能在任意时刻接管机器人。跌倒恢复策略不需要等行走策略“交接”,它随时可以介入。
在仿真里排练这件事:
uv run scripts/infer_policy.py --walking walk.onnx --standing stand.onnx \
--sitstand sitstand.onnx --roulade roulade.onnx --new-cmd-obs
这个脚本排练的正是机上运行时的切换逻辑。它支持 --debug、--save-csv、--record,专门用于做 sim2real 对比——同一段指令序列在仿真和真机上各跑一遍,对比曲线。
上机之后怎么看
策略上机之后,robotctl monitor 是主要的观察窗口。它把客户端请求了什么和实际执行了什么并排显示,并在两者不同时说明原因。
sim2real 阶段最常见的情况是安全限制在钳制策略输出——摇杆推到底但机器人不动,原因会明确写出来,比如:
deadman — no intent arrived recently, velocity zeroed
底部边框会显示当前加载的哪个 .onnx。这一点在换策略调试时很重要:walk 是一个模式名,两个步态完全不同的版本都会报告成 walk。
导出完整状态做离线分析:
robotctl monitor --json --hz 50 > run.jsonl
配合 infer_policy.py --save-csv 的仿真数据,就能做逐关节的 sim2real 对比。