系列 · 强化学习 · 第 10 篇

强化学习(十):离线强化学习

离线强化学习从固定数据集学习策略,无需任何在线交互。本文系统讲解分布偏移、外推误差,以及 CQL、BCQ、IQL、Decision Transformer 四类主流方法,配有完整的 CQL PyTorch 实现与 D4RL 基准对比。

到目前为止,我们学过的所有算法都遵循同一个核心循环:行动、观察、更新。这个循环让强化学习得以运转,却也恰恰阻碍了它在现实世界中的部署。自动驾驶系统不可能靠撞车来练习通过路口;临床决策支持模型不能在真实患者身上随意尝试随机策略;工厂里的机械臂也无法在产线上反复测试上万种抓取方式。

不过,这些场景通常都积累了海量日志——数百万小时的人类驾驶记录、几十年的脱敏病历,以及 TB 级别的行为克隆数据。离线强化学习(Offline RL,也称 Batch RL)正是要回答这样一个问题:能否仅凭一个固定的数据集,在完全不与环境交互的前提下,提炼出一个高性能策略?

答案是:“可以,但必须格外谨慎。” 这一限制背后的核心挑战,也正是本文的主题:生成数据的行为策略(behavior policy)与我们试图优化的学习策略(learned policy)之间存在分布偏移(distributional shift)。


你将学到什么#

  • 为什么直接套用标准 off-policy 算法在离线场景会失败:外推误差、价值高估与“死亡螺旋”。
  • CQL(Conservative Q-Learning):一种悲观正则化方法,为真实价值提供下界保障。
  • BCQ(Batch-Constrained Q-Learning):通过生成式动作提议,确保策略始终停留在数据流形内。
  • IQL(Implicit Q-Learning):利用 expectile 回归近似 max 操作,完全避免查询分布外(OOD)。
  • Decision Transformer:将强化学习重新表述为基于“剩余回报”(returns-to-go)的条件序列建模问题。
  • D4RL 基准测试结果,帮你快速判断该优先选用哪种算法。
  • 一份完整、可运行的 CQL PyTorch 实现

前置知识#

  • 掌握 Q-learning、目标网络和 Actor-Critic 方法(第 2 部分第 6 部分 )。
  • 熟悉 Bellman 最优算子与重要性采样。
  • 熟练使用 PyTorch 和 Gym/Gymnasium API。

离线 RL 为何如此困难#

在线 vs 离线 RL:缺失的反馈回路

在线 RL 中,策略与数据分布是耦合的:一旦策略开始高估某个动作,下一次交互就会将其暴露出来,Bellman 更新随即纠正这一偏差。但在离线 RL 中,数据集是冻结的,没有第二次机会。模型对任何未见过的动作所做出的错误估计都会永久留在 Q 函数中,而 argmax 操作会毫不犹豫地加以利用。

分布偏移#

$$d_{\pi_\theta}(s,a)\neq d_{\pi_\beta}(s,a).$$

$\pi_\theta$ 试图选择一个 $\pi_\beta(a\mid s)\approx 0$ 的动作时,问题就出现了。Q 网络从未在该 $(s,a)$ 对上见过任何目标值;它返回的任何数值都只是神经网络在完全陌生输入区域上的纯外推,极不可靠。

外推误差与死亡螺旋#

分布偏移导致 OOD 动作上的 Q 值被严重高估

$$Q(s,a)\leftarrow r + \gamma\,\max_{a'}Q(s',a').$$

这里的 max 操作会专门挑选最乐观的外推值。只要 Q 函数对某个 OOD 动作高估了哪怕 $\epsilon$ ,这一偏差就会成为前一时刻的 bootstrap 目标,再往前传播一步,如此层层累积。实证表明,这种误差通常在标准基准上仅需几千次梯度更新就会导致发散Fujimoto et al., 2019 )。上图右侧面板值得牢记:绿色曲线代表真实情况,红色曲线是网络的信念,而策略会径直走向 OOD 峰值处的悬崖。

下面介绍的几类算法都在应对这一问题,区别在于它们约束的对象不同

类别核心理念约束对象
策略约束(BCQ, BEAR, TD3+BC)“只模仿数据中的行为”$\pi_\theta$
价值悲观(CQL, MOPO)“对陌生的 Q 值保持怀疑”$Q$
样本内学习(IQL, AWAC)“绝不查询 OOD 动作”损失函数本身
序列建模(Decision Transformer, Trajectory Transformer)“彻底绕开 Bellman 方程”问题的定义方式

