PPO
DQN解决了离散问题,于是人们开始着手于解决更加现实的连续控制问题。时至今日,连续控制领域基本被PPO(on-policy)和SAC(off-policy)这两个算法统治,但在早期发展中依然经历了诸多变革。本章以最终形态PPO作为核心,简要整理on-policy路线中最核心的概念。
PPO(Proximal Policy Optimization)由OpenAI在2017年提出,是目前工业界和学术界使用最广泛的强化学习算法之一。从ChatGPT的RLHF训练,到机器人控制,到游戏AI(OpenAI Five),PPO几乎无处不在。它的核心卖点是:在策略梯度算法的基础上,用极其简洁的代码实现了接近TRPO的理论性能保证,同时训练稳定、调参简单。
发展历程
在理解PPO之前,我们需要先梳理从最基础的策略梯度到PPO的整条进化链路:
A3C - Asynchronous Advantage Actor-Critic
A3C由DeepMind在2016年提出(Mnih et al., "Asynchronous Methods for Deep Reinforcement Learning"),是第一个真正在连续控制任务上大规模成功的深度强化学习算法。
核心思想: 用多个并行的worker(线程/进程)同时和各自独立的环境副本交互,异步地更新一个共享的全局网络。
A3C的关键创新有两点:
-
异步并行训练: 每个worker独立地收集经验并计算梯度,然后异步地把梯度推送(push)到全局网络。全局网络一收到梯度就立即更新参数,然后worker再从全局网络拉取(pull)最新参数继续训练。这种方式极大地提高了数据收集效率,并且由于不同worker看到的状态分布不同,天然地起到了"打破数据相关性"的作用——这和DQN用经验回放解决的是同一个问题,但A3C用了完全不同的思路。
-
Advantage Actor-Critic架构: A3C同时训练两个网络(通常共享底层参数):
- Actor(策略网络): 输出动作的概率分布 \(\pi_\theta(a|s)\)
- Critic(价值网络): 输出状态价值 \(V_\phi(s)\)
Actor根据Advantage函数 \(A(s,a) = Q(s,a) - V(s)\) 来更新策略。用Advantage代替原始的回报 \(G_t\) 可以大幅降低方差,同时保持无偏性。
A3C的问题: 异步更新会导致"梯度过期"问题。当worker A计算完梯度准备上传时,全局网络可能已经被worker B更新过了,worker A的梯度是基于旧参数算的,可能会把全局网络往错误的方向拉。
A2C - Synchronous Advantage Actor-Critic
A2C是A3C的同步版本。它的改进很简单:所有worker同步地收集一轮数据,一起等齐了,再统一更新全局网络。
虽然这牺牲了一些时间效率(快的worker要等慢的),但消除了异步带来的梯度噪声。实验表明,A2C在相同的计算预算下往往能达到和A3C相同甚至更好的性能,同时实现更简单、更容易调试。
A2C是理解PPO的重要铺垫,因为PPO本质上就是在A2C的基础上加了一个"信赖域"约束来稳定训练。
Trust Region - 信赖域的概念
在正式讲TRPO之前,我们需要理解一个优化领域的核心概念:信赖域(Trust Region)。
在传统的梯度下降中,我们计算出梯度方向,然后沿着这个方向走一步。但走多远?这由学习率(Learning Rate)决定。问题是:梯度只在当前点的极小邻域内是准确的。 如果学习率太大,你走出了这个邻域,梯度方向就不再可靠,可能导致性能暴跌。
信赖域方法的思路完全不同:
- 在当前参数 \(\theta\) 的附近,构造一个目标函数的局部近似模型(比如二次近似)。
- 定义一个信赖域(Trust Region),即我们"信任"这个近似模型准确的范围。
- 在这个信赖域内,找到近似模型的最优解,作为下一步的参数。
- 如果新参数确实让真实目标函数变好了,就扩大信赖域;如果变差了,就缩小信赖域。
在强化学习中,信赖域的概念尤其重要。因为策略梯度算法有一个致命问题:策略更新一步太大,可能导致策略直接崩溃,而且很难恢复。 这不像监督学习中,即使某一步loss增大了,下一步还可以修正回来。在RL中,一旦策略变差,它收集到的数据质量也会变差,进而导致下一轮更新更差——形成恶性循环。
TRPO - Trust Region Policy Optimization
TRPO由Schulman等人在2015年提出,是第一个将信赖域方法正式引入策略梯度的算法。它在理论上保证了每一步更新都能单调提升策略性能(或至少不会变差)。
理论基础: TRPO的出发点是Kakade和Langford在2002年证明的一个重要不等式。对于任何两个策略 \(\pi\) 和 \(\pi'\),新策略的期望回报可以表示为:
其中 \(\eta(\pi)\) 是策略 \(\pi\) 的期望回报,\(A^\pi(s,a)\) 是旧策略下的优势函数,\(d^{\pi'}\) 是新策略下的状态访问分布。
问题在于,右边的期望是在新策略 \(\pi'\) 的状态分布下计算的,而我们还没有执行新策略,无法获得这个分布。TRPO的关键洞察是:如果新旧策略足够接近,我们可以用旧策略的状态分布来近似,得到一个代理目标函数(Surrogate Objective):
为了保证这个近似是合理的(新旧策略确实足够接近),TRPO对策略更新施加了一个KL散度约束:
其中 \(D_{\text{KL}}\) 是KL散度,衡量两个概率分布之间的差异;\(\delta\) 是一个很小的阈值,限制新旧策略的差异不能太大。
求解方法: 这是一个带约束的优化问题,不能直接用SGD求解。TRPO使用了以下技巧:
- 对目标函数做一阶Taylor展开(线性近似):
其中 \(g = \nabla_\theta L |_{\theta_{\text{old}}}\) 是策略梯度。
- 对KL约束做二阶Taylor展开(二次近似):
其中 \(F\) 是Fisher信息矩阵(Fisher Information Matrix),描述了参数空间中策略变化的曲率。
-
用共轭梯度法(Conjugate Gradient)求解: 直接计算 \(F^{-1}g\) 需要对Fisher矩阵求逆,对于大规模神经网络是不可行的。TRPO巧妙地使用共轭梯度法,只需要计算Fisher矩阵与向量的乘积 \(Fv\)(这可以通过自动微分高效完成),就能近似地求解出更新方向。
-
线搜索(Line Search): 在共轭梯度给出更新方向后,还需要沿着这个方向做线搜索,确保KL约束确实被满足,并且目标函数确实提高了。
TRPO的致命缺点:
- 实现极其复杂(需要共轭梯度、线搜索、Fisher向量积等)
- 不兼容参数共享的网络架构(Actor和Critic共享层时很难处理)
- 不兼容Dropout、Batch Normalization等常见的正则化技术
- 计算开销大
正是因为TRPO这些工程上的问题,PPO才应运而生。
PPO核心算法
PPO的设计哲学是:保留TRPO"限制策略更新步长"的核心思想,但用一种极其简单的方式来实现。 不需要共轭梯度,不需要Fisher矩阵,不需要线搜索。只需要一阶优化器(如Adam)就能搞定。
策略梯度回顾
在讲PPO的具体公式之前,我们先回顾策略梯度(Policy Gradient)的基本定理。
策略梯度的目标是最大化期望回报:
根据策略梯度定理(Policy Gradient Theorem),\(J(\theta)\) 对参数 \(\theta\) 的梯度为:
其中:
- \(\pi_\theta(a_t|s_t)\) 是策略在状态 \(s_t\) 下选择动作 \(a_t\) 的概率
- \(A^{\pi_\theta}(s_t, a_t)\) 是优势函数,衡量动作 \(a_t\) 相对于平均水平的好坏
- \(\nabla_\theta \log \pi_\theta(a_t|s_t)\) 是对数概率的梯度,指明了增大该动作概率的参数更新方向
直觉理解: 如果动作 \(a_t\) 的优势 \(A > 0\)(比平均好),就增大这个动作的概率;如果 \(A < 0\)(比平均差),就减小这个动作的概率。增大/减小的幅度与 \(|A|\) 成正比。
原始策略梯度的问题: 它是一个on-policy算法,每收集一轮数据只能更新一次参数,然后数据就必须丢掉(因为数据是旧策略生成的,不能用于新策略的梯度估计)。这导致样本效率极低。
Importance Sampling - 重要性采样
为了提高样本效率,我们希望能用旧策略 \(\pi_{\theta_{\text{old}}}\) 收集的数据来更新新策略 \(\pi_\theta\)。这就需要用到重要性采样(Importance Sampling)。
重要性采样的基本原理是:如果我们想计算函数 \(f(x)\) 在分布 \(p(x)\) 下的期望,但我们只有来自另一个分布 \(q(x)\) 的样本,可以通过加权来修正:
其中 \(\frac{p(x)}{q(x)}\) 就是重要性权重(Importance Weight)。
应用到策略梯度中,我们用旧策略的数据来估计新策略的目标函数:
我们定义概率比(Probability Ratio):
那么代理目标函数可以简写为:
当 \(\theta = \theta_{\text{old}}\) 时,\(r_t = 1\),代理目标函数就退化为普通的策略梯度目标。
重要性采样的问题: 当新旧策略差异太大时,重要性权重 \(r_t(\theta)\) 可能非常大或非常小,导致梯度估计的方差极大,训练不稳定。TRPO通过KL散度约束来限制这个差异,而PPO选择了一种更直接的方式——Clipping。
Clipped Surrogate Objective - 截断代理目标
这是PPO最核心的创新,也是整个算法最精妙的地方。
PPO-Clip直接在目标函数里对概率比 \(r_t(\theta)\) 进行截断:
其中:
- \(r_t(\theta) = \frac{\pi_\theta(a_t|s_t)}{\pi_{\theta_{\text{old}}}(a_t|s_t)}\) 是概率比
- \(\epsilon\) 是一个超参数,通常取 \(0.2\),控制截断范围
- \(\text{clip}(r_t, 1-\epsilon, 1+\epsilon)\) 将概率比限制在 \([0.8, 1.2]\) 之间
- \(\min\) 操作取两项中的较小值
为什么Clipping能起作用?我们分四种情况来分析:
情况1:\(A_t > 0\)(动作好于平均),\(r_t > 1\)(新策略更倾向于选这个动作)
- 不截断项:\(r_t \cdot A_t\),随着 \(r_t\) 增大而增大
- 截断项:\(\text{clip}(r_t, 0.8, 1.2) \cdot A_t = 1.2 \cdot A_t\),被封顶
- \(\min\) 取较小值 \(= 1.2 \cdot A_t\)(被截断的那个)
效果: 新策略已经比旧策略更倾向于选这个好动作了,但目标函数不再给予额外奖励。防止"过度优化"——不要因为某个动作碰巧好了一次,就疯狂地把这个动作的概率拉到极高。
情况2:\(A_t > 0\)(动作好于平均),\(r_t < 1\)(新策略不太倾向于选这个动作)
- 不截断项:\(r_t \cdot A_t\),小于 \(A_t\)
- 截断项:因为 \(r_t < 1 < 1+\epsilon\),截断通常不生效,\(\text{clip}(r_t, 0.8, 1.2) = \max(r_t, 0.8)\)
- \(\min\) 取较小值 \(= r_t \cdot A_t\)(不截断项更小)
效果: 好动作的概率在下降,目标函数给出正常的梯度信号,鼓励恢复这个动作的概率。
情况3:\(A_t < 0\)(动作差于平均),\(r_t < 1\)(新策略减少了这个动作的概率)
- 不截断项:\(r_t \cdot A_t\),注意这是两个数相乘,\(r_t\) 越小结果越大(越接近0)
- 截断项:\(0.8 \cdot A_t\),被封底
- \(\min\) 取较小值 \(= 0.8 \cdot A_t\)(被截断的那个)
效果: 新策略已经在减少这个坏动作的概率了,但目标函数不再给予额外惩罚。防止"过度回避"。
情况4:\(A_t < 0\)(动作差于平均),\(r_t > 1\)(新策略反而增加了这个动作的概率)
- 不截断项:\(r_t \cdot A_t\),\(r_t\) 越大结果越小(越负)
- 截断项:通常不生效
- \(\min\) 取较小值 \(= r_t \cdot A_t\)
效果: 坏动作的概率在上升,目标函数给出正常的梯度信号,惩罚这种行为。
总结: Clipping的本质逻辑是——当策略变化方向正确时(好动作概率增大或坏动作概率减小),限制变化幅度;当策略变化方向错误时,不限制,让梯度自由修正。 这就实现了"悲观的下界"估计:我们总是在优化目标函数的一个下界,确保真实性能不会比我们估计的更差。
值函数损失
PPO中的Critic网络负责估计状态价值函数 \(V_\phi(s)\),用于计算优势函数。Critic的损失函数通常是简单的MSE:
其中 \(V_t^{\text{target}}\) 是价值函数的目标值,通常取GAE计算出的回报估计 \(\hat{R}_t\)(后面会详细介绍):
在PPO原论文中,作者还提出了对值函数也做Clipping的变体:
其中 \(V_\phi^{\text{clip}} = \text{clip}(V_\phi(s_t), V_{\phi_{\text{old}}}(s_t) - \epsilon, V_{\phi_{\text{old}}}(s_t) + \epsilon)\)。但实践中发现,值函数Clipping的效果不太一致,很多高性能实现中直接使用简单的MSE。
熵奖励
为了防止策略过早收敛到某个确定性的动作(即"早熟"),PPO在目标函数中加入了一个熵奖励(Entropy Bonus):
熵衡量的是策略输出的"随机程度"。熵越大,策略越随机(各动作概率越均匀);熵越小,策略越确定(集中于某一个动作)。
加入熵奖励的目的:
- 鼓励探索: 防止策略在训练初期就坍缩到某个局部最优解。
- 避免动作概率趋零: 如果某个动作的概率趋近于0,\(\log \pi\) 会趋近负无穷,数值上会出问题。熵奖励会阻止这种情况发生。
- 提高鲁棒性: 稍微随机的策略在面对环境扰动时更加鲁棒。
PPO的完整目标函数
将上述三个部分合在一起,PPO的完整目标函数为:
其中:
- \(L_t^{\text{CLIP}}(\theta)\):截断代理目标(最大化)
- \(L_t^{VF}(\phi)\):值函数损失(最小化,所以前面是负号)
- \(H(\pi_\theta)\):策略熵(最大化,所以前面是正号)
- \(c_1\):值函数损失的系数,通常取 \(0.5\) 或 \(1.0\)
- \(c_2\):熵奖励的系数,通常取 \(0.01\)
注意,这里PPO的常见实现有两种做法:
- 共享网络: Actor和Critic共享底层特征提取层(如CNN的卷积层),最后分出两个头。此时三项损失放在一起优化,\(c_1\) 和 \(c_2\) 用于平衡三者的重要性。
- 分离网络: Actor和Critic使用完全独立的网络。此时策略损失和值函数损失可以分开优化。
实现细节
PPO的论文本身非常简洁,但真正让PPO表现优秀的,是大量的实现细节。这些细节在原论文中要么一笔带过,要么完全没提,但它们对性能影响巨大。
GAE - Generalized Advantage Estimation
GAE(广义优势估计)是PPO中计算优势函数 \(A_t\) 的标准方法,由Schulman在2016年单独发表的论文中提出。
为什么需要GAE? 计算优势函数面临一个经典的bias-variance trade-off:
方法1:蒙特卡洛回报(高方差,零偏差)
用完整的回合回报来估计优势。优点是无偏,缺点是方差很大(因为回报受后续所有随机性的影响)。
方法2:单步TD误差(低方差,有偏差)
只看一步的奖励加上下一步的价值估计。方差小(只依赖一步的随机性),但有偏差(因为 \(V(s_{t+1})\) 本身就是一个不完美的估计)。
GAE的解法:用一个参数 \(\lambda\) 来平滑地在两者之间做插值。
首先定义单步TD误差:
然后,GAE被定义为TD误差的指数加权平均:
展开来看:
- 当 \(\lambda = 0\) 时:\(\hat{A}_t = \delta_t\),退化为单步TD误差(低方差,高偏差)
- 当 \(\lambda = 1\) 时:\(\hat{A}_t = \sum_{l=0}^{T-t} \gamma^l \delta_{t+l}\),等价于蒙特卡洛回报减去基线(高方差,零偏差)
实践中,\(\lambda\) 通常取 \(0.95\),在方差和偏差之间取得良好的平衡。
GAE的递推计算: 在实现中,GAE可以从轨迹末端反向递推计算,效率很高:
这和计算折扣回报的递推公式形式完全一致,代码实现非常简洁:
# GAE计算(从后往前递推)
advantages = torch.zeros_like(rewards)
last_gae = 0
for t in reversed(range(T)):
delta = rewards[t] + gamma * values[t+1] * (1 - dones[t]) - values[t]
advantages[t] = last_gae = delta + gamma * lam * (1 - dones[t]) * last_gae
returns = advantages + values[:-1] # V_target = A + V
Mini-batch Updates
PPO的一个关键实现细节是:收集一大批数据后,将其打散成多个mini-batch,多次遍历更新。
流程如下:
- 用当前策略 \(\pi_{\theta_{\text{old}}}\) 与环境交互,收集 \(N\) 个并行环境各 \(T\) 步的数据,共 \(N \times T\) 个transition。
- 用Critic网络计算所有状态的价值 \(V(s_t)\),然后用GAE计算所有的优势 \(\hat{A}_t\)。
- 将这 \(N \times T\) 个数据点随机打散(shuffle)。
- 将打散后的数据划分为若干mini-batch(每个大小通常为64或128)。
- 对每个mini-batch,计算PPO损失并用Adam更新网络参数。
- 重复步骤3-5共 \(K\) 个epoch。
- 丢弃所有数据,回到步骤1。
Multiple Epochs per Rollout
PPO允许对同一批数据进行多个epoch的更新(通常 \(K = 3 \sim 10\)),这是相对于原始策略梯度(只能更新一次)的巨大效率提升。
这之所以可行,是因为Clipping机制天然地限制了策略变化的幅度。即使我们在同一批数据上多次更新,策略也不会偏离旧策略太远。当概率比 \(r_t\) 超出 \([1-\epsilon, 1+\epsilon]\) 范围时,梯度会被截断为零,相当于自动停止更新。
但也不能设置太多epoch。经验上,\(K\) 太大会导致:
- 策略过拟合于当前这一批数据
- 概率比大面积被截断,有效梯度趋近于零,更新停滞
- KL散度过大,策略偏离旧策略太远
Advantage Normalization
在每个mini-batch中,通常会对优势函数进行标准化(Normalization):
其中 \(\mu\) 和 \(\sigma\) 分别是当前mini-batch中优势值的均值和标准差,\(\epsilon\) 是一个很小的数(如 \(10^{-8}\))防止除零。
标准化的好处是:
- 使得优势值的尺度不依赖于具体任务的奖励大小
- 大约一半的优势值为正(增大概率),一半为负(减小概率),梯度方向更均衡
- 显著提高训练稳定性
超参数
以下是PPO最重要的超参数及其常见取值:
| 超参数 | 符号 | 常见取值 | 说明 |
|---|---|---|---|
| 学习率 | \(\alpha\) | \(3 \times 10^{-4}\) | Actor和Critic通常使用相同学习率 |
| 截断系数 | \(\epsilon\) | \(0.2\) | 控制策略更新幅度,核心超参数 |
| 折扣因子 | \(\gamma\) | \(0.99\) | 未来奖励的折扣 |
| GAE参数 | \(\lambda\) | \(0.95\) | 控制偏差-方差权衡 |
| 更新轮数 | \(K\) | \(3 \sim 10\) | 每批数据的训练epoch数 |
| Mini-batch大小 | \(M\) | \(64\) | 每次梯度更新使用的样本数 |
| 值函数系数 | \(c_1\) | \(0.5\) | 值函数损失的权重 |
| 熵系数 | \(c_2\) | \(0.01\) | 熵奖励的权重 |
| 并行环境数 | \(N\) | \(8 \sim 128\) | 同时交互的环境副本数 |
| Rollout步数 | \(T\) | \(128 \sim 2048\) | 每个环境收集的步数 |
| 最大梯度范数 | - | \(0.5\) | 梯度裁剪,防止梯度爆炸 |
注意:这些值只是经验值,不同任务可能需要调整。其中 \(\epsilon\) 和学习率对性能影响最大。
PPO的训练流程
将上述所有内容整合,PPO的完整训练流程如下:
PPO算法伪代码:
1. 初始化策略网络 π_θ 和价值网络 V_ϕ(可共享底层参数)
2. for iteration = 1, 2, ... do
3. 用当前策略 π_θ_old 在 N 个并行环境中各运行 T 步
4. 收集轨迹数据:{(s_t, a_t, r_t, s_{t+1}, done_t, log π_θ_old(a_t|s_t))}
5. 用 V_ϕ 计算所有状态的价值估计
6. 用 GAE 计算所有时间步的优势估计 Â_t 和回报目标 R̂_t
7. for epoch = 1, ..., K do
8. 随机打散所有 N×T 个数据点
9. 将数据划分为大小为 M 的 mini-batch
10. for each mini-batch do
11. 计算当前策略下的 log π_θ(a_t|s_t)
12. 计算概率比 r_t = exp(log π_θ - log π_θ_old)
13. 计算截断代理目标 L^CLIP
14. 计算值函数损失 L^VF
15. 计算熵奖励 H
16. 总损失 L = -L^CLIP + c1 * L^VF - c2 * H
17. 反向传播,梯度裁剪,Adam更新
18. end for
19. end for
20. θ_old ← θ(更新旧策略参数)
21. end for
PPO vs TRPO
| 对比维度 | TRPO | PPO |
|---|---|---|
| 年份 | 2015 | 2017 |
| 约束方式 | 硬约束(KL散度 \(\leq \delta\)) | 软约束(Clipping截断) |
| 优化方法 | 二阶优化(共轭梯度 + 线搜索) | 一阶优化(Adam SGD) |
| 实现复杂度 | 极高(需要Fisher矩阵、共轭梯度) | 极低(核心代码不到50行) |
| 计算开销 | 大(每步需要多次共轭梯度迭代) | 小(标准的mini-batch SGD) |
| 理论保证 | 有严格的单调改进保证 | 没有严格保证,但实践中表现良好 |
| 网络架构兼容性 | 不兼容参数共享、Dropout等 | 完全兼容所有标准深度学习技术 |
| 性能 | 在大多数任务上两者接近 | 在大多数任务上两者接近 |
| 调参难度 | 较难(\(\delta\) 的选取敏感) | 简单(\(\epsilon = 0.2\) 几乎普适) |
| 工业界使用 | 很少 | 极其广泛 |
PPO的成功告诉我们一个深刻的道理:在工程实践中,简单而鲁棒的方法往往比理论上更优但实现复杂的方法更有价值。 TRPO提供了重要的理论洞察,而PPO将这些洞察转化为了人人都能使用的实用工具。
PPO的变体与补充
PPO-Penalty
除了PPO-Clip,原论文还提出了另一个变体PPO-Penalty,它不使用Clipping,而是直接将KL散度作为惩罚项加入目标函数:
其中 \(\beta\) 是一个自适应系数:
- 如果实际KL散度 \(> 1.5 \cdot d_{\text{targ}}\)(超过目标值太多),则 \(\beta \leftarrow 2\beta\)
- 如果实际KL散度 \(< d_{\text{targ}} / 1.5\)(远低于目标值),则 \(\beta \leftarrow \beta / 2\)
实践中,PPO-Clip的表现通常优于PPO-Penalty,因此PPO-Clip是默认选择。
PPO在RLHF中的应用
PPO在大语言模型的RLHF(Reinforcement Learning from Human Feedback)训练中扮演了核心角色。在这个场景中:
- 环境: 语言模型根据prompt生成回复
- 动作: 每个token的生成
- 奖励: 由人类偏好训练的Reward Model给出
- 策略: 语言模型本身就是策略网络
RLHF中的PPO还加入了一个额外的KL惩罚项,防止模型偏离预训练模型太远:
其中 \(\pi_{\text{ref}}\) 是SFT(Supervised Fine-Tuning)后的参考模型。
关于On-Policy的讨论
PPO是一个on-policy算法,这意味着每次更新后都必须重新收集数据。虽然通过Clipping和多epoch更新提高了样本利用率,但和SAC等off-policy算法相比,PPO的样本效率仍然较低。
然而,PPO有一个off-policy算法难以匹敌的优势:训练稳定性。 由于PPO不存在replay buffer中数据过期的问题(死亡三角),也不需要维护target network,其训练过程通常非常平稳,不容易出现DQN中常见的策略崩塌现象。
在实际选择中:
- 如果样本获取成本低(如仿真环境): PPO是首选,因为稳定可靠
- 如果样本获取成本高(如真实机器人): SAC是首选,因为样本效率高
- 如果动作空间是离散的: PPO天然支持,SAC需要额外改造