PPO(Proximal Policy Optimization)是一种高效、稳定的强化学习算法,是目前最流行的策略优化方法之一。
作者:风会记得2002
时间:2025年5月5日
一、PPO 简介与背景
1.1 策略梯度基本思想
强化学习的目标是学习一个策略 π θ ( a ∣ s ) \pi_\theta(a|s) πθ(a∣s),使得智能体与环境交互后获得尽可能高的累计奖励。
我们希望最大化以下目标:
J ( θ ) = E π θ [ ∑ t = 0 ∞ γ t r ( s t , a t ) ] J(\theta) = \mathbb{E}_{\pi_\theta} \left[ \sum_{t=0}^{\infty} \gamma^t r(s_t, a_t) \right] J(θ)=Eπθ[t=0∑∞γtr(st,at)]
策略梯度方法通过以下公式来更新策略参数 θ \theta θ:
∇ θ J ( θ ) = E π θ [ ∇ θ log π θ ( a ∣ s ) ⋅ A ( s , a ) ] \nabla_\theta J(\theta) = \mathbb{E}_{\pi_\theta} \left[ \nabla_\theta \log \pi_\theta(a|s) \cdot A(s, a) \right] ∇θJ(θ)=Eπθ[∇θlogπθ(a∣s)⋅A(s,a)]
其中:
- π θ ( a ∣ s ) \pi_\theta(a|s) πθ(a∣s):在状态 s s s 下采取动作 a a a 的概率;
- A ( s , a ) A(s, a) A(s,a):优势函数,衡量一个动作比平均水平好多少;
- log π θ ( a ∣ s ) \log \pi_\theta(a|s) logπθ(a∣s):该动作的对数概率;
- ∇ θ \nabla_\theta ∇θ:对策略参数的梯度。
1.2 PPO 的改进点:限制策略更新幅度
直接使用策略梯度更新策略可能导致不稳定或发散。为了解决这个问题,PPO 引入了剪切(clip)策略优化目标:
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)
这是当前策略与旧策略在同一动作下的概率比。
PPO 的核心损失函数是:
L CLIP ( θ ) = E t [ min ( r t ( θ ) A t , clip ( r t ( θ ) , 1 − ϵ , 1 + ϵ ) A t ) ] L^{\text{CLIP}}(\theta) = \mathbb{E}_t \left[ \min \left( r_t(\theta) A_t, \ \text{clip}(r_t(\theta), 1 - \epsilon, 1 + \epsilon) A_t \right) \right] LCLIP(θ)=Et[min(rt(θ)At, clip(rt(θ),1−ϵ,1+ϵ)At)]
含义:
- r t ( θ ) A t r_t(\theta) A_t rt(θ)At:标准策略梯度目标;
- clip 限制比值 r t r_t rt 在 [ 1 − ϵ , 1 + ϵ ] [1 - \epsilon, 1 + \epsilon] [1−ϵ,1+ϵ] 区间;
- 防止策略更新“跳跃太大”,提升训练稳定性;
- 通常 ϵ = 0.2 \epsilon=0.2 ϵ=0.2。
二、Actor-Critic 网络结构
PPO 使用 Actor-Critic 架构,包含两个子网络:
- Actor(策略网络):输出动作分布参数(如高斯分布的均值和标准差);
- Critic(价值网络):输出状态价值 V ( s ) V(s) V(s),用于估算优势函数。
三、Critic 网络实现(价值函数)
class Critic(nn.Module):
def __init__(self, obs_dim):
super(Critic, self).__init__()
self.fc1 = nn.Linear(obs_dim, 64)
self.fc2 = nn.Linear(64, 64)
self.value = nn.Linear(64, 1)
def forward(self, x):
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
value = self.value(x)
return value
解释:
- 输入维度为
obs_dim
,即观测空间的维度; - 网络结构是 2 层全连接,每层 64 个神经元;
- 输出层为线性层,输出一个值 V ( s ) V(s) V(s),表示状态价值。
该输出值参与计算优势:
A ( s , a ) = R ( s , a ) − V ( s ) A(s, a) = R(s, a) - V(s) A(s,a)=R(s,a)−V(s)
四、Actor 网络实现(策略函数)
Actor 网络的任务是输出动作分布,以便在环境中采样动作。在连续动作空间中,我们通常使用 高斯分布(正态分布)来建模动作策略:
π ( a ∣ s ) = N ( μ ( s ) , σ ( s ) 2 ) \pi(a|s) = \mathcal{N}(\mu(s), \sigma(s)^2) π(a∣s)=N(μ(s),σ(s)2)
即对于每个状态 s s s,Actor 网络输出一个高斯分布的均值 μ ( s ) \mu(s) μ(s) 和标准差 σ ( s ) \sigma(s) σ(s),然后从中采样得到动作 a a a。
🎯 代码实现(PyTorch)
class Actor(nn.Module):
def __init__(self, obs_dim, act_dim):
super(Actor, self).__init__()
self.fc1 = nn.Linear(obs_dim, 64)
self.fc2 = nn.Linear(64, 64)
self.mu_head = nn.Linear(64, act_dim)
self.log_std = nn.Parameter(torch.zeros(act_dim)) # log(σ)
def forward(self, x):
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
mu = self.mu_head(x)
std = self.log_std.exp() # 转换为正的标准差
return mu, std
✅ 结构说明
- 输入:状态向量
s
s
s,维度为
obs_dim
; - 隐藏层:两个 ReLU 激活的全连接层,每层 64 单元;
- 输出:
mu
:动作均值 μ ( s ) \mu(s) μ(s);std
:通过log_std
参数生成标准差 σ ( s ) \sigma(s) σ(s),并通过.exp()
保证其为正值;
- 动作采样:在训练时通过
Normal(mu, std).sample()
进行动作采样。
🤔 为什么用 log_std 而不是 std?
直接优化标准差 σ \sigma σ 不方便(需保证其为正)。所以通常使用对数标准差 log σ \log \sigma logσ 作为网络参数,并在前向传播时使用:
σ = exp ( log σ ) \sigma = \exp(\log \sigma) σ=exp(logσ)
这样既能确保 σ > 0 \sigma > 0 σ>0,又便于训练。
🧮 动作采样与概率计算
在 PPO 更新中,我们需要从当前策略中采样动作,并计算其对应的对数概率:
mu, std = actor(state)
dist = Normal(mu, std)
action = dist.sample() # 训练时采样
log_prob = dist.log_prob(action).sum(dim=-1)
同时,PPO 损失也用到了这个 log_prob
与之前保存的 old_log_prob
的比值:
r t ( θ ) = π θ ( a t ∣ s t ) π θ old ( a t ∣ s t ) = exp ( log π θ − log π θ old ) r_t(\theta) = \frac{\pi_\theta(a_t|s_t)}{\pi_{\theta_{\text{old}}}(a_t|s_t)} = \exp(\log \pi_\theta - \log \pi_{\theta_{\text{old}}}) rt(θ)=πθold(at∣st)πθ(at∣st)=exp(logπθ−logπθold)
🔐 总结:Actor 网络的职责
模块 | 功能 |
---|---|
μ ( s ) \mu(s) μ(s) | 动作的预测均值 |
σ ( s ) \sigma(s) σ(s) | 控制探索程度(分布范围) |
log prob | 用于计算概率比,更新策略 |
dist.entropy() | 用于鼓励探索,防止陷入局部最优 |
Actor 网络配合 Critic 网络,构成 PPO 的核心框架。
五、PPO 更新函数详解
def ppo_update(ppo_epochs, mini_batch_size, states, actions, log_probs, returns, advantages,
actor, critic, actor_optim, critic_optim, clip_epsilon=0.2, entropy_coef=0.05):
for _ in range(ppo_epochs):
for state_mb, action_mb, old_log_probs_mb, advantage_mb, return_mb in zip(
states.split(mini_batch_size),
actions.split(mini_batch_size),
log_probs.split(mini_batch_size),
advantages.split(mini_batch_size),
returns.split(mini_batch_size)
):
# Critic 损失 = MSE(V - R)
values = critic(state_mb)
critic_loss = (values - return_mb).pow(2).mean()
# Actor 网络输出动作分布
mu, std = actor(state_mb)
dist = Normal(mu, std)
# 重新计算 log 概率
new_log_probs = dist.log_prob(action_mb).sum(dim=-1)
# 概率比
ratio = torch.exp(new_log_probs - old_log_probs_mb)
# 剪切损失
surr1 = ratio * advantage_mb
surr2 = torch.clamp(ratio, 1.0 - clip_epsilon, 1.0 + clip_epsilon) * advantage_mb
actor_loss = -torch.min(surr1, surr2).mean()
# 熵项:鼓励探索
entropy = dist.entropy().mean()
actor_loss -= entropy_coef * entropy
# 更新 actor
actor_optim.zero_grad()
actor_loss.backward()
actor_optim.step()
# 更新 critic
critic_optim.zero_grad()
critic_loss.backward()
critic_optim.step()
参数说明:
ppo_epochs
:每次训练中 PPO 轮数;mini_batch_size
:小批量样本数;states, actions, log_probs, returns, advantages
:从交互中采样得到的数据;actor, critic
:策略与价值网络;actor_optim, critic_optim
:对应优化器;clip_epsilon
:clip 范围,一般设为 0.2;entropy_coef
:策略熵系数,鼓励探索。
六、训练主循环(多环境并行采样 + PPO 更新)
接下来我们进入最核心的一部分 —— 训练主循环。这里我们使用 PyTorch + Gym 的 vector.Env
实现多环境并行采样,能大幅提升 PPO 的采样效率和稳定性。
🔧 超参数设置
num_envs = 8 # 并行环境数量
num_episodes = 1000 # 训练总轮数
gamma = 0.99 # 奖励折扣因子
lam = 0.95 # GAE 衰减因子
ppo_epochs = 10 # 每轮 PPO 更新的 epoch 次数
mini_batch_size = 64 # mini-batch 大小
max_steps_per_episode = 200 # 每个 episode 最大步数
🌍 创建并行环境
envs = gym.vector.SyncVectorEnv([make_env() for _ in range(num_envs)])
obs_dim = envs.single_observation_space.shape[0]
action_dim = envs.single_action_space.shape[0]
我们使用 SyncVectorEnv
并行启动多个环境,每次同时执行多个智能体采样,这可以大幅加快训练,并提升策略的泛化性。
⚙️ 初始化网络与优化器
actor = Actor(obs_dim, action_dim)
critic = Critic(obs_dim)
actor_optim = optim.Adam(actor.parameters(), lr=3e-4)
critic_optim = optim.Adam(critic.parameters(), lr=3e-4)
使用 Adam 优化器,学习率设为 3 × 1 0 − 4 3 \times 10^{-4} 3×10−4,这个是 PPO 常用的默认设置。
📈 初始化 TensorBoard
writer = SummaryWriter(log_dir="./ppo_rendezvous_tensorboard/")
用于记录训练过程中的 reward、loss 等指标,方便可视化观察模型收敛情况。
🔁 主训练循环开始!
global_step = 0
for episode in range(num_episodes):
# 缓存采样数据
states, actions, log_probs, rewards, dones, values = [], [], [], [], [], []
obs, _ = envs.reset()
episode_rewards = np.zeros(num_envs)
🚀 采样 rollout 轨迹
for step in range(max_steps_per_episode):
global_step += num_envs
state_tensor = torch.FloatTensor(obs)
with torch.no_grad():
mu, std = actor(state_tensor)
dist = Normal(mu, std)
action = dist.sample()
log_prob = dist.log_prob(action).sum(-1)
value = critic(state_tensor)
next_obs, reward, done, _, info = envs.step(action.numpy())
# 保存数据
states.append(state_tensor)
actions.append(action)
log_probs.append(log_prob)
rewards.append(torch.FloatTensor(reward))
dones.append(torch.FloatTensor(done))
values.append(value.squeeze())
episode_rewards += reward
obs = next_obs
# 如果有环境结束,记录奖励
if done.any():
for i, d in enumerate(done):
if d:
print(f"Episode: {episode}, Reward: {episode_rewards[i]:.2f}")
writer.add_scalar("charts/episodic_return", episode_rewards[i], global_step)
episode_rewards[i] = 0
📊 GAE 优势估计 + 回报计算
with torch.no_grad():
returns = []
advantages = []
R = 0
gae = 0
for i in reversed(range(len(rewards))):
next_value = 0 if i == len(rewards) - 1 else values[i+1]
delta = rewards[i] + gamma * next_value * (1 - dones[i]) - values[i]
gae = delta + gamma * lam * (1 - dones[i]) * gae
returns.insert(0, gae + values[i])
advantages.insert(0, gae)
returns = torch.tensor(returns, dtype=torch.float32)
advantages = torch.tensor(advantages, dtype=torch.float32)
advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
使用 GAE(Generalized Advantage Estimation) 进行优势函数计算:
δ 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)
A t = δ t + γ λ δ t + 1 + ⋯ A_t = \delta_t + \gamma \lambda \delta_{t+1} + \cdots At=δt+γλδt+1+⋯
这种方法在 PPO 中非常常见,能显著减少方差,提高学习稳定性。
🧠 进行 PPO 更新
ppo_update(
ppo_epochs, mini_batch_size,
states, actions, log_probs, returns, advantages,
actor, critic, actor_optim, critic_optim
)
这一步调用之前实现的 ppo_update()
函数,对 Actor 与 Critic 网络进行多轮更新。
📋 日志记录
writer.add_scalar("losses/value_loss", (values - returns).pow(2).mean().item(), global_step)
writer.add_scalar("charts/mean_reward", rewards.mean().item(), global_step)
将损失函数与平均奖励写入 TensorBoard,可视化学习效果。
✅ 全部训练完成
print("训练完成!")
writer.close()
🔚 小结
本节我们讲解了 PPO 的完整训练流程:
- 并行环境采样(高效收集数据);
- Actor 采样动作,Critic 评估状态;
- 使用 GAE 估计优势;
- 使用剪切比值进行 PPO 更新;
- 使用 TensorBoard 记录训练效果。
至此,一个可运行的、支持多环境并行的 PPO 强化学习系统就构建完成了!可以根据任务修改环境、状态和动作空间,也可以微调超参数进一步提升性能!
七、总结
PPO 是一种高效的策略优化算法,具有以下特点:
- 无需二阶导数,比 TRPO 更易实现;
- 剪切目标函数确保策略稳定更新;
- 与 Actor-Critic 架构天然兼容;
- 在多个复杂任务中性能优秀。
如果你对本文有任何疑问或想深入交流 PPO 算法的实现细节,欢迎在评论区留言交流 👇!