
在 CartPole 这类经典控制任务上很多同学第一次接触 Actor-Critic 时都会有类似的困惑明明 Q-Learning 已经能跑通为什么还要多搞一个网络出来策略梯度里的 REINFORCE 也能训练为什么它的曲线抖得厉害本文就从这两个疑问出发把 Actor-Critic 的网络架构、优势函数、GAE、A2C 与 A3C 的差异完整拆开最后给出一份可以直接运行的 PyTorch 实现。读完之后你应该能做到三件事自己搭出一个 Actor-Critic 网络、理解策略损失和价值损失为什么要这样配、遇到训练不收敛时知道从哪里开始查。1. 从策略梯度到 Actor-Critic为什么需要两套网络1.1 纯策略梯度方法的两个痛点强化学习的本质是让智能体在与环境的交互中学会在什么状态下做什么动作。最直接的做法是把策略参数化成一个神经网络输入状态、输出动作概率然后用回合累计回报去加权更新这个网络这就是 REINFORCE 代表的纯策略梯度方法。它的推导干净、实现简单但落地时会碰到两个很难绕开的问题。第一个问题是方差大。REINFORCE 用整条轨迹的回报 $G_t$ 去乘对数概率的梯度而 $G_t$ 是随机采样的结果同一个状态在不同回合里可能对应完全不同的回报值。这就导致梯度估计的噪声非常大网络参数会被高方差的信号来回拉扯训练曲线呈现出明显的锯齿状。虽然可以通过采很多条轨迹取平均来缓解但样本效率会迅速下降。第二个问题是只能学、不能评。纯策略梯度只关心这个动作概率该调大还是调小却不知道这个状态本身到底值多少钱。没有价值基准就无法判断某次回报是因为动作选得好还是因为这一局运气好。缺少这个参照系策略更新就缺少稳定的方向感。1.2 Actor-Critic 的核心思想Actor-Critic 的思路非常直白既然策略梯度缺少一个打分器那就再训练一个网络专门给状态估值用它来充当基准。于是整个系统被拆成两个部分Actor 负责输出动作分布Critic 负责估计状态价值 $V(s)$。Critic 的输出不直接产生动作但它提供的信息能让 Actor 的更新信号变得更加精准。在实际更新时Actor 不再使用原始回报 $G_t$而是使用回报减去基准值得到的优势值。这个减法的意义在于如果某个动作带来的回报比预期高就提高它的概率如果低于预期就降低。正如一个学生在考试中拿到 80 分单看分数很难判断好坏但如果班级平均分是 60那这 80 分就明显值得肯定。Critic 提供的就是这个平均分。1.3 与 Q-Learning、REINFORCE 的关系对比为了把定位说清楚这里把三类方法放在一起做个对照。Q-Learning 属于值函数方法它学的是动作价值 $Q(s,a)$策略是从 Q 值里隐式导出的取最大值或按 softmax 采样REINFORCE 属于策略方法直接优化策略参数Actor-Critic 则是两者的结合体既有显式的策略网络又有显式的价值网络。方法类型学什么策略来源优点主要缺点Q-Learning / DQN$Q(s,a)$由 Q 值导出样本利用率较高只能处理离散动作REINFORCE策略参数 $\theta$直接输出分布支持连续动作方差极大Actor-Critic策略 价值策略网络直接输出方差较低、支持连续动作两个网络耦合调参更复杂需要强调一点Actor-Critic 不是一种具体算法而是一类架构范式。A2C、A3C、PPO、SAC、DDPG 都可以看作这个范式下的不同变体区别主要在于优势怎么算、更新怎么截断、数据怎么并行收集。2. Actor 与 Critic 的概念拆解2.1 Actor负责做决定的策略网络Actor 网络的结构取决于动作空间。如果动作是离散的比如 CartPole 里的向左、向右网络输出层就是动作数量个 logits经过 softmax 后得到概率分布采样时用Categorical分布即可如果动作是连续的比如机械臂的关节角度输出层通常拆成均值和对数标准差两部分用高斯分布采样。这里有一个容易被忽略的设计细节Actor 的输出层不应该做很强的初始化。因为初始策略越接近均匀分布探索性越好如果输出层权重初始得过大softmax 会立刻饱和成几乎确定性的策略智能体一开始就只会做同一个动作很容易陷入局部最优。实践中常用的做法是把 Actor 输出层的正交初始化增益设得很小比如 0.01。2.2 Critic负责打分的价值网络Critic 网络的输出是一个标量表示当前状态的价值估计。它的训练目标比较明确让预测值逼近真实的折扣回报。这是一个标准的回归问题损失函数用均方误差即可。相比 Actor 那边的策略梯度Critic 的监督信号更直接所以通常收敛得更快。不过 Critic 也有自己的麻烦。它估计的目标本身是随着策略变化而变化的策略一更新价值分布就跟着漂移了。这就是所谓的移动靶问题。如果 Critic 学得太慢优势估计就不准Actor 会拿到错误的梯度如果 Critic 学得太快、拟合过强又容易过拟合到旧策略的价值上。实际训练中通常靠控制价值损失系数和更新轮数来平衡。2.3 优势函数让策略更新更有方向感优势函数的定义是 $A(s,a) Q(s,a) - V(s)$表示在状态 s 下选择动作 a 比平均水平好多少。在 Actor-Critic 里我们通常不去显式算 Q而是用一步时序差分误差来近似delta_t r_t gamma * V(s_{t1}) - V(s_t)这个 delta 就是优势的一个有偏估计。把它代进策略梯度就得到最基础的 Actor-Critic 更新形式。相比用完整回报 $G_t$delta 的方差小得多因为它只依赖一步随机性后面的不确定性都被 Critic 的估计吸收掉了。代价是引入了偏差。如果 Critic 估得不准delta 就是有偏的。这构成了强化学习中经典的偏差-方差权衡用越多步的真实回报偏差越小、方差越大越依赖 Critic 的自举估计方差越小、偏差越大。后面的 GAE 就是在这个权衡上做的一个可调参数化方案。2.4 自举与偏差-方差权衡自举bootstrap这个词来自统计学在这里指的是用估计值去更新估计值。Critic 在计算目标时用到的 $V(s_{t1})$ 本身也是网络输出这就是自举。自举让价值学习可以单步进行不必等整个回合结束但它也带来了估计偏差累积的风险。用一个极端例子来理解如果 Critic 一开始把所有状态的价值都估成 0那么 delta 就退化成即时奖励 $r_t$此时优势估计完全不包含未来信息Actor 学到的就是短视策略。反过来如果完全不用自举、只用蒙特卡洛回报偏差消失但方差飙升。GAE 用参数 lambda 在两者之间做线性插值lambda 取 0 就退化成单步 TD取 1 就退化成蒙特卡洛实践中常取 0.95 左右。3. 环境准备与版本说明3.1 运行环境与依赖本文示例使用 Python 和 PyTorch 实现环境交互部分使用 Gymnasium。由于这些库的版本迭代较快具体版本号请以你本地实际安装的为准本文重点演示配置思路和代码结构不锁定具体小版本号。需要准备的基础组件包括Python 3.9 及以上版本、PyTorchCPU 版本即可跑通 CartPole、Gymnasium、NumPy。如果你有可用的 GPU代码无需修改把张量放到对应设备上就行但 CartPole 这种小任务用 CPU 完全够用强行上 GPU 反而因为数据传输开销变慢。# 创建虚拟环境可选但推荐 python -m venv rl_env source rl_env/bin/activate # Windows 下用 rl_env\Scripts\activate # 安装依赖 pip install torch numpy gymnasium安装完成后建议先做一次环境自检确认 Gymnasium 的接口是较新的五元组返回形式避免后面代码对不上。3.2 安装命令与验证# 文件路径check_env.py import gymnasium as gym import torch print(torch:, torch.__version__) env gym.make(CartPole-v1) obs, info env.reset(seed0) print(obs shape:, obs.shape) print(action space:, env.action_space) next_obs, reward, terminated, truncated, info env.step(0) print(reward:, reward, terminated:, terminated, truncated:, truncated) env.close()预期输出中obs shape应为(4,)action space为Discrete(2)说明环境的观测维度是 4、动作空间是 2。如果step只返回四个值说明你用的是旧版 gym 接口需要调整解包写法或者直接升级到 Gymnasium。3.3 示例项目结构为了便于后续扩展建议把代码拆成几个文件而不是全部塞进一个脚本。下面是一个够用但不过度设计的结构actor_critic_demo/ ├── model.py # Actor-Critic 网络定义 ├── gae.py # 优势估计 ├── train_a2c.py # 训练主循环 └── README.md如果你只是想快速验证把三个文件合并成一个脚本也完全没问题。本文下面的完整示例会按文件拆分的方式给出方便你直接对照创建。4. Actor-Critic 网络架构设计要点4.1 共享主干还是两套独立网络这是搭网络时第一个要做的决定。共享主干指的是 Actor 和 Critic 前面几层共用只在最后分叉出两个输出头独立网络则是两套完全分开的参数。两种做法各有适用场景。共享主干的好处是参数量小、特征复用训练早期收敛更快因为 Critic 的梯度也能帮助主干学到有用的状态表示。缺点是两个目标的梯度会互相干扰尤其是当策略损失和价值损失量级差很多时主干参数会被一方主导。独立网络没有干扰问题但参数量翻倍小任务上容易过拟合训练也更慢。实践中的常见选择是状态维度不高、任务简单时用共享主干状态是图像、特征提取成本高时也倾向共享如果发现训练震荡明显、两个损失量级差得离谱就改成独立网络试试。本文的 CartPole 示例采用共享主干加双头输出的结构。4.2 离散动作空间的输出层设计离散动作空间的 Actor 输出层就是一个线性层输出维度等于动作数。它的输出是 logits不需要手动加 softmax因为 PyTorch 的Categorical分布接受 logits 并在内部做归一化这样在数值上更稳定。# 文件路径model.py离散动作版本的核心片段 import numpy as np import torch import torch.nn as nn class ActorCritic(nn.Module): def __init__(self, obs_dim, act_dim, hidden_dim128): super().__init__() self.backbone nn.Sequential( nn.Linear(obs_dim, hidden_dim), nn.Tanh(), nn.Linear(hidden_dim, hidden_dim), nn.Tanh(), ) self.actor_head nn.Linear(hidden_dim, act_dim) self.critic_head nn.Linear(hidden_dim, 1) self._init_weights() def _init_weights(self): for layer in self.backbone: if isinstance(layer, nn.Linear): nn.init.orthogonal_(layer.weight, gainnp.sqrt(2)) nn.init.constant_(layer.bias, 0.0) # Actor 输出层小增益保证初始策略接近均匀分布 nn.init.orthogonal_(self.actor_head.weight, gain0.01) nn.init.constant_(self.actor_head.bias, 0.0) # Critic 输出层正常增益 nn.init.orthogonal_(self.critic_head.weight, gain1.0) nn.init.constant_(self.critic_head.bias, 0.0) def forward(self, obs): h self.backbone(obs) logits self.actor_head(h) value self.critic_head(h).squeeze(-1) return logits, value这里激活函数用的是Tanh而不是ReLU。原因在于策略梯度的稳定性Tanh的输出有界梯度不会因为状态数值范围波动而爆炸这在连续控制任务里尤其重要。虽然ReLU在监督学习里更常用但在策略网络里有界激活往往训练更稳。4.3 连续动作空间的高斯策略连续动作的处理方式不同。网络不再输出每个动作的概率而是输出一个高斯分布的均值和标准差采样时从这个分布里取值。为了让标准差恒为正通常让它以对数形式作为可学习参数。# 文件路径model.py连续动作版本的核心片段 class GaussianActorCritic(nn.Module): def __init__(self, obs_dim, act_dim, hidden_dim128): super().__init__() self.backbone nn.Sequential( nn.Linear(obs_dim, hidden_dim), nn.Tanh(), nn.Linear(hidden_dim, hidden_dim), nn.Tanh(), ) self.mean_head nn.Linear(hidden_dim, act_dim) self.critic_head nn.Linear(hidden_dim, 1) # 对数标准差作为独立可学习参数初始为 0 表示标准差为 1 self.log_std nn.Parameter(torch.zeros(act_dim)) def forward(self, obs): h self.backbone(obs) mean self.mean_head(h) std torch.exp(self.log_std).expand_as(mean) value self.critic_head(h).squeeze(-1) return mean, std, value用这个网络采样时构造torch.distributions.Normal(mean, std)然后调用sample()并取log_prob()即可。需要注意的是连续动作通常需要按动作空间的范围做缩放否则策略输出的动作可能超出环境允许的边界导致物理仿真直接报错或行为异常。4.4 参数初始化的细节上面代码里的初始化方式值得单独说一下。正交初始化orthogonal initialization在强化学习中之所以流行是因为它能保证初始权重矩阵的各行近似正交前向传播时不会把输入的不同维度过度压缩到同一方向梯度传播更均匀。Actor 输出层增益设为 0.01目的是让初始 logits 接近 0softmax 后接近均匀分布。如果这里用默认初始化logits 可能有 ±1 以上的差异softmax 就会给出 70% 以上的偏向性探索性直接受损。Critic 输出层用增益 1保证初始价值估计有合理的数值范围不会一开始就全部压到 0 附近。5. 完整实战在 CartPole 上实现 A2C5.1 定义 Actor-Critic 网络网络定义在 4.2 节已经给出这里补充一个带动作采样的方法方便训练循环调用。把采样逻辑封装进模型可以让训练代码更干净也便于后续替换成其他动作分布。# 文件路径model.py续接在 ActorCritic 类内部 torch.no_grad() def act(self, obs): logits, value self.forward(obs) dist torch.distributions.Categorical(logitslogits) action dist.sample() log_prob dist.log_prob(action) return action, log_prob, value这个方法加了torch.no_grad()装饰器因为它只用于数据收集不需要构建计算图。省掉这部分计算图能明显降低显存占用对于长 rollout 尤其重要。5.2 实现 GAE 优势估计优势估计是整个训练流程里最容易写错的部分。GAE 的递推公式从后往前算每一步依赖下一步的优势值因此必须逆序遍历。这里给出一个针对单环境的实现返回优势和回报两部分。# 文件路径gae.py import numpy as np def compute_gae(rewards, values, terminated, gamma0.99, lam0.95): rewards: 长度 T 的奖励列表 values: 长度 T1 的价值列表最后一个元素是 bootstrap 价值 terminated: 长度 T 的真实终止标记1.0 表示回合真实结束 T len(rewards) advantages np.zeros(T, dtypenp.float32) last_gae 0.0 next_value values[T] for t in reversed(range(T)): # 只有真实终止才切断自举时间截断仍然要用 bootstrap mask 1.0 - terminated[t] delta rewards[t] gamma * next_value * mask - values[t] last_gae delta gamma * lam * mask * last_gae advantages[t] last_gae next_value values[t] returns advantages np.asarray(values[:T], dtypenp.float32) return advantages, returns这里有一个细节特别值得强调terminated和truncated要区分对待。回合因为到达终止状态而结束时未来的价值确实应该是 0mask 取 0 是对的但回合仅仅因为达到最大步数被截断时未来其实还有价值此时不应该切断自举。很多实现图省事直接用done terminated or truncated在截断时错误地把 bootstrap 置零导致价值估计系统性偏低训练后期表现不稳定。5.3 经验收集与数据组织数据收集阶段要做的事情是按固定步数收集一条轨迹记录状态、动作、旧对数概率、奖励、终止标记和价值估计。为什么要记录旧对数概率因为在 A2C 的单次更新里它其实用不上但一旦你后面想升级到 PPO 做重要性采样比这个值就是必需的提前存下来可以少改一次代码。# 文件路径train_a2c.py数据收集片段 import torch def collect_rollout(env, model, obs, rollout_len): obs_buf, act_buf, logp_buf [], [], [] rew_buf, term_buf, val_buf [], [], [] ep_return 0.0 finished_returns [] for _ in range(rollout_len): obs_t torch.as_tensor(obs, dtypetorch.float32).unsqueeze(0) action, log_prob, value model.act(obs_t) a int(action.item()) next_obs, reward, terminated, truncated, _ env.step(a) done terminated or truncated obs_buf.append(obs) act_buf.append(a) logp_buf.append(float(log_prob.item())) rew_buf.append(float(reward)) term_buf.append(float(terminated)) val_buf.append(float(value.item())) ep_return reward obs next_obs if done: finished_returns.append(ep_return) ep_return 0.0 obs, _ env.reset() # 收集结束后补一个 bootstrap 价值 with torch.no_grad(): obs_t torch.as_tensor(obs, dtypetorch.float32).unsqueeze(0) _, last_value model(obs_t) val_buf.append(float(last_value.item())) return obs, (obs_buf, act_buf, logp_buf, rew_buf, term_buf, val_buf), finished_returns注意obs_buf里存的是原始 NumPy 数组而不是张量。这样做的好处是内存占用小等真正要更新时再一次性转成张量避免在收集阶段反复分配小张量。5.4 损失函数与参数更新损失由三部分组成策略损失、价值损失和熵正则项。策略损失用优势加权对数概率的负均值价值损失用均方误差熵正则鼓励策略保持一定的随机性。三者用系数加权求和。# 文件路径train_a2c.py更新片段 import torch.nn.functional as F def update_model(model, optimizer, buffers, advantages, returns, value_coef0.5, entropy_coef0.01, max_grad_norm0.5): obs_buf, act_buf, logp_buf, _, _, _ buffers obs_t torch.as_tensor(np.array(obs_buf), dtypetorch.float32) act_t torch.as_tensor(act_buf, dtypetorch.long) adv_t torch.as_tensor(advantages, dtypetorch.float32) ret_t torch.as_tensor(returns, dtypetorch.float32) # 优势标准化显著降低梯度方差 adv_t (adv_t - adv_t.mean()) / (adv_t.std() 1e-8) logits, values model(obs_t) dist torch.distributions.Categorical(logitslogits) new_logp dist.log_prob(act_t) entropy dist.entropy().mean() policy_loss -(new_logp * adv_t).mean() value_loss F.mse_loss(values, ret_t) loss policy_loss value_coef * value_loss - entropy_coef * entropy optimizer.zero_grad() loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_grad_norm) optimizer.step() return float(policy_loss.item()), float(value_loss.item()), float(entropy.item())优势标准化这一步看起来不起眼但对训练稳定性的影响非常大。未标准化的优势值量级会随着奖励尺度变化学习率稍大一点就会发散。标准化之后优势的均值是 0、标准差是 1梯度尺度被控制在一个稳定范围内几乎成了标配操作。梯度裁剪也是同理。Actor 和 Critic 共享主干时某一次异常的 Critic 梯度可能通过主干传导到策略侧把策略参数推离合理区间。裁剪到 0.5 左右可以兜住这种极端情况。5.5 运行脚本与结果说明把上面的片段组装起来就得到完整的训练脚本。主循环的结构是收集一段 → 算优势 → 更新一次这也是 A2C 名字里同步的含义。# 文件路径train_a2c.py主函数 import random from collections import deque import gymnasium as gym import numpy as np import torch from model import ActorCritic from gae import compute_gae def train(): torch.manual_seed(0) np.random.seed(0) random.seed(0) env gym.make(CartPole-v1) obs_dim env.observation_space.shape[0] act_dim env.action_space.n model ActorCritic(obs_dim, act_dim, hidden_dim128) optimizer torch.optim.Adam(model.parameters(), lr7e-4, eps1e-5) gamma, lam 0.99, 0.95 rollout_len 20 num_updates 5000 recent_returns deque(maxlen20) obs, _ env.reset(seed0) global_step 0 for update in range(1, num_updates 1): obs, buffers, finished collect_rollout(env, model, obs, rollout_len) global_step rollout_len for r in finished: recent_returns.append(r) _, _, _, rew_buf, term_buf, val_buf buffers advantages, returns compute_gae(rew_buf, val_buf, term_buf, gamma, lam) p_loss, v_loss, ent update_model(model, optimizer, buffers, advantages, returns) if update % 50 0: avg np.mean(recent_returns) if recent_returns else 0.0 print(fupdate {update:5d} | step {global_step:7d} | favg_return {avg:6.1f} | pi_loss {p_loss:7.3f} | fv_loss {v_loss:7.3f} | entropy {ent:6.3f}) if len(recent_returns) recent_returns.maxlen and np.mean(recent_returns) 475: print(最近 20 回合平均回报已达标提前结束训练) break env.close() torch.save(model.state_dict(), a2c_cartpole.pt) print(模型已保存到 a2c_cartpole.pt) if __name__ __main__: train()运行命令很简单python train_a2c.py预期输出会随着训练推进逐步变化。前期avg_return在 20 到 50 之间波动v_loss从大值快速下降entropy缓慢减小。中期回报开始爬升出现 100 以上的回合。后期稳定在 400 以上最终触发提前结束条件。如果跑到 5000 次更新还没收敛说明超参数需要调整具体排查方向见第 7 节。有一点需要提前说明CartPole 的随机性虽然不大但不同随机种子下的收敛速度差异仍然明显。有时候一次运行 200 个回合就到 500 分有时候要 500 个回合以上。这不代表代码有问题而是强化学习训练本身的特性。建议至少跑三个不同种子看平均表现再下结论。6. 相关算法家族A2C、A3C、GAE 与 PPO6.1 A2C 同步优势 Actor-CriticA2C 的核心特征是同步开多个环境实例每个实例并行跑一段收集完所有数据后统一计算优势、统一更新一次参数。相比单环境串行它能在同样墙钟时间内获得更多样的经验批次内的数据相关性也更低梯度估计质量更高。实现上把单环境换成gym.vector.SyncVectorEnv所有缓冲区多一个环境维度GAE 计算时对每个环境独立逆序递推即可。其余逻辑完全不变。这个改动量不大但能明显提升训练速度是工程实践中值得优先做的优化。6.2 A3C 异步并行A3C 和 A2C 的差别在于并行方式A3C 起多个工作线程每个线程有自己的环境副本各自算梯度并异步地更新同一个全局网络。它不需要等待所有环境同步因此理论上吞吐更高。但在实际工程里A3C 的优势并不明显。Python 的 GIL 让多线程 CPU 并行效果有限异步更新还会带来梯度陈旧的问题也就是某个线程算梯度时用的参数已经过期了这会引入额外的不稳定性。所以在 GPU 环境下大家更倾向用 A2C 的同步多环境方案或者干脆用 PPO。A3C 更多是作为理解异步思想的教学案例存在。6.3 GAE 广义优势估计GAE 的价值在于它把偏差-方差权衡变成了一个连续可调的参数。公式的递推形式是A_t^{GAE} delta_t (gamma * lambda) * A_{t1}^{GAE} delta_t r_t gamma * V(s_{t1}) - V(s_t)当 lambda 取 0 时优势退化成单步 TD 误差偏差最大、方差最小取 1 时退化成蒙特卡洛优势方差最大、偏差最小。中间取值在两者之间平滑过渡。调 lambda 的经验是任务奖励越稠密、回合越短lambda 可以取小一些因为单步信息已经足够任务奖励越稀疏、回合越长lambda 越要接近 1否则信号传不到早期动作。CartPole 每步都有奖励0.95 是个稳妥的默认值如果是迷宫类任务只在终点给奖励可能需要 0.98 甚至更高。6.4 PPO 与 A2C 的血缘关系PPO 常被当作独立算法讲但从架构上看它就是 A2C 的加强版。它保留了 Actor-Critic 双网络结构和 GAE 优势估计主要改动是在策略损失里加了重要性采样比率的截断ratio exp(log_pi_new(a|s) - log_pi_old(a|s)) policy_loss -min(ratio * A, clip(ratio, 1-eps, 1eps) * A).mean()这个截断的作用是限制单次更新的策略变化幅度防止一步更新把策略推得太远导致崩溃。也正因为有了这个保护PPO 可以对同一批数据做多轮更新比如 10 个 epoch样本效率比 A2C 高不少而 A2C 通常一批数据只更新一次。理解了 A2CPPO 的代码基本就是在它的基础上加几行。所以把 A2C 练熟是很划算的投入。7. 常见问题与排查思路训练不收敛是强化学习里最常见的问题而且原因往往不止一个。下面这张表整理了 Actor-Critic 实现中最容易踩的坑按出现频率排序。问题现象常见原因排查与解决思路回报长期在 10~20 徘徊不涨策略过早收敛熵过小或 Actor 输出层初始化增益过大提高 entropy_coef检查 Actor 头是否用了小增益初始化训练前期就崩回报断崖下跌学习率过大未做优势标准化未做梯度裁剪降 lr 到 3e-4加优势标准化和 0.5 的梯度裁剪价值损失震荡不降Critic 学习率相对 Actor 过高或过低奖励尺度太大调整 value_coef对奖励做缩放检查回报计算是否有误回合被截断后价值估计偏低把 truncated 当成 terminated 处理错误切断自举收集时分别记录 terminated 和 truncatedmask 只用 terminated策略完全确定不再探索熵系数为 0 或过小优势全为正/全为负加熵正则检查优势标准化是否生效同一份代码换个种子就崩随机性敏感超参在边界上固定多种子跑对照把 lr 调保守一些增加 rollout 长度GPU 上比 CPU 还慢小任务数据传输开销大于计算收益CartPole 这类任务直接用 CPU训练中途 loss 变 NaN学习率过大log_prob 出现 inf未做数值稳定处理加 eps 到优化器检查 logits 是否溢出降低 lr排查时建议按这个顺序来先确认环境交互正确奖励、终止标记、观测维度再确认优势计算正确可以打印几个样本手工核对最后才动超参数。很多调参调不动的情况根因其实是数据管线里的一个索引错位。还有一个实用技巧把value_loss、policy_loss、entropy、平均回合长度这四个量一起看。如果value_loss在降但回报不涨问题多半在策略侧如果value_loss不降Critic 都没学好策略侧的梯度就不可信先修 Critic。8. 最佳实践与工程建议8.1 训练稳定性优先于收敛速度初学者容易一开始就追求快把学习率调大、rollout 缩短结果训练曲线剧烈震荡根本分不清是算法问题还是超参问题。更稳妥的做法是先让训练稳定跑起来再逐步提速。具体来说学习率从 3e-4 或 7e-4 起步rollout 长度不要低于 16优势一定做标准化梯度一定做裁剪。这四条是底线做到之后大部分小任务都能稳定收敛。等基线跑通了再去做多环境并行、调大网络这类提速优化。8.2 日志要记到能复现问题的程度只打印平均回报是不够的。建议至少记录每次更新的策略损失、价值损失、熵、平均回合长度、当前学习率。有条件的话把策略的动作分布熵和价值的均值也记下来。这些量在出问题时能快速定位是哪一部分失衡。另外随机种子、超参数配置、代码版本都要一起记录。强化学习对随机性极其敏感没有种子记录的实验等于没做。建议把配置写成字典或 YAML训练开始时打印一遍并存档。8.3 代码组织与可复现性网络定义、优势计算、训练循环分开放在不同文件里看起来多写了几行 import但收益在于换算法时只需要改其中一个文件。比如你想从 A2C 换到 PPOmodel.py和gae.py几乎不用动只改训练循环里的损失部分。另一个建议是把环境封装一层。直接在训练循环里调env.step会让代码和具体环境耦合后面想换任务就得改多处。包一个make_env函数统一处理种子、奖励缩放、回合统计训练代码就能保持干净。8.4 生产环境与安全边界如果要把训练好的策略模型部署到真实系统有几条底线必须守住。第一模型输出必须经过动作边界检查超出物理限制的动作要截断或拒绝执行不能直接下发给执行机构。第二任何在线学习或参数更新都必须有回滚机制和人工确认流程绝不能让模型在无人监管的情况下直接修改生产参数。第三训练数据和模型文件要做好版本管理出现异常行为时能快速定位到是哪一版模型。在仿真环境里验证充分之前不要接入真实设备。强化学习策略的失效模式往往是在训练分布内表现良好一旦遇到分布外状态就出现完全不合理的行为这类问题在仿真里很难完全暴露必须有额外的安全兜底逻辑。9. 总结与学习路线回过头看Actor-Critic 的核心其实就三件事用一个策略网络决定动作用一个价值网络提供基准用优势值把两者串起来。网络结构、损失系数、GAE 参数都是在这三件事上做的工程调整理解了主干剩下的变体都是可以推导出来的。如果你已经跑通了本文的 CartPole 示例下一步建议按这个顺序推进。先把手写实现改成多环境并行版本体会同步收集对训练速度的影响然后把策略损失换成 PPO 的截断形式观察同一批数据多轮更新的效果接着把动作空间换成连续型在 Pendulum 上跑一遍高斯策略版本这会逼你处理动作缩放和标准差参数化的问题最后再去看 SAC、DDPG 这类离线策略的 Actor-Critic 变体理解它们如何通过经验回放提升样本效率。每一步都不需要从头造轮子但一定要自己动手改代码、看曲线、调参数。强化学习的很多直觉是看文档看不出来的只有亲自经历过一次改了某个系数训练就崩了下次才知道那个系数的敏感边界在哪里。最后提醒一句训练日志和随机种子一定要留着。当你哪天想复现一个当时跑得特别好的结果却发现自己没存种子的时候那种感觉确实不太好受。