Microduck 强化学习源码拆解:一只 800g 的机器鸭怎么学会走路

Microduck 强化学习源码拆解:一只 800g 的机器鸭怎么学会走路

逐行读 pollen-robotics/microduck_rl 的训练配置、奖励函数、课程学习和域随机化,把 PPO 训练栈拆开看。


写在前面:这只鸭子用了什么

Microduck 是 Hugging Face × Pollen Robotics 在 2026 年 8 月开源的小型双足机器人:25cm 高、约 800g、15 个舵机。它的强化学习训练代码全部公开在 microduck_rl 仓库里。

技术栈一句话说完:

mjlab(MuJoCo Warp 的 GPU 并行后端)+ rsl_rl 的 PPO + BAM 舵机电机模型,在 4096 个并行环境里自举出一套步态。

!外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(https://img-home.csdnimg.cn/images/20230724024159.png?![请添加图片描述](https://i-blog.csdnimg.cn/direct/f8db67b327e54726acd5438544f86795.png)

origin_url=blog_assets%2Frl1_stack.png&pos_id=img-gmLax6mT-1789114306091)

它和大多数"开源机器人 + RL"项目的区别在于执行器保真度:不图省事用理想 PD 控制,而是把 Dynamixel XL330 这枚小舵机建到了电压控制律和摩擦模型的层面。原因写在 README 里,很直白:

At this scale --- tiny servos driving a ~800 g biped --- actuator fidelity is most of the sim2real gap.

先给一份代码地图,后面每一节都对应到具体文件:

文件 职责 本文关注度
tasks/microduck_velocity_env_cfg.py 走路任务的主配置:奖励、观测、课程、域随机化开关、PPO 超参 ★★★
tasks/mdp.py 自定义奖励/观测/课程函数实现(约 7200 行) ★★★
tasks/symmetry.py 左右镜像对称增强 / 镜像损失 ★★
robot/microduck_constants.py 机器人配置、HOME 姿态、BAM 执行器参数 ★★
actuator/friction_dr_bam.py BAM + 摩擦域随机化 + 齿隙编码器反馈 ★★
train_cli.py / train_hook.py train 入口与 --hf-jobs 拦截

一、算法:PPO 的快照

算法部分用 rsl_rl 的 PPO,配置在 MicroduckRlCfg 里。全部超参如下:

超参 说明
clip_param 0.2 PPO 的招牌------限制每轮策略更新幅度
num_learning_epochs 5 同一批数据反复利用 5 遍
num_mini_batches 4 每遍切成 4 个 mini-batch
learning_rate 1e-3,schedule="adaptive" 按实测 KL 动态调,目标 desired_kl=0.01
gamma / lam 0.99 / 0.95 折扣因子 / GAE 优势估计的 λ
entropy_coef 0.01 熵正则,维持探索
max_grad_norm 1.0 梯度裁剪
value_loss_coef 1.0,use_clipped_value_loss=True 价值损失同样做裁剪
num_steps_per_env 24 每环境每次迭代采样 24 步
max_iterations 50 000 上限;实际 1~2 小时就出可用步态

关于 schedule="adaptive":这是 rsl_rl 的一个实用设计------它不按固定曲线衰减学习率,而是盯着 KL 散度自动调。KL 太大说明步子迈太猛,降学习率;太小说明学得慢,升学习率。对这种奖励尺度需要反复调整的任务,比手工设衰减曲线省心得多。

采样规模算一下:24 步 × 4096 环境 ≈ 每轮迭代 10 万步经验。官方标称 4096 环境在 4090 上约 1~2 小时收敛出一套能走的步态。


二、网络:非对称 Actor-Critic

这是这个项目里设计得最讲究的一块。

Actor 和 Critic 都是 512-256-128 的 MLP + ELU 激活 ,都开 obs_normalization=True。Actor 输出高斯分布的均值和标准差(init_std=1.0std_type="scalar",即所有动作维共享一个标准差),采样得到 14 维动作。

区别在观测:

