上一讲第 6 讲:CNN 架构 下一讲第 8 讲:注意力机制与 Transformer

第 7 讲:循环神经网络

Recurrent Neural Networks —— 序列建模的四种形式、隐藏状态与随时间展开、一个手算到底的 RNN 实例、随时间反向传播与截断技巧、字符级语言模型的训练与采样、梯度消失与爆炸、LSTM 的门控机制,以及 RNN 在 Transformer 时代的回声

本讲概览:

本讲是课程从"看一张图"迈向"理解一段序列"的转折点。老师先花几分钟澄清了上一讲两处容易混淆的内容(Dropout 测试期缩放中超参数 \(p\) 的两种口径、LayerNorm 与权重初始化的关系),并顺带推荐了实验管理工具 Weights & Biases;随后正式切入序列建模(sequence modeling):当输入不再是固定尺寸的张量、而是变长的序列时,网络应该长什么样?答案是 Transformer 出现之前的主力模型——循环神经网络(Recurrent Neural Network, RNN)。老师从"隐藏状态随序列逐步更新"这一核心思想出发,给出了 RNN 的递归公式与展开计算图,并用一节课上唯一一个从头手算的例子(检测输入序列中"连续两个 1")把三个权重矩阵的分工讲透;接着讲多对多、多对一、一对多设定下损失与梯度的计算方式(BPTT),以及应对长序列显存爆炸的截断反向传播(truncated BPTT)。后半段以字符级语言模型为例展示 RNN 的生成能力与惊人可解释性,盘点它在图像标注、视觉问答等视觉任务上的辉煌与"幻觉"问题;最后深入 RNN 的命门——梯度消失与爆炸,引出 LSTM 的四门结构与"信息高速公路",并把 RNN 与 ResNet 跳跃连接、以及今天复活 RNN 思想的状态空间模型(Mamba、RWKV)串联起来,为下一讲的注意力与 Transformer 铺路。

一、开场:上一讲的两个澄清与一个工具推荐

正式开讲前,老师回应了课后论坛上的两个高赞帖子,都是上一讲遗留的细节问题。

第一个是 Dropout 在测试期如何缩放。Dropout 有一个超参数 \(p\),但在不同实现里它的含义可能相反:多数库里 \(p\) 指被丢弃的神经元比例,但也有实现(包括课程幻灯片里那份代码)把 \(p\) 定义为保留的比例——这正是课堂讲解与幻灯片出现"口径不匹配"造成困惑的原因。无论哪种口径,基本原则只有一条:测试时的期望输出要与训练时一致。具体地说,如果训练时丢弃了 25% 的激活值,那么测试时就应当把输出整体乘以 0.75 来补偿,这样两边激活值的期望才相等。记住原则,符号口径只是表象。

第二个是 LayerNorm 能否"治好"糟糕的权重初始化。老师展示的实验沿用上一讲的设置:二维输入、两层 ReLU 网络学习一个"象限函数"(按点落在第几象限输出 1/2/3/4)。对比三条训练/测试损失曲线:坏初始化(标准差过大)、坏初始化 + LayerNorm、以及 Kaiming 好初始化。结论有两层:其一,LayerNorm 确实解决了坏初始化带来的大部分问题(曲线明显改善),但差距依然存在——想拿到最好性能,仍然需要好的初始化,不能指望只靠归一化蒙混过关;其二,LayerNorm 是否有帮助取决于问题本身:象限分类并不需要知道每个点的精确二维坐标,所以归一化无伤大雅、反而有益;但对那些必须依赖精确坐标才能给出正确输出的函数,减均值、除标准差会直接抹掉输入的绝对位置信息,LayerNorm 反而损害性能。

随后老师快速回顾了到目前为止课程讨论的都是"普通"(非循环)神经网络:固定尺寸输入映射到固定尺寸输出,外围设施包括激活函数的选择、按固定均值和标准差做的数据预处理、权重初始化、归一化、迁移学习(在 ImageNet 等大数据集上预训练后初始化权重效果更好),以及训练动态的看护(学习率挑选、超参数按验证集表现更新、测试时增强等)。针对"训练看护"这一环,老师推荐了自己几乎每个项目都在用的工具 Weights & Biases:它可以为多组不同超参数的运行生成彩色编码的对比图,例如把不同 dropout 取值的验证集精度画在一起,一眼就能看出较低的 dropout 在该任务上普遍精度更高。类似工具还有 TensorBoard,但老师个人偏爱前者——如果有算力把实验反复跑很多遍,这类可视化非常值得用。

