# TD3：双延迟深度确定性策略梯度

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

> **要点速览**
>
> - TD3 在 DDPG 基础上加入三项改进，以缓解函数近似带来的 Q 值高估。
> - 截断双 Q 学习：训练两个 Critic，计算目标时取二者的较小值 min(Q₁, Q₂)。
> - 延迟策略更新：Critic 每更新若干次（通常 2 次），Actor 和目标网络才更新一次。
> - 目标策略平滑：计算目标时给目标动作加上截断的高斯噪声，使 Q 函数在动作附近更平滑。

TD3（Twin Delayed DDPG，Fujimoto et al., 2018）针对 [DDPG](https://dqn.cn/advanced/ddpg) 中函数近似误差导致的 Q 值高估和训练不稳定问题，提出了三项简单而有效的改进：使用两个 Critic 取较小值（Clipped Double Q-learning）、延迟更新 Actor，以及目标策略平滑。这些改动只需修改几行代码，却让 TD3 在 MuJoCo 连续控制基准上大幅超过 DDPG，成为连续控制中强有力的基线之一。

## 动机：Actor-Critic 中的高估问题

在 [Double DQN](https://dqn.cn/advanced/dqn-improvements) 中我们看到，对带噪声的估计取最大值会导致高估。在 Actor-Critic 中，虽然没有显式的 $\max$，但 Actor 被训练成最大化 $Q_\phi(s, \mu_\theta(s))$——它会**主动寻找 Critic 高估的动作**，效果相当于取了最大值。Fujimoto 等人在论文中通过实验证实了 DDPG 的价值估计显著高于真实回报，而高估误差又会通过 TD 自举不断累积。

他们还发现 Double DQN 的做法（在线网络选动作、目标网络评估）在 Actor-Critic 中效果有限，因为策略变化缓慢，在线网络和目标网络太相似，无法提供独立的估计。

## 三项关键改进

### Clipped Double Q-learning

训练两个独立的 Critic $Q_{\phi_1}, Q_{\phi_2}$，计算目标时取两者的**较小值**：

$$
y = r + \gamma (1 - d) \min_{i=1,2} Q_{\phi_i^-}\big(s', \tilde{a}'\big)
$$

两个 Critic 都回归到同一个目标 $y$。取最小值可能导致**低估**，但低估不会像高估那样被 Actor 主动利用和放大，因此更安全。Actor 只用第一个 Critic 更新：$\max_\theta Q_{\phi_1}(s, \mu_\theta(s))$。

### 延迟策略更新

如果 Critic 的估计还不准，Actor 跟着不准的梯度更新只会让情况更糟（策略变差 → 价值估计更差）。TD3 让 Critic **每更新 $d$ 次（通常 $d = 2$），Actor 和目标网络才更新一次**。这样 Actor 总是基于相对准确的 Q 值更新，减小了误差的累积。

### 目标策略平滑

确定性策略容易对 Q 函数中尖锐的峰值过拟合。TD3 在计算目标时，给目标动作加上裁剪过的噪声：

$$
\tilde{a}' = \text{clip}\Big(\mu_{\theta^-}(s') + \text{clip}(\epsilon, -c, c),\ a_{\text{low}},\ a_{\text{high}}\Big), \qquad \epsilon \sim \mathcal{N}(0, \sigma^2)
$$

这相当于让 Q 值在动作的小邻域内取平均，起到正则化的作用：相似的动作应该有相似的价值。常用 $\sigma = 0.2$，$c = 0.5$（相对于归一化的动作范围）。

## 算法流程

```text
初始化 Actor μ_θ、两个 Critic Q_φ1, Q_φ2 及其目标网络，回放缓冲区 D
对每一步 t：
    a = clip(μ_θ(s) + N(0, σ_explore))，执行并存入 D
    从 D 采样小批量 (s, a, r, s', d)
    ã' = clip(μ_θ⁻(s') + clip(N(0, σ), −c, c))          # 目标策略平滑
    y = r + γ(1−d) · min(Q_φ1⁻(s', ã'), Q_φ2⁻(s', ã'))   # 取较小值
    更新两个 Critic：最小化 Σ_i (Q_φi(s,a) − y)²
    若 t mod d == 0：                                     # 延迟更新
        更新 Actor：最大化 Q_φ1(s, μ_θ(s))
        软更新所有目标网络
```

## 代码实现