Actor Critic
观测 61 维(真机传感器给得出的量) 61 维 + 基座线速度
额外特权 --- base_lin_vel、脚部接触力/高度/腾空时间
是否部署 ✅ 导出成 ONNX 上机器人 ❌ 训练完丢弃

代码里的体现就三行:从 actor 观测里删掉 base_lin_vel,只往 critic 加回去。

python 复制代码
# 观测裁剪
del cfg.observations["actor"].terms["base_lin_vel"]      # Actor 看不到线速度
del cfg.observations["actor"].terms["height_scan"]
del cfg.observations["critic"].terms["height_scan"]

# 特权信息只给 Critic
cfg.observations["critic"].terms["base_lin_vel"] = ObservationTermCfg(
    func=mdp.base_lin_vel, scale=1.0,
)

为什么这么做:机器人身上没有测量自身平移速度的传感器。如果训练时把线速度喂给 Actor,策略就会依赖一个部署时根本不存在的观测量------训练指标漂亮,上真机就崩。把这类信息交给 Critic 独占,价值估计更准(Critic 只是训练时的"老师"),而 Actor 学到的仍然是可以部署的映射。

同样的思路还体现在两处细节上:

  • IMU 安装误差随机化只作用于 Actorbase_ang_velprojected_gravity 会被一个随机轴、最大 6° 的旋转扰动------模拟真实 IMU 的安装偏差;Critic 拿到的仍是真值。
  • 编码器零位偏置只给 Actorjoint_pos 观测项有个 biased=True 开关,Actor 读到的是"带 ±0.015 rad 零点误差"的关节角(真编码器的样子),Critic 读真值。

三、61 维观测契约

所有策略------走路、起身、踢球、前滚翻------共用同一套 61 维观测布局。这是整个项目的架构基石。

索引 字段 维度 内容
[0:3] base_ang_vel 3 IMU 角速度(roll/pitch/yaw)
[3:6] projected_gravity 3 重力在机体坐标系的投影 ≈ 姿态
[6:20] joint_pos_rel 14 14 个关节位置(相对默认姿态)
[20:34] joint_vel_rel 14 14 个关节速度
[34:48] last_action 14 上一帧动作
[48:51] twist 指令 3 前进 / 横移 / 转向
[51:55] head 指令 4 颈/头 4 自由度目标增量
[55:61] body 指令 6 躯干位姿增量 [x,y,z,roll,pitch,yaw]

关节顺序也是固定的:

复制代码
0-4   左腿: hip_yaw, hip_roll, hip_pitch, knee, ankle
5-8   颈头: neck_pitch, head_pitch, head_yaw, head_roll
9-13  右腿: hip_yaw, hip_roll, hip_pitch, knee, ankle

为什么"固定布局"值得单独讲 :18 个任务是各自独立训练的------一个策略只学走路,另一个只学前滚翻。但它们共用观测槽位,所以部署时可以在任意时刻热切换策略,机器人侧不需要改任何代码。用不到的指令维度补零而不是删掉 ,就是为了形状永远对齐。仓库里 scripts/infer_policy.py 演示的正是这件事:一条命令同时挂载走路/站立/坐站/前滚翻四个 ONNX,运行时按键切换。

观测里的三个"真实感"处理

这部分是 sim2real 的功夫所在,全在 microduck_velocity_env_cfg.py 的观测配置段:

1. 延迟建模。 真实舵机的编码器读数是滞后的,IMU 也不是瞬时。代码里显式配了延迟:

python 复制代码
# IMU 通道:0~1 个控制周期延迟(原来配到 3,即最坏 60ms;2026-07 复核后收到 ±20ms)
cfg.observations["actor"].terms["base_ang_vel"].delay_max_lag = 1
cfg.observations["actor"].terms[gravity_term_name].delay_max_lag = 1

# 关节速度:固定延迟 1 个控制周期
# 原因:Dynamixel 固件用滑动平均算 present_velocity,策略读到的值就是"旧"的
cfg.observations["actor"].terms["joint_vel"].delay_min_lag = 1
cfg.observations["actor"].terms["joint_vel"].delay_max_lag = 1

