上篇聊了Actor-Critic框架和各种变体。如果你只能学一个强化学习算法,那一定是PPO(Proximal Policy Optimization,近端策略优化)。它是OpenAI在2017年提出的,现在几乎是RL领域的默认选择——不管是学术研究还是工业应用,PPO都是出场率最高的算法。

为什么PPO这么流行?因为它在性能和稳定性之间取得了非常好的平衡。不像DDPG那样容易崩溃,不像A2C那样样本效率低,也不像TRPO那样实现复杂。PPO的代码简洁,超参数不敏感,开箱即用。

策略更新的核心问题

在理解PPO之前,先搞清楚为什么策略更新这么难。

策略梯度方法用梯度上升来更新策略。问题是:更新步幅多大合适?步幅太小,训练太慢;步幅太大,新策略可能跟旧策略差太远,性能急剧下降,甚至崩溃。

这个问题在off-policy场景中更严重。如果你用旧策略采集的数据来更新新策略,新旧策略差距越大,数据的参考价值越低,梯度估计越不准确。如果不加限制,新策略可能在某次更新后变得很烂,然后用这个烂策略采到的数据来继续更新,越来越烂,恶性循环。

TRPO(Trust Region Policy Optimization)是PPO的前身,它用KL散度约束来限制每次更新的策略变化量。效果很好,但实现需要计算二阶优化(Fisher信息矩阵的逆),代码复杂,计算开销大。

PPO的核心思想:截断重要性采样比率

PPO的思路比TRPO简单得多。它用重要性采样比率(importance sampling ratio)来衡量新旧策略的差异:

r_t(θ) = π_θ(a_t|s_t) / π_θ_old(a_t|s_t)

如果新旧策略一样,r_t=1。如果新策略在某个动作上的概率比旧策略高,r_t>1;反之r_t<1。

PPO的目标函数是:

L = E[min(r_t · A_t, clip(r_t, 1-ε, 1+ε) · A_t)]

clip操作把r_t限制在[1-ε, 1+ε]范围内,ε通常取0.2。

# PPO的核心loss计算
# ratio = π_new(a|s) / π_old(a|s)
# surr1 = ratio * advantage
# surr2 = clip(ratio, 1-eps, 1+eps) * advantage
# loss_actor = -min(surr1, surr2).mean()
# loss_critic = (V(s) - return).pow(2).mean()

这个截断机制的直觉是:当优势函数A>0时(好动作),PPO增大该动作的概率,但r_t超过1+ε后就不再增大了;当A<0时(坏动作),PPO减小该动作的概率,但r_t低于1-ε后就不再减小了。

这相当于给每次策略更新设了一个"安全范围"。不管梯度怎么说,策略的变化幅度被限制住了。这避免了策略一步走太远导致的崩溃问题。

PPO的完整训练流程

PPO的训练是迭代进行的。每一轮迭代分两步:采样和更新。