二、从固定输入到序列建模:四种任务形式

以上所有内容都建立在一个隐含前提上:输入是一张固定尺寸的图。本讲开始讨论序列建模(sequence modeling):如果输入是一个长度可变的序列,网络该如何应对?老师同时预告了两件事:第一,今天讲的是 Transformer 时代之前人们使用的简单序列网络,主要是 RNN 及其变体;第二,课末会用一页幻灯片把 RNN 与当今的语言模型新流派——状态空间模型(state space models),例如 Mamba——联系起来,说明 RNN 的关键思想至今仍在被使用,而且在某些方面对 Transformer 有实打实的优势。

先把问题的形式化说清楚。按照输入序列长度与输出序列长度的关系,可以分出四种情形:

讲 RNN 的工作原理时,本讲主要采用最直接的最后一种设定:每来一个输入就产出一个输出。其他三种设定都可以在此基础上做小改造得到。这个"逐帧输出"设定的经典例题是对视频的每一帧做分类

三、RNN 的基本思想:隐藏状态与随时间展开

什么是 RNN?给定输入序列 \(x\) 和输出序列 \(y\),让 RNN 成其为 RNN 的是它的循环(recurrent)本性。在示意图上,这表现为一个从方块输出绕回自身输入的回环箭头——读架构图时看到这种自反馈,就知道遇到循环层了。它的实际含义是:RNN 内部维护一个内部状态,通常叫隐藏状态(hidden state),随序列的处理不断更新——每当模型收到一个新输入,就结合它与上一步的内部状态计算出新的隐藏状态。换句话说,隐藏状态既依赖当前输入,也依赖历史。

老师提醒,带回环的示意图虽然直观,但很难看清梯度的计算路径与运算的先后次序,所以人们更常画展开(unrolled)的 RNN:把时间步横向铺开,显式画出"当前隐藏状态的计算依赖当前时间步的输入 \(x_t\) 与上一个 RNN 状态 \(h_{t-1}\)"。展开图把每个输出到底依赖了哪些量交代得一清二楚,是沿计算图反向传播时的思维底稿。

接下来把文字变成数学。RNN 处理向量序列 \(x\),在每个时间步套用同一个递归公式

$$ h_t = f_W(h_{t-1}, x_t) $$

这与前面课程学过的普通神经网络层几乎一模一样:权重矩阵做乘法,后跟激活函数。唯一的区别在于它现在是递归的——同一组参数 \(W\) 和同一个激活函数在每个时间步被重复使用。而要从隐藏状态得到输出,还有另外一个单独的函数、单独一组参数:

$$ y_t = W_{hy} h_t $$

老师在 \(W_{hy}\) 上停留了很久,强调它同时干两件事:其一是改变维度——隐藏状态的维度是设计者自由决定的,输出维度由任务决定,两者可以不同,\(W_{hy}\) 负责把隐藏维度映射到输出维度;其二是提供一次可学习的变换。所以整个 RNN 里有两组职能分明的参数:一组负责"如何从旧状态和新输入算出新状态",另一组负责"如何把状态翻译成输出"。

两个课堂问答值得记录。问:旧的输出 \(y\) 能影响新的隐藏状态吗?在某些形式下可以,最常见的做法出现在语言建模/自回归建模里——既然要"给定前面的值预测下一个值",人们干脆把先前的输出直接当作下一步的输入喂回去,这是让 \(y\) 显式影响 \(h\) 的标准途径。问:第一个时间步的 \(h\) 与 \(x\) 有什么区别?短答案:它们使用不同的权重——\(h_0\) 的更新走的是与所有隐藏状态更新相同的那组权重。展开图还提醒我们:隐藏状态需要一个初始值 \(h_0\),理论上可以任意取,实践中通常把它设为一个可学习的向量

四、Vanilla RNN 公式与一个手算到底的实例

当人们说 Vanilla RNN(朴素循环神经网络)时,几乎总是指下面这个最简形式:

$$ h_t = \tanh(W_{hh} h_{t-1} + W_{xh} x_t), \qquad y_t = W_{hy} h_t $$

激活函数选 \(\tanh\)(双曲正切)是深思熟虑的:它的输出有界于 \([-1, 1]\),于是无论递归操作重复多少次,数值都困在这个区间里不会发散;同时它零中心,正负值都能表达。

