PPO:为什么今天默认用它
如果你只打算记住这本书里的一个算法,记这个。它简单到能用十几行写完,稳到从机器人一路用到了 ChatGPT。而它全部的聪明,只有一句话。
要解决的那个问题
上一章末尾留下的:策略更新一步迈太大,就会崩,而且崩了回不来。
再把那个正反馈说一遍,因为 PPO 的全部设计都是冲着它去的:
策略更新过头
↓
走到很差的区域
↓
采到的全是烂数据
↓
用烂数据算出的梯度更烂
↓
(回到第一行)
监督学习没有这个问题——数据集是固定的,模型学坏了下一轮还是同一批数据,能救。强化学习里,你的数据是你自己采的。策略崩了,数据源也就一起毁了。
那自然的想法是:限制每次更新的幅度。
但「幅度」该怎么量?限制参数 θ 的变化量?不行——参数动一点点,策略可能天翻地覆(想想 softmax 在饱和区附近);参数动很多,策略也可能几乎没变。
该限制的是策略本身的变化,不是参数的变化。
TRPO 的答案,和它的问题
2015 年的 TRPO 给了一个数学上很漂亮的方案:在「新旧策略的 KL 散度不超过 δ」这个约束下,最大化目标。
它真的有单调改进的理论保证。但它有个很现实的毛病:要解一个带约束的优化问题——共轭梯度、Fisher 信息矩阵的向量积、线搜索。实现起来又长又容易写错,跑起来还慢。
PPO 的答案:不解约束,直接把目标削平
2017 年的 PPO 说:既然是要「别走太远」,那我不用约束,我把目标函数改成「走太远就没有额外好处」的形状就行了。
先定义概率比:
ratio = π_new(a|s) / π_old(a|s) ratio = 1 :新策略和旧策略一样 ratio = 2 :新策略把这个动作的概率翻了一倍 ratio = 0.5 :砍了一半
然后是那个目标:
L = min( ratio · A , clip(ratio, 1-ε, 1+ε) · A )
ε 通常取 0.2。
你会看到削平的方向翻转了。这个不对称是这个 min 全部的精妙之处。
那个 min 在干什么
分两种情况看,它们的行为完全不同:
A > 0:这个动作比平均好
我们想提高它的概率。但目标在 ratio = 1+ε 处被削平了——提到 1.2 倍之后,再提也不会让目标变大,梯度归零。
意思是:「这个动作确实好,但你一次别把它的概率提太多。」
而 ratio 往下走那一侧不削(引擎算的:ratio=0.5、A=+1 时目标是 0.50,没被裁)。为什么?因为那一侧本来就在惩罚你——你把一个好动作的概率砍了,目标自然低。不需要额外限制。
A < 0:这个动作比平均差
削平的方向翻过来了:ratio = 1−ε 那一侧平了(引擎算的:ratio=0.5、A=−1 时目标是 −0.80,被裁了)。
意思是:「这个动作确实差,但你一次别把它砍得太狠。」
关键在于:那个优势 A,是用旧策略采的样估出来的。
它说的是「在旧策略走出来的那些状态里,这个动作比平均好 0.3」。新策略一旦走远,它会去到完全不同的状态分布——而在那些状态里,这个动作好不好,这批数据什么都没说。
你在用一张过期的地图指路。走一步还行,走十步就完全不知道自己在哪了。
PPO 全部的聪明就是这一句:相信这批数据,但只相信一步之内。
而「一步」有多大?就是那个 ε。0.2 意味着「概率变化不超过 ±20%」。
它换来了什么
这个改动带来一个非常实际的好处,而且它才是 PPO 真正流行的原因:
同一批数据可以反复用好几轮。
纯策略梯度是严格 on-policy 的:采一批、更新一次、扔掉。样本效率极低。
PPO 因为有那个刹车,可以拿同一批数据做 4 到 10 轮梯度更新——反正走远了目标就平掉,不会失控。样本效率提升了好几倍,而稳定性几乎没损失。
L = L_clip - c₁ · L_value + c₂ · H[π]
└─┬──┘ └────┬────┘ └──┬──┘
策略(上面那个) Critic 的回归 熵奖励
三项各司其职:
- L_clip:上面讲的那个。Actor 的部分。
- L_value:Critic 去拟合回报的均方误差。
c₁通常 0.5。 - H[π]:策略的熵,鼓励它别太快变确定。
c₂通常 0.0 到 0.01。
第三项就是第 4 章说的「探索被写进了目标函数」。熵大 = 策略随机 = 还在探索。这一项在防止过早收敛到次优策略——那种失败很隐蔽,曲线会漂亮地平掉,看起来像收敛了。
为什么是它成了默认选择
| PPO | SAC | DQN | |
|---|---|---|---|
| 连续动作 | ✓ | ✓ | ✗ |
| 离散动作 | ✓ | 需改造 | ✓ |
| 样本效率 | 中 | 高 | 高 |
| 稳定性 / 好调 | 高 | 中 | 低 |
| 实现复杂度 | 低 | 中 | 中 |
| 并行采样 | 天然适合 | 一般 | 一般 |
PPO 没有一项是最强的。它样本效率不如 SAC,理论保证不如 TRPO。
但它什么都能干,而且不容易崩——这在工程上是压倒性的优点。你可以拿同一份 PPO 代码去跑 Atari、跑机械臂、跑 Dota、跑语言模型,改改超参就行。
OpenAI Five(Dota 2)、OpenAI 的机械手(解魔方)、以及 ChatGPT 的 RLHF,用的都是 PPO。
2020 年前后有几篇实证研究做了件很有意思的事:把 PPO 的各种实现细节逐个拆下来做消融。
结论有点尴尬:那些「代码层面的小技巧」贡献的性能,可能不比 clip 本身少。包括:
- 观测归一化(跑一个滑动均值方差,把状态标准化)
- 优势归一化(每个 batch 内把 A 减均值除标准差)
- 学习率线性退火
- 正交初始化 + 特定的增益系数
- 梯度裁剪(
max_grad_norm=0.5) - 价值函数损失也做裁剪
这件事有两个启示,都值得记住:
① 别自己从论文伪代码复现 PPO 然后纳闷为什么效果差。论文里没写的细节可能贡献了一半性能。用 SB3、CleanRL 这些成熟实现。
② 这个领域的「算法贡献」和「工程贡献」经常混在一起,而后者往往被低估。第 15 章那个「DQN 五个零件里只有一个是深度学习」,说的是同一件事。
from stable_baselines3 import PPO
model = PPO("MlpPolicy", env,
learning_rate=3e-4, # ① 最该先调的。不收敛先减半
n_steps=2048, # ② 每个环境采多少步才更新一次
batch_size=64, # ③ 每次梯度更新用多少
n_epochs=10, # ④ 同一批数据重复用几轮(clip 让这成为可能)
gamma=0.99, # ⑤ 第 6 章
gae_lambda=0.95, # ⑥ 第 17 章那个滑块
clip_range=0.2, # ⑦ 就是那个 ε
ent_coef=0.0, # ⑧ 熵系数。探索不足就调到 0.01
vf_coef=0.5,
max_grad_norm=0.5) # 梯度裁剪,别关掉
三条实用建议:
① 先用向量化环境。PPO 天生适合并行采样,make_vec_env(env_id, n_envs=8) 通常是最划算的一次改动——墙钟时间直接快几倍,而且样本更 i.i.d.。
② 不收敛的时候,调参顺序是:学习率 → n_steps → ent_coef。clip_range 几乎不用动,0.2 是个很稳的值。
③ 而在调这些之前,先回去检查奖励函数。这是下一卷的主题,也是我认为最容易被跳过的一步——因为奖励的问题不会在任何一条曲线上表现出来。
那个学走路的小人,如果是 PPO 训的,你能从视频里看出那个 clip 的效果:
它的进步是渐进的、单调的。今天比昨天好一点,很少出现「练了半天突然全废了」。
对比一下没有刹车的策略梯度:曲线会剧烈震荡,动不动一个跟头栽回原点,然后要重新爬很久。那种「训练崩了」的曲线,PPO 之后基本消失了。
而且这个稳定性有个副作用值得一提:它让「训练 RLHF」这件事在工程上变得可行。你没法接受一个几百亿参数的模型在训练中途莫名其妙地崩掉——重训一次的成本太高了。
这一章的一句话
相信这批数据,但只相信一步之内。一个 min 就实现了它,而这一句让强化学习从「能跑通的研究」变成了「能交付的工程」。
卷 IV 到此结束。你现在从表格一路走到了现代算法,手上有一套能真正跑起来的东西。
但整整四卷,我们一直假定那个奖励函数是给定的、是对的。下一卷开始,我们回过头去看那一行——那是你唯一真正在写的代码,也是最容易出事的地方。