Microduck RL训练踩坑实录:奖励函数设计、PPO调参与策略部署
Meta Description: 基于Pollen Robotics官方microduck_rl仓库的实战经验,拆解双足机器人RL训练中的奖励函数设计陷阱、PPO参数调优技巧,以及从仿真到真机部署的全流程。
双足机器人用强化学习训练步态,听起来很酷对吧?但真上手做一遍,你会发现大部分时间不是在调网络,而是在跟奖励函数打架。
Pollen Robotics 把 Microduck 的训练栈全开源了——microduck_rl 仓库基于 MuJoCo Warp(mjlab)和 PPO(rsl_rl),4096 个并行环境跑在 GPU 上,1-2 小时能训出一个可用的步态。但仓库里那个 AGENTS.md 文件,读一遍就知道他们踩了多少坑。
这篇文章不重复官方 README,而是把 AGENTS.md 里那些”每个规则都是用教训换来的”的经验,结合复刻过程中的实操,拆开来讲。
奖励函数设计的五个血泪教训
AGENTS.md 里有个专门章节讲奖励函数规则,每条后面都写着”learned the hard way”。这五个问题是在复刻中一定会遇到的。
1. 符号搞反,四轮训练白费
这是最容易被新手坑到的:Microduck 的 mdp.py 有两种惩罚风格。mjlab 基础的成本函数返回 ≥ 0,用负权重;而 Microduck 自有的 _penalty 和 _l1 函数返回 ≤ 0(已经自带了负号),所以用正权重。
如果你对一个返回 ≤ 0 的惩罚项用了负权重,那它就双负为正了——变成奖励这个违规行为。策略会疯狂利用它,比如 butt-hopping(屁股跳)或者 crash-sits(摔倒坐着)。
验证方法:在 wandb 上,每个 Episode_Reward/ 的值必须 ≤ 0。如果某个惩罚项是正数,那你的符号反了。
2. RL 优化的是奖励字面意思,不是你心里想的
原话:”RL optimizes the letter of the reward.”
每一个没明确约束的自由度都会被利用。你想让鸭子向前翻滚,它学会了 ballistic whip(弹射甩鞭)。你想让它站起来,它学会了用头做三角支撑。
解决方案:用硬的状态门控(state-based gates)来编码”什么算这个动作”,而不是用小惩罚来微调。比如检查特定脚掌的接触状态、躯干朝向角度、用 latch(锁存器)记录动作阶段。
3. 没有”Jackpot”
如果你设了一个”到达 X 位置就给奖励”的规则,而且到达后每步还继续拿奖励——这就是一个 jackpot,策略会为了拿这笔钱做出任何极端动作。
正确做法:对”到达 X”的奖励做 rate-limited 或 slewed(缓变)。对于指令转换任务,用一个 slewed 的内部目标(匀速插值),让策略跑在目标前面没有额外好处。
4. 观测空间必须和奖励测量同一个视图
这是 AGENTS.md 里的一条 invariant:
“If an obs is remapped to a sensor view (backlash encoder, bias), any tracking REWARD on the same quantity must measure the same view — otherwise the policy is punished for correcting what it sees.”
如果观测走的是 backlash 编码器视角,但奖励测量的是理想关节角度,策略会看到”我明明把读数纠正了,怎么还在扣分”,训练直接乱掉。
️5. 域随机化不能跨 episode 累积
mjlab 1.3.0 的 DR 操作符(add/scale)原生就是非累积的,但如果你写自定义 DR 函数,必须记住 restore-then-apply。
Pollen 团队踩过这个坑:一个累积的 CoM 随机化曾经导致连续几个月的训练退化——每次 reset 时 CoM 偏移量越积越大,策略面对的训练分布不断偏移。
PPO 训练的实际配置
从 microduck_rl 的源码和配置可以提取出 PPO 训练的典型参数:
环境配置:
- 并行环境数:4096(GPU 显存够的话)
- 控制频率:50 Hz
- 观测空间:61 维(48 本体感受 + 13 指令)
- 动作空间:15 维(14 个舵机目标角度 + 1 个预留)
观测布局:61 维在所有任务族间共享,保证策略可以热切换。48 维本体感受包括关节角度、速度、IMU 姿态等。13 维指令块是 [twist(3), head_pose(4), body_pose(6)],这个顺序不能变。不用某个指令槽的就 ZERO-PAD 它,绝不删除槽位。
关节布局:14 个舵机,ctrl idx = joint idx:
- 0-4:左腿(hip_yaw, hip_roll, hip_pitch, knee, ankle)
- 5-8:脖子/头(neck_pitch, head_pitch, head_yaw, head_roll)
- 9-13:右腿
策略导出:ONNX 导出时把观测归一化器(normalizer)一起烤进去。scripts/export.py 自动做这件事。如果用 in-sim play 测试看不出问题(因为 play 也加了归一化),但手转换 checkpint 就会在真机上翻车。
策略是 unfiltered 的:训练时不加动作低通滤波。加 EMA 滤波必须有匹配的运行时标志位和传输测试——训练时有、部署时没有,或者反过来,都会破坏 transfer。
构建新环境的正确流程
AGENTS.md 给了一个六步工作流,其中第二步是最多人跳过的:
- 选最接近的模板(不要从零开始)
- 在训练前先验证物理假设——这是最大的省时器
- 配置约定
- 写 cfg 测试
- 冒烟测试(64 envs,5 iters)
- 训练
第二点特别值得展开:一个目标/静止位必须是一个稳定平衡点。从随机初始状态保持 3 秒,检查倾斜角而不是只看高度——一个只看 z 的 settle test 会把已经摔倒的状态报告为”休息得很好”。
AGENTS.md 举了个例子:一个 5 mm 错误的 STANd_Z 曾经让训练目标变成了几天都达不到的 impossible target。
冒烟测试:95% 的配置错误都能逮住
64 个并行环境,5 次迭代,跑一次冒烟测试。这能抓住约 95% 的配置错误,花费几乎为零。
冒烟测试检查的内容:
- 环境能构建成功
- 步进过程没有 NaN
- 观测是 61D
- 每个奖励项都能计算
- ONNX 能导出
规则:没有冒烟测试,不要跑长训。
从训练到真机部署的管线
训练完成后,流程是:
uv run scripts/export.py导出 ONNX(含归一化器)uv run publish上传到 Hugging Face Hub- 真机运行时通过
robotctl policy add加载
在 CPU MuJoCo 里可以用 scripts/infer_policy.py 做部署演练——用和训练时一样的 BAM M6 执行器模型模拟。加 --no-bam 就回退到 XML PD 执行器。
真机运行时支持热切换策略:walk / recover / trick 共享 61 维观测契约,任何时刻可以互相接管。
在复刻场景下的实操建议
如果你像我一样用国产飞特舵机替换 XL330,有几个额外的注意点:
执行器模型替换后:BAM 的参数全变了,需要重新做执行器辨识(上篇文章的 6 阶段流程)。但 microduck_rl 的奖励函数和 PPO 配置不需要大改——因为奖励函数绑定的是”走路”这个任务,不是”某个舵机”。你只需要在 microduck_constants.py 里更新 BAM 执行器配置,训练脚本基本不动。
冒烟测试特别重要:换舵机后很多隐性的配置错误会在训练时暴露。先跑冒烟测试再跑长训,否则你会在 2 小时后才发现训练数据全是 NaN。
注意动作空间的边界:不同舵机的角度范围不同。飞特 HL-1910 的物理限位和 XL330 不一样,必须更新 action_scale 和关节限位配置,否则训练出来的策略可能在真机上撞限位。
从官方仓库的 commit 历史来看,microduck_rl 的奖励函数经历了多轮迭代。每一步迭代背后都是一个真实机器人摔倒的故事。如果你也在做双足机器人 RL 训练,AGENTS.md 是必读文档——里面的每一条规则都是用训练时间和硬件损耗换来的。
复刻项目的训练进度和实验记录持续更新在 duck.whatled.com。
数据来源:pollen-robotics/microduck_rl 仓库 AGENTS.md (2026)、microduck_rl README (2026)、pollen-robotics/microduck 官方仓库、mjlab (MuJoCo Warp) 文档。