公式看懂容易,权重矩阵究竟各自"知道"什么却很抽象。所以老师做了一个课上唯一一次全程手算的例子:不靠梯度下降,手工构造一个能干活的 RNN,把前向传播的每一步、每个矩阵的分工全部摊开。

手算

任务定义。输入是 0/1 序列,目标:当输入中连续出现两个 1 时输出 1,其余情况输出 0。例如输入 0, 1, 0, 1, 1, 1, …:前四个输入(0,1,0,1)都没有"连续两个 1",输出 0;到第五个输入时出现了连续的 1, 1,输出 1;第六个输入继续输出 1,因为它与前者又构成一对。这是标准的"每个输入对应一个输出"的多对多任务。

第一步:隐藏状态该记什么?老师先提问:要完成这个任务,内部状态里必须保存什么信息?答案是两样——上一时间步的输入值,以及当前时间步的输入值(0 或 1)。有了这两个数,判断"连续两个 1"就是一次简单比较。

第二步:设计隐藏状态的布局与初始化。把隐藏状态设为三维列向量:

h = [ 当前值, 前一个值, 1 ]ᵀ

第三维的常数 1 纯粹是为输出计算服务的"偏置技巧",让最后的判断只需一次内积;没有它也能构造,但有了它数学更干净。初始状态取 \(h_0 = (0, 0, 1)\),语义上相当于假设"模型此前已经读过两个 0"。激活函数选 ReLU——因为本例中只出现 0 和 1,\(\max(0, \cdot)\) 不会引入任何额外计算量,纯粹为了让数学好算(老师说明用 \(\tanh\) 同样可以构造出等价模型)。

第三步:\(W_{xh}\) —— 把输入放进"当前值"槽位。取 3×1 的列向量:

W_xh = [1, 0, 0]ᵀ

这样 \(x\) 为 0 时 \(W_{xh} \cdot x\) 得到零向量,\(x\) 为 1 时得到 \((1, 0, 0)\)。它与 \(x\) 的乘积恰好负责写入"当前值"这一格——顶部要么是 0 要么是 1,其余两格为零。

第四步:\(W_{hh}\) —— 在状态内部"搬运"信息。取 3×3 矩阵:

      [ 0 0 0 ]
W_hh = [ 1 0 0 ]
      [ 0 0 1 ]

逐行解读它的用意:第一行全零,意味着新状态的"当前值"槽位完全不受上一状态影响——这一格只听 \(W_{xh}\) 那一路的(这正是第三步的成果得以保留的原因);第二行 \((1, 0, 0)\),取与上一状态的内积恰好把上一时刻的"当前值"原样抄写到新状态的"前一个值"槽位——完成了"昨天的当前值变成今天的前一个值"这次位移;第三行 \((0, 0, 1)\),维持常数 1 不变。合在一起:

W_hh · h_{t−1} + W_xh · x_t
  = [ 0, 当前值_{t−1}, 1 ]ᵀ + [ x_t, 0, 0 ]ᵀ
  = [ 当前值_t, 前一个值_t, 1 ]ᵀ      (再过 ReLU,数值不变)

第五步:\(W_{hy}\) —— 用内积完成判断。取 1×3 的行向量 \([1, 1, -1]\),则输出为:

$$ y_t = \operatorname{ReLU}(\text{当前值} + \text{前一个值} - 1) $$

代入数字验证全部四种情形:当前 1、前一 1 时,\(1 + 1 - 1 = 1\),ReLU 后输出 1;当前 0、前一 0 时,\(0 + 0 - 1 = -1\),ReLU 截断输出 0;一单一双(\(1, 0\) 或 \(0, 1\))时得 0,同样输出 0。完美。常数 1 那一维在这里派上用场——它就是内积里的 −1 偏置项。

课堂问答:这些权重矩阵实际怎么来?答案与本课程所有权重一模一样:梯度下降。手算例子只是为了看清每个矩阵在前向传播中的角色;真实 RNN 的隐藏维度比 3 大得多,把这种小网络当初始化再去训练别的任务固然算是"迁移学习",但实践中不会好用——隐藏状态实在太小。

这个例子还展示了 RNN 最重要的一条性质:同一组 \(W\) 在所有时间步共享。正因为"更新规则每一步都相同",模型尺寸与输入长度无关,任意长的序列都能处理——这是后面盘点 RNN 优缺点的核心。

