YouDuck.ai

sim2real 配方

最后更新

策略在仿真里走得很好、上机就摔,是这类项目最常见的失败模式。microduck_rl 把整套 sim2real 配方编码进了仓库本身,而不是留在某个人的经验里。

这一页梳理它做对了哪几件事——理解这些,比照抄参数更有用。

一、执行器物理不能用理想模型

用的是 BAMM6 模型来建模 Dynamixel XL330 舵机,包含:

  • 电压控制律
  • 反电动势(back-EMF)
  • 库仑摩擦、Stribeck 摩擦、负载相关摩擦

这一层是 sim2real 的地基。把舵机当成理想力矩源,仿真里学到的步态在真机上几乎必然失效——真实舵机的摩擦和电压特性会吃掉策略依赖的那部分响应。

二、随机化的是物理参数,不是噪声

域随机化逐环境施加在这四个量上:

随机化对象 为什么是它
电池电压 满电和快没电的鸭子,舵机能出的力矩不一样
负载下的电压跌落 多个舵机同时发力时电压会塌,这直接影响步态
指令延迟 真实系统的控制链路有延迟,策略必须对此鲁棒
摩擦力大小 个体差异与磨损

实现是 src/mjlab_microduck/actuator/ 里的 FrictionDRBamActuator

注意这四个都是物理参数,不是往观测里加高斯噪声。这是两种完全不同的随机化——前者让策略学会应对真实世界的参数分布,后者只是让它对观测噪声不敏感。

三、齿隙必须建模在正确的一侧

这是最容易做错的一环,也是 microduck_rl 里最值得学的实现细节。

每个主任务都有 Backlash 变体,在 14 个舵机关节上各串联 ±1°(总计 2°) 的齿轮间隙。

关键在于编码器的位置

真实机器人的编码器装在齿隙的输出侧。

所以仿真里必须匹配这个结构:

  1. 每个舵机加一个非驱动passive_<joint>_backlash 铰链;
  2. 固件 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 对比。

相关