四足机器人强化学习系列 08:从公式走进 rsl_rl 与 legged_gym 的 PPO 训练闭环

写在前面:我缺的不是下一条公式,而是公式在代码里的位置

前面的笔记已经分别整理过 MDP、Observation、Action 和 GAE。但单独看这些概念时,我仍然经常有一种割裂感:

  • 我知道 γ\gamma 是折扣因子,却不知道它在哪一段循环里真正参与计算。
  • 我见过 A^t\hat A_t,却不知道 rollout buffer 为什么必须先存 VtV_t、done 和旧策略的 log_prob。
  • 我知道 PPO 有 clip,却容易把它与 action clip、torque clip 混在一起。
  • 我看到 num_steps_per_env = 24、num_mini_batches = 4,却没有立刻想到它们怎样改变一轮更新的数据形状。

这篇不再按“概念百科”的方式写,而是沿着一条真实的数据流往下走:

legged_gym 产生交互数据,rsl_rl 保存这批数据、计算 GAE,再用 PPO 更新 actor 和 critic。公式不是悬在代码上方的装饰,而是每个张量为什么要存在的理由。

文中五个实验均在浏览器本地运行,不加载第三方绘图库。可以调参数、暂停动画;系统开启“减少动态效果”时会自动显示静态状态。

0. 先给出整篇的一句话结论

一次 PPO iteration 可以压缩成下面六步:

N 个并行机器人各走 T 步
↓
保存 obs、action、reward、done、V、旧 log_prob
↓
从最后一步向前计算 GAE 和 return target
↓
把 [T, N, ...] 展平成 B = T × N 个 transition
↓
分成 mini-batch,重复 K 个 epoch 更新 actor / critic
↓
清空旧 rollout,用更新后的策略重新采数据
动态图 1 · TRAINING LOOP

一轮训练:环境步与优化步不是同一个循环

PPO 从环境采样、计算 GAE 到 mini-batch 更新的数据流。

在旧版配套代码里,这条主循环就在 OnPolicyRunner.learn():先循环 num_steps_per_env 次调用 alg.act() 和 env.step(),再调用 compute_returns() 与 update()。所以读代码时,最好的入口不是 PPO loss,而是 runner。

1. 先固定版本:不要把两代 rsl_rl API 混在一起

legged_gym 是基于 Isaac Gym 的经典项目。它的官方 README 明确要求安装 rsl_rl v1.0.2。而今天的 rsl_rl 已经发展到 5.x,模型、storage 和 observation 接口都经过重构。

本文采用两条清楚分开的版本线:

用途 固定版本 为什么这样选
正文逐行对照 legged_gym@8fa29ac + rsl_rl v1.0.2@2ad79cf 这是 legged_gym 官方指定的配套 API,调用链能够直接闭合
现代实现对照 rsl_rl v5.5.0@89869d8 2026-08-27 的官方最新稳定版,用于说明概念未变、代码结构怎样变化
最容易踩的版本坑 旧代码里的 `ActorCritic`、`privileged_observations` 与 `RolloutStorage.Transition`,不能直接拿去解释 5.x 的独立 actor/critic、`TensorDict` 与 `obs_groups`。本文每个代码链接都固定到 commit,不引用会继续变化的 `main`。

这篇的目标不是证明某个版本更好,而是先用最容易闭环的经典代码理解 PPO;理解之后,再看 5.x 的抽象就不会只剩下类名。

2. 两个仓库各自负责什么

先把边界划清:

仓库 主要责任 数学上的位置
legged_gym 并行环境、物理步进、observation、action 到 torque、reward、done/reset、随机化、课程学习 定义 oto_t、ata_t 如何作用于环境、rtr_t、dtd_t 与转移 PP
rsl_rl actor/critic、动作分布、rollout storage、GAE、PPO loss、mini-batch、优化器、日志 学习 πθ\pi_\theta 与 VϕV_\phi

它们之间真正的接口很窄:

(ot+1,ot+1critic,rt,dt,infot)=env.step(at)(o_{t+1},o_{t+1}^{\mathrm{critic}},r_t,d_t,\mathrm{info}_t) =\mathrm{env.step}(a_t)

对于 NN 个并行环境,典型张量形状是:

