直接套用 torch.nn.Module 写 PPO 网络易崩,因 Actor 与 Critic 必须职责分离:Actor 输出概率分布(Categorical/Normal),Critic 输出标量价值;共用 backbone 时需 detach() 隔离梯度;连续动作须用 rsample(),log_std 初始化为 -1.0;GAE 计算、rollout 和 old_log_prob 均需 torch.no_grad()。

为什么直接套用 torch.nn.Module 写 PPO 网络容易崩
因为 PPO 不是单个模型,而是两个强耦合但职责分离的网络:一个输出动作概率(Actor),一个评估状态价值(Critic)。很多人把它们塞进同一个 forward 里返回两个张量,结果在更新时梯混淆、梯度泄漏、loss.backward() 报错。
正确做法是明确拆开——哪怕共用部分 backbone,也要让 Actor 和 Critic 各自拥有独立的输出头和参数路径。
- Actor 输出必须是分布对象(如
torch.distributions.Categorical或Normal),不能只返回 logits - Critic 只能输出标量价值(
shape == [batch_size, 1]),不能带梯度到 Actor 分支 - 共用 backbone 时,务必用
detach()或with torch.no_grad():隔离 Critic 推理对 Actor 的干扰(尤其在 GAE 计算中)
Actor 网络怎么处理离散/连续动作空间
动作类型决定输出层和分布封装方式,错配会导致 log_prob() 返回 NaN 或训练发散。
离散动作(如 CartPole):
self.actor_head = nn.Linear(hidden_dim, num_actions) # forward 中: logits = self.actor_head(x) dist = Categorical(logits=logits) action = dist.sample() log_prob = dist.log_prob(action)
连续动作(如 Pendulum):
self.mu_head = nn.Linear(hidden_dim, action_dim) self.log_std_head = nn.Linear(hidden_dim, action_dim) # 别用 softplus 包裹 log_std,先保持 raw # forward 中: mu = torch.tanh(self.mu_head(x)) * max_action # 截断均值 log_std = torch.clamp(self.log_std_head(x), -20, 2) # 防止 std 极端小/大 std = torch.exp(log_std) dist = Normal(mu, std) action = dist.rsample() # 用 rsample 支持重参数化 log_prob = dist.log_prob(action).sum(axis=-1)
- 连续空间必须用
rsample()而非sample(),否则策略梯度不可导 -
log_std初始化建议全为 -1.0 左右,避免初始 std 过大导致探索失控 - 不要在
log_prob()后再加.mean()—— PPO loss 要求每个样本独立参与 ratio 计算
Critic 网络的输出和训练目标是什么
Critic 不预测 reward,而是预测从当前 state 开始的 discounted return 期望值(即 V(s))。它的 loss 是均方误差,但目标不是真实 reward,而是 GAE 估计的 advantage 加上旧 value 的 bootstrapped 值。
- 输出必须是
[batch_size, 1],且不能被 softmax 或 sigmoid 激活(会压缩范围,破坏 return 量级) - 训练时 target =
values_old + advantages(PPO-Clip 标准实现),不是 reward 本身 - value loss 权重通常设为 0.5,远小于 policy loss,否则 Critic 过拟合会拖慢策略更新
- 如果发现
advantages全为负或剧烈震荡,大概率是 GAE 的gamma或lam设得不合理(推荐gamma=0.99, lam=0.95)
训练循环里最容易漏掉的三个 torch.no_grad() 场景
PPO 的 rollout 数据采集必须冻结网络参数,否则采样过程会污染梯度。但新手常只记得 freeze actor,忘了 critic 和 GAE 计算。
- rollout 时整个 agent(actor + critic)都要进
torch.no_grad(),否则 buffer 存的数据含计算图,backward()直接爆显存 - 计算 GAE 时,
next_values来自 critic 推理,必须next_values.detach(),否则优势函数会把梯度反传回 rollout 步骤 - 计算 ratio =
new_log_prob / old_log_prob时,old_log_prob必须来自 buffer 中缓存的原始值,不能重新 forward —— 除非你确定用了相同的随机 seed 和 dropout mask
这些点不显式处理,模型可能前 100 轮看着正常,第 101 轮 loss 突然 nan,debug 成本极高。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











