
很多人第一次自己动手写强化学习训练循环时都会撞上同一堵墙用纯策略梯度REINFORCE跑 CartPole前几十轮回报看着还行再往后曲线就开始剧烈抖动同样一套超参数换个随机种子结果能差出一倍。想调它又不知道从哪下手——是学习率不对还是采样太少还是这方法本身就不稳更麻烦的是当你去看 PPO、A3C、SAC、DDPG 这些几乎撑起整个现代深度强化学习实践的算法时会发现它们的代码骨架惊人地相似一个网络出动作分布另一个网络出价值估计然后拿价值当“尺子”去修正策略。这个共同骨架就是 Actor Critic。这篇文章要讲的不是某一个算法而是一套被反复验证过的网络架构范式。Actor 负责决定“在什么状态下做什么”Critic 负责评估“这个状态到底有多好”。两者共用一个特征提取主干各自带一个输出头用 Critic 的价值估计替代 REINFORCE 里那条噪声极大的完整回报从而把策略梯度的方差压下来。理解了这一层你会发现自己看 PPO 的 clip 目标、看 SAC 的软价值、看 GAE 的 λ 参数时能很快定位到它们在同一个框架里各自改的是哪一块。我的判断是Actor Critic 真正降低的不是“算法精度”这件事的成本而是策略梯度的方差成本。纯策略梯度必须等一整条轨迹跑完才能算出回报再用这个高方差信号去更新策略而 Actor Critic 允许你在每一步都拿到一个相对低方差的优势估计用更高的样本利用率换取更稳定的梯度方向。代价是引入了一个会“估错”的 Critic——这也是后面所有工程坑的源头。所以这篇文章会从问题出发一路写到能跑先讲清 Actor Critic 到底解决了什么、网络结构怎么拆、优势函数和 TD 误差的数学关系再给出一套完整可复制的 PyTorch 实现包括网络定义、动作采样、GAE 优势估计、损失组装和训练循环最后把训练不收敛、价值损失爆炸、熵过早塌缩这些真实项目里最常遇到的问题逐个拆开。如果你是刚学完策略梯度、正准备往 PPO 走的人或者你已经在用 PPO 但说不清它为什么这么设计这篇内容应该能帮你把中间那块缺失的拼图补上。1. 策略梯度训练不稳Actor Critic 到底在解决什么问题要理解 Actor Critic 的价值得先看清纯策略梯度的具体病灶。REINFORCE 的核心逻辑是跑完一整条轨迹把这条轨迹上每个动作的对数概率乘以从该步开始的累计回报再对参数求梯度。数学上它是无偏的但这个“无偏”是靠极高方差换来的。因为一条轨迹的累计回报里混入了大量与该步动作无关的随机因素——环境本身的转移噪声、后续动作的随机性、甚至只是这一局运气好。梯度信号被这些噪声稀释之后参数更新的方向就会飘。更具体一点想象你在 CartPole 里推了 3 步小车就倒了累计回报是 12。这个 12 里前两步的动作可能都是对的第三步错了。但 REINFORCE 会把这个 12 同样地分摊给每一步的对数概率结果就是“正确的动作被奖励得不够错误的动作被惩罚得不够”。你只能靠大量采样让统计规律浮现这就是为什么纯策略梯度那么样本饥渴。引入一个基线可以缓解这个问题。数学上有一个很漂亮的结论在对数概率项上减去一个只与状态有关的函数 b(s)期望梯度不变但方差会下降。最自然的基线就是状态价值 V(s)——也就是“从当前状态出发按当前策略走下去平均能拿多少分”。减掉它之后剩下的部分就是“这一步实际拿到的比平均水平好多少”这就是优势函数的直觉来源。而 Actor Critic 的精髓就是把 V(s) 用一个神经网络来学而不是等轨迹跑完再算。这样一来架构上就自然分出了两个角色。Actor 是策略网络输入状态输出动作分布Critic 是价值网络输入状态输出一个标量表示这个状态的平均预期回报。训练时Actor 用 Critic 给出的优势信号更新Critic 自己用 TD 目标更新。两个网络互相配合Critic 慢慢变准Actor 的梯度就慢慢变稳。理解了这一层你就明白为什么 Actor Critic 不是“一个算法”而是一个可以衍生出无数变体的框架。A2C 是在线同步版本A3C 是把多个环境副本的梯度异步汇总PPO 是在 Actor 更新时加了 clip 约束防止一步走太远SAC 把 Critic 换成 Q 网络并加入熵正则DDPG 则是确定性策略配 Q 网络。它们的差异都在“怎么估优势”“怎么限制更新幅度”这些细节上而“Actor 出动作、Critic 出评估”这个骨架一直没变。这里还想提醒一个容易被忽略的点Actor Critic 并不是在所有场景下都优于纯值函数方法。如果你的动作空间是离散且有限的Q-learning 那一套DQN 系列往往更简单也更好调只有当动作空间连续、或者你明确需要一个随机策略时Actor Critic 的优势才真正显现。因为 Q-learning 要对每个动作求最大值连续动作空间下这个 max 没有解析解必须再套一个优化过程工程复杂度会陡增。而 Actor 天然可以输出连续分布这是它的结构性优势。2. Actor Critic 的核心概念与网络结构拆解2.1 Actor从状态到动作分布的映射Actor 的本质是一个参数化策略 π_θ(a|s)。在离散动作场景下它通常输出一个长度为动作数的 logits 向量经过 softmax 变成概率分布再从中采样在连续动作场景下它输出高斯分布的均值 μ 和标准差 σ或对数标准差然后从这个高斯分布采样并做 tanh 截断。这里真正容易踩坑的地方是Actor 输出的不是一个确定的动作而是一个分布的参数。很多人第一次写会习惯性地对 logits 取 argmax 直接输出动作结果训练出来的策略完全没有探索能力一旦初始化偏差大就再也不会有机会纠正。为什么必须用采样因为策略梯度的推导本身就基于“从策略分布中采样动作”这个前提。如果你用 argmax策略就退化成了确定性映射对数概率这一项也就不再良定义。当然确定性策略梯度DPG是另一条技术路线但它需要配合 Q 网络和额外的探索噪声不属于本文讨论的范围。另一个关键细节是 log 概率的计算。离散场景下用Categorical(logits...)的log_prob连续场景下用Normal(mu, std)的log_prob并做维度求和。需要注意 PyTorch 的Normal在 std 接近 0 时数值会不稳所以实践里通常让网络输出log_std并做 clamp比如限制在 [-20, 2] 区间内。2.2 Critic把“这个状态有多好”量化成一个数Critic 学习的是状态价值函数 V_φ(s)输入观测输出一个标量。它的训练目标来自贝尔曼方程的自举bootstrapping当前状态的价值应该等于即时奖励加上折扣后的下一状态价值。也就是V(s) ≈ r γ·V(s)。训练时把这个右边当作回归目标不参与求导让左边去拟合它这就是 TD 学习的基本做法。Critic 的质量直接决定了 Actor 的梯度质量。如果 Critic 估值系统性偏高Actor 会以为某些动作特别好而过度强化如果偏低则会把好的动作压下去。更隐蔽的问题是Critic 和 Actor 共享主干网络时Critic 的损失梯度会通过共享层回传到特征提取部分如果价值损失的量级远大于策略损失特征就会被“拉偏”导致 Actor 学不到有用的表征。这里有个实用经验共享主干时一定要对价值损失加权通常取 0.5同时把优势做标准化减均值除标准差让策略损失的数值量级稳定在 1 附近。这两步在实现里几乎不花成本但对训练稳定性影响很大。2.3 共享主干与双头输出为什么不干脆用两个独立网络Actor 和 Critic 的组合方式有两种。一种是完全独立的两个网络各自有独立的参数另一种是共享特征提取层在最后分叉出两个输出头。这两种方式各有取舍选错了会在调试阶段多走不少弯路。对比维度共享主干 双头完全独立网络参数量少特征复用多接近两倍训练速度前向一次拿到两个输出快需要两次前向慢特征冲突风险存在价值梯度可能污染策略特征不存在互不干扰调参难度需要平衡两个损失的权重各自独立调更直观适用场景观测维度高、任务相关性强的图像/高维输入观测维度低、价值与策略需要不同抽象的场景在 CartPole 这类低维观测4 维的任务上共享主干基本没坏处但在 Atari 这类图像输入任务中共享主干能显著减少参数量并加速收敛。而在一些动作敏感的任务里比如机械臂精细控制策略需要更细粒度的特征价值只需要粗略判断这时候独立网络往往效果更好。实际项目中更推荐先用共享主干跑通如果发现价值损失一直压不下去或者策略更新异常再拆成独立网络对比。3. 从策略梯度定理到优势函数算法背后的数学3.1 策略梯度的原始形式策略梯度的基本形式可以写成梯度的期望等于对数概率的梯度乘以回报。用伪代码表达就是grad ∇log π(a|s) · R。这个式子的含义很直观如果某个动作最终带来了高回报就提高它被选中的概率反之降低。但它的问题前面已经说过——R 是整条轨迹的累计回报包含了大量与该动作无关的信息。打个比方你在一条路上开车最后到达目的地花了 30 分钟。这个 30 分钟里你第 2 个路口的变道决策只贡献了一小部分但 REINFORCE 会把整段 30 分钟的“成绩单”都归因到那一次变道上。归因越粗糙梯度噪声就越大。3.2 基线的作用只改变方差不改变期望关键结论是减去任意只依赖状态 s 的基线函数 b(s)策略梯度在期望意义下保持不变。证明只需要用到Σ_a π(a|s) ∇log π(a|s) ∇Σ_a π(a|s) ∇1 0。这个性质非常实用因为它意味着我们可以放心地减掉一个估计出来的值而不用担心引入偏差。最优的基线在理论上是状态价值函数 V(s)减去它之后剩下的项就是优势函数 A(s,a) Q(s,a) - V(s)。优势函数的语义非常清楚在当前状态下选择动作 a 比按平均水平随便选能多拿多少回报。正优势意味着“这个动作比平均好”负优势意味着“不如平均”。这种相对化的信号比绝对回报更适合做策略更新因为它自动消除了不同状态之间的基准差异。3.3 用 TD 误差近似优势偏差与方差的取舍理论上 A(s,a) 需要知道 Q 和 V 两个函数但我们只有 V。于是有了几种近似方式它们构成了一条从高偏差到高方差的谱系单步 TD 误差δ r γV(s) - V(s)。只用一步真实奖励加一次自举方差极小但如果 V 不准偏差就大。n 步回报往前看 n 步真实奖励再自举。n 越大方差越大、偏差越小。蒙特卡洛看完整条轨迹无偏但方差极大本质上就是回到 REINFORCE。GAE广义优势估计对多个 n 步优势做指数加权平均用一个参数 λ 在偏差和方差之间连续调节。GAE 的递推形式是A_t δ_t γλ·A_{t1}其中 δ_t 是单步 TD 误差。当 λ0 时退化成单步 TD当 λ1 时退化成蒙特卡洛。实践中最常用的取值是 λ0.95既保留了大部分真实信号又能把方差压到可接受范围。理解了这条谱系你再看 PPO 论文里为什么用 GAE、为什么 λ 通常取 0.95就不会觉得是玄学了。那个参数就是在调“我有多相信 Critic 的估计”这个问题。4. 环境准备与依赖安装本文的示例代码基于 Python 和 PyTorch环境管理建议用 conda 或 venv 隔离。具体版本请以你实际项目的依赖解析结果为准这里只给出经过验证的组合思路Python 3.10 及以上、PyTorch 2.x 系列、gymnasium 0.29 及以上。gymnasium 是 OpenAI Gym 的维护分支API 有变化主要是reset()返回(obs, info)元组、step()返回五元组(obs, reward, terminated, truncated, info)这一点在迁移老代码时特别容易出问题。创建环境并安装依赖# 创建并激活虚拟环境 conda create -n actor_critic_demo python3.10 -y conda activate actor_critic_demo # 安装 PyTorchCPU 版本足够跑 CartPole # 如果你的机器有 CUDA请到 PyTorch 官网选择对应的安装命令 pip install torch --index-url https://download.pytorch.org/whl/cpu # 安装环境与工具库 pip install gymnasium[classic-control]0.29 numpy matplotlib安装完成后建议跑一次快速验证确认 gymnasium 的 API 形态符合预期# 文件路径check_env.py import gymnasium as gym env gym.make(CartPole-v1) obs, info env.reset(seed42) print(observation shape:, obs.shape) print(action space:, env.action_space) total 0 for step in range(200): action env.action_space.sample() obs, reward, terminated, truncated, info env.step(action) total reward if terminated or truncated: print(fepisode finished at step {step}, return{total}) break env.close()如果这段代码能正常打印出观测形状(4,)和动作空间Discrete(2)说明环境没问题。如果报ValueError: too many values to unpack基本可以断定是装了老的gym而不是gymnasium或者代码里混用了两套 API。这是新手最常见的第一道坎遇到时先检查pip list | grep gym的输出。