ot∈RN×Do,at∈RN×Da,rt,dt∈RNo_t\in\mathbb R^{N\times D_o},\quad a_t\in\mathbb R^{N\times D_a},\quad r_t,d_t\in\mathbb R^N

legged_gym 不负责计算 PPO ratio;rsl_rl 也不知道某个 reward 是速度跟踪还是抬脚奖励。两边只通过这些张量合作。

2.1 legged_gym 把 MDP 落到代码

在 legged_robot.py 中,可以按下面顺序找:

  1. step(actions):接收 policy 动作,执行多个 physics substep。
  2. _compute_torques(actions):把动作解释为位置、速度或力矩控制目标。
  3. post_physics_step():刷新状态、检查终止、计算 reward 与下一帧 observation。
  4. compute_reward():把所有启用的 reward term 按 scale 求和。
  5. compute_observations():拼 actor 能看到的输入并按配置加入噪声。
  6. reset_idx():只重置已经终止的环境,并可更新 terrain/command curriculum。

所以一行 env.step(actions) 背后,实际包含控制、动力学、接触、奖励、终止和重置。

2.2 rsl_rl 把一次 iteration 拆成采样与学习

从真正的命令入口开始,调用链是:train.py 调用 task_registry.make_env() 与 make_alg_runner(),registry 再把 LeggedRobotCfgPPO 转成字典并构造 OnPolicyRunner。因此两个仓库是在 runner 创建处接上的,不是靠隐式的全局状态互相调用。

经典 runner 的关键顺序可写成不依赖具体 API 的伪代码:

for update in range(max_iterations):
with no_grad():
for step in range(T):
action = ppo.act(actor_obs, critic_obs)
next_obs, reward, done, info = env.step(action)
ppo.record_step(reward, done, info)
ppo.compute_returns(last_critic_obs)
ppo.update()

对应源码:OnPolicyRunner.learn()。

这里有两个重要边界:

  • rollout 时使用 torch.inference_mode(),因为采样阶段不需要为每一步保留反向传播图;
  • update 使用刚收集的一批旧策略数据,更新结束后清空 storage,再用新策略重新采样。

3. Actor–Critic:一个决定动作,一个估计当前位置

3.1 Actor 输出的是动作分布,不只是一个动作值

经典 ActorCritic 的 actor MLP 输出均值 μθ(ot)\mu_\theta(o_t),再配合一个可学习标准差向量 σ\sigma 形成对角 Gaussian:

πθ(at∣ot)=N ⁣(μθ(ot),diag⁡(σ2))\pi_\theta(a_t\mid o_t) =\mathcal N\!\left(\mu_\theta(o_t),\operatorname{diag}(\sigma^2)\right)

训练时采样:

at∼πθ(⋅∣ot)a_t\sim\pi_\theta(\cdot\mid o_t)

部署时通常直接使用均值:

at=μθ(ot)a_t=\mu_\theta(o_t)

对应源码:ActorCritic.update_distribution()、act() 与 act_inference()。

动态图 2 · POLICY TO TORQUE

Gaussian 探索如何经过 action clip、scale 与 PD 变成 torque

Gaussian 动作分布、采样动作、关节目标与 PD 力矩的动态关系。

这张图把四个经常混淆的量拆开了:

  • μ\mu 与 σ\sigma 属于 policy 分布;
  • sample 出来的 utu_t 是 policy 的原始随机变量,rollout 保存的 log_prob 也是在这个动作上计算;
  • 环境先把 utu_t 截到允许范围得到 ata_t;
  • action_scale、默认关节角、PD gain 与 torque limit 属于环境控制接口。

at=clip⁡(ut,−amax⁡,amax⁡)a_t=\operatorname{clip}(u_t,-a_{\max},a_{\max})

legged_gym 的位置控制可以写成:

qttarget=qdefault+saatq_t^{\mathrm{target}}=q^{\mathrm{default}}+s_a a_t

τt=Kp(qttarget−qt)−Kdq˙t\tau_t=K_p(q_t^{\mathrm{target}}-q_t)-K_d\dot q_t

最后还有:

τt←clip⁡(τt,−τmax⁡,τmax⁡)\tau_t\leftarrow \operatorname{clip}(\tau_t,-\tau_{\max},\tau_{\max})

