卷 IV · 换脑CH 16深度 16/24

两个补丁,救活一个算法

上一章说经验回放和目标网络是补丁。补丁总得有个伤口。这一章把那个伤口摆出来——它有名字,叫致命三角;而它的判例简单到只有 7 个状态、8 个权重,却能把一个奖励全为 0 的问题炸成 10⁶。

致命三角Baird 反例为什么表格法不需要补丁

先说一件我本来以为会成立的事

写这一章的时候,我本来打算做一个「表格版消融实验」:在第 13 章那个悬崖世界上跑 Q-learning,把经验回放和目标网络分别打开关闭,看差别。

引擎里那两个开关我都实现了,八个种子跑了一遍。结果是这样:

                末 100 局均值    8 次里学到最优路径(13 步)的
都不开              -48.6              8 次
只开经验回放         -50.3              8 次
只开目标网络         -49.0              8 次
两个都开            -51.9              8 次

几乎没有区别。四种配置全都稳稳学到最优路径,平均回报差在噪声范围内。

这个「失败的实验」本身就是答案:

◆ 那两个补丁治的不是自举的病,是函数逼近的病

表格法里,每个状态各存各的。你更新 Q[s][a],其它格子一个字节都不会动

所以就算目标在飘、样本高度相关,坏影响也被关在一个格子里,传不出去。

而函数逼近的定义就是参数共享——你更新一个状态,整个函数都会跟着动一点。

补丁是给共享打的,不是给自举打的。

那把伤口摆出来

Sutton 给这个伤口起了个很吓人的名字:致命三角(the deadly triad)。它说的是三样东西凑齐了就可能发散:

成分是什么为什么我们非要它不可
自举用估计更新估计(第 11 章)不然只能等一局结束,样本效率极低
离策略走的和学的不是同一个策略(第 12 章)不然没法用经验回放,也没法从旧数据学
函数逼近参数共享(第 14 章)不然状态多一点就装不下

看第三列。三样我们都想要,而且每一样都有非要不可的理由。这就是为什么这个三角这么烦人——你不能靠「别用它」来躲开。

少任何一样都安全:

  • 去掉函数逼近 → 表格法,收敛有保证(上面那个实验)
  • 去掉离策略 → on-policy 方法(PPO 这一系),实践中稳得多
  • 去掉自举 → 蒙特卡洛,无偏但方差大

判例:Baird 反例

Leemon Baird 1995 年构造的一个例子,只有 7 个状态、8 个权重。它的设计目的就一个:证明这三样凑齐真的会炸,不是理论上的担心。

这个例子有个特别恶劣的地方:

所有转移的奖励都是 0。
所有状态的真实价值都是 0。
一个把所有权重设成 0 的解,就是完美解。

一个笨到只会输出 0 的算法都能做对。

▶ 动手 · 先看「裸的半梯度 TD」,注意纵轴是对数刻度

然后挨个切换后面三个配置。对数坐标上的直线,意味着指数爆炸。

它炸得有多干脆

跑 5000 步,权重的最大绝对值:

10 → 79 → 310 → 1200 → 4900 → 1.6e4 → 5.6e4 → 1.4e5 → 4.8e5 → 1.3e6 → 4.5e6

最终 5.68×10⁶,而且完全没有停下来的意思。

真值是 0。

而三个补救措施的效果:

配置5000 步后权重最大值
裸的半梯度 TD5.68×10⁶★ 发散
学习率降到 1/1081.0没炸(但也没收敛到 0)
加目标网络(冻 200 步)6.16收住了
加目标网络(冻 1000 步)5.88收住了

一个补丁,从 10⁶ 降到 10⁰。这就是目标网络存在的全部理由,摆在这儿了。

为什么会炸

机制不复杂,值得讲清楚。

Baird 的例子里,状态 1–6 的特征是 2·e_i + e_8,状态 7 是 e_7 + 2·e_8注意所有状态都用到了第 8 个权重——这就是「参数共享」的具体形态。