注释里解释得很清楚:Dynamixel 固件通过一个移动平均窗口计算 present_velocity,所以策略实际读到的速度值大约滞后一个控制周期。如果不在仿真里建这个延迟,策略会学会依赖瞬时速度反馈,上真机就失效。

2. 观测噪声。 全部用均匀噪声,而且数值经过多轮收紧:

观测项 噪声范围 曾经的值
base_ang_vel ±0.03 0.2
projected_gravity ±0.01 0.15
joint_pos ±0.001 0.05
joint_vel ±0.25 2.0

3. Critic 的 NaN 防护。 Critic 有三个观测项来自传感器(脚部接触力、脚部高度、脚部腾空时间)。这些数据 MuJoCo 有可能返回非有限值,而 nan_state 终止检查只看关节和根状态------inspector 不到它们。rsl_rl 的 check_nan 一旦发现 NaN 会让整个训练挂掉(代码注释里点名了这次事故:2026-08-21 Velocity2-Rough-Backlash crash)。于是给这三个项套了 NaN-safe 包装:

python 复制代码
for _term, _safe in (
    ("foot_contact_forces", microduck_mdp.foot_contact_forces_safe),
    ("foot_height", microduck_mdp.foot_height_safe),
    ("foot_air_time", microduck_mdp.foot_air_time_safe),
):
    if _term in cfg.observations["critic"].terms:
        cfg.observations["critic"].terms[_term].func = _safe

因为是 critic-only,这一层清洗对策略毫无代价。


四、动作空间:14 维位置控制

动作就是 14 个关节的目标角度,位置控制,scale = 1.0(不做缩放)。

python 复制代码
joint_pos_action = cfg.actions["joint_pos"]
joint_pos_action.scale = 1.0

一个容易忽略的点:机器人有 15 个舵机,但策略只输出 14 维动作 。领部/嘴部连杆在 MJCF 模型里是被动关节,统一命名为 passive_*------它们不进动作空间,也不进关节观测。所有需要"受控关节"的地方都用同一个正则把它们排除掉:

python 复制代码
joint_names=(r"^(?!passive_).*",)

passive_* 命名约定是有意为之的:齿隙铰链、轮滑轮子也都用这个前缀,凡是不受控的关节一律以此命名,选择器统一写成"非 passive"。


五、奖励函数:一张完整的权重表

走路任务(Velocity)的奖励是所有任务里最完整的。先看全景:

奖励项 权重 作用
air_time 步态腾空 +3.0 鼓励抬脚离地、正常摆步(窗口 0.125~0.300 s)
track_linear_velocity +2.0 主目标:走多快听指令
track_angular_velocity +2.0 主目标:转多快听指令
head_pose_tracking +2.0 头部姿态跟踪(4 关节逐关节高斯)
upright 躯干直立 +2.0 刻意加强(详见下文)
pose 腿部姿态 +1.0 站立收紧、行走放宽
dof_pos_limits −1.0 关节别顶到限位
self_collisions −1.0 腿别砸到机身
foot_clearance 抬脚高度 −2.0 目标 2cm,防止拖脚
foot_swing_height 摆动高度 −0.25 目标 2cm
foot_slip 打滑 −0.1 刻意很弱
action_rate_l2 动作平滑 −0.1 → −1.0 课程式收紧
body_ang_vel −0.05 抑制躯干乱转
angular_momentum −0.02 抑制角动量堆积
body_pose_tracking 0.0 保留槽位,权重为零
head_pose_bias 低头偏置 0 → 3.0 后启用的 DC 项

奖励设计里最有价值的不是这张表,而是表背后的三个故事------每一条都写在代码注释里。

故事一:为什么打滑惩罚只有 −0.1

python 复制代码
# foot_slip deliberately weak (-0.1, not -1.0): -1.0 was too restrictive
# for this robot's pivot-heavy turning.
cfg.rewards["foot_slip"].weight = -0.1