对应 _compute_torques()。这也是为什么“代码考虑得更多”:策略公式只写 at∼πa_t\sim\pi,而工程实现还必须处理单位、缩放、控制模式、PD、饱和与 decimation。

3.2 Critic 的服务对象是 advantage,不是电机

Critic 输出一个标量:

Vϕ(otcritic)≈E ⁣[∑k=0∞γkrt+k]V_\phi(o_t^{\mathrm{critic}}) \approx \mathbb E\!\left[\sum_{k=0}^{\infty}\gamma^k r_{t+k}\right]

它回答的是:“从现在开始,按当前策略走下去,大概还能拿多少累计回报?”

Critic 不直接输出 action,也不进入 PD 控制器。它的主要用途是构造 TD error、advantage 和 value target,帮助 actor 判断刚才采到的动作相对预期是好还是坏。

如果环境提供 privileged observation,旧版 runner 会让 actor 使用普通 observation,critic 使用 privileged observation;没有 privileged observation 时二者输入相同。对应 OnPolicyRunner.__init__() 与 learn()。

4. Rollout buffer:为什么必须保存这些张量

令:

  • NN:并行环境数;
  • TT:每个环境每轮采样步数,即 num_steps_per_env;
  • DoD_o:actor observation 维度;
  • DcD_c:critic observation 维度;
  • DaD_a:动作维度;
  • B=NTB=NT:一轮 rollout 的 transition 数量。

经典 RolloutStorage 会分配:

数据 典型形状 后面服务于什么
observations [T,N,Do][T,N,D_o] 重算新策略的动作分布
privileged_observations [T,N,Dc][T,N,D_c] 重算 critic value
actions [T,N,Da][T,N,D_a] 在新旧策略下评估同一批动作
rewards、dones [T,N,1][T,N,1] TD、GAE、终止 mask
values [T,N,1][T,N,1] baseline 与 value clipping 的旧值
returns、advantages [T,N,1][T,N,1] critic target 与 policy update 权重
actions_log_prob [T,N,1][T,N,1] 计算 PPO ratio 的旧 log-prob
mu、sigma [T,N,Da][T,N,D_a] 估计新旧 Gaussian 的 KL

基础配置给出 N=4096N=4096、Do=235D_o=235、Da=12D_a=12、T=24T=24。因此一个典型 rollout 中:

observations       [24, 4096, 235]
actions / μ / σ [24, 4096, 12]
scalar fields [24, 4096, 1]
B = T × N 98,304 transitions

这是基础类的尺寸示例,具体机器人配置可以覆盖 observation 与 action 维数;真正应该记住的是形状之间的关系。

最关键的问题是:为什么需要旧 log_prob?

因为 PPO 更新时参数已经变成 θ\theta,但采样动作来自更新前的 θold\theta_{\mathrm{old}}。必须同时知道同一动作在新旧策略下的概率密度,才能判断更新幅度:

rt(θ)=πθ(at∣ot)πθold(at∣ot)=exp⁡ ⁣(log⁡πθ(at∣ot)−log⁡πθold(at∣ot))r_t(\theta) =\frac{\pi_\theta(a_t\mid o_t)} {\pi_{\theta_{\mathrm{old}}}(a_t\mid o_t)} =\exp\!\left( \log\pi_\theta(a_t\mid o_t) -\log\pi_{\theta_{\mathrm{old}}}(a_t\mid o_t) \right)

连续动作的 π(a∣o)\pi(a\mid o) 是概率密度,不是“选中动作的百分比”。代码用 log-prob 相减再 exp,数值上也比先计算很多很小的密度再相除更稳定。

对于 DaD_a 维独立 Gaussian,代码先把各动作维的 log-prob 相加,得到联合动作的 log-prob:

log⁡π(at∣ot)=∑j=1Dalog⁡N(at,j;μj,σj2)\log\pi(a_t\mid o_t) =\sum_{j=1}^{D_a}\log\mathcal N(a_{t,j};\mu_j,\sigma_j^2)

所以 PPO ratio 比较的是整组关节动作,不是任选一个关节单独比较。

4.1 真正终止与 timeout 不完全相同

最严谨的想法是给它们两个不同的 mask:

