Q-Learning 的工程实践:让不收敛的算法真的跑起来
上一讲证明了带函数逼近的 Q-learning 没有收敛保证。本讲不去修复理论,而是把它变成一个在 Atari 上真能打赢人类的算法:replay buffer、target network、Double Q、N-step return、连续动作下的 max,以及一份可以照抄的调参清单。
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 长这样:
- 用某个策略 $\pi_\theta(a|s)$ 在环境里走一步,得到 $(s_i,a_i,s_i')$,存进 buffer $\mathcal{R}$;
- 从 $\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')$;
- 用 $\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} $$
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) $$
注意这个式子里 $\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 迭代)并排放在一起。
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:
- 用 $\pi_\theta$ 走一步,得到 $(s_i,a_i,s_i')$,存入 $\mathcal{R}$;
- 算目标 $y_i=r(s_i,a_i)+\gamma\max_{a'}\,Q_{\bar\theta}(s_i',a')$; ← 用目标网络
- 用 $\nabla_\theta\sum_{i=1}^{B}\|Q_\theta(s_i,a_i)-y_i\|^2$ 更新 $\theta$;
- 每 $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 只是它的一个特例。
3.1 一般化的 Q-learning 算法框
- 保存目标网络参数:$\bar\theta\leftarrow\theta$;
- 用某个策略收集数据 $\{(s_i,a_i,s_i')\}$,加入 $\mathcal{R}$; (重复 $N$ 次)
- 从 $\mathcal{R}$ 采一个 batch $(s_i,a_i,s_i')$; (重复 $K$ 次)
- $\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 的算法:
- 执行动作 $a_i$,观测到 $(s_i,a_i,s_i')$,加入 $\mathcal{R}$;
- 从 $\mathcal{R}$ 均匀采一个 mini-batch $\{s_i,a_i,s_i'\}$;
- 用目标网络 $Q_{\bar\theta}$ 计算 $y_i=r(s_i,a_i)+\gamma\max_{a_i'}Q_{\bar\theta}(s_i',a_i')$;
- $\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)$;
- 更新 $\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$ 步复制一次」看起来无伤大雅,但它其实制造了一个相当别扭的现象。
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,本质上是同一个系统——它由三个并行运行的进程组成,区别只在于让谁跑得快、谁跑得慢。
5.1 三个进程
- 进程 1:数据收集。执行行为策略,产出转移,写入 buffer,并按 FIFO 淘汰旧数据。它的速度就是「环境交互步数 / 秒」。
- 进程 2:目标网络更新。把 $\theta$ 同步到 $\bar\theta$。速度由 $N$(或软更新的 $\tau$)决定。
- 进程 3:Q 函数回归。从 buffer 采样、算 loss、做梯度步。速度就是「梯度步 / 秒」。
| 算法 | 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 比率(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 与它的代价
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 先看证据
「$Q$ 涨、回报也涨」听上去挺好。但如果去问一个更苛刻的问题——预测的 $Q$ 值在数值上准不准——答案就难看了。
7.2 病根:对噪声取最大值必然偏高
设 $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 用两套参数 $\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,只改一个下标
| 选动作用 | 估值用 | 目标公式 | |
|---|---|---|---|
| 标准 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
这一页是通往后面 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$ 就成了本身就很难的非凸优化问题。
8.1 路线一:把它当成优化问题解
最直接的想法是对 $a$ 做梯度上升:$a\leftarrow a+\eta\nabla_a Q_\theta(s,a)$。可行,但在内循环里跑一个迭代优化器代价太高,而且 $Q$ 关于 $a$ 非凸,容易陷进局部极大。Levine 的观察是:动作空间通常维度很低(几维到几十维,远小于状态维度),既然维度低,随机优化就变得很有竞争力。
朴素采样。从均匀分布(或当前 actor)采 $N$ 个候选动作,把它们和状态一起打包成一个 batch 送进网络,取最大的那个 $Q$。这在 GPU 上是一次矩阵乘法,几乎不增加墙钟时间。$N$ 通常取 10~100。
为什么「不准」可以忍?因为目标值本来就是个粗糙的估计——$Q_{\bar\theta}$ 自己就有误差,目标网络还滞后着,采样带来的那点低估相比之下不算什么。而且第 7 节刚说过,低估比高估安全:采样近似必然给出 $\le$ 真实 $\max$ 的值,恰好抵消了一部分过估计偏差。这是一个「两个错误互相抵消」的美妙巧合。
CEM(cross-entropy method,交叉熵方法)。朴素采样的迭代版本,四步循环:
- 从高斯 $\mathcal{N}(\mu,\Sigma)$ 采 $N$ 个动作(初始 $\mu$ 为动作空间中心,$\Sigma$ 很大);
- 算出 $N$ 个 $Q$ 值,取最高的 $M$ 个(elite,比如 top 10%);
- 用这 $M$ 个 elite 重新拟合 $\mu,\Sigma$;
- 回到第 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$ 的凹函数,最大值点和最大值都直接读出来:
算法结构和 DQN 一模一样,没有第二个网络、没有新的超参数、没有额外的优化循环。代价是表达力被严重限制:真实的 $Q(s,\cdot)$ 常常是多峰的(比如绕过障碍物可以从左也可以从右),而二次型只能有一个峰,网络被迫在两个峰之间取一个折中的、可能很糟的动作。
8.3 路线三:学一个近似最大化器(DDPG)
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 完整算法:
- 执行动作 $a_i$($\mu_\theta(s)$ 加探索噪声),观测 $(s_i,a_i,s_i')$,存入 $\mathcal{R}$;
- 从 $\mathcal{R}$ 均匀采 mini-batch $\{s_j,a_j,s_j'\}$;
- 用目标网络算 $y_j=r(s_j,a_j)+\gamma\,Q_{\bar\phi}\big(s_j',\mu_{\bar\theta}(s_j')\big)$;
- 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)$;
- 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)$;
- 用 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-rank | NAF (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。下面把每一条原话摘出来,再补上「为什么」。
9.1 先在简单、可靠的任务上验证实现
这条排在第一位不是偶然。Q-learning 的失败模式有一个恶性特征:实现 bug 和超参数不好的表现完全一样——都是「学不出来」。如果你直接上 Atari,一次实验跑几小时,你根本无法区分是 detach 忘了写,还是学习率大了 3 倍。
推荐的验证阶梯:
- 小型表格 MDP:手写一个 5 状态的确定性链,用你的 DQN 代码跑,把学出的 $Q$ 和用值迭代精确算出的 $Q^\star$ 直接比数值。这一步能抓出 90% 的 bug。
- CartPole / MountainCar:几分钟内应该稳定解决。CartPole 解不了,一定是实现问题,不是调参问题。
- Pong:Atari 里最容易的,任何正确实现都能打到 +20 左右。但也正因为太容易,它区分不出算法优劣——上图里所有方法在 Pong 上重合成一条线。Pong 只能证明「没坏」,不能证明「好」。
- 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。
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$。
压缩性是相对某一个特定范数而言的。$\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 也不收敛
这一页的杀伤力在于:它说明第 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 这到底意味着什么
- 「不保证收敛」不等于「没法用」。它意味着你不能像训练监督模型那样期待 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 与它的直系改进
- Playing Atari with Deep Reinforcement Learning (Mnih et al., 2013) — 本讲第 3 节那个算法框的原始出处,replay buffer + target network 的组合首次在高维像素输入上跑通。
- Human-level control through deep reinforcement learning (Mnih et al., 2015, Nature) — 上文的期刊版,补充了 49 个 Atari 游戏的完整评测和全部超参数表,是复现 DQN 时最该查的一份附录。
- Deep Reinforcement Learning with Double Q-learning (van Hasselt et al., 2015) — 第 7 节那张「估计值远高于真值」的图就来自这里;同时给出「只改一个下标」的实践版本。
- Prioritized Experience Replay (Schaul et al., 2015) — 不再均匀采样 buffer,而是按 TD 误差大小加权,本讲第 9 节那四张抖动的曲线出自其图 7。
- Dueling Network Architectures for Deep RL (Wang et al., 2015) — 把 $Q(s,a)$ 分解成 $V(s)+A(s,a)$ 两个头,在动作影响不大的状态上学得更快。
- Rainbow: Combining Improvements in Deep RL (Hessel et al., 2017) — 把 double、prioritized、dueling、N-step、分布式值函数、NoisyNet 六项一起打包并做消融,是判断「哪个技巧真的有用」的最佳单篇参考。
N-step 与 off-policy 修正
- Safe and Efficient Off-Policy Reinforcement Learning (Munos et al., 2016) — Retrace$(\lambda)$,用裁剪的重要性比值把 N-step 目标修正成对任意 off-policy 数据都安全的形式。
- IMPALA (Espeholt et al., 2018) — V-trace 修正,大规模分布式训练下处理「行为策略落后于学习策略」的标准做法。
- Distributed Prioritized Experience Replay / Ape-X (Horgan et al., 2018) — 把第 5 节的三进程真正拆成分布式的多个 actor + 一个 learner,是 UTD 视角的极好例证。
连续动作
- Continuous Control with Deep Reinforcement Learning / DDPG (Lillicrap et al., 2015) — 第 8.3 节的原始论文,「学一个近似最大化器」路线的代表。
- Continuous Deep Q-Learning with Model-based Acceleration / NAF (Gu et al., 2016) — 第 8.2 节的二次型结构,用表达力换取解析最优。
- Addressing Function Approximation Error in Actor-Critic Methods / TD3 (Fujimoto et al., 2018) — clipped double-Q、目标策略平滑、延迟 actor 更新,本讲第 7.5 节和 8.3 节的直接延续。
- Soft Actor-Critic (Haarnoja et al., 2018) — 把 $\max_a$ 换成带熵正则的软最大,连续控制上最稳的基线之一。
- QT-Opt (Kalashnikov et al., 2018) — 用 CEM 在真实机器人抓取上做连续动作 Q-learning,第 8.1 节路线的大规模实证。
不收敛问题与近年的稳定化工具
- Residual Algorithms: Reinforcement Learning with Function Approximation (Baird, 1995) — 提出残差梯度,也给出了那个著名的「星形」发散反例。
- Diagnosing Bottlenecks in Deep Q-learning Algorithms (Fu et al., 2018) — 用可精确求解的环境把「函数逼近误差 / 采样误差 / 分布偏移」三个因素分离开做实证,Levine 组的工作。
- Off-Policy Deep RL without Exploration (Fujimoto et al., 2018) — 指出 off-policy 值函数在分布外动作上的外插误差,是理解「UTD 过高会过拟合」的入口。
- What Matters In On-Policy Reinforcement Learning? (Andrychowicz et al., 2020) — 虽然做的是 on-policy 方法,但它用上万次实验说明的道理对 Q-learning 同样成立:第 9 节那些「小细节」的影响常常大于算法本身的差别。