Conservative Q-Learning (CQL)#

Conservative Q-Learning 对 OOD 动作做惩罚,naive offline Q 直接发散。

强化学习(十):离线强化学习 — 章节小结图

CQL(Kumar et al., 2020 )是目前最广泛使用的离线 RL 基线方法。它不改变网络结构、actor 或数据流水线,仅在损失函数中增加一项。

一句话概括其思想#

先压低策略可能考虑的所有动作的 Q 值,再将数据中实际出现的动作 Q 值拉回。 最终得到的 Q 函数会在缺乏证据的区域表现出恰到好处的悲观。

目标函数#

$$ \mathcal{L}_{\mathrm{CQL}} \;=\; \alpha\,\Big[\,\underbrace{\log\sum_{a}\exp Q(s,a)}_{\text{整体压低}} \;-\; \underbrace{\mathbb{E}_{a\sim\mathcal{D}}\big[Q(s,a)\big]}_{\text{数据动作拉回}}\Big] \;+\; \mathcal{L}_{\mathrm{TD}}. $$

其中 logsumexp 是对所有动作的软最大值:最小化它会拉低所有 Q 值;减去数据分布下的期望后,数据内的动作 Q 值得以回升,而 OOD 动作则只承受向下的压力。

$$ \hat{Q}^{\pi}(s,a)\;\leq\;Q^{\pi}(s,a)\quad\forall (s,a). $$

这正是我们需要的:一个真实价值的下界。任何最大化该下界的策略都不会选择灾难性动作,因为它根本看不到那些虚假的高回报。

CQL:正则项把 OOD 区域的 Q 值压低,让 argmax 留在数据支撑集内

左图展示了普通 SAC-style critic 在离线数据上的表现:argmax 直接奔向 OOD 的虚假峰值。右图则是 CQL 的修正效果:橙色阴影区域代表悲观惩罚,新的 argmax 安稳地位于数据支撑范围内。

实践要点#

  • 在连续动作空间中,logsumexp 无法精确计算,通常用 n_random 个均匀采样动作加上 n_actor 个当前策略采样动作来近似。10–20 个样本通常已足够。
  • 原论文中的 “CQL($\mathcal{H}$ )” 变体引入了一个拉格朗日乘子,自动调节 $\alpha$ 以达到目标差距;这也是开源库 d3rlpyJaxRL 默认采用的版本。
  • 实验表明,CQL 在 D4RL 的 medium-replaymedium 数据集上表现稳健,但在 medium-expert 上略显保守,此时 IQL 或 DT 往往更胜一筹。

完整的 CQL 实现#

  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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import torch
import torch.nn as nn
import torch.nn.functional as F

class QNetwork(nn.Module):
    """孪生 Q 网络(Clipped Double Q),用于抑制高估。"""

    def __init__(self, state_dim, action_dim, hidden=256):
        super().__init__()
        self.q1 = self._mlp(state_dim + action_dim, hidden)
        self.q2 = self._mlp(state_dim + action_dim, hidden)

    @staticmethod
    def _mlp(in_dim, hidden):
        return nn.Sequential(
            nn.Linear(in_dim, hidden), nn.ReLU(),
            nn.Linear(hidden, hidden), nn.ReLU(),
            nn.Linear(hidden, 1),
        )

    def forward(self, state, action):
        sa = torch.cat([state, action], dim=1)
        return self.q1(sa), self.q2(sa)

class GaussianPolicy(nn.Module):
    """SAC 风格的 tanh-squashed 高斯策略。"""

    def __init__(self, state_dim, action_dim, hidden=256):
        super().__init__()
        self.trunk = nn.Sequential(
            nn.Linear(state_dim, hidden), nn.ReLU(),
            nn.Linear(hidden, hidden), nn.ReLU(),
        )
        self.mean = nn.Linear(hidden, action_dim)
        self.log_std = nn.Linear(hidden, action_dim)

    def sample(self, state):
        x = self.trunk(state)
        mean = self.mean(x)
        log_std = self.log_std(x).clamp(-20, 2)
        std = log_std.exp()
        normal = torch.distributions.Normal(mean, std)
        z = normal.rsample()
        action = torch.tanh(z)
        # tanh 变换的雅可比项
        log_prob = (normal.log_prob(z)
                    - torch.log(1 - action.pow(2) + 1e-6)).sum(1, keepdim=True)
        return action, log_prob