δt=rt+γbtV(st+1)−V(st),A^t=δt+γλctA^t+1\delta_t=r_t+\gamma b_tV(s_{t+1})-V(s_t),\qquad \hat A_t=\delta_t+\gamma\lambda c_t\hat A_{t+1}

其中 btb_t 控制“能否从边界状态 bootstrap”,ctc_t 控制“GAE 能否继续穿过边界传播”:

边界 btb_t ctc_t 含义
普通 transition 1 1 bootstrap,并向后继续递推
真正 terminal(摔倒等) 0 0 状态价值为零,递推也停止
timeout / 时间上限截断 1 0 用截断前状态 bootstrap,但不能串到 reset 后的新 episode

从语义上说,timeout 应使用 reset 前截断状态的 value,而不是 reset 后新 episode 的 observation。v1.0.2 的具体实现更早:PPO.process_env_step() 直接把 rollout 时已保存的 transition.values 乘 γ\gamma 补进 reward,随后 storage 仍通过 done 停止 GAE;它在代码结构上把 bt=1b_t=1 的效果提前并入了 rtr_t。对应 timeout bootstrap 与 storage 写入。这是经典兼容版的历史写法,不要把它误读成“可以连接 reset 后的新状态”。

5. 从 reward 到 advantage:TD 与 GAE 服务于“动作比预期好多少”

5.1 Reward 是环境给的,advantage 是算法算的

legged_gym.compute_reward() 会把所有非零 reward scale 对应的函数结果加起来:

rt=∑iwirt(i)r_t=\sum_i w_i r_t^{(i)}

例如速度跟踪、姿态、力矩、动作变化、碰撞等。PPO 并不知道每一项的物理含义,它只接收最终 rtr_t。

先忽略边界、令 bt=ct=1b_t=c_t=1,单步 TD error 是:

δt=rt+γVt+1−Vt\delta_t =r_t+\gamma V_{t+1}-V_t

它在问:

实际拿到的 reward 加上下一状态的预计价值,是否高于 critic 对当前状态原来的预计?

5.2 GAE 把未来几步的 TD error 加权回来

有限 rollout 中,GAE 用反向递推实现:

A^t=δt+γλA^t+1\hat A_t =\delta_t+\gamma\lambda\hat A_{t+1}

展开后可以看到未来第 ll 步 TD error 的权重:

A^t=δt+(γλ)δt+1+(γλ)2δt+2+⋯\hat A_t =\delta_t +(\gamma\lambda)\delta_{t+1} +(\gamma\lambda)^2\delta_{t+2}+\cdots

Critic 的回归目标则是:

R^t=A^t+Vt\hat R_t=\hat A_t+V_t

这里的 A^t\hat A_t 是尚未标准化的原始 advantage:实现先用它构造 return target,再单独标准化 policy update 所用的 advantage。不能反过来拿均值为 0 的标准化 advantage 构造 critic target。

对应 RolloutStorage.compute_returns()。

动态图 3 · GAE BACKWARD PASS

γ 与 λ 怎样决定一次 TD error 能影响多远

GAE 从 rollout 末端反向递推,并在 done 位置停止传播。

这个实验帮助区分两个参数:

  • γ\gamma 决定环境回报本身如何折扣,也进入 TD bootstrap;
  • λ\lambda 只控制多个 TD error 混合的时间跨度,是 GAE 的 bias–variance 旋钮。

λ=0\lambda=0 时几乎只看一步 TD,偏差较大但方差较小;λ→1\lambda\to1 时看得更远,通常偏差更小但方差更大。两种边界都会阻止 GAE 穿进 reset 后的新 episode;差别在于 timeout 仍可对边界状态做一次 bootstrap,真正 terminal 不可以。

5.3 为什么要归一化 advantage

旧版实现会对整批 rollout 做:

A^t←A^t−mean⁡(A^)std⁡(A^)+10−8\hat A_t\leftarrow \frac{\hat A_t-\operatorname{mean}(\hat A)} {\operatorname{std}(\hat A)+10^{-8}}

它不改变“相对好坏”的排序,但让一次 update 的梯度尺度更可控。需要注意:advantage normalization、observation normalization 和 reward normalization 是三件不同的事,不能只看到 normalization 就当作同一层处理。