五、随时间反向传播(BPTT)与截断技巧

前向传播清楚了,梯度怎么算?回到计算图:\(x_1, x_2, \dots\) 依次进入,每个时间步算一个隐藏状态,且每一步用的都是同一组 \(W\)。先看多对多设定:每个输出都可以配一个损失(这一步的预测有多对),把它们全部相加就是整个输入序列上的总损失:

$$ L = \sum_t L_t $$

反向传播求 \(W\) 的梯度时有个概念上的诀窍:可以假装每个时间步用的是各自独立的 W,按普通计算图为每个"分身 \(W\)"分别求梯度——最后因为它们其实是同一个矩阵,把各时间步的梯度全部求和即可。这就是随时间反向传播(Backpropagation Through Time, BPTT)

多对一设定下只有一个损失,通常(视问题而定)只用最终隐藏状态经输出层计算 \(y\);但像视频分类这种任务,整段视频从头到尾都携带线索,更合理的做法是对每一步的隐藏状态做池化(平均池化或最大池化)再得输出。一对多设定(比如图像标注)则呼应了前面的问答:递归函数 \(f_W\) 期待一个输入向量 \(x\),序列开始后没有新的 \(x\),可以补零,也可以把上一步的输出填进输入位置。

5.1 显存危机与截断 BPTT

如果把整套概念直接照搬去训练,马上会遇到非常实际的麻烦:GPU 显存耗尽(老师笑称这几乎是训练神经网络一切问题的根源)和损失变 NaN。原因不难理解——序列一长,你必须把每个时间步的激活值和梯度统统留在内存里再逐级求和,开销随序列长度线性膨胀,很快爆掉。

标准解法是截断 BPTT(truncated backpropagation through time):固定一个时间窗口,假装窗口内的这一小段就是模型的全部训练数据。从 \(h_0\) 出发,在窗口内逐步步进、逐输出算损失,窗口结束后做一次完整的反向传播与梯度更新。然后滑动到下一块——关键细节是:下一块的初始隐藏状态沿用上一块结束时算出的隐藏状态(前向的信息接续传递),但梯度不再跨块回传。块与块之间需要保存的只有一个量:本块初始隐藏状态关于损失的梯度。更新完权重、清空中间内存,继续下一块,如此扫完整个序列。

老师补充了两点。其一,这种"各块独立算梯度、依次应用到同一组权重"的模式,与分布式训练中多块 GPU 各自计算梯度再统一应用到同一份权重上的做法如出一辙,课程后面讲分布式训练时会再见到。其二,截断并不完美:你做的是多次独立更新而非一次全局更新;在每步有损失的设定里,截断意味着每次只看到全部损失的一个邻域——信息确实有丢失。理论上能全部装进内存当然更好,截断是工程上的务实妥协。

另一个课堂问答:如果每个时间步真的用不同的 \(W\) 会怎样?那就不再是递归关系了——相当于给每个时间步配一个独立的层,各自学各自的权重。对"一堆彼此无序的东西要分类"的非序列问题或许可行,但对变长序列完全行不通:你得预先知道序列长度才能确定网络结构,而且等于为每个位置单独训练一个网络,完全丢掉了序列建模的本意,表现也会更差。

六、RNN 做语言模型:训练、采样与嵌入层

接下来是一个"没法把整个 RNN 画进幻灯片"的实用例子:字符级语言模型(character-level language model)。老师感慨道,这套方法十年前就被证明相当有效,而今天这一波大语言模型浪潮,恰恰是从"用 RNN 预测下一个字符"这么朴素的思路一步步累积起来的。

6.1 训练:逐步分类问题

训练数据的组织方式:把文本按字符切开,每个字符用独热编码(one-hot encoding)表示——向量里只有一个 1(对应该字符的位置),其余全是 0。每个时间步输入一个字符,经共享的递归公式算出隐藏状态,再经输出层产生对下一个字符的预测:目标是下一时间步的真实字符。输出层可以理解为 softmax——幻灯片上的例子给出了 logits 分数,比如正确目标字符 e 只拿到 2.2、低于别的候选的 4.1,说明这一步预测错了,损失函数会重重地罚它(老师顺便分享了课堂趣闻:这张幻灯片用了很多年,这次课被同学们当场发现了这个数字标注错误)。

