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)$,即当前状态整体的未来价值。

image_1

    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 学习策略 π

image 1_1

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.

\[(s_t, a_t, r_t, s_{t+1}, done, log\_ prob_{old}, V(s_t)_{Critic}) \]

2. 计算 Advantage 、Return

PPO算法需要准确的优势函数 \(A(s,a)\) 来衡量动作的好坏。PPO算法引入了 GAE 方法,通过多步 TD 误差的衰减累加,在“Monte Carlo 的高方差”与“单步TD的高偏差”之间取平衡。成为了维持 PPO 训练平衡的关键。

单步 TD 误差(\(δₜ\)

仅用“一步真实奖励“估计当前时刻的真实回报与预测价值的差。

\[\delta_t = r_t + \gamma V(s_{t+1}) - V(s_t) \]

	  # TD误差
		delta = rewards_buffer[i] + gamma * next_val * (1.0 - float(dones_buffer[i])) - values_buffer[i]

GAE 递归公式(核心)

从后往前累加多步 TD 误差,权重为 \((γλ)^l\),GAE 的求和也会在 done 处停止向后传播,不会越过 episode 边界。

\[A_t^{\text{GAE}} = \delta_t + \gamma \lambda A_{t+1}^{\text{GAE}} \]

     # 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\)

\[V(s_{t+1}) = \begin{cases} 0 & \text{if } s_{t+1} \text{ is terminal} \\ V(s_{t+1}) & \text{otherwise} \end{cases} \]

		# (1.0 - float(dones_buffer[i]) :
		done = terminated or truncated
		dones_buffer.append(done)

以下公式显式展示 GAE 是多步 TD 误差的指数加权和。实际代码中,GAE 只在当前回合内累加(到终止状态为止)。

\[A_t^{\text{GAE}} = \sum_{l=0}^{\infty} (\gamma \lambda)^l \delta_{t+l} \]

计算 return \(R_t\)

PPO 的 Critic 训练目标通常是 value target / return 的 MSE loss,return \(R_t\)通常由 GAE advantage 加上旧的 value 得到:

\[R_t=A_t+V_{old}(s_t) \]

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 的信任区域约束,而且完全不需要计算复杂的二阶导数,只用最基本的梯度下降(一阶优化)就能轻松实现。

\[r_t(\theta) = \frac{\pi_\theta(a_t | s_t)}{\pi_{old}(a_t | s_t)} \]

ratio更好的地方在于把“变化幅度”显式算出来了。

这个 \(r_t(θ)\)代表了 Actor network 新策略和旧策略产生某一动作的概率比值。如果 r=1,说明新旧策略一样;如果 r偏离 1 太多,说明策略发生了剧变。

ratio = torch.exp(new_log_probs - batch_old_log_probs)

原始目标 surrogate 1

根据 advantage 和新旧策略概率比,决定这个动作该被加强还是削弱

\[surr1=r_t(θ)A_t \]

 surr1 = ratio * batch_advantages

裁剪后的目标 surrogate 2

限制 ratio 变化幅度,设置上下界对r幅度进行裁剪,防止策略改太猛。

\[surr2=clip(r_t(θ),1−ϵ,1+ϵ)A_t \]

    surr2 = torch.clamp(ratio, 1.0 - clip_epsilon, 1.0 + clip_epsilon) * batch_advantages      

优化目标

surr1 不限制更新,surr2 限制更新。通过min实现始终取更保守的作为目标函数。正 advantage 时,min 限制“概率涨太多”;负 advantage 时,因为乘了负数,min 反过来限制“概率降太多”。
目的是约束最大化代理目标函数的更新

最大化目标如下:

\[L^{CLIP}(θ)=E_t[min(surr1,surr2)] \]

		actor_loss = -torch.min(surr1, surr2).mean()
  • 通过配置 clip_epsilon 限制 ratio 的变化范围
  • \(E_t\) 对一批 transition (batch size) 求平均。即代码中的 .mean()

5. 熵正则项 Entropy loss(可选)

熵可以计算策略的信息量。熵越大,说明策略越随机,探索越强;熵越小,说明策略越确定,动作分布越集中。

\[H(π) \]

通过给 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

\[L^{VF}(θ)=1/N∑(V_θ(s_t)−R_t)^2 \]

为什么不是直接对 GAE Advantage 优化:因为PPO 会对同一批 rollout 训练多个 epoch,critic 参数一直在变,所以后面:

\[V_θ(s_t) ≠ V_{old}(s_t) \]

因此 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

\[L = L_{actor} + c_1L_{critic}−c_2H \]

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制作 仅作参考

PPO_clip_连续


引用

  1. Schulman, J., Wolski, F., Dhariwal, P., Radford, A. and Klimov, O., 2017. Proximal policy optimization algorithms. arXiv preprint arXiv:1707.06347.

  2. 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.

posted @ 2026-06-11 19:02  离心律  阅读(24)  评论(0)    收藏  举报