TD3 的代码在 [DDPG](https://dqn.cn/advanced/ddpg) 的基础上只改了三处（双 Critic、目标平滑、延迟更新），其余设置完全相同，便于对比。

```python
"""TD3：Pendulum-v1（双 Critic 取小 + 延迟策略更新 + 目标策略平滑）。"""
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

GAMMA, TAU, LR_A, LR_C, BATCH = 0.99, 0.005, 1e-3, 1e-3, 256
TOTAL_STEPS, START_STEPS, EXPL_NOISE = 20_000, 1_000, 0.1
POLICY_NOISE, NOISE_CLIP, POLICY_DELAY = 0.2, 0.5, 2
random.seed(0); np.random.seed(0); torch.manual_seed(0)

env = gym.make("Pendulum-v1")
env.action_space.seed(0)  # 预热阶段的随机动作也要可复现
obs_dim, act_dim = env.observation_space.shape[0], env.action_space.shape[0]
max_action = float(env.action_space.high[0])        # Pendulum 的力矩范围 [-2, 2]

def mlp(i, o, out_act=None):
    layers = [nn.Linear(i, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU(), nn.Linear(256, o)]
    return nn.Sequential(*layers, *( [out_act] if out_act else [] ))

actor = mlp(obs_dim, act_dim, nn.Tanh())             # μ(s) ∈ [-1, 1]，再乘 max_action
critic1, critic2 = mlp(obs_dim + act_dim, 1), mlp(obs_dim + act_dim, 1)   # 两个独立的 Q 网络
actor_t, critic1_t, critic2_t = copy.deepcopy(actor), copy.deepcopy(critic1), copy.deepcopy(critic2)
opt_a = torch.optim.Adam(actor.parameters(), lr=LR_A)
opt_c = torch.optim.Adam(list(critic1.parameters()) + list(critic2.parameters()), lr=LR_C)

class Replay:
    def __init__(self, size=100_000):
        self.s = np.zeros((size, obs_dim), np.float32); self.a = np.zeros((size, act_dim), np.float32)
        self.r = np.zeros((size, 1), np.float32); self.s2 = np.zeros((size, obs_dim), np.float32)
        self.d = np.zeros((size, 1), np.float32); self.ptr, self.n, self.size = 0, 0, size

    def add(self, s, a, r, s2, d):
        self.s[self.ptr], self.a[self.ptr], self.r[self.ptr], self.s2[self.ptr], self.d[self.ptr] = s, a, r, s2, d
        self.ptr = (self.ptr + 1) % self.size; self.n = min(self.n + 1, self.size)

    def sample(self, k):
        i = np.random.randint(0, self.n, k)
        return [torch.as_tensor(x[i]) for x in (self.s, self.a, self.r, self.s2, self.d)]

def soft_update(net, target):
    with torch.no_grad():
        for p, tp in zip(net.parameters(), target.parameters()):
            tp.mul_(1 - TAU).add_(TAU * p)

buf = Replay()
obs, _ = env.reset(seed=0)
ep_ret, returns = 0.0, []
for step in range(TOTAL_STEPS):
    if step < START_STEPS:
        a = env.action_space.sample()                                   # 预热：均匀随机动作
    else:
        with torch.no_grad():
            a = max_action * actor(torch.as_tensor(obs, dtype=torch.float32)).numpy()
        a = np.clip(a + np.random.normal(0, EXPL_NOISE * max_action, act_dim), -max_action, max_action)
    obs2, r, term, trunc, _ = env.step(a)
    buf.add(obs, a / max_action, 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}")

    if step >= START_STEPS:
        s, a_b, r_b, s2, d = buf.sample(BATCH)
        with torch.no_grad():
            # 目标策略平滑：给目标动作加裁剪过的高斯噪声（动作已归一化到 [-1, 1]）
            noise = (torch.randn_like(a_b) * POLICY_NOISE).clamp(-NOISE_CLIP, NOISE_CLIP)
            a2 = (actor_t(s2) + noise).clamp(-1, 1)
            sa2 = torch.cat([s2, a2], 1)
            y = r_b + GAMMA * (1 - d) * torch.min(critic1_t(sa2), critic2_t(sa2))   # 取两个 Q 的较小值
        sa = torch.cat([s, a_b], 1)
        c_loss = F.mse_loss(critic1(sa), y) + F.mse_loss(critic2(sa), y)
        opt_c.zero_grad(); c_loss.backward(); opt_c.step()

        if step % POLICY_DELAY == 0:                                     # 延迟更新 Actor 与目标网络
            a_loss = -critic1(torch.cat([s, actor(s)], 1)).mean()
            opt_a.zero_grad(); a_loss.backward(); opt_a.step()
            soft_update(actor, actor_t); soft_update(critic1, critic1_t); soft_update(critic2, critic2_t)

# 确定性策略评估
scores = []
for i in range(10):
    o, _ = env.reset(seed=1000 + i); done, total = False, 0.0
    while not done:
        with torch.no_grad():
            o, r, te, tr, _ = env.step(max_action * actor(torch.as_tensor(o, dtype=torch.float32)).numpy())
        total += r; done = te or tr
    scores.append(total)
print(f"确定性策略评估（10 回合）平均回报: {np.mean(scores):.1f}")
```
实际运行输出（`Pendulum-v1`，2 万步）：

```text
step   2000 | 最近10回合平均回报  -1348.5
step   4000 | 最近10回合平均回报  -1309.6
step   6000 | 最近10回合平均回报   -582.4
step   8000 | 最近10回合平均回报   -133.3
step  10000 | 最近10回合平均回报   -190.0
step  12000 | 最近10回合平均回报   -147.8
step  14000 | 最近10回合平均回报   -177.6
step  16000 | 最近10回合平均回报   -187.7
step  18000 | 最近10回合平均回报   -144.4
step  20000 | 最近10回合平均回报   -202.4
确定性策略评估（10 回合）平均回报: -169.1
```

TD3 在约 8000 步时学会了任务，确定性策略评估平均为 −169.1，与 DDPG（−168.0）相当。由于延迟更新，TD3 的 Actor 更新次数只有 DDPG 的一半，早期学得稍慢一些。在 Pendulum 这样简单的任务上，DDPG 本身就能工作，TD3 的优势不明显；论文中 TD3 的显著优势来自 HalfCheetah、Hopper、Walker2d、Ant 等更难的 MuJoCo 任务（单个种子的小实验不足以比较两者的优劣）。

## 与 DDPG、SAC 的比较

| | DDPG | TD3 | SAC |
| --- | --- | --- | --- |
| 策略 | 确定性 | 确定性 | 随机（高斯 + tanh） |
| Critic 数量 | 1 | 2（取小） | 2（取小） |
| 探索 | 外加噪声 | 外加噪声 | 最大熵，内在随机性 |
| Actor 更新频率 | 每步 | 每 2 步 | 每步 |
| 目标策略平滑 | 无 | 有 | 随机策略自带 |
| 超参数敏感性 | 高 | 中 | 低（自动调温） |

## 实践技巧

- 作者官方代码仓库（sfujim/TD3）当前的默认超参数：学习率 $3 \times 10^{-4}$、批量 256、$\tau = 0.005$、探索噪声 0.1、目标噪声 0.2、噪声裁剪 0.5、延迟 $d = 2$、预热 25000 步（MuJoCo）。论文原版的部分数值（如学习率 $10^{-3}$、批量 100）与此略有不同。
- 官方实现：[github.com/sfujim/TD3](https://github.com/sfujim/TD3)，代码简洁，值得阅读。
- 目标策略平滑的噪声大小要与动作范围匹配：本例中动作已归一化到 $[-1, 1]$。

## 常见误区

- **两个 Critic 用了不同的目标**：两者都应回归到同一个 $y$（取最小值后的结果）。
- **Actor 用两个 Critic 的最小值更新**：原论文只用 $Q_{\phi_1}$，用最小值也可以但不是标准做法。
- **延迟更新时忘记同时延迟目标网络的软更新**。
- **目标平滑的噪声不裁剪**：可能产生过大的扰动，偏离真实动作。

## 小结

- TD3 用 Clipped Double Q-learning 抑制高估，用延迟更新减少误差累积，用目标策略平滑正则化 Q 函数。
- 三项改动都很简单，但对稳定性提升显著。
- 在简单任务上与 DDPG 差别不大，在复杂连续控制任务上优势明显。

## 练习题

1. 分别去掉三项改进中的一项（消融实验），在 Pendulum 上观察影响。
2. 记录训练过程中两个 Critic 的 Q 估计差异。
3. 把延迟 $d$ 改为 1、2、5，比较结果。
4. 为什么取两个 Critic 的最小值会导致低估？低估为什么比高估更“安全”？
5. 在 `HalfCheetah-v5`（需安装 MuJoCo）上比较 DDPG 和 TD3（需要较长训练时间）。

## 参考文献

- Fujimoto, S., van Hoof, H., & Meger, D. (2018). Addressing Function Approximation Error in Actor-Critic Methods. *ICML*. [arXiv:1802.09477](https://arxiv.org/abs/1802.09477)
- van Hasselt, H. (2010). Double Q-learning. *NeurIPS*.
- Lillicrap, T. P. et al. (2016). Continuous control with deep reinforcement learning. *ICLR*. [arXiv:1509.02971](https://arxiv.org/abs/1509.02971)

## 下一步

上一节：[DDPG](https://dqn.cn/advanced/ddpg)　｜　继续阅读：[SAC](https://dqn.cn/advanced/sac)

## 常见问题速查（FAQ）

**问：TD3 比 DDPG 好在哪里？**

答：TD3 显著缓解了 DDPG 的 Q 值高估和训练不稳定问题，在 MuJoCo 等较难的连续控制任务上通常明显优于 DDPG。在 Pendulum 这样的简单任务上两者差别不大。

**问：TD3 和 SAC 应该选哪个？**

答：两者都是强大的离策略连续控制算法。SAC 使用随机策略和熵正则，探索更好、对超参数更不敏感；TD3 实现更简单、策略是确定性的。可以都试一试。

**问：为什么取两个 Q 的最小值？**

答：单个 Q 网络的估计误差会被策略优化利用而产生系统性高估；取两个独立估计的最小值得到偏保守的目标，抵消这种高估。

## 如何引用本文

DQN.CN 编辑部. TD3：双延迟深度确定性策略梯度[EB/OL]. DQN.CN, 2026-10-29（更新于 2026-10-30）. https://dqn.cn/advanced/td3.
