跳到主要内容

6. 策略训练

上一章的 trot 是我们编程让 Pupper 走;这一章则让它通过强化学习自己寻找动作策略。我们会把 MuJoCo 仿真封装成 Gymnasium 环境,再用 Stable-Baselines3 的 PPO 训练一条能够接收 (vx, vy, wz) 速度命令的低层控制策略。

本章目标

  • 能把 MuJoCo 仿真封装成标准 gymnasium.Env
  • 能解释基础模式的 45 维观测和 gait 模式的 50 维观测
  • 能理解 12 维关节位置残差动作和 PD 位置控制
  • 能读懂 7 项基础奖励以及可选的 gait 接触奖励
  • 能通过 YAML 配置运行、续训和评估 PPO 策略
  • 能根据仓库中的真实训练报告判断策略已经学会了什么、还没有学会什么
  • 能理解把步态从"能走"打磨到"走得好"的一整套机制:奖励经济学与课程训练、动作滤波、步态条件化和摆动整形

前置阅读

6.1 任务定义

策略每个控制周期接收目标速度:

指令含义训练范围
vx机身坐标系前后速度[-0.75, 0.75] m/s
vy机身坐标系横向速度[-0.5, 0.5] m/s
wz绕竖直轴的角速度[-2.0, 2.0] rad/s

PPO 输出 12 个归一化关节位置残差。环境把残差加到默认站姿上,再由 MuJoCo 的 position actuator 和 PD 参数产生关节力矩。

基础模式只指定机身应该怎么移动,不强制四只脚使用某一种步态。打开 gait.enabled 后,策略还会收到 walktrotpace 的步态标识与周期相位,并通过额外的足端接触奖励学习目标接触顺序。

6.2 Gymnasium 环境

环境定义在 6.rl_pupper/pupper_env.py,机器人模型复用课程公共资源 assets/mjcfs/pupper_v3.xml。关键控制参数如下:

DT_PHYSICS = 0.004 # MuJoCo 物理步长:250 Hz
DT_CONTROL = 0.02 # 策略控制周期:50 Hz
KP, KD = 5.0, 0.25
ACTION_SCALE = 0.5
MAX_STEPS = 1000 # 20 秒一个 episode

环境遵循 Gymnasium 的五元组接口:

obs, info = env.reset(seed=0)
obs, reward, terminated, truncated, info = env.step(action)

其中:

  • terminated=True 表示机身过低或倾斜超过阈值,机器人已经跌倒;
  • truncated=True 表示回合达到 1000 个控制步;
  • info 保存每个已缩放奖励项,gait 模式还会保存目标/实际接触和接触匹配率。

6.2.1 Reset 随机化

每次 reset 会:

  1. 把机身放在 (0, 0, 0.28)
  2. 随机采样 [-π, π] 的 yaw;
  3. 在默认站姿附近给 12 个关节加入 ±0.05 rad 噪声;
  4. 清空速度、上一动作和足端腾空计时;
  5. 随机采样一组新的 (vx, vy, wz) 命令。

当前实现没有质量、摩擦、PD 增益随机化,也没有外力 kick、观测噪声或动作延迟。它首先解决平地仿真里的速度跟踪,尚不是可直接部署到真机的 sim2real 环境。

6.2.2 终止条件

满足任一条件就提前终止:

body_z < 0.10
up_in_body_z < cos(0.52)

第二个条件等价于机身倾斜约 30° 以上。提前终止既限制危险姿态,也让 PPO 不必继续收集已经摔倒后的无效数据。

6.3 Observation 与 Action

6.3.1 基础模式:45 维观测

基础观测是单帧状态,不做历史帧堆叠:

顺序内容维度
1机身坐标系角速度3
2机身坐标系重力方向3
3目标命令 (vx, vy, wz)3
4关节角相对默认站姿的偏差12
5关节速度12
6上一步动作12
合计45

核心拼接逻辑是:

obs = np.concatenate([
ang_vel, # 3
gravity, # 3
self.cmd, # 3
joint_angles - DEFAULT_POSE, # 12
joint_vel, # 12
self.last_action, # 12
]).astype(np.float32)

重力方向比世界坐标系四元数更适合做姿态特征,因为它不依赖绝对 yaw。上一动作让策略知道自己刚刚发出了什么命令,有助于学习更平滑的动作变化。