$$ L_t = -\log p_t(y_{t+1}), \qquad p_t = \operatorname{softmax}(s_t) $$

值得刻在脑子里的视角:语言模型本质上就是"按时间步的分类问题"——每一步都在 softmax 上做一个多类分类,只不过类别是词表里的全部字符/词元。今天的大语言模型在此意义上与这个 112 行 Python 代码的 RNN 做的是同一件事。

测试时的生成是自回归(autoregressive)的:逐字符采样,把模型刚生成的字符喂回作为下一步输入,循环往复,直到生成出想要的文本。序列的起点可以是随机一个字符,更常见的做法是给语言模型一个预定的起始符(start token)——现代语言模型几乎都有这个约定。

6.2 嵌入层:为什么不用独热向量喂模型

实践里人们并不真的把独热向量直接输入模型,而是先过一个嵌入层(embedding layer)。它本质上就是一张巨大的可学习矩阵:词表大小 D × 嵌入维度,查表操作等价于一次矩阵乘法——按输入字符的索引取出对应那一行作为输入向量。为什么这样做更好?老师给出的直觉:模型普遍偏好分散(spread out)的向量输入,而不是"一个 1 带一堆 0"的高度稀疏表示;嵌入矩阵可以用 Kaiming 等方法初始化成很小的值,然后与其他权重一起被梯度下降共同优化——输入表示本身也成了学习对象,优化效果更好。

6.3 从莎士比亚到 Linux 内核:生成的进化

这套实现可以只用 112 行 Python 写完,正是前讲师 Andrej Karpathy 在 2015 年那篇著名博文《The Unreasonable Effectiveness of Recurrent Neural Networks》展示的"前 LLM 时代"能玩出的花样。给它喂莎士比亚十四行诗,可以看到训练过程中的清晰进化阶段:最初输出纯是乱码(W 还没学到任何东西);训练一阵后开始"像英语"——至少有些词是对的;继续训练,它竟然学会了语料的风格结构,比如十四行诗里说话人名字的排版格式,生成出颇为可信的段落——尽管生成得越长,句子越容易渐渐散架。喂它 Linux 内核源代码,它能生成看起来像模像样的 C 代码(老师:不确定能不能编译,但看着挺合理)。

老师特意把这个老例子与当下连接:今天的编程助手类语言模型,训练任务与此高度同源——吞下海量既有代码,只是把"预测下一个字符"换成"预测下一个词元(token)",即一组字符;词元怎么切分因模型而异,细节很多,但高层逻辑完全一样:自回归地一个接一个预测。当年的伏笔,如今成了整个行业。

6.4 三个课堂问答:标注、采样与中间输出

问:语言模型怎么标注数据?这是它最迷人的一点:完全不需要标注。它只做"预测下一个词元"这一件事,监督信号天然就是文本自身——下一个字符就是标签。所以人们可以把互联网上能爬到的文本全部拿来训练。语言模型之所以强大,正因为数据几乎无限。

问:如果每步都取概率最大的输出,模型岂不是每次都生成一模一样的内容?是的,完全正确。这种每步取 argmax 的做法叫贪心解码(greedy decoding),同样的输入必然得到同样的输出。实践中人们不这么干,而是按 softmax 给出的分布采样:比如某步输出分布是 0.84 与 0.13 等,就以相应的概率抽取候选。更进一步还有束搜索(beam search)——同时向前探索多条候选路径,选整条序列总概率最高的那条。如何从语言模型采样本身就是一个活跃的研究领域,但简短的答案是:别总挑概率最高的

问:多对一任务要不要在每个时间步都输出?出于省算力的考虑,人们一般不会计算派不上用场的输出;但技术上完全可以每步都输出,而且有时颇有价值——比如观察中间输出随训练是否逐渐收敛、模型是否在某几个关键帧上被"触发",这能帮你理解模型内部在依赖什么。

七、窥视 RNN 的内心:可解释的隐藏细胞

RNN 有一桩意外的美德:把激活值画出来,常常能直接看出模型在跟踪什么。回看手算例子,我们手工规定了隐藏状态的三格分别记"当前值、前一个值、常数"——而真实训练出的 RNN 会自发学出类似的东西。

可视化方法:模型用 \(\tanh\) 激活,取值范围 \([-1, 1]\),把 −1 画成红色、+1 画成蓝色,对每个输入字符标注该细胞在该时间步的激活值,就得到一张"细胞随序列起伏"的图。大多数细胞看不出名堂(近似随机),但总有一些令人惊喜的可解释细胞,全部来自真实训练的语言模型:

