加法问题(20 步序列,要找两个随机位置上的数求和),什么都不学直接输出 0 的误差是 0.3576。 RNN 跑了一百轮还在 0.4241,比基线还差;LSTM 稳步降到 0.0689,好 5.2 倍。
上一节讲清了两类序列网络的结构差别。 这一节回到 8.2.3 那个「梯度消失」的洞,看 LSTM 用什么办法把它补上。
一、加法问题:为什么非要跨 20 步
序列 20 步,第一个通道是随机数,第二个通道只在两个随机位置标 1。 答案 = 那两个位置上的数之和除以 2。 这个任务有个狠的地方:标记位置是随机的,可能在第 1、2 步,也可能在 19、20 步。 网络必须一路记着「哪几个位置被标了、标的是什么数」,直到第 20 步才用得上。 这就是长距离依赖的标准测法。它比「记住第一个数」更难,因为中间还有 18 个随机数在干扰。 我先试了「记住第一个数」的版本,结果普通 RNN 也能学会——因为后面 19 步输入全是 0, 隐状态会收敛到由第一个输入决定的不动点,信息没被冲掉。换成加法问题才把 RNN 逼到墙角。
二、RNN 那一列:一百轮没动一步
第 1 轮 0.3808,第 40 轮 0.3720,第 100 轮 0.4241。 整整一百轮,它在 0.38 到 0.44 之间来回晃。 基线(什么都不学、恒输出 0)是 0.3576。RNN 比基线还差,说明它不仅没学会,还在做负功。 原因就是 8.2.3 算过的衰减:答案要对第 1 到 20 步上的两个位置都敏感, 梯度得从第 20 步一路乘回第 1 步。二十次小于 1 的乘积之后,只剩渣。 这里用的是 Adam 优化器,它对微小梯度也做了归一化放大。 即便这样还是学不动——说明这个洞不是优化器能填的,得改结构。
三、LSTM 那一列:靠加法通道
LSTM 多了一个细胞状态 c,更新式是:c 新值 = 遗忘门乘 c 旧值 + 输入门乘候选值。 关键是这里是加法,不是乘法。 遗忘门学到接近 1 时,c 基本原样往下带,梯度沿着这条通道回传时不会被反复压缩。 三个门各管一件事:遗忘门决定「旧记忆留多少」,输入门决定「这一刻的数要不要写进去」, 输出门决定「这一刻要不要把记忆读出来」。 对应到加法问题:标记位是 1 的时候把数写进去,是 0 的时候不写——这就是输入门在学的事。 实测从 0.3660 一路降到 0.0689,比基线好 5.2 倍。 第 40 轮有个小回升(0.1146),之后继续降,属于正常抖动。
四、这个 Demo 的边界在哪
只跑一百轮、隐层 16 维、样本 200 条。两个模型都远没到极限。 看的是趋势不是绝对数值。 真实任务上数据量大得多、训练久得多,LSTM 的优势没有 5 倍这么悬殊——但它能学会、RNN 学不会, 这个定性结论是稳的。 我手写的 BPTT 里,LSTM 的遗忘门偏置初始化成 2.0(对应 sigmoid 约 0.88),这是常见做法, 目的是让训练初期就倾向「记住」而不是「忘掉」。如果初始化成 0,前期记忆衰减太快,收敛会慢很多。 真实的 LSTM 实现还有 peephole 连接、层归一化等等变体,这里都没加。 核心机制就是那条加法通道和三个门,其余都是工程优化。
五、动手 Demo:加法问题:RNN 一百轮不动,LSTM 降到基线的五分之一
造 200 条 20 步的序列:第 1 通道是随机数,第 2 通道只在两个随机位置标 1, 答案是那两处数值之和除以 2;训练 150 条、测试 50 条。 分别用单层 RNN 和单层 LSTM 各跑 100 轮,手写 BPTT 加 Adam, 每 10 到 20 轮记录一次测试集平均绝对误差。
for t in range(T, 0, -1):
z, f, i, g, o, cp, c, tc = cache[t - 1]
dc = dc + dh * o * (1 - tc * tc) # 细胞状态的梯度
da[3 * hid:] = dh * tc * o * (1 - o) # 输出门
da[hid:2 * hid] = dc * g * i * (1 - i) # 输入门
da[2 * hid:3 * hid] = dc * i * (1 - g * g)
da[:hid] = dc * cp * f * (1 - f) # 遗忘门
dc = dc * f # 沿加法通道回传
dh = (W @ da)[2:]
实跑输出:
--------------------------------------------------------------------
LSTM 对 RNN:加法问题,信息要跨二十步
--------------------------------------------------------------------
任务 序列 20 步,第 1 个通道放随机数,第 2 个通道只在两个随机位置标 1;要输出那两个位置上的数之和,再除以 2
为什么难 两个标记位置可能在序列任意处,答案要到第 20 步才用得上,而且中间不能忘
样本数 200(训练 150,测试 50)
隐层维度 16,两种结构完全相同
答案的离散程度 标准差 0.401
什么都不学、直接输出 0 的误差 0.3576
测试集平均绝对误差(越低越好,直接输出 0 的基线是 0.3576):
第几轮 RNN LSTM
-----------------------
1 0.3808 0.3660
10 0.4421 0.1508
20 0.3781 0.1028
40 0.3720 0.1146
60 0.4412 0.0925
80 0.4378 0.0710
100 0.4241 0.0689
RNN 最终误差 0.4241(比基线 0.3576 还差)
LSTM 最终误差 0.0689
LSTM 比基线好 5.2 倍
RNN 那一列从第 1 轮就在 0.38~0.44 之间晃,一百轮下来没往下走一步,甚至比「直接输出 0」还差——它在两个值之间来回摆,纯属欠拟合。
原因就是上一节算过的衰减:答案要对第 1~20 步上的两个位置都敏感,梯度得从第 20 步一路乘回第 1 步,二十次小于 1 的乘积之后只剩渣。
LSTM 那一列稳步下降,最后到 0.0689。
它靠的是一条叫「细胞状态」的通道:c_t = f × c_{t-1} + i × g。遗忘门 f 学到接近 1 时,c 基本原样往下带;因为是加法不是乘法,梯度沿着这条通道回传时不会被反复压缩。
两个门各管一件事:输入门 i 决定「这一刻的数要不要写进记忆」,输出门 o 决定「这一刻要不要把记忆读出来」。标记位是 1 的时候写、是 0 的时候不写,就靠 i 来学。
这个 Demo 只跑一百轮、隐层 16 维、样本 200 条,两个模型都远没到极限,看的是趋势不是绝对数值。真实任务上数据量大得多,LSTM 的优势没有这么悬殊——但「加门控解决长距离依赖」这个结论是站得住的,这也是 2015 年前后LSTM 统治序列任务的原因。
基线 0.3576、RNN 0.4241、LSTM 0.0689。 RNN 比基线差,这个对比最能说明问题——它不是「学得慢」,是学到了错的方向。 LSTM 第 40 轮有个小回升(0.1146 对第 20 轮的 0.1028),采样本少时常见,不用管。 整体趋势是单调向下的。 RNN 那一百轮一直没动,是因为梯度传不到第 1 步。 我试过把学习率调到 0.1,结果更糟——步子大了反而把它推到更平的地方。 我第一版写错了梯度的形状(输出层回传的维度没对齐),跑出来 RNN 是 0.0000、LSTM 是 1.0000, 两个都「完美」。那种一跑就完美的结果基本都有 bug,看到就该先怀疑代码而不是庆祝。 这个 Demo 是手写 BPTT,不是调库。手写的好处是每一步的形状和梯度都看得见,坏处是容易错。 真实项目里当然用框架,但知道里面长什么样,调不出来的时候才查得出问题。
30 秒实验
- 把序列长度从 20 提到 40,看 RNN 和 LSTM 的差距拉大到多少。
- 把遗忘门偏置从 2.0 改成 0.0,看 LSTM 的收敛速度掉多少。
- 想一想:为什么「加法通道」能缓解梯度消失,而普通的乘法递推不能?
常见误区
- 「LSTM 一定比 RNN 好」——短序列、简单任务上两者差不多,LSTM 还更慢更吃显存。
- 「门控解决了所有长距离问题」——它把能记的长度从十几步推到几百步,不是无限。
- 「误差比基线还差说明代码错了」——不一定,欠拟合时模型确实可能不如常数预测。
自测三问(点开对答案)
1. LSTM 靠什么缓解梯度消失? —— 一条细胞状态的加法通道,遗忘门接近 1 时记忆和梯度都能近似原样往下传。 2. 三个门分别管什么? —— 遗忘门管旧记忆留多少,输入门管新信息写不写,输出门管这一刻读不读出来。 3. 为什么「记住第一个数」这个任务 RNN 也能学会? —— 后面输入全为 0,隐状态收敛到由第一个输入决定的不动点,信息没被冲掉。六种架构讲完了。第八章到此结束,下一章换到「训练完之后怎么办」:怎么评估好坏、怎么优化。
- ☐ (本节完,下一节 8.3.1 认识模型:LoRA模型训练概述)