主题
Actor-Critic:A2C / A3C
Actor-Critic 方法同时学习两个部分:Actor(策略
Actor 与 Critic
优势函数
策略梯度可以写成多种等价形式,区别在于用什么量
| 方法 | 偏差 / 方差 | |
|---|---|---|
| REINFORCE | 无偏 / 高方差 | |
| REINFORCE + 基线 | 无偏 / 较低方差 | |
| Q Actor-Critic | 依赖 Q 的精度 | |
| Advantage Actor-Critic | 方差最低(理论上) | |
| TD Actor-Critic | 有偏 / 低方差 |
优势函数
TD 误差作为优势估计
不必单独学习 Q。由于
TD 误差
A3C:异步并行
单个智能体连续采集的数据高度相关,直接用于在线更新会不稳定。DQN 用经验回放解决这个问题,但回放只适用于异策略算法。Mnih 等人(2016)提出另一种思路:并行运行多个智能体,各自与环境副本交互,不同环境处于不同状态,数据天然去相关。
A3C(Asynchronous Advantage Actor-Critic)的做法:
- 每个工作线程拷贝全局参数,在自己的环境中跑
步(论文中 ); - 用 n 步回报计算优势,计算梯度;
- 异步地把梯度应用到全局参数上(不加锁,类似 Hogwild!)。
A3C 在多核 CPU 上就能高效训练,当时在 Atari 上以更少的训练时间超过了 DQN。
A2C:同步版本
后来 OpenAI 的实践发现,异步更新并不是性能的关键。A2C(Advantage Actor-Critic)改为同步:所有环境同时走一步,凑成一个批次后统一更新。好处是:
- 实现简单,结果可复现;
- 批量计算能更好地利用 GPU;
- 在相同的计算量下效果与 A3C 相当甚至更好。
现在提到“并行环境 + Actor-Critic”,通常指的就是 A2C。
广义优势估计(GAE)
用单步 TD 误差估计优势偏差大(依赖
: ,单步 TD,偏差大、方差小; : ,蒙特卡洛,无偏、方差大。
实现时从后往前递推:
熵正则化
为防止策略过早收敛到确定性策略(探索不足),在目标中加入策略熵:
完整的 A2C 损失为:
常用
代码实现
下面是 8 个并行环境、每次采样 5 步、带 GAE 和熵正则的 A2C。要点:
- 使用
SyncVectorEnv,autoreset_mode=SAME_STEP让结束的环境在同一步自动重置,真正的最终观测在info["final_obs"]中; - 对因时间上限被截断的回合,把
加到奖励上再视为结束,这样不会把“到达 500 步”误当成失败; - Actor 和 Critic 用独立网络,梯度范数裁剪到 0.5。
py
"""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}")1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
实际运行输出(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.01
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
A2C 在约 10 万步时训练回报接近满分,之后在 300–480 之间波动(训练回报是按概率采样动作得到的);训练结束后的确定性(argmax)策略在 10 个评估回合中都达到了满分 500。这种波动是同策略 Actor-Critic 的常见现象:一次过大的更新就可能让策略变差。下一节的 TRPO 和 PPO 正是为了约束更新幅度而提出的。
实践技巧
- 并行环境数量:8–16 个是常见选择;环境越多,梯度方差越小。
- n 步长度:A2C 常用 5 步(短);PPO 常用 128–2048 步(长)。
- 学习率:A2C 原实现使用 RMSProp,学习率
;用 Adam 也可以。 - 共享网络 vs 独立网络:Atari 等图像任务通常共享卷积层;低维任务用独立网络更稳定。
- 梯度裁剪:
clip_grad_norm_(0.5)几乎是标配。
常见误区
- Critic 的梯度流进了策略损失:计算策略损失时优势必须
detach()。 - 回合结束时仍自举:真正终止时
应置 0;截断时应自举。 - 价值损失系数过大:在共享网络中可能压过策略梯度。
- 把 A3C 的异步当成性能关键:同步的 A2C 通常同样好。
小结
- Actor-Critic = 策略梯度(Actor)+ 价值函数自举(Critic)。
- 优势函数
是降低方差的关键,TD 误差是它的估计。 - A3C 用异步并行去相关,A2C 用同步批量,后者更常用。
- GAE 用
在偏差和方差间连续调节。
练习题
- 把 GAE 的
分别设为 0、0.95、1,比较学习曲线。 - 去掉熵正则(
ENT_COEF = 0),策略的熵如何变化?对学习有何影响? - 去掉截断时的价值自举,观察训练后期的回报是否受影响。
- 证明当
时,TD 误差 是优势 的无偏估计。 - 把并行环境数改为 1 和 32,在相同总步数下比较结果。
延伸阅读
交互工具:折扣回报与 GAE 计算器
参考文献
- 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
- Schulman, J., Moritz, P., Levine, S., Jordan, M., & Abbeel, P. (2016). High-Dimensional Continuous Control Using Generalized Advantage Estimation. ICLR. arXiv: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(ACKTR,文中给出了同步 A2C 的对比)
下一步
上一节:策略梯度:REINFORCE | 继续阅读:TRPO