当前观测没有直接提供机身线速度。策略需要从关节状态、姿态变化以及多步交互中间接推断速度,这也是低速跟踪仍会超调的原因之一(6.9 的进阶版补上了这一项)。

6.3.2 Gait 模式:50 维观测

gait.enabled: true 时,在原 45 维末尾追加:

  • walk/trot/pace 三维 one-hot:3 维;
  • sin(phase), cos(phase):2 维。

使用正余弦表达周期相位,可以避免 phase 从 1 跳回 0 时产生数值不连续。

6.3.3 12 维 PD 残差动作

动作空间为:

spaces.Box(-1.0, 1.0, shape=(12,), dtype=np.float32)

动作先转换成默认站姿附近的目标角,再裁剪到关节限位:

motor_target = np.clip(
DEFAULT_POSE + 0.5 * action,
JOINT_LOWERS,
JOINT_UPPERS,
)

残差动作让策略从能够站立的姿态开始探索,比直接输出任意目标关节角更稳定。物理仿真每 4 ms 前进一步,每个策略动作保持 5 个物理步。

6.4 奖励设计

当前基础环境使用 7 项奖励:

REWARD_WEIGHTS = {
"tracking_lin_vel": 1.5,
"tracking_ang_vel": 0.8,
"orientation": -5.0,
"torques": -2e-4,
"action_rate": -0.01,
"feet_air_time": 0.2,
"termination": -100.0,
}

奖励项函数和权重遵循同一符号约定:tracking 项返回越大越好的正值,惩罚项返回非负幅值并乘负权重。

奖励项含义
tracking_lin_vel跟踪机身坐标系下的 (vx, vy)
tracking_ang_vel跟踪绕竖直轴的 wz
orientation惩罚机身竖直方向偏离世界竖直方向
torques惩罚 12 个关节的平方力矩和
action_rate惩罚当前动作与上一动作的平方差
feet_air_time非静止命令下,在足端重新落地时奖励足够长的腾空时间
termination跌倒时施加一次终止惩罚

6.4.1 速度跟踪

线速度奖励为:

角速度奖励使用相同形式:

指数速度奖励有上界:误差越小越接近 1,误差增大时平滑衰减。
指数速度奖励有上界:误差越小越接近 1,误差增大时平滑衰减。

6.4.2 稳定、能耗和动作平滑

姿态项使用机身竖直轴在世界坐标系水平面的投影平方和;力矩项读取 data.qfrc_actuator;动作变化项计算 ||a_t-a_{t-1}||²。三者分别限制身体倾斜、电机输出和高频抖动。

足端接触目前通过 foot site 高度近似判断,而不是遍历完整的 MuJoCo contact pair:

foot_z = site_z - FOOT_RADIUS
contact = foot_z < 1e-3

6.4.3 总奖励

所有已缩放项先求和、乘控制周期,再裁剪为非负数:

reward = np.clip(sum(scaled.values()) * 0.02, 0.0, 10000.0)

因此 episode return 不会出现负值。跌倒惩罚会压低终止步的奖励,同时 terminated=True 结束当前回合。

6.4.4 Gait 接触奖励

gait 模式会根据当前 gait、周期相位、相位偏移和 duty factor 计算四只脚的期望接触状态,再计算:

contact_match = mean(actual_contact == expected_contact)
r_gait_contact = contact_reward_weight * contact_match

默认迁移配置使用权重 0.5;从零训练的 gait 对照配置将权重提高到 4.0

6.5 三种训练配置

配置文件位于 6.rl_pupper/configs/

配置观测训练方式预算用途
train.yaml45从零训练20M默认基础策略
train_no_gait.yaml45从零训练20M公平对照 A
train_gait.yaml50从零训练20M公平对照 B
finetune_gait.yaml50从 45 维 19M checkpoint 迁移5M快速验证 gait 链路

默认 train.yaml 的主要参数是:

run:
seed: 0
total_timesteps: 20000000
n_envs: 8
device: cpu
tensorboard: false
output_dir: outputs

environment:
max_steps: 1000

ppo:
n_steps: 2048
batch_size: 256
n_epochs: 4
learning_rate: 0.0003
gamma: 0.97
gae_lambda: 0.95
clip_range: 0.2
ent_coef: 0.01
net_arch: [256, 256]

