PPO学习与应用笔记(二):PPO 算法实现 :原理 + 网络结构 + 代码讲解

PPO(Proximal Policy Optimization)是一种高效、稳定的强化学习算法,是目前最流行的策略优化方法之一。


作者:风会记得2002
时间:2025年5月5日


一、PPO 简介与背景

1.1 策略梯度基本思想

强化学习的目标是学习一个策略 π θ ( a ∣ s ) \pi_\theta(a|s) πθ(as),使得智能体与环境交互后获得尽可能高的累计奖励。

我们希望最大化以下目标:

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πθ(as)A(s,a)]

其中:

  • π θ ( a ∣ s ) \pi_\theta(a|s) πθ(as):在状态 s s s 下采取动作 a a a 的概率;
  • A ( s , a ) A(s, a) A(s,a):优势函数,衡量一个动作比平均水平好多少;
  • log ⁡ π θ ( a ∣ s ) \log \pi_\theta(a|s) logπθ(as):该动作的对数概率;
  • ∇ θ \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(atst)πθ(atst)

这是当前策略与旧策略在同一动作下的概率比。

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) π(as)=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(atst)πθ(atst)=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×104,这个是 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 算法的实现细节,欢迎在评论区留言交流 👇!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值