然后发生的事是这样:

  1. V(s') 的时候用到了共享的 w₈。
  2. 为了让 V(s) 逼近 γV(s'),梯度推高了 w₈。
  3. 但 w₈ 一高,V(s') 也跟着高了——它俩用的是同一个权重。
  4. 于是目标又变远了,梯度继续推……

这是一个正反馈回路。而离策略那个重要性采样比(这里在实线转移上是 7 倍)给这个回路加了个放大器:更新幅度被放大七倍,回路的增益超过 1,指数爆炸就发生了。

◆ 「半梯度」这个词值得较真

你可能注意到我一直说「半梯度 TD」,不是「梯度 TD」。这个「半」字是问题的核心。

损失是 [r + γV(s';θ) − V(s;θ)]²θ 在两处都出现了。但标准做法在求梯度时假装目标那一项是常数——只对 V(s;θ) 求导,不对 γV(s';θ) 求导。

所以它不是任何一个函数的真正梯度。它是一个「看起来像梯度」的更新方向,而梯度下降的收敛保证对它不适用

PyTorch 里那句 with torch.no_grad(): 包住 target 的计算,做的就是这件事——它是这个「半」字的实现。

那为什么不算全梯度?因为算了效果反而更差(叫 residual gradient,收敛但慢得没法用)。这是一个真实存在的、大家都知道但都接受了的理论缺口。

那两个补丁各治什么

把这一章和上一章串起来,两个补丁的分工其实很清楚:

补丁治什么机制
目标网络 正反馈回路 把回路剪断:目标用的是冻住的 θ⁻,推高 θ 不会立刻推高目标。回路的增益被强行压到 0,直到下次同步
经验回放 样本相关性 打散数据:连续几帧几乎相同,梯度全朝一个方向;随机抽样把这个方向平均掉,避免局部过拟合和灾难性遗忘

注意它们治的是两个不同的病。上面那个 demo 里,目标网络能治住 Baird,因为 Baird 炸的是正反馈;经验回放对 Baird 帮助不大,因为 Baird 的样本本来就是随机的。

⚠ 补丁不是证明,是缓解

说清楚一件事:加了这两个补丁,DQN 依然没有收敛保证。致命三角还在那儿,只是被压住了。

DQN 在实践中会发散,只是概率低了很多。你在训练里可能遇到的具体现象:

  • Q 值单调上涨,涨到几百几千,而这个游戏的实际回报最多几十。这是最典型的征兆,值得专门画一条 Q 值曲线盯着
  • 分数练到一半突然崩到 0,再也回不来。
  • loss 变 NaN。这是最晚期、也最容易发现的阶段。

标准的应急手段(第 24 章那张表里也有):学习率降一个数量级、梯度裁剪、目标网络同步间隔调大、检查有没有漏 (1 - terminated)

而如果你反复搞不定,换 PPO——它是 on-policy 的,三角缺一角,稳得多。这也是为什么今天的默认选择是 PPO 而不是 DQN。

✎ 真正解决它的尝试

补丁之外,也有人正面去修这个理论缺口,值得知道方向:

  • Gradient TD 方法(GTD、TDC):构造一个真正的梯度目标(最小化投影贝尔曼误差),在离策略 + 线性逼近下有收敛保证。代价是要维护第二组权重、实现更复杂,实践中用得不多。
  • Emphatic TD:给不同状态的更新加权,修正离策略带来的分布偏移。
  • 换掉离策略:这才是工业界的实际选择。PPO 走的就是这条路。

值得注意的是:2013 年 DQN 那篇论文没有解决这个理论问题,只是绕过去了,而且绕得非常成功。这在这个领域很典型——工程上的可行常常跑在理论前面很远。

⌗ 换成真机:三行代码看懂这两个补丁
# 目标网络:另存一份,定期同步
target_net = copy.deepcopy(q_net)
for p in target_net.parameters():
    p.requires_grad = False          # 它不参与梯度,只负责报数

# 算目标的时候用它,而且包在 no_grad 里 —— 这就是那个「半」
with torch.no_grad():
    target = r + gamma * target_net(s2).max(1).values * (1 - terminated)

# 每 N 步同步一次
if step % target_update_interval == 0:
    target_net.load_state_dict(q_net.state_dict())

SB3 里对应的参数:

DQN("MlpPolicy", env,
    target_update_interval=1000,   # 目标网络多久同步一次
    buffer_size=100_000,           # 经验回放多大
    max_grad_norm=10,              # 梯度裁剪 —— 第三道保险
    tau=1.0)                       # 1.0=硬拷贝;<1 则是软更新(SAC 那种)

那个 tau 值得一提:软更新 θ⁻ ← τθ + (1−τ)θ⁻(τ 取 0.005 之类)是另一种做法——目标网络每步都动一点点,而不是每一万步跳一大下。DDPG 和 SAC 用的是这个,平滑得多。

↩ 回到那个视频

你在网上看到的「AI 训练失败」的搞笑合集——小人抽搐着飞上天、以物理引擎不允许的姿势穿模、原地高速旋转——其中有相当一部分不是奖励写错了,是训练发散了。

发散之后的 agent 会表现出一种很有辨识度的行为:动作幅度全部顶到极限值。因为价值估计已经变成了天文数字,策略网络被推到饱和区,所有输出都撞在边界上。

怎么和「奖励写错了」区分开?看 Q 值或者 loss 曲线。发散会在数值上留下痕迹(Q 单调上涨、loss 爆炸);奖励写错了不会——那时候一切数值都很正常,只有行为是错的

后一种情况,正是接下来一整卷的主题。

这一章的一句话

自举 + 离策略 + 函数逼近,三样凑齐就可能发散——一个奖励全为 0 的问题能被炸到 10⁶。经验回放和目标网络不是调参技巧,是在给这个三角拆炸弹。

下一章:既然带 max 的自举这么危险,能不能干脆不学价值?直接把策略参数化,对期望回报求梯度。这条路绕开了 max,也就顺带绕开了「动作必须可枚举」这条限制——连续控制的大门从这里打开。