checkpoint:
every_steps: 1000000
name_prefix: pupper_ppo
final_name: pupper_ppo

resume_from 用于同一观测结构的断点续训;transfer_from 专门用于把 45 维策略迁移到 50 维 gait 策略。迁移时保留输入层前 45 列权重,将新增 5 列权重初始化为零。两者不能同时设置。

6.6 安装与训练

下面的命令都从 codes/practices/quadruped/cs123 目录执行。

6.6.1 安装依赖

cd codes/practices/quadruped/cs123
uv sync

父目录的 pyproject.toml 已声明 Gymnasium、MuJoCo、Stable-Baselines3、PyYAML、ImageIO、Matplotlib、Pytest 和 TensorBoard 等依赖。

6.6.2 短训练

第一次先用较小预算确认整个流程能运行:

uv run python 6.rl_pupper/train.py \
--timesteps 100000 \
--n-envs 4

6.6.3 默认训练

uv run python 6.rl_pupper/train.py

默认读取 6.rl_pupper/configs/train.yaml,最终模型保存到:

6.rl_pupper/outputs/pupper_ppo.zip

每 1M 环境步还会保存一个中间 checkpoint。

命令行参数可以覆盖 YAML 中 run 下的字段:

uv run python 6.rl_pupper/train.py \
--timesteps 20000000 \
--n-envs 16 \
--tensorboard \
--device cpu

MuJoCo 仿真仍在 CPU 上运行。Apple Silicon 可以尝试 --device mps 加速 PPO 网络更新,但不会把 MuJoCo 环境搬到 GPU;没有 CUDA 时,SB3 的 auto 默认回退 CPU,不会自动选择 MPS。

6.6.4 训练设备参考

这个实验包含两类计算:并行 MuJoCo 环境由 CPU 执行,device 只决定 PPO 神经网络使用 CPU、CUDA 还是 MPS。由于当前策略网络只有两层 [256, 256],增加 GPU 并不一定比增加 CPU 并行环境更有效。

下面的配置适合作为起点,实际速度仍取决于 CPU 单核性能、可用核心数、系统负载和 PyTorch 后端:

设备场景建议 device建议 n_envs适合用途
普通笔记本,4–8 个逻辑核心cpu2–4100k–1M 短训练和流程验证
桌面级多核 CPU,8–16 个逻辑核心cpu4–8基础实验和较长训练
高核心数工作站cpu8–1620M 正式训练与多组对照
Apple Silicon先用 cpu,再实测 mps4–16MPS 只加速策略更新,MuJoCo 仍受 CPU 限制
CUDA 工作站cuda按 CPU 核心数设置GPU 加速策略更新,环境吞吐仍主要取决于 CPU

仓库报告所用机器是 Apple M3 Ultra:28 个逻辑 CPU 核心、96 GiB 统一内存、60 核 GPU。实际训练采用 device=cpu 和 16 个 SubprocVecEnv,完成约 20M 步用时 49 分钟。这个结果可作为高端多核设备的参考,不代表其他机器的固定耗时。

选择 n_envs 时应从较小值逐步增加;当继续增加环境数却不再提高每秒环境步数时,说明 CPU 调度或内存带宽已经成为瓶颈。CUDA/MPS 是否更快也应以相同配置下的实际吞吐为准。

6.6.5 TensorBoard

启用日志后,在另一个终端运行:

uv run tensorboard --logdir 6.rl_pupper/outputs/tb

重点观察:

  • rollout/ep_rew_mean:总回报是否持续改善;
  • rollout/ep_len_mean:策略能否逐渐完成更长回合;
  • train/explained_variance:value function 是否学到有效估计;
  • train/std:探索标准差是否持续增大。

6.6.6 断点续训

uv run python 6.rl_pupper/train.py \
--timesteps 20000000 \
--checkpoint 6.rl_pupper/outputs/pupper_ppo.zip

6.6.7 Gait 迁移与公平对照

# 从 19M 基础策略迁移并微调 5M
uv run python 6.rl_pupper/train.py \
--config 6.rl_pupper/configs/finetune_gait.yaml

# 两组从零训练的公平对照
uv run python 6.rl_pupper/train.py \
--config 6.rl_pupper/configs/train_no_gait.yaml
uv run python 6.rl_pupper/train.py \
--config 6.rl_pupper/configs/train_gait.yaml