这些只是某一层里的单个激活值,却不需要任何花哨的分析技巧就能直接映射回输入序列。老师在手算例子里手工分配给隐藏状态的语义,RNN 通过梯度下降自己学会了——这个对应关系恐怕是本讲最值得回味的观察之一。

八、RNN 的优点与缺点

老师专门用一页幻灯片盘点 RNN 的取舍,这页值得完整记住,因为它直接解释了 RNN 的兴衰。

优点:

缺点:

九、RNN 在计算机视觉中的应用

作为视觉课程,老师随后盘点了一批 RNN 曾经建功、如今多数已被大一统模型接管的视觉-语言任务。

9.1 图像标注:CNN 与 RNN 的两级流水线

图像标注(一对多任务)的经典做法分两级:先用一个视觉编码器(通常是 ImageNet 上预训练的 CNN)提取图像特征——注意用的是倒数第二层的表示而非最终类别得分,这正是迁移学习里取特征的惯用策略;然后把这个视觉特征经由一组新权重 \(W_{ih}\) 注入 RNN,与已生成的文本一起决定后续的隐藏状态。序列从起始符(start token)开始,用采样(贪心或按分布)逐步生成词元,一旦采到结束符(end token)就停笔。这套当年相当成功的模型能产出大量相当靠谱的字幕。

但失败案例同样有教育意义,老师展示了几类典型错误:一个人双手捧着手机,被描述成"在用鼠标"——因为捧持的姿势像握鼠标,但手机是平的、掌心朝上,人类一眼可辨;女士穿毛领衣服被说成"抱着一只猫";画面里有沙滩就默认"有冲浪板";树上没有鸟却说"鸟栖息在树上";球员明明在接球,却说他扔球。这些错误的共同根源是训练数据中的共现偏差:沙滩常配冲浪板、树常栖鸟,模型学会了这种统计关联,却没有学会把"这个场景通常有什么"与"这幅图里到底有什么"解耦——毕竟训练目标只有"输出字幕",从没要求它解释理由。老师特别提醒:这类幻觉(hallucination)在今天的视觉语言模型上依然极其常见,可谓老毛病新犯。

9.2 视觉问答的两种形式

视觉问答(Visual Question Answering, VQA)是另一种常用 RNN 的语言-视觉任务,当年常见两种形式。第一种走生成路线:把问题喂给一个字幕式的模型,让它自由生成答案文本,然后比较各候选答案的序列概率(把每个字符/词元的概率连乘得到整句概率),选概率最高者。第二种更常见,走分类路线:把问题和若干候选答案分别作为输入一起喂给模型,输出端是一个多分类器——比如四个候选答案就是四分类,直接输出每个答案的概率。此外还有视觉对话(visual dialogue)——围绕一张图持续问答聊天。老师感慨:当年这些是各自独立的任务、各自专门的模型,如今一个模型几乎全包,而这类能力在最近两年才真正爆发。另一个例子是视觉导航:输入环境图像序列,输出在二维平面图上一连串的移动方向,走到目标位置。

9.3 多层 RNN:时间与深度的网格

和 CNN、全连接网络一样,RNN 也可以多层堆叠——前面展示的其实大多已是多层 RNN。规则要分两个维度说清楚:沿时间维度,每层只和"自己这一层"的上一步隐藏状态递归,第一层吃真实输入 \(x\),第二层及以上的输入是下一层在同一时间步的输出 \(y\);沿深度维度,每一层拥有独立的一组权重,而层内所有时间步共享该层的权重。整体计算图铺成一张网格:要算出最右上角那个隐藏状态,得先把左下方向整片网格的隐藏状态全算完——这就直观解释了多层 RNN 的训练为何如此低效。

十、命门:梯度的消失与爆炸

铺垫完毕,老师转向 RNN 被架构变革取代的根本原因。先把 Vanilla RNN 的公式整理成等价的紧凑写法:把 \(h_{t-1}\) 与 \(x_t\) 两个向量纵向拼接,把 \(W_{hh}\) 与 \(W_{xh}\) 按块拼成一个大 \(W\):

$$ h_t = \tanh\left( W \cdot \left[ h_{t-1} ;\, x_t \right] \right) $$