6. PPO ratio:把“这个动作好不好”变成“该增大还是减小概率”

Advantage 的符号给出方向:

  • A^t>0\hat A_t>0:这个动作比 critic 的基线预期更好,希望提高它的概率密度;
  • A^t<0\hat A_t<0:这个动作比预期差,希望降低它的概率密度。

Ratio 表示新策略相对旧策略改变了多少:

  • rt>1r_t>1:新策略更偏爱这个已采样动作;
  • rt<1r_t<1:新策略降低了这个动作的概率密度;
  • rt=1r_t=1:对这个样本而言,新旧策略没有变化。

如果只最大化 rtA^tr_t\hat A_t,网络可能为了少数样本一步走得太远。PPO 的 clipped surrogate objective 是:

LCLIP(θ)=Et ⁣[min⁡ ⁣(rt(θ)A^t,clip⁡(rt(θ),1−ϵ,1+ϵ)A^t)]L^{\mathrm{CLIP}}(\theta) =\mathbb E_t\!\left[ \min\!\left( r_t(\theta)\hat A_t, \operatorname{clip}(r_t(\theta),1-\epsilon,1+\epsilon)\hat A_t \right) \right]

代码做梯度下降,因此等价地计算负号后的最大值。对应 PPO.update() 中 ratio 与 surrogate loss。

动态图 4 · CLIPPED OBJECTIVE

Clip 裁的是概率比带来的收益,不是把 action 截成 ±ε

正负 advantage 下 PPO clipped objective 随 probability ratio 变化的曲线。

正负 advantage 的平台方向并不相同:

Advantage 希望 ratio 往哪里走 哪一侧不再继续奖励
A^>0\hat A>0 往 r>1r>1 走,提高好动作概率 r>1+ϵr>1+\epsilon 的右侧
A^<0\hat A<0 往 r<1r<1 走,降低坏动作概率 r<1−ϵr<1-\epsilon 的左侧

所以只画一条“对称截断曲线”会掩盖 min 与 advantage 符号共同产生的非对称效果。

6.1 三种 clip 必须分开

名称 发生位置 目的
PPO ratio clip policy loss 限制同一批旧数据驱动策略继续获益的范围
action clip policy 输出进入环境之前 防止归一化动作超过约定范围
torque clip PD/torque 计算之后 遵守执行器物理上限

PPO clip 并没有直接改写发送给机器人的 action。它只改变训练时的目标函数。

7. Critic、Entropy、KL:PPO 不只包含一条 policy loss

7.1 Value loss 让 critic 学会预测 return target

最基本的 value loss 是:

LV(ϕ)=Et[(Vϕ(otc)−R^t)2]L_V(\phi)=\mathbb E_t\left[(V_\phi(o_t^c)-\hat R_t)^2\right]

启用 value clipping 时,先限制新 value 相对 rollout 时旧 value 的变化:

Vtclip=Vtold+clip⁡(Vt−Vtold,−ϵ,ϵ)V_t^{\mathrm{clip}} =V_t^{\mathrm{old}} +\operatorname{clip} \left(V_t-V_t^{\mathrm{old}},-\epsilon,\epsilon\right)

再取未裁剪与裁剪误差中的较大者:

LV=Et[max⁡((Vt−R^t)2,(Vtclip−R^t)2)]L_V =\mathbb E_t\left[ \max\left( (V_t-\hat R_t)^2, (V_t^{\mathrm{clip}}-\hat R_t)^2 \right) \right]

对应 value clipping 实现。这里取较大的误差,是为了避免 critic 通过一次过大的更新获得过于乐观的“改善”。

7.2 Entropy 让动作分布不要过早塌缩

对角 Gaussian 的 entropy 会对动作维求和:

H(π)=∑j=1DaH(N(μj,σj2))\mathcal H(\pi) =\sum_{j=1}^{D_a} \mathcal H\left(\mathcal N(\mu_j,\sigma_j^2)\right)

σ\sigma 较大时探索更广,entropy 较高;σ\sigma 过早变得很小时,策略容易只重复一小片动作。经典实现中的总损失是:

L=Lsurr+cVLV−cHE[H(π)]L =L_{\mathrm{surr}} +c_VL_V -c_H\mathbb E[\mathcal H(\pi)]

