# Actor-Critic：A2C / A3C

> 来源：https://dqn.cn/advanced/actor-critic ｜ 作者：DQN.CN 编辑部 ｜ 发布：2026-10-29 ｜ 更新：2026-10-30 ｜ 许可：CC BY-NC-SA 4.0（文字）/ MIT（代码）

> **要点速览**
>
> - Actor-Critic 同时学习策略（Actor）和价值函数（Critic），用 Critic 估计的优势代替蒙特卡洛回报，降低策略梯度的方差。
> - 优势函数 A(s,a) = Q(s,a) − V(s) 衡量一个动作比平均水平好多少；最简单的估计是 TD 误差 δ_t。
> - GAE（广义优势估计）用 (γλ) 加权的多步 TD 误差之和估计优势，λ 在偏差与方差之间折中，常用 λ=0.95。
> - A3C 用多个异步线程并行采样；A2C 是其同步版本，实现更简单、在 GPU 上更高效。

Actor-Critic 方法同时学习两个部分：**Actor**（策略 $\pi_\theta$）负责选动作，**Critic**（价值函数 $V_w$）负责评价，为 Actor 的策略梯度提供低方差的优势估计。与 [REINFORCE](https://dqn.cn/advanced/reinforce) 不同，Critic 通过 TD 自举学习，因此不必等回合结束就能更新。A3C（Mnih et al., 2016）使用多个异步并行的工作进程采样和更新，A2C 是其同步版本，实践中效果相当且更易实现、更好利用 GPU。广义优势估计（GAE, Schulman et al., 2016）是在偏差与方差之间权衡的标准技巧，也被 [PPO](https://dqn.cn/advanced/ppo) 沿用。

## Actor 与 Critic

### 优势函数

策略梯度可以写成多种等价形式，区别在于用什么量 $\Psi_t$ 乘以 $\nabla_\theta \log \pi_\theta(A_t \mid S_t)$：

| $\Psi_t$ | 方法 | 偏差 / 方差 |
| --- | --- | --- |
| $G_t$ | REINFORCE | 无偏 / 高方差 |
| $G_t - b(S_t)$ | REINFORCE + 基线 | 无偏 / 较低方差 |
| $Q(S_t, A_t)$ | Q Actor-Critic | 依赖 Q 的精度 |
| $A(S_t, A_t) = Q(S_t, A_t) - V(S_t)$ | Advantage Actor-Critic | 方差最低（理论上） |
| $\delta_t = R_{t+1} + \gamma V(S_{t+1}) - V(S_t)$ | TD Actor-Critic | 有偏 / 低方差 |

**优势函数** $A_\pi(s, a) = q_\pi(s, a) - v_\pi(s)$ 衡量动作 $a$ 比策略的平均水平好多少，正优势的动作概率被提高，负优势的被降低。

### TD 误差作为优势估计

不必单独学习 Q。由于

$$
\mathbb{E}\left[R_{t+1} + \gamma v_\pi(S_{t+1}) \mid S_t, A_t\right] = q_\pi(S_t, A_t)
$$

TD 误差 $\delta_t = R_{t+1} + \gamma V_w(S_{t+1}) - V_w(S_t)$ 是优势 $A_\pi(S_t, A_t)$ 的估计（当 $V_w = v_\pi$ 时无偏）。于是只需一个 Critic 网络 $V_w$：

$$
\begin{aligned}
\text{Critic:} &\quad w \leftarrow w + \alpha_w\, \delta_t\, \nabla_w V_w(S_t) \\
\text{Actor:} &\quad \theta \leftarrow \theta + \alpha_\theta\, \delta_t\, \nabla_\theta \log \pi_\theta(A_t \mid S_t)
\end{aligned}
$$

## A3C：异步并行

单个智能体连续采集的数据高度相关，直接用于在线更新会不稳定。DQN 用经验回放解决这个问题，但回放只适用于异策略算法。Mnih 等人（2016）提出另一种思路：**并行运行多个智能体**，各自与环境副本交互，不同环境处于不同状态，数据天然去相关。

A3C（Asynchronous Advantage Actor-Critic）的做法：

- 每个工作线程拷贝全局参数，在自己的环境中跑 $n$ 步（论文中 $t_{\max} = 5$）；
- 用 n 步回报计算优势，计算梯度；
- **异步地**把梯度应用到全局参数上（不加锁，类似 Hogwild!）。

A3C 在多核 CPU 上就能高效训练，当时在 Atari 上以更少的训练时间超过了 DQN。

## A2C：同步版本

后来 OpenAI 的实践发现，异步更新并不是性能的关键。**A2C**（Advantage Actor-Critic）改为**同步**：所有环境同时走一步，凑成一个批次后统一更新。好处是：

- 实现简单，结果可复现；
- 批量计算能更好地利用 GPU；
- 在相同的计算量下效果与 A3C 相当甚至更好。

现在提到“并行环境 + Actor-Critic”，通常指的就是 A2C。

## 广义优势估计（GAE）

用单步 TD 误差估计优势偏差大（依赖 $V$ 的精度），用蒙特卡洛回报方差大。GAE 把多步 TD 误差按 $(\gamma\lambda)^l$ 加权求和：

$$
\hat{A}_t^{\text{GAE}(\gamma, \lambda)} = \sum_{l=0}^{\infty} (\gamma \lambda)^l\, \delta_{t+l}, \qquad \delta_t = R_{t+1} + \gamma V(S_{t+1}) - V(S_t)
$$

- $\lambda = 0$：$\hat{A}_t = \delta_t$，单步 TD，偏差大、方差小；
- $\lambda = 1$：$\hat{A}_t = G_t - V(S_t)$，蒙特卡洛，无偏、方差大。

实现时从后往前递推：$\hat{A}_t = \delta_t + \gamma\lambda (1 - d_t) \hat{A}_{t+1}$，其中 $d_t$ 表示回合在 $t$ 步结束。这与 [TD(λ)](https://dqn.cn/basics/td-learning) 中 λ-回报的思想完全一致。常用 $\lambda = 0.95$。Critic 的回归目标为 $\hat{A}_t + V(S_t)$。

## 熵正则化

为防止策略过早收敛到确定性策略（探索不足），在目标中加入策略熵：

$$
\mathcal{H}(\pi_\theta(\cdot \mid s)) = -\sum_a \pi_\theta(a \mid s) \log \pi_\theta(a \mid s)
$$

完整的 A2C 损失为：

$$
L(\theta, w) = \underbrace{-\frac{1}{N}\sum_t \hat{A}_t \log \pi_\theta(A_t \mid S_t)}_{\text{策略损失}} + c_v \underbrace{\frac{1}{N}\sum_t \left(V_w(S_t) - \hat{R}_t\right)^2}_{\text{价值损失}} - c_e \underbrace{\frac{1}{N}\sum_t \mathcal{H}(\pi_\theta(\cdot \mid S_t))}_{\text{熵奖励}}
$$

常用 $c_v = 0.5$，$c_e = 0.01$。

## 代码实现

下面是 8 个并行环境、每次采样 5 步、带 GAE 和熵正则的 A2C。要点：

- 使用 `SyncVectorEnv`，`autoreset_mode=SAME_STEP` 让结束的环境在同一步自动重置，真正的最终观测在 `info["final_obs"]` 中；
- 对因时间上限被**截断**的回合，把 $\gamma V(s_{\text{final}})$ 加到奖励上再视为结束，这样不会把“到达 500 步”误当成失败；
- Actor 和 Critic 用独立网络，梯度范数裁剪到 0.5。

```python
"""A2C（同步 Advantage Actor-Critic + GAE）：CartPole-v1，8 个并行环境。"""
import gymnasium as gym
import numpy as np
import torch
import torch.nn as nn
from torch.distributions import Categorical

N_ENVS, N_STEPS, TOTAL_STEPS = 8, 5, 200_000
GAMMA, LAM, LR, ENT_COEF, VF_COEF = 0.99, 0.95, 7e-4, 0.01, 0.5
torch.manual_seed(0)

envs = gym.vector.SyncVectorEnv(
    [lambda: gym.make("CartPole-v1") for _ in range(N_ENVS)],
    autoreset_mode=gym.vector.AutoresetMode.SAME_STEP,   # 回合结束时同一步自动 reset
)
envs = gym.wrappers.vector.RecordEpisodeStatistics(envs)
obs_dim, n_actions = envs.single_observation_space.shape[0], envs.single_action_space.n

class ActorCritic(nn.Module):
    def __init__(self):
        super().__init__()
        self.actor = nn.Sequential(nn.Linear(obs_dim, 64), nn.Tanh(), nn.Linear(64, 64), nn.Tanh(), nn.Linear(64, n_actions))
        self.critic = nn.Sequential(nn.Linear(obs_dim, 64), nn.Tanh(), nn.Linear(64, 64), nn.Tanh(), nn.Linear(64, 1))

    def forward(self, x):
        return Categorical(logits=self.actor(x)), self.critic(x).squeeze(-1)

model = ActorCritic()
opt = torch.optim.Adam(model.parameters(), lr=LR)
obs, _ = envs.reset(seed=0)
obs = torch.as_tensor(obs, dtype=torch.float32)
ep_returns = []

for update in range(TOTAL_STEPS // (N_ENVS * N_STEPS)):
    log_probs, values, rewards, dones, entropies = [], [], [], [], []
    for _ in range(N_STEPS):
        dist, v = model(obs)
        a = dist.sample()
        next_obs, r, term, trunc, info = envs.step(a.numpy())
        r = torch.as_tensor(r, dtype=torch.float32)
        # 被截断（truncated）的回合：把最终状态的价值自举进奖励，避免把时间上限误当成失败
        if trunc.any():
            final = torch.as_tensor(np.stack([info["final_obs"][i] if trunc[i] else next_obs[i] for i in range(N_ENVS)]), dtype=torch.float32)
            with torch.no_grad():
                r = r + GAMMA * model(final)[1] * torch.as_tensor(trunc, dtype=torch.float32)
        if "episode" in info:
            ep_returns += list(info["episode"]["r"][info["_episode"]])
        log_probs.append(dist.log_prob(a)); values.append(v); entropies.append(dist.entropy())
        rewards.append(r); dones.append(torch.as_tensor(term | trunc, dtype=torch.float32))
        obs = torch.as_tensor(next_obs, dtype=torch.float32)

    # GAE(λ) 计算优势
    with torch.no_grad():
        next_v = model(obs)[1]
    adv, gae = [], torch.zeros(N_ENVS)
    for t in reversed(range(N_STEPS)):
        v_next = next_v if t == N_STEPS - 1 else values[t + 1].detach()
        delta = rewards[t] + GAMMA * v_next * (1 - dones[t]) - values[t].detach()
        gae = delta + GAMMA * LAM * (1 - dones[t]) * gae
        adv.insert(0, gae)
    adv = torch.stack(adv)
    values, log_probs, entropies = torch.stack(values), torch.stack(log_probs), torch.stack(entropies)
    returns = adv + values.detach()

    loss = -(log_probs * adv).mean() + VF_COEF * ((returns - values) ** 2).mean() - ENT_COEF * entropies.mean()
    opt.zero_grad(); loss.backward()
    nn.utils.clip_grad_norm_(model.parameters(), 0.5)
    opt.step()

    steps = (update + 1) * N_ENVS * N_STEPS
    if steps % 20_000 == 0 and ep_returns:
        print(f"steps {steps:7d} | 最近20回合平均回报 {np.mean(ep_returns[-20:]):6.1f}")

# ---------- 训练结束：用确定性（argmax）策略评估 10 个回合 ----------
eval_env = gym.make("CartPole-v1")
scores = []
for i in range(10):
    o, _ = eval_env.reset(seed=10_000 + i)
    done, total = False, 0.0
    while not done:
        with torch.no_grad():
            act = int(model.actor(torch.as_tensor(o, dtype=torch.float32)).argmax())
        o, r, te, tr, _ = eval_env.step(act)
        total += r; done = te or tr
    scores.append(total)
print(f"贪心评估（10 回合）平均回报: {np.mean(scores):.1f}")
```
实际运行输出（20 万步）：

```text
steps   20000 | 最近20回合平均回报  103.5
steps   40000 | 最近20回合平均回报   52.4
steps   60000 | 最近20回合平均回报  192.6
steps   80000 | 最近20回合平均回报  343.3
steps  100000 | 最近20回合平均回报  476.9
steps  120000 | 最近20回合平均回报  324.4
steps  140000 | 最近20回合平均回报  305.6
steps  160000 | 最近20回合平均回报  448.9
steps  180000 | 最近20回合平均回报  426.1
steps  200000 | 最近20回合平均回报  319.8
贪心评估（10 回合）平均回报: 500.0
```

A2C 在约 10 万步时训练回报接近满分，之后在 300–480 之间波动（训练回报是按概率采样动作得到的）；训练结束后的确定性（argmax）策略在 10 个评估回合中都达到了满分 500。这种波动是同策略 Actor-Critic 的常见现象：一次过大的更新就可能让策略变差。下一节的 [TRPO](https://dqn.cn/advanced/trpo) 和 [PPO](https://dqn.cn/advanced/ppo) 正是为了约束更新幅度而提出的。

## 实践技巧

- **并行环境数量**：8–16 个是常见选择；环境越多，梯度方差越小。
- **n 步长度**：A2C 常用 5 步（短）；PPO 常用 128–2048 步（长）。
- **学习率**：A2C 原实现使用 RMSProp，学习率 $7 \times 10^{-4}$；用 Adam 也可以。
- **共享网络 vs 独立网络**：Atari 等图像任务通常共享卷积层；低维任务用独立网络更稳定。
- **梯度裁剪**：`clip_grad_norm_(0.5)` 几乎是标配。

## 常见误区

- **Critic 的梯度流进了策略损失**：计算策略损失时优势必须 `detach()`。
- **回合结束时仍自举**：真正终止时 $V(S_{t+1})$ 应置 0；截断时应自举。
- **价值损失系数过大**：在共享网络中可能压过策略梯度。
- **把 A3C 的异步当成性能关键**：同步的 A2C 通常同样好。

## 小结

- Actor-Critic = 策略梯度（Actor）+ 价值函数自举（Critic）。
- 优势函数 $A(s, a) = Q(s, a) - V(s)$ 是降低方差的关键，TD 误差是它的估计。
- A3C 用异步并行去相关，A2C 用同步批量，后者更常用。
- GAE 用 $\lambda$ 在偏差和方差间连续调节。

## 练习题

1. 把 GAE 的 $\lambda$ 分别设为 0、0.95、1，比较学习曲线。
2. 去掉熵正则（`ENT_COEF = 0`），策略的熵如何变化？对学习有何影响？
3. 去掉截断时的价值自举，观察训练后期的回报是否受影响。
4. 证明当 $V = v_\pi$ 时，TD 误差 $\delta_t$ 是优势 $A_\pi(S_t, A_t)$ 的无偏估计。
5. 把并行环境数改为 1 和 32，在相同总步数下比较结果。

**延伸阅读**

交互工具：[折扣回报与 GAE 计算器](https://dqn.cn/tools/returns)

## 参考文献

- Konda, V. R., & Tsitsiklis, J. N. (2000). Actor-Critic Algorithms. *NeurIPS*.
- Mnih, V. et al. (2016). Asynchronous Methods for Deep Reinforcement Learning. *ICML*. [arXiv:1602.01783](https://arxiv.org/abs/1602.01783)
- Schulman, J., Moritz, P., Levine, S., Jordan, M., & Abbeel, P. (2016). High-Dimensional Continuous Control Using Generalized Advantage Estimation. *ICLR*. [arXiv:1506.02438](https://arxiv.org/abs/1506.02438)
- Wu, Y., Mansimov, E., Liao, S., Grosse, R., & Ba, J. (2017). Scalable trust-region method for deep reinforcement learning using Kronecker-factored approximation. *NeurIPS*. [arXiv:1708.05144](https://arxiv.org/abs/1708.05144)（ACKTR，文中给出了同步 A2C 的对比）

## 下一步

上一节：[策略梯度：REINFORCE](https://dqn.cn/advanced/reinforce)　｜　继续阅读：[TRPO](https://dqn.cn/advanced/trpo)

## 常见问题速查（FAQ）

**问：A2C 和 A3C 有什么区别？**

答：A3C 的多个工作线程异步地计算梯度并更新共享参数；A2C 等所有环境都走完若干步后同步地做一次更新。实践中 A2C 通常同样有效且更易实现。

**问：GAE 中的 λ 怎么选？**

答：λ=0 只用一步 TD 误差，方差小但偏差大；λ=1 等价于蒙特卡洛优势，无偏但方差大。PPO 等算法常用 λ=0.95、γ=0.99。

**问：为什么要加熵正则化？**

答：在损失中加入策略熵可以防止策略过早变得确定，保持探索；系数通常很小，如 0.01。

## 如何引用本文

DQN.CN 编辑部. Actor-Critic：A2C / A3C[EB/OL]. DQN.CN, 2026-10-29（更新于 2026-10-30）. https://dqn.cn/advanced/actor-critic.