按常理,打滑是走路的敌人,惩罚应该给足。但这个机器人转身时依赖大量枢转动作 ------脚掌拧着地面转。打滑惩罚给到 −1.0 会把这些"必要的打滑"一并罚掉,策略学到的是不敢转身的步态。所以最终只给 −0.1。

反例同样有价值:foot_clearancefoot_swing_height 的处罚很重(−2.0 / −0.25),因为拖脚是完全没必要的坏习惯。该罚的罚狠,该放的放到最松------这条在 RL 奖励设计里比任何公式都重要。

故事二:为什么把"躯干直立"加重了一倍

python 复制代码
# upright: deliberately strong (2.0 / std²=0.05, was 1.0 / std²=0.1).
# 2026-07 pitch-vs-speed eval: the policy walks with a +2-4° steady forward
# lean ... ~2/3 of push-induced falls at speed are FORWARD.
cfg.rewards["upright"].weight = 2.0
cfg.rewards["upright"].params["std"] = math.sqrt(0.05)

作者做了一次"前倾角 vs 速度"的评测,发现策略会以 +2~4° 的稳定前倾 走路(90 分位到 6~8°),而推倒事故里有三分之二发生在正前方 。用原来 1.0 的权重、std²=0.1 的容差算一下:4° 前倾每步只花掉约 0.05 的奖励------等于不要钱。

于是把权重翻倍、容差收紧一半(std² 0.1 → 0.05):同样 4° 前倾变成每步约 0.19 的成本,这个梯度足够让策略在稳态行走时把躯干端平,同时又允许加速、抗扰时的瞬时倾斜。这是典型的"用奖励梯度给姿态定精度"的手法。

故事三:让鸭子"罢工"的那次实验 ------ head_pose_bias 的由来

这是全仓库最精彩的一段注释,值得原文引用:

python 复制代码
# Head droop fix (2026-08-20). The head walks pitched ~15° down (measured:
# run ww1g2198 head_pose_tracking 1.544/2.0 → 14.6° mean joint error).
# DO NOT fix this by tightening head_pose_tracking's std: run 5yay13u4 tried
# fine_std=0.1 and the policy stopped walking entirely by iter 300 (air_time
# 1.01 → 0.02, peak foot height 15 mm → 2 mm, entropy collapsed 10.9 → 1.9).

情况是这样的:鸭子走路时头一直低着约 15° 。很自然的想法是把头部跟踪奖励的容差收紧(fine_std=0.1),逼它把头抬起来。

结果------鸭子不走了。到第 300 轮迭代,腾空奖励从 1.01 掉到 0.02,抬脚高度从 15mm 掉到 2mm,熵从 10.9 崩到 1.9。原因算一下就明白:

一个 280g 的头(占机器人总质量 38%)走路时必然晃动。瞬时收紧的容差等于给"走路"这件事加了一道 0.77/步 的永久税 ------而腾空奖励总共才 1.01/步,税占 76%。更要命的是这笔税无法逃避,因为头必须摆。于是"站着不动"反而是得分最高的策略。策略没有犯错,它只是正确地找到了最优解。

修正办法很聪明:区分可逃避的直流偏置不可避免的振荡 。低头是重力造成的稳态下垂,策略其实可以主动把脖子指令往上偏一点来抵消------这是可逃避的。而走路时的晃动是不可逃避的。所以只惩罚前者的时间平均值:

python 复制代码
alpha = min(1.0, float(env.step_dt) / max(tau_s, 1e-6))
env._head_bias_ema = (1.0 - alpha) * env._head_bias_ema + alpha * err
out = -env._head_bias_ema.abs().mean(dim=-1)     # L1 惩罚 EMA 的绝对值

1 秒时间常数的 EMA 把振荡平均掉,只留下直流分量;惩罚用 L1 而不是高斯,因为 L1 在大误差处梯度恒定,不会像收紧的高斯那样"梯度死掉":

python 复制代码
# L1 (not Gaussian) on purpose: the gradient stays constant at large bias,
# where a tight Gaussian would be flat and dead.

