主题
SAC:柔性 Actor-Critic
SAC(Soft Actor-Critic,Haarnoja et al., 2018)基于最大熵强化学习框架:在最大化回报的同时最大化策略的熵,鼓励智能体在“同样好”的动作之间保持随机性,从而充分探索、提高鲁棒性。SAC 是异策略算法,使用经验回放,样本效率高;它采用随机策略和双 Q 网络,训练稳定,对超参数不敏感。后续版本(Haarnoja et al., 2018b)引入了自动调节温度系数
动机
DDPG 和 TD3 使用确定性策略,依赖外加噪声探索,探索方式比较“盲目”;PPO 使用随机策略,但它是同策略算法,样本效率低。SAC 想同时获得两者的优点:随机策略的探索能力 + 异策略的样本效率。
最大熵框架还带来了额外的好处:如果有多条路径都能完成任务,最大熵策略会给它们都分配概率,而不是只押注一条,这让策略对环境扰动更鲁棒,也更容易作为预训练策略迁移到新任务。
最大熵强化学习
目标函数
标准强化学习最大化期望回报;最大熵强化学习在每一步的奖励上加上策略熵:
温度系数
软价值函数
在最大熵框架下,价值函数也包含熵项。软 Q 函数满足软贝尔曼方程:
可以证明,软策略迭代(软策略评估 + 软策略改进)会收敛到最大熵目标下的最优策略。在软策略改进中,新策略向 Q 值的玻尔兹曼分布靠拢:
Soft Q 更新与策略更新
Critic:与 TD3 一样使用两个 Q 网络和目标网络,目标值中取两者较小值并减去熵项:
注意
Actor:最小化上面的 KL 散度,展开后等价于:
即在保持高熵的同时选择 Q 值高的动作。
重参数化技巧
Actor 损失中的期望是对
这样随机性被转移到与参数无关的 Normal.rsample()。
tanh 压缩的对数概率修正:动作被 tanh 压缩到
漏掉这一项是实现 SAC 时最常见的错误之一。
自动调节温度
当策略熵低于目标时
算法流程
text
初始化 Actor π_θ、Q_φ1、Q_φ2、目标网络 Q_φ1⁻、Q_φ2⁻、log α、回放缓冲区 D
对每一步:
a ~ π_θ(·|s)(前若干步用均匀随机动作),执行并存入 D
从 D 采样小批量 (s, a, r, s', d)
ã' ~ π_θ(·|s')
y = r + γ(1−d) [min_i Q_φi⁻(s', ã') − α log π_θ(ã'|s')]
更新 Q_φ1、Q_φ2:最小化 (Q_φi(s,a) − y)²
ã ~ π_θ(·|s)(重参数化)
更新 Actor:最小化 α log π_θ(ã|s) − min_i Q_φi(s, ã)
更新 α:最小化 −α (log π_θ(ã|s) + H̄)
软更新目标网络1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
代码实现
py
"""SAC(自动调节温度 α):Pendulum-v1。"""
import copy
import random
import gymnasium as gym
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.distributions import Normal
GAMMA, TAU, LR, BATCH = 0.99, 0.005, 3e-4, 256
TOTAL_STEPS, START_STEPS = 20_000, 1_000
random.seed(0); np.random.seed(0); torch.manual_seed(0)
env = gym.make("Pendulum-v1")
obs_dim, act_dim = env.observation_space.shape[0], env.action_space.shape[0]
max_action = float(env.action_space.high[0])
def mlp(i, o):
return nn.Sequential(nn.Linear(i, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU(), nn.Linear(256, o))
class Actor(nn.Module):
"""高斯策略 + tanh 压缩:a = tanh(u), u ~ N(μ(s), σ(s))"""
def __init__(self):
super().__init__()
self.net = mlp(obs_dim, 2 * act_dim)
def forward(self, s):
mu, log_std = self.net(s).chunk(2, dim=-1)
log_std = log_std.clamp(-20, 2)
dist = Normal(mu, log_std.exp())
u = dist.rsample() # 重参数化采样,梯度可回传
a = torch.tanh(u)
# tanh 变换的对数概率修正:log π(a|s) = log N(u) - Σ log(1 - tanh(u)^2)
log_prob = dist.log_prob(u).sum(-1) - torch.log(1 - a.pow(2) + 1e-6).sum(-1)
return a, log_prob, torch.tanh(mu)
actor = Actor()
q1, q2 = mlp(obs_dim + act_dim, 1), mlp(obs_dim + act_dim, 1)
q1_t, q2_t = copy.deepcopy(q1), copy.deepcopy(q2)
opt_a = torch.optim.Adam(actor.parameters(), lr=LR)
opt_q = torch.optim.Adam(list(q1.parameters()) + list(q2.parameters()), lr=LR)
log_alpha = torch.zeros(1, requires_grad=True) # 学习 log α 保证 α > 0
opt_alpha = torch.optim.Adam([log_alpha], lr=LR)
target_entropy = -act_dim # 常用经验值:-|A|
S = np.zeros((TOTAL_STEPS, obs_dim), np.float32); A = np.zeros((TOTAL_STEPS, act_dim), np.float32)
R = np.zeros((TOTAL_STEPS, 1), np.float32); S2 = np.zeros_like(S); D = np.zeros_like(R)
obs, _ = env.reset(seed=0)
ep_ret, returns = 0.0, []
for step in range(TOTAL_STEPS):
if step < START_STEPS:
a = np.random.uniform(-1, 1, act_dim).astype(np.float32)
else:
with torch.no_grad():
a = actor(torch.as_tensor(obs, dtype=torch.float32))[0].numpy()
obs2, r, term, trunc, _ = env.step(a * max_action)
S[step], A[step], R[step], S2[step], D[step] = obs, a, r, obs2, float(term)
obs, ep_ret = obs2, ep_ret + r
if term or trunc:
returns.append(ep_ret); obs, _ = env.reset(); ep_ret = 0.0
if len(returns) % 10 == 0:
print(f"step {step + 1:6d} | 最近10回合平均回报 {np.mean(returns[-10:]):8.1f} | α={log_alpha.exp().item():.3f}")
if step >= START_STEPS:
i = np.random.randint(0, step + 1, BATCH)
s, a_b, r_b, s2, d = (torch.as_tensor(x[i]) for x in (S, A, R, S2, D))
alpha = log_alpha.exp().detach()
# ---- Soft Q 更新:y = r + γ (min Q_targ(s',a') - α log π(a'|s')) ----
with torch.no_grad():
a2, logp2, _ = actor(s2)
sa2 = torch.cat([s2, a2], 1)
y = r_b + GAMMA * (1 - d) * (torch.min(q1_t(sa2), q2_t(sa2)) - alpha * logp2.unsqueeze(1))
sa = torch.cat([s, a_b], 1)
q_loss = F.mse_loss(q1(sa), y) + F.mse_loss(q2(sa), y)
opt_q.zero_grad(); q_loss.backward(); opt_q.step()
# ---- 策略更新:min E[α log π(a|s) - min Q(s,a)] ----
a_new, logp, _ = actor(s)
sa_new = torch.cat([s, a_new], 1)
a_loss = (alpha * logp - torch.min(q1(sa_new), q2(sa_new)).squeeze(1)).mean()
opt_a.zero_grad(); a_loss.backward(); opt_a.step()
# ---- 温度更新:让策略熵接近 target_entropy ----
alpha_loss = -(log_alpha * (logp.detach() + target_entropy)).mean()
opt_alpha.zero_grad(); alpha_loss.backward(); opt_alpha.step()
with torch.no_grad():
for net, tgt in ((q1, q1_t), (q2, q2_t)):
for p, tp in zip(net.parameters(), tgt.parameters()):
tp.mul_(1 - TAU).add_(TAU * p)
scores = []
for k in range(10):
o, _ = env.reset(seed=1000 + k); done, total = False, 0.0
while not done:
with torch.no_grad():
act = actor(torch.as_tensor(o, dtype=torch.float32))[2].numpy() # 评估时用均值动作 tanh(μ)
o, r, te, tr, _ = env.step(act * max_action)
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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
实际运行输出(Pendulum-v1,2 万步):
text
step 2000 | 最近10回合平均回报 -1327.5 | α=0.745
step 4000 | 最近10回合平均回报 -940.6 | α=0.466
step 6000 | 最近10回合平均回报 -140.7 | α=0.307
step 8000 | 最近10回合平均回报 -287.7 | α=0.192
step 10000 | 最近10回合平均回报 -188.6 | α=0.130
step 12000 | 最近10回合平均回报 -132.3 | α=0.093
step 14000 | 最近10回合平均回报 -166.5 | α=0.075
step 16000 | 最近10回合平均回报 -177.1 | α=0.061
step 18000 | 最近10回合平均回报 -141.1 | α=0.049
step 20000 | 最近10回合平均回报 -190.3 | α=0.041
确定性策略评估(10 回合)平均回报: -166.31
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
SAC 在约 6000 步时学会了任务;评估时使用均值动作
离散动作 SAC
SAC 也可以用于离散动作(Christodoulou, 2019,常称 SAC-Discrete)。动作有限时,许多期望可以精确计算,不再需要重参数化采样:
- 策略直接输出各动作的概率
(softmax);Q 网络一次输出所有动作的 Q 值(与 DQN 相同)。 - 软价值对动作精确求和:
- 策略损失也对动作求和:
。 - 温度损失用精确的熵
与目标熵 比较。目标熵通常取最大熵 的一个比例。
py
"""离散动作 SAC:CartPole-v1。
与连续 SAC 的区别:策略直接输出各动作概率,软价值、策略损失和温度损失都可以对动作“精确求期望”,不需要重参数化采样。
用法:python sac_discrete_cartpole.py [种子];DQN_QUICK=1 只跑很短的版本。"""
import copy
import os
import sys
import gymnasium as gym
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
QUICK = os.environ.get("DQN_QUICK") == "1"
torch.set_num_threads(1)
TOTAL_STEPS = 5_000 if QUICK else 40_000
START_STEPS, BATCH, GAMMA, TAU, LR = 1_000, 128, 0.99, 0.005, 3e-4
TARGET_ENT_RATIO = 0.5 # 目标熵 = 0.5 × log|A|。实测取 0.98(原论文)时 α 持续增大到上万,采样策略接近均匀随机
def mlp(i, o):
return nn.Sequential(nn.Linear(i, 128), nn.ReLU(), nn.Linear(128, 128), nn.ReLU(), nn.Linear(128, o))
def run(seed):
rng = np.random.default_rng(seed); torch.manual_seed(seed)
env, test_env = gym.make("CartPole-v1"), gym.make("CartPole-v1")
nS, nA = env.observation_space.shape[0], env.action_space.n
actor, q1, q2 = mlp(nS, nA), mlp(nS, nA), mlp(nS, nA) # Q 网络一次输出所有动作的 Q 值
q1_t, q2_t = copy.deepcopy(q1), copy.deepcopy(q2)
opt_a = torch.optim.Adam(actor.parameters(), lr=LR)
opt_q = torch.optim.Adam(list(q1.parameters()) + list(q2.parameters()), lr=LR)
log_alpha = torch.zeros(1, requires_grad=True); opt_al = torch.optim.Adam([log_alpha], lr=LR)
target_ent = TARGET_ENT_RATIO * np.log(nA)
buf = {k: np.zeros((TOTAL_STEPS, d), np.float32) for k, d in (("s", nS), ("a", 1), ("r", 1), ("s2", nS), ("d", 1))}
def evaluate(n=10):
tot = []
for k in range(n):
o, _ = test_env.reset(seed=10_000 + k); done, G = False, 0.0
while not done:
with torch.no_grad():
a = int(actor(torch.as_tensor(o)).argmax())
o, r, te, tr, _ = test_env.step(a); G += r; done = te or tr
tot.append(G)
return float(np.mean(tot))
o, _ = env.reset(seed=seed); log, ep_ret, rets = [], 0.0, []
for step in range(TOTAL_STEPS):
if step < START_STEPS:
a = int(rng.integers(nA))
else:
with torch.no_grad():
a = int(torch.distributions.Categorical(logits=actor(torch.as_tensor(o))).sample())
o2, r, te, tr, _ = env.step(a); ep_ret += r
for k, v in (("s", o), ("a", [a]), ("r", [r]), ("s2", o2), ("d", [float(te)])): # 只有真正终止才截断自举
buf[k][step] = v
o = o2
if te or tr:
rets.append(ep_ret); ep_ret = 0.0; o, _ = env.reset()
if step + 1 < START_STEPS:
continue
j = rng.integers(0, step + 1, BATCH)
s, a_, r_, s2, d = (torch.as_tensor(buf[k][j]) for k in ("s", "a", "r", "s2", "d"))
alpha = log_alpha.exp().detach()
with torch.no_grad(): # 软价值 V(s') = Σ_a π(a|s') [min Q(s',a) - α log π(a|s')]
logp2 = F.log_softmax(actor(s2), -1); p2 = logp2.exp()
v2 = (p2 * (torch.min(q1_t(s2), q2_t(s2)) - alpha * logp2)).sum(-1, keepdim=True)
y = r_ + GAMMA * (1 - d) * v2
a_idx = a_.long()
lq = F.mse_loss(q1(s).gather(1, a_idx), y) + F.mse_loss(q2(s).gather(1, a_idx), y)
opt_q.zero_grad(); lq.backward(); opt_q.step()
logp = F.log_softmax(actor(s), -1); p = logp.exp()
with torch.no_grad():
qmin = torch.min(q1(s), q2(s))
la = (p * (alpha * logp - qmin)).sum(-1).mean() # 策略损失:对动作精确求期望
opt_a.zero_grad(); la.backward(); opt_a.step()
ent = -(p.detach() * logp.detach()).sum(-1)
lal = (log_alpha * (ent - target_ent)).mean() # 熵高于目标时降低 α,反之升高
opt_al.zero_grad(); lal.backward(); opt_al.step()
with torch.no_grad():
for n_, t_ in ((q1, q1_t), (q2, q2_t)):
for p_, tp in zip(n_.parameters(), t_.parameters()):
tp.mul_(1 - TAU).add_(TAU * p_)
if (step + 1) % (1000 if QUICK else 5000) == 0:
log.append((step + 1, evaluate()))
print(f" 种子{seed} 步数 {step + 1:6d} 最近10回合训练回报 {np.mean(rets[-10:]):6.1f} 贪心评估 {log[-1][1]:6.1f} α={alpha.item():.3f}", flush=True)
return log
if __name__ == "__main__":
seeds = [int(sys.argv[1])] if len(sys.argv) > 1 else ([0] if QUICK else [0, 1, 2])
for sd in seeds:
log = run(sd)
print(f"种子{sd}: " + " ".join(f"{s}:{v:.0f}" for s, v in log))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
89
90
91
92
93
94
95
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
89
90
91
92
93
94
95
设置:CartPole-v1,共 4 万步,前 1000 步随机动作,批量 128,目标熵为
| 种子 | 5000 步 | 1 万步 | 2 万步 | 2.5 万步 | 3 万步 | 3.5 万步 | 4 万步 |
|---|---|---|---|---|---|---|---|
| 0 | 214 | 242 | 222 | 301 | 498 | 496 | 500 |
| 1 | 300 | 215 | 236 | 500 | 500 | 500 | 500 |
| 2 | 332 | 381 | 284 | 360 | 467 | 459 | 385 |
种子 0、1 在 3 万步左右稳定达到满分,种子 2 在 4 万步时仍在 385–467 之间波动;训练结束时温度
目标熵的选择很关键。 原论文建议目标熵取
实践技巧
| 超参数 | 常用值 |
|---|---|
| 学习率(Actor、Critic、α) | |
| 批量大小 | 256 |
| 软更新 | 0.005 |
| 目标熵 | |
| 网络 | 2 层 × 256 单元 |
- 奖励尺度:固定
时,奖励的缩放等价于改变温度;使用自动调温可以缓解这个问题。 - 评估用均值动作:训练时采样,评估时用
通常表现更好。 - 离散动作版本:Christodoulou (2019) 提出了 SAC-Discrete,可以直接对动作求期望,不需要重参数化,见上文的 CartPole 实验。
常见误区
- 漏掉 tanh 的对数概率修正:会导致熵估计错误,α 自动调节失效。
- 目标值中的
用了回放缓冲区中的动作:应从当前策略中重新采样。 - α 的梯度流入了 Actor:更新 Actor 时 α 应视为常数(
detach),更新 α 时 log π 应detach。 - 直接学习 α 而不是 log α:α 可能变为负数。
小结
- SAC 最大化“回报 + 熵”,兼顾探索与利用,策略更鲁棒。
- 双 Q 网络取小值、软贝尔曼目标、重参数化的随机 Actor 是其核心组件。
- 自动调节温度
让 SAC 对超参数不敏感。 - 异策略 + 随机策略,使 SAC 成为连续控制的常用首选。
练习题
- 把 α 固定为 0.2、0.05、0.01(关闭自动调节),比较学习曲线。
- 去掉 tanh 的对数概率修正项,观察 α 和回报的变化。
- 推导软贝尔曼方程,并说明
时它退化为什么。 - 证明对固定的
,使 最大的策略是 。 - 在
sac_discrete_cartpole.py中把目标熵比例分别设为 0.2、0.5、0.98,记录 α 和训练回报的变化。
延伸阅读
参考文献
- Haarnoja, T., Zhou, A., Abbeel, P., & Levine, S. (2018). Soft Actor-Critic: Off-Policy Maximum Entropy Deep Reinforcement Learning with a Stochastic Actor. ICML. arXiv:1801.01290
- Haarnoja, T. et al. (2018b). Soft Actor-Critic Algorithms and Applications. arXiv:1812.05905(自动调节温度)
- Haarnoja, T., Tang, H., Abbeel, P., & Levine, S. (2017). Reinforcement Learning with Deep Energy-Based Policies. ICML. arXiv:1702.08165(Soft Q-learning)
- Ziebart, B. D. (2010). Modeling Purposeful Adaptive Behavior with the Principle of Maximum Causal Entropy. PhD thesis, Carnegie Mellon University.
- Christodoulou, P. (2019). Soft Actor-Critic for Discrete Action Settings. arXiv:1910.07207
下一步
上一节:TD3 | 继续阅读:目标导向强化学习与 HER