class CQLAgent:
    """在 SAC 基础上添加 CQL 正则项。cql_weight 对应公式中的 alpha。"""

    def __init__(self, state_dim, action_dim, lr=3e-4,
                 gamma=0.99, tau=0.005, alpha=0.2,
                 cql_weight=1.0, n_random=10):
        self.gamma, self.tau = gamma, tau
        self.cql_weight, self.n_random = cql_weight, n_random
        self.action_dim = action_dim

        self.q_net = QNetwork(state_dim, action_dim)
        self.target_q = QNetwork(state_dim, action_dim)
        self.target_q.load_state_dict(self.q_net.state_dict())
        self.policy = GaussianPolicy(state_dim, action_dim)

        self.q_opt = torch.optim.Adam(self.q_net.parameters(), lr=lr)
        self.pi_opt = torch.optim.Adam(self.policy.parameters(), lr=lr)
        self.alpha = alpha

    # ---------- 与普通 SAC 唯一不同的地方 ----------
    def _cql_penalty(self, states, q1_data, q2_data):
        """对动作做 logsumexp,再减去数据上的 Q 平均值。"""
        b = states.size(0)
        # (1) [-1, 1] 上的均匀随机动作
        rand_a = torch.empty(b * self.n_random, self.action_dim).uniform_(-1, 1)
        # (2) 当前策略在状态 s 下采样的动作
        rep_s = states.repeat_interleave(self.n_random, 0)
        pi_a, pi_lp = self.policy.sample(rep_s)

        def _q(s, a):
            return self.q_net(s, a)

        q1_rand, q2_rand = _q(rep_s, rand_a)
        q1_pi,   q2_pi   = _q(rep_s, pi_a)

        # 重要性加权 logsumexp(Kumar 等, Eq. 4)
        log_pi_uniform = -float(self.action_dim) * torch.log(torch.tensor(2.0))
        cat1 = torch.cat([q1_rand - log_pi_uniform,
                          q1_pi   - pi_lp.detach()], 0).view(b, -1)
        cat2 = torch.cat([q2_rand - log_pi_uniform,
                          q2_pi   - pi_lp.detach()], 0).view(b, -1)

        lse1 = torch.logsumexp(cat1, 1, keepdim=True)
        lse2 = torch.logsumexp(cat2, 1, keepdim=True)
        return ((lse1 - q1_data) + (lse2 - q2_data)).mean()

    def update(self, states, actions, rewards, next_states, dones):
        # 1. Bellman 目标(clipped double Q + 熵奖励)
        with torch.no_grad():
            next_a, next_lp = self.policy.sample(next_states)
            tq1, tq2 = self.target_q(next_states, next_a)
            target_q = torch.min(tq1, tq2) - self.alpha * next_lp
            target = rewards + self.gamma * (1 - dones) * target_q

        q1, q2 = self.q_net(states, actions)
        td_loss = F.mse_loss(q1, target) + F.mse_loss(q2, target)
        cql = self._cql_penalty(states, q1, q2)
        q_loss = td_loss + self.cql_weight * cql

        self.q_opt.zero_grad()
        q_loss.backward()
        self.q_opt.step()

        # 2. Actor 更新(标准 SAC)
        a, lp = self.policy.sample(states)
        q1_a, q2_a = self.q_net(states, a)
        pi_loss = (self.alpha * lp - torch.min(q1_a, q2_a)).mean()
        self.pi_opt.zero_grad()
        pi_loss.backward()
        self.pi_opt.step()

        # 3. 目标 Q 的 Polyak 平均
        with torch.no_grad():
            for p, tp in zip(self.q_net.parameters(),
                             self.target_q.parameters()):
                tp.data.mul_(1 - self.tau).add_(self.tau * p.data)

        return {"td": td_loss.item(),
                "cql": cql.item(),
                "pi":  pi_loss.item()}

将一个离线回放缓冲区(通过 minarid4rl-pybullet 加载 D4RL 数据)接入 update 函数,训练约 100 万次梯度步,即可复现下方基准图中的典型结果。


BCQ:生成式动作提议#

CQL 约束的是价值函数,而 BCQ(Fujimoto et al., 2019 )则直接约束策略本身。其核心原则很直接:绝不让 Q 函数评估行为策略本不会产生的动作。

BCQ:VAE 提供候选动作,扰动网络小幅调整,最后用 argmax 选择最优动作

