4. PPO策略更新循环
任务目标
实现一个完整的PPO策略更新循环,包含与环境交互采样、GAE优势估计、策略网络与价值网络的裁剪更新。本案例使用CartPole-v1作为验证环境,在单次训练迭代中完成采样→优势计算→多轮更新的完整闭环。
核心实现
import torch
import torch.nn as nn
import torch.optim as optim
import gymnasium as gym
from torch.distributions import Categorical
# ---------- 网络定义 ----------
class PolicyNet(nn.Module):
def __init__(self, obs_dim, act_dim):
super().__init__()
self.fc = nn.Sequential(
nn.Linear(obs_dim, 64), nn.Tanh(),
nn.Linear(64, 64), nn.Tanh())
self.pi = nn.Linear(64, act_dim)
self.v = nn.Linear(64, 1)
def forward(self, x):
h = self.fc(x)
return Categorical(logits=self.pi(h)), self.v(h).squeeze(-1)
# ---------- 超参数 ----------
LR = 3e-4
GAMMA = 0.99
LAMBDA = 0.95
CLIP_EPS = 0.2
EPOCHS = 4
BATCH_SIZE = 64
MAX_STEPS = 2000
# ---------- 采样 ----------
def collect_rollout(env, policy, max_steps):
obs_list, act_list, rew_list, logp_list = [], [], [], []
obs, _ = env.reset()
for _ in range(max_steps):
obs_t = torch.FloatTensor(obs)
dist, _ = policy(obs_t)
act = dist.sample()
logp = dist.log_prob(act)
obs2, rew, term, trunc, _ = env.step(act.item())
obs_list.append(obs)
act_list.append(act.item())
rew_list.append(rew)
logp_list.append(logp.item())
obs = obs2
if term or trunc:
obs, _ = env.reset()
return obs_list, act_list, rew_list, logp_list
# ---------- GAE优势估计 ----------
def compute_gae(policy, obs_list, rew_list):
obs_t = torch.FloatTensor(obs_list)
with torch.no_grad():
_, values = policy(obs_t)
advantages = []
gae = 0.0
for t in reversed(range(len(rew_list))):
next_v = values[t + 1] if t + 1 < len(rew_list) else 0.0
delta = rew_list[t] + GAMMA * next_v - values[t]
gae = delta + GAMMA * LAMBDA * gae
advantages.insert(0, gae)
returns = [adv + v for adv, v in zip(advantages, values.tolist())]
return torch.FloatTensor(advantages), torch.FloatTensor(returns)
# ---------- PPO更新 ----------
def ppo_update(policy, optimizer, obs_list, act_list, logp_list, advantages, returns):
obs_t = torch.FloatTensor(obs_list)
act_t = torch.LongTensor(act_list)
old_logp_t = torch.FloatTensor(logp_list)
adv_t = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
for _ in range(EPOCHS):
idx = torch.randperm(len(obs_t))
for i in range(0, len(obs_t), BATCH_SIZE):
batch = idx[i:i + BATCH_SIZE]
dist, values = policy(obs_t[batch])
logp = dist.log_prob(act_t[batch])
ratio = torch.exp(logp - old_logp_t[batch])
surr1 = ratio * adv_t[batch]
surr2 = torch.clamp(ratio, 1 - CLIP_EPS, 1 + CLIP_EPS) * adv_t[batch]
policy_loss = -torch.min(surr1, surr2).mean()
value_loss = nn.MSELoss()(values, returns[batch])
loss = policy_loss + 0.5 * value_loss
optimizer.zero_grad()
loss.backward()
optimizer.step()
# ---------- 训练主循环 ----------
env = gym.make("CartPole-v1")
policy = PolicyNet(4, 2)
optimizer = optim.Adam(policy.parameters(), lr=LR)
obs_list, act_list, rew_list, logp_list = collect_rollout(env, policy, MAX_STEPS)
advantages, returns = compute_gae(policy, obs_list, rew_list)
ppo_update(policy, optimizer, obs_list, act_list, logp_list, advantages, returns)
print(f"采样步数: {len(rew_list)}, 平均奖励: {sum(rew_list) / len(rew_list):.2f}")
运行与验证
依赖安装:
pip install torch gymnasium
运行方式:直接执行上述脚本。程序会完成一次完整的PPO更新迭代(2000步采样 + 4轮小批量更新)。
通过条件:
- 脚本无报错正常退出,并打印采样步数与平均奖励;
- 平均奖励应大于随机策略基线(CartPole随机策略平均约20~30步),说明策略已从采样数据中获得有效学习信号;
- 将上述主循环放入
for iteration in range(100)中重复执行,平均奖励应随迭代次数逐步上升,最终接近或达到500(CartPole-v1的终止上限),即可确认PPO更新循环正确收敛。
关键点说明:
- GAE计算中
values[t+1]在终止步后置0,这是优势估计正确性的关键; - 优势标准化(减均值除标准差)能显著提升训练稳定性;
- 策略损失使用
min(surr1, surr2)实现裁剪,防止更新步幅过大。