# SAC：柔性 Actor-Critic

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

> **要点速览**
>
> - SAC（Soft Actor-Critic）是一种基于最大熵框架的异策略 Actor-Critic 算法，目标是在最大化回报的同时最大化策略熵。
> - 最大熵目标为 J(π) = Σ E[r(s_t,a_t) + α·H(π(·|s_t))]，温度 α 控制探索与利用的权衡。
> - SAC 使用两个 Q 网络取最小值、重参数化的高斯策略加 tanh 压缩，并可以自动调节 α 使策略熵接近目标值（常取 −动作维度）。
> - SAC 样本效率高、跨随机种子稳定，是连续控制任务最常用的基线之一。

SAC（Soft Actor-Critic，Haarnoja et al., 2018）基于**最大熵强化学习**框架：在最大化回报的同时最大化策略的熵，鼓励智能体在“同样好”的动作之间保持随机性，从而充分探索、提高鲁棒性。SAC 是异策略算法，使用经验回放，样本效率高；它采用随机策略和双 Q 网络，训练稳定，对超参数不敏感。后续版本（Haarnoja et al., 2018b）引入了自动调节温度系数 $\alpha$ 的机制，进一步免去了最关键超参数的调节。SAC 是目前连续控制领域最常用的算法之一。

## 动机

[DDPG](https://dqn.cn/advanced/ddpg) 和 [TD3](https://dqn.cn/advanced/td3) 使用确定性策略，依赖外加噪声探索，探索方式比较“盲目”；[PPO](https://dqn.cn/advanced/ppo) 使用随机策略，但它是同策略算法，样本效率低。SAC 想同时获得两者的优点：**随机策略的探索能力 + 异策略的样本效率**。

最大熵框架还带来了额外的好处：如果有多条路径都能完成任务，最大熵策略会给它们都分配概率，而不是只押注一条，这让策略对环境扰动更鲁棒，也更容易作为预训练策略迁移到新任务。

## 最大熵强化学习

### 目标函数

标准强化学习最大化期望回报；最大熵强化学习在每一步的奖励上加上策略熵：

$$
J(\pi) = \sum_{t} \mathbb{E}_{(s_t, a_t) \sim \rho_\pi}\left[r(s_t, a_t) + \alpha\, \mathcal{H}\big(\pi(\cdot \mid s_t)\big)\right], \qquad \mathcal{H}(\pi(\cdot \mid s)) = -\mathbb{E}_{a \sim \pi}\left[\log \pi(a \mid s)\right]
$$

**温度系数** $\alpha > 0$ 控制熵的相对重要性：$\alpha \to 0$ 时退化为标准强化学习。

### 软价值函数

在最大熵框架下，价值函数也包含熵项。**软 Q 函数**满足软贝尔曼方程：

$$
Q(s, a) = r(s, a) + \gamma\, \mathbb{E}_{s'}\left[V(s')\right], \qquad
V(s) = \mathbb{E}_{a \sim \pi}\left[Q(s, a) - \alpha \log \pi(a \mid s)\right]
$$

可以证明，软策略迭代（软策略评估 + 软策略改进）会收敛到最大熵目标下的最优策略。在软策略改进中，新策略向 Q 值的玻尔兹曼分布靠拢：

$$
\pi_{\text{new}} = \arg\min_{\pi' \in \Pi} D_{\text{KL}}\left(\pi'(\cdot \mid s)\ \Big\|\ \frac{\exp\left(Q^{\pi_{\text{old}}}(s, \cdot) / \alpha\right)}{Z(s)}\right)
$$

## Soft Q 更新与策略更新

**Critic**：与 TD3 一样使用两个 Q 网络和目标网络，目标值中取两者较小值并减去熵项：

$$
y = r + \gamma (1 - d)\left(\min_{i=1,2} Q_{\phi_i^-}(s', \tilde{a}') - \alpha \log \pi_\theta(\tilde{a}' \mid s')\right), \qquad \tilde{a}' \sim \pi_\theta(\cdot \mid s')
$$

注意 $\tilde{a}'$ 是从**当前策略**中新采样的，而不是来自回放缓冲区。SAC 的第二版去掉了第一版中单独的状态价值网络 $V_\psi$。

**Actor**：最小化上面的 KL 散度，展开后等价于：

$$
L(\theta) = \mathbb{E}_{s \sim \mathcal{D},\, a \sim \pi_\theta}\left[\alpha \log \pi_\theta(a \mid s) - \min_{i=1,2} Q_{\phi_i}(s, a)\right]
$$

即在保持高熵的同时选择 Q 值高的动作。