6.7 评估与输出

基础 45 维策略训练完成后运行:

uv run python 6.rl_pupper/evaluate.py

默认加载 6.rl_pupper/outputs/pupper_ppo.zip,生成:

6.rl_pupper/outputs/demo.gif
6.rl_pupper/outputs/velocity_tracking.webp

也可以显式指定 checkpoint 和输出目录:

uv run python 6.rl_pupper/evaluate.py \
--checkpoint 6.rl_pupper/reports/raw/checkpoints/pupper_ppo_19000000_steps.zip \
--out 6.rl_pupper/outputs/eval_19m

demo.gif 依次测试:

  1. 站立 2 秒;
  2. vx=0.5 m/s 前进 3 秒;
  3. wz=0.5 rad/s 左转 2.5 秒;
  4. vx=-0.3 m/s 后退 2.5 秒;
  5. 再次站立 2 秒。

速度图分别在 vx=0.2/0.4/0.6 m/s 下运行 6 秒,绘制目标速度与实际速度。

Linux 无头环境渲染失败时,可以设置 EGL 后端:

MUJOCO_GL=egl uv run python 6.rl_pupper/evaluate.py

当前 evaluate.py 构造的是基础 45 维环境,不能直接加载 50 维 gait checkpoint。gait 接触评估及其图表保存在 reports/gait_finetune/ 中。

6.8 当前真实训练结果

仓库已经保存了一次 Apple M3 Ultra CPU 训练报告。以下数据是特定机器和随机种子下的实验记录,不应当当作所有机器都能复现的固定速度。

6.8.1 20M 基础训练

  • 16 个 CPU SubprocVecEnv
  • 实际完成 20,021,248 步
  • 用时约 49 分钟
  • ep_rew_mean:2.61 → 24.58,峰值 25.49
  • ep_len_mean:258 → 830,峰值 884,尚未稳定达到 1000
  • 最佳 checkpoint:19M
  • 三档前向速度平均 MAE:0.085 m/s
  • 固定速度评估没有摔倒,但完整命令切换演示在 t=4.16 s 发生过一次终止
20M 步基础训练曲线:回报和回合长度总体上升,但回合长度尚未稳定达到 1000;策略标准差持续增大。
20M 步基础训练曲线:回报和回合长度总体上升,但回合长度尚未稳定达到 1000;策略标准差持续增大。

下图展示最佳 19M checkpoint 对 0.2/0.4/0.6 m/s 三档前向速度命令的响应。中高速命令已经能被跟踪,低速段的波动和超调仍然明显。

最佳 19M checkpoint 的速度跟踪结果:红色虚线为目标速度,蓝线为机身坐标系下的实际前向速度。
最佳 19M checkpoint 的速度跟踪结果:红色虚线为目标速度,蓝线为机身坐标系下的实际前向速度。

最终策略的命令切换效果如下。GIF 依次包含站立、前进、左转、后退和再次站立;它可以完成主要动作,但切换过程尚未完全稳定。

19M checkpoint 的命令切换演示。
19M checkpoint 的命令切换演示。

完整记录、训练曲线、checkpoint 对比和原始 CSV 见 6.rl_pupper/reports/README.md

这次实验说明策略已经学会稳定前进、后退、转向和粗粒度变速,但低速命令仍然明显超调,命令切换也没有完全稳定。报告没有对 vy 横移能力做系统评估,因此不能仅凭这组结果宣称已经完成全方向速度跟踪。

6.8.2 5M Gait 迁移微调

迁移实验从 19M 的 45 维策略出发,扩展到 50 维后训练 5M 步。最佳 4.5M checkpoint 的结果是:

  • 三种 gait 平均速度 MAE:0.076 m/s;
  • 四足接触匹配率:0.485;
  • 评估重置次数:0。

速度与稳定性得到保留,但 0.485 的接触匹配接近未按相位区分触地的基准水平。当前实验没有成功形成标准 walk、trot 和 pace,只能证明 gait 输入、相位、奖励、迁移和评估链路已经跑通。(6.9.4 的进阶版把接触奖励权重提高到 4.0 并配合摆动整形后,trot 匹配率可达 85.6%。)

