LECTURE 08

Q-Learning 的工程实践:让不收敛的算法真的跑起来

上一讲证明了带函数逼近的 Q-learning 没有收敛保证。本讲不去修复理论,而是把它变成一个在 Atari 上真能打赢人类的算法:replay buffer、target network、Double Q、N-step return、连续动作下的 max,以及一份可以照抄的调参清单。

讲师:Sergey Levine UC Berkeley 原始材料:lec-8.pdf(36 页)

0. 本讲导读

上一讲的结尾留下了一个相当灰暗的结论:把表格换成神经网络之后,值迭代和 Q 迭代的收敛证明全线崩塌——Bellman 算子 $\mathcal{B}$ 在 $\infty$-范数下是 $\gamma$-压缩,回归投影算子 $\Pi$ 在 $\ell_2$-范数下是压缩,但两个不同范数下的压缩复合起来什么也不是。理论上,深度 Q-learning 可以发散。

然而 2013 年 Mnih 等人用同一个算法在 Atari 上做出了轰动性的结果。本讲要回答的就是这个落差:既然没有保证,为什么它还能工作?我们到底往里面加了哪些东西?每一个技巧修的是哪一个具体的病?

Levine 的推进路线是典型的「先指出病灶,再给药方」:

  • 病灶一:样本高度相关。在线 Q-learning 用刚刚采到的那一个转移做一次梯度步,连续的样本几乎相同,等价于对同一个点反复过拟合。药方是 replay buffer(经验回放池)——上一讲已经引入。
  • 病灶二:目标在动。回归的标签 $y_i$ 本身是当前网络算出来的,参数一变标签就变,这不是「回归」而是在追一个自己甩出去的影子。而且这个更新根本不是任何目标函数的梯度下降。药方是 target network(目标网络)。
  • 把两副药合在一起就是 DQN;再把 DQN 放进一个更一般的框架里,你会发现 online Q-learning、DQN、fitted Q-iteration 只是同一个三进程系统在不同速度比下的特例。
  • 病灶三:系统性高估。目标里的 $\max_{a'}Q_{\bar\theta}(s',a')$ 因为 $\E[\max]\ge\max[\E]$ 而必然偏高,噪声越大偏得越多,还会被 bootstrap 不断放大。药方是 Double Q-learning:让「选动作」和「评估值」用不同的网络。
  • 病灶四:值传播太慢。单步 bootstrap 让奖励信息每次只往回爬一格。药方是 N-step return,代价是 off-policy 下不再无偏。
  • 病灶五:连续动作没法取 $\max$。三条出路:随机采样/CEM、可解析求最大值的网络结构(NAF)、学一个近似最大化器(DDPG)。第三条把 Q-learning 和 actor-critic 缝在了一起。
  • 最后是实践清单与一点理论:为什么这些技巧只是让问题「没那么糟」,而不是解决了它。

本讲的记号沿用前面:$s_t$ 状态、$a_t$ 动作、$r(s,a)$ 奖励、$p(s'|s,a)$ 转移、$\gamma\in[0,1)$ 折扣、$\pi_\theta(a|s)$ 策略、$Q^\pi(s,a)$ 动作值函数。一个新增的关键约定:$\theta$ 表示当前网络(online network)的参数,$\bar\theta$(有时也写 $\phi'$)表示目标网络的参数。请从一开始就把这两者在脑子里分开,本讲一大半内容都是在讨论它们该差多远。

核心结论
  • Q-learning 的参数更新 $\theta\leftarrow\theta-\alpha\frac{dQ_\theta}{d\theta}(Q_\theta-y)$ 不是任何函数的梯度,因为我们人为切断了 $y$ 对 $\theta$ 的依赖。它是所谓的 semi-gradient(半梯度)方法。
  • replay buffer 修「样本相关」,target network 修「目标在动」——两个正交的病,两副独立的药,缺一不可。
  • DQN = 一般化 fitted Q-iteration 在 $N=1,K=1$ 时的特例;改变三个进程(收数据 / 更新目标 / 回归)的相对速度就得到整个算法谱系。
  • $\E[\max_a Q]\ge\max_a\E[Q]$:只要 $Q$ 有噪声,$\max$ 就系统性偏高。Double Q-learning 用「选动作的网络 $\ne$ 估值的网络」把噪声解耦,实现代价几乎为零。
  • 连续动作下取 $\max$ 有三条路:采样/CEM(简单但粗糙)、NAF(精确但限制表达力)、DDPG(通用但引入第二个网络)。DDPG 表明 Q-learning 与 actor-critic 本质是同一件事。
  • 所有这些技巧都不改变「$\Pi\mathcal{B}$ 不是压缩」这个事实,它们只是把发散的概率压低。这也解释了为什么 Q-learning 比策略梯度更依赖调参和多随机种子。

1. 在线 Q-learning 的两个病灶

先把上一讲结束时的算法原封不动搬过来。带 replay buffer 的 Q-learning 长这样:

  1. 用某个策略 $\pi_\theta(a|s)$ 在环境里走一步,得到 $(s_i,a_i,s_i')$,存进 buffer $\mathcal{R}$;
  2. 从 $\mathcal{R}$ 里采一个 batch $\{(s_i,a_i,s_i')\}_{i=1}^{B}$,算目标 $y_i=r(s_i,a_i)+\gamma\max_{a'}Q_\theta(s_i',a')$;
  3. 用 $\nabla_\theta\sum_{i=1}^{B}\|Q_\theta(s_i,a_i)-y_i\|^2$ 更新 $\theta$。

其中行为策略通常是 $\epsilon$-greedy:

$$ \pi_\theta(a|s)=\begin{cases}1-\epsilon & \text{if } a=\argmax_{a'} Q_\theta(s,a')\\ \epsilon/(|\mathcal{A}|-1) & \text{otherwise}\end{cases} $$
带 replay buffer 的 Q-learning 三步循环
上一讲的落点:Q-learning 的三步循环 + replay buffer。左边绿色回环表示三步反复执行;右边圆柱是回放池,每走一步把新转移「存进去」,训练时「从里面取一个 batch」。右下角标注 off-policy: B ≥ 1 —— 因为更新只依赖 $(s,a,r,s')$ 而与采集它的策略无关,所以 batch 可以任意大、数据可以任意旧。

1.1 为什么需要 buffer:相关样本会毁掉 SGD

如果不用 buffer(纯在线 Q-learning),第 $t$ 步和第 $t+1$ 步的样本是同一条轨迹上相邻的两个转移。想象一个机器人在走廊里往前开:连续 50 帧的图像几乎一模一样,动作也几乎一样。用这 50 个「基本相同」的样本连做 50 次梯度步,网络会被强行拽向局部的一小块状态空间,把别处已经学好的值全部破坏掉。

随机梯度下降的收敛分析依赖一个前提:每个 mini-batch 是从数据分布中独立同分布抽取的,因此 mini-batch 梯度是全量梯度的无偏估计。相关样本让这个前提彻底失效——批量大小名义上是 1,实际有效样本数远小于 1(因为连续样本携带的信息高度重复),梯度估计的方差和偏差都极大。

replay buffer 的作用是把「时间上相邻」和「训练时相邻」解耦:池子里存着过去几十万步的转移,随机均匀采样出来的一个 batch 覆盖了各种各样的状态,近似独立同分布。同时还有第二个好处:数据可以被重复使用多次,样本效率大幅提升,这正是 off-policy 算法的核心优势。

1.2 为什么 buffer 不够:这根本不是梯度下降

把第 3 步的更新式子完整展开写出来($\phi$ 表示更新前的参数):

$$ \theta\leftarrow\phi-\alpha\frac{dQ_\theta}{d\theta}(s_i,a_i)\Big(Q_\theta(s_i,a_i)-\big[\underbrace{r(s_i,a_i)+\gamma\max_{a'}Q_\theta(s_i',a')}_{\text{目标 }y_i}\big]\Big) $$
Q-learning 不是梯度下降:目标值里没有梯度回传
红圈圈出的是目标值 $y_i=r+\gamma\max_{a'}Q_\theta(s',a')$。它显然依赖 $\theta$,但我们在求导时假装它是常数——「no gradient through target value」。这就是 Levine 强调的那句话:Q-learning is not gradient descent!

注意这个式子里 $\theta$ 出现了两次:一次在 $Q_\theta(s_i,a_i)$(预测),一次在 $\max_{a'}Q_\theta(s_i',a')$(目标)。但我们只对第一次求了导,对第二次视而不见。

推导:为什么不能对目标求导

假设我们「诚实地」把它当成最小化 Bellman 残差平方,即目标函数

$$ L(\theta)=\tfrac12\E\Big[\big(Q_\theta(s,a)-r-\gamma\max_{a'}Q_\theta(s',a')\big)^2\Big] $$

它的真梯度是

$$ \nabla_\theta L=\E\Big[\big(Q_\theta(s,a)-y\big)\big(\nabla_\theta Q_\theta(s,a)-\gamma\nabla_\theta\max_{a'}Q_\theta(s',a')\big)\Big] $$

比我们实际用的更新多了 $-\gamma\nabla_\theta\max_{a'}Q_\theta(s',a')$ 一项。这一项叫 residual gradient(残差梯度)。它确实存在,Baird 在 1995 年也确实提出过用它做真正的梯度下降。问题在于:

  • 在随机环境下,样本形式的 $\big(Q_\theta(s,a)-r-\gamma\max Q_\theta(s',\cdot)\big)$ 里两个 $s'$ 相关项相乘会引入 double sampling bias——要正确估计需要从同一个 $(s,a)$ 采两个独立的 $s'$,而真实环境给不了。
  • 即使能算,残差梯度在实践中收敛极慢:它会同时把 $Q(s,a)$ 往下拉和把 $Q(s',a')$ 往上推,两边互相扯皮,学习信号被稀释。

所以所有实用算法都选择丢掉那一项,得到的东西叫 semi-gradient(半梯度)。代价就是:我们失去了「在优化某个明确的目标函数」这一保障,因此也失去了所有基于「目标函数单调下降」的收敛论证。

常见误区

「既然写了 loss = (Q - y)**2,那 PyTorch 自动求导不就把两项都算了吗?」——不会,前提是你写了 y = y.detach()(或在 torch.no_grad() 里算 $y$)。忘记 detach 是 DQN 实现中最常见、最隐蔽的 bug:训练不会报错,loss 甚至会更快降到接近 0,因为网络发现了一个作弊解——把所有 $Q$ 都压成同一个常数,残差自然变小。你会得到一个 loss 漂亮但完全不会玩游戏的智能体。

2. 目标网络:把移动的靶子钉住

要看清「目标在动」这个病有多严重,最好的办法是把在线 Q-learning 和上一讲的 fitted Q-iteration(拟合 Q 迭代)并排放在一起。

在线 Q-learning 与完整 fitted Q-iteration 的对比
上半部分是 Q-learning:只做「一次梯度步」,而且目标随参数一起动(one gradient step, moving target)。下半部分是完整的 fitted Q-iteration:先收一批数据、把 $y_i$ 全部算好固定住、再做一个彻底的回归 $\theta\leftarrow\argmin_\theta\frac12\sum_i\|Q_\theta(s_i,a_i)-y_i\|^2$。两个更新式子长得几乎一样,唯一差别是最后一行 $Q$ 的下标:一个是 $\theta$(当前参数),一个是 $\bar\theta$(回归内循环中不变的参数)。这一个下标之差,就是「不稳定」和「完美良定义的稳定回归」的分界线。

2.1 一次梯度步 vs 一次完整回归

fitted Q-iteration 的第 2 步「set $y_i\leftarrow r(s_i,a_i)+\gamma\max_{a'}Q_{\bar\theta}(s_i',a')$」把标签一次性算完并缓存下来。之后第 3 步的内层 SGD 循环里,无论 $\theta$ 怎么变,$y_i$ 都是一堆固定的数字。于是这一步就是一个标准的监督回归问题:有输入 $(s_i,a_i)$,有固定标签 $y_i$,最小化平方损失。这种问题我们太熟悉了,它有明确的目标函数、有收敛性、可以放心地跑很多个 epoch、可以用 early stopping、可以看 loss 曲线判断是否收敛。Levine 在幻灯片上给它的评语是「perfectly well-defined, stable regression」。

在线 Q-learning 则相反:它只做一次梯度步就重新计算标签。你可以把它想象成拿着枪打靶,但每开一枪靶子就跟着你的准星移动一点。更糟的是,靶子的移动方向和你瞄准的方向是正相关的——你把 $Q_\theta(s,a)$ 调高,因为神经网络的泛化,附近的 $Q_\theta(s',a')$ 也会被调高,于是目标 $y=r+\gamma\max Q_\theta(s',\cdot)$ 也变高,你下一步还得继续往上追。这个正反馈回路正是 Q 值发散的直接机制。

直觉

想象你在做房价预测,但每训练一步就用模型自己当前的预测去更新训练集的标签。如果模型某次偶然把某个房子预测高了 5%,新标签就变成高 5% 的值,下一轮它会学得更高……这就是 bootstrap(自举)与函数逼近相遇时的核心危险。target network 的全部作用,就是把「用自己的预测当标签」这件事的频率从每步一次降到每一万步一次。

2.2 折中方案:target network

完整的 fitted Q-iteration 稳定,但它要求「收一大批数据 → 做一次彻底回归」,样本效率低且不适合边玩边学的在线场景。在线 Q-learning 高效但不稳。target network 是两者之间的插值:保留每步一次的梯度更新,但让标签所用的参数慢慢变。

带目标网络的 Q-learning 四步算法
算法只改了两个地方:第 2 步算目标时用红圈标出的 $Q_{\bar\theta}$(目标网络)而不是 $Q_\theta$;新增第 4 步「每 10000 步左右,令 $\bar\theta\leftarrow\theta$」。下方三个黄色标签是 Levine 的课堂提问:目标每 10000 步才变一次——这为什么好?又为什么不好?

带目标网络的 Q-learning:

  1. 用 $\pi_\theta$ 走一步,得到 $(s_i,a_i,s_i')$,存入 $\mathcal{R}$;
  2. 算目标 $y_i=r(s_i,a_i)+\gamma\max_{a'}\,Q_{\bar\theta}(s_i',a')$; ← 用目标网络
  3. 用 $\nabla_\theta\sum_{i=1}^{B}\|Q_\theta(s_i,a_i)-y_i\|^2$ 更新 $\theta$;
  4. 每 $N\approx 10000$ 步,令 $\bar\theta\leftarrow\theta$。

为什么好?在两次同步之间的一万步里,$\bar\theta$ 是常数。因此这一万步做的事情,就是对一个固定的目标函数做随机梯度下降:

$$ L(\theta)=\E_{(s,a,s')\sim\mathcal{R}}\Big[\big(Q_\theta(s,a)-r(s,a)-\gamma\max_{a'}Q_{\bar\theta}(s',a')\big)^2\Big] $$

这个 $L(\theta)$ 里 $\theta$ 只出现一次,梯度是真梯度,标准 SGD 理论适用(至少在这一万步的尺度上)。上面说的正反馈回路被切断了:你把 $Q_\theta$ 调高不会立刻让目标变高。

为什么不好?三点代价:

  • 信息滞后。目标网络永远落后当前网络最多 $N$ 步。刚刚学到的「这里有个大奖励」要等下次同步才能开始向前一格传播。这会显著拖慢学习速度,尤其是需要长距离信用分配的任务。
  • 滞后量不均匀。刚同步完的那一刻滞后为 0,同步前一刻滞后为 $N$。同一个算法在一个周期内的行为差别很大——下一节会专门讲这个问题。
  • 多了一个超参数 $N$。$N$ 太小退化回不稳定的在线 Q-learning,$N$ 太大则学习极慢、目标网络严重过时。Atari 上常用 $N=10^4$(按梯度步计),但换个任务就要重调。
核心结论

replay buffer 和 target network 修的是两个完全不同的病,不能互相替代:

  • replay buffer → 让 batch 里的样本近似 i.i.d.,修的是输入分布的问题;
  • target network → 让回归标签在一段时间内固定,修的是监督信号的问题。

去掉任何一个,DQN 在 Atari 上的性能都会大幅下降;两个都去掉,基本什么也学不到。

3. 经典 DQN:算法框与最小实现

把 replay buffer 和 target network 合起来,就得到 2013 年那篇让深度强化学习出圈的算法。Levine 先给出一个一般化的版本,再指出 DQN 只是它的一个特例。

一般化 Q-learning 与经典 DQN 算法框
上框是「带 replay buffer 和 target network 的一般化 Q-learning」,三层绿色回环从外到内分别是:目标网络同步(最外)、收数据并循环 $N$ 次(中)、梯度步循环 $K$ 次(内)。下框是「经典 deep Q-learning(DQN)」,右侧大括号标注 $K=1$——即每收一个转移就只做一次梯度步。两个框其实是同一个算法,只是超参数不同。

3.1 一般化的 Q-learning 算法框

  1. 保存目标网络参数:$\bar\theta\leftarrow\theta$;
  2. 用某个策略收集数据 $\{(s_i,a_i,s_i')\}$,加入 $\mathcal{R}$; (重复 $N$ 次)
  3. 从 $\mathcal{R}$ 采一个 batch $(s_i,a_i,s_i')$; (重复 $K$ 次)
  4. $\theta\leftarrow\theta-\alpha\sum_i\frac{dQ_\theta}{d\theta}(s_i,a_i)\Big(Q_\theta(s_i,a_i)-\big[r(s_i,a_i)+\gamma\max_{a'}Q_{\bar\theta}(s_i',a')\big]\Big)$。

第 3、4 步构成内层循环(做 $K$ 次),第 2 步到第 4 步构成中层循环(做 $N$ 次),第 1 步是最外层。

3.2 「经典」DQN

取 $K=1$,并把外层循环摊平成一个流水线,就是 Mnih et al. 2013 的算法:

  1. 执行动作 $a_i$,观测到 $(s_i,a_i,s_i')$,加入 $\mathcal{R}$;
  2. 从 $\mathcal{R}$ 均匀采一个 mini-batch $\{s_i,a_i,s_i'\}$;
  3. 用目标网络 $Q_{\bar\theta}$ 计算 $y_i=r(s_i,a_i)+\gamma\max_{a_i'}Q_{\bar\theta}(s_i',a_i')$;
  4. $\theta\leftarrow\theta-\alpha\sum_i\frac{dQ_\theta}{d\theta}(s_i,a_i)\big(Q_\theta(s_i,a_i)-y_i\big)$;
  5. 更新 $\bar\theta$:每 $N$ 步把 $\theta$ 复制过去。

三行代码级别的差异,带来的是能不能训出东西的差异。几个实现细节值得单独强调:

  • 终止状态。如果 $s_i'$ 是终止状态,目标必须写成 $y_i=r_i$,即把 $\gamma\max_{a'}Q_{\bar\theta}$ 项乘上 $(1-d_i)$,其中 $d_i\in\{0,1\}$ 是 done 标志。漏掉这一项会让智能体以为「死了之后还有未来收益」,在很多任务上直接学不出来。注意区分真终止(游戏结束)与超时截断(time limit),后者严格来说不应该被当成终止。
  • 只对采取过的动作反传。网络输出是一个 $|\mathcal{A}|$ 维向量,我们只在 $a_i$ 这个分量上有监督信号,其余分量的梯度必须是 0。用 gather 取出对应分量即可。
  • $\epsilon$ 的退火。Atari 的标准配置是 $\epsilon$ 从 1.0 在前 100 万帧内线性降到 0.1(后期再降到 0.01),而不是一开始就用小 $\epsilon$。

3.3 可运行的最小 PyTorch 实现

下面这段代码去掉了卷积、帧堆叠、reward clipping 等 Atari 特有的工程细节,保留 DQN 的全部核心逻辑,可以直接在 CartPole 这类小任务上跑通。注意 y 是在 torch.no_grad() 里算的——这正是第 1 节说的「切断目标的梯度」。

import random, collections
import torch, torch.nn as nn, torch.nn.functional as F

class ReplayBuffer:
    """环形回放池,存 (s, a, r, s', done) 五元组。"""
    def __init__(self, capacity=100_000):
        self.buf = collections.deque(maxlen=capacity)

    def add(self, s, a, r, s2, done):
        self.buf.append((s, a, r, s2, float(done)))

    def sample(self, batch_size):
        batch = random.sample(self.buf, batch_size)
        s, a, r, s2, d = zip(*batch)
        return (torch.as_tensor(s, dtype=torch.float32),
                torch.as_tensor(a, dtype=torch.int64),
                torch.as_tensor(r, dtype=torch.float32),
                torch.as_tensor(s2, dtype=torch.float32),
                torch.as_tensor(d, dtype=torch.float32))

    def __len__(self):
        return len(self.buf)


def mlp(obs_dim, n_act, hidden=128):
    return nn.Sequential(nn.Linear(obs_dim, hidden), nn.ReLU(),
                         nn.Linear(hidden, hidden), nn.ReLU(),
                         nn.Linear(hidden, n_act))


class DQN:
    def __init__(self, obs_dim, n_act, gamma=0.99, lr=1e-3,
                 target_sync=500, double_q=True):
        self.q       = mlp(obs_dim, n_act)          # 当前网络  theta
        self.q_targ  = mlp(obs_dim, n_act)          # 目标网络  theta_bar
        self.q_targ.load_state_dict(self.q.state_dict())
        for p in self.q_targ.parameters():          # 目标网络永远不接收梯度
            p.requires_grad_(False)
        self.opt = torch.optim.Adam(self.q.parameters(), lr=lr)
        self.gamma, self.n_act = gamma, n_act
        self.target_sync, self.double_q = target_sync, double_q
        self.step_count = 0

    @torch.no_grad()
    def act(self, s, eps):
        """epsilon-greedy 行为策略。"""
        if random.random() < eps:
            return random.randrange(self.n_act)
        s = torch.as_tensor(s, dtype=torch.float32).unsqueeze(0)
        return int(self.q(s).argmax(dim=1).item())

    def update(self, buffer, batch_size=64):
        s, a, r, s2, d = buffer.sample(batch_size)

        # ---- 目标值:整段不回传梯度 ----
        with torch.no_grad():
            if self.double_q:
                # 用【当前网络】选动作,用【目标网络】估值
                a_star = self.q(s2).argmax(dim=1, keepdim=True)
                q_next = self.q_targ(s2).gather(1, a_star).squeeze(1)
            else:
                q_next = self.q_targ(s2).max(dim=1).values
            y = r + self.gamma * (1.0 - d) * q_next   # 终止状态截断 bootstrap

        # ---- 预测值:只取实际执行过的那个动作 ----
        q_pred = self.q(s).gather(1, a.unsqueeze(1)).squeeze(1)

        loss = F.smooth_l1_loss(q_pred, y)            # Huber loss
        self.opt.zero_grad()
        loss.backward()
        nn.utils.clip_grad_norm_(self.q.parameters(), 10.0)
        self.opt.step()

        # ---- 周期性硬同步目标网络 ----
        self.step_count += 1
        if self.step_count % self.target_sync == 0:
            self.q_targ.load_state_dict(self.q.state_dict())
        return float(loss)


def train(env, episodes=500, warmup=1000,
          eps_start=1.0, eps_end=0.05, eps_decay_steps=10_000):
    agent  = DQN(env.observation_space.shape[0], env.action_space.n)
    buffer = ReplayBuffer()
    total  = 0
    for ep in range(episodes):
        s, _ = env.reset()
        done, ep_ret = False, 0.0
        while not done:
            frac = min(1.0, total / eps_decay_steps)          # 线性退火
            eps  = eps_start + frac * (eps_end - eps_start)
            a = agent.act(s, eps)
            s2, r, term, trunc, _ = env.step(a)
            # 只有真终止才截断 bootstrap,超时截断不算
            buffer.add(s, a, r, s2, term)
            s, ep_ret, total, done = s2, ep_ret + r, total + 1, term or trunc
            if len(buffer) >= warmup:
                agent.update(buffer)
        if ep % 20 == 0:
            print(f'episode {ep:4d}  return {ep_ret:7.1f}  eps {eps:.3f}')
    return agent
注意

代码里有三个「不写就会悄悄失效」的点:(1) with torch.no_grad() 包住目标计算;(2) (1.0 - d) 处理终止状态,且传入的是 term 而非 term or trunc;(3) warmup 步之前只收集数据不更新——否则前几十个 batch 全是重复采样的同几个转移,网络会先严重过拟合到一个错误的解上。

4. 目标网络怎么更新:硬拷贝的锯齿与 Polyak 平均

第 5 步「每 $N$ 步复制一次」看起来无伤大雅,但它其实制造了一个相当别扭的现象。

目标网络滞后量的锯齿形变化与 Polyak 平均
下方色带展示训练时间轴:蓝色块是收集转移 $(s,a,s')$,绿色块是参数更新 $\theta$。黑色弧线表示「目标是从哪一时刻的参数取来的」。刚做完 $\bar\theta\leftarrow\theta$ 的瞬间,弧线长度为 0(no lag here);到周期末尾,弧线跨越整个周期(maximal lag)。这种滞后量从 0 线性涨到 $N$ 再瞬间归零的「锯齿」让人不安:Feels weirdly uneven, can we always have the same lag?

4.1 硬更新的问题:滞后量在 0 和 N 之间锯齿跳变

设当前是同步后的第 $k$ 步($0\le k<N$)。此刻的目标网络参数等于 $k$ 步之前的当前网络参数,即滞后 $k$ 步。于是:

  • $k=0$ 时滞后为 0——目标网络就是当前网络,等价于完全没有目标网络的在线 Q-learning,最不稳定的状态;
  • $k=N-1$ 时滞后为 $N$——目标网络严重过时,学到的东西全部来自一万步前的旧知识。

换句话说,DQN 在一个周期内的「稳定性/时效性」权衡是连续变化的:周期开头最不稳但最新鲜,周期末尾最稳但最陈旧。而且在 $\bar\theta\leftarrow\theta$ 那一瞬间,所有样本的目标值会集体跳变,loss 曲线上会看到规律的尖峰。这不是 bug,但它意味着优化器(尤其是带动量的 Adam)的内部状态在每次同步后都要重新适应一次新的损失曲面。

4.2 Polyak 平均:让滞后永远一样

更受欢迎的替代方案是软更新(soft update),形式上类似 Polyak 平均:把第 5 步换成每一步都做一次微小的指数滑动平均,

$$ \bar\theta\leftarrow\tau\,\bar\theta+(1-\tau)\,\theta,\qquad \tau=0.999\ \text{通常效果不错} $$

(很多论文写作 $\phi'\leftarrow\tau\phi+(1-\tau)\phi'$,其中 $\phi$ 是当前 critic 参数、$\phi'$ 是目标 critic 参数;有的库把 $\tau$ 定义成小的那一侧,即 $\bar\theta\leftarrow(1-\tau)\bar\theta+\tau\theta$,$\tau=0.005$。读代码时务必确认哪个是「保留比例」。)

推导:软更新的等效滞后

把递推展开。记 $\beta=1-\tau$,则

$$ \bar\theta_t=\beta\sum_{k=0}^{\infty}\tau^{k}\,\theta_{t-k} $$

也就是说目标网络是历史所有当前参数的指数加权平均,权重和为 $\beta\sum_k\tau^k=\beta\cdot\frac{1}{1-\tau}=1$。这个加权平均的「平均滞后」是

$$ \E[k]=\beta\sum_{k=0}^{\infty}k\,\tau^{k}=\beta\cdot\frac{\tau}{(1-\tau)^2}=\frac{\tau}{1-\tau} $$

取 $\tau=0.999$ 得 $\E[k]=999$ 步;取 $\tau=0.995$ 得 199 步。所以 $\tau$ 和硬更新周期 $N$ 大致有 $N\approx\frac{2\tau}{1-\tau}$ 量级的换算关系(硬更新平均滞后是 $N/2$)。关键区别在于这个滞后是恒定的、不跳变的,而且目标网络是一个平滑的参数平均,本身就比任何单个快照噪声更小。

4.3 两种更新方式的对比

硬更新(周期复制)软更新(Polyak)
形式每 $N$ 步 $\bar\theta\leftarrow\theta$每步 $\bar\theta\leftarrow\tau\bar\theta+(1-\tau)\theta$
滞后量在 $0\sim N$ 之间锯齿变化恒为 $\tau/(1-\tau)$,平稳
目标跳变同步瞬间集体跳变,loss 有周期尖峰连续平滑,无跳变
额外开销几乎为零($N$ 步才拷一次)每步一次全参数的加权和,略贵
超参数敏感度$N$ 需要按任务调,量级差别大$\tau\in[0.99,0.999]$ 在很多任务上通用
典型使用者DQN / Rainbow 系列(离散动作)DDPG / TD3 / SAC 系列(连续动作)
直觉

软更新还有一个隐性好处:参数平均本身有类似 SWA(stochastic weight averaging)的正则效果。SGD 在损失曲面上的轨迹是围绕某个区域来回震荡的,取平均往往落在比任何单点都更平坦、泛化更好的位置。目标网络恰好就是这样一个平均,所以在很多连续控制任务上,$Q_{\bar\theta}$ 的预测比 $Q_\theta$ 更平滑、噪声更小——这对下一节要讲的过估计问题也有间接帮助。

注意

软更新不是万能替换。当 $\tau$ 过大(比如 0.9999)时,目标网络几乎冻结,值传播极慢;当 $\tau$ 过小(比如 0.9)时,等效滞后只有 9 步,几乎退化成没有目标网络。另外,如果网络里有 BatchNorm,其 running statistics 也属于需要同步的 buffer,直接对可训练参数做 Polyak 而忘了 buffer 会导致目标网络行为诡异——这也是为什么 RL 里大家更偏爱 LayerNorm 或干脆不用归一化层。

5. 统一视角:三个进程与它们的相对速度

这一节是本讲最有「组织性」的部分。Levine 指出,从在线 Q-learning 到 DQN 再到 fitted Q-iteration,本质上是同一个系统——它由三个并行运行的进程组成,区别只在于让谁跑得快、谁跑得慢。

Q-learning 的三个并行进程示意图
把一般化的 Q-learning 算法框画成系统图:进程 1(数据收集)——用 $\pi(a|s)$(比如 $\epsilon$-greedy)在真实环境(地球图标)里交互,把 $(s,a,s')$ 灌进回放池,同时把最老的数据丢进垃圾桶(evict old data);进程 2(目标更新)——把当前参数 $\theta$ 拷贝到目标参数 $\bar\theta$;进程 3(Q 函数回归)——从回放池取 batch,用 $\bar\theta$ 算目标、更新 $\theta$。右上角标注了 DQN: $N=1$, $K=1$。

5.1 三个进程

  • 进程 1:数据收集。执行行为策略,产出转移,写入 buffer,并按 FIFO 淘汰旧数据。它的速度就是「环境交互步数 / 秒」。
  • 进程 2:目标网络更新。把 $\theta$ 同步到 $\bar\theta$。速度由 $N$(或软更新的 $\tau$)决定。
  • 进程 3:Q 函数回归。从 buffer 采样、算 loss、做梯度步。速度就是「梯度步 / 秒」。
三个进程的相对速度决定了算法名字
同一张系统图,下方三条要点给出「改变相对速度 = 换一个算法」的字典:在线 Q-learning 是三个进程等速且立即淘汰数据;DQN 是进程 1、3 等速而进程 2 很慢;fitted Q-iteration 则是进程 3 嵌在进程 2 的内循环、进程 2 又嵌在进程 1 的内循环。
算法buffer进程 1(收数据)进程 2(更目标)进程 3(回归)
在线 Q-learning无(立即淘汰)1 步每步都同步(等价于无目标网络)1 次梯度步
DQN大($10^5\sim10^6$)1 步每 $N=10^4$ 步一次(慢)1 次梯度步($K=1$)
fitted Q-iteration整批数据集最外层:收一大批中层:每批数据更新若干轮最内层:跑完整回归($K\gg1$)

这张表的价值在于:它把三个看起来不同的算法归约成两个整数 $N$ 和 $K$ 的取值。你在读任何一篇值函数论文时,都可以先问「它的三个进程速度比是多少」,往往就抓住了它相对于 DQN 的全部改动。

5.2 增大 $K$:UTD 比率

UTD 比率:增大 K 会怎样
回到算法框,Levine 问了两个问题:增大 $K$ 会怎样?减小 $N$ 会怎样?并给出 $K$ 的行话——update-to-data(UTD)ratio,即「每收集一个环境转移做多少次梯度更新」。高 UTD 学得快但更耗计算;UTD 过高会「过拟合」。

UTD 比率(update-to-data ratio,有时也写作 replay ratio)是现代 off-policy RL 里最重要的效率旋钮。DQN 的默认设置其实是 UTD $=0.25$(每 4 个环境步做 1 次梯度步),SAC 常用 UTD $=1$,而 REDQ、DroQ 这类「样本高效」算法把它推到 10 甚至 20。

  • UTD 高 ⇒ 样本效率高。同样的 100 万个环境交互,做 2000 万次梯度步显然比做 25 万次学得更充分。当环境交互很贵(真实机器人)而计算很便宜时,这笔买卖非常划算。
  • UTD 高 ⇒ 计算成本线性上升。墙钟时间和显卡占用都按比例增加。
  • UTD 过高 ⇒ 「过拟合」。这是最微妙的一点。同一批数据被反复回归,网络会对 buffer 里那些 $(s,a)$ 记得过牢,而在 buffer 之外的 $(s',a')$ 上的外插变得离谱。偏偏 bootstrap 目标 $\max_{a'}Q_{\bar\theta}(s',a')$ 就要在这些没见过的动作上求值,误差被系统性放大。这个现象在离线 RL(offline RL)里是核心难题,后面的课会专门讲。缓解手段包括周期性重置网络(primacy bias / reset)、加大集成规模、层归一化等。

5.3 减小 $N$:稳定性与时效性的另一端

$N$ 变小 ⇒ 目标网络更新更频繁 ⇒ 新学到的值传播得更快,但目标更不稳定;$N\to1$ 就完全退回到病灶二。所以 $N$ 和 $K$ 是一对方向相反的旋钮:$K$ 决定你从每个样本里榨取多少信息,$N$ 决定你敢让 bootstrap 的自反馈回路转得多快。两个都往激进方向拧,训练几乎必然发散。

核心结论

DQN 不是一个「新算法」,它是一般化 fitted Q-iteration 在 $N=1,K=1$、buffer 很大、目标网络很慢时的一个特例。理解了三进程视角,你就能把 fitted Q-iteration、DQN、SAC、REDQ、离线 RL 里的各种方法放在同一张坐标图上比较,而不是把它们当成互不相干的一串缩写。

6. 值传播太慢:N-step return 与它的代价

值传播示意:走廊网格与 N-step return 公式
右上角是一个走廊型网格世界:起点 S 在最左,唯一的 $+1$ 奖励在最右,中间隔着 5 格空地(红色是墙)。算法框里第 3 步的单步目标被划掉,换成下方的 $n$-step 目标。两个黄色提问依旧是「why is this good? / why is this bad?」。

6.1 一个具体的数值例子

假设 $Q_{\bar\theta}(s,a)=0$ 对所有 $(s,a)$(网络刚初始化)。智能体从 $S$ 出发,走到最右格拿到 $+1$。这一步的转移 $(s_5,a,s_{\text{goal}})$ 进入 buffer 后,它的目标是 $y=1+\gamma\cdot 0=1$,于是 $Q(s_5,a)$ 被拉向 1。但 $Q(s_4,\cdot)$ 仍然是 0,因为它的目标是 $0+\gamma\max_{a'}Q_{\bar\theta}(s_5,a')$,而 $\bar\theta$ 还是老参数。

要让 $+1$ 传到 $s_4$,需要:(a) $\theta$ 学会 $Q(s_5)\approx1$;(b) 目标网络同步一次;(c) 再采样到 $(s_4,a,s_5)$ 并做一次更新。走廊长 5 格,就要重复这个流程 5 遍。如果 $N=10^4$,光是「等目标网络同步」这一项就要 $5\times10^4$ 个梯度步——而这只是一条 5 格的走廊。在 Atari 里,从「吃到一颗豆」倒推到「几百帧前的那次转向」,单步 bootstrap 需要的更新次数是灾难性的。

直觉

单步 bootstrap 让奖励信息像渗水一样一格一格往回渗,速度由目标网络同步频率决定。而 Monte Carlo(用整条轨迹的实际回报当目标)则是一步到位:只要走通一次,起点立刻知道「这条路值 $\gamma^5$」。$n$-step return 就是在这两个极端之间取一个 $n$。

6.2 N-step return 的定义

把单步目标里的「一步奖励 + bootstrap」推广成「$n$ 步奖励 + bootstrap」:

$$ y_t^{(i)}=\sum_{t'=t}^{t+n-1}\gamma^{t'-t}\,r\big(s_{t'}^{(i)},a_{t'}^{(i)}\big)+\gamma^{n}\max_{a_{t+n}^{(i)}}Q_{\bar\theta}\big(s_{t+n}^{(i)},a_{t+n}^{(i)}\big) $$

$n=1$ 时退回标准 Q-learning;$n\to\infty$(或直到 episode 结束)时退化为 Monte Carlo 回报,完全不依赖 $Q_{\bar\theta}$。

回到走廊例子:取 $n=5$,那么从 $S$ 出发的那条 5 步子轨迹一次就把 $+1$ 直接写进 $y$,$Q(S,a)\to\gamma^4$,一次更新完成全部传播,完全不需要等目标网络。

6.3 好处:更低的偏差、更快的早期学习

  • 偏差更小。$Q_{\bar\theta}$ 在训练早期基本是垃圾(初始化的随机值)。单步目标里,$\gamma\max Q_{\bar\theta}$ 这一项占了目标的绝大部分权重,等于「用垃圾监督垃圾」。而 $n$-step 目标里真实奖励占 $\sum_{k=0}^{n-1}\gamma^k$ 的权重,$Q_{\bar\theta}$ 只占 $\gamma^n$。$n$ 越大,目标里「真实信号」的比例越高。
  • 早期学习显著更快。这也是 Rainbow 的消融实验里 $n$-step(通常 $n=3$)被认为是贡献最大的单项改进之一的原因。

6.4 坏处:off-policy 下它不再正确

这是关键的技术细节。单步目标 $r(s,a)+\gamma\max_{a'}Q(s',a')$ 只用到 $(s,a,r,s')$,而给定 $(s,a)$ 后,$r$ 和 $s'$ 的分布只由环境决定,与策略无关——这正是 Q-learning 能 off-policy 的根本原因。

但 $n$-step 目标里出现了 $r(s_{t+1},a_{t+1}),\dots,r(s_{t+n-1},a_{t+n-1})$,这些奖励依赖于 $a_{t+1},\dots,a_{t+n-1}$,而这些动作是当年采数据时那个旧策略选的。我们想估计的是「从 $s_t$ 出发、之后一直按最优策略行动」的价值,用的却是「之后按旧的 $\epsilon$-greedy 策略行动」的真实奖励。两者不一致。

推导:偏差从哪里来

我们想要的是 $Q^\star(s_t,a_t)=\E\big[r_t+\gamma\max_{a}Q^\star(s_{t+1},a)\big]$。展开两步:

$$ Q^\star(s_t,a_t)=\E\Big[r_t+\gamma\,r(s_{t+1},a^\star_{t+1})+\gamma^2\max_a Q^\star(s_{t+2},a)\Big],\quad a^\star_{t+1}=\argmax_a Q^\star(s_{t+1},a) $$

注意第二项里的动作必须是 $a^\star_{t+1}$。而 buffer 里存的是 $a_{t+1}\sim\pi_{\text{old}}$。只有当 $a_{t+1}=a^\star_{t+1}$ 时两者才一致。

因此 $n$-step 目标估计的其实是介于 $Q^{\pi_{\text{old}}}$ 和 $Q^\star$ 之间的某个东西:$n$ 越大,越偏向「按旧策略行动的价值」。特例是 $n=1$——此时不涉及任何后续动作,所以严格 off-policy 正确。这就是为什么 $n=1$ 是唯一「免费」的选择。

另外还有一个纯统计层面的代价:方差随 $n$ 增大。$n$-step 目标里累加了 $n$ 个随机奖励和 $n$ 步随机转移,方差大致随 $n$ 线性增长(在奖励独立的粗略近似下)。这就是经典的偏差-方差权衡:小 $n$ 偏差大(依赖不准的 $Q_{\bar\theta}$)方差小,大 $n$ 偏差小方差大。实践中 $n=3\sim5$ 是甜点。

6.5 三种修正办法

方案做法评价
1. 直接无视就当它是对的,照用不误Levine 的原话是「often works quite well」。因为 $\epsilon$ 通常已经很小,旧策略与贪心策略差别不大;而且 $n$ 只取 3~5,偏差累积有限。Rainbow、Ape-X、R2D2 全都这么干。
2. 动态截断(cut the trace)从 $t$ 开始往后扫,只要发现某一步的 $a_{t'}\ne\argmax_a Q(s_{t'},a)$,就在那里截断,用截断后的实际长度 $n'\le n$ 做目标只保留「碰巧是 on-policy」的那一段,因此严格无偏。代价是当 $\epsilon$ 较大或数据较旧时,$n'$ 经常退化成 1,好处消失。在小 $\epsilon$ 且数据新鲜时效果好。
3. 重要性采样(importance sampling)给每一步奖励乘上比值 $\prod_{t'=t+1}^{t+k}\frac{\pi(a_{t'}|s_{t'})}{\pi_{\text{old}}(a_{t'}|s_{t'})}$ 做加权理论上无偏,但连乘导致方差指数爆炸(下一讲会详细分析这个现象)。实用做法是裁剪比值,比如 Retrace$(\lambda)$ 用 $\min(1,\rho_{t'})$、Tree-Backup、V-trace 等,用一点偏差换回可控的方差。
常见误区

「$n$-step 需要按顺序存整条轨迹,所以不能用 replay buffer」——不对。标准做法是在写入 buffer 时就预先把 $n$ 步的折扣奖励和累加好,存 $(s_t,a_t,R_t^{(n)},s_{t+n},\gamma^n,\text{done})$ 这样的六元组,采样时和普通 DQN 完全一样。需要额外维护的只是一个长度为 $n$ 的滑动窗口队列,以及在 episode 结束时正确处理不足 $n$ 步的尾巴。

7. 过估计:$\E[\max]\ge\max[\E]$ 与 Double Q-learning

前面所有讨论都围绕「优化过程稳不稳」。这一节换一个角度:即使优化过程完全稳定,Q-learning 学出来的值也是系统性偏高的。

7.1 先看证据

DQN 在 Breakout 和 Seaquest 上的回报与预测 Q 值曲线
DQN 原论文的经典图。上排左二是 Breakout 和 Seaquest 的每回合平均奖励——抖动剧烈;右二是同期的平均预测 $Q$ 值——单调平滑上升。Levine 用它说明:预测 $Q$ 涨,回报也跟着涨,所以 $Q$ 值曲线是比回报曲线更好的训练监控量。下排展示了在具体帧上 $Q$ 值的变化:Breakout 中球即将击中砖块时 $V$ 冲高;Pong 中在球飞来时 UP/DOWN/NO-OP 三个动作的 $Q$ 值出现明显分化,说明网络确实学到了有意义的动作偏好。

「$Q$ 涨、回报也涨」听上去挺好。但如果去问一个更苛刻的问题——预测的 $Q$ 值在数值上准不准——答案就难看了。

DQN 与 Double DQN 的值估计与真实值对比
来自 Double DQN 论文的四个 Atari 游戏。红色曲线是 DQN 的值估计,红色水平线是对应策略的真实折扣回报;蓝色是 Double DQN 的估计与真实值。四张图里红色曲线都远高于红色水平线——Alien 上估计约 15 而真实约 9,Zaxxon 上估计约 7 而真实约 1,高估了好几倍。蓝色曲线则贴着蓝色水平线,几乎没有偏差,而且蓝色的真实值本身比红色更高,说明修掉高估还能换来更好的策略。

7.2 病根:对噪声取最大值必然偏高

过估计的数学根源:E[max] ≥ max[E]
右上角的小图是理解这一切的钥匙:绿色平滑曲线是真实的 $Q(s',a')$ 随动作变化的形状,橙色抖动曲线是带噪声的估计 $Q_{\bar\theta}$。橙色星星标出的是噪声曲线的最大值,黄色星星是真实曲线的最大值——橙星总是比黄星高。左侧文字给出结论链:目标里的 $\max_{a'}Q_{\bar\theta}(s',a')$ 是问题所在;$\E[\max(X_1,X_2)]\ge\max(\E[X_1],\E[X_2])$;$Q_{\bar\theta}$ 有噪声;所以 $\max_{a'}Q_{\bar\theta}$ 高估了下一状态的值。最后一行是关键的重写:$\max_{a'}Q_{\bar\theta}(s',a')=Q_{\bar\theta}\big(s',\argmax_{a'}Q_{\bar\theta}(s',a')\big)$——值来自 $Q_{\bar\theta}$,动作也由 $Q_{\bar\theta}$ 选。
推导:$\E[\max_a Q]\ge\max_a\E[Q]$

设 $X_1,\dots,X_m$ 是任意随机变量。对任意固定的 $j$,逐点有 $\max_i X_i\ge X_j$,两边取期望得 $\E[\max_i X_i]\ge\E[X_j]$。由于这对每个 $j$ 都成立,取右侧的最大值:

$$ \E\big[\max_i X_i\big]\ \ge\ \max_j \E[X_j] $$

(这其实就是 Jensen 不等式对凸函数 $\max$ 的一个特例。)

什么时候是严格不等?只要各 $X_i$ 不是几乎处处相等,就严格大于。差距的大小取决于噪声的规模和动作数:设 $Q_{\bar\theta}(s',a)=Q^\star(s',a)+\varepsilon_a$,$\varepsilon_a\sim\mathcal{N}(0,\sigma^2)$ 独立同分布。若所有动作真值相同(最坏情形),则

$$ \E\big[\max_a Q_{\bar\theta}(s',a)\big]-\max_a Q^\star(s',a)=\sigma\cdot\E\big[\max_a \varepsilon_a/\sigma\big]\approx\sigma\sqrt{2\ln m} $$

取 $m=18$(Atari 的动作数)、$\sigma=0.5$,偏差约 $0.5\times\sqrt{2\ln 18}\approx 1.2$。这看起来不大,但——

致命的是它会被 bootstrap 复利放大。每一步目标都比真值高 $b$,于是 $Q$ 收敛到的不动点满足 $Q=r+\gamma(Q^{\star}_{\text{next}}+b)$,累积偏差是 $b+\gamma b+\gamma^2 b+\dots=\frac{b}{1-\gamma}$。$\gamma=0.99$ 时放大 100 倍:$1.2\to120$。这正是上一张图里「估计 7、真值 1」的来源。

直觉

$\max$ 是一个「噪声放大器」:它专门挑那个碰巧被噪声推得最高的动作。假设 18 个动作真值都是 0,估计值是 18 个独立的标准正态样本,那么 $\max$ 的期望约 2.1,而不是 0。取 $\max$ 这个操作本身就带着一种「乐观偏见」——它会把随机运气当成真本事。用赌博打比方:让 100 个人各扔 10 次硬币,最好的那个人大概率扔出 9 次正面,但你不会因此认为他扔硬币有技术。$\max_a Q$ 恰恰就在做这个错误的推断。

7.3 解耦:Double Q-learning

Double Q-learning:用两个网络分别选动作与估值
核心洞见写在中间:$\max_{a'}Q_{\bar\theta}=Q_{\bar\theta}(s',\argmax_{a'}Q_{\bar\theta}(s',a'))$ 里,「取值」和「选动作」用的是同一个网络,所以两处的噪声完全相关——选中的动作恰恰是噪声最正的那个,取值时又把这个正噪声原封不动地读出来。if the noise in these is decorrelated, the problem goes away! 于是用两个网络交叉更新。右下角的小图画出两条不同噪声的曲线,各自的最大值点不重合,取另一条曲线在该点的值就不会系统偏高。

形式化的 double Q-learning 用两套参数 $\theta_A,\theta_B$,交叉更新:

$$ Q_{\theta_A}(s,a)\leftarrow r+\gamma\,Q_{\theta_B}\Big(s',\ \argmax_{a'}Q_{\theta_A}(s',a')\Big) $$ $$ Q_{\theta_B}(s,a)\leftarrow r+\gamma\,Q_{\theta_A}\Big(s',\ \argmax_{a'}Q_{\theta_B}(s',a')\Big) $$

为什么这就消除了偏差?设 $a^\star=\argmax_{a'}Q_{\theta_A}(s',a')$。这个动作之所以被选中,可能是因为 $\theta_A$ 在它上面的噪声 $\varepsilon^A_{a^\star}$ 特别正。但我们接下来读的是 $Q_{\theta_B}(s',a^\star)$,它的噪声 $\varepsilon^B_{a^\star}$ 与 $\varepsilon^A_{a^\star}$ 独立,条件期望为 0。于是

$$ \E\big[Q_{\theta_B}(s',a^\star)\ \big|\ a^\star\big]=Q^\star(s',a^\star)\ \le\ \max_{a'}Q^\star(s',a') $$

不但没有高估,反而可能略微低估(因为 $a^\star$ 不一定是真正的最优动作)。这个方向的偏差在 RL 里危害小得多——低估会让智能体保守,而高估会让它反复去撞一个根本不存在的高价值状态。

7.4 实践版本:Double DQN,只改一个下标

Double DQN 的目标公式与标准 Q-learning 的对比
「两个 Q 函数从哪来?直接用当前网络和目标网络!」标准 Q-learning 的目标是 $y=r+\gamma Q_{\bar\theta}(s',\argmax_{a'}Q_{\bar\theta}(s',a'))$;double Q-learning 只把红圈里 argmax 内部的下标从 $\bar\theta$ 换成 $\theta$:$y=r+\gamma Q_{\bar\theta}(s',\argmax_{a'}Q_{\theta}(s',a'))$。用当前网络选动作,仍用目标网络估值。
选动作用估值用目标公式
标准 DQN$Q_{\bar\theta}$$Q_{\bar\theta}$$y=r+\gamma\max_{a'}Q_{\bar\theta}(s',a')$
Double DQN$Q_{\theta}$$Q_{\bar\theta}$$y=r+\gamma\,Q_{\bar\theta}\big(s',\argmax_{a'}Q_{\theta}(s',a')\big)$

代码上就是把 q_targ(s2).max(1).values 换成先 q(s2).argmax(1) 再 q_targ(s2).gather(...)——本讲第 3 节的实现里已经写好了这两条分支。计算开销几乎为零(多一次前向,而这次前向本来在选动作时也常常要做)。

注意

$\theta$ 和 $\bar\theta$ 并不真正独立——$\bar\theta$ 是 $\theta$ 若干步之前的快照,两者的噪声相关性很高。所以 Double DQN 只是「部分」解耦,它减轻而非消除过估计。真正独立的做法是维护两个从不同随机种子初始化、用不同数据顺序训练的网络(如 TD3 的双 critic),这也是为什么 TD3/SAC 的效果比 Double DQN 更彻底。此外,目标网络的滞后越大,$\theta$ 与 $\bar\theta$ 的解耦越充分,所以 Double DQN 与较慢的目标更新是互补的。

7.5 推广:任意策略与 clipped double-Q

广义 double Q-learning 与 clipped double Q-learning
上半部分把 $\argmax_{a'}Q_\theta(s',a')$ 重新解释成「从某个策略 $\pi_\theta(a'|s')$ 里采动作再取期望」:$\E_{a'\sim\pi_\theta}[Q_{\bar\theta}(s',a')]$。原本 $\pi_\theta$ 是那个退化的贪心分布(1/0 指示),但红线划掉了这个限制——could be anything, including another actor! 下半部分给出「clipped」double Q-learning:$y=r+\gamma\,\E_{a'\sim\pi_\theta}\big[\min_{i\in\{1,2\}}Q_{\bar\theta_i}(s',a')\big]$,即对一个(通常是 2 个)目标 Q 网络的集成取 min。黄色标注:这在纯 Q-learning 里不常见,但在 Q 函数 actor-critic 里极其常用。

这一页是通往后面 TD3/SAC 的桥梁,值得逐层拆开:

  • 第一层推广:把 argmax 换成一个策略。目标写成 $y=r+\gamma\,\E_{a'\sim\pi(a'|s')}[Q_{\bar\theta}(s',a')]$。当 $\pi$ 是贪心策略时,这就是原来的 $\max$;当 $\pi$ 是一个单独训练的 actor 网络时,这就变成了 off-policy actor-critic;当 $\pi$ 是带熵正则的软策略时,就是 SAC。Q-learning 和 actor-critic 的界限在这里彻底消失了。
  • 第二层推广:把「另一个网络」换成「一组网络取 min」。$\min_{i\in\{1,2\}}Q_{\bar\theta_i}$ 比 Double DQN 更激进:Double DQN 只是解耦噪声,而 min 是主动往下压。它必然引入低估偏差,但在连续动作 + actor 会主动去搜索 $Q$ 高点的场景下,这种保守性是必需的——否则 actor 会精确地找到 critic 的误差峰值并疯狂利用它(所谓的 exploitation of critic error)。

为什么 min 在离散动作 Q-learning 里不常用,在连续动作里却是标配?因为离散动作只有十几个候选,$\max$ 的乐观偏差有限;而连续动作空间里 actor 可以在整个 $\mathbb{R}^d$ 上做梯度上升,它总能找到 critic 外插最离谱的那个角落。TD3 论文正是因为这个观察提出 clipped double-Q。

8. 连续动作:那个 $\max_a$ 怎么办

到目前为止的所有讨论都默认动作是离散的——网络输出 $|\mathcal{A}|$ 个数,取 $\max$ 就是取一个向量的最大分量,$O(|\mathcal{A}|)$ 时间。一旦动作变成 $\mathbb{R}^d$ 中的连续向量(机器人关节力矩、方向盘转角),这个 $\max$ 就成了本身就很难的非凸优化问题。

连续动作下的两处 max
红圈标出两处需要 $\max$ 的地方:一是行为策略 $\pi(a_t|s_t)$ 里的 $\argmax_{a_t}Q_\theta(s_t,a_t)$,二是目标值里的 $\gamma\max_{a'}Q_{\bar\theta}(s',a')$。红字特别注明第二处「particularly problematic」——因为它在训练的内循环里,每个 batch 的每个样本都要算一次。第一处每个环境步只算一次,慢一点还能忍;第二处慢一点,整个训练就没法进行了。

8.1 路线一:把它当成优化问题解

最直接的想法是对 $a$ 做梯度上升:$a\leftarrow a+\eta\nabla_a Q_\theta(s,a)$。可行,但在内循环里跑一个迭代优化器代价太高,而且 $Q$ 关于 $a$ 非凸,容易陷进局部极大。Levine 的观察是:动作空间通常维度很低(几维到几十维,远小于状态维度),既然维度低,随机优化就变得很有竞争力。

用随机优化近似连续动作的 max
最简单的方案:$\max_a Q(s,a)\approx\max\{Q(s,a_1),\dots,Q(s,a_N)\}$,其中 $(a_1,\dots,a_N)$ 从某个分布(例如均匀分布)采样。右边三行评价:+ 极其简单、+ 可高效并行、- 不太准确。中间一句反问点破关键:但……我们在乎吗?目标值到底需要多准? 下方是更精确的方案:交叉熵方法(CEM)和 CMA-ES,注明「在约 40 维以内工作良好」。右侧黄标:这类做法在离线 RL 里作为 actor-critic 的改进极为流行,也叫 sample and rank 或 rejection sampling。

朴素采样。从均匀分布(或当前 actor)采 $N$ 个候选动作,把它们和状态一起打包成一个 batch 送进网络,取最大的那个 $Q$。这在 GPU 上是一次矩阵乘法,几乎不增加墙钟时间。$N$ 通常取 10~100。

为什么「不准」可以忍?因为目标值本来就是个粗糙的估计——$Q_{\bar\theta}$ 自己就有误差,目标网络还滞后着,采样带来的那点低估相比之下不算什么。而且第 7 节刚说过,低估比高估安全:采样近似必然给出 $\le$ 真实 $\max$ 的值,恰好抵消了一部分过估计偏差。这是一个「两个错误互相抵消」的美妙巧合。

CEM(cross-entropy method,交叉熵方法)。朴素采样的迭代版本,四步循环:

  1. 从高斯 $\mathcal{N}(\mu,\Sigma)$ 采 $N$ 个动作(初始 $\mu$ 为动作空间中心,$\Sigma$ 很大);
  2. 算出 $N$ 个 $Q$ 值,取最高的 $M$ 个(elite,比如 top 10%);
  3. 用这 $M$ 个 elite 重新拟合 $\mu,\Sigma$;
  4. 回到第 1 步,迭代 3~5 轮。

CEM 保留了「一次前向评估一大批动作」的并行优势,同时通过迭代收缩搜索区域大幅提高精度。CMA-ES 是它的进化版,额外维护协方差矩阵的自适应更新,更精确但实现复杂得多。两者都在 40 维以内可用,超过之后样本需求爆炸。

8.2 路线二:设计一个能解析求最大值的网络

另一条思路是:既然通用网络的 $\max_a$ 难求,那就限制网络结构,让 $\max_a$ 有闭式解。NAF(Normalized Advantage Functions,归一化优势函数)把 $Q$ 强制写成关于动作的二次型:

$$ Q_\theta(s,a)=-\tfrac12\big(a-\mu_\theta(s)\big)^{\!\top}P_\theta(s)\big(a-\mu_\theta(s)\big)+V_\theta(s) $$

网络输出三个头:$\mu_\theta(s)\in\mathbb{R}^d$、$V_\theta(s)\in\mathbb{R}$,以及一个下三角矩阵 $L_\theta(s)$,令 $P_\theta(s)=L_\theta(s)L_\theta(s)^\top$ 保证半正定(对角元用 exp 保证为正)。于是二次型是关于 $a$ 的凹函数,最大值点和最大值都直接读出来:

$$ \argmax_a Q_\theta(s,a)=\mu_\theta(s),\qquad \max_a Q_\theta(s,a)=V_\theta(s) $$

算法结构和 DQN 一模一样,没有第二个网络、没有新的超参数、没有额外的优化循环。代价是表达力被严重限制:真实的 $Q(s,\cdot)$ 常常是多峰的(比如绕过障碍物可以从左也可以从右),而二次型只能有一个峰,网络被迫在两个峰之间取一个折中的、可能很糟的动作。

8.3 路线三:学一个近似最大化器(DDPG)

DDPG:训练一个网络去逼近 argmax
推导链条:$\max_a Q_\phi(s,a)=Q_\phi(s,\argmax_a Q_\phi(s,a))$,所以只要训练另一个网络 $\mu_\theta(s)\approx\argmax_a Q_\phi(s,a)$ 就够了。怎么训?直接解 $\theta\leftarrow\argmax_\theta Q_\phi(s,\mu_\theta(s))$,梯度由链式法则给出 $\frac{dQ_\phi}{d\theta}=\frac{da}{d\theta}\frac{dQ_\phi}{da}$。新的目标变成 $y_j=r_j+\gamma Q_{\phi'}(s_j',\mu_\theta(s_j'))\approx r_j+\gamma\max_{a'}Q_{\phi'}(s_j',a')$。两个黄标点出全局意义:这与之前学过的重参数化梯度非常相似;所以 actor-critic 和 Q-learning 并没有那么不同!

DDPG(Deep Deterministic Policy Gradient)的思路一句话就能说清:既然每次现场求 $\argmax$ 太贵,那就把 argmax 缓存进一个网络。$\mu_\theta(s)$ 是一个确定性策略(deterministic policy),它的训练目标就是让 $Q_\phi(s,\mu_\theta(s))$ 尽可能大。

注意 $\frac{dQ_\phi}{d\theta}=\frac{d\mu}{d\theta}\cdot\frac{dQ_\phi}{da}$ 这个链式法则:梯度先从 critic 对动作求导(告诉 actor「动作往哪个方向改会让 $Q$ 变大」),再穿过 actor 网络传到 $\theta$。这与策略梯度里的 reparameterization trick 在数学形式上完全一致——区别只是 DDPG 的策略是确定性的,没有噪声要重参数化。

DDPG 算法与 off-policy actor-critic 的逐行对照
上下两个算法框逐行对齐:DDPG 与 off-policy actor-critic 的第 1、2、4、6 步完全相同,只有第 3 步(目标怎么算)和第 5 步(actor 怎么更新)不同。DDPG 用 $Q_{\bar\phi}(s',\mu_{\bar\theta}(s'))$ 这个确定性代入,actor 用 $\frac{d\mu}{d\theta}\frac{dQ_\phi}{da}$ 这个路径导数;off-policy actor-critic 用 $\E_{a'\sim\pi_{\bar\theta}}[Q_{\bar\phi}(s',a')]$ 这个期望,actor 用 $\nabla_\theta\log\pi_\theta(a|s)Q_\phi(s,a)$ 这个 likelihood-ratio 梯度。右侧箭头指出两者都可以用 clipped double-Q 改进:$\E_{a'\sim\pi_\theta}[\min_{i\in\{1,2\}}Q_{\bar\phi_i}(s',a')]$。

DDPG 完整算法:

  1. 执行动作 $a_i$($\mu_\theta(s)$ 加探索噪声),观测 $(s_i,a_i,s_i')$,存入 $\mathcal{R}$;
  2. 从 $\mathcal{R}$ 均匀采 mini-batch $\{s_j,a_j,s_j'\}$;
  3. 用目标网络算 $y_j=r(s_j,a_j)+\gamma\,Q_{\bar\phi}\big(s_j',\mu_{\bar\theta}(s_j')\big)$;
  4. critic 更新:$\phi\leftarrow\phi-\alpha\sum_j\frac{dQ_\phi}{d\phi}(s_j,a_j)\big(Q_\phi(s_j,a_j)-y_j\big)$;
  5. actor 更新:$\theta\leftarrow\theta+\beta\sum_j\frac{d\mu}{d\theta}(s_j)\frac{dQ_\phi}{da}\big(s_j,\mu(s_j)\big)$;
  6. 用 Polyak 平均更新 $\bar\phi$ 和 $\bar\theta$。

Levine 在最后一页问「我们还学过哪些改进可以加进来?」——答案是本讲的全套:clipped double-Q(TD3 的第一个改进)、目标策略平滑(给 $\mu_{\bar\theta}(s')$ 加裁剪噪声,TD3 的第二个改进)、延迟 actor 更新(TD3 的第三个改进)、N-step return、更大的 buffer。TD3 = DDPG + 前三项,SAC 则进一步把确定性 actor 换成带熵正则的随机 actor。

8.4 三条路线的取舍

随机优化(采样 / CEM)NAF(可解析最大化)DDPG(学最大化器)
核心思路采一堆动作挑最好的限制 $Q$ 为动作的二次型训练 $\mu_\theta(s)\approx\argmax_a Q$
额外网络无无(同一网络多个输出头)有:actor + actor 的目标网络
$\max$ 的精度朴素采样粗糙,CEM 较好在假设成立时精确取决于 actor 训得好不好
表达力不受限严重受限:单峰、二次不受限
维度可扩展性约 40 维以内;再高样本量爆炸好,但 $P(s)$ 是 $d\times d$,$d$ 大时参数多好,可上百维
实现复杂度最低低(注意 Cholesky 参数化)最高,超参数最多
稳定性稳(本身还有轻微低估,是好事)与 DQN 相当较差,对超参数敏感,需 TD3 类修补
典型代表QT-Opt、离线 RL 的 sample-and-rankNAF (Gu et al. 2016)DDPG / TD3 / SAC
核心结论

DDPG 揭示了一个深刻的统一:「Q-learning 里学一个近似最大化器」和「actor-critic 里学一个策略」是同一件事。把目标里的 $\max_{a'}$ 写成 $\E_{a'\sim\pi}[\cdot]$,把 actor 的更新写成路径导数,你会发现两个算法族只是在同一个设计空间的不同角落。这也解释了为什么下一讲的 off-policy 策略梯度会和本讲的技巧大量重叠。

9. 实践清单:Levine 的调参建议逐条展开

这两页是整门课里「工程含量」最高的内容,部分借自 John Schulman。下面把每一条原话摘出来,再补上「为什么」。

Q-learning 的基础实践建议
四条基础建议:先在简单可靠的任务上测试实现正确性;大 replay buffer 有助于稳定;要有耐心,很长一段时间里可能不比随机策略好;探索的 $\epsilon$ 从高到低逐渐降低。中间的四张曲线来自 Prioritized Experience Replay 论文的图 7:Pong 上所有方法几秒钟就贴到天花板(说明它区分不出算法好坏),Breakout 上各方法有明显但收敛的差异,而 Video Pinball 和 Venture 上曲线剧烈抖动、彼此交错——同一个算法不同种子的差别可能比不同算法的差别还大。

9.1 先在简单、可靠的任务上验证实现

这条排在第一位不是偶然。Q-learning 的失败模式有一个恶性特征:实现 bug 和超参数不好的表现完全一样——都是「学不出来」。如果你直接上 Atari,一次实验跑几小时,你根本无法区分是 detach 忘了写,还是学习率大了 3 倍。

推荐的验证阶梯:

  1. 小型表格 MDP:手写一个 5 状态的确定性链,用你的 DQN 代码跑,把学出的 $Q$ 和用值迭代精确算出的 $Q^\star$ 直接比数值。这一步能抓出 90% 的 bug。
  2. CartPole / MountainCar:几分钟内应该稳定解决。CartPole 解不了,一定是实现问题,不是调参问题。
  3. Pong:Atari 里最容易的,任何正确实现都能打到 +20 左右。但也正因为太容易,它区分不出算法优劣——上图里所有方法在 Pong 上重合成一条线。Pong 只能证明「没坏」,不能证明「好」。
  4. Breakout / Seaquest 等:真正开始有区分度的地方。

9.2 大 replay buffer 有助于稳定

幻灯片的注解是「looks more like fitted Q-iteration」。回到第 5 节的三进程视角:buffer 越大,数据越多样、越接近一个固定的静态数据集,训练就越像监督学习的批量回归——而那是我们唯一有理论保证的部分。反过来,小 buffer 意味着数据分布随策略快速漂移,非平稳性加剧。Atari 的标准是 $10^6$ 个转移(约 7GB,通常用 uint8 存帧并共享相邻帧来压缩)。

9.3 要有耐心

「it takes time, be patient — might be no better than random for a while」。这是 Q-learning 与策略梯度的重要体感差异:策略梯度通常从第一批数据起就缓慢但单调地改善;Q-learning 则常常在很长时间里回报毫无起色,然后突然开始上涨。原因在第 6 节讲过——值需要一格一格往回传播,在传到起点之前,贪心策略与随机策略没有区别。所以不要在前 10% 的训练步就判死刑,也不要用「跑 1 万步看谁高」来做超参数搜索。

9.4 探索从高到低退火

训练初期 $Q$ 完全是噪声,此时的 $\argmax_a Q$ 毫无意义,贪心行动等于随机行动却少了随机的均匀覆盖,所以初期就该用大 $\epsilon$(甚至 $\epsilon=1$,纯随机)把状态空间铺开。到后期 $Q$ 已经可靠,$\epsilon$ 大反而在浪费交互、并且让 replay buffer 里塞满低质量数据。标准配置:$\epsilon$ 在前 100 万帧从 1.0 线性降到 0.1,之后(有些实现)再缓降到 0.01。

Q-learning 的进阶实践建议与 Huber loss
五条进阶建议,配一张 Huber 损失的图:蓝色抛物线是平方损失,绿色是 Huber——在 $|x|\le\delta$ 内与平方损失重合,之外变成斜率恒定的直线,因此梯度被限制在 $\pm\delta$。公式写在左边:$L(x)=x^2/2$ 若 $|x|\le\delta$,否则 $\delta|x|-\delta^2/2$。

9.5 Bellman 误差的梯度可能很大:梯度裁剪或 Huber 损失

为什么 Bellman 误差会特别大?三个叠加原因:(a) 奖励尺度不可控,有的游戏一次得 1 分,有的一次得 10000 分;(b) 折扣回报的量级是 $\frac{r_{\max}}{1-\gamma}$,$\gamma=0.99$ 时把奖励放大 100 倍;(c) 目标本身在跳变(同步时刻)。于是偶尔出现残差 $|Q-y|=100$ 的样本,平方损失给它的梯度权重是 100 倍于普通样本,一个 batch 的更新被这一个离群点主宰,参数被一脚踹飞。

$$ L_\delta(x)=\begin{cases}\tfrac12 x^2 & |x|\le\delta\\[2pt] \delta|x|-\tfrac12\delta^2 & \text{otherwise}\end{cases} \qquad \frac{dL_\delta}{dx}=\begin{cases}x & |x|\le\delta\\ \delta\,\mathrm{sign}(x)&\text{otherwise}\end{cases} $$

Huber 损失(PyTorch 里是 F.smooth_l1_loss,$\delta=1$)在小误差处保留平方损失的良好曲率,在大误差处把梯度封顶为常数 $\delta$。注意它和 $L_1$ 损失的区别:纯 $L_1$ 在 0 附近梯度不连续、不衰减,收敛到最优解附近会来回震荡;Huber 在两个区域各取所长。另一个等效手段是 clip_grad_norm_,两者常常一起用。Atari 上还有第三个常见做法:reward clipping,把所有奖励裁到 $[-1,1]$——代价是丢失了「这个奖励比那个大得多」的信息。

9.6 Double Q-learning:大有帮助,简单且无副作用

Levine 的原话是「helps a lot in practice, simple and no downsides」。这在整门课里是罕见的无保留推荐——因为它不增加超参数、几乎不增加计算、不引入新的失败模式,而收益(第 7 节那几张图)非常明显。写 DQN 就应该默认打开它。

9.7 N-step return:帮助很大,但有代价

对比措辞可以看出 Levine 的评价梯度:double Q 是「no downsides」,N-step 是「have some downsides」。代价就是第 6.4 节分析的 off-policy 偏差与方差上升。实践建议:从 $n=3$ 开始试,配合较新的数据(不要用太旧的 buffer 段),$\epsilon$ 较大时保守一些。

9.8 学习率与优化器

「schedule exploration (high to low) and learning rates (high to low), Adam optimizer can help too」。

  • 为什么学习率也要退火?训练早期目标粗糙,大步走能快速把 $Q$ 拉到正确量级;后期需要精细区分动作之间的微小差异($Q$ 值差 0.01 就决定选哪个动作),大学习率会让这些差异被噪声淹没。
  • 为什么 Adam 有帮助?Q 网络不同输出维度的梯度尺度差异巨大——常访问的状态梯度密集,罕见状态梯度稀疏;不同游戏的奖励量级差好几个数量级。Adam 的按参数自适应缩放天然消化了这种尺度差异,让同一组超参数能跨任务复用。典型值:$\alpha=10^{-4}$(Atari)到 $3\times10^{-4}$(连续控制),并把 $\epsilon_{\text{Adam}}$ 调大到 $10^{-4}\sim10^{-8}$ 之间试。

9.9 跑多个随机种子

「run multiple random seeds, it's very inconsistent between runs」。这条被低估得最厉害。上面那张 Prioritized Replay 的图已经说明问题:在 Venture 这样的游戏上,同一算法不同种子的曲线可以差出好几倍。用单个种子的结果比较两个算法是没有意义的,尤其当你在调自己的实现时,很容易把「这个种子运气好」误当成「我的改动有效」。至少 3~5 个种子,报告中位数或四分位区间(IQM),不要只报最好的那条曲线。

注意

还有几条 Levine 没写在幻灯片上但同样重要的:(1) 监控 $Q$ 值的绝对量级——如果 $Q$ 一路涨到 $10^6$,说明发散了,而回报曲线可能要过很久才反映出来;(2) 监控 TD 误差的分布而不只是均值;(3) 定期在 $\epsilon=0.01$ 下做纯评估 rollout,训练时的回报因为带探索噪声而系统性偏低;(4) 检查 buffer 里 done 标志的比例,如果几乎全为 0 或全为 1,多半是环境包装出了问题。

10. 一点理论:为什么这些技巧只是「让问题没那么糟」

本讲最后回到上一讲的负面结果,把它讲透,并推出一个更让人不安的推论。

10.1 表格情形:$\mathcal{B}$ 是压缩,所以值迭代必然收敛

定义 Bellman 最优算子 $\mathcal{B}$:$\mathcal{B}V=\max_a\big(r_a+\gamma\mathcal{T}_a V\big)$,其中 $r_a$ 是所有状态在动作 $a$ 下的奖励堆成的向量,$\mathcal{T}_a$ 是转移矩阵,$\mathcal{T}_{a,i,j}=p(s'=i|s=j,a)$。$V^\star$ 是 $\mathcal{B}$ 的不动点:$V^\star=\mathcal{B}V^\star$,它总存在、总唯一、总对应最优策略。

值迭代收敛的理由是 $\mathcal{B}$ 在 $\infty$-范数下是模 $\gamma$ 的压缩:

$$ \|\mathcal{B}V-\mathcal{B}\bar V\|_\infty\le\gamma\|V-\bar V\|_\infty $$

取 $\bar V=V^\star$,由于 $\mathcal{B}V^\star=V^\star$,得 $\|\mathcal{B}V-V^\star\|_\infty\le\gamma\|V-V^\star\|_\infty$:每迭代一次,与最优值的最大偏差至少缩小到 $\gamma$ 倍。$\gamma=0.9$ 时 100 次迭代后误差衰减到 $0.9^{100}\approx2.7\times10^{-5}$。

10.2 换成神经网络:多了一个投影算子 $\Pi$

用神经网络表示 $V$ 时,我们无法真的把 $\mathcal{B}V$ 存下来——只能在网络能表示的函数集合 $\Omega$ 里找一个最接近它的。这就是投影算子:

$$ \Pi V=\argmin_{V'\in\Omega}\tfrac12\sum_s\|V'(s)-V(s)\|^2 $$

于是拟合值迭代做的事情不是 $V\leftarrow\mathcal{B}V$,而是 $V\leftarrow\Pi\mathcal{B}V$。

ΠB 不是任何范数下的压缩
核心的三行:$\mathcal{B}$ 是 $\infty$-范数(max norm)下的压缩;$\Pi$ 是 $\ell_2$-范数(欧氏距离)下的压缩(投影只会让点更近,不会更远);但 $\Pi\mathcal{B}$ 不是任何意义下的压缩。左下两张图对比:纯值迭代(左)沿着蓝点稳步逼近黄星;拟合值迭代(右)先被 $\mathcal{B}$ 拉向 $\mathcal{B}V$,再被 $\Pi$ 拍回蓝色直线(网络能表示的函数集合),结果 $V'$ 反而可能比 $V$ 离黄星更远。右边写着结论:值迭代收敛(表格情形);拟合值迭代不收敛——一般不收敛,实践中也经常不收敛。
推导:为什么两个压缩复合起来不是压缩

压缩性是相对某一个特定范数而言的。$\mathcal{B}$ 保证 $\|\mathcal{B}V-\mathcal{B}\bar V\|_\infty\le\gamma\|V-\bar V\|_\infty$,$\Pi$ 保证 $\|\Pi V-\Pi\bar V\|_2\le\|V-\bar V\|_2$。要把它们串起来,需要在中间做范数转换,而 $\|x\|_\infty\le\|x\|_2\le\sqrt{n}\,\|x\|_\infty$ 中的 $\sqrt{n}$ 会把 $\gamma<1$ 的收益吃掉——$n$ 是状态数,$\sqrt{n}\gamma\gg1$。

更直观地看图:$\mathcal{B}$ 在 $\infty$-范数下把点拉近,$\Pi$ 在 $\ell_2$-范数下把点投影。$\ell_2$ 投影会最小化平均误差,它完全可能为了让大多数状态的误差变小,而把某一个状态的误差放大很多——而 $\infty$-范数只看最大的那个。于是 $\Pi$ 可以把 $\mathcal{B}$ 辛苦缩小的 $\infty$-范数误差重新放大。反复迭代,误差可以无界增长。

10.3 对 Q 函数完全一样,对在线 Q-learning 也一样

把 $V$ 换成 $Q$,算子写成 $\mathcal{B}Q=r+\gamma\mathcal{T}\max_a Q$(注意 $\max$ 现在在转移算子之后),投影算子 $\Pi Q=\argmin_{Q'\in\Omega}\frac12\sum\|Q'(s,a)-Q(s,a)\|^2$,拟合 Q 迭代就是 $Q\leftarrow\Pi\mathcal{B}Q$。结论逐字相同:$\mathcal{B}$ 是 $\infty$-范数压缩,$\Pi$ 是 $\ell_2$ 压缩,$\Pi\mathcal{B}$ 什么也不是。

Levine 特意用红字补了一句:这同样适用于在线 Q-learning。很多人有一个误解,觉得「在线 Q-learning 每步只做一个梯度步,不是真的在做投影,所以不受这个结论约束」。恰恰相反——一个梯度步是不完整的投影,它连 $\ell_2$ 压缩这一半保证都没有。所以 DQN 加了那么多技巧,也仍然只是把发散的概率降低。

10.4 一个悲伤的推论:actor-critic 的 critic 也不收敛

batch actor-critic 的 critic 同样不收敛
左边是第 6 讲的 batch actor-critic 算法框。右边把它的第 2 步拆开:目标 $y_{i,t}\approx r(s_{i,t},a_{i,t})+\gamma\hat V^\pi_\phi(s_{i,t+1})$ 对应一个 $\ell_\infty$ 压缩的算子 $\mathcal{B}$(但没有 max,因为是策略评估而非最优),损失 $\mathcal{L}(\phi)=\frac12\sum_i\|\hat V^\pi_\phi(s_i)-y_i\|^2$ 对应 $\ell_2$ 压缩的 $\Pi$。结论:拟合的自举策略评估同样不收敛! 左下蓝框澄清术语:$V^\pi$ 是策略 $\pi$ 的值函数,这是 critic 在做的事;$V^\star$ 是最优策略的值函数,这是值迭代在做的事。

这一页的杀伤力在于:它说明第 6 讲的 actor-critic 并没有因为「有个 actor」就逃过一劫。critic 的更新同样是「$\ell_\infty$ 压缩的 Bellman 算子」+「$\ell_2$ 压缩的回归投影」的复合,同样不收敛。唯一的差别是策略评估算子 $\mathcal{B}^\pi V=r^\pi+\gamma\mathcal{T}^\pi V$ 里没有 $\max$,因此没有第 7 节的过估计问题,但不收敛这一条依然成立。

顺带把术语钉死:$V^\pi$ 是「按策略 $\pi$ 行动能拿多少」,$V^\star$ 是「按最优策略行动能拿多少」。critic 学的是前者,值迭代/Q-learning 学的是后者。两者的 Bellman 算子分别是 $\mathcal{B}^\pi$(线性,无 max)和 $\mathcal{B}$(非线性,有 max)。这个区分在后面的课里会反复用到。

10.5 这到底意味着什么

本讲理论部分的总结要点
结语页的三层结论:拟合值方法(Q-learning、Q 函数 actor-critic)常常有点不稳定——实践中表现为值函数可能不收敛,也解释了为什么它们需要更多超参数调优;有一些技巧能让问题「没那么糟」——目标网络真的有用,回放池真的有用;这个方向仍有大量空间去设计更稳定的算法、更好地理解现有算法。
  • 「不保证收敛」不等于「没法用」。它意味着你不能像训练监督模型那样期待 loss 单调下降到某个最优;你需要监控、需要多种子、需要早停策略。
  • 这解释了 Q-learning 为什么比策略梯度更「玄学」。策略梯度虽然方差大,但它在优化一个明确的目标 $J(\theta)$,梯度是真梯度;Q-learning 用的是半梯度,且复合算子不压缩,所以它对超参数的敏感度是结构性的,不是调参水平问题。
  • 本讲的每个技巧都在做同一件事:让实际迭代更接近那个有保证的表格情形。大 buffer → 数据分布更静态;target network → 目标固定,那一段真的在做有保证的回归;Double Q → 削掉 $\max$ 引入的系统偏差;Huber/梯度裁剪 → 防止单个离群点把参数踹出可行区。它们是一整套「把 $\Pi\mathcal{B}$ 的破坏力压到最小」的工程学。
  • 这仍是开放问题。后续的课(离线 RL、稳定性分析)会继续挖,而层归一化、网络周期性重置、值函数分类化(HL-Gauss、C51 那一类把回归换成分类的做法)都是近年被证明有效的新工具。

本讲小结

一页速查

问题症状解决手段代价 / 新超参数
样本高度相关网络被局部轨迹拖着走,忘记别处学过的replay buffer内存;buffer 大小
目标随参数一起动正反馈回路,$Q$ 值发散target network(硬更新 $N$ / 软更新 $\tau$)信息滞后;$N$ 或 $\tau$
不是真梯度下降无目标函数,无收敛保证无法根治,只能靠上面两条缓解—
$\max$ 造成系统性高估预测 $Q$ 比真实回报高数倍Double DQN;clipped double-Q(min of 2)几乎为零;min 版会低估
值传播太慢长时间毫无进展,稀疏奖励学不出来N-step return($n=3\sim5$)off-policy 偏差、方差上升;$n$
连续动作没法取 max目标计算不可行采样/CEM · NAF · DDPG精度 / 表达力 / 复杂度 三选一
Bellman 误差量级失控梯度爆炸,参数被离群点踹飞Huber loss、梯度裁剪、reward clipping$\delta$;奖励信息损失
结果不可复现不同种子差好几倍多种子 + 报告中位数/IQM计算量

要点清单

  • Q-learning 的更新是 semi-gradient:人为切断目标对参数的依赖。写代码必须 detach()。
  • replay buffer 修输入分布,target network 修监督信号——两个正交的病,缺一不可。
  • DQN = 一般化 fitted Q-iteration 在 $N=1,K=1$ 时的特例。三个进程(收数据 / 更目标 / 回归)的速度比决定了你在算法谱系里的位置;$K$ 就是 UTD 比率。
  • 硬更新的滞后在 $0\sim N$ 之间锯齿跳变;Polyak 软更新 $\bar\theta\leftarrow\tau\bar\theta+(1-\tau)\theta$ 给出恒定的等效滞后 $\tau/(1-\tau)$,且自带参数平均的正则效果。
  • $\E[\max_a Q]\ge\max_a\E[Q]$:只要 $Q$ 有噪声就必然高估,偏差被 bootstrap 放大 $\frac{1}{1-\gamma}$ 倍。Double DQN 只改一个下标:$\argmax$ 用 $Q_\theta$,取值用 $Q_{\bar\theta}$。
  • N-step return 用「更多真实奖励、更少 bootstrap」换取更低偏差和更快传播,但严格来说只有 $n=1$ 才 off-policy 正确。三种修正:无视(最常用)、动态截断、(裁剪的)重要性采样。
  • 连续动作三条路:随机优化(简单、并行、略微低估反而安全)、NAF(解析最优但只能单峰)、DDPG(通用但要多训一个 actor)。DDPG 表明 Q-learning 与 actor-critic 是同一件事的两种写法。
  • 调参口诀:先小任务验证正确性 → 大 buffer → 有耐心 → $\epsilon$ 与学习率都从高到低 → Huber + 梯度裁剪 → 默认开 Double Q → 多随机种子。
  • 理论上 $\Pi\mathcal{B}$ 不是任何范数下的压缩,所以拟合值方法不保证收敛;这一结论对拟合 Q 迭代、在线 Q-learning、以及 actor-critic 的 critic 全都成立。所有技巧只是让问题「没那么糟」。

延伸阅读

DQN 与它的直系改进

N-step 与 off-policy 修正

连续动作

不收敛问题与近年的稳定化工具