而且这个项的权重是课程式后启用的------前 600 轮迭代权重为 0,之后才从 1.0 升到 3.0。理由同样直白:

python 复制代码
# Held at 0 early because a posture-precision term is a distraction before
# a gait exists.

步态还不存在的时候,去纠正姿态是纯粹的干扰。 先让它学会走,再让它走好看。


六、课程学习:五条自动加难曲线

课程学习的实现方式很朴素但很有效:同一个参数按训练进度分档推进mdp.py 里提供了几个通用的分档函数。

reward_weight------这是最基础的机制,遍历所有阶段、把"已经到达的阶段"的权重写到活着的 term 配置上:

python 复制代码
def reward_weight(env, env_ids, reward_name, weight_stages):
    del env_ids
    term_cfg = env.reward_manager.get_term_cfg(reward_name)
    for stage in weight_stages:
        if env.common_step_counter > stage["step"]:
            term_cfg.weight = stage["weight"]
    return torch.tensor([term_cfg.weight])

注意一个坑:必须改 live 的 manager 配置,不能改 env.cfg 。因为 manager 初始化时对 cfg 做了 deepcopy,改原始 cfg 是个静默的空操作。这条在 push_curriculumcom_range_curriculum 里都单独写了警告注释------说明作者踩过。

速度任务里实际启用了这几条曲线:

曲线 起 → 止 触发时机 目的
动作平滑权重 −0.1 → −1.0 iter 0 → 1500(6 档) 步态未成形时先松,成形后再收紧
站立环境占比 2% → 25% iter 0 → 2000(6 档) 先学会走,再学会站定
躯干质心随机化 ±3 → ±15 mm iter 0 → 1500(4 档) 上限卡在脚掌支撑面内
头部指令范围 5% → 100% iter 0 → 2000(5 档) 逐档放大,按关节可达范围缩放
低头偏置惩罚 0 → 3.0 iter 600 → 1500(4 档) 前 600 轮为 0,避免干扰学步
头部指令幅值 ±0.05 → ±1.10 rad 同上 neck_pitch 为例

一个"不做"的决定:指令范围不加宽

大部分 locomotion 项目会做速度指令范围课程 ------一开始只要求慢慢走,逐渐加大到快跑。Microduck 明确取消了这条

python 复制代码
# Modest, FIXED command ranges (no widening curriculum): a ramp to
# lin ±0.4 / ang ±2.0 outpaced the robot's capability and tracked a
# post-iter-1000 reward/episode-length decline. ang ±1.0 is the big
# change --- it makes turning learnable.
command.ranges.lin_vel_x = (-0.4, 0.4)
command.ranges.lin_vel_y = (-0.3, 0.3)
command.ranges.ang_vel_z = (-1.0, 1.0)      # 原来的 ±2.0 学不会

指令范围固定为 lin ±0.4 / ang ±1.0。原因:原来把角速度加宽到 ±2.0,超出了这个小机器人的实际能力,导致 1000 轮之后奖励和回合长度同步下滑------课程跑得比机器人的能力快了。收到 ±1.0 之后转向才真正学得会。

还有一个有意思的采样技巧:转身桶

python 复制代码
TURN_IN_PLACE_FRACTION = 0.15
command.rel_turn_in_place_envs = TURN_IN_PLACE_FRACTION

独立均匀采样线速度和角速度时,"原地转圈"(lin=0|ang| 较大)的组合大约只占 2% 的数据------等于没训过。所以显式划出 15% 的环境专门做原地转身。


七、域随机化

这是 sim2real 的正面战场。开关集中在每个 env cfg 文件顶部,用 ENABLE_* 布尔量控制。