该架构包含三个组件:

  1. 一个条件 VAE $G_\omega(s)$ ,通过标准 ELBO 在 $\mathcal{D}$ 上训练,用于建模 $\pi_\beta(a\mid s)$ 。从中采样的动作天然位于行为策略的数据流形上。
  2. 一个扰动网络 $\xi_\phi(s,a)\in[-\Phi,\Phi]^{\dim(a)}$ ,对每个 VAE 样本施加小幅、有界的修正。当 $\Phi$ 较小(通常为 0.05)时,修正后的动作无法大幅偏离数据;若 $\Phi=0$ ,BCQ 就退化为加权模仿学习。
  3. 一对孪生 Q 网络,用于对 $N$ 个修正后的候选动作打分,最终通过 argmax 选出执行动作。

其巧妙之处在于:扰动网络被训练为最大化 Q 值。因此,BCQ 既能获得策略改进的好处(不只是简单模仿),又避免了 OOD 外推的风险(动作无法远离数据)。代价是流程更复杂——需要四个网络协同工作,且 VAE 必须训练良好,否则候选动作本身就会引入偏差。


IQL:彻底规避 Bootstrap 问题#

IQL(Kostrikov et al., 2022 )是三者中最优雅的方法。它的洞察很简单:所有离线 RL 的病态问题都源于 Bellman 目标中的 $\max_{a'}Q(s',a')$ ,因为 OOD 动作正是从这里混入的。那么,干脆不用它。

$$ \mathcal{L}_V \;=\; \mathbb{E}_{(s,a)\sim\mathcal{D}}\big[L_2^{\tau}\big(Q(s,a)-V(s)\big)\big],\qquad L_2^{\tau}(u)=\big|\tau-\mathbb{1}(u<0)\big|\,u^{2}. $$

$\tau=0.5$ 时,这就是普通的 MSE,$V$ 学习的是均值;当 $\tau\to 1$ 时,损失函数对低估的惩罚远大于高估,因此 $V$ 收敛到数据中动作对应的 Q 值的上 expectile

IQL 的非对称期望分位数损失:调高 tau 会让 V 逼近 max_a Q,但全程不离开数据

$$ y \;=\; r + \gamma\,V(s'), $$ $$ \mathcal{L}_\pi \;=\; -\mathbb{E}_{(s,a)\sim\mathcal{D}}\big[\exp\big(\beta\,(Q(s,a)-V(s))\big)\,\log\pi_\theta(a\mid s)\big]. $$

IQL 是现代离线 RL 算法中结构最简洁的一种,在 D4RL 的 AntMaze 和 Adroit 环境中 稳定登顶榜单——这些环境中高质量数据稀疏,用 max 引导最容易引发灾难。

Decision Transformer:将 RL 视为序列建模#

既然决定抛弃 Bellman 方程,何不更进一步?Decision TransformerChen et al., 2021 )连价值函数也一并舍弃,直接将 RL 视为下一个 token 的预测问题。

Decision Transformer:以 (return-to-go, state, action) 三元组为输入,送入因果 Transformer

一条轨迹被展开为序列 $(\hat{R}_1, s_1, a_1, \hat{R}_2, s_2, a_2, \ldots)$ ,其中 $\hat{R}_t = \sum_{t'\geq t} r_{t'}$ 是从时间步 $t$ 开始的剩余回报(return-to-go)。一个标准的因果 Transformer(类似 GPT 风格)通过交叉熵或 MSE 训练,目标是根据此前所有内容预测 $a_t$

测试时,只需将你期望的剩余回报作为 $\hat{R}_1$ 输入,模型便会自回归地生成动作序列。这个 $\hat{R}_1$ 就像一个旋钮:要求越高,策略就越激进。

这种方法的优势

  • 无 bootstrap,故无外推误差。
  • 长上下文让模型天然具备基于部分观测历史进行条件推理的能力。
  • 可直接利用所有 Transformer 工程优化(LayerNorm、RoPE、FlashAttention、混合精度等)。
  • 同一套框架可扩展至 D4RL、Atari 乃至大规模多任务场景(如 Gato)。

代价

  • 无法超越数据中出现过的最佳回报:若 $\mathcal{D}$ 中没有任何轨迹得分达到 90,要求 90 只会产出垃圾。
  • 缺乏因果信用分配——模型学习的是“在给定结果下模仿轨迹”,而非推理“哪个动作导致了该结果”。
  • 在小型基准上,通常需要比 CQL/IQL 更大的模型和更多数据才能达到同等分数。

用数据说话:D4RL 基准#

D4RL MuJoCo 运动控制:BC、BCQ、CQL、IQL、DT 在三种数据质量上的表现

该图汇总了原始 CQL(Kumar et al., 2020 )、IQL(Kostrikov et al., 2022 )和 DT(Chen et al., 2021 )论文在标准 D4RL MuJoCo 运动控制套件(hopperhalfcheetahwalker2d 取平均)上的归一化得分。100 表示专家水平,0 表示随机策略。

三个关键结论:

  1. medium-replay 最需要保守性。该数据集来自 SAC 训练早期的检查点——包含大量低质量动作和恢复过程。CQL 和 IQL 的得分约为行为克隆(BC)的两倍;DT 因缺乏价值引导,难以拼接优质子轨迹,表现落后。
  2. medium-expert 是 DT 的主场。当数据已包含近最优轨迹时,序列建模的简洁性使其脱颖而出。
  3. IQL 最为稳健。它很少在单一数据集上绝对领先,但也几乎从不跌出前三,且训练稳定性在四者中最佳。

若拿不定主意,建议:优先尝试 IQL(最稳)或 CQL(最简);当数据接近专家水平时,再考虑 DT


常见问题#

Q:离线 RL 在什么情况下会彻底失效? 三种典型失败模式已被充分验证:(i) 数据覆盖过窄——仅有专家轨迹,缺乏错误恢复样本,导致策略无法应对自身失误;(ii) 数据质量极低——在长时任务中使用随机策略生成的数据;(iii) 评估环境偏移——测试 MDP 的转移动态与生成 $\mathcal{D}$ 的环境不一致。

Q:如何调节 CQL 的 $\alpha$ ? 粗略经验:专家数据用 $\alpha=0.5$$1.0$ ,中等数据用 $1.0$$5.0$ ,随机/replay 数据用 $5.0$$10.0$ 。更优方案是采用原论文的拉格朗日变体,它会自动调节 $\alpha$ ,使差距 $\mathbb{E}_{a\sim\pi}Q - \mathbb{E}_{a\sim\mathcal{D}}Q$ 维持在目标阈值附近(通常设为 5–10)。

Q:离线 RL 与模仿学习的本质区别是什么? 行为克隆(BC)忽略奖励信号,无法超越示范者。而离线 RL 利用奖励信号,能够拼接(stitch)不同轨迹中的优质片段,生成一个严格优于 $\mathcal{D}$ 中任意单条轨迹的策略。经典例子:轨迹 A 笨拙地到达状态 $s$ 但后续表现优异,轨迹 B 高效到达 $s$ 但后续糟糕——离线 RL 可组合出“B 的开头 + A 的结尾”,而 BC 无能为力。

Q:是否应直接选用基于模型的离线方法? 在高维连续控制中,MOPO/MOReL/COMBO 具有竞争力,但需额外学习动力学模型并引入悲观项。它们在小数据场景下表现更好(模型提供归纳偏置),但工程复杂度更高。截至 2025 年,无模型的 CQL/IQL 仍是默认起点。

Q:离线到在线微调该如何做? 过去两年,IQL 在此方向上表现突出。AWAC、Cal-QL 和 RLPD 等方法先用离线策略初始化,再用少量在线数据继续训练。CQL 初始化往往过于保守,不适合微调;相比之下,IQL 或 AWAC 的初始化通常更受青睐。

参考文献#

本系列

强化学习 12 篇

  1. 01 强化学习(一):基础与核心概念
  2. 02 强化学习(二):Q-Learning 与深度 Q 网络(DQN)
  3. 03 强化学习(三):Policy Gradient 与 Actor-Critic 方法
  4. 04 强化学习(四):探索策略与好奇心驱动学习
  5. 05 强化学习(五):Model-Based 强化学习与世界模型
  6. 06 强化学习(六):PPO 与 TRPO —— 信任域策略优化
  7. 07 强化学习(七):模仿学习与逆强化学习
  8. 08 强化学习(八):AlphaGo 与蒙特卡洛树搜索
  9. 09 强化学习(九):多智能体强化学习
  10. 10 强化学习(十):离线强化学习 当前
  11. 11 强化学习(十一):层次化强化学习与元学习
  12. 12 强化学习(十二):RLHF 与大语言模型应用

读有所得?

GitHub 关注我 → 新文周更

GitHub