注意这个大 \(W\) 里其实有大量零元素(分块对角排布——\(W_{hh}\) 根本不与 \(x_t\) 相互作用),三种写法只是记法繁简之别,思想上完全等价。

现在沿时间反向传播。若损失只在最终输出计算,梯度要沿隐藏状态链一路传回,每跨一个时间步都要重复乘两样东西:权重矩阵 \(W_{hh}\)\(\tanh\) 的导数。形式化地,相邻时间步隐藏状态之间的雅可比矩阵是:

$$ \frac{\partial h_t}{\partial h_{t-1}} = \operatorname{diag}(\tanh') \cdot W_{hh} $$ $$ \prod_{t=1}^{T} \frac{\partial h_t}{\partial h_{t-1}} = \prod_{t=1}^{T} \left( \operatorname{diag}(\tanh') \cdot W_{hh} \right) $$

要算最初时刻的梯度,就得把这个矩阵连乘 \(T\) 次。问题就出在这连乘上,且来源有两个独立层面:

两种病,一种有药,一种难医。爆炸的修复简单粗暴:梯度裁剪(gradient clipping)——发现梯度过大就按比例缩放或截断,别让一步更新迈太大。而梯度消失才是 RNN 真正的绝症:\(\tanh\) 的导数性质加上权重矩阵的收缩倾向,使得实践中没人能用 vanilla RNN 处理很长的序列。正是这一点推动了 RNN 架构的革新,也是 RNN 被放弃的主要原因之一。

十一、LSTM:四个门与一条信息高速公路

解决之道的名字叫 LSTM(Long Short-Term Memory,长短期记忆网络),早在 1990 年代就被提出,在 Transformer 革命到来之前长期占据序列建模的主导地位。老师先给课程预期管理:不要求大家掌握 LSTM 的操作细节(作业里也不会实现),真正希望带走的是——RNN 有哪些关键缺陷,LSTM 如何缓解它们;后面的现代状态空间模型,本质上也在解同一套问题。

LSTM 的高层设计可以概括成三句话:第一,不再只有一个隐藏状态,而是维护多个内部量,其中最重要的是一条独立的细胞状态(cell state)第二,模型在每一步预先算出四个门控量,分别裁决信息的去留;第三,为长程信息单独开辟一条不经激活函数的加法通路。逐个门拆开看(对应老师课上"相当复杂的图"):

而最关键的巧思在细胞状态那条顶部通路。老师把它比作一条高速公路(highway):这条路上没有任何激活函数——没有 \(\tanh\),每步唯一的操作就是乘一次遗忘门。对比第十节的分析:梯度消失的两大来源是 \(\tanh\) 导数与反复的矩阵乘法,高速公路恰好把两者都绕开了——只要遗忘门不全为 0(即模型不是每步都清空记忆),信息与梯度就能沿着这条加法通路长距离直通。实践中这套设计效果拔群。

深入

LSTM 的完整公式逐步拆解。老师课上展示的是概念图,这里补全标准记法帮助对照理解。每个时间步先并行算出四个量(\(\sigma\) 为 sigmoid,把门压到 \((0,1)\) 区间;\(\odot\) 为逐元素乘):

$$ i_t = \sigma(W_{xi} x_t + W_{hi} h_{t-1}) \quad \text{(输入门)} \qquad f_t = \sigma(W_{xf} x_t + W_{hf} h_{t-1}) \quad \text{(遗忘门)} $$
$$ o_t = \sigma(W_{xo} x_t + W_{ho} h_{t-1}) \quad \text{(输出门)} \qquad g_t = \tanh(W_{xg} x_t + W_{hg} h_{t-1}) \quad \text{(候选写入)} $$

然后是两步状态更新——注意第一式的加法结构,它就是"高速公路"的数学化身:

$$ c_t = f_t \odot c_{t-1} + i_t \odot g_t \qquad h_t = o_t \odot \tanh(c_t) $$