类别 随机化项 范围 / 备注
几何 / 质量 躯干质心偏移 ±3 → ±15 mm(课程),上限受支撑面约束
头部组件质心偏移 ±3 → ±10 mm(课程),逐个头部刚体
质量 + 惯量 ±5%,用 pseudo_inertia 物理一致缩放
执行器 电池电压 6.5 ~ 8.2 V(每环境启动时采样)
负载压降 压降系数 0 ~ 0.2,电压下限 6.0 V
关节摩擦预算 ×0.9 ~ 1.1(BAM 原生路径)
反射转子惯量 ±10%(armature)
指令延迟 3 ~ 6 个仿真步
传感器 IMU 安装误差 随机轴,最大 6°
编码器零位偏置 ±0.015 rad(≈ ±0.86°),每环境恒定
关节速度延迟 1 个控制周期
观测噪声 见上一节表格
外部扰动 随机推力 每 3~6 s 施加 ±0.3 m/s 速度踢
脚底摩擦 0.7 ~ 1.3
粗糙地形 台阶 ≤1.5 cm、随机格子 ≤1 cm、坡度 1.7°~5.7°

同样地,调参的历史留在注释里,而且每一个数字都能讲出理由。

教训一:质心偏移的上限是被脚掌卡住的

python 复制代码
# Capped at ±15 mm (2026-07 audit): the previous ramp to ±30 mm
# exceeded the foot support polygon (heel is only 20 mm behind
# the ankle) --- the randomized CoM could sit entirely outside
# support, forcing a wide/fast hyper-reactive gait and making
# BACKWARD balance untrainable.

原来课程一路加到 ±30mm,结果发现脚后跟离踝关节只有 20mm ------随机出来的质心可以整个跑到支撑多边形外面。后果不是"更难",而是"错":策略被逼成一种又宽又快的过度反应步态,向后平衡永远学不会 。作者还给了回归时间线佐证:0.015 → 0.02 → 0.03 每加一次,策略就变差一档。最终封顶 ±15mm。

教训二:推力从 ±0.5 降到 ±0.3

python 复制代码
VELOCITY_PUSH_RANGE = (-0.3, 0.3)  # Velocity change range in m/s. Was ±0.5 --- an
# ADDITIVE kick larger than max walk speed (0.4) every 3-6 s trains a permanently
# nervous fall-recovery gait (2026-07 audit). ±0.3 keeps push robustness while
# letting a calmer gait be optimal.

原来每 3~6 秒施加 ±0.5 m/s 的增量 踢------这比最大行走速度(0.4 m/s)还大!结果是训出一种"神经质"的、时刻准备摔倒恢复的步态。降到 ±0.3 后既保住了抗扰能力,又让更平静的步态成为最优解

这句话值得记住:随机化不是越大越好。超出合理范围,你训练出的不是鲁棒性,而是应激反应。

一个技术细节:非累积

域随机化有个经典陷阱------如果每回合直接往当前值上加偏移,误差会逐回合累积,跑几十轮之后参数就飘到天边了。作者为此专门写了一段说明:

python 复制代码
# In mjlab 1.3.0 the stock dr.* ops with operation="add"/"scale" read from the
# compile-time default field each reset (Operation.uses_defaults=True), so they
# are NON-accumulating natively --- this upstream behavior replaces microduck's old
# custom restore-then-add functions that worked around the accumulation footgun.

mjlab 1.3.0 的 dr.* 操作每次重采样都从编译期默认值出发,天然非累积,所以自己写的那套"先还原再加偏移"的祖传代码可以删了。


八、执行器:BAM M6,为什么不用理想 PD

这是全项目我最想推的一节。

典型的 RL 机器人项目里,执行器就是一个理想 PD 控制器:τ = kp·(q_target − q) − kd·(dq/dt),给多少力矩就有多少力矩,瞬时响应、没有上限。对大型机器人(比如宇树那类)这没问题,因为电机足够强,PD 假设基本成立。

但 Microduck 是 800g 的双足靠 15 个小舵机驱动 ------这枚 Dynamixel XL330 每一档都在它的物理极限附近工作。于是项目用了 BAM 的 M6 模型,把执行器建到了电压控制律级别:

python 复制代码
_BAM_ACTUATOR_KWARGS = dict(
    motor_name="xl330",
    model="m6",
    target_names_expr=(r"^(?!passive_).*",),
    kp_fw=200.0,              # 保留固件的刚度(对比:microban 用 125)
    vin_range=(6.5, 8.2),     # 每环境采样的电池电压
    vin_drop_gain_range=(0.0, 0.2),  # 负载相关压降 V_drop = gain * Σ|τ|
    vin_min=6.0,              # 压降后的电压下限
    delay_min_lag=3,
    delay_max_lag=6,
)

