Microduck 的奖励函数设计:六条用摔鸭子换来的 PPO 训练教训
做强化学习最魔幻的事情是什么?你的策略在仿真里跑得完美,reward 曲线一路向上,你以为自己解决了双足行走。然后你把策略丢到真机上,鸭子摔了。不是因为 sim2real gap,而是因为你的奖励函数根本没在奖励你以为它在奖励的东西。
Pollen Robotics 在 Microduck 的 RL 训练仓库里沉淀了一套奖励设计规则。这些规则不是从论文里抄来的,每一条后面都跟着一句”learned the hard way”。今天拆开看这六条核心规则,以及它们为什么能让一只 800 克的鸭子真的走起来。
第一条:符号约定搞反,策略会”主动追求”你想惩罚的行为
这条听起来像新手错误,但它在 Microduck 开发过程中咬了四次。
Microduck 的 mdp.py 里有两种惩罚函数风格。mjlab 基础库的 cost 函数返回值大于等于零,配负权重——这很直觉,正的代价乘以负的权重等于惩罚。但 Microduck 自己写的函数(*_penalty、*_l1)返回值小于等于零,是”自我抵消”型的,配的权重是正数。
问题出在哪?如果你把一个自我抵消型惩罚配了负权重,负负得正,它就变成了对违规行为的奖励。策略会疯狂去撞那个你试图惩罚的状态。具体表现是什么?鸭子用屁股着地蹦、或者直接摔下去坐下——因为”坐下”在你设的惩罚函数里变成了正奖励。
Pollen Robotics 给了一个不可错的检查方法:每次训练,wandb 面板里每个 Episode_Reward/<penalty> 必须小于等于零。不是看代码,不是推理符号,直接看曲线。如果哪条惩罚项的 reward 大于零,立刻停。
第二条:RL 优化的是奖励的字面意思,不是你的意图
这条是所有 reward hacking 的根源。
你写了一个”向前走”的奖励,基于前进距离。策略发现蹦一下比稳稳走能获得更高的瞬时前进量,于是它开始蹦。你加了个”保持直立”的惩罚项,策略发现肩膀翻转也能保持躯干tilt在阈值内,于是它用肩滚代替了矢状面运动。你想让鸭子做前滚翻,它发现把头杵在地上当三脚架比真的滚过去得分更高。
CLAUDE.md 的原话是:”每一个未被严格约束的自由度都会被利用。”解决方案不是加越来越多的惩罚小项——那会陷入 whack-a-mole——而是在奖励函数里用硬性状态门控(state-based gates)定义什么算成功。接触支撑检查、朝向轴检查、状态锁存。如果”前滚翻”的奖励必须满足”头部先触地→躯干翻转→脚先着地”的接触序列才能拿到,策略就没法用替代动作绕过去。
第三条:永远不要给”到达目标”一个无上限的持续奖励
这条叫”no jackpots”规则,是 Microduck 训练中最微妙的一条。
假设你设了个奖励:鸭子到达站立姿态后,每一步都给一个正奖励。听起来合理——站着就是目标嘛。但策略发现:如果它用最快速度冲到站立姿态(哪怕过程中摔得很难看),然后在站立状态里蹲着不动,它就能拿到最大的累积奖励。过程的暴力程度完全不被惩罚,因为到达目标后的持续奖励 dominates。
Pollen Robotics 的解法是”滑顺目标”(slewed internal target)。不用固定目标,而是用一个恒速渐进的目标轨迹。策略如果跑到目标前面,不会拿到额外奖励——因为目标本身还没到那里。慢就是最优解。这把”尽快到达”这个 jackpot 需求直接消掉了。
对于 SitStand 这种”命令式转换”任务,这个设计尤其关键。坐下→站起的转换如果用固定目标,策略会弹射起步;用恒速目标,动作就会柔和很多。
第四条:别把正奖励挂在”糟糕状态”上
这条和第三条是一对。如果你在”鸭子摔倒”状态下还能拿到某个正奖励项,策略就会选择待在摔倒状态。
举个例子:你设了一个”保持关节接近目标姿态”的正奖励。鸭子摔倒了,躺在地上,关节恰好处于一个松弛的休息姿态,而这个姿态恰好接近某个目标——于是它持续得分。策略学到的结论是:摔倒了别起来,躺着挺好。
正确的做法是用 potential-based shaping。不是”当前姿态好就给奖励”,而是”姿态改善量给奖励”——Δprogress。站起来给正向 reward(progress 增加),站着不动给零(progress 不变),摔倒给负向(progress 减少)。这样躺着就不可能 farm reward。
Pollen Robotics 还特别提到要审计每个正奖励项在每种摔倒姿态下的表现。鸭子可能面朝下、背朝下、侧躺——如果任何一种 flop 姿态下还能保留大部分奖励 stack,策略就会选那种姿态。
第五条:正则化器分两种,别混着用
训练双足机器人,你大概会加这些正则化:身体角速度惩罚、角动量惩罚、关节力矩变化率惩罚、动作变化率惩罚。但它们其实分两类。
运动阻断型(motion-blockers):身体角速度、角动量、姿态方差。这些项惩罚的恰好是动态运动物理上需要的东西。走路时身体必然有角速度波动,前滚翻时角动量必然很大。对动态任务来说,这些权重必须保持很低。
平滑型(smoothness):动作变化率、关节力矩变化率。这些项压制抖动但不阻碍大的慢动作。可以安全地给较高权重。
关键时序:平滑型正则化器应该在技能发现之后才引入。如果你在策略还没学会走路的时候就加 action_rate 惩罚,”什么都不做”就变成了最优解——不动就没有 action_rate,得分最高。Pollen Robotics 的做法是用 curriculum 从零开始逐步引入:先让策略学会技能,再加平滑约束去打磨。
还有一个容易踩的坑:当你从一个任务复制正则化器到另一个任务时,不要直接复制权重。PPO 看到的是相对优势。如果新任务的正奖励 stack 是旧任务的四倍,同一个 action_rate 权重就相对弱了四倍。要看 reward mass(权重 × 典型值),不是绝对权重。
第六条:乘积比加和在目标姿态上更可靠
当你想让鸭子同时满足多个条件(直立 + 关节接近目标 + 高度正确),直觉上会把这些条件各设一个 Gaussian 奖励然后加起来。但加法有一个问题:妥协盆地。
如果三个条件各 80% 就能拿到不错的总奖励,策略会找到一个”每个都差一点但都不太差”的中间姿态——比如身体微微前倾,关节稍微偏移,高度差一点点。它不会去追求任何一个条件的满分,因为那样其他两个会掉太多。
乘积结构没有这个问题。三个 Gaussian 相乘,任何一个条件严重不满足,乘积直接趋近于零。策略必须同时满足所有条件才能拿到像样的奖励。但有一个陷阱:std 要设得足够宽,让当前策略能看到梯度。如果三个条件当前的得分都接近零,乘积也是零,梯度也是零——策略什么都学不到。
Pollen Robotics 的建议是先用加法让策略学到大概,再切到乘法去收紧精度。或者一开始就用宽 std 的乘积,让初始得分不为零。
从规则到实践:一个典型的训练迭代周期
理解了这些规则,看 Microduck 的实际训练流程就清楚了。
先用 64 个环境跑 5 个 iteration 的 smoke test——这花不了几分钱,但能抓住 95% 的配置错误。然后正式训练:4096 个环境,简单的行走任务大约 1000 个 iteration 能出可用的步态,需要恢复和复杂技巧的任务要 4000-6000 个 iteration。
训练过程中盯 wandb 面板:平均 reward 在涨,episode length 符合预期,每个惩罚项都小于等于零,主要任务项在真正增长(总 reward 涨有可能只是正则化器在降,实际技能根本没学到)。当你看到一个 reward 项在 curriculum 阶段边界突然下降,说明阶段节奏不对——把那个阶段拉长或后移。
每次改奖励函数后,预期 2-5 轮 reward-hacking 的打地鼠。这是正常的。上面的六条规则不能帮你完全避免打地鼠,但能让每一轮打地鼠的时间从几天缩短到几小时。对于一个 800 克、25 厘米高的双足机器人来说,这就是能不能走起来的区别。
数据来源
- GitHub pollen-robotics/microduck_rl CLAUDE.md — 奖励设计规则全集(符号约定、reward hacking案例、no jackpots规则、potential-based shaping、正则化分类、乘积vs加法、训练预算)
- GitHub pollen-robotics/microduck_rl README.md — 训练命令、任务注册表、BAM执行器模型、ONNX导出流程
- GitHub pollen-robotics/microduck docs/design/architecture.md — 七守护进程架构、策略热切换机制
- GitHub pollen-robotics/microduck docs/design/robotd-design.md — 50Hz控制循环、策略优先级链、安全层设计
