阶段 3 · 处理序列

RNN:一个网络
被反复用了一百次

图像是一整块,一次看完就行。但句子是一个字一个字来的—— 读到"猫"的时候,你还不知道后面会不会出现"不"。 RNN 的答案是:给网络加一个记事本,边读边记。

1

为什么普通 MLP 处理不了句子

问题具体表现
① 长度不固定 "好" 是 1 个字,"这个方案我觉得不太行" 是 10 个字。MLP 的输入层大小是写死的
② 词序信息丢失 "猫追狗" 和 "狗追猫" 意思完全相反,但如果你把每个词单独数一遍、不看顺序(这叫词袋),两者一样
③ 参数爆炸 想覆盖 100 个词的长度,就得为每个位置单独配一套权重
④ 换个位置就白学了 在"猫很可爱"里学会的"猫",到了"这只猫很可爱"里还得重新学一遍——同一个词,位置一变就成了新输入

互动 · 同一个序列,两种处理方式要多少参数

💡 一句话

所有问题的根源是:MLP 把输入当成一个整体,而序列是一个过程。 RNN 的核心 trick 只有一句话——让网络看一个词、更新一次记忆,然后带着记忆看下一个词。

2

循环:同一组权重,反复使用

RNN 的全部内容就是这一个公式:

把鼠标停在公式里的 tanh、Wh 上——它们是第 3 节「梯度每往回传一步乘了什么」的主角(那里把这个倍率叫 γ)。

公式里,x 是这个词的词向量(上一章给每个词配的那串数);h 是一串数,装着网络读到这里为止的全部记忆,叫记忆向量。 每一格具体存什么,不是人定的,是训练自己学出来的——Karpathy 2015 年的字符级 RNN 实验里,就有一个格子慢慢变成了「引号开了没」的检测器。 读第一项是"新输入",第二项是"旧记忆"。两者加起来过一层 tanh,就成了新的记忆。 注意 Wh 上面没有下标 t——同一个矩阵,在每一个时间步被重复使用。

互动 · 逐帧看记忆是怎么被更新的

每一步吐出的那个输出可以拿去做一件事:预测下一个词——读「我」时猜「昨天」,读「昨天」时猜「在」。训练时先把整句话读完、记下每一步的损失,再一路往回传梯度,这就是下一节的 BPTT。

🎯 类比

像一个只有一个抽屉的秘书。每次来一份新文件(xt), 他就打开抽屉看看旧文件(ht−1),把两者揉在一起写成一份新摘要,放回抽屉。 抽屉里永远只有一份文件——这就既是 RNN 的强大,也是它的致命伤。

示意图 · 同一块矩阵,被四个时间步反复使用

3

训练:把时间压扁成一条很深的链

RNN 在训练时会被沿时间展开(unroll)成一个普通的前馈网络: 100 个词的句子 → 100 层的网络,每层共享同一组权重。 然后直接用反向传播。这个做法叫 BPTT(沿时间反向传播)。

往回传的时候,每走一步都要乘一次「这一步对上一步有多敏感」——它由 Wh 有多大和 tanh 有多饱和一起决定。 要把它画成一条看得见的曲线,这里把它简化成一个数 γ:γ 小于 1,梯度每传一步就缩一次。

先猜一个数:γ = 0.9 听着很接近 1,连乘 50 次之后还剩多少?只剩 0.5%(0.950 ≈ 0.005)。把下面的滑块从 0.9 往下拉,看它掉得多快。

对比 · 每多走一步,梯度就乘一次 γ

γ 是演示用的简化值:真实网络里它由 Wh 的大小和 tanh 有多饱和一起决定,不是一个固定的数。

第 1 步的梯度
1.000
第 50 步的梯度
—
第 200 步的梯度
—

⚠️ 这是 RNN 的死因

梯度要从最后一步一路乘回到第一步。每一个时间步都会乘上一次 Wh 和 tanh 的导数; tanh 这个激活函数的导数最大只有 1,通常远小于 1。
乘 100 次之后,梯度小到对参数几乎没有影响——前面的词学不到了。 所谓"长期依赖",就是句首的词要影响很后面的判断:「我在法国长大……我会说流利的 ___」里,空格填什么要靠最前面的"法国"。 不是模型不想记,是梯度传不回去。

4

梯度消失与爆炸,同一件事的两面

两个问题来自同一个乘法,只是结果方向相反:

对比项梯度消失梯度爆炸
条件缩放因子 < 1(最常见)缩放因子 > 1(比较少见但更致命)
现象模型学不会长期依赖;只记得最近几个词 损失变成 NaN("不是一个数",训练直接崩),权重数值瞬间爆炸
怎么修换 LSTM / GRU(门控:给记忆加一套「留多少、扔多少」的开关);换注意力 梯度裁剪;正交初始化(让循环矩阵的缩放接近 1);调小学习率
能否根治RNN 结构上无法根治可以,梯度裁剪非常有效
💡 一句话记住