## 重参数化技巧

Actor 损失中的期望是对 $a \sim \pi_\theta$ 取的，要对 $\theta$ 求梯度，SAC 使用**重参数化技巧**：

$$
a = f_\theta(\epsilon; s) = \tanh\big(\mu_\theta(s) + \sigma_\theta(s) \odot \epsilon\big), \qquad \epsilon \sim \mathcal{N}(0, I)
$$

这样随机性被转移到与参数无关的 $\epsilon$ 上，梯度可以直接通过 $Q(s, f_\theta(\epsilon; s))$ 反向传播到 $\theta$，方差比似然比（REINFORCE 式）梯度小得多。PyTorch 中对应 `Normal.rsample()`。

**tanh 压缩的对数概率修正**：动作被 tanh 压缩到 $(-1, 1)$，由变量替换公式，

$$
\log \pi(a \mid s) = \log \mathcal{N}(u; \mu, \sigma) - \sum_{i} \log\left(1 - \tanh^2(u_i)\right), \qquad a = \tanh(u)
$$

漏掉这一项是实现 SAC 时最常见的错误之一。

## 自动调节温度

$\alpha$ 的最佳取值依赖于任务和奖励尺度，而且在训练过程中也应该变化（初期多探索，后期少探索）。SAC 第二版把它变成一个带约束的优化问题：在保证策略熵不低于目标值 $\bar{\mathcal{H}}$ 的前提下最大化回报。对偶问题给出 $\alpha$ 的损失：

$$
L(\alpha) = \mathbb{E}_{a \sim \pi_\theta}\left[-\alpha\left(\log \pi_\theta(a \mid s) + \bar{\mathcal{H}}\right)\right]
$$

当策略熵低于目标时 $\alpha$ 增大（鼓励探索），高于目标时 $\alpha$ 减小。目标熵通常设为 $\bar{\mathcal{H}} = -\dim(\mathcal{A})$。实现中学习 $\log \alpha$ 以保证 $\alpha > 0$。

## 算法流程

```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̄)
    软更新目标网络
```

## 代码实现

```python
"""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}")
```
实际运行输出（`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.3
```

SAC 在约 6000 步时学会了任务；评估时使用均值动作 $\tanh(\mu_\theta(s))$，10 个回合平均回报为 −166.3。可以看到温度 $\alpha$ 从 1 开始自动下降到约 0.04：随着策略越来越确定，需要的探索越来越少。三个连续控制算法在本例中的确定性评估结果分别为 DDPG −175.6、TD3 −172.7、SAC −166.3，都是单个种子的结果，差异在随机波动范围内，不足以据此排名。

## 离散动作 SAC

SAC 也可以用于离散动作（Christodoulou, 2019，常称 SAC-Discrete）。动作有限时，许多期望可以**精确计算**，不再需要重参数化采样：

- **策略**直接输出各动作的概率 $\pi(\cdot \mid s)$（softmax）；**Q 网络**一次输出所有动作的 Q 值（与 DQN 相同）。
- **软价值**对动作精确求和：
$$
V(s') = \sum_{a'} \pi(a' \mid s') \left[\min_{i=1,2} Q_{\bar\phi_i}(s', a') - \alpha \log \pi(a' \mid s')\right]
$$
- **策略损失**也对动作求和：$L(\theta) = \mathbb{E}_{s}\left[\sum_a \pi_\theta(a \mid s)\left(\alpha \log \pi_\theta(a \mid s) - \min_i Q_{\phi_i}(s, a)\right)\right]$。
- **温度损失**用精确的熵 $\mathcal{H}(\pi(\cdot \mid s)) = -\sum_a \pi \log \pi$ 与目标熵 $\bar{\mathcal{H}}$ 比较。目标熵通常取最大熵 $\log |\mathcal{A}|$ 的一个比例。

```python
"""离散动作 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))
```
设置：CartPole-v1，共 4 万步，前 1000 步随机动作，批量 128，目标熵为 $0.5 \log 2$，3 个种子，每 5000 步用贪心策略（argmax）评估 10 个回合：

| 种子 | 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 之间波动；训练结束时温度 $\alpha$ 约为 0.25–0.28。

**目标熵的选择很关键。** 原论文建议目标熵取 $0.98 \log |\mathcal{A}|$（接近均匀分布）。我们用同样的代码只改这一项跑了 2 个种子：由于策略熵始终达不到这么高的目标，$\alpha$ 一路上升到 2 万以上，采样出的动作接近均匀随机，最近 10 回合的训练回报在 4 万步时跌到约 40–51；但 argmax 贪心策略的评估仍在 3.5 万步达到 500——Q 值本身学得不错，只是“训练时的行为”几乎完全变成了探索。对动作很少的任务，较低的目标熵比例（如 0.5 或更低）更合适。