目标接触时序与实测接触的差异可以直接从下图看出:左侧是规则的目标周期,右侧实测触地频率更高,也没有稳定复现三种 gait 的相位关系。

最佳 4.5M gait checkpoint 的目标接触时序与实测足端接触。
最佳 4.5M gait checkpoint 的目标接触时序与实测足端接触。
依次输入 walk、trot、pace 的 gait 演示;三段运动的视觉和接触差异仍不充分。
依次输入 walk、trot、pace 的 gait 演示;三段运动的视觉和接触差异仍不充分。

完整结果见 reports/gait_finetune/README.md

6.9 进阶:从"能走"到"走得好"

上面的最小实验证明了策略"能走",但把它的演示和第 5 章手写 trot 放在一起看,差距一目了然:站立时原地踏步、行走时机身弹跳、步频快而碎、几乎看不到抬腿,命令切换时还可能摔倒。针对这些问题的优化实现在 6.rl_pupper_v26.rl_pupper_v2_5(v1 目录保持原样作为基线),本节按机制归纳其设计原理。

6.9.1 闭环观测:补上机身线速度

最小实验的头号局限是观测里没有机身线速度,策略只能从关节状态间接推断自己走多快,速度控制本质上是开环的。进阶版在观测末尾追加机身坐标系线速度 3 维(45 → 48 维),跟踪变成闭环,低速超调随之消失。

追加在末尾而不是插在中间,是为了让旧 checkpoint 可以按"新增输入列权重置零"的方式迁移到新结构(transfer_from 机制),不浪费已训练的参数。注意线速度在仿真中可以直接读取,部署到真机需要状态估计器提供等价量,或改用帧堆叠让策略自行估计。

6.9.2 奖励经济学:为什么策略会"站着不动"

给最小实验直接补上竖直速度、横滚俯仰角速度、关节加速度、足端打滑等平滑惩罚,然后从零训练——得到的往往不是更平滑的步态,而是一只一动不动的机器狗。这是本章最值得理解的一个现象,原因藏在奖励的"经济学"里:

  • 总奖励下限被裁剪为 0(见 6.4.3),尝试迈步时惩罚超过收益会被截断成 0;
  • 而站着不动时,指数型跟踪奖励仍会发放"残余分红"——以 为例,命令 0.5 m/s 下原地不动仍能拿到 的线速度跟踪分;
  • 于是"站桩保本"的期望收益高于"学步期交税摔跤",策略理性地选择冻结。

进阶版用三件事拆掉这个局部最优:

  1. dont_wait 惩罚:命令非零、四足全触地且机身不动时按步罚分,把"站桩抗命"的净收益打到 0 以下;
  2. 收紧跟踪 sigma(0.25 → 0.15):原地不动的残余分红从 37% 降到 19%,而误差 0.1 以内的良好跟踪几乎不受影响;
  3. 两阶段课程:先用削弱的平滑惩罚 bootstrap 出行走(bootstrap_walk.yaml),再恢复全额惩罚打磨步态(smooth_finetune.yaml)。惩罚只负责修剪已经存在的行为,而不是阻止行为出现。

配套的还有两类扰动:开局随机初速度(策略从第一步就必须迈步,消除"发现行走"的探索鸿沟)和训练时的随机 kick(站桩扛不住踢,必须学会迈步恢复),以及熵系数在阶段内线性衰减(前期高熵保探索、末期低熵保平滑)。

奖励表在 7 项基础上扩展到 17 项,新增项各司其职:

新增项治什么
lin_vel_z / ang_vel_xy机身弹跳、横滚俯仰摇摆
joint_acceleration / foot_slip关节抖动、足端拖地打滑
stand_still / stand_still_joint_velocity零命令时原地踏步
abduction_angle四腿外八劈叉
feet_stance_time三腿跛行:feet_air_time 只奖励抬起来的腿,钉死不抬的腿零成本,策略会把一条腿当锚拖行;对连续触地超时的脚按步罚分补上这个漏洞
foot_clearance看不到抬腿:贴地掠过是平地上的能量最优解,摆动相足端高度在奖励里一分钱不值;给它一个目标高度(3.5 cm,对齐第 5 章的 step_height),真实地形部署也依赖这一项
dont_wait站桩抗命(见上)

6.9.3 动作低通滤波:手写步态平滑的等价先验