梯度爆炸好治,梯度消失难治。 梯度爆炸用"梯度裁剪"(超过阈值就等比缩回去)几乎总能按住; 但梯度消失是结构问题——你没法把已经乘成 0 的东西乘回来。 这就是为什么下一代结构必须换掉循环本身。

5

RNN 家族:谁修好了哪个毛病

变体怎么做解决什么
Elman RNNht = tanh(Wxxt + Whht−1 + b) 最经典的形式,也叫"简单 RNN"。Elman 1990 年的原文用的是 sigmoid,今天通行写法是 tanh
Jordan RNN每一步更新记忆的同时还会吐出一个输出,记作 y;这里把 ht−1 换成 yt−1(上一步的输出) 输出也参与记忆。用得较少
双向 RNN (BiRNN)一个从左往右读,一个从右往左读,两个方向的记忆合起来——标准做法是拼接成 2h 维 让每个词同时看到左右两边。但不能用于生成(生成时看不到未来)
深层 RNN把 RNN 叠好几层,上一层的输出序列喂给下一层 更强的表示能力,但更难训
LSTM / GRU加门控,让记忆可以被"选择性地保留"(门 = 一个 0~1 之间的开关,决定旧记忆留多少) 缓解梯度消失,是 RNN 时代真正能用的版本
SRU / QRNN用卷积或简化结构替代部分循环,让计算能并行 提速,准确率还能持平或更好

示意图 · 双向 RNN:一个从左读、一个从右读

M

数学 · 一步里记忆怎么被重写

前面说过「读一个词、更新一次记忆」。这句话拆开只有三个动作: 把词投进记忆空间、把旧记忆投进记忆空间、两者相加再压一下。 下面这张图把这三步用真数字算给你看——每一根柱子都是一个能对上号的值。

互动 · 一个时间步的六行数字:从词到新记忆

互动 · 每个符号管图上的哪一块

📐 看不明白 tanh 为什么「越压越死」?看一眼它的导数

6

RNN 的三个结构性缺陷

缺陷为什么后果
① 无法并行 ht 必须等 ht−1 算完。这是一个严格的串行链 每一步都在等上一步,GPU 大多在空转。训练极慢
② 记忆容量固定 不管句子有 10 个词还是 1000 个词,记忆都是同一个固定大小的向量 信息被强行压缩进一个"瓶颈",长句必然有损
③ 梯度传不远 每一步都要乘一次小于 1 的因子(第 3 节)。LSTM 能缓解(加法直通路),但信息还是要一步步传过去;注意力让任意两个词一步直达 句首的词几乎影响不到训练:它的梯度传到后面已经接近 0

互动 · 为什么它输给 Transformer:串行 vs 并行

互动 · 为什么长句必然有损:老词的痕迹被一步步重写

"无法并行"这一条在今天看来是致命的。2017 年 Transformer 的第一句卖点就是: 自注意力可以在一次矩阵乘法里同时处理所有位置——训练算力只要同类模型的几分之一到几十分之一(论文 Table 2 比的是训练所需的运算量)。 这是 RNN 被取代的直接原因,而不是因为它"效果不好"。

7

小结

RNN 这一章藏着一个很容易被当成"实现细节"的东西——Wh 上面那个不存在的下标 t。 它其实是整个结构里唯一真正算得上"想法"的部分。

它对应哪条线 ② 没有免费午餐 → 必须有归纳偏置—— RNN 用一组共享权重,对世界做了一个明确的假设。
一句话 RNN 断言"处理语言的规则不随位置改变":句首的"我"和句尾的"猫"都用同一组 Wx、Wh。 代价是逐字递推:每个时间步都要等上一步算完。 RNN 输给 Transformer,输的不是精度,是并行度。
它牺牲了什么 牺牲了并行和记忆容量(回扣暗线 B)。 它还牺牲了一个更隐蔽的东西:表达力—— 共享权重意味着"同一个模式在不同位置必须长得一样", 而语言里大量结构(嵌套、例外、句首句尾的不同角色)恰恰不是这样的。 换来的泛化是真的:它从很少的数据里就能学会很长的模式
🎬 自己验一遍:你现在能指着哪个互动说这句话

回到第 2 节那个互动(标题是「逐帧看记忆是怎么被更新的」)。 连点 7 次「→ 走一步」,把「我 昨天 在 公园 看见 一只 猫」读完。

