# Microduck 的 RL 训练流水线:从冒烟测试到 ONNX 部署的完整工作流
Meta Description: 从 64 环境的冒烟测试到 4096 环境的正式训练,再到 ONNX 导出和真机部署——Microduck 的 RL 训练流水线拆解,每一步为什么这么做,踩过什么坑。
如果你在复刻 Microduck,迟早要面对一个问题:官方给的 9 个 ONNX 策略绑定的是 XL330 舵机的动力学参数,换了国产飞特 HL 系列后,这些策略只能降级为参考。想要鸭子真正走起来,你得自己训策略。
这篇文章不讲奖励函数设计(上一篇已经拆过),而是把 从零到 ONNX 部署的完整训练工作流 拉一遍——mijlab + PPO 的脚手架怎么搭、冒烟测试怎么做、正式训练跑什么参数、出了 NaN 怎么排查、最后怎么导出能在真机上跑的 ONNX 文件。
整个工作流的全景图
microduck_rl 仓库的训练流水线分为四个阶段,每个阶段的产物是下一个阶段的输入:
阶段 1: 环境搭建 → MJCF 模型 + BAM 执行器参数
阶段 2: 冒烟测试 → 64 环境 × 5 迭代,验证配置不崩
阶段 3: 正式训练 → 4096 环境 × 数万迭代,产出 PTH 检查点
阶段 4: ONNX 导出 → 烘焙观测归一化参数,产出 .onnx 文件
然后这个 .onnx 文件被 robotd 运行时加载,在 50Hz 控制循环里实时推理。
阶段 1:环境搭建——MJCF 模型 + BAM 执行器
训练的第一步不是在代码里调奖励权重,而是确保仿真里的鸭子和你手上的鸭子物理一致。
Microduck 的 MJCF 模型从 Onshape 导出,通过 onshape-to-robot 工具生成 XML。仓库里维护了 4 个模型变体:
- **robot_walk.xml**:用于 Velocity 任务。去掉了躯干和头部的碰撞检测——因为走路摔倒的代价”便宜”,不需要精确的全身碰撞
- **robot_groundcontact.xml**:用于 VelStand、StandUp、SitStand、GroundPick、BallKick、Roulade。精心筛选了与地面接触的零件碰撞体
- **robot_groundcontact_rollers.xml**:轮式任务的碰撞模型
- **robot_allcollisions.xml**:全碰撞模型,每个零件都有碰撞体
如果你在复刻时换了舵机,需要改的不是 MJCF 模型本身,而是 BAM 执行器参数。BAM(Better Actuator Models)把舵机建模拆成 6 个阶段:电机参数(M1)、粘性摩擦(M2)、库仑摩擦(M3)、静摩擦(M4)、反向间隙(M5)、弹性变形(M6)。这些参数定义在 microduck_constants.py 里,对应 XL330 的出厂值。换了飞特 HL-1910 后,M1-M6 全都要重测。
一个容易忽略的细节:BAM 参数注册必须在环境配置启动时完成。CLAUDE.md 里专门强调了一条规则:
> 任何独立的环境配置必须注册 expand_bam_friction_fields 启动事件。
如果不注册这个事件,BAM 的执行器摩擦模型不会生效——训练时 MuJoCo 的 dof_frictionloss 是 0,策略在仿真里走得很稳,一上真机就抖。这个 bug 极其隐蔽,因为仿真效果看起来完全正常。
阶段 2:冒烟测试——用最小的代价发现最大问题
官方 CLAUDE.md 有一条黄金规则:
> Always smoke test before any long run. A 5-iteration smoke test at 64 envs catches ~95% of config errors for cents.
命令只有一行:
uv run train Mjlab-Velocity-Flat-MicroDuck --env.scene.num-envs 64 --agent.max_iterations 5
这 5 次迭代(在 64 个并行环境里)跑什么?
- **环境初始化**:MJCF 加载、BAM 执行器注册、域随机化参数分配、61 维观测空间验证
- **第一步前向推理**:PPO 用随机权重输出动作,环境 step,看观测和奖励是否正常
- **梯度回传**:第一个 PPO 更新步骤,确认 loss 不为 NaN
- **观测归一化器填充**:normalizer 在这 5 步里收集初始数据分布
- **ONNX 导出测试**:检查 export 脚本能否处理这个 task 配置
CLAUDE.md 记录了一个常见陷阱:观测归一化器必须在 ONNX 导出时烘焙进去。冒烟测试里如果跳过 export,你不会发现这个问题——因为在 MuJoCo viewer 里跑时,normalizer 仍然生效,导出的 ONNX 在真机上就挂了。
所以冒烟测试的正确姿势是跑完整链路:train → export → infer,全通才算 smoke pass。
还有一个很重要的细节:冒烟测试不需要 GPU 也能跑。测试代码(tests/ 目录下)全部跑在 CPU 上,验证的是配置不变性——关节索引是否正确、奖励权重符号是否对、观测维度是否 61D。正式训练才需要 CUDA GPU。
阶段 3:正式训练——4096 并行环境 × 数万迭代
冒烟测试通过后,启动正式训练:
uv run train Mjlab-Velocity-Flat-MicroDuck --env.scene.num-envs 4096
4096 个并行环境是什么意思?不是仿真器跑 4096 次——而是 MuJoCo Warp 在 GPU 上同时模拟 4096 只鸭子,每个环境有自己的域随机化参数(电池电压、摩擦系数、舵机延迟、质量分布各不相同)。每个 tick,4096 个环境的观测同时进入 PPO 的 actor 网络,输出 4096 组动作,再同时 step。
这个架构有两个直接影响:
- **训练速度**:4096 环境 × 50Hz 控制频率 = 每秒 204,800 步环境交互。一张 RTX 4090 上,一个可用的步态大约 1-2 小时就能训出来
- **域随机化覆盖**:4096 个环境同时覆盖各种”身体条件”的鸭子,策略在仿真里就见过各种各样的偏差,到真机上自然适应
训练过程中的关键监控指标,官方 CLAUDE.md 列了几个必看的:
- **`Episode_Reward/
`**:每个惩罚项必须 ≤ 0。如果某个 penalty 项的值是正的,说明权重符号反了——策略在做”反向惩罚”,等于在奖励做这件事 - **`ep_len`**:平均每轮存活步数。expD 从 315 爬到 744,说明站稳了但步态没成型——ep_len 长不等于走得好,只是摔得少
- **`Episode_Reward/total`**:总奖励。如果总奖励在涨但步态没改善,检查是哪个分量在驱动上涨——可能是某个过拟合的”作弊”行为
奖励设计中的常见陷阱
CLAUDE.md 有一长段关于奖励设计的教训,值得单独拎出来:
关于符号约定:mijlab 的 cost 函数返回 ≥0(用负权重),而 microduck 的自定义惩罚函数返回 ≤0(用正权重)。混用会出大问题——把负权重加到自惩罚函数上,双重否定,策略会主动去犯错赚钱。
关于奖励门槛:不要给”到达某状态”设置一次性的高奖励。如果策略早到目标状态然后每步持续收钱,它会为了快速到达而使用暴力动作。正确做法是用 slewed 内部目标——奖励的是追进度的变化量,不是达到状态本身。
关于门控奖励:永远不要对”处于坏状态”(摔倒、位置太低)给予正奖励。策略会停在最便宜的那个坏姿势里不动。用 potential-based shaping 替代(奖励变化量,不是绝对量)。
关于正则化器:动作平滑惩罚(action_rate、joint_torque_rate)是无害的,会抑制抖动。运动抑制器(body_ang_vel、angular_momentum)对动态任务要放得很低,因为它们惩罚的就是动态动作本身需要的东西。
阶段 4:ONNX 导出——在仿真里能走 ≠ 在真机上能走
训练完成后,导出 ONNX:
uv run scripts/export.py Mjlab-Velocity-Flat-MicroDuck --wandb-run-path
这一步最关键的隐含操作:烘焙观测归一化器。训练时,PPO 的 actor 网络看到的是归一化后的观测(mean=0, std=1)。如果不烘焙归一化器,导出的 ONNX 在真机上收到的是原始观测值(关节角度 0-360°、速度 -10~10 rad/s),范围完全不对,策略输出全错。
export.py 做的事情简单说就是:
- 加载训练好的 PTH 检查点
- 从 wandb 记录中读取观测归一化器的均值和标准差
- 把这些参数作为 ONNX 图的一部分(图的前几个节点做归一化)
- 输出完整的 ONNX 文件
不要在仿真 viewer 里测试导出的 ONNX 然后说”好了”。在 MuJoCo viewer 里跑时,即使不烘焙归一化器,viewer 也会自动应用它——导出脚本在 viewer 场景下隐藏了这个 bug。
正确的验证姿势是用 infer_policy.py 在 CPU MuJoCo 里跑:
uv run scripts/infer_policy.py --walking output.onnx
这个脚本不加载 PTH 检查点,只加载 ONNX。如果在这里能走,才算真过。
从零训一条策略要多久?
以 4096 环境在 RTX 4090 上为例:
- **冒烟测试**:~2 分钟
- **初次训练(50k 迭代)**:2-3 小时。产出第一个能走的策略,步态可能不太自然
- **奖励调优后的二次训练**:2-3 小时 × 2-5 轮。CLAUDE.md 说”expect 2-5 iterations of reward-hacking whack-a-mole”
- **ONNX 导出 + 真机验证**:30 分钟(不含真机调试)
对于复刻项目来说,换了舵机后的关键路径是:BAM 辨识(1-2 周)→ 移植参数到训练配置(1 天)→ 冒烟测试(2 小时)→ 正式训练(2-3 小时/轮)→ ONNX 导出(30 分钟)→ 真机部署验证(3-5 天)。
瓶颈不在训练,在 BAM 辨识。M1-M6 六个阶段每个都需要做单摆台架实验采集数据,拟合参数。换了 15 只舵机,至少要做 5-10 只的抽样辨识,确认一致性。飞特 HL-2915 已经到货 2 只,台架六课的前 3 课(PING、遥测流、闭环小步幅)可以提前开练——这是复刻项目里接下来最重要的技术动作。
写在最后
回头看整个训练流水线,Pollen Robotics 做得最聪明的一点不是奖励函数设计有多巧妙(虽然确实不错),而是 把训练流程做成了可重复的工程流水线。从冒烟测试到 ONNX 导出,每个环节都有明确的输入输出、验证标准和常见故障排查方法。
对于一个 399 美元的开源机器人,能让一个独立开发者(没有 RL 团队)也能从零训出一条步态策略——这种工程化的思维,比任何炫酷的算法改进都更有价值。
数据来源:
- Pollen Robotics. microduck_rl 仓库. GitHub. CLAUDE.md, microduck_velocity_env_cfg.py, README. 2026.
- Polen Robotics. microduck 仓库. robotd-design.md, architecture.md. GitHunb. 2026.
- duck.whatled.com 复刻进度记录.
- Rhoban Lab. BAM: Better Actuator Models for Sim-to-Real Transfer. ICRA 2025.
- mijlab 文档. mujocolab/mijlab. GitHunb.