手写步态"天然平滑"的本质是动作频带受限——摆线轨迹是低频周期函数。而神经网络每 20 ms 输出一次新目标,高频分量靠惩罚项压不干净。进阶版把同样的频带先验直接加给 RL:策略输出先过一阶 EMA 滤波(α=0.5,50 Hz 下截止约 5.5 Hz)再进 PD 伺服,保留步态基频与摆动相、滤掉高频抖动。

关键是训练和评估都带滤波,策略学会穿过滤波器工作,而不是事后补救。这也是真机部署的标准做法。

6.9.4 步态节律与摆动整形

消除抖动之后,与手写步态的观感差距主要剩三点,各有对应机制:

  • 节律不规整:涌现步态没有固定周期和相位。用 6.4.4 的 gait 条件化给策略加"节拍器"——只训练 trot 一种步态、接触奖励权重提到 4.0(最小实验的 0.5 验证了不足以改变时序)。
  • 步频太快:腾空时间目标 0.1 s 等于给 5 Hz 小碎步发许可证,提高到 0.25 s 后短促点地会在触地时被扣分,步频落向 trot 时钟的 2 Hz。
  • 速度与步幅的物理边界:步幅 × 步频 = 速度,Pupper 步幅上限约 10 cm,0.5 m/s 命令下步频物理上必须 4 Hz 以上——第 5 章手写演示看起来从容,一半原因是它只以 0.04–0.1 m/s 踱步。因此评估新增了与手写演示同速档的 0.15 m/s 巡航演示,同速对比才公平。

最后一步是稳定性打磨:摆动变大变慢后平均支撑腿减少、机身晃动回升,行为定型后把 lin_vel_zang_vel_xy 惩罚恢复满额、提高速度跟踪权重、低熵收尾即可(纯配置微调,见 stability_polish.yaml)。

6.9.5 最终效果

同一命令序列演示下,最终版(6.rl_pupper_v2_5)与最小实验基线对比:

指标基线(v1)最终版
前进速度波动(cmd 0.5 m/s)±0.190±0.067
机身高度波动±0.0217 m±0.0050 m
站立偏航晃动±0.543 rad/s±0.349 rad/s
trot 接触匹配率—(无节律约束)85.6%
巡航步频(0.15 m/s)2.6 Hz(trot 时钟 2 Hz)
四脚触地占空比一腿 92% 钉地四腿均衡(54–66%)
演示全程摔倒1 次0 次
最终版 0.15 m/s 巡航演示:与第 5 章手写 trot 同速档,闭环平衡下接近其节律与观感。
最终版 0.15 m/s 巡航演示:与第 5 章手写 trot 同速档,闭环平衡下接近其节律与观感。
最终版标准命令序列演示:站立、前进 0.5 m/s、左转、后退、站立,全程零摔倒。
最终版标准命令序列演示:站立、前进 0.5 m/s、左转、后退、站立,全程零摔倒。

各机制的消融实验如下,每个目录只引入一组改动:

目录引入的机制
6.rl_pupper_v2线速度观测、17 项奖励、命令重采样与扰动、两阶段课程
6.rl_pupper_v2_1钉腿超时惩罚(治三腿跛行)
6.rl_pupper_v2_2动作低通滤波(治高频抖动)
6.rl_pupper_v2_3trot 步态条件化(治节律不齐)
6.rl_pupper_v2_4摆动整形与巡航演示(治小碎步、不抬腿)
6.rl_pupper_v2_5稳定性打磨(纯配置微调)

每个目录的 reports/README.md 都包含该级的训练配置、checkpoint 对比和与上一级的逐项对照表。

6.10 局限与下一步

进阶版解决了最小实验的多数局限,剩余的开放问题:

  1. 固定 2 Hz 的 trot 时钟对高速命令物理上不可满足,高速跟踪偏保守;改进方向是让步态周期随命令速度缩放,或改用残差 RL——在第 5 章开环轨迹上学习残差,节律由手写轨迹保证、平衡由 RL 负责;
  2. 机身线速度观测依赖仿真直读,真机部署需要状态估计器或帧堆叠方案;
  3. 域随机化仅有初速度与 kick 扰动,观测噪声、动作延迟、摩擦/质量随机化尚未加入,也没有真机部署;
  4. 足端接触仍使用 site 高度近似。

参考资料