1. PPO 为什么成了强化学习里的“默认选项”
说句实话,这两年做强化学习,不论你是搞机器人控制、游戏 AI、推荐系统还是大模型对齐,最后落到工业界能稳定跑起来的,十有八九是 PPO 及其变体。我接触强化学习这些年,从 DQN 时代一路走过来,眼看着 PPO 把之前那些“玄学调参”的算法一点点挤出了主流生态,现在大家聊起强化学习,基本默认就是 PPO,顶多加个 SAC 做连续控制的对照组。
PPO 全称是 Proximal Policy Optimization,中文一般叫近端策略优化。它属于策略梯度家族的成员,2017 年由 OpenAI 提出,论文名字就叫《Proximal Policy Optimization Algorithms》。它的设计目标非常朴素:既要像 TRPO 那样保证策略更新不会“步子迈太大导致崩掉”,又要让实现足够简单,计算开销足够小,让研究者能在一台普通 GPU 机器上把算法跑起来。事实证明它做到了,而且在很多任务上效果还比 TRPO 更好。
这篇总结我想换个角度,不把 PPO 当论文来解读,而是从“为什么用它、怎么把它调稳、有哪些家族成员、换到离线场景怎么办”这几个实际问题出发,把我踩过的坑和验证过的经验一起整理出来。
如果你正准备上手强化学习,或者已经在跑 PPO 但总觉得效果不稳定的,这篇文章值得花点时间看完。下面的内容会涉及一些数学公式,但我会把它们拆开讲明白,保证不堆公式。
2. PPO 要解决的问题,以及它和 TRPO 的关系
2.1 策略梯度方法的致命伤:更新步长难调
强化学习的核心目标是学到一个策略,让智能体在与环境交互中获得最大的累计奖励。策略梯度方法的思想很简单:把策略参数化,然后沿着奖励增大的方向调整参数。
问题出在“调整多少”上。步长太小,学习速度慢得让人绝望;步长太大,策略一下就偏了,后面采到的数据全部作废,训练直接崩掉,这个问题在强化学习里有个专业说法叫“策略崩溃”。我早期用原始策略梯度跑一个简单的 CartPole 实验都经常不稳定,更别说复杂的连续控制任务了。
为什么步长敏感?因为策略梯度是用当前策略采样的数据来估计梯度的,一旦策略更新后,这些数据的分布就“过时”了。在统计学上这叫分布偏移。如果更新步长太大,新策略采样的数据分布和旧策略差别太大,梯度的估计就不再准确,整个训练过程就会进入恶性循环。
那能不能给更新加个“刹车”呢?这就是 TRPO(Trust Region Policy Optimization)的思路——它把更新限制在一个可信赖的区域内,保证新旧策略的 KL 散度(衡量两个分布差异的指标)不超过某个固定阈值。TRPO 在理论上很漂亮,但实现起来非常繁琐,因为它要在每次迭代里解一个带约束的优化问题,还需要计算二阶导数,计算量相当大。
2.2 TRPO 的约束优化太慢,PPO 用“裁剪”绕过它
PPO 的设计者换了一个思路:不是去严格限制策略更新的步长,而是在目标函数里加一个惩罚或裁剪机制,让那些“让新旧策略差异过大的更新”得不到回报。
这个思路最核心的就是那行裁剪目标函数。设新旧策略的概率比为 r_t(θ) = π_θ(a_t|s_t) / π_θ_old(a_t|s_t),这个比例表示“新策略在当前状态下选择这个动作的概率比旧策略高了多少”。如果 r 远大于 1,说明新策略显著增加了这个动作的概率;如果 r 小于 1,说明新策略降低了这个动作的概率。
PPO 的目标函数是:
L(θ) = E_t[min(r_t(θ) * A_t, clip(r_t(θ), 1-ε, 1+ε) * A_t)]
其中 A_t 是优势函数,表示在状态 s_t 下采取动作 a_t 比平均水平好多少。ε 是裁剪范围,通常取 0.1 到 0.3。
这样做的效果是:如果 A_t 为正,说明这个动作好,我们应该增大它的概率,但如果 r 超过了 1+ε,那再多增大就没有额外收益了;如果 A_t 为负,说明这个动作差,我们应该压低它的概率,但如果 r 低于 1-ε,额外的压低也不会带来更多收益。
这个“裁剪”机制的作用,我用人话来解释一下:它像是一个保险杠,防止策略更新时某个动作的概率被推得太极端。我见过不少刚接触 PPO 的同学不理解为什么要裁剪,以为只是为了限制更新幅度。其实裁剪更重要的作用是让目标函数变成“一阶连续”的近端优化,它保证了即使在奖励信号有噪声的情况下,更新方向也不会被个别极端样本带偏。
TRPO 的约束优化需要计算共轭梯度和 Fisher 信息矩阵,实现复杂、计算量大。PPO 把硬约束换成了软裁剪,不需要二阶信息,只要一阶梯度就能训练,这就让实现和调试都容易了一个数量级。OpenAI 当初设计 PPO 的初衷,很大程度上就是想把 TRPO 的良好性质“平替”到简单实现之上。
2.3 重要性采样:PPO 的数据效率基础
PPO 的另一个关键是重要性采样。我们前面提到,策略更新后旧数据就“过时”了,但直接丢弃太浪费,重要性采样允许我们用旧策略采样的数据来估计新策略的期望值。
具体做法就是前面那个概率比 r_t(θ)。如果新策略在某个状态下选择某个动作的概率比旧策略大,那么这条经验对新策略来说权重就更高。用这个比例来修正梯度估计,就可以安全地使用旧数据多更新几次。
这里的易错点是:重要性采样只在两个分布差异不大时才可靠。如果新旧策略差异太大,权重方差会爆炸,导致梯度估计极其不稳定。所以 PPO 的裁剪机制其实也是在保护重要性采样,让 r 保持在合理范围内,避免极端权重影响训练。
3. PPO 的完整工作流程与核心实现细节
3.1 PPO 的整体流程:采集、计算、更新三步循环
PPO 的训练过程可以用三步循环来概括:
第一步,用当前策略和环境交互,采集一批轨迹数据,记录状态、动作、奖励、下一状态等信息。
第二步,用这些数据计算优势函数估计 A_t。这一步通常使用 GAE(Generalized Advantage Estimation,广义优势估计),它用一个 λ 参数在偏差和方差之间做权衡,是 PPO 能够稳定训练的关键组件之一。
第三步,用这些数据对策略网络和价值网络进行多轮梯度更新。更新时用重要性采样修正目标函数,用裁剪机制限制更新幅度。
这三步循环往复,直到策略收敛或训练预算耗尽。实际工程中,我们通常把采集数据和更新网络分成独立的进程,用经验池缓存数据,这样数据采集和梯度更新可以流水线并行,显著提升训练效率。
在实际代码实现中,我需要强调一个容易被忽略的细节:GAE 的计算顺序。GAE 需要从轨迹末尾往回递归计算,如果写成从前往后遍历,结果就完全错了。我还见过有人的 GAE 在计算时没有加上最后一个状态的 value 估计,导致优势估计少了一项,虽然训练还能跑起来,但学习效率会明显下降。
3.2 GAE 参数选择:λ 的值决定学习效率
GAE 的核心公式是:
A_t = Σ (γλ)^l δ_{t+l}
其中 δ_t = r_t + γ*V(s_{t+1}) - V(s_t) 是时序差分误差,γ 是折扣因子,λ 是 GAE 的衰减参数。
λ 的含义可以理解为:向前看多少步来估计优势。λ=0 时,只看一步的时序差分误差,偏差大但方差小;λ=1 时,看整个轨迹的累计回报,方差大但偏差小。实际操作中,λ 通常取 0.95 左右,这样既考虑了多步的信息,又不会让方差过大。
我在实际应用中会这样调:如果环境奖励稀疏,我会把 λ 调高到 0.97 甚至 0.99,让优势估计看得更远;如果环境奖励密集但噪声大,我会把 λ 调低到 0.9,避免方差过大。这是一个很实用的调参技巧。
3.3 价值网络的训练:一个容易翻车的环节
价值网络负责估计状态价值 V(s),它直接影响 GAE 的计算。如果价值网络不准确,GAE 就会出现系统性偏差,进而误导策略更新。价值网络的训练通常用均方误差损失,目标是最小化预测价值与实际回报之间的差距。
实际操作中,损失函数还需要加一个裁剪项,防止价值网络的预测值在相邻更新之间跳变太大。OpenAI 的 Spinning Up 实现里给 value loss 也加了 clip,这个细节我一开始没注意,导致训练过程中价值网络的输出经常剧烈震荡,策略也跟着不稳定。
另外,在更新价值网络时,目标值一般使用 TD(λ) 或者 GAE 计算出来的 return。我个人的习惯是,价值网络更新的轮数比策略网络少一些,防止价值网络过早过拟合到当前这批数据上。对于一般的连续控制任务,策略网络更新 3 轮、价值网络更新 2 轮是一个不错的起点配置。
3.4 伪代码级实操:一个 PPO 训练步骤拆解
从工程实现的角度,PPO 的单次迭代可以拆成下面几个步骤。我自己写代码时基本遵循这个结构,你可以直接把下面的伪代码当成底层模板来参考:
# PPO 单次迭代的伪代码结构 # 1. 用当前策略在环境中采样若干条轨迹 states, actions, rewards, next_states, dones = collect_trajectories(env, policy, rollout_steps) # 2. 用 GAE 计算优势值 deltas = rewards + gamma * value(next_states) * (1 - dones) - value(states) advantages = calculate_gae(deltas, gamma, lam) # 3. 归一化优势值 advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8) # 4. 循环更新策略网络 K 轮,每一轮从数据中取小批量 for epoch in range(update_epochs): for batch in create_minibatches(states, actions, log_probs, returns, advantages): # 4.1 计算新旧策略的概率比值 log_probs_new = policy.log_prob(batch.states, batch.actions) ratio = exp(log_probs_new - batch.log_probs_old) # 4.2 裁剪目标函数 surr1 = ratio * batch.advantages surr2 = clamp(ratio, 1 - clip_epsilon, 1 + clip_epsilon) * batch.advantages policy_loss = -min(surr1, surr2).mean() # 4.3 计算价值损失 value_pred = value_network(batch.states) value_loss = mse_loss(value_pred, batch.returns) # 4.4 总损失反向传播 loss = policy_loss + value_weight * value_loss - entropy_weight * policy.entropy().mean() optimizer.zero_grad() loss.backward() # 梯度裁剪 nn_utils.clip_grad_norm_(policy.parameters(), max_grad_norm) optimizer.step() # 5. 当前轮次结束,用已更新的策略进入下一轮这里的“优势值归一化”经常被新接触 PPO 的人忽略,但实际上非常重要。它的作用是让优势值的尺度统一,避免奖励数值大小对更新幅度产生过大影响。我在处理稀疏奖励环境时发现,如果不归一化,优势值可能从负几十到正几千跨度极大,更新方向会被少数大优势样本主导,整个策略都会变得很激进。归一化之后,训练明显稳定多了。
还有一个细节值得说:伪代码中的 log_probs_old 必须在采样后立刻保存。因为策略在一轮更新后就会变化,如果不保存旧概率,后续计算比率时就没有基准了。这个错误特别隐蔽,如果代码里误用了当前策略重新计算概率,表面看起来训练过程没问题,但实际上裁剪机制完全没有起作用,相当于跑了一个带噪声的原始策略梯度。
4. PPO 家族:从单智能体到多智能体,从在线到离线
4.1 分布式 PPO(DPPO):把采样效率拉满
PPO 天生适合并行化,因为它的数据采集过程和更新过程是解耦的。分布式 PPO(Distributed PPO)把多个环境实例分布在不同的进程中,每个进程用自己的策略副本采样数据,采集完毕后再汇总到主进程更新策略,然后把新策略广播回各个进程。这种方式能大幅提高数据采样效率,特别是对需要大量交互数据的任务效果显著。
我在做机械臂操作任务时就用到过类似的架构:8 个并行环境同时跑,每个环境采集 2000 步数据,一轮下来就有 16000 条经验,训练速度比单环境提升了接近 7 倍(受限于通信开销,不是严格线性提升)。OpenAI 的 Five 打 Dota 用的就是类似 DPPO 的思路,效果有目共睹。
4.2 MAPPO:多智能体场景下的 PPO 变体
多智能体强化学习(MARL)是近几年非常热的方向,MAPPO(Multi-Agent PPO)就是 PPO 在多智能体场景下的直接扩展。MAPPO 的核心思路是每个智能体都有自己的策略网络,共享一个集中的价值网络(称为 CTDE,Centralized Training with Decentralized Execution)。训练时,价值网络能看到所有智能体的状态和动作,从而更准确地估计每个智能体的价值;执行时,每个智能体只用自己的策略网络和局部观测做出决策。
我之所以把 MAPPO 单独拎出来,是因为它的实现并不复杂,但性能却比很多专门设计的 MARL 算法好,特别是在星际争霸多智能体挑战(SMAC)这类场景下,MAPPO 的表现非常亮眼。如果你在做多智能体项目,可以优先考虑 MAPPO,而不是去尝试那些理论优美但实现复杂的专用算法。
4.3 Dual-Clip PPO:处理奖励离群值
Dual-Clip PPO 是对 PPO 裁剪机制的一种改进,为了解决奖励中存在极端离群值(outlier)时 PPO 训练不稳定的问题。标准 PPO 的裁剪只限制了正优势情况下的概率比上界,但在极端情况下,某个动作的优势值非常大,裁剪后的目标函数仍然可能给出过大的更新信号。Dual-Clip 在原有基础上增加了一个下限约束,对负优势情况也做对称限制。
我第一次看到 Dual-Clip 是在处理稀疏奖励环境的时候。当时标准 PPO 经常在中途遇到一个极小概率的高奖励事件,策略被这一个样本“带偏”,后面几百轮都缓不过来。换成 Dual-Clip 之后,这类极端样本的影响被直接截断,训练稳定性明显改善。如果你遇到“奖励偶尔出现一个极大值导致训练崩溃”的情况,优先试 Dual-Clip。
4.4 IQL(离线强化学习)与 PPO 的关联
IQL(Implicit Q-Learning)是离线强化学习的代表算法之一。离线强化学习解决的是这样一个问题:只有一批静态数据集,不能和真实环境交互,只能在这批数据上学习策略。这正好触及 PPO 的短板——PPO 极度依赖在线交互数据,因为它的重要性采样修正需要有新旧策略的分布差异才能工作。在离线场景下,新策略发布后没有环境反馈,重要性采样的权重会失控。
IQL 的处理方式和 PPO 差异很大,它用期望回归(expectile regression)的方式隐式地学习一个最优价值函数,避免了显式地对策略进行约束。但它们解决的本质问题是相似的,都是希望在不稳定更新条件下训练策略。如果你有离线数据,想做强化学习,我建议不要直接套 PPO,而是先看 IQL、CQL 这类专门为离线场景设计的算法。
4.5 其他值得关注的 PPO 变体
PPO 家族远不止上面几个。H-PPO 通过分层策略处理复杂任务,先学高层的子任务切换,再学低层具体动作;BPO(Best-Practices PPO)强调对超参数进行系统调优,OpenAI 在论文里给了不少实用的调参建议;PPO-Contextual 把上下文信息引入策略,适合多任务同时学习的场景。如果以后有需要深入的方向,这些变体都是可以参考的思路。
5. 实操经验:连续动作、奖励设计与超参数调优
5.1 连续动作场景的 PPO 配置
在机器人控制这类连续动作任务中,动作空间是连续的,比如机械臂的关节力矩、无人机的前进速度。PPO 在连续动作场景下通常假设动作服从高斯分布,策略网络输出动作均值,以及一个独立的方差向量(或者用状态相关的方差,但后者实现更复杂,收益不一定明显)。
实际操作中,最影响连续动作 PPO 训练稳定性的不是策略网络结构,而是三个点:
第一,动作的缩放。如果动作范围是 [-1, 1],但网络输出的均值可以超出这个范围,就需要在输出层加 tanh 激活函数把均值压到 [-1, 1] 内。有人觉得这只是细节,但实际影响非常大——一旦动作超出环境允许的范围,环境反馈会变得不可预测,训练很容易发散。
第二,对数概率的计算。高斯分布的对数概率要考虑到方差的影响。在连续动作空间里,对数概率的值可能很小甚至为负,但概率比值只需要关注新旧策略的相对大小,所以绝对值的大小并不重要,重要的是同一状态下新旧动作概率的比值是否合理。
第三,熵正则的系数。连续控制任务里熵正则用来鼓励探索,一般取 0 到 0.01 之间的值。我在训练时发现,这个系数如果设得太高,策略会持续随机游走,学习效率低下;设得太低,策略会迅速收敛到一个局部最优。建议先用 0 跑一遍作为基线,如果太久没进展再加一点探索。
5.2 奖励设计:PPO 对奖励尺度非常敏感
我见过太多人忽视奖励设计,结果模型怎么都学不出来。PPO 对奖励尺度非常敏感,原因是它对优势值的处理依赖价值网络的估计,如果奖励尺度过大,价值网络的预测误差也大,优势估计的噪声就高,策略更新就会被噪声主导。
一个有效做法是给奖励做 scale 或 clamp。比如把奖励除以一个常数,让奖励范围落在 [-1, 1] 或者 [0, 1] 区间;遇到极端奖励值,直接 clamp 到合理区间。这样能显著提升训练稳定性。
奖励稀疏的问题则更基础。如果绝大多数时间步奖励都是 0,价值网络很难学到有用信息,GAE 也会退化成单纯的未来回报预测,策略梯度几乎等于随机搜索。我的经验是,先设计 shaping reward(形状奖励),让环境给出一个渐进式的反馈信号,再去逐步稀疏化。比如训练机械臂抓取,可以先给“接近物体”的奖励,再给“接触物体”的奖励,最后才给“成功抓取”的大奖励。
5.3 PPO 超参速查表与调参顺序
PPO 的超参数虽然不多,但组合起来也很讲究。我把常用的配置整理成一张表,方便你对着调:
| 超参数 | 常用范围 | 备注 |
|---|---|---|
| 裁剪范围 ε | 0.1 ~ 0.3 | 0.2 是最常用起点 |
| 折扣因子 γ | 0.99 ~ 0.999 | 任务越长,γ 越接近 1 |
| GAE λ | 0.9 ~ 0.99 | 默认 0.95,稀疏任务调高 |
| 策略网络学习率 | 3e-4 ~ 1e-3 | 常用 3e-4(Adam 优化器) |
| 每个 rollout 的步数 | 1024 ~ 4096 | 和环境复杂度有关,太长太短都不好 |
| 更新轮数 K | 3 ~ 10 | Spinning Up 默认 3 轮 |
| Mini-batch 大小 | 64 ~ 1024 | 大一点稳定,小一点快 |
| 价值损失权重 | 0.5 ~ 1.0 | 默认 1.0 |
| 熵正则权重 | 0 ~ 0.01 | 探索不够时调大 |
| 最大梯度范数 | 0.5 ~ 1.0 | 防止梯度爆炸 |
调参的顺序建议是:先固定裁剪范围 ε = 0.2、γ = 0.99、λ = 0.95、学习率 = 3e-4 这一组“黄金默认值”,跑通整个流程,确认训练曲线在上升,再针对具体问题调整特定参数。不要一上来就同时动好几个参数,这是新手最容易犯的错误,改了四个参数结果它不收敛了,根本不知道哪一步出了问题。
5.4 从环境交互到策略更新的完整训练代码参考
我把一个包含 GAE、管道式采样和策略更新的完整训练循环写出来,方便你直接参考。这段代码吸取了我这些年实战调试的经验,可以直接套到大多数连续控制任务里。
import torch import torch.nn as nn import numpy as np class GAEBuffer: """存储轨迹数据并按 GAE 计算优势""" def __init__(self, gamma=0.99, lam=0.95): self.gamma = gamma self.lam = lam self.reset() def reset(self): self.states, self.actions, self.log_probs = [], [], [] self.rewards, self.dones, self.values = [], [], [] self.next_values = [] def add(self, state, action, log_prob, reward, done, value, next_value): self.states.append(state) self.actions.append(action) self.log_probs.append(log_prob) self.rewards.append(reward) self.dones.append(done) self.values.append(value) self.next_values.append(next_value) def compute_gae(self, last_value=0.0): states = torch.tensor(np.array(self.states), dtype=torch.float32) actions = torch.tensor(np.array(self.actions), dtype=torch.float32) log_probs = torch.tensor(np.array(self.log_probs), dtype=torch.float32) rewards = torch.tensor(self.rewards, dtype=torch.float32) values = torch.tensor(self.values, dtype=torch.float32) next_values = torch.tensor(self.next_values + [last_value], dtype=torch.float32) advantages = torch.zeros_like(rewards) gae = 0.0 # 从轨迹末尾往前递归计算 GAE for t in reversed(range(len(rewards))): delta = rewards[t] + self.gamma * next_values[t] * (1 - self.dones[t]) - values[t] gae = delta + self.gamma * self.lam * (1 - self.dones[t]) * gae advantages[t] = gae returns = advantages + values return states, actions, log_probs, returns, advantages def ppo_update(policy, value_net, optimizer, data, cfg): states, actions, old_log_probs, returns, advantages = data # 归一化优势,提升稳定性 advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8) for _ in range(cfg.update_epochs): for idx in range(0, len(states), cfg.batch_size): batch = slice(idx, idx + cfg.batch_size) s, a, old_lp = states[batch], actions[batch], old_log_probs[batch] ret, adv = returns[batch], advantages[batch] dist = policy(s) new_log_probs = dist.log_prob(a) entropy = dist.entropy().mean() ratio = (new_log_probs - old_lp).exp() surr1 = ratio * adv surr2 = torch.clamp(ratio, 1 - cfg.clip_eps, 1 + cfg.clip_eps) * adv policy_loss = -torch.min(surr1, surr2).mean() value_pred = value_net(s).squeeze(-1) value_loss = nn.functional.mse_loss(value_pred, ret) loss = policy_loss + cfg.value_weight * value_loss - cfg.entropy_weight * entropy optimizer.zero_grad() loss.backward() nn.utils.clip_grad_norm_(list(policy.parameters()) + list(value_net.parameters()), cfg.max_grad_norm) optimizer.step()这里有个小细节需要注意:在把next_values传入计算时,我用self.next_values + [last_value]把轨迹最后一个 transition 的下一状态价值补上了。last_value是轨迹终止后(如果未真正结束)价值网络的输出;如果轨迹因达到终止状态而结束,这个值应设为 0,也就是代码里默认的last_value=0.0。这个 0 存在的意义是,终止状态之后没有任何奖励,价值应该是 0,直接补 0 和公式是一致的。
6. 常见问题排查:我踩过的 PPO 的坑
6.1 训练曲线上升后又突然崩掉
这个问题我遇到太多次了。最常见的两个原因:一是学习率设置过高,策略更新时跨过了好策略区域;二是奖励 shaping 出了问题,某个短暂的局部策略拿到了异常高的奖励,PPO 把策略推向那个方向,结果后面环境反馈迅速恶化。
解决办法是先把学习率降低一个数量级试试。如果仍然崩,就把 GAE 的 λ 降低到 0.9 左右,减少价值估计偏差的累积。另外我也推荐在训练过程中记录策略网络梯度的 L2 范数,如果某个时刻梯度范数突然激增,那个时间点附近的奖励样本一定有问题。
6.2 优势值的 scale 差异过大
我在 3.4 节中已经推广过归一化优势值,这里再补充一个场景:当奖励存在负数时,优势估计可能也是负的。裁剪机制对负优势同样有效,因为它限定了概率比的下界为 1-ε,防止负优势样本把某个动作的概率压得过低。
但有一个暗坑:如果数据集里正负优势样本的比例严重失衡,比如 99% 是负优势,策略网络会迅速把所有动作概率压低,最后陷入一个什么都不做的局部最优。解决办法是给负优势样本一个更小的学习率,或者提高 GAE λ 让优势估计更平滑。我在实际项目中用过后一种方案,效果不错。
6.3 价值网络发散或振荡严重
价值网络发散有几种可能:一是价值网络和策略网络共用一个优化器,价值损失和策略损失的尺度差异太大,导致梯度方向被价值损失主导;二是价值网络的结构太深或太宽,在小规模任务上过拟合噪声。
针对第一点,我的建议是给价值网络单独使用一个优化器,并把价值损失权重调到 0.5。针对第二点,控制任务里价值网络用两层 64 或 128 的全连接网络就够了,不需要太复杂。
6.4 训练时间过长但收益甚微
这种情况通常源于探索不足。策略被困在某个局部区域,采集到的数据几乎一模一样,价值网络很快就拟合到这批数据上,但无法推广到其他状态空间。我推荐的解决思路是按顺序排查:
先检查熵正则权重,把熵正则从 0 提高到 0.005 或 0.01,增强探索。如果效果不明显,检查 GAE λ 是否过小,调大到 0.97 让优势估计看得更远。最后再检查奖励函数是否有引导信号,如果奖励全为 0 或全为常数,算法再强也没用。
6.5 训练速度慢:怎么判断是算力问题还是算法问题
有时候你发现 PPO 跑得很慢,但不一定是算法有问题。先用 profiler 看时间消耗分布:如果 90% 时间花在环境仿真上,那瓶颈在环境,分布式采样可以解决;如果 90% 时间花在网络前向和反向传播,那瓶颈在模型,考虑减小网络规模或者使用混合精度训练。
一个实用的经验是:如果环境步进时间小于 1ms,单环境跑 PPO 完全够用,不需要分布式;如果环境步进时间超过 10ms,建议至少开 4 个并行环境。这样能把采样端和训练端的等待时间压到最少。
7. 我的心得:什么场景下不要用 PPO
最后我还是想泼一点冷水。PPO 虽然强大,但并不是万能的,我见过不少项目在错误的场景里硬套 PPO,最后投入产出比非常低。
第一个不适合 PPO 的场景是样本获取极其昂贵的场景。PPO 的在线采样特性意味着它需要大量与环境交互的数据,像真机机器人控制这种一次实验成本极高的场景,纯 PPO 并不合适,更推荐的方案是先用 Offline RL 在已有数据集上预训练,再用少量在线交互做微调。
第二个不适合的场景是需要精确最优策略的场景。PPO 是一个带约束的近似优化算法,它更擅长找到一个“足够好”的策略,而不是数学上的最优策略。如果任务对性能要求极高,可以考虑 SAC、TD3 这类基于最大熵框架的算法,它们在连续控制任务上往往表现更好。
第三个不适合的场景是数据极度稀疏且奖励存在大量噪声的环境。PPO 的价值网络和 GAE 都依赖奖励信号的可用性,如果奖励信号几乎全是噪声,PPO 基本学不出什么东西。这时候先花精力设计好的奖励函数,比换任何算法都重要。
我在实际项目里的经验是,遇到一个新任务,会先花一周时间把奖励设计、状态表征、动作空间这些基础问题想清楚,然后再动手跑 PPO。因为算法层面的问题往往不是瓶颈,真正决定成败的都是这些看似琐碎的细节。
如果你正在跑 PPO 遇到问题了,建议先把你当前环境里最基础的版本跑通——用最小的状态空间、最简单的奖励、最短的轨迹长度,把整条链路验证完,再逐步往上加复杂度。这个习惯帮我少走了非常多弯路,也分享给你。