2017 年,OpenAI 团队发表《Proximal Policy Optimization Algorithms》,没有提出颠覆性的全新理论,却在此后数年间彻底改变了强化学习的落地格局 —— 从机器人控制、游戏 AI 到大模型 RLHF,PPO(近端策略优化)几乎成了所有场景的默认基线算法。
它的核心贡献可以用一句话概括:用最简单的一阶梯度下降,实现了接近 TRPO(信任域策略优化)的稳定训练效果,同时兼顾了易实现性、通用性与样本效率。
本文将带你从零开始,完整吃透 PPO 的原理推导、算法流程、代码实战与调参技巧,并附上全套官方资料与优质开源资源,读完即可实现从入门到落地的完整掌握。
一、论文官方资料汇总
先附上论文的完整官方信息与获取渠道,方便随时对照原文精读。
1. 论文基础信息
| 论文标题 | Proximal Policy Optimization Algorithms |
| 作者团队 | John Schulman, Filip Wolski, Prafulla Dhariwal, Alec Radford, Oleg Klimov(OpenAI) |
| 发表时间 | 2017 年 7 月(v1 预印本),2017 年 8 月(v2 修订版) |
| arXiv 编号 | arXiv:1707.06347 |
| 研究领域 | 深度强化学习・策略梯度优化算法 |
2. 论文下载链接
-
arXiv 摘要页(最稳定,推荐):https://arxiv.org/abs/1707.06347
-
arXiv 直接 PDF 下载:https://arxiv.org/pdf/1707.06347
-
OpenAI 官方原版 PDF(博客配套版):https://openai-public.s3-us-west-2.amazonaws.com/blog/2017-07/ppo/ppo-arxiv.pdf
注:该链接为 AWS S3 存储,部分网络环境可能无法直接访问。
3. 配套官方资源
-
OpenAI 官方介绍博客:https://blog.openai.com/openai-baselines-ppo/(境外网页,部分网络环境可能无法访问)
-
官方基线代码(PPO2 原版实现,TensorFlow 1.x):https://github.com/openai/baselines/tree/master/baselines/ppo2
-
SpinningUp 官方教程解读:https://spinningup.openai.com/en/latest/algorithms/ppo.html(境外网页,部分网络环境可能无法访问)
二、前置知识:PPO 要解决的核心问题
在 PPO 出现之前,深度强化学习已有三条主流技术路线,但每一条都存在致命短板,这正是 PPO 诞生的背景。
1. 三类方法的各自困境
(1)价值类方法(DQN 为代表)
在 Atari 等离散动作游戏上表现惊艳,但天然难以适配连续控制场景(机械臂、机器人行走);且函数逼近下收敛性缺乏理论保证,很多简单任务上都会训练失效。
(2)原生策略梯度(Vanilla PG)
直接沿策略梯度方向更新参数,逻辑简单但问题突出:
-
一批样本通常只做一次梯度更新,样本利用率极低;
-
步长难以把控:步长太大容易策略崩溃,步长太小收敛极慢;
-
训练过程方差大,结果可复现性差。
复用数据在理论上并非不可行,但会导致新旧策略差异过大,重要性采样的偏差急剧放大,梯度估计不再有效,最终极易训练崩溃。这也是原生策略梯度在实践中通常一批数据只做一次更新的核心原因。
(3)信任域策略优化(TRPO)
通过引入 KL 散度信任域约束,保证策略每次更新都不会偏离过远,实现了单调性能提升。但工程实现极其复杂,需要共轭梯度法求解约束优化问题,且不兼容 Dropout、参数共享等深度学习常用技巧,落地门槛极高。
PPO 的核心目标,就是解决这一矛盾:保留 TRPO 的稳定性优势,同时用普通的 SGD/Adam 就能完成训练。
2. 必须理解的基础概念
在进入 PPO 原理前,先明确三个核心概念:
-
策略 π_θ:由参数 θ 决定的函数,输入状态 s,输出动作 a 的概率分布;
-
优势函数 Â_t:表示在状态 s_t 下执行动作 a_t,比平均水平好多少。正值说明动作优于平均,负值说明差于平均;
-
重要性采样:用旧策略采集的样本,去估计新策略的梯度,是复用数据、提升样本效率的核心手段。
三、PPO 核心原理:裁剪目标为什么这么有效
PPO 的核心设计思路非常朴素:既然直接最大化策略目标会导致更新幅度过大,那就直接给目标函数加上一个「悲观下界」,从目标层面主动限制策略的更新步长。
1. 起点:保守策略迭代(CPI)目标
策略梯度的重要性采样形式,也叫保守策略迭代(Conservative Policy Iteration, CPI)目标,公式如下:
L
C
P
I
(
θ
)
=
E
^
t
[
π
θ
(
a
t
∣
s
t
)
π
θ
old
(
a
t
∣
s
t
)
A
^
t
]
=
E
^
t
[
r
t
(
θ
)
A
^
t
]
L^{CPI}(\\theta) = \\hat{\\mathbb{E}}_t\\left[ \\frac{\\pi_\\theta(a_t|s_t)}{\\pi_{\\theta_{\\text{old}}}(a_t|s_t)} \\hat{A}_t \\right] = \\hat{\\mathbb{E}}_t\\left[ r_t(\\theta) \\hat{A}_t \\right]
LCPI(θ)=E^t[πθold(at∣st)πθ(at∣st)A^t]=E^t[rt(θ)A^t]
其中概率比
r
t
(
θ
)
=
π
θ
(
a
t
∣
s
t
)
π
θ
old
(
a
t
∣
s
t
)
r_t(\\theta) = \\frac{\\pi_\\theta(a_t|s_t)}{\\pi_{\\theta_{\\text{old}}}(a_t|s_t)}
rt(θ)=πθold(at∣st)πθ(at∣st)。
当 θ = θ_old 时,r_t = 1;策略更新幅度越大,r_t 偏离 1 越远。
问题所在:如果直接对这个目标做多轮梯度更新,r_t 会严重偏离 1,导致策略更新幅度过大,训练直接崩溃。
2. 核心突破:裁剪替代目标(PPO-Clip)
PPO 最经典的版本,就是通过裁剪概率比,给更新幅度加上「安全锁」,目标函数为:
L
C
L
I
P
(
θ
)
=
E
^
t
[
min
(
r
t
(
θ
)
A
^
t
,
;
clip
(
r
t
(
θ
)
,
1
−
ϵ
,
1
+
ϵ
)
A
^
t
)
]
L^{CLIP}(\\theta) = \\hat{\\mathbb{E}}_t\\left[ \\min\\left( r_t(\\theta)\\hat{A}_t,; \\text{clip}(r_t(\\theta), 1-\\epsilon, 1+\\epsilon)\\hat{A}_t \\right) \\right]
LCLIP(θ)=E^t[min(rt(θ)A^t,;clip(rt(θ),1−ϵ,1+ϵ)A^t)]
其中 ε 是裁剪超参数。
这个公式的设计非常精巧,我们分两种情况拆解:
情况 1:优势为正(Â_t > 0,动作优于平均)
-
当 r_t 在 [1, 1+ε] 区间内:不裁剪,正常放大该动作的概率;
-
当 r_t > 1+ε:概率比被截断在 1+ε,即使梯度继续更新,目标也不会再提升,失去继续增大概率的动力。
直观理解:好动作的收益有上限,防止为了一个好动作把策略改得太激进。
情况 2:优势为负(Â_t < 0,动作差于平均)
-
当 r_t 在 [1-ε, 1] 区间内:不裁剪,正常降低该动作的概率;
-
当 r_t < 1-ε:概率比被截断在 1-ε,即使梯度继续更新,目标也不会再变差,避免坏动作的惩罚被无限放大。
直观理解:坏动作的惩罚有下限,防止因为一个坏动作把策略改得面目全非。
外层 min 的意义:悲观下界
最终目标取裁剪后与未裁剪的最小值,其精确逻辑是:
-
当裁剪后的值小于未裁剪的值(即裁剪限制了目标提升),我们取裁剪值,接受这一限制;
-
当裁剪后的值大于未裁剪的值(即裁剪试图改善目标,例如负优势场景),我们取未裁剪值,拒绝这种虚假的改善。
这保证了最终目标始终是未裁剪目标的一个悲观下界(pessimistic lower bound),从而策略更新永远不会因为步长过大而导致性能意外下降。
3. 备选方案:自适应 KL 惩罚版
论文同时提出了另一种思路:在目标中加入 KL 散度惩罚项,并根据实际 KL 散度自适应调整惩罚系数 β,让每次更新的 KL 散度维持在目标值 d_targ 附近。
惩罚系数 β 的更新规则(论文第 4 节):
-
若实际 KL < d_targ / 1.5,说明更新太保守,β 减半;
-
若实际 KL > d_targ × 1.5,说明更新过大,β 加倍。
但实验结果表明(论文 Table 1),KL 惩罚版本的整体效果不如裁剪版本。因此业界后续提到 PPO,默认指的都是裁剪版(PPO-Clip)。
4. 完整损失函数
实际工程中,PPO 通常和 Actor-Critic 框架结合,完整损失由三部分构成(论文公式 9):
L
t
C
L
I
P
+
V
F
+
S
(
θ
)
=
E
^
t
[
L
t
C
L
I
P
(
θ
)
−
c
1
L
t
V
F
(
θ
)
+
c
2
S
[
π
θ
]
(
s
t
)
]
L_t^{CLIP+VF+S}(\\theta) = \\hat{\\mathbb{E}}_t\\left[ L_t^{CLIP}(\\theta) – c_1 L_t^{VF}(\\theta) + c_2 S[\\pi_\\theta](s_t) \\right]
LtCLIP+VF+S(θ)=E^t[LtCLIP(θ)−c1LtVF(θ)+c2S[πθ](st)]
三部分分别是:
策略损失(L_CLIP):裁剪替代目标,用来更新策略网络(Actor);
价值损失(L_VF):价值网络(Critic)的均方误差
(
V
θ
(
s
t
)
−
V
t
t
a
r
g
)
2
(V_\\theta(s_t) – V_t^{targ})^2
(Vθ(st)−Vttarg)2,用来拟合状态价值,计算优势函数;
熵奖励(S):策略熵的加权项,鼓励探索,避免策略过早收敛到确定性动作。
其中 c1、c2 为权重系数:
-
共享参数的网络结构中 c1 常用 0.5~1.0,Atari 实验中 c1=1;
-
c2 在 Atari 等离散动作任务中常用 0.01,连续控制基准实验中论文未使用熵奖励(系数为 0),实际工程中可根据探索需求设置更小值(如 0.001)。
5. 广义优势估计(GAE)
优势函数的估计质量直接影响训练效果。论文采用广义优势估计(Generalized Advantage Estimation, GAE),通过参数 λ 平衡偏差与方差(论文公式 11):
A
^
t
=
δ
t
+
(
γ
λ
)
δ
t
+
1
+
⋯
+
(
γ
λ
)
T
−
t
+
1
δ
T
−
1
\\hat{A}_t = \\delta_t + (\\gamma\\lambda)\\delta_{t+1} + \\cdots + (\\gamma\\lambda)^{T-t+1}\\delta_{T-1}
A^t=δt+(γλ)δt+1+⋯+(γλ)T−t+1δT−1
其中单步 TD 误差为:
δ
t
=
r
t
+
γ
V
(
s
t
+
1
)
−
V
(
s
t
)
\\delta_t = r_t + \\gamma V(s_{t+1}) – V(s_t)
δt=rt+γV(st+1)−V(st)
对于固定长度的截断轨迹(horizon T 远小于 episode 长度),轨迹末端并非终止状态,必须用 V (s_T) 作为未来收益的 bootstrap 估计,不能直接设为 0。
常用配置为 γ=0.99、λ=0.95,在大多数任务上都能取得不错的效果。
四、完整算法流程与超参数指南
1. 标准 PPO 算法伪代码(Actor-Critic 风格)
输入:初始策略与价值网络参数 θ
循环(每次迭代):
0. 锁定旧参数:θ_old ← θ(本次迭代全程保持不变)
1. 采样阶段:N 个并行环境各运行 T 步,用 π_θ_old 收集 {s_t, a_t, r_t, log_prob_old, dones_t}
2. 估值阶段:用价值网络计算各时间步的 GAE 优势 Â_t 与价值目标 V_targ
3. 优化阶段:
将 NT 条数据打乱,分成多个小批量
对损失函数执行 K 轮小批量梯度下降(等价于最大化裁剪替代目标)
4. 进入下一轮迭代
这种「采样一次、更新多轮」的模式,大幅提升了样本利用效率,同时裁剪目标保证了多轮更新不会破坏策略稳定性。
2. 关键超参数详解与调参指南
| 裁剪系数 ε | 0.2(MuJoCo 连续控制) / 0.1(Atari,随训练线性退火) | 控制策略更新幅度的核心参数。0.2 是通用基准值;任务简单可适当调大加快收敛;任务复杂易崩溃可调小增强稳定性。 |
| 学习率 α | 3e-4(MuJoCo 连续控制) / 2.5e-4 × α(Atari,α 线性退火) | 通用默认值。训练震荡可适当调小;收敛太慢可适当调大。大模型 RLHF 场景通常会小一个数量级。 |
| 单轮采样步长 T | 2048(MuJoCo 连续控制) / 128(Atari) | T 越大优势估计越准,但单轮训练时间越长。通常取 2 的幂次,配合并行环境数保证总 batch 大小合适。 |
| 更新轮数 K | 10(MuJoCo 连续控制) / 3(Atari) | 采样数据复用的次数。K 越大样本效率越高,但越容易过拟合、破坏裁剪约束。简单任务可设大,复杂任务设小。 |
| 小批量大小 M | 64(MuJoCo 连续控制) / 32×8=256(Atari,8 为并行环境数) | 每次梯度更新的样本量。根据显存调整,通常取 32~2048。 |
| 折扣因子 γ | 0.99(两者同) | 远期奖励的衰减系数。长周期任务可调高(如 0.995),短周期可调低(如 0.95)。 |
| GAE 参数 λ | 0.95(两者同) | 平衡优势估计的偏差与方差。0.95 是通用取值,一般不需要大幅调整。 |
| 价值损失系数 c1 | 1.0(共享网络时) | 价值损失的权重。策略与价值网络共享参数时常用 0.5~1.0;不共享时可不使用。 |
| 熵系数 c2 | 0.01(Atari) / 0(MuJoCo 连续控制,论文未使用) | 控制探索程度。过早收敛可调大,训练不稳定可调小。连续控制中若需要探索可适当设小值(如 0.001)。 |
注:表中连续控制参数对应 MuJoCo 低维基准任务;高维复杂任务(如 Roboschool 人形机器人)需大幅调整批量、更新轮数与学习率策略(论文 Table 4 中批量为 4096、更新轮数 15、学习率基于 KL 自适应)。
五、动手实战:从调用库到手写实现
1. 5 分钟上手:用 Stable Baselines3 快速训练
Stable Baselines3(SB3)是目前工业界最主流、维护最活跃的 PyTorch 强化学习库,内置了经过大量验证的 PPO 实现,开箱即用。
步骤 1:环境安装
pip install stable-baselines3 gymnasium
步骤 2:完整训练代码(CartPole 倒立摆)
import gymnasium as gym
from stable_baselines3 import PPO
from stable_baselines3.common.evaluation import evaluate_policy
# 1. 创建环境
env = gym.make("CartPole-v1", render_mode="human")
# 2. 初始化 PPO 模型
model = PPO(
"MlpPolicy",
env,
learning_rate=3e-4,
n_steps=2048,
batch_size=64,
n_epochs=10,
gamma=0.99,
gae_lambda=0.95,
clip_range=0.2,
ent_coef=0.01,
verbose=1
)
# 3. 训练 10 万步
model.learn(total_timesteps=100_000)
# 4. 保存模型
model.save("ppo_cartpole")
# 5. 评估模型
mean_reward, std_reward = evaluate_policy(model, env, n_eval_episodes=10)
print(f"平均奖励: {mean_reward:.2f} ± {std_reward:.2f}")
# 6. 可视化测试
obs, _ = env.reset()
for _ in range(1000):
action, _states = model.predict(obs, deterministic=True)
obs, reward, terminated, truncated, info = env.step(action)
if terminated or truncated:
obs, _ = env.reset()
env.close()
只需十几行代码,就能完成一个完整的 PPO 训练与测试。替换不同环境 ID(如连续控制的 Pendulum-v1、Atari 游戏),即可快速适配不同任务。
2. 深度吃透:PyTorch 极简手写 PPO
如果想彻底吃透 PPO 核心逻辑,建议从零手写一个极简版本,重点掌握四个核心模块。以下是核心逻辑片段(离散动作版):
(1)Actor-Critic 网络定义
import torch
import torch.nn as nn
import torch.optim as optim
from torch.distributions import Categorical
class ActorCritic(nn.Module):
def __init__(self, state_dim, action_dim, hidden_dim=64):
super().__init__()
# 策略网络(Actor)
self.actor = nn.Sequential(
nn.Linear(state_dim, hidden_dim),
nn.Tanh(),
nn.Linear(hidden_dim, hidden_dim),
nn.Tanh(),
nn.Linear(hidden_dim, action_dim)
)
# 价值网络(Critic)
self.critic = nn.Sequential(
nn.Linear(state_dim, hidden_dim),
nn.Tanh(),
nn.Linear(hidden_dim, hidden_dim),
nn.Tanh(),
nn.Linear(hidden_dim, 1)
)
def get_action_and_value(self, state, action=None):
logits = self.actor(state)
dist = Categorical(logits=logits)
if action is None:
action = dist.sample()
log_prob = dist.log_prob(action)
entropy = dist.entropy()
value = self.critic(state).squeeze(–1)
return action, log_prob, entropy, value
(2)GAE 优势计算函数(截断轨迹标准实现)
def compute_gae(rewards, values, dones, gamma=0.99, gae_lambda=0.95):
"""
输入维度说明(固定长度截断轨迹):
– rewards, dones: 长度 T,对应 t=0 ~ T-1 步的奖励与终止标记
dones[t] = 1 表示第 t 步执行后 episode 终止,s_{t+1} 为终止状态
– values: 长度 T+1,对应 V(s_0) ~ V(s_T) 的状态价值估计
末尾 V(s_T) 用于截断处的 bootstrap,非终止态
"""
advantages = []
last_advantage = 0.0
# 从后往前反向计算
for t in reversed(range(len(rewards))):
# 下一状态价值:终止态为0,否则取 values[t+1](含截断处 V(s_T))
next_value = values[t+1] * (1.0 – dones[t])
# 单步TD误差
delta = rewards[t] + gamma * next_value – values[t]
# 递推GAE
last_advantage = delta + gamma * gae_lambda * (1.0 – dones[t]) * last_advantage
advantages.insert(0, last_advantage)
advantages = torch.tensor(advantages, dtype=torch.float32)
returns = advantages + torch.tensor(values[:–1], dtype=torch.float32)
# 优势归一化(工程常用技巧,稳定训练)
advantages = (advantages – advantages.mean()) / (advantages.std() + 1e-8)
return advantages, returns
(3)PPO 裁剪损失更新
def ppo_update(policy, optimizer, states, actions, old_log_probs, advantages, returns,
clip_eps=0.2, vf_coef=0.5, ent_coef=0.01, n_epochs=4):
"""
注:完整工程实现中,每个 epoch 前需随机打乱样本顺序,
并拆分为多个小批量更新,避免数据相关性影响训练效果。
"""
for _ in range(n_epochs):
_, new_log_probs, entropy, new_values = policy.get_action_and_value(states, actions)
# 概率比
ratio = torch.exp(new_log_probs – old_log_probs)
# 裁剪损失
surr1 = ratio * advantages
surr2 = torch.clamp(ratio, 1 – clip_eps, 1 + clip_eps) * advantages
policy_loss = –torch.min(surr1, surr2).mean()
# 价值损失
value_loss = ((new_values – returns) ** 2).mean()
# 熵奖励(负号因为要最大化熵,总损失做梯度下降)
entropy_loss = –entropy.mean()
# 总损失
loss = policy_loss + vf_coef * value_loss + ent_coef * entropy_loss
optimizer.zero_grad()
loss.backward()
optimizer.step()
完整手写一遍上述核心逻辑,再跑通 CartPole 环境,对 PPO 的理解会远高于只调用封装库。
六、PPO 的主流衍生与落地场景
1. 经典衍生变体
| PPO-Clip | 标准裁剪版,论文主推方案 | 绝大多数通用场景,默认首选 |
| PPO-Penalty | 自适应 KL 惩罚版,更新更保守 | 对稳定性要求极高的场景 |
| Dual-Clip PPO | 双裁剪机制,对负优势额外限制 | 奖励波动大、噪声强的工业场景 |
| MAPPO / IPPO | 多智能体版本,中心化训练去中心化执行 | 多智能体博弈、集群机器人、游戏 AI |
| GRPO | 分组相对 PPO,去掉 Critic,组内归一化优势 | 大模型推理对齐、数学 / 代码生成 |
| PPO-LSTM / PPO-Transformer | 骨干网络替换为循环 / 注意力结构 | 部分可观测、长时序依赖场景 |
2. 核心落地领域
-
大语言模型对齐(RLHF):ChatGPT 为代表的大模型,RLHF 阶段的核心优化算法就是 PPO,后续衍生的 GRPO 也源于 PPO 框架;
-
机器人控制与仿真:机械臂抓取、四足 / 人形机器人运动、无人机控制,是仿真环境的默认基线算法;
-
游戏 AI 与智能 NPC:从 Atari 单机游戏到 MOBA、RTS 多人竞技游戏的 AI 开发;
-
推荐系统与计算广告:优化用户长期留存、生命周期价值等长期目标;
-
资源调度与运营优化:算力调度、物流路径、生产排程等复杂序列决策问题。
七、精选学习资源与开源仓库
1. 官方权威资料
原论文:https://arxiv.org/abs/1707.06347
OpenAI Baselines 原版实现(TensorFlow 1.x):https://github.com/openai/baselines/tree/master/baselines/ppo2
SpinningUp 官方教程:https://spinningup.openai.com/en/latest/algorithms/ppo.html(境外网页,部分网络环境可能无法访问)
2. 优质开源实现分类推荐
工业落地首选
-
Stable Baselines3(PyTorch,工业级标准):https://github.com/DLR-RM/stable-baselines3
-
Ray RLlib(分布式大规模训练):https://github.com/ray-project/ray/tree/master/rllib
源码学习首选
-
CleanRL(单文件无封装,最适合精读):https://github.com/vwxyzjn/cleanrl
-
PPO-PyTorch(极简入门版,核心逻辑清晰):https://github.com/nikhilbarhate99/PPO-PyTorch
大模型 RLHF 专用
-
Hugging Face TRL(Transformers 生态官方 RL 库):https://github.com/huggingface/trl
-
DeepSpeed-Chat(千亿级模型分布式 PPO):https://github.com/microsoft/DeepSpeedExamples/tree/master/applications/DeepSpeed-Chat
-
OpenRL(国产框架,兼容传统 RL 与大模型 RL):https://github.com/OpenRL-Lab/openrl
多智能体专用
-
MAPPO 官方实现:https://github.com/marlbenchmark/on-policy
-
EPyMARL 基准仓库:https://github.com/uoe-agents/epymarl
3. 进阶学习路径建议
打基础:弄懂 MDP、策略梯度、Actor-Critic、优势函数的核心概念;
读论文:精读第 3 章裁剪目标与第 5 章算法流程,对照公式推导理解;
跑通样例:用 SB3 跑通 2~3 个经典环境,建立直观认知;
手写复现:从零实现极简 PPO,对照论文细节调试验证;
场景实践:针对自己的业务 / 研究方向,在自定义环境中调参优化;
进阶延伸:学习 MAPPO、GRPO 等变体,拓展到多智能体或大模型场景。

