复刻 Microduck 的人盯仿真,最容易犯一个想当然的错误:以为仿真越像真机,策略越能用。真实答案是反的。Pollen Robotics 的 microduck_rl 仓库把模拟到现实(sim2real)的整套配方写得很清楚,域随机化(Domain Randomization,DR)是其中最核心的一环——不是让仿真更准,而是让它”故意不准”,让策略见过各种乱况,真机上才不会一碰就懵。
这篇文章不讲大道理,直接拆 microduck_rl 里那组真实的 DR 配置:哪些变量在随机、各加多少、背后为什么这么设,以及那条坑了团队几个月的”非累积”铁律。
DR 在解决什么问题
一句话:仿真里”太干净”是灾难。
你在干净仿真里训出一个完美步态,拿到真机上一跑,十有八九扑街。因为真机有摩擦散布、编码器零漂、IMU 安装歪了几度、每个舵机质量还不一样——这些”脏东西”仿真默认全没有。策略没见过这些变化,一遇到就慌。
域随机化的思路是反过来:训练时每个 episode 随机重置一批物理参数,让策略在”捏造出来的各种真实”里学会鲁棒。等到真机表面那些不确定性,落在策略见过的参数区间内,就直接接得住。
microduck_rl 的 README 原话是:这仓库把完整 sim2real 配方编码进去了——BAM 执行器物理、域随机化、反向间隙仿真、奖励设计教训。DR 就是这套配方的第二个核心。
看一组真实的 DR 配置
microduck_rl 的 microduck_velocity_env_cfg.py 里,每个随机化都是一个开关加一组范围。我把关键几个抄在下面:
质量与惯性(±5%)
MASS_INERTIA_RANDOMIZATION_RANGE = (0.95, 1.05)——质量和对地惯量一起,各随机 ±5%。这个开关注释特别写了一句话:”Can enable once walking is stable”(等走路稳定了再开)。因为质量随机化是最”伤”训练稳定性的,得先让基础步态训出来,再逐步加乱。
质心偏移(CoM,±3mm 起步,课程化爬到 ±8mm)
COM_RANDOMIZATION_RANGE = 0.003。这是拿 dr.body_ipos 在 reset 时给躯干质心加点偏移,而且范围是随训练进度课程化(ramped)的——先 ±3mm,稳定了再加大到 ±8mm。头部的质心也单独随机(±3mm同样课程化),因为 Microduck 那颗大头外挂着一堆传感器,重心对步态影响不小。质量随机化是整体尺度,质心随机化是位置这一个具体点,两个加起来模拟”打出来这批壳子和装配”的个体差异。
关节摩擦(±10%)
JOINT_FRICTION_RANDOMIZATION_RANGE = (0.9, 1.1)。这里有个技术细节很关键,藏着 DR 的一个大坑,我下面单独说。它随机的不是 MuJoCo 的 dof_frictionloss,而是 BAM 执行器内部那笔摩擦预算。
编码器偏置(±0.86°)
ENCODER_BIAS_RANGE = (-0.015, 0.015),换算过来大概 ±0.86°。每个环境(env)给它一个恒定的关节编码器零漂,动作观测(actor obs)里读到的是 joint_pos + bias。模拟的是每个舵机编码器出厂校准不一样那种事。
IMU 安装误差(最多 6°)
IMU_ORIENTATION_RANDOMIZATION_ANGLE = 6.0——随机轴的 6° 安装误差,而且明确”零中心”(zero-centered)。注释特意强调:它训练的是对偏移幅度的容忍,不是对俯仰偏置的容忍。因为真机板子有个约 5° 的系统性 pitch 偏差,是在 runtime 里用 imu-pitch-offset 源头纠正的,不在仿真里重复加。
速度推力(每 3-6 秒 ±0.3 m/s)
VELOCITY_PUSH_INTERVAL_S = (3.0, 6.0),VELOCITY_PUSH_RANGE = (-0.3, 0.3)。训练中途随机推一把速度,模拟被绊到、被碰到的扰动。这也是为什么鸭子能抗住真机上那些小磕碰。
电机电枢/转子惯量(±10%)
ARMATURE_RANDOMIZATION_RANGE = (0.9, 1.1)。注释点名这是 microban 风格——反射转子惯量。特别强调”Does affect BAM”(armature 是设了值,不是清零),说明它和 BAM 执行器是联动的。
这些不是模型凭空编的参数,是仓库里原样能查到的 cfg。每个开关还有配套的 ENABLE_* 丰值,可以单独开合,方便做消融实验。
最大的坑:DR 绝不能跨 episode 累积
这是 microduck_rl 里最值钱的一条经验。CLAUDE.md 写得很重:
> Domain randomization must not accumulate across resets.(DR 绝不能跨重置累积)——曾经有一个累积性质的 CoM 随机化器,拖垮了每一个长训练,持续了好几个月。
为什么累积会出问题?因为如果你的随机化实现是把”当前值再往一个方向加一点”,那跑了几百个 episode 之后,质心可能已经漂到物理上根本不可能的位置,或者偏移量超出真实情况好几个量级。策略学到的不是”真实范围里的鲁棒”,而是”一套荒谬环境下的扭曲行为”。训练日志看起来 loss 在降,实际在学一堆没用的东西,真机照样摔。
解决办法:每个 reset 重新读取编译期默认值,先还原再施加新随机样本——也就是”非累积”。microduck_rl 用的是 mjlab 1.3.0 的 dr.* 操作,operation="add"/"scale" 原生就是非累积的(它们每次 reset 重读默认字段)。但自定义的 DR 函数必须自己保证 restore-then-apply。这条坑之所以阴,是因为它不报错、不崩、训练显得正常,只有真机验证时才暴露。
别小看那个摩擦的细节
前面说关节摩擦随机化有个技术细节,这里展开。microduck_rl 专门写了一个执行器子类 FrictionDRBamActuator,注释说得很明白:
在 BAM 模式下,MuJoCo 的 dof_frictionloss 是被清零的,BAM 自己在 compute() 里算摩擦。 所以直接去随机化 dof_frictionloss,等于随机了个寂寞——静默无效(silent no-op)。这是 CLI 文档里点名警告过的坑。
于是团队写了个薄子类,加一个 per-env 的 friction_scale,去乘 BAM 内部那笔与速度无关的摩擦预算(Coulomb + Stribeck + load-dependent,也就是粘滞/齿轮箱那部分主宰 sim2real 不确定性的项)。粘性项(与速度成正比)保持标称值。而且它同样遵循非累积原则:friction_scale 每个 episode 先 reset 回 1.0,再由 randomize_bam_friction 事件重新采样。
这个细节给所有复刻者的启示是:照抄官方 DR 配置之前,先确认你用的执行器模型摩擦到底从哪来。 换个 actuator 实现,同一个 DR 写法可能就静默失效了。
对复刻的人意味着什么
Microduck 的 DR 不是”加个随机数”这么简单,它是一套精心配平的体系:每个变量一个独立开关、一组保守先证明稳定的范围、能课程化地逐步加大、严格保证非累积。这才是它从 1.3 版一路推到今天的底气。
如果你在复刻,我的建议是:先别自己发明 DR 范围。 照搬官方这套保守取值(±5% 质量、±8mm 最大质心、±10% 摩擦、±0.86° 编码器、6° IMU),把步态训稳了,再一点点加码。自己乱调范围,容易一上来就把训练搞崩,还找不到原因——因为 DR 一旦累积起来,是不报错的。
还有一个更贴近实战的提醒:真机验证才是 DR 宁非调参也骗不过的裁判。 仿真里 loss 再听话,都不如鸭子真站起来的 5 秒钟可信。DR 的参数区间开多少,最终要以”真机能不能覆盖”来反推,而不是以”训练曲线好不好看”来定。
数据来源:
- Pollen Robotics. microduck_rl 仓库:README.md、CLAUDE.md、src/mjlab_microduck/tasks/microduck_velocity_env_cfg.py、src/mjlab_microduck/actuator/friction_dr_bam.py(DR 配置范围、BAM 摩擦细节、非累积铁律)。GitHub.
- Pollen Robotics. microduck 仓库(约 800g/25cm、14 个 Dynamixel XL330、stability,50Hz 控制循环、ONNX 策略部署)。GitHub.
- 复刻项目进度记录: duck.whatled.com.