也就是说,仿真里输入的是电压而不是力矩,中间环节全建了:

  • 反电动势------转速越高,可用力矩越低;
  • 库仑 + Stribeck + 负载相关摩擦------不是常数,是模型;
  • 负载下的电压压降 ------电池不是理想电源,用劲大了电压就掉,vin_min=6.0 是硬下限。

摩擦随机化要绕个弯

BAM 自己算摩擦,所以 MuJoCo 的 dof_frictionlossedit_spec 里被清零了------标准的 dr.dof_frictionloss 在这里是个空操作。项目为此写了一个薄子类:

python 复制代码
class FrictionDRBamActuator(BamActuator):
    """BamActuator + per-env friction_scale on the BAM friction budget."""

    def _compute_friction_budget(self, motor_torque, external_torque, stribeck_coeff):
        base = super()._compute_friction_budget(motor_torque, external_torque, stribeck_coeff)
        fs = getattr(self, "friction_scale", None)
        return base if fs is None else base * fs      # 每环境缩放

只缩放速度无关的摩擦预算(库仑 + Stribeck + 负载相关)------因为那一项才是"静摩擦/齿轮箱"这个 sim2real 主要不确定性的载体。

齿隙:编码器装在间隙的输出侧

每个主任务都有一个 -Backlash 孪生版本,训练模型里给 14 个舵机各串一个 ±1° 的被动铰链(总计 2° 行程)。关键在于位置:

python 复制代码
class BacklashEncoderBamActuator(FrictionDRBamActuator):
    """FrictionDRBamActuator whose firmware PD reads the encoder THROUGH backlash."""

真实编码器装在齿轮间隙的输出侧 ,所以固件 PD 和关节位置观测都要"读穿"间隙:qpos[舵机] + qpos[间隙]。这一点如果不建,后果很微妙------头部可以靠着间隙下垂却不受任何惩罚,而策略如果主动去补偿它,反而因为在舵机侧产生了"误差"而被罚。观测和动作维度都不变,所以 ONNX 导出和运行时完全不需要改。


九、任务家族:18 个策略共享一个大脑

仓库注册的任务(uv run list-envs 可列出实时注册表):

