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

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

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

  • 我知道 γ\gamma 是折扣因子,却不知道它在哪一段循环里真正参与计算。
  • 我见过 A^t\hat A_t,却不知道 rollout buffer 为什么必须先存 VtV_tdone 和旧策略的 log_prob
  • 我知道 PPO 有 clip,却容易把它与 action clip、torque clip 混在一起。
  • 我看到 num_steps_per_env = 24num_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_tata_t 如何作用于环境、rtr_tdtd_t 与转移 PP
rsl_rl actor/critic、动作分布、rollout storage、GAE、PPO loss、mini-batch、优化器、日志 学习 πθ\pi_\thetaVϕ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 个并行环境,典型张量形状是:

otRN×Do,atRN×Da,rt,dtRNo_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:

πθ(atot)=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(qttargetqt)Kdq˙t\tau_t=K_p(q_t^{\mathrm{target}}-q_t)-K_d\dot q_t

最后还有:

τtclip(τ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] 在新旧策略下评估同一批动作
rewardsdones [T,N,1][T,N,1] TD、GAE、终止 mask
values [T,N,1][T,N,1] baseline 与 value clipping 的旧值
returnsadvantages [T,N,1][T,N,1] critic target 与 policy update 权重
actions_log_prob [T,N,1][T,N,1] 计算 PPO ratio 的旧 log-prob
musigma [T,N,Da][T,N,D_a] 估计新旧 Gaussian 的 KL

基础配置给出 N=4096N=4096Do=235D_o=235Da=12D_a=12T=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(θ)=πθ(atot)πθold(atot)=exp ⁣(logπθ(atot)logπθold(atot))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)

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

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

logπ(atot)=j=1DalogN(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+1Vt\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^tA^tmean(A^)std(A^)+108\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(VtVtold,ϵ,ϵ)V_t^{\mathrm{clip}} =V_t^{\mathrm{old}} +\operatorname{clip} \left(V_t-V_t^{\mathrm{old}},-\epsilon,\epsilon\right)

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

LV=Et[max((VtR^t)2,(VtclipR^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+cVLVcHE[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,并限制在 [105,102][10^{-5},10^{-2}]

η{max(105,η/1.5),DˉKL>2Dmin(102,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=BMb=\left\lfloor\frac{B}{M}\right\rfloor

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

K×MK\times M

其中 KKnum_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=98304B=4096\times24=98\,304

每个 mini-batch 约有:

b=98304/4=24576b=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 的 DD^* 允许的新旧分布距离目标改变 learning rate、KL
entropy_coef cHc_H 更鼓励探索、动作噪声衰减更慢 mean noise std、reward
value_loss_coef cVc_V critic 梯度在总损失中占比更高 value loss、explained variance
max_grad_norm g2|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.actorPPO.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. 总结:每条公式到底服务于哪个场景

公式 在训练里解决什么问题 对应代码位置
aN(μ,σ2)a\sim\mathcal N(\mu,\sigma^2) 连续动作与探索 actor distribution
τ=Kp(qq)Kdq˙\tau=K_p(q^*-q)-K_d\dot q 把 policy action 变成可执行控制 legged_gym._compute_torques()
δ=r+γVV\delta=r+\gamma V'-V 判断一步结果是否超出预期 storage GAE 递推
A=δ+γλAA=\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
(VR^)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 变成一次受约束的梯度更新。

参考资料

3d打印 actor-critic adaptive sampling ai辅助设计 algorithm algorithms anymal apriltag ardupilot atlas attention axis-angle bang-bang belief encoder blender bode cadquery calibration camera calibration chrome cmake cmakelists cnn colcon conan control cpp cpu d435i dagger data_struct db depth camera design-pattern direct collocation dots economics eigen elevation map factory-pattern fcpx fiducial marker figure finance forge fourier fov freecad gae gazebo gdb git gnu gru guitar hardware humanoid ibus imu interest isaac gym isaac lab isaaclab kdl laplace latent variable latex launch learning-notes legged locomotion legged robotics legged-robot legged_gym life linux linux-kernel mac math matlab matrix memory mlp money motion imitation motion-control motor moveit mpc mujoco music-theory network neural mapping ocs2 ode openscad operator optimal algorithm optimal-control perceptive locomotion perf performance personal-finance piano pixhawk pixhawk 6c policy distillation ppo privileged learning profiling px4 python qgroundcontrol qos quadrotor realsense reinforcement learning representation learning reward tuning rnn robot robot parkour robotics ros ros2 rsl_rl rtb security shell signal-processing sim-to-real simulation socket soft dynamics constraints spot stairs stl stm32 tcp-ip teacher policy teacher student teacher-student temporal convolution thread tools tron1 twist ubuntu uml uncertainty unitree unitree g1 urdf vae valgrind vcxsrv velocity vim web wifi wiring work workflow wsl z-transform zero-shot transfer 中文输入 交叉编译 人形机器人 依赖管理 分支管理 四旋翼 四足机器人 实验诊断 强化学习 机器人 机器人控制 机器人视觉 构建系统 深度学习 深度相机 点云 版本控制 神经网络 自主回充 航模 视觉定位 训练曲线 足式机器人 输入法 配置类 采购记录 音乐 飞控
知识共享许可协议