PPO (Proximal Policy Optimization)
OpenAI 2017 年提出。简单、稳定、高效,强化学习界的“瑞士军刀”。
遇事不决 PPO
OpenAI 的默认算法。它在连续控制(机器人)和离散控制(游戏)上都表现出色。
稳定性强
通过限制策略更新的幅度(Clip 机制),避免了传统策略梯度算法中因步长过大导致的崩溃。
实现简单
相比于它的前身 TRPO(需要求解复杂的约束优化),PPO 只需要标准的一阶优化器(如 Adam)。
预备知识:重要性采样 (Importance Sampling)
在展开 PPO 之前,我们需要理解一个统计学概念。假设我们需要计算分布 $p(x)$ 下函数 $f(x)$ 的期望,但我们很难直接从 $p(x)$ 中采样。 这时,我们可以从另一个容易采样的分布 $q(x)$ 中采样,并引入一个重要性权重来修正偏差。
其中 $\frac{p(x)}{q(x)}$ 就是重要性权重。这告诉我们:我们可以用“旧策略”产生的样本,来估计“新策略”的梯度,只要乘上这个权重即可。 但为了保证估计的方差不要太大,分布 $p$ 和 $q$ 不能差得太远,即权重应该接近 1。这正是 PPO 核心思想的数学基础。
核心机制:Clip (截断)
PPO 的前身 TRPO 使用复杂的 KL 散度约束来限制更新幅度。PPO 简化了这一点,直接在目标函数里进行 Clip(截断)。
定义概率比 $r_t(\theta) = \frac{\pi_\theta(a_t|s_t)}{\pi_{\theta_{old}}(a_t|s_t)}$。当策略没有变化时,$r_t(\theta) = 1$。 PPO 的损失函数试图最大化这一项,但如果不加限制,策略可能会更新过头。
Clip 机制可视化
我们希望 $r_t(\theta)$ 在 $(1-\epsilon, 1+\epsilon)$ 的安全范围内变动(通常 $\epsilon=0.2$)。如果超出这个范围,并且优势函数 $\hat{A}_t$ 指示我们应该进一步偏离时,PPO 会“切断”梯度,阻止更新。
关键技术:GAE (Generalized Advantage Estimation)
在 PPO 中,我们需要估计优势函数 $\hat{A}_t$,即“在当前状态下,动作 $a_t$ 比平均水平好多少”。 常用的方法是 GAE,它在“偏差”和“方差”之间做了完美的平衡。
- $\lambda = 0$ 时:$\hat{A}_t = \delta_t^V$,即传统的 TD Error。偏差大,方差小。
- $\lambda = 1$ 时:$\hat{A}_t$ 接近 Monte Carlo 估计。偏差小,方差大。
- 通常取 $\lambda = 0.95$,取得最佳平衡。
PPO 训练流程
1. 收集轨迹
使用旧策略 $\pi_{\theta_{old}}$ 在环境中运行 $T$ 步,收集 $(s, a, r, s')$。
2. 计算优势 (GAE)
利用 Critic 网络 $V(s)$ 计算 GAE 优势 $\hat{A}_t$ 和回报目标 $R_t$。
3. 多次更新 (Epochs)
将数据打乱(Shuffle)并分批(Batch),优化 Surrogate Loss。这里可以使用梯度下降更新多次,因为 Clip 机制保证了安全。
常见问题与思考
PyTorch 代码核心
def update(self): # 1. 计算 Monte Carlo 回报和 GAE 优势 rewards = [] discounted_reward = 0 for reward, is_terminal in zip(reversed(self.buffer.rewards), reversed(self.buffer.is_terminals)): if is_terminal: discounted_reward = 0 discounted_reward = reward + (self.gamma * discounted_reward) rewards.insert(0, discounted_reward) # ... 标准化 rewards / 计算 advantages ... # 2. PPO Epochs for _ in range(self.K_epochs): # 计算新策略的 log probs logprobs, state_values, dist_entropy = self.policy.evaluate(old_states, old_actions) # 计算比率 r(θ) ratios = torch.exp(logprobs - old_logprobs.detach()) # 计算 Surrogate Loss (Clip) surr1 = ratios * advantages surr2 = torch.clamp(ratios, 1-self.eps_clip, 1+self.eps_clip) * advantages # 总 Loss = Actor Loss + Critic Loss - Entropy Bonus loss = -torch.min(surr1, surr2) + 0.5 * self.MseLoss(state_values, rewards) - 0.01 * dist_entropy # 梯度下降 self.optimizer.zero_grad() loss.mean().backward() self.optimizer.step()