卷 IV · 换脑CH 18深度 18/24

PPO:为什么今天默认用它

如果你只打算记住这本书里的一个算法,记这个。它简单到能用十几行写完,稳到从机器人一路用到了 ChatGPT。而它全部的聪明,只有一句话。

PPO裁剪目标信任域

要解决的那个问题

上一章末尾留下的:策略更新一步迈太大,就会崩,而且崩了回不来。

再把那个正反馈说一遍,因为 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。

▶ 动手 · 把 A 从 +1 拖到 −1,盯住绿线哪边平了

你会看到削平的方向翻转了。这个不对称是这个 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 轮梯度更新——反正走远了目标就平掉,不会失控。样本效率提升了好几倍,而稳定性几乎没损失。

◆ 完整的 PPO 目标
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 章说的「探索被写进了目标函数」。熵大 = 策略随机 = 还在探索。这一项在防止过早收敛到次优策略——那种失败很隐蔽,曲线会漂亮地平掉,看起来像收敛了。

为什么是它成了默认选择

PPOSACDQN
连续动作
离散动作需改造
样本效率
稳定性 / 好调
实现复杂度
并行采样天然适合一般一般

PPO 没有一项是最强的。它样本效率不如 SAC,理论保证不如 TRPO。

但它什么都能干,而且不容易崩——这在工程上是压倒性的优点。你可以拿同一份 PPO 代码去跑 Atari、跑机械臂、跑 Dota、跑语言模型,改改超参就行。

OpenAI Five(Dota 2)、OpenAI 的机械手(解魔方)、以及 ChatGPT 的 RLHF,用的都是 PPO。

⚠ 「PPO 的效果主要来自那个 clip」——这个说法后来被打了折扣

2020 年前后有几篇实证研究做了件很有意思的事:把 PPO 的各种实现细节逐个拆下来做消融。

结论有点尴尬:那些「代码层面的小技巧」贡献的性能,可能不比 clip 本身少。包括:

  • 观测归一化(跑一个滑动均值方差,把状态标准化)
  • 优势归一化(每个 batch 内把 A 减均值除标准差)
  • 学习率线性退火
  • 正交初始化 + 特定的增益系数
  • 梯度裁剪(max_grad_norm=0.5
  • 价值函数损失也做裁剪

这件事有两个启示,都值得记住:

① 别自己从论文伪代码复现 PPO 然后纳闷为什么效果差。论文里没写的细节可能贡献了一半性能。用 SB3、CleanRL 这些成熟实现。

② 这个领域的「算法贡献」和「工程贡献」经常混在一起,而后者往往被低估。第 15 章那个「DQN 五个零件里只有一个是深度学习」,说的是同一件事。

⌗ 换成真机:PPO 的超参,按重要性排
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 到此结束。你现在从表格一路走到了现代算法,手上有一套能真正跑起来的东西。

但整整四卷,我们一直假定那个奖励函数是给定的、是对的。下一卷开始,我们回过头去看那一行——那是你唯一真正在写的代码,也是最容易出事的地方。