对应 loss 组合与梯度裁剪。

7.3 Adaptive KL 是油门,不是另一种 clip

对于对角 Gaussian,代码估计旧策略到新策略的 KL divergence。当平均 KL 太大时把 learning rate 除以 1.51.5,太小时乘以 1.51.5,并限制在 [10−5,10−2][10^{-5},10^{-2}]。

η←{max⁡(10−5,η/1.5),DˉKL>2D∗min⁡(10−2,1.5η),0<DˉKL<D∗/2η,otherwise\eta\leftarrow \begin{cases} \max(10^{-5},\eta/1.5), & \bar D_{\mathrm{KL}}>2D^* \\ \min(10^{-2},1.5\eta), & 0<\bar D_{\mathrm{KL}}<D^*/2 \\ \eta, & \text{otherwise} \end{cases}

对应 adaptive KL schedule。

可以这样记:

  • ratio clip 改的是 objective 的形状;
  • adaptive KL 改的是 optimizer 下一步走多快;
  • gradient clipping 限制的是一次反向传播得到的总梯度范数。

8. Mini-batch 与多个 epoch:一批 on-policy 数据为什么还能重复用

rollout 收集完以后,feedforward storage 把前两维展平:

[T,N,D]⟶[B,D],B=TN[T,N,D]\longrightarrow[B,D],\qquad B=TN

再随机打乱并分为 MM 个 mini-batch:

b=⌊BM⌋b=\left\lfloor\frac{B}{M}\right\rfloor

每个 epoch 遍历这些 mini-batch,总 optimizer step 数为:

K×MK\times M

其中 KK 是 num_learning_epochs。对应 mini_batch_generator()。

这里再抠一个版本细节:v1.0.2 在进入 epoch 循环前只调用一次 randperm,因此同一轮 update 的多个 epoch 复用同一组随机分块;有些后续实现会每个 epoch 重新打乱。理解 PPO 不依赖哪一种选择,但复现代码时不能凭印象把它们写成同一个实现。

动态图 5 · ROLLOUT TO MINI-BATCH

N × T 的 rollout 如何被打乱、切块并重复利用

Rollout 网格展平、随机打乱并进入多轮 mini-batch 更新的动画。

用 legged_gym 基础配置举例:

num_envs = 4096
num_steps_per_env = 24
num_mini_batches = 4
num_learning_epochs = 5

则:

B=4096×24=98 304B=4096\times24=98\,304

每个 mini-batch 约有:

b=98 304/4=24 576b=98\,304/4=24\,576

一轮 iteration 做 5×4=205\times4=20 次 optimizer step。每个 transition 在这一轮里被看约 5 次,然后整批丢弃。

这仍然叫 on-policy,是因为数据只被当前这轮的旧策略采样并短暂复用;PPO 用 ratio clip 和 KL 调度限制它在多次复用中偏离旧策略太远。它并不会像 replay buffer 那样长期混用很多历史策略的数据。

上面的展平只对应 feedforward policy。使用 recurrent policy 时,不能把 timestep 当作独立样本任意打散;经典实现会按 episode boundary 分割并补齐轨迹,同时传递 mask 与初始 hidden state,再走单独的 recurrent mini-batch generator。

整除问题 经典 feedforward generator 使用整数除法计算 mini-batch size。配置最好保证 $NT$ 能被 mini-batch 数整除,否则尾部 transition 可能不进入本轮更新。

9. 把 legged_gym 配置项逐个放回公式

官方基础配置集中在 LeggedRobotCfgPPO。下面不只抄默认值,而是说明它在训练链路里服务什么。

配置 公式/代码位置 调大时最直接的变化 先看什么指标
num_steps_per_env rollout 的 TT 每轮数据更多、GAE 序列更长、显存增加 collection time、FPS、显存
num_mini_batches b=B/Mb=B/M 单个 batch 更小、optimizer step 更多 update time、KL 波动
num_learning_epochs 同批数据复用 KK 次 拟合更充分,也更容易偏离旧策略 KL、clip fraction
clip_param ϵ\epsilon surrogate/value 可获益区间变宽 KL、surrogate loss
gamma TD 与 return 的 γ\gamma 更看重远期 reward value loss、episode return
lam GAE 的 λ\lambda advantage 更依赖远期 TD advantage 方差、value loss
learning_rate optimizer 步长 η\eta 参数更新更快 KL、loss 尖峰
desired_kl adaptive schedule 的 D∗D^* 允许的新旧分布距离目标改变 learning rate、KL
entropy_coef cHc_H 更鼓励探索、动作噪声衰减更慢 mean noise std、reward
value_loss_coef cVc_V critic 梯度在总损失中占比更高 value loss、explained variance
max_grad_norm ∣g∣2|g|_2 上限 允许更大的单步梯度 grad norm、NaN
use_clipped_value_loss VclipV^{\mathrm{clip}} 是否限制 critic 相对旧预测跳变 value loss

这些参数不是互相独立的旋钮。例如增大 epoch、learning rate 与 clip range 都可能提高策略偏移;看到 KL 变大时,不能只盯着其中一个。

还要区分“库构造函数默认值”和“项目实际传入值”:PPO.__init__() 自己的默认 gamma=.998、epoch 1、mini-batch 1、固定学习率,并不是 legged_gym 的训练配置。真正跑项目时以上表 LeggedRobotCfgPPO 覆盖后的参数为准。

10. 为什么这套结构特别适合四足机器人

10.1 GPU 并行环境让 on-policy 数据不再那么昂贵

四足接触任务的单条 trajectory 噪声很大,但 Isaac Gym 可以同时运行成千上万个环境。一次短 rollout 就能得到 NTNT 个 transition,使 PPO 在不长期保存旧经验的情况下仍有很大的 batch。

10.2 对角 Gaussian 很适合连续关节动作

actor 直接输出每个动作维的均值,标准差控制探索;这与连续的关节位置偏移、速度目标或 torque 自然对应。动作维的 log-prob 与 entropy 求和后,每个 transition 得到一个联合动作评分。

10.3 Clipped update 能缓和接触动力学与 reward 的高噪声

脚落地、打滑、碰撞和 reset 都会让回报曲面不平滑。PPO 不能消除这些非平稳性,但限制同一批数据驱动策略走得过远,通常比完全不约束的 policy gradient 更容易稳定训练。

不过 PPO 不能修复坏的任务定义:

  • observation 缺失关键状态,PPO 不会凭空补出来;
  • action scale 或 PD gain 不合理,PPO 只会在错误接口里挣扎;
  • reward 有漏洞,PPO 会认真利用漏洞;
  • timeout 与 terminal 处理错,GAE 会得到系统性偏差;
  • sim-to-real 接口不一致,训练 reward 再高也不能保证部署成功。

11. 从训练现象反推是哪一段出了问题

现象 优先检查的 PPO 环节 还要回查的环境环节
KL 突然很高、learning rate 连续下降 epoch、LR、clip、advantage scale reward 是否突然跳变、reset 是否异常
action std 很快接近 0 entropy、初始化 std observation 是否足够、reward 是否只偏爱一个僵硬解
value loss 爆炸 return/GAE、value coefficient、bootstrap reward scale、timeout/done
loss 看起来正常但 reward 不升 actor 得到的 advantage 是否有信息 observation、action、reward 设计
reward 上升但机器人动作很难看 PPO 可能正在正确优化错误目标 reward hacking、动作/力矩惩罚、课程学习
训练好、部署差 不是先调 PPO clip 观测可获得性、延迟、噪声、随机化、控制频率

更完整的曲线排查可以继续看:强化学习训练曲线诊断。

12. rsl_rl v5.5.0 中哪些名字变了,哪些公式没变

现代 rsl_rl 仍然是“rollout → GAE → PPO update”,但抽象层已经不同:

经典 v1.0.2 v5.5.0 概念是否改变
一个 ActorCritic 容纳两张网络 PPO.actor 与 PPO.critic 是独立 model Actor/critic 数学角色不变
observations + privileged_observations 完整 TensorDict + obs_groups 非对称输入思想不变
ActorCriticRecurrent actor/critic 可分别选择 RNNModel recurrent 的能力更可组合
storage 保存固定字段 storage 保存 observation TensorDict 与标准 PPO 字段 rollout 目的不变
PPO 内手写 Gaussian KL distribution 对象提供 KL adaptive KL 逻辑不变

现代版的关键代码入口:

v5.5.0 还特别调整了 observation normalizer 的更新时间:整轮 PPO 优化期间统计量保持固定,优化结束后再用整个 rollout 更新并在多 GPU 间同步。这个细节提醒我:代码里常见的工程设计,往往不是新公式,而是在保护公式成立时所依赖的数据分布与时序。

名字相同也可能不是同一机制:经典 legged_gym 配置中的 normalization 主要是人工设定的 observation 分量缩放、噪声与 clip;v5.5.0 的 EmpiricalNormalization 才维护 running mean/variance。阅读配置时不能把二者直接画等号。

如果直接使用今天的 rsl_rl,应以它的官方配置文档与固定 release 为准,不要照搬本文 v1.0.2 的类名;但 GAE、ratio、clipped objective、value target 与 entropy 的理解可以直接迁移。

13. 最容易混淆的八件事

  1. PPO ratio clip 不是 action clip。 前者裁训练收益,后者裁发给环境的动作。
  2. Critic 不控制机器人。 它训练期提供 baseline 和 value target,部署通常只保留 actor。
  3. reward 不等于 return。 Reward 是一步分数;return/GAE 把未来信息带回来。
  4. done 不一定都该把 bootstrap 置零。 真终止与时间截断要区分。
  5. Policy loss 不要求像监督学习 loss 那样单调下降。 数据分布每轮都在变。
  6. 多 epoch 不等于离线 replay。 同一批数据只短暂复用,更新后就丢弃。
  7. PPO 稳定不等于机器人一定安全。 Action、PD、torque limit 和真机保护属于另一层。
  8. Privileged critic 不代表部署 actor 偷看真值。 特权信息只帮助训练 value;actor 输入必须满足部署可获得性。

14. 下次读代码时,我会按这个顺序找

1. runner.learn()
└─ 一轮收集多少步,何时 compute_returns,何时 update

2. env.step()
└─ action 怎样进控制器,reward/done/next obs 怎样产生

3. PPO.act() + process_env_step()
└─ rollout 前后分别保存什么

4. RolloutStorage
└─ 张量形状、GAE 递推、flatten、mini-batch

5. Actor / Critic / Distribution
└─ μ、σ、sample、log_prob、entropy、V

6. PPO.update()
└─ ratio、surrogate、value、entropy、KL、grad clip

7. config + log
└─ 每个超参数在哪条公式里,异常时看什么指标

如果能顺着这七步把一个 transition 从 oto_t 追到 optimizer step,再从更新后的 policy 追回下一轮 ata_t,PPO 就不再只是几条分散的公式。

15. 总结:每条公式到底服务于哪个场景

公式 在训练里解决什么问题 对应代码位置
a∼N(μ,σ2)a\sim\mathcal N(\mu,\sigma^2) 连续动作与探索 actor distribution
τ=Kp(q∗−q)−Kdq˙\tau=K_p(q^*-q)-K_d\dot q 把 policy action 变成可执行控制 legged_gym._compute_torques()
δ=r+γV′−V\delta=r+\gamma V'-V 判断一步结果是否超出预期 storage GAE 递推
A=δ+γλA′A=\delta+\gamma\lambda A' 在偏差与方差之间传播信用 compute_returns()
r(θ)=exp⁡(log⁡π−log⁡πold)r(\theta)=\exp(\log\pi-\log\pi_{old}) 衡量新旧策略对同一动作的变化 PPO update
LCLIPL^{CLIP} 避免同批旧数据继续推动过大更新 surrogate loss
(V−R^)2(V-\hat R)^2 训练 critic 预测未来回报 value loss
−cHH-c_H\mathcal H 防止探索过早消失 entropy bonus
DKLD_{KL} 用分布距离调节学习率 adaptive schedule
B=NTB=NT 把并行采样变成训练 batch rollout + generator

最后把整篇压缩成一句话:

legged_gym 决定机器人经历什么,rsl_rl 决定怎样从这些经历更新策略;reward 先经 critic 与 GAE 变成 advantage,再经新旧策略 ratio 与 PPO clip 变成一次受约束的梯度更新。

参考资料

知识共享许可协议