PPO 算法直觉
PPO 算法
Proximal Policy Optimization (PPO) 是一种强化学习(RL)算法,由 OpenAI 于 2017 年提出,用于在连续或离散动作空间中高效训练智能体。其核心思想是在保证学习稳定性的同时提升样本效率,是一种策略梯度方法的改进形式。
PPO算法类型
on-policy
PPO用当前策略网络生成的 rollout 来优化更新当前策略,旧数据很快过期,不能长期复用。训练过程中持续用新策略与环境交互采样,故属于 on-policy策略。但因为PPO允许用刚刚旧策略采样的数据对新策略做有限更新,所以有一点近似 off-policy 的形式。
stochastic policy gradient
PPO 使用随机策略梯度:Actor 参数化动作分布 \(\pi_\theta(a|s)\),在连续动作任务中通常输出均值和方差,并从分布中采样动作。优化目标是提高高 advantage 动作的概率、降低低 advantage 动作的概率,同时用 clip 约束策略更新幅度。
actor-critic
PPO 基于 actor-critic 结构,训练时依赖两个角色:Actor 负责输出动作策略并被 PPO clip objective 更新,Critic 负责估计状态价值 \(V(s)\),用来计算 advantage :评估 Actor 某个动作比预期好还是差。核心是“用 Critic 评价动作,用 policy gradient 更新 Actor”。
算法直觉(PPO with Clip)
PPO算法基于AC框架,维护两个网络:
AC是强化学习中连接 value-based 与 policy-based / policy-gradient 的重要框架。Actor负责学习策略,决定当前的动作;Critic负责评价动作的好坏,指导Actor的优化更新。因此,Actor-Critic 通常比纯 policy gradient 更稳定,也比单纯 value-based 方法更适合处理连续动作和复杂策略问题。
Critic network 学习 \(V(s)\)
PPO 算法的动作来自随机策略,所以 Critic 只需要评价状态好不好:故Critic 估计的是状态价值$ V(s)$,即当前状态整体的未来价值。

self.net = nn.Sequential(
nn.Linear(state_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, 1)
)
Actor network 学习策略 π

