XgoDuck 是一个小型双足机器人(几何/惯量不同于上游 MicroDuck),基于 mjlab(MuJoCo Warp GPU 并行物理引擎)构建强化学习环境,使用 PPO 算法训练运动技能策略,导出为 ONNX 部署到真机。
MuJoCo(mujoco-warp 1.12.0),GPU 并行 4096 环境,5ms 物理步长 × 5 decimation = 50Hz 控制频率
rsl_rl PPO 实现,Actor-Critic 架构,自适应学习率,GAE 优势估计
BAM(Better Actuator Models),HLS1910/STS3215 舵机电磁-摩擦辨识,非理想 PD
14 自由度(每腿 5 + 头部 4),位置控制,2 个被动下颌连杆排除
XgoDuck 共 14 个可控关节(另有 2 个被动下颌连杆 passive_* 排除在观测外):
| 序号 | 关节 | 序号 | 关节 |
|---|---|---|---|
| 0–4 | 左腿: hip_yaw, hip_roll, hip_pitch, knee, ankle | 9–13 | 右腿: hip_yaw, hip_roll, hip_pitch, knee, ankle |
| 5–8 | 头部: neck_pitch, head_pitch, head_yaw, head_roll | — | — |
髋俯仰/踝关节 ±22°(Walk 变体 ±24°),髋翻滚 ±5°,颈部/头部俯仰各 20°,使脚底恰好高于地面 1mm。站立躯干高度 XGODUCK_STAND_Z = 0.139158 m。
| 碰撞体 | contype | conaffinity | condim | 说明 |
|---|---|---|---|---|
*_foot_collision | 1 | 0 | 3 | 仅与地面接触,带摩擦锥 |
trunk_collision / head_collision | 3 | 2 | 1 | 与地面和彼此碰撞 |
其他 *_collision | 1 | 0 | 1 | 仅地面,无机器人间碰撞 |
Walk 模型仅脚部有碰撞体;Standup 模型全身有碰撞网格(用于起立/翻滚等全身接触场景)。
BAM 是 Rhoban 开发的执行器物理模型,将真实舵机的电磁学和摩擦学嵌入仿真,而非简单使用 PD 控制器。
| 参数 | 含义 | 值 |
|---|---|---|
kt | 电机转矩常数 | 0.624 N·m/A |
R | 绕组电阻 | 4.91 Ω |
armature | 转子惯量 | 0.00178 kg·m² |
friction_base | 库仑摩擦基值 | 0.0158 |
friction_stribeck | Stribeck 摩擦系数 | 0.0096 |
friction_viscous | 粘性摩擦 | 0.00031 |
load_friction_* | 负载相关摩擦(线性/二次) | 多项 |
max_velocity | 100(禁用固件目标限速) | — |
command_delay | 6ms(由 mjlab delay buffer 覆盖) | — |
compute() 流程)其中 Stribeck 项随速度衰减,模拟静动摩擦转换;负载相关项捕捉齿轮箱变形带来的摩擦。load_friction_motor_quad 和 load_friction_external_quad 提供二次项精度。
kp_fw=5,读取编码器位置作反馈,通过 q_target_smooth 实现目标限速(max_velocity=100 禁用此限速,速度由电压+反电动势决定)。
在 _compute_friction_budget 中乘以每环境 friction_scale,实现摩擦量级的域随机化。因为 BAM 下 MuJoCo 的 dof_frictionloss 被置零,标准 DR 失效——这是 BAM 原生路径。
编码器穿过齿隙读取——固件 PD 反馈的是 qpos[servo] + qpos[backlash],仿真真实舵机编码器在齿隙输出侧的行为。串联齿隙铰链模型 passive_*_backlash。
观测向量按项插入顺序拼接,Actor 与 Critic 信息不对称:
| 维度 | 内容 | Actor | Critic | 说明 |
|---|---|---|---|---|
| [0:3] | base_ang_vel | ✓ | ✓ | 机体角速度 (roll, pitch, yaw),IMU 体坐标 |
| [3:6] | projected_gravity | ✓ | ✓ | 投影重力 (gx, gy, gz),体坐标 |
| [6:20] | joint_pos_rel | ✓ | ✓ | 14 关节位置(相对默认姿态),含编码器偏置 |
| [20:34] | joint_vel_rel | ✓ | ✓ | 14 关节速度,1 步延迟(模拟 Dynamixel 移动平均) |
| [34:48] | last_action | ✓ | ✓ | 14 维上一步动作 |
| [48:51] | twist command | ✓ | ✓ | 速度指令 (lin_x, lin_y, ang_z) |
| [51:55] | head command | ✓ | ✓ | 头部姿态指令 4D |
| [55:61] | body command | ✓ | ✓ | 躯干位姿指令 6D |
| — | base_lin_vel | ✗ | 特权 | 真实线速度(策略不可见) |
| — | foot_contact 等 | ✗ | 特权 | 脚步接触力/离地高度传感器数据 |
| 设计 | 机制 | 目的 |
|---|---|---|
| IMU 安装误差 DR | 每环境固定旋转 ≤6°(零中心随机轴),旋转角速度和投影重力 | 模拟 IMU 装配误差 |
| 编码器偏置 DR | 每环境 ±0.86° 固定偏置,仅 Actor 可见 | 模拟编码器校准误差 |
| 观测噪声 | 角速度 ±0.03, 重力 ±0.01, 关节位置 ±0.001, 速度 ±0.25 | 传感器噪声建模 |
| 延迟模拟 | 角速度/重力 0–1 步延迟(≈20ms),关节速度 1 步延迟 | 匹配真实通信延迟 |
JointPositionActionCfg,scale=1.0:策略输出 14 维关节位置偏移量,叠加到默认姿态偏移上。StandUp 任务使用 FreezeThenJointPositionActionCfg——前 2 秒冻结动作(让机器人在掉落后稳定),然后放开控制。
twist均匀采样:\(v_x \in [-0.4, 0.4]\) m/s, \(v_y \in [-0.3, 0.3]\) m/s, \(\omega_z \in [-1.0, 1.0]\) rad/s
head_pose4D 从 HOME 的偏移量 [neck_pitch, head_pitch, head_yaw, head_roll],2–5s 重采样。课程分 5 阶段从 ±5% 扩展到各关节机械极限:
body_pose6D 偏移量 [x, y, z, roll, pitch, yaw]。在 Velocity 任务中权重为 0(保持输入神经元活跃但不主导),Standup 任务提升权重。
Velocity 任务的总奖励为多项加权求和,采用高斯核形式 \(\exp(-e^2/\sigma^2)\) 保证梯度始终非零。
| 项 | 权重 | σ | 说明 |
|---|---|---|---|
| 线速度跟踪 | w₁ = 3.0 (XgoDuck) | √0.25 | 误差为 σ 时奖励 = 1/e ≈ 0.37 |
| 角速度跟踪 | w₂ = 2.0 | √0.5 | 角速度容差更大(转弯难度高) |
按速度分级 σ(standing / walking / running),低速时更紧、高速时更松。XgoDuck 调高权重至 1.3,并收紧 hip_yaw 的 σ(standing 0.05, walking 0.15, running 0.25)。
280g 头部占机器人 38% 质量,行走时不可避免振荡。瞬时紧容差会永久惩罚行走(不可逃避),导致策略放弃走路。但 DC 偏置(重力下垂)是可逃避的——策略可偏置颈部指令抵消重力。EMA 滤波让振荡抵消,只惩罚偏置。
课程从 iter 600 开始:w: 0 → 1.0 → 2.0 → 3.0。在 weight 3.0 时,15° 残余偏置每步代价 0.79,2° 偏置代价 0.10——精确地价格可改善的误差。
| 奖励 | 权重 | 公式/说明 |
|---|---|---|
upright | +2.0 | \(\exp(-\theta_{tilt}^2 / 0.05)\),强惩罚躯干倾斜 |
air_time | +3.0 | 摆动相时长在 [0.125, 0.3]s 窗口内奖励 |
foot_swing_height | -5.0 | L1 惩罚摆动脚高偏离目标 0.02m |
foot_clearance | — | 惩罚拖步 |
foot_slip | -0.1 | 弱惩罚脚底滑动(弱因转弯需枢轴转动) |
body_ang_vel | -0.05 | 惩罚躯干旋转 |
angular_momentum | -0.02 | 惩罚全身角动量(稳定性) |
action_rate_l2 | 课程 -0.1→-1.0 | 动作平滑度,逐步收紧 |
self_collisions | -1.0 | 腿撞躯干的惩罚 |
hip_yaw_home | -1.0 | 低速时髋偏航保持 HOME(允许转弯) |
这是 sim2real 迁移的核心,每 episode reset 重新采样(非累积):
| 随机化项 | 范围 | 机制 | 频率 |
|---|---|---|---|
| 躯干质心偏移 | ±3mm → ±15mm(课程) | dr.body_ipos add | reset |
| 头部质心偏移 | ±3mm → ±10mm | 同上,逐 body | reset |
| 质量+惯量 | ±5% | \(\alpha\) 缩放 \(m\) 和 \(I\) 同时 \(e^{2\alpha}\),CoM 不变 | startup |
| 关节摩擦 | ±10% | FrictionDRBamActuator.friction_scale | reset |
| 电枢(转子惯量) | ±10% | dr.joint_armature scale | reset |
| 脚底摩擦 | 0.4 – 1.5 | 几何属性 | reset |
| IMU 安装误差 | ≤6° | 观测级旋转 | startup |
| 编码器偏置 | ±0.86° | 观测级,固定/env | startup |
| 速度推力 | ±0.3 m/s | 踢基底速度 | 每 3–6s |
| 电压跌落 | vin 7.4–8.0V, drop 0–0.2 | BAM 参数 | reset |
| 命令延迟 | Walk 3–6步 (15–30ms),其他 1步 | mjlab delay buffer | 固定 |
CoM 随机化上限从 ±30mm 降至 ±15mm:脚后跟距踝仅 20mm,±30mm 可使 CoM 完全在支撑多边形外,迫使超反应步态,使后退平衡不可训练。
所有训练通过 mjlab 的 train 命令启动,使用 uv run 在项目虚拟环境中执行。命令格式:
uv run train <TASK_ID> [--env.<path>.<value> ...] [--agent.<path>.<value> ...] [其他全局选项]
uv run list-envs
打印每个已注册的任务 ID。XgoDuck 相关任务包括:
| 任务 ID | 地形 | 日志目录 | 说明 |
|---|---|---|---|
Mjlab-Velocity-Flat-XgoDuck | 平地 | logs/rsl_rl/xgoduck_velocity/ | 主行走任务 |
Mjlab-Velocity-Rough-XgoDuck | 崎岖 | logs/rsl_rl/xgoduck_velocity/ | 粗糙地形行走 |
Mjlab-Velocity-Swizzle-XgoDuck | 平地 | logs/rsl_rl/xgoduck_velocity_swizzle/ | 滑冰式交替蹬地 |
Mjlab-StandUp-Flat-XgoDuck | 平地 | logs/rsl_rl/xgoduck_standup/ | 从倒地起立 |
Mjlab-StandUp-Rough-XgoDuck | 崎岖 | logs/rsl_rl/xgoduck_standup/ | 粗糙地形起立 |
Mjlab-GroundPick-Flat-XgoDuck | 平地 | logs/rsl_rl/xgoduck_ground_pick/ | 蹲下拾取 |
Mjlab-GroundPick-Rough-XgoDuck | 崎岖 | logs/rsl_rl/xgoduck_ground_pick/ | 粗糙地形拾取 |
Mjlab-SitStand-Flat-XgoDuck | 平地 | logs/rsl_rl/xgoduck_sitstand/ | 坐↔站转换 |
Mjlab-SitStand-Rough-XgoDuck | 崎岖 | logs/rsl_rl/xgoduck_sitstand/ | 粗糙地形坐站 |
Mjlab-BallKick-Flat-XgoDuck | 平地 | logs/rsl_rl/xgoduck_ball_kick_right/ | 右脚踢球 |
Mjlab-BallKick-Left-Flat-XgoDuck | 平地 | logs/rsl_rl/xgoduck_ball_kick_left/ | 左脚踢球 |
Mjlab-Roulade-Flat-XgoDuck | 平地 | logs/rsl_rl/xgoduck_roulade/ | 前滚翻 |
Mjlab-Roulade-Angle-Flat-XgoDuck | 平地 | logs/rsl_rl/xgoduck_roulade_angle/ | 带角度观测的前滚翻 |
uv run train Mjlab-Velocity-Flat-XgoDuck --env.scene.num-envs 4096
uv run train Mjlab-Velocity-Rough-XgoDuck --env.scene.num-envs 4096
uv run train Mjlab-Velocity-Swizzle-XgoDuck --env.scene.num-envs 4096
uv run train Mjlab-Velocity-Flat-XgoDuck --env.scene.num-envs 2048
uv run train Mjlab-Velocity-Flat-XgoDuck --agent.max-iterations 5 --env.scene.num-envs 64
mjlab 使用 tyro CLI 框架解析命令行参数,支持通过点分路径覆盖环境配置 (--env.*) 和 RL 配置 (--agent.*) 中的任意字段。
--env.*)| 参数 | 默认值 | 说明 |
|---|---|---|
--env.scene.num-envs | 4096 | 并行环境数。越大采样越快,但显存消耗线性增长。2048 适合 8GB GPU |
--env.sim.mujoco.timestep | 0.005 (Walk) / 0.004 (其他) | 物理仿真步长(秒)。Walk 用 5ms,StandUp 等用 4ms |
--env.decimation | 5 | 控制频率 = 1 / (timestep × decimation)。5 × 4ms = 50Hz |
--env.sim.nconmax | 35 (平地) / 200 (崎岖) | 最大接触数。崎岖地形需增大防止接触溢出→NaN |
--env.sim.mujoco.iterations | 10 (平地) / 30 (崎岖) | MuJoCo 约束求解器迭代次数 |
--env.sim.mujoco.ls-iterations | 20 (平地) / 50 (崎岖) | 线搜索迭代次数 |
--env.episode-length-s | 任务特定 | 单 episode 时长(秒)。Velocity 通常 20s,StandUp 8s |
--env.scene.terrain.terrain-type | "plane" / "generator" | 地形类型。平地="plane",崎岖="generator" |
--agent.*)| 参数 | 默认值 | 说明 |
|---|---|---|
--agent.max-iterations | 50,000 (Vel) / 15,000 (StandUp) | 最大训练迭代数。每个迭代 = num_envs × num_steps_per_env 步 |
--agent.num-steps-per-env | 24 | 每次 rollout 每环境采集步数。4096×24 = 98,304 样本/迭代 |
--agent.save-interval | 250 | 每多少迭代保存一次 checkpoint |
--agent.learning-rate | 1e-3 | 初始学习率(adaptive 模式下基于 KL 自适应调整) |
--agent.clip-param | 0.2 | PPO 裁剪比率 ε |
--agent.entropy-coef | 0.01 | 熵正则化系数(鼓励探索) |
--agent.gamma | 0.99 | 折扣因子 γ |
--agent.lam | 0.95 | GAE 参数 λ |
--agent.desired-kl | 0.01 | 目标 KL 散度,自适应 LR 的锚点 |
--agent.max-grad-norm | 1.0 | 梯度裁剪范数 |
--agent.num-learning-epochs | 5 | 每批数据 PPO 训练轮数 |
--agent.num-mini-batches | 4 | 每轮切分的 minibatch 数 |
--agent.actor.hidden-dims | (512, 256, 128) | Actor MLP 隐藏层维度 |
--agent.critic.hidden-dims | (512, 256, 128) | Critic MLP 隐藏层维度 |
--agent.experiment-name | "xgoduck_velocity" 等 | TensorBoard/checkpoint 目录名 |
--agent.run-name | "xgoduck" | wandb 中的运行名 |
| 选项 | 说明 |
|---|---|
--gpu-ids <ids> | 指定使用的 GPU(如 --gpu-ids 0 或 --gpu-ids 0,1) |
--checkpoint-file <path> | 从指定 checkpoint 恢复训练 |
--hf-jobs | 提交训练到 HuggingFace Jobs 远程执行(见 6.6 节) |
--headless | 无头模式(无可视化窗口,适合服务器) |
-h / --help | 打印完整参数帮助(列出所有可覆盖字段) |
运行 uv run train Mjlab-Velocity-Flat-XgoDuck --help 可列出所有可通过 CLI 覆盖的参数,包括嵌套的环境配置和 RL 超参数。tyro 会自动从 dataclass 生成完整的参数文档。
| 参数 | Velocity 值 | StandUp 值 | 说明 |
|---|---|---|---|
| Actor 网络 | (512, 256, 128) ELU | (512, 256, 128) ELU | 观测归一化(EmpiricalNormalization 内嵌) |
| Critic 网络 | (512, 256, 128) ELU | (512, 256, 128) ELU | 额外特权信息输入 |
| 分布 | Gaussian, scalar std | Gaussian, scalar std | 初始 std = 1.0 |
| clip_param | 0.2 | 0.2 | PPO 裁剪比率 |
| entropy_coef | 0.01 | 0.01 | 探索激励 |
| num_learning_epochs | 5 | 5 | 每批数据训练轮数 |
| num_mini_batches | 4 | 4 | 每轮切分数 |
| learning_rate | 1e-3 | 1e-3 | adaptive:基于 KL 自适应 |
| gamma | 0.99 | 0.99 | 折扣因子 |
| λ (GAE) | 0.95 | 0.95 | 广义优势估计 |
| desired_kl | 0.01 | 0.01 | KL 目标,自适应 LR 锚点 |
| max_grad_norm | 1.0 | 1.0 | 梯度裁剪 |
| num_steps_per_env | 24 | 24 | 每次 rollout 环境步数 |
| num_envs | 4096 | 4096 | 并行环境数 |
| max_iterations | 50,000 | 15,000 | 最大训练迭代 |
| save_interval | 250 | 250 | checkpoint 保存间隔 |
| logger | tensorboard | tensorboard | 日志后端 |
| upload_model | False | False | 是否上传到 wandb |
代码实现了完整的左右对称镜像增强(61 维排列 + 符号表),镜像规则:
当前 ENABLE_SYMMETRY = False,镜像损失系数 0.5。可在代码中开启以加速收敛。
项目提供两个 Shell 脚本实现顺序多阶段训练——每个阶段独立从头训练,不共享 checkpoint。
scripts/train_xgoduck_getup_then_velocity.sh
# 默认:4096 环境,平地
bash scripts/train_xgoduck_getup_then_velocity.sh
# 崎岖地形
bash scripts/train_xgoduck_getup_then_velocity.sh --rough
# 跳过起立阶段,仅训练行走
bash scripts/train_xgoduck_getup_then_velocity.sh --skip-getup
# 自定义环境数 + 指定 GPU
NUM_ENVS=2048 bash scripts/train_xgoduck_getup_then_velocity.sh --gpu-ids 0
# 转发额外参数给 train 命令(如冒烟测试)
bash scripts/train_xgoduck_getup_then_velocity.sh --agent.max-iterations 5
流程:① Mjlab-StandUp-Flat-XgoDuck → ② Mjlab-Velocity-Flat-XgoDuck
scripts/train_xgoduck_pick_standup_velocity.sh
# 完整三阶段
bash scripts/train_xgoduck_pick_standup_velocity.sh
# 跳过拾取和起立,仅训练行走
bash scripts/train_xgoduck_pick_standup_velocity.sh --skip-pick --skip-standup
# 崎岖地形全部三阶段
bash scripts/train_xgoduck_pick_standup_velocity.sh --rough
流程:① Mjlab-GroundPick-Flat-XgoDuck → ② Mjlab-StandUp-Flat-XgoDuck → ③ Mjlab-Velocity-Flat-XgoDuck
| 选项 | 说明 |
|---|---|
--skip-getup / --skip-standup | 跳过起立阶段 |
--skip-pick | 跳过拾取阶段 |
--rough | 使用崎岖地形版本的任务 |
NUM_ENVS=<N> | 环境变量,覆盖默认 4096 |
--agent.max-iterations <N> | 转发给 train 的参数(冒烟测试用) |
-- | 分隔符:之后的参数全部转发给 train |
添加 --hf-jobs 标志可将训练提交到 HuggingFace Jobs 远程 GPU 节点执行,无需本地 GPU。
uv run train Mjlab-Velocity-Flat-XgoDuck \
--env.scene.num-envs 4096 \
--agent.max-iterations 4000 \
--hf-jobs
此命令会在导入阶段被 train_hook.py 拦截,将源码打包上传到 HF,然后在远程容器内执行 uv run train。
| 参数 | 默认值 | 说明 |
|---|---|---|
--flavor | l4x1 | GPU 硬件规格(NVIDIA L4 × 1) |
--image | pytorch/pytorch:2.5.1-cuda12.4-cudnn9-runtime | Docker 镜像 |
--timeout | 12h | 任务最大运行时长 |
--namespace | 交互选择 | HF 命名空间(个人账号或组织) |
--run-name | task+timestamp | 运行标签 |
--detach | False | 提交后立即返回(不流式传输日志) |
--dry-run | False | 打印任务规格但不提交 |
--no-wandb | False | 不转发 wandb API key |
--uv-cache | False | 挂载持久化 uv 缓存桶(默认关闭,FUSE 挂载不支持 hardlink) |
scripts/hf/uploader.py 后台监控 logs/rsl_rl/,自动推送到 HF model repoAUTO_EXPORT=1)$WANDB_API_KEY 或 ~/.netrc 获取 key 并注入容器多维度渐进式课程,分阶段开启难度。以下以 Velocity 任务为例:
| 课程 | 阶段 | iter 范围 | 变化 |
|---|---|---|---|
| action_rate 平滑度 | 1 | 0 | -0.1 |
| 2 | 500 | -0.2 | |
| 3 | 750 | -0.4 | |
| 4 | 1000 | -0.6 | |
| 5 | 1250 | -0.8 | |
| 6 | 1500 | -1.0 | |
| 站立环境比例 | 1 | 0 | 2% |
| 2 | 500 | 5% | |
| 3 | 750 | 10% | |
| 4 | 1000 | 15% | |
| 5 | 1500 | 20% | |
| 6 | 2000 | 25% | |
| 头部姿态指令范围 | 1 | 0 | 5% |
| 2 | 500 | 15% | |
| 3 | 1000 | 35% | |
| 4 | 1500 | 65% | |
| 5 | 2000 | 100% | |
| 头部偏置惩罚 | 1 | 0–600 | 0 (关闭) |
| 2 | 600 | 1.0 | |
| 3 | 1000 | 2.0 | |
| 4 | 1500 | 3.0 | |
| 躯干质心随机化 | 1 | 0 | ±3mm |
| 2 | 500 | ±5mm | |
| 3 | 1000 | ±10mm | |
| 4 | 1500 | ±15mm |
| 课程 | 说明 |
|---|---|
ground_state_mix | 初始姿态混合:iter 0 以坐/站为主 → iter 2500 以面朝下/面朝上为主(逆课程,先学简单再学难恢复) |
arrival_damping_weight | 到达阻尼:iter 0–3000 关闭 → iter 3000 引入 → iter 4000 达 -0.05(技能发现后再精调) |
head_pose_bias_weight | 头部偏置:iter 0–1000 关闭 → iter 4000 达 3.0 |
torque_rate_weight | 力矩变化率:iter 0–3000 关闭 → iter 3000 引入 -1e-3(防抖) |
body_pose_tracking_weight | 躯干跟踪:iter 0–2500 关闭 → iter 4000 达 4.0(恢复技能掌握后开启身体控制) |
push_magnitude | 推力幅度:iter 0 零推力 → iter 1000 达 ±0.3 m/s(渐进增加鲁棒性) |
uv run train Mjlab-Velocity-Flat-XgoDuck --env.scene.num-envs 4096
| 指标 | Velocity | StandUp | 说明 |
|---|---|---|---|
| 每迭代样本数 | 98,304 | 98,304 | 4096 × 24 |
| 每迭代梯度更新 | 20 | 20 | 5 epochs × 4 minibatches |
| 物理步长 | 5ms | 4ms | Walk 用 5ms,其他用 4ms |
| 控制频率 | 50Hz | 50Hz | 1 / (timestep × decimation) |
| 最大迭代 | 50,000 | 15,000 | Vel 训练约 8–12 小时 |
| 总交互步数 | ~4.9B | ~1.5B | max_iter × num_envs × steps_per_env |
tensorboard --logdir logs/rsl_rl/
监控奖励曲线、学习率、KL 散度、episode 长度等。XgoDuck 默认使用 TensorBoard(logger="tensorboard")。
uv run play Mjlab-Velocity-Flat-XgoDuck
uv run play Mjlab-Velocity-Swizzle-XgoDuck
无 checkpoint 标志时自动加载最新 model_*.pt。用 --checkpoint-file 指定特定 checkpoint。
uv run python scripts/wandb_utils.py
查找最近的 wandb 运行,打印奖励分解、学习率、checkpoint 列表等。
uv run python scripts/plot_observations_comparison_plotly.py
对比不同策略的观察值分布,使用 Plotly 交互式图表。
| 指标 | 含义 | 健康信号 |
|---|---|---|
Train/mean_reward | 平均 episode 奖励 | 稳步上升 |
Episode_Reward/track_linear_velocity | 线速度跟踪奖励 | 接近 1.0(满分) |
Episode_Reward/track_angular_velocity | 角速度跟踪奖励 | 接近 1.0 |
Episode_Reward/air_time | 摆动相时长奖励 | 接近 1.0(步态稳定) |
Episode_Reward/head_pose_tracking | 头部姿态跟踪 | 接近 1.0 |
Episode_Termination/nan_state | NaN 终止次数 | ≈0(物理稳定) |
Loss/learning_rate | 当前学习率 | adaptive 模式下随 KL 波动 |
Loss/mean KL | 新旧策略 KL 散度 | 接近 desired_kl=0.01 |
Loss/entropy | 策略熵 | 缓慢下降(探索→收敛) |
Episode/length | 平均 episode 长度 | 接近 episode_length_s / dt |
uv run train Mjlab-Velocity-Flat-XgoDuck \
--checkpoint-file logs/rsl_rl/xgoduck_velocity/<run>/model_3000.pt
# 自动找到 logs/rsl_rl/xgoduck_velocity/ 下最新的 model_*.pt
uv run play Mjlab-Velocity-Flat-XgoDuck
uv run play Mjlab-Velocity-Flat-XgoDuck \
--checkpoint-file logs/rsl_rl/xgoduck_velocity/<run>/model_5000.pt
uv run play Mjlab-Velocity-Flat-XgoDuck \
--wandb-run-path pollen-robotics/mjlab_microduck/<run_id>
| 任务 | 目标 | 物理模型 | max_iter |
|---|---|---|---|
| Velocity | 行走 + 转向 + 头部控制 | Walk(仅脚碰撞) | 50,000 |
| StandUp | 从倒地起立 | 全身碰撞,冻结+掉落起启 | 15,000 |
| SitStand | 坐 ↔ 站转换 | 全身碰撞 | 15,000 |
| GroundPick | 蹲下嘴触地 → 起身 | 全身碰撞,相位化姿态跟踪 | 15,000 |
| BallKick | 踢球(左/右脚) | Walk + 球 | 15,000 |
| Roulade | 前滚翻 | 全身碰撞(排除重叠关节) | 15,000 |
| Swizzle | 滑冰式交替蹬地 | 被动轮模型 | 50,000 |
| Backlash 变体 | 全部任务的齿隙版本 | 串联齿隙铰链 + 编码器穿过齿隙 | 同基础任务 |
cd xgoduck_rl && uv syncuv sync 设置 UV_HTTP_TIMEOUT=600(CUDA wheel 较大)logs/rsl_rl/<experiment_name>/<run>/actor(normalizer(obs))BAM 执行器模型精确模拟电磁-摩擦耦合,而非理想 PD——这是 sim2real 成功的关键。摩擦预算包含库仑、Stribeck、负载相关、粘性四项。
头部偏置惩罚用 EMA 滤波只惩罚 DC 分量——"只惩罚策略能改善的误差"。行走振荡不可逃避(不罚),重力下垂可逃避(罚)。
从简单到困难:先建立步态(低 action_rate 惩罚),再逐步施加精度要求(头部跟踪)、鲁棒性要求(CoM 随机化)和稳定性(偏置惩罚)。
策略只看传感器级信息(模拟真机):带噪声、带延迟、带偏置。Critic 看特权信息(真实线速度、接触力)加速训练收敛。
从质心到摩擦到 IMU 装配误差,覆盖所有真实不确定性来源。每 episode reset 重新采样,非累积。
\(\exp(-e^2/\sigma^2)\) 保证梯度始终非零,误差为 σ 时奖励 1/e。均值形式(非平方和)保证单维度偏差不杀死整体梯度。
前面 8 章全是公式和术语,这章我们用最通俗的大白话把整个事情从头讲一遍。不用懂强化学习,不用懂数学,看完你就知道这机器人的"大脑"长什么样、吃什么吐什么、怎么学会走路的。
黄色光点表示数据正向传播:传感器输入 → 三层全连接 → 14 个关节动作输出
别被"PPO""Actor-Critic"这些词吓到。本质上,模型就是一个三层全连接神经网络(MLP),跟你手写的那个没什么区别:
输入(61 个数字)
│
▼
┌──────────────┐
│ 全连接层 1 │ 512 个神经元,激活函数 ELU
├──────────────┤
│ 全连接层 2 │ 256 个神经元,激活函数 ELU
├──────────────┤
│ 全连接层 3 │ 128 个神经元,激活函数 ELU
├──────────────┤
│ 输出层 │ 14 个数字(对应 14 个关节)
└──────────────┘
没有 Transformer,没有注意力机制,没有卷积。就是最朴素的 Linear → ELU → Linear → ELU → Linear → ELU → Linear。三层隐藏层,规模从大到小(512→256→128),像一个漏斗,把信息逐步压缩。
ReLU 在负数区域输出恒为 0("死掉"),机器人控制中很多信号是有正有负的(比如关节角度偏差左偏右偏)。ELU 在负数区域也有输出(平滑的负值),梯度不会断,训练更稳。
严格说,这不是一个网络,而是两个网络共享前三层:
看完传感器数据,直接输出 14 个关节该怎么动。这就是最终部署到真机上的那个大脑。它看到的信息和真机一模一样:带噪声、带延迟、带偏置。
看完同样的情况,输出一个数字:当前这个状态值多少分?它比 Actor 多看一些特权信息(真实速度、真实接触力等真机测不到的东西)。它只在训练时用,帮 Actor 更快学习,部署时不要它。
每 0.02 秒(50Hz),机器人的大脑会收到 61 个数字,这就是它的全部"感官"。来源包括:
| 感官 | 维度 | 通俗解释 |
|---|---|---|
| 本体姿态指令 | ~3 | 命令你"往前走 / 转弯 / 停下",别人给的遥控指令 |
| 关节角度偏差 | ~14 | 每个关节"现在在哪"和"应该在哪"差多少,本体感觉 |
| 关节速度 | ~14 | 每个关节转得多快,类似你感觉自己手臂挥动的速度 |
| 上一帧动作 | ~14 | 你上次让关节动了多少,记住自己刚做了什么 |
| 身体姿态(IMU) | ~6 | 身体朝哪倾斜、转了多少,类似人的前庭系统(管平衡的) |
| 身体角速度 | ~3 | 身体正在往哪边倒的速度 |
| 线速度(估计) | ~3 | 整体往前/往后/往侧面飘的速度 |
| 足部接触 | ~2 | 左右脚是否踩在地上,类似脚底的触觉 |
| 其他(定时器等) | ~2 | 辅助信息,比如原地转弯的时间到了没 |
大脑吐出 14 个数字,每个数字在 −1 到 +1 之间(经过 tanh 压缩)。这 14 个数字分别对应机器人 14 个关节的目标角度偏移量:
输出 = [关节1目标偏移, 关节2目标偏移, ..., 关节14目标偏移]
↑ ↑
范围 [-1, 1] 乘以缩放系数后加到"默认站姿"上
也就是说,不是让关节直接去某个绝对位置,而是在默认姿势基础上微调。比如默认是站直的,输出 [0.1, -0.05, ...] 就意味着"比站直稍微抬一下左腿、收一点右腿"。这种设计让网络不用从零学起,学习效率高很多。
每条腿 5 个(髋关节 3 个:俯仰/滚转/偏航,膝关节 1 个,踝关节 1 个),两条腿共 10 个。头部 4 个(颈部俯仰/偏航 + 头部俯仰/偏航),用来控制嘴巴和头的朝向。
这 14 个目标角度不会直接发给电机。中间还隔着一层 BAM 执行器模型——它模拟真实舵机的电磁力、摩擦、延迟,把"目标角度"变成"实际转到的角度"。所以仿真里的机器人和真机一样"笨拙"。
训练的本质就是试错。没有人类示范,没有预编程动作,机器人完全从零开始,靠奖励信号自己摸索。流程是这样的:
机器人怎么知道自己做得好不好?靠一堆奖励项加减。每个奖励项就是一个"规则",符合规则加分,违反扣分。主要的奖励项用大白话说:
| 奖励项 | 权重 | 大白话 |
|---|---|---|
| 跟踪线速度 | 3.0 | 让你往前走,走对方向就给糖(最重要的一项) |
| 跟踪角速度 | 1.5 | 让你按指令转弯,转对了给糖 |
| 跟踪站立姿态 | 2.0 | 没让你走的时候就站好,别乱动 |
| 头部姿态跟踪 | 0.5 | 头别歪,眼睛看前方 |
| 动作平滑惩罚 | -0.05 | 别抽搐,动作别太剧烈(扣分项) |
| 动作变化率惩罚 | -0.005 | 别一帧一帧地抖,动作要连贯(扣分项) |
| 足部交替惩罚 | -0.5 | 别两只脚同时离地,也别拖着脚走 |
| 滑步惩罚 | -0.1 | 脚踩在地上就别滑,要真走不要溜冰 |
| 躯干朝向惩罚 | -0.2 | 身体别歪,保持直立 |
| 超限惩罚 | -0.05 | 关节别转到极限,留余地 |
每一项的权重决定了机器人更在乎什么。比如线速度权重 3.0 最高——最重要的就是把路走对。动作变化率惩罚只有 −0.005——轻微提醒别抖,但不会为了不抖而牺牲走路能力。
如果一上来就把所有要求都加上,机器人啥也不会,每项都扣分,总分永远负的,根本不知道该怎么改。所以用课程学习——一开始要求很低,慢慢加难度:
第 0 轮: 只要别摔就行,动作怎么抽搐都无所谓
↓ 慢慢加难度
第 5000 轮: 要走对方向,动作开始要求平滑
↓ 继续加难度
第 15000 轮:头要稳,身体要直,脚不能滑
↓ 最后阶段
第 40000 轮:全身随机化都打开,连质心位置都随机变
目标:在什么烂条件下都能走稳
具体哪些难度是慢慢加的:
| 课程维度 | 开始 | 结束 | 大白话 |
|---|---|---|---|
| 动作平滑要求 | 几乎不罚 | 正常罚 | 先让你随便动,再要求动作好看 |
| 站立比例 | 10% | 30% | 先多练走路,后面加站立训练 |
| 头部精度要求 | 松 | 紧 | 先不管头,后面要求头也要稳 |
| 质心随机化 | 无 | ±2cm | 先在理想条件下练,后面加不确定性 |
| 偏置惩罚 | 关闭 | 开启 | 先不管姿态偏置,后期才罚 |
纯随机试错效率太低。PPO(Proximal Policy Optimization)的核心思想是:
| 任务 | 训练轮数 | 大概含义 |
|---|---|---|
| 站立起身(StandUp) | 15,000 轮 | 学会从摔倒状态爬起来 |
| 地面拾取(GroundPick) | 15,000 轮 | 学会蹲下捡东西再站起来 |
| 行走(Velocity) | 50,000 轮 | 学会往前走、转弯、站立 |
在一张 GPU 上,行走任务大约跑 3-5 小时。4096 个机器人同时练,效率很高。
导出时,输入归一化(减均值除标准差)会被烘焙进 ONNX 文件。也就是说,真机拿到的是 61 个原始数字,ONNX 内部自己先做归一化再推理,真机代码不用管归一化的事,直接喂原始数据就行。
┌─────────────────────────────────────────────────────────┐
│ 训练阶段(仿真器里) │
│ │
│ 4096 个机器人同时跑 │
│ │ │
│ ▼ │
│ 每个机器人感知到 61 个数字(带噪声/延迟/偏置) │
│ │ │
│ ▼ │
│ Actor 神经网络(512→256→128)输出 14 个关节动作 │
│ │ │
│ ▼ │
│ BAM 执行器模拟真实舵机 → 关节实际转动 │
│ │ │
│ ▼ │
│ 物理引擎算出机器人怎么动了 → 算奖励(走对了+分,摔了-分) │
│ │ │
│ ▼ │
│ Critic(看特权信息)打分 → PPO 小幅更新 Actor 参数 │
│ │ │
│ ↺ 重复 5 万轮 │
│ │
├─────────────────────────────────────────────────────────┤
│ 部署阶段(真机上) │
│ │
│ 传感器读数(61 个数字)→ ONNX 模型 → 14 个关节动作 → 电机 │
│ 每 20ms 循环一次,没有 Critic,没有奖励,纯推理 │
│ │
└─────────────────────────────────────────────────────────┘