## 实践技巧

| 超参数 | 常用值 |
| --- | --- |
| 学习率（Actor、Critic、α） | $3 \times 10^{-4}$ |
| 批量大小 | 256 |
| 软更新 $\tau$ | 0.005 |
| 目标熵 $\bar{\mathcal{H}}$ | $-\dim(\mathcal{A})$ |
| $\log \sigma$ 的裁剪范围 | $[-20, 2]$ 或 $[-5, 2]$ |
| 网络 | 2 层 × 256 单元 |

- **奖励尺度**：固定 $\alpha$ 时，奖励的缩放等价于改变温度；使用自动调温可以缓解这个问题。
- **评估用均值动作**：训练时采样，评估时用 $\tanh(\mu)$ 通常表现更好。
- **离散动作版本**：Christodoulou (2019) 提出了 SAC-Discrete，可以直接对动作求期望，不需要重参数化，见上文的 CartPole 实验。

## 常见误区

- **漏掉 tanh 的对数概率修正**：会导致熵估计错误，α 自动调节失效。
- **目标值中的 $\tilde a'$ 用了回放缓冲区中的动作**：应从当前策略中重新采样。
- **α 的梯度流入了 Actor**：更新 Actor 时 α 应视为常数（`detach`），更新 α 时 log π 应 `detach`。
- **直接学习 α 而不是 log α**：α 可能变为负数。

## 小结

- SAC 最大化“回报 + 熵”，兼顾探索与利用，策略更鲁棒。
- 双 Q 网络取小值、软贝尔曼目标、重参数化的随机 Actor 是其核心组件。
- 自动调节温度 $\alpha$ 让 SAC 对超参数不敏感。
- 异策略 + 随机策略，使 SAC 成为连续控制的常用首选。

## 练习题

1. 把 α 固定为 0.2、0.05、0.01（关闭自动调节），比较学习曲线。
2. 去掉 tanh 的对数概率修正项，观察 α 和回报的变化。
3. 推导软贝尔曼方程，并说明 $\alpha \to 0$ 时它退化为什么。
4. 证明对固定的 $Q$，使 $\mathbb{E}_{a \sim \pi}[Q(s, a) - \alpha \log \pi(a \mid s)]$ 最大的策略是 $\pi(a \mid s) \propto \exp(Q(s, a)/\alpha)$。
5. 在 `sac_discrete_cartpole.py` 中把目标熵比例分别设为 0.2、0.5、0.98，记录 α 和训练回报的变化。

**延伸阅读**

[论文速递：SAC 解读](https://dqn.cn/papers/2026-10-sac)

## 参考文献

- 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](https://arxiv.org/abs/1801.01290)
- Haarnoja, T. et al. (2018b). Soft Actor-Critic Algorithms and Applications. [arXiv:1812.05905](https://arxiv.org/abs/1812.05905)（自动调节温度）
- Haarnoja, T., Tang, H., Abbeel, P., & Levine, S. (2017). Reinforcement Learning with Deep Energy-Based Policies. *ICML*. [arXiv:1702.08165](https://arxiv.org/abs/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](https://arxiv.org/abs/1910.07207)

## 下一步

上一节：[TD3](https://dqn.cn/advanced/td3)　｜　继续阅读：[目标导向强化学习与 HER](https://dqn.cn/advanced/goal-conditioned)

## 常见问题速查（FAQ）

**问：SAC 中的温度 α 有什么作用？**

答：α 是熵项的权重：α 大时策略更随机、探索更多；α 小时策略更确定。自动调温会根据当前策略熵与目标熵的差距调整 α，在本站 Pendulum 实验中，α 在训练后期降到约 0.05。

**问：SAC 可以用于离散动作吗？**

答：原始 SAC 面向连续动作。离散动作有专门的 SAC-Discrete 变体，但离散问题通常也可以直接用 DQN 或 PPO。

**问：SAC 和 PPO 哪个好？**

答：SAC 是异策略算法，样本效率通常更高，适合仿真代价高或真实机器人任务；PPO 是同策略算法，实现简单、适用于离散和连续动作，易于大规模并行。

## 如何引用本文

DQN.CN 编辑部. SAC：柔性 Actor-Critic[EB/OL]. DQN.CN, 2026-10-29（更新于 2026-10-30）. https://dqn.cn/advanced/sac.
