# 离线强化学习

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

> **要点速览**
>
> - 离线强化学习只使用事先收集好的固定数据集学习策略，不再与环境交互，适合医疗、自动驾驶等在线试错代价高的场景。
> - 核心难题是外推误差：Q 函数对数据中没有出现过的动作估计不准，而策略优化恰好会选中这些被高估的动作。
> - 主流解决思路包括约束策略接近数据（BCQ、TD3+BC）、保守地压低分布外动作的价值（CQL）、只在数据内动作上学习（IQL），以及序列建模（Decision Transformer）。
> - 离线 RL 并不总是优于行为克隆，效果高度依赖数据集的质量和构成。

离线强化学习（Offline RL，也称 Batch RL）只利用**事先收集好的固定数据集**学习策略，训练过程中不再与环境交互。它适用于医疗、自动驾驶、推荐系统、工业控制等在线试错成本高或有风险、但已经积累了大量历史数据的场景。核心挑战是**分布偏移**：学到的策略可能选择数据集中很少出现的动作，而 Q 函数对这些动作的估计往往严重偏高，又无法通过交互来纠正。代表方法包括 BCQ、TD3+BC、CQL、IQL 和 Decision Transformer。

## 问题设定

给定数据集 $\mathcal{D} = \{(s_i, a_i, r_i, s'_i, d_i)\}$，由某个（或多个、未知的）**行为策略** $\pi_\beta$ 收集。目标是学到一个策略 $\pi$，使其在真实环境中的回报尽可能高，而训练期间**不能**收集任何新数据。

### 与在线 RL、模仿学习的区别

| | 在线 RL | 离线 RL | 模仿学习（BC） |
| --- | --- | --- | --- |
| 与环境交互 | 可以 | 不可以 | 不可以 |
| 需要奖励 | 是 | 是 | 否 |
| 数据质量要求 | — | 任意（可以是次优数据） | 需要专家数据 |
| 能否超过数据中的策略 | — | 有可能 | 一般不能 |

离线 RL 的吸引力在于：它有可能从**次优**数据中“拼接”出比任何一条数据轨迹都好的策略，而 [行为克隆](https://dqn.cn/advanced/imitation-learning) 最多只能模仿数据中的行为。

### 分布偏移与外推误差

为什么不直接在固定数据上跑 DQN 或 SAC？考虑 Q-learning 的目标：

$$
y = r + \gamma \max_{a'} Q(s', a')
$$

$\max$ 会查询**所有**动作的 Q 值，包括数据中从未出现过的动作（分布外动作，OOD）。神经网络对这些动作的估计没有数据约束，可能任意偏高（**外推误差**，Fujimoto et al., 2019）。在线 RL 中，智能体会去尝试这些动作，发现它们并不好，从而纠正估计；离线情况下没有这个纠错机制，高估会通过自举不断传播，最终策略会选择这些“看起来很好、实际上很差”的动作。

离线 RL 的各种方法本质上都在回答：**如何让策略或 Q 函数待在数据分布附近？**

## 策略约束方法：BCQ、TD3+BC

**BCQ**（Batch-Constrained Q-learning，Fujimoto et al., 2019）：用生成模型（VAE）拟合行为策略，只在生成模型认为“数据中可能出现”的动作中选取 Q 值最高的，从而避免 OOD 动作。

**TD3+BC**（Fujimoto & Gu, 2021）：极简的做法——在 [TD3](https://dqn.cn/advanced/td3) 的 Actor 损失上加一个行为克隆项：

$$
\pi = \arg\max_\pi\ \mathbb{E}_{(s, a) \sim \mathcal{D}}\left[\lambda\, Q(s, \pi(s)) - \left(\pi(s) - a\right)^2\right]
$$

其中 $\lambda$ 根据 Q 值的尺度归一化。只改了几行代码，却在 D4RL 基准上与当时更复杂的方法相当。

## 保守价值估计：CQL

**CQL**（Conservative Q-Learning，Kumar et al., 2020）不约束策略，而是直接让 Q 函数变得**保守**：在标准 TD 损失之外，压低所有动作（特别是 OOD 动作）的 Q 值，同时抬高数据中动作的 Q 值。对离散动作，常用的 CQL(H) 正则项为：

$$
\mathcal{L}_{\text{CQL}}(\theta) = \alpha\, \mathbb{E}_{s \sim \mathcal{D}}\left[\log \sum_a \exp Q_\theta(s, a) - \mathbb{E}_{a \sim \mathcal{D}}\left[Q_\theta(s, a)\right]\right] + \frac{1}{2}\mathbb{E}_{\mathcal{D}}\left[\left(Q_\theta(s, a) - y\right)^2\right]
$$

$\log\sum\exp$ 是 $\max$ 的软近似，最小化它会压低最大的 Q 值。论文证明了在适当条件下，学到的 Q 函数是真实价值的**下界**，因此策略不会被高估的 OOD 动作吸引。

## IQL

**IQL**（Implicit Q-Learning，Kostrikov et al., 2022）更进一步：训练过程中**完全不查询数据集外的动作**。它用期望分位数回归（expectile regression）从数据中的 $Q(s, a)$ 学习 $V(s)$，近似“数据支持范围内的最大值”：

$$
L_V(\psi) = \mathbb{E}_{(s, a) \sim \mathcal{D}}\left[L_2^\tau\left(Q(s, a) - V_\psi(s)\right)\right], \qquad L_2^\tau(u) = |\tau - \mathbb{1}(u < 0)|\, u^2
$$

Q 的目标改为 $r + \gamma V(s')$，避免了对 $a'$ 取 max。最后用优势加权回归提取策略：$\max_\pi \mathbb{E}_\mathcal{D}[\exp(\beta (Q(s, a) - V(s))) \log \pi(a \mid s)]$。IQL 实现简单、稳定，也适合离线到在线的微调。

## 序列建模视角：Decision Transformer

**Decision Transformer**（Chen et al., 2021）完全换了一个视角：把强化学习看成**条件序列建模**。把轨迹表示为 $(\hat{R}_1, s_1, a_1, \hat{R}_2, s_2, a_2, \dots)$，其中 $\hat{R}_t = \sum_{t' \ge t} r_{t'}$ 是剩余回报（return-to-go），用 GPT 式 Transformer 以自回归方式预测动作。测试时给定一个期望的目标回报，模型就生成对应的动作。它不需要 TD 学习和价值函数，在多个基准上取得了有竞争力的结果，但后续研究也指出，它在需要“拼接”次优轨迹的任务上不如基于 TD 的方法。

## 代码：CartPole 上的离线 RL 玩具实验

为了直观看到分布偏移的影响，我们在 CartPole 上做一个小实验。用一个手写的“专家”规则 $a = \mathbb{1}[\theta + 0.5\dot\theta > 0]$（实测可以稳定拿满 500 分）收集两种数据集：

1. **纯专家数据**：5000 条转移，没有任何随机动作——动作覆盖极窄；
2. **高噪声数据**：20000 条转移，80% 的概率执行随机动作。

在每个数据集上分别训练：行为克隆（BC）、朴素离线 DQN、离散动作 CQL（$\alpha = 1$），然后在真实环境中评估 20 个回合。

```python
"""离线强化学习玩具实验（CartPole-v1）：行为克隆（BC） vs 朴素离线 DQN vs 离散动作 CQL。
专家规则 a = 1[θ + 0.5·θ̇ > 0]。对比两个数据集：
  (1) 纯专家数据（5000 条，无噪声，动作覆盖极窄）
  (2) 高噪声数据（20000 条，80% 概率随机动作）"""
import random

import gymnasium as gym
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F

random.seed(0); np.random.seed(0); torch.manual_seed(0)
env = gym.make("CartPole-v1")
env.action_space.seed(0)  # action_space.sample() 使用独立的随机数生成器
GAMMA = 0.99

def expert(o):
    return int(o[2] + 0.5 * o[3] > 0)

# ---------- 1) 收集固定数据集，之后不再与环境交互 ----------
def collect(eps_data, n_transitions):
    data, ep_returns = [], []
    o, _ = env.reset(seed=0); G = 0.0
    while len(data) < n_transitions:
        a = env.action_space.sample() if random.random() < eps_data else expert(o)
        o2, r, te, tr, _ = env.step(a)
        data.append((o, a, r, o2, float(te))); G += r; o = o2
        if te or tr:
            ep_returns.append(G); G = 0.0; o, _ = env.reset()
    S, A, R, S2, D = (torch.as_tensor(np.array(x), dtype=torch.float32) for x in zip(*data))
    return S, A.long(), R, S2, D, np.mean(ep_returns)

def net():
    return nn.Sequential(nn.Linear(4, 128), nn.ReLU(), nn.Linear(128, 128), nn.ReLU(), nn.Linear(128, 2))

def evaluate(model, n=20):
    e = gym.make("CartPole-v1"); tot = []
    for i in range(n):
        o, _ = e.reset(seed=10_000 + i); done, g = False, 0.0
        while not done:
            with torch.no_grad():
                o, r, te, tr, _ = e.step(int(model(torch.as_tensor(o, dtype=torch.float32)).argmax()))
            g += r; done = te or tr
        tot.append(g)
    return np.mean(tot)

def train_bc(steps=5_000):
    pi = net(); opt = torch.optim.Adam(pi.parameters(), lr=1e-3)
    for _ in range(steps):
        i = torch.randint(0, len(S), (256,))
        loss = F.cross_entropy(pi(S[i]), A[i])
        opt.zero_grad(); loss.backward(); opt.step()
    return pi

def train_q(cql_alpha=0.0, steps=20_000):
    q, q_t = net(), net(); q_t.load_state_dict(q.state_dict())
    opt = torch.optim.Adam(q.parameters(), lr=3e-4)
    for t in range(steps):
        i = torch.randint(0, len(S), (256,))
        qs = q(S[i])
        q_sa = qs.gather(1, A[i, None]).squeeze(1)
        with torch.no_grad():
            y = R[i] + GAMMA * (1 - D[i]) * q_t(S2[i]).max(1).values
        loss = F.mse_loss(q_sa, y)
        if cql_alpha > 0:   # CQL 正则：压低所有动作的 Q（logsumexp），抬高数据中动作的 Q
            loss = loss + cql_alpha * (torch.logsumexp(qs, dim=1) - q_sa).mean()
        opt.zero_grad(); loss.backward(); opt.step()
        if t % 500 == 0:
            q_t.load_state_dict(q.state_dict())
    return q

for name, eps_data, n in [("纯专家数据", 0.0, 5_000), ("高噪声数据", 0.8, 20_000)]:
    S, A, R, S2, D, behavior_return = collect(eps_data, n)
    print(f"== {name}：{n} 条转移，行为策略平均回报 {behavior_return:.1f}")
    print(f"   行为克隆 BC  : 评估回报 {evaluate(train_bc()):6.1f}")
    print(f"   朴素离线 DQN : 评估回报 {evaluate(train_q(0.0)):6.1f}")
    print(f"   CQL (α=1.0)  : 评估回报 {evaluate(train_q(1.0)):6.1f}")
```
实际运行输出：

```text
== 纯专家数据：5000 条转移，行为策略平均回报 500.0
   行为克隆 BC  : 评估回报  500.0
   朴素离线 DQN : 评估回报    9.4
   CQL (α=1.0)  : 评估回报  500.0
== 高噪声数据：20000 条转移，行为策略平均回报 39.3
   行为克隆 BC  : 评估回报  498.7
   朴素离线 DQN : 评估回报  189.9
   CQL (α=1.0)  : 评估回报  267.9
```

结果解读（单次运行，玩具规模，请勿过度推广）：

- **纯专家数据上，朴素离线 DQN 彻底失败**（9.4 分，比随机策略还差）。数据中每个状态只出现专家选择的那一个动作，另一个动作的 Q 值完全没有数据约束，$\max$ 操作恰恰会选中这些被高估的分布外动作——这正是外推误差的典型表现。加上 CQL 正则后，分布外动作的 Q 值被压低，策略回到数据分布内，拿到满分。
- **高噪声数据上，CQL 比朴素 DQN 好一些（267.9 对 189.9），但两者都远不如行为克隆（498.7）**。原因在于这个数据集的特殊性：80% 随机 + 20% 专家，意味着在每个状态下专家动作出现的概率约为 $0.2 + 0.8 \times 0.5 = 0.6$，高于另一个动作，所以 BC 学到的“最常见动作”恰好就是专家动作。而 Q-learning 类方法需要从行为策略平均只能坚持约 39 步的短轨迹中正确估计长期价值，在 2 万步的训练预算内没有学好。
- **高噪声数据上 CQL 与朴素 DQN 的相对优劣并不稳定。** 在给 `env.action_space` 设置种子之前，我们的两次运行（数据集中的随机动作不同）分别得到 DQN 118.8 / CQL 138.0 和 DQN 397.1 / CQL 134.8，而 BC 三次都在 470 分以上。也就是说，在这个玩具设置下“BC 远好于两种 Q-learning 方法”是稳定的结论，“CQL 优于朴素 DQN”则不是——要得出可靠结论需要多个种子、多份数据集。
- 这提醒我们：离线 RL 方法并不总是优于简单的 BC，结果高度依赖数据集的构成。Kumar 等人（2022, *When Should We Prefer Offline Reinforcement Learning Over Behavioral Cloning?*）对此有系统的讨论。

## 代码：Pendulum 上的连续动作 CQL

离散动作的 CQL 可以直接对所有动作求 $\log\sum_a \exp Q(s,a)$；连续动作没法枚举，常见做法（也是 CQL 原论文的 CQL(H) 实现）是用采样近似这个 logsumexp：对每个状态采 $N$ 个均匀随机动作和 $N$ 个当前策略的动作，用各自的采样密度做重要性加权：

$$
\log\sum_a e^{Q(s,a)} \;\approx\; \log \frac{1}{2N}\Big[\sum_{i=1}^{N}\frac{e^{Q(s,a^{\text{unif}}_i)}}{\mu(a^{\text{unif}}_i)} + \sum_{i=1}^{N}\frac{e^{Q(s,a^{\pi}_i)}}{\pi(a^{\pi}_i\mid s)}\Big]
$$

再把 $\alpha\,\big(\text{上式} - Q(s,a_{\text{data}})\big)$ 加到 SAC 的 critic 损失上，其余部分（双 Q、自动温度、软目标更新）与 [SAC](https://dqn.cn/advanced/sac) 相同。

实验设置：

- **数据集**：在线 SAC 训练 6000 步得到的完整回放缓冲区（前 1000 步为均匀随机动作），共 30 个回合，回合回报从约 −1800 到接近 0，平均约 −820。这类“从差到好混在一起”的数据类似 D4RL 的 medium-replay。
- **三种方法**，各用这份固定数据训练 2 万步、不与环境交互：行为克隆 BC、朴素离线 SAC（不加任何约束）、CQL（$\alpha=5$，$N=10$）。
- 在真实环境中用确定性策略评估 10 个回合；另外记录训练结束时 Q 网络对数据集中 $(s,a)$ 的平均估计。

```python
"""连续动作离线强化学习：Pendulum-v1 上的 BC、朴素离线 SAC 与 CQL(H)（基于 SAC）。
数据集：在线 SAC 训练前 DATA_STEPS 步的完整回放缓冲区（前 1000 步为随机动作），质量从随机到较好混杂，
类似 D4RL 的 "medium-replay"。之后三种方法只用这份固定数据训练，不再与环境交互。
用法：python cql_pendulum.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
from torch.distributions import Normal

QUICK = os.environ.get("DQN_QUICK") == "1"
torch.set_num_threads(1)
DATA_STEPS = 2_000 if QUICK else 6_000
OFFLINE_STEPS = 2_000 if QUICK else 20_000
GAMMA, TAU, LR, BATCH, A_MAX = 0.99, 0.005, 3e-4, 256, 2.0
CQL_ALPHA, CQL_N = 5.0, 10            # CQL 正则系数、每个状态采样的动作数

def mlp(i, o, h=256):
    return nn.Sequential(nn.Linear(i, h), nn.ReLU(), nn.Linear(h, h), nn.ReLU(), nn.Linear(h, o))

class Actor(nn.Module):
    def __init__(self):
        super().__init__(); self.net = mlp(3, 2)

    def forward(self, s):
        mu, log_std = self.net(s).chunk(2, -1)
        d = Normal(mu, log_std.clamp(-20, 2).exp()); u = d.rsample(); a = torch.tanh(u)
        logp = d.log_prob(u).sum(-1) - torch.log(1 - a.pow(2) + 1e-6).sum(-1)
        return a, logp, torch.tanh(mu)

class SAC:
    def __init__(self):
        self.actor, self.q1, self.q2 = Actor(), mlp(4, 1), mlp(4, 1)
        self.q1_t, self.q2_t = copy.deepcopy(self.q1), copy.deepcopy(self.q2)
        self.opt_a = torch.optim.Adam(self.actor.parameters(), lr=LR)
        self.opt_q = torch.optim.Adam(list(self.q1.parameters()) + list(self.q2.parameters()), lr=LR)
        self.log_alpha = torch.zeros(1, requires_grad=True); self.opt_al = torch.optim.Adam([self.log_alpha], lr=LR)

    def update(self, s, a, r, s2, cql=0.0):
        alpha = self.log_alpha.exp().detach()
        with torch.no_grad():
            a2, lp2, _ = self.actor(s2); sa2 = torch.cat([s2, a2], 1)
            y = r + GAMMA * (torch.min(self.q1_t(sa2), self.q2_t(sa2)) - alpha * lp2[:, None])
        sa = torch.cat([s, a], 1); q1, q2 = self.q1(sa), self.q2(sa)
        lq = F.mse_loss(q1, y) + F.mse_loss(q2, y)
        if cql > 0:
            # CQL(H)：压低 logsumexp_a Q(s,a)（用均匀动作 + 当前策略动作做重要性采样估计），抬高数据中的 Q(s,a)
            B = len(s); s_rep = s.repeat_interleave(CQL_N, 0)
            a_rand = torch.rand(B * CQL_N, 1) * 2 - 1
            with torch.no_grad():
                a_pi, lp_pi, _ = self.actor(s_rep)
            for q_net, q_data in ((self.q1, q1), (self.q2, q2)):
                q_rand = q_net(torch.cat([s_rep, a_rand], 1)).view(B, CQL_N) - np.log(0.5)   # 均匀分布密度 1/2
                q_pi = q_net(torch.cat([s_rep, a_pi], 1)).view(B, CQL_N) - lp_pi.view(B, CQL_N)
                lse = torch.logsumexp(torch.cat([q_rand, q_pi], 1), 1) - np.log(2 * CQL_N)
                lq = lq + cql * (lse.mean() - q_data.mean())
        self.opt_q.zero_grad(); lq.backward(); self.opt_q.step()
        an, lp, _ = self.actor(s); san = torch.cat([s, an], 1)
        la = (alpha * lp - torch.min(self.q1(san), self.q2(san)).squeeze(1)).mean()
        self.opt_a.zero_grad(); la.backward(); self.opt_a.step()
        lal = -(self.log_alpha * (lp.detach() - 1.0)).mean()      # 目标熵 = -|A| = -1
        self.opt_al.zero_grad(); lal.backward(); self.opt_al.step()
        with torch.no_grad():
            for n, t in ((self.q1, self.q1_t), (self.q2, self.q2_t)):
                for p, tp in zip(n.parameters(), t.parameters()):
                    tp.mul_(1 - TAU).add_(TAU * p)

def evaluate(act, n=10):
    env, tot = gym.make("Pendulum-v1"), []
    for k in range(n):
        o, _ = env.reset(seed=10_000 + k); done, G = False, 0.0
        while not done:
            with torch.no_grad():
                a = act(torch.as_tensor(o, dtype=torch.float32))
            o, r, te, tr, _ = env.step(a.numpy() * A_MAX); G += r; done = te or tr
        tot.append(G)
    return float(np.mean(tot))

def collect(seed):
    """在线 SAC 训练 DATA_STEPS 步，把整个回放缓冲区作为离线数据集。"""
    rng = np.random.default_rng(seed); env = gym.make("Pendulum-v1"); agent = SAC()
    D = {k: torch.zeros(DATA_STEPS, d) for k, d in (("s", 3), ("a", 1), ("r", 1), ("s2", 3))}
    o, _ = env.reset(seed=seed); rets, G = [], 0.0
    for t in range(DATA_STEPS):
        if t < 1000:
            a = rng.uniform(-1, 1, 1).astype(np.float32)
        else:
            with torch.no_grad():
                a = agent.actor(torch.as_tensor(o, dtype=torch.float32))[0].numpy()
        o2, r, te, tr, _ = env.step(a * A_MAX); G += r
        for k, v in (("s", o), ("a", a), ("r", [r]), ("s2", o2)):
            D[k][t] = torch.as_tensor(np.asarray(v, np.float32))
        o = o2
        if te or tr:
            rets.append(G); G = 0.0; o, _ = env.reset()
        if t >= 1000:
            j = torch.as_tensor(rng.integers(0, t + 1, BATCH))
            agent.update(*(D[k][j] for k in ("s", "a", "r", "s2")))
    return D, rets

def train_offline(D, method, seed):
    torch.manual_seed(seed); rng = np.random.default_rng(seed); N = len(D["s"])
    if method == "bc":
        pi = mlp(3, 1); opt = torch.optim.Adam(pi.parameters(), lr=1e-3)
        for _ in range(OFFLINE_STEPS):
            j = torch.as_tensor(rng.integers(0, N, BATCH))
            loss = F.mse_loss(torch.tanh(pi(D["s"][j])), D["a"][j]); opt.zero_grad(); loss.backward(); opt.step()
        return evaluate(lambda o: torch.tanh(pi(o)))
    agent = SAC()
    for _ in range(OFFLINE_STEPS):
        j = torch.as_tensor(rng.integers(0, N, BATCH))
        agent.update(*(D[k][j] for k in ("s", "a", "r", "s2")), cql=CQL_ALPHA if method == "cql" else 0.0)
    with torch.no_grad():
        q_data = torch.min(agent.q1(torch.cat([D["s"], D["a"]], 1)), agent.q2(torch.cat([D["s"], D["a"]], 1))).mean().item()
    return evaluate(lambda o: agent.actor(o)[2]), q_data

if __name__ == "__main__":
    seeds = [int(sys.argv[1])] if len(sys.argv) > 1 else ([0] if QUICK else [0, 1])
    for sd in seeds:
        torch.manual_seed(sd)
        D, rets = collect(sd)
        print(f"种子{sd}: 数据集 {len(D['s'])} 条转移，{len(rets)} 个回合，回合回报 最差 {min(rets):.0f} / 平均 {np.mean(rets):.0f} / 最好 {max(rets):.0f}", flush=True)
        print(f"  BC          评估回报 {train_offline(D, 'bc', sd):8.1f}", flush=True)
        for m in ("sac", "cql"):
            score, q = train_offline(D, m, sd)
            name = "朴素离线 SAC" if m == "sac" else f"CQL(α={CQL_ALPHA:g})"
            print(f"  {name:11s} 评估回报 {score:8.1f}   数据上的平均 Q 估计 {q:9.1f}", flush=True)
```
实际运行输出（两个种子，每个种子的数据集也不同）：

```text
种子0: 数据集 6000 条转移，30 个回合，回合回报 最差 -1801 / 平均 -823 / 最好 -2
  BC          评估回报   -892.9
  朴素离线 SAC    评估回报   -108.5   数据上的平均 Q 估计     -85.1
  CQL(α=5)    评估回报   -125.5   数据上的平均 Q 估计    -145.0
种子1: 数据集 6000 条转移，30 个回合，回合回报 最差 -1721 / 平均 -812 / 最好 -1
  BC          评估回报   -821.9
  朴素离线 SAC    评估回报   -108.9   数据上的平均 Q 估计     -84.9
  CQL(α=5)    评估回报   -124.8   数据上的平均 Q 估计    -142.8
```

结果解读（两个种子，结论只针对这个设置）：

- **BC 只学到了数据的“平均水平”**（−893 / −822，和数据集平均回报 −820 左右相当）。数据里大量是随机动作和早期的差策略，模仿它们自然得不到好策略——这正是离线 RL 想要超越 BC 的场景。
- **在这份数据上，朴素离线 SAC 反而最好**（约 −109），CQL 稍差（约 −125），两者都远好于数据平均。原因是这份数据的覆盖很广：前 1000 步的均匀随机动作让每个状态附近的各种动作都有样本，外推误差没有严重到毁掉朴素方法。这与上面 CartPole 纯专家数据（覆盖极窄）上朴素 DQN 崩溃形成对照：**外推误差有多严重，主要取决于数据覆盖。**
- **CQL 确实更保守**：它对数据中 $(s,a)$ 的平均 Q 估计约为 −144，明显低于朴素 SAC 的约 −85。在覆盖充分的数据上，这种额外的保守性没有带来收益，反而让策略略差；$\alpha=5$ 可能偏大，我们没有在这里搜索 $\alpha$（见练习 6）。
- 我们没有测量真实的 $Q^\pi$，所以不能断言哪一个 Q 估计“更准”，只能说 CQL 的估计更低。

## 数据集与基准：D4RL / Minari

- **D4RL**（Fu et al., 2020）：离线 RL 最常用的基准，包含 MuJoCo 运动（random / medium / medium-replay / medium-expert / expert 等不同质量的数据）、AntMaze 迷宫、Adroit 灵巧手、厨房任务等。
- **Minari**：Farama 基金会维护的离线 RL 数据集标准库，与 Gymnasium 配套，D4RL 的数据集已迁移到 Minari。
- **RL Unplugged**（Gulcehre et al., 2020）：DeepMind 发布的离线 RL 基准，包括 Atari 和控制任务。

## 实践建议与常见误区

- **先跑 BC 基线**：在高质量数据上，BC 往往已经很强。
- **超参数选择是离线 RL 的隐患**：真实场景中不能用在线评估来调参，需要离线策略评估（OPE）方法。
- **CQL 的 $\alpha$ 很关键**：太大会过于保守（退化为 BC），太小会高估。
- **误区：把离线 RL 当成“更好的 BC”**：两者解决的问题不同，离线 RL 的优势体现在次优数据和需要拼接的任务上。
- **误区：忽略数据覆盖**：如果数据从未覆盖好的区域，任何离线算法都无法学到好的策略。

## 小结

- 离线 RL 只用固定数据集学习，核心挑战是分布外动作的外推误差。
- 策略约束（BCQ、TD3+BC）让策略靠近数据；保守估计（CQL）压低分布外动作的 Q 值；IQL 完全避免查询分布外动作。
- Decision Transformer 把 RL 看作条件序列建模。
- 我们的玩具实验清楚地展示了朴素离线 DQN 在窄分布数据上的失败，也展示了 BC 在某些数据上可以胜过离线 RL。
- 连续动作的 Pendulum 实验则显示了另一面：数据覆盖充分时，朴素离线 SAC 已经很好（约 −109），CQL 更保守但没有更好（约 −125），而 BC 只能达到数据平均水平。

## 练习题

1. 在纯专家数据上，打印朴素离线 DQN 对数据中动作和另一个动作的 Q 值，验证分布外动作被高估。
2. 把 CQL 的 $\alpha$ 改为 0.1、5、20，在两个数据集上比较结果。
3. 增加高噪声数据集上的训练步数（例如 10 万步），CQL 和 DQN 的表现会改善吗？
4. 实现离散动作版本的 IQL，与 CQL 比较。
5. 构造一个 BC 会失败而离线 RL 能成功的数据集（提示：混合一个好策略和一个差策略的数据，且差策略的数据更多）。
6. 在 `cql_pendulum.py` 中把 $\alpha$ 改为 0.5、1、2，观察评估回报和“数据上的平均 Q 估计”如何变化。
7. 去掉 `cql_pendulum.py` 数据集中前 1000 步的随机动作（只保留 SAC 策略产生的数据），朴素离线 SAC 和 CQL 的差距会怎样变化？

**延伸阅读**

[论文速递：CQL 解读](https://dqn.cn/papers/2026-10-cql)、[Decision Transformer 解读](https://dqn.cn/papers/2026-10-decision-transformer)

## 参考文献

- Lange, S., Gabel, T., & Riedmiller, M. (2012). Batch Reinforcement Learning. In *Reinforcement Learning: State-of-the-Art*, Springer.
- Levine, S., Kumar, A., Tucker, G., & Fu, J. (2020). Offline Reinforcement Learning: Tutorial, Review, and Perspectives on Open Problems. [arXiv:2005.01643](https://arxiv.org/abs/2005.01643)
- Fujimoto, S., Meger, D., & Precup, D. (2019). Off-Policy Deep Reinforcement Learning without Exploration. *ICML*. [arXiv:1812.02900](https://arxiv.org/abs/1812.02900)（BCQ）
- Kumar, A., Zhou, A., Tucker, G., & Levine, S. (2020). Conservative Q-Learning for Offline Reinforcement Learning. *NeurIPS*. [arXiv:2006.04779](https://arxiv.org/abs/2006.04779)
- Fu, J., Kumar, A., Nachum, O., Tucker, G., & Levine, S. (2020). D4RL: Datasets for Deep Data-Driven Reinforcement Learning. [arXiv:2004.07219](https://arxiv.org/abs/2004.07219)
- Fujimoto, S., & Gu, S. S. (2021). A Minimalist Approach to Offline Reinforcement Learning. *NeurIPS*. [arXiv:2106.06860](https://arxiv.org/abs/2106.06860)（TD3+BC）
- Chen, L. et al. (2021). Decision Transformer: Reinforcement Learning via Sequence Modeling. *NeurIPS*. [arXiv:2106.01345](https://arxiv.org/abs/2106.01345)
- Kostrikov, I., Nair, A., & Levine, S. (2022). Offline Reinforcement Learning with Implicit Q-Learning. *ICLR*. [arXiv:2110.06169](https://arxiv.org/abs/2110.06169)
- Kumar, A., Hong, J., Singh, A., & Levine, S. (2022). When Should We Prefer Offline Reinforcement Learning Over Behavioral Cloning? *ICLR*. [arXiv:2204.05618](https://arxiv.org/abs/2204.05618)

## 下一步

上一节：[MBPO](https://dqn.cn/advanced/mbpo)　｜　继续阅读：[多智能体强化学习](https://dqn.cn/advanced/multi-agent)

## 常见问题速查（FAQ）

**问：离线强化学习和模仿学习有什么区别？**

答：模仿学习假设数据来自专家，直接模仿其动作；离线强化学习利用数据中的奖励信息，目标是学到比数据中的行为策略更好的策略，数据可以是次优或混合质量的。

**问：为什么普通 DQN 直接在离线数据上训练会失败？**

答：数据只覆盖部分动作，网络对其他动作的 Q 值没有约束，max 操作会选中被高估的分布外动作，误差通过自举不断放大。本站在纯专家数据上的实验中，朴素离线 DQN 的评估回报只有 9.4，而 CQL 和行为克隆都达到 500。

**问：CQL 的核心思想是什么？**

答：在贝尔曼误差之外加入正则项，压低策略可能选择的动作的 Q 值、抬高数据中动作的 Q 值，从而学到真实价值的保守下界。

## 如何引用本文

DQN.CN 编辑部. 离线强化学习[EB/OL]. DQN.CN, 2026-10-29（更新于 2026-10-30）. https://dqn.cn/advanced/offline-rl.