采样阶段:用当前策略π_old在环境中跑N个episode(或者固定步数),收集所有的(s, a, r, s', log_prob, advantage)。这一步产生一批训练数据。

更新阶段:用这批数据做K个epoch的梯度更新。每个epoch中,随机采样mini-batch的数据,计算PPO的目标函数,更新Actor和Critic的参数。注意这里的数据是重复使用的——同一批数据用K次,这就是PPO能比纯策略梯度样本效率更高的原因。

更新完成后,π_old = π_new,丢弃旧数据,开始下一轮采样。

几个工程上的细节值得注意。价值函数也常用clip来稳定训练:V_clipped = V_old + clip(V - V_old, -ε, ε),防止Critic的更新幅度过大。网络初始化也有讲究——很多实现用正交初始化(Orthogonal Initialization)而不是默认的Xavier初始化,这对PPO的训练稳定性有帮助。学习率通常用线性衰减,从初始值逐步降到零。

Critic的loss可以加上价值函数的clip,也可以不加。实验表明加了之后训练更稳定,特别是在奖励尺度变化大的任务中。另外Critic的学习率通常比Actor大一些(比如Actor 3e-4,Critic 1e-3),因为Critic需要更快地跟上策略的变化。

# PPO训练循环伪代码
# for iteration in range(num_iterations):
#     data = collect_trajectories(policy_old, env)
#     advantages = compute_gae(data)
#     for epoch in range(K):  # 通常K=4-10
#         for batch in mini_batches(data):
#             ratio = policy(batch) / policy_old(batch)
#             loss = ppo_loss(ratio, batch.advantages)
#             optimizer.step()
#     policy_old = policy  # 同步旧策略

K和mini-batch大小是两个关键超参数。K太小数据利用不充分,太大会导致过拟合到这一批数据上(策略变化反而太大)。实践中K=4-10,mini-batch大小64-256是比较常见的选择。

PPO在机器人中的应用

PPO在机器人领域有非常广泛的应用。OpenAI Five用PPO训练Dota2 AI,达到了职业选手水平。OpenAI还用它训练机械臂做灵巧操作。在MuJoCo和Isaac Gym的各种机器人控制基准上,PPO几乎都是baseline。

PPO特别适合机器人的原因有几个。代码简单,调试方便。超参数不敏感,不需要花大量时间调参。训练稳定,不容易崩溃。在仿真环境中,PPO通常能在几小时内训练好一个行走或抓取策略。

在Sim2Real(仿真到真实迁移)场景中,PPO也是首选算法。配合域随机化(Domain Randomization),PPO训练的策略可以直接部署到真实机器人上。这是因为PPO训练出的策略通常比较"平滑"——由于截断机制限制了策略的剧烈变化,学到的策略对输入扰动有一定的鲁棒性。NVIDIA的Isaac Gym用PPO在几小时内就能训练出各种机器人的行走策略,然后通过Sim2Real直接部署到真实硬件上,整个过程不需要任何真实数据。

面试要点

PPO的面试考点非常明确。

clip机制的原理和作用。面试官一定会问为什么PPO要用clip,不用行不行。答案是:不限制策略变化幅度的话,策略更新可能一步走太远,导致性能崩溃。clip提供了一个简单有效的约束,实现成本低但效果好。跟TRPO的KL约束相比,clip是工程上的简化,效果接近但实现简单得多。

PPO是on-policy还是off-policy。严格来说PPO是on-policy的,因为它只用当前策略采集的数据。但它通过多epoch重复使用同一批数据,在一定程度上提高了样本效率。有人把它叫"准on-policy"——不像纯策略梯度那样数据用一次就扔,但也不像off-policy方法那样能用任意旧的数据。

PPO和SAC的对比。PPO是on-policy,样本效率低但训练稳定。SAC是off-policy,样本效率高但训练更复杂。在机器人仿真中,如果采样成本低(仿真跑得快),用PPO就好;如果采样成本高(真实机器人),用SAC更合适。

PPO的局限性也要知道。PPO的样本效率不如off-policy方法,在真实机器人上训练时这是个很大的限制。另外PPO在非常复杂的任务中(比如需要长时间规划和记忆的任务)可能不如SAC或者基于Transformer的方法。还有一个常见问题:PPO训练出的策略有时会在某个局部最优附近振荡,不能稳定地执行最优行为。这通常需要通过增大熵系数或者调整网络结构来缓解。

给你的建议

PPO是必须动手实现的算法。建议从Stable-Baselines3的PPO实现入手,先跑通几个经典环境(Humanoid、Ant、HalfCheetah),观察训练曲线。然后试着修改clip范围、学习率、GAE的λ等超参数,观察它们对训练的影响。

如果你想深入理解PPO,推荐读John Schulman的原论文和他的演讲slides。代码方面,CleanRL是一个很好的参考——它提供了单文件实现的PPO,代码量只有几百行,比Stable-Baselines3更容易读懂。


上一篇:第303篇 Actor-Critic方法详解

下一篇预告:第305篇 SAC——最大熵强化学习

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