Actor 网络输入当前状态,先用 feature_extractor 提取特征。
再用 mean_head 输出连续动作的均值向量;同时用 log_std 输出标准差用于控制探索范围。
最后构造得到高斯策略:\(a_t \sim \pi_\theta(a_t \mid s_t)=\mathcal{N}(\mu_\theta(s_t),\sigma_\theta^2)\)
self.feature_extractor = nn.Sequential(
nn.Linear(state_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU()
)
# 输出动作均值:根据当前状态决定动作中心
self.mean_head = nn.Linear(hidden_dim, action_dim)
# 输出动作标准差:全局可学习参数,不依赖状态(图中属于依赖情况)
self.log_std = nn.Parameter(torch.zeros(action_dim))
前向传播:
features = self.feature_extractor(state)
# 连续动作均值,限制在 [-1, 1]
mean = torch.tanh(self.mean_head(features))
# 标准差必须为正
std = torch.exp(self.log_std)
构造策略
# 构造连续动作的高斯分布
dist = torch.distributions.Normal(mean, std)
1. 收集 rollout buffer
用 \(\pi_{old}\) 与环境交互收集数据
通过输入当前状态,用旧策略 \(\pi_{old}\) (Actor network)输出符合高斯分布的动作范围。
# 从分布中采样动作
action = dist.sample()
计算 log_prob_old
PPO 的 ratio 需要的是整个动作向量 a 在状态 s 下的概率,假设各动作维度独立,那么联合概率是各维概率相乘。取log 则变为求和。
# 计算log概率(对多维动作求和)
prob = dist.log_prob(action).sum(dim=-1)
用 Critic 计算状态价值函数 \(V(s)\)
value = critic(state).item()
与环境交互,完成采集 rollout buffer.
2. 计算 Advantage 、Return
PPO算法需要准确的优势函数 \(A(s,a)\) 来衡量动作的好坏。PPO算法引入了 GAE 方法,通过多步 TD 误差的衰减累加,在“Monte Carlo 的高方差”与“单步TD的高偏差”之间取平衡。成为了维持 PPO 训练平衡的关键。
单步 TD 误差(\(δₜ\))
仅用“一步真实奖励“估计当前时刻的真实回报与预测价值的差。
# TD误差
delta = rewards_buffer[i] + gamma * next_val * (1.0 - float(dones_buffer[i])) - values_buffer[i]
GAE 递归公式(核心)
从后往前累加多步 TD 误差,权重为 \((γλ)^l\),GAE 的求和也会在 done 处停止向后传播,不会越过 episode 边界。
# GAE
gae = delta + gamma * gae_lambda * (1.0 - float(dones_buffer[i])) * gae
终止状态处理(Done Flag)—— GAE 的计算会依据 done(包括 terminated 或 truncated)进行截断,若当前状态是终止状态(done=True),则下一状态价值 \(V(s_{t+1})=0\)
# (1.0 - float(dones_buffer[i]) :
done = terminated or truncated
dones_buffer.append(done)
以下公式显式展示 GAE 是多步 TD 误差的指数加权和。实际代码中,GAE 只在当前回合内累加(到终止状态为止)。
计算 return \(R_t\)
PPO 的 Critic 训练目标通常是 value target / return 的 MSE loss,return \(R_t\)通常由 GAE advantage 加上旧的 value 得到:
returns.insert(0, gae + values_buffer[i].item())
完整计算:
这段代码通过反向遍历缓冲区,用dones_buffer标记局结束点,从终止状态开始逐帧累加多步TD误差(GAE),最终得到每个状态的优势估计(advantages)和真实回报(returns),为PPO的策略更新提供稳定信号。
# 计算Advantage(GAE)
advantages = []
returns = []
gae = 0
# 从后往前算
for i in reversed(range(len(rewards_buffer))):
if i == len(rewards_buffer) - 1:
next_val = next_value
else:
next_val = values_buffer[i + 1].item()
# TD误差:δ = r + γV(s') - V(s)
delta = rewards_buffer[i] + gamma * next_val * (1.0 - float(dones_buffer[i])) - values[i].item()
# GAE:A = δ + γλA'
gae = delta + gamma * gae_lambda * (1.0 - float(dones_buffer[i])) * gae
advantages.insert(0, gae)
returns.insert(0, gae + values_buffer[i].item())
advantages = torch.FloatTensor(advantages).to(device)
returns = torch.FloatTensor(returns).to(device)
3. 基于这批 rollout 训练多个 epoch
PPO算法在一轮更新中反复使用同一批 rollout 数据训练多个 epoch 但是不会长期复用;更新完成后必须用新策略重新采样。
4. 构造 Actor loss
用 \(\pi_{new}\) 与环境交互计算 log_prob_new
重新计算 rollout 旧数据 同一个状态s和同一个动作a
# ===== 计算新策略的log概率(连续动作) =====
mean, std = actor(batch_states)
new_dist = torch.distributions.Normal(mean, std)
# 对多维动作的log_prob求和
new_log_probs = new_dist.log_prob(batch_actions).sum(dim=-1)
构造优化目标 —— 基于重要性采样(Importance Sampling)
PPO 算法的核心。重要性采样通过“新旧策略概率比值(ratio)”修正数据分布差异,让旧数据能被新策略使用。
为什么有效:这种粗暴的截断,在数学上被证明能够很好地替代 TRPO 的信任区域约束,而且完全不需要计算复杂的二阶导数,只用最基本的梯度下降(一阶优化)就能轻松实现。
ratio更好的地方在于把“变化幅度”显式算出来了。
这个 \(r_t(θ)\)代表了 Actor network 新策略和旧策略产生某一动作的概率比值。如果 r=1,说明新旧策略一样;如果 r偏离 1 太多,说明策略发生了剧变。
ratio = torch.exp(new_log_probs - batch_old_log_probs)
原始目标 surrogate 1
根据 advantage 和新旧策略概率比,决定这个动作该被加强还是削弱
surr1 = ratio * batch_advantages
裁剪后的目标 surrogate 2
限制 ratio 变化幅度,设置上下界对r幅度进行裁剪,防止策略改太猛。
surr2 = torch.clamp(ratio, 1.0 - clip_epsilon, 1.0 + clip_epsilon) * batch_advantages
优化目标
surr1 不限制更新,surr2 限制更新。通过min实现始终取更保守的作为目标函数。正 advantage 时,min 限制“概率涨太多”;负 advantage 时,因为乘了负数,min 反过来限制“概率降太多”。
目的是约束最大化代理目标函数的更新
最大化目标如下:
actor_loss = -torch.min(surr1, surr2).mean()
- 通过配置 clip_epsilon 限制 ratio 的变化范围
- \(E_t\) 对一批 transition (batch size) 求平均。即代码中的 .mean()
5. 熵正则项 Entropy loss(可选)
熵可以计算策略的信息量。熵越大,说明策略越随机,探索越强;熵越小,说明策略越确定,动作分布越集中。
通过给 Actor loss 减去一个熵值作为其正则项,最小化这个loss时模型会倾向于让熵变大一点,也就避免了模型太早变得过于确定。
# 熵奖励(鼓励探索)
entropy = new_dist.entropy().sum(dim=-1).mean()
actor_loss = actor_loss - entropy_coef * entropy
6. 构造 Critic loss
Critic 预测:这个状态未来大概值多少
return \(R_t\) 目标:根据 rollout reward 和 GAE 算出来的目标值
通过 MSE loss 实现凸优化 Critic loss
为什么不是直接对 GAE Advantage 优化:因为PPO 会对同一批 rollout 训练多个 epoch,critic 参数一直在变,所以后面:
因此 critic loss 里的差值就不再是原来的 \(A_t^{GAE}\),而是 value prediction error
# Critic损失
batch_values = critic(batch_states).squeeze()
critic_loss = nn.MSELoss()(batch_values, batch_returns)
7. 总 loss
value_coef 是控制“Critic loss”在 PPO 总 loss 里有多重要的超参数,太大训练会更重视 Critic
entropy_coef 控制探索奖励的强度,太大容易一直乱试,太小几乎没效果。
total_loss = actor_loss + value_coef * critic_loss - entropy_coef * entropy
梯度裁剪
梯度爆炸的原理是:反向传播时梯度会经过链式法则连续相乘,如果某些局部导数、loss 数值、advantage、value error 或 log_prob 的变化很大,乘出来的梯度就会变得特别大,导致一次 optimizer.step() 把网络参数改得过猛。
通俗理解就是:模型本来只该小步调整,但因为这批数据算出来的“错误信号”太强,参数被猛推了一大步,结果策略分布、value 预测都突然变坏,训练开始震荡甚至崩掉。连续动作里如果标准差很小,动作概率密度会对均值变化特别敏感,所以 log_prob 和 ratio 的梯度也更容易突然变大。梯度裁剪就是在参数更新前检查“这一步推力是否太大”,太大就按比例缩小,保证每次更新别跨太远。
# ===== 反向传播 + 梯度裁剪 =====
actor_optimizer.zero_grad()
critic_optimizer.zero_grad()
total_loss.backward()
# 梯度裁剪(防止梯度爆炸,连续动作很重要)
nn.utils.clip_grad_norm_(actor.parameters(), max_grad_norm)
nn.utils.clip_grad_norm_(critic.parameters(), max_grad_norm)
actor_optimizer.step()
critic_optimizer.step()
用新的 Actor 采集下一批 rollout
算法流程图
GPT制作 仅作参考

引用
-
Schulman, J., Wolski, F., Dhariwal, P., Radford, A. and Klimov, O., 2017. Proximal policy optimization algorithms. arXiv preprint arXiv:1707.06347.
-
Schulman, J., Levine, S., Moritz, P., Jordan, M. and Abbeel, P., 2015. Trust region policy optimization. In: Proceedings of the 32nd International Conference on Machine Learning, pp.1889–1897.

浙公网安备 33010602011771号