盯住右边那个公式里的 Wx 和 Wh—— 每一帧都是同一组字母。处理"我"和处理"猫"用的是同一组权重, 网络从没为七个位置分别配一套。这就是那句假设的全部内容。

而代价也在同一张图上:左边那个 h 向量每走一步就被整体重写一次, 它的大小从头到尾没变过(就是那 6 个格子),这正是第 6 节说的"记忆有瓶颈"。 你如果没想指着那个 h 向量说这句话,就点「↻ 重置」再看一遍。

它在暗线里站在哪

下面就是这一章的答案——你读后面的章时会发现,每章都有这么一块。

暗线这一章的回答
A 信息流动 每个时间步取一个切片 [batch, d],过 Wx 后和上一步的记忆 [batch, h] 相加、过 tanh,形状从头到尾不变——§M 那张六行图每一步做的就是这件事
B 什么被牺牲了 牺牲了三样:并行度(每一步都要等上一步)、 记忆容量(不管句子多长都塞进同一个固定向量)、 梯度传不远(路径长度 O(n))。
换来的东西也实在:推理时的显存是 O(1):生成时 Transformer 要把前面所有词的中间结果都留着(KV cache), 句子越长占的内存越多;RNN 的"过去"就是那一个向量,不占额外空间。 这正是 2023 年以来 Mamba / RWKV 这类新模型又重新回到"带状态的循环"的原因
C 参数账本 以隐藏层 h = 512、输入 d = 512 的单层为例: Wx(512×512)+ Wh(512×512)+ b =约 52.5 万参数。
同样 512 维:一层 LSTM 是它的 4 倍(四个门,约 210 万参数); 一层 Transformer 的注意力(d=512、8 头,Q/K/V/O 四个 512×512)约 105 万(8 个头是把这四张表切成 8 份各算各的,参数总数不变)。
真正吓人的是时间账而不是参数账:处理 1000 个 token, RNN 要串行发 1000 次矩阵乘——每次启动开销几微秒, 光启动就可能比那次乘法本身还贵; 而同一批计算换成矩阵乘法可以一次发完。 参数量不等于训练速度——这是 RNN 最贵的一课
D 跑在什么上 它慢不是因为算得多,而是每一步都得等上一步:没有足够多互相独立的工作可以同时丢给 GPU, 所以 GPU 利用率常常只有个位数。这个区别很重要。
对策只有一个:把 batch 做大,让单步的矩阵乘变胖,用"同时处理很多句子"来填满 GPU。 完整的算力账在 《硬件与算力账本》
E 它假设了什么 两条假设,都很强:
① “处理规则不随位置改变”(权重共享)——句首和句尾用同一套判断规则;
② “一个固定大小的向量足以承载到目前为止的一切”——相信过去能被无损地压缩成一个摘要。
第二条失败得很明显:嵌套一深就装不下:不管句子多长,抽屉还是那么大
F 违背了哪个直觉 “参数少 = 省钱”是错的。RNN 的参数量远小于同尺寸的 Transformer (权重共享),但它恰恰是那个跑不快的——参数账和速度账是两本账。
第二个:“给网络加记忆会让它更强”——错,加记忆让训练变难了。 ht 让每一步都依赖上一步,梯度路径从 O(1) 变成了 O(n), 这才是长期依赖学不出来的原因。
第三个:“RNN 已经被淘汰了”——在长序列推理上它正在回来。 因为它的推理状态是 O(1),而 Transformer 的 KV cache 随长度线性增长
🎯 前后钩子

它接住了上一章的什么:词向量(《词向量》) 给每个词配了一个向量,但那个向量与句子无关—— "银行"在两句不同的话里长得一模一样。这一章第一次让表示随读取过程变化。

它给下一章留了什么:梯度传不回去这件事,RNN 自己修不了。 既然问题是"每个时间步都要乘一次小于 1 的因子", 那就别乘——给记忆开一条不受乘法影响的高速公路。 这就是《LSTM / GRU》要回答的问题。

一句话带走 RNN

RNN = 一组权重被反复使用,用来维护一个"记忆向量"。
它的思想(用状态贯穿序列)是对的,但结构上有三个死穴: 不能并行、记忆容量固定、梯度传不远。
梯度传不远:LSTM / GRU 用门控缓解——给记忆加一条加法直通路,梯度不必每一步都乘一个小数;但它还是一步一步走,路径仍是 O(n)。 不能并行和记忆瓶颈,要靠注意力(→ Transformer)解决:那里任意两个词一步直达。

8

拓展阅读

上面讲的都是「够用」的版本。想往下挖,这里有三个入口—— 它们不是必修内容,是给想再往前走一步的读者准备的。

📄 这一章的说法从哪来

💻 工业界怎么写

∑ 更严格的形式