任务 地形 说明
Mjlab-Velocity-{Flat,Rough}-MicroDuck 平/崎岖 主任务:速度指令 + 头部姿态指令
Mjlab-VelStand-{Flat,Rough}-MicroDuck 平/崎岖 走路 + 摔倒恢复,一个策略
Mjlab-StandUp-{Flat,Rough}-MicroDuck 平/崎岖 从趴着/仰着/坐着起身,再保持站立
Mjlab-SitStand-{Flat,Rough}-MicroDuck 平/崎岖 坐下 ↔ 站起,一个策略
Mjlab-GroundPick-{Flat,Rough}-MicroDuck 平/崎岖 蹲下用喙触地,再站起来
Mjlab-BallKick-Flat-MicroDuck 踢 70mm/15g 的球(Actor 看不见球
Mjlab-Roulade-Flat-MicroDuck 前滚翻过顶,落回脚上
Mjlab-Velocity-Flat-MicroDuck-Rollers 轮滑速度跟踪(脚下是被动轮)
Mjlab-Velocity-Swizzle-MicroDuck 对称 swizzle 滑行
Mjlab-RollerCrouch-Flat-MicroDuck 滑行中蹲低
Mjlab-RollerSlope-Flat-MicroDuck 轮滑下坡
Mjlab-RollerStandUp-Flat-MicroDuck 从地面站到轮子上
Mjlab-Spin-Flat-MicroDuck 轮滑原地高速旋转

一共 18 个基础任务 id (13 个任务家族),每个主任务还有一个 -Backlash 孪生变体(在任务名里插入 -Backlash,如 Mjlab-Velocity-Flat-Backlash-MicroDuck)。它们全部共享那套 61 维观测契约------这是整个"18 个策略共用一只鸭子"的架构前提。


十、如果你想自己动手

训练命令(在 microduck_rl 目录下):

bash 复制代码
# 主任务:走路(4096 环境,4090 上约 1~2 小时出可用步态)
uv run train Mjlab-Velocity-Flat-MicroDuck --env.scene.num-envs 4096

# 冒烟测试:验证环境能跑通
uv run train Mjlab-Velocity-Flat-MicroDuck --env.scene.num-envs 64 --agent.max_iterations 5

没有 GPU 怎么办 :任意训练命令后面加 --hf-jobs,会自动提交到 Hugging Face Jobs(默认 L4 显卡)上跑,本地零 GPU 需求。

几个常用的改动入口:

想改什么 改哪里
奖励权重 microduck_velocity_env_cfg.pycfg.rewards[...]
观测内容 同文件的 cfg.observations["actor"/"critic"].terms
域随机化开关 文件顶部的 ENABLE_* 布尔量
课程曲线节点 同文件 cfg.curriculum[...]*_stages
网络结构 / PPO 超参 文件末尾的 MicroduckRlCfg
舵机模型参数 robot/microduck_constants.py_BAM_ACTUATOR_KWARGS

另外注意:导出的 ONNX 一定要用 scripts/export.py 生成的,因为导出器会把观测归一化器烘焙进计算图里------手工转换的 checkpoint 会让策略在运行时读到未归一化的观测。


结语:这个仓库最值得学的三件事

  1. 奖励设计是"反直觉"的 。打滑惩罚要刻意调弱(否则学不会转身)、姿态惩罚要刻意调强(否则前倾摔倒)、而"提高精度"这种看起来永远正确的操作,可能直接让策略罢工。判据只有一条:这个惩罚可不可逃避?不可逃避的惩罚,加多少都是在教机器人躺着不动。

  2. 课程学习的关键是知道何时"不加难" 。五条曲线都是先松后紧,头部精度项甚至先关掉 600 轮。而速度指令范围明确不加宽------因为课程跑得比机器人能力快,只会训出崩坏的策略。Sim2real 的随机化同理:±0.5 的推力比最大步速还大,训出来的不是鲁棒,是应激。

  3. 保真度花在刀刃上 。算力有限时,不要均匀地提升所有仿真细节------这个项目把预算全押在执行器建模上(电压控制律、反电动势、摩擦、齿隙),因为这是"小舵机驱动小机器人"场景下 sim2real 差距的主要来源。其余部分(视觉、复杂地形)反而克制甚至不做。

相关推荐
俊哥V1 小时前
AI 今日研究简报 · 2026-09-11
人工智能·ai
nagualky1231 小时前
AI Agent 别只返回“已完成”:用五字段验收契约定义任务终点
人工智能·机器学习·软件工程
冬奇Lab1 小时前
一天一个开源项目(第215篇):Langflow - 可视化拖拽构建 AI 应用的低代码平台
人工智能·开源·资讯
loser.with.m1 小时前
【AgentScope 2.0】05-从数据库配置到可运行 Agent:十个 Builder 开关怎么把一行 JSON 变成 HarnessAgent
人工智能·spring boot·ai
2601_949499942 小时前
DT‑1414PTZ如何百分百兼容(安华高)HFBR‑1414PTZ
运维·网络·人工智能·科技·光模块
百胜软件@百胜软件2 小时前
胜券AI的Skills可插拔技能包,让零售AI从“泛”到“专”
大数据·人工智能·零售
瓶 盖2 小时前
AI 味为什么改词改不掉?把 61,608 篇小说拆开之后
人工智能
行走的领路人2 小时前
FlyEnv 本地开发环境与 AI 协同能力深度评测
人工智能
汇智信科2 小时前
基于 Jmis 框架的制度与流程管控系统设计与应用|项目全生命周期管理
大数据·人工智能·微服务·云原生·汇智信科