逐句对照老师的高层描述:\(c_t\) 的更新中,旧细胞状态只乘一个门 \(f_t\)、再加一项受门 \(i_t\) 调制的新内容——没有对旧状态整体做 \(\tanh\) 压缩,也没有乘一个大权重矩阵。若 \(f_t \approx 1\)、\(i_t \approx 0\),细胞状态几乎原样向前传递,梯度沿这条加法链回传时每步近似只乘 \(f_t\),不再被 \(\tanh'\) 与矩阵连乘反复盘剥。\(\tanh\) 只出现在两处"支路"上(生成候选 \(g_t\)、把细胞状态压成输出 \(h_t\)),都不在信息主干上。这就是"选择性放行信息"的准确含义。

课堂问答:LSTM 彻底解决梯度消失了吗?没有保证,但显著缓解。要害在于对比:vanilla RNN 想让信息跨过很多时间步,必须学出一组"每一步都恰好保信息"的权重矩阵——同一条变换、每步都执行、还夹着激活函数,这个条件苛刻到难以学成;而 LSTM 直接提供了一条无激活的直通路径,让长期依赖的学习容易得多,实证效果也非常好。所以此后若仍走循环建模路线,人们一般训练 LSTM 而不是 vanilla RNN——不过老师坦言,这整套路线总体上已经相当过时,学它的意义在于理解"人们如何针对 RNN 的缺陷设计架构"。

最后是一个漂亮的呼应:LSTM 与 ResNet 是同一个思想在两个维度的化身。把值复制后绕过若干层直接相加——这正是在 ResNet 里学过的跳跃连接(skip connection);LSTM 细胞状态那条加法通路做的是同样的事,只不过 ResNet 跳的是深度方向的层,LSTM 跳的是时间方向的时间步。同一味药,治两种"太深/太长导致的梯度病"。

拓展

GRU:LSTM 的精简表亲。本讲字幕中未展开,但阅读文献几乎必然会遇到它。门控循环单元(Gated Recurrent Unit, GRU)把 LSTM 的四个门精简为两个:更新门(update gate)同时承担"遗忘旧状态"与"写入新内容"两个决策(两者互补,加起来恒为一),重置门(reset gate)决定生成候选内容时参考多少历史。GRU 还取消了独立的细胞状态,只留一个隐藏状态,参数比 LSTM 少约四分之一、训练更快,在不少任务上与 LSTM 打平。三者的选择至今没有定论,经验法则:数据/算力紧张可先试 GRU,追求极限性能可都试。

$$ z_t = \sigma(W_{xz} x_t + W_{hz} h_{t-1}) \quad \text{(更新门)} \qquad r_t = \sigma(W_{xr} x_t + W_{hr} h_{t-1}) \quad \text{(重置门)} $$
$$ \tilde{h}_t = \tanh\left( W_{xh} x_t + W_{hh} (r_t \odot h_{t-1}) \right) \quad \text{(候选状态)} \qquad h_t = (1 - z_t) \odot h_{t-1} + z_t \odot \tilde{h}_t $$

十二、RNN 的当代回声:状态空间模型

最后一页幻灯片讲一个老师自己也觉得有趣的变化:RNN 在最近一两年重新翻红了。老师半开玩笑地说,如果这门课开在一两年前,他很可能会干脆把 RNN 从大纲里删掉——但事实是 RNN 的核心优点在当下显得越来越值钱,主要有两条,条条戳中 Transformer 的痛处:

沿这条脉络,老师点名了两篇代表作:RWKVMamba——它们主打线性时间序列建模,把 RNN 式的循环状态更新(配合类似 LSTM 的选择性记忆思想)用现代方式重新设计,在长上下文问题上计算开销更优。当下的活跃研究方向正是:如何兼得 Transformer 的性能与 RNN 的扩展性——两者 hybrid 混搭也是常见答案。

拓展

RNN 在 Transformer 时代的位置。把本讲的谱系放进时间轴:1997 年 LSTM 问世,此后近二十年是循环网络统治序列建模的年代(机器翻译、语言模型、语音识别全在其上);2017 年 Transformer 以"完全抛弃递归、全靠注意力并行"取代了它;而 2023 年前后的 Mamba(选择性状态空间模型,S6)与 RWKV 又把"固定大小的循环状态 + 选择性门控"请了回来,配合硬件友好的扫描算法,在超长序列上重新与 Transformer 分庭抗礼,也催生了注意力与循环层混用的混合架构。用本讲的概念看 Mamba:它的状态更新同样是 \(h_t = f(h_{t-1}, x_t)\),只不过"门"由输入自适应生成——这正是 LSTM 精神的一次转世。理解了 RNN、LSTM 与注意力(下一讲),你就有了解读这一切新架构的完整词汇表。

十三、本讲要点回顾

上一讲第 6 讲:CNN 架构 下一讲第 8 讲:注意力机制与 Transformer