从 RNN、LSTM、GRU 到 ELMo:循环神经网络如何学会记忆上下文

刚学完机器学习时,我们接触的通常是这样的数据:给模型一套房子的面积、楼层、位置,让它预测房价;或者给模型一封邮件的若干特征,让它判断是不是垃圾邮件。

这些样本通常可以一条一条独立处理。但自然语言不一样,一个词是什么意思,经常要看它前后出现了什么。

例如下面两句话都有“苹果”:

  • 我吃了一个苹果。
  • 我买了一台苹果电脑。

只看“苹果”两个字,计算机无法判断它是水果还是公司。人类觉得很简单,是因为我们读到“吃”“电脑”时,会把上下文一起考虑进去。

RNN、LSTM 和 GRU 要解决的,就是怎样让神经网络拥有这种“读到后面,还能记得前面”的能力。ELMo 又向前走了一步:它让同一个词在不同上下文中得到不同的向量表示。

本文按照下面的顺序展开:

flowchart LR A["RNN
让信息沿句子传递"] --> B["LSTM
学会管理长期记忆"] B --> C["GRU
用更简单的结构做门控"] C --> D["ELMo
生成随上下文变化的词向量"]

文章中的实验都可以在配套 Notebook 中直接运行:

下载 RNN、LSTM、GRU 与 ELMo 实验 Notebook

一、RNN:让神经网络一边阅读,一边做笔记

1. 普通神经网络缺少什么?

最普通的多层感知机(MLP)没有内置的时序记忆。如果把一句话拆成多个词,模型在处理当前词时,并不会自动知道前面出现过什么。

RNN(Recurrent Neural Network,循环神经网络)增加了一份“随身笔记”:

  1. 读当前词;
  2. 看一眼上一步留下的笔记;
  3. 把两者合起来,写出一份新笔记;
  4. 把新笔记传给下一个词。

这份笔记叫做隐藏状态,通常写成 $h_t$。其中的 $t$ 表示当前读到了第几个词。

flowchart LR X1["词 1"] --> R1["RNN"] R1 -- "笔记 h₁" --> R2["RNN"] X2["词 2"] --> R2 R2 -- "笔记 h₂" --> R3["RNN"] X3["词 3"] --> R3 R3 -- "笔记 h₃" --> N["继续向后"]

图中虽然画了多个 RNN 方框,但它们其实是同一个单元反复使用。处理第一个词和处理第三十个词时,模型使用的是同一套参数。这叫做参数共享。

它有两个好处:

  • 句子变长时,模型的参数数量不会跟着增长;
  • 在句首学到的处理方法,也能用在句尾。

2. RNN 只保留一条公式就够了

RNN 最核心的计算只有一条:

$$ h_t=\tanh(W_xx_t+W_hh_{t-1}+b) $$

第一次看到这条公式,不必急着研究矩阵形状。先把它翻译成人话:

新笔记 = 处理一下(当前词 + 上一步笔记)

公式里的符号只是在把这句话写得更准确:

  • $x_t$:当前读到的词;
  • $h_{t-1}$:上一步留下的笔记;
  • $W_x$、$W_h$:模型通过训练学到的处理方法;
  • $\tanh$:把数值限制在一个稳定范围内;
  • $h_t$:更新后的笔记。

理解到这里已经足够继续往下读。真正重要的是:当前状态不是只由当前词决定,它还带着前面的信息。

3. 这些隐藏状态可以怎么用?

RNN 每读一个词都会产生一份隐藏状态。不同任务会选择不同的使用方式。

任务怎样使用隐藏状态例子
给每个词分类每个位置的状态各自分类词性标注、命名实体识别
给整句话分类使用最后一个状态统一分类情感分析、垃圾邮件识别
预测下一个词用当前状态预测后面的词文本生成、输入法联想

例如做情感分析时,RNN 读完“这部电影一点也不好看”,最后一份笔记可以看成对整句话的压缩,再交给分类器判断正面还是负面。

4. Jupyter 实验:亲眼看看隐藏状态在变化

先准备两句话,每句话 5 个词,然后分别交给 PyTorch 中的 RNN、LSTM 和 GRU:

layers = {
    "RNN": nn.RNN(5, 6, batch_first=True),
    "LSTM": nn.LSTM(5, 6, batch_first=True),
    "GRU": nn.GRU(5, 6, batch_first=True),
}

for name, layer in layers.items():
    output, state = layer(embedded)
    print(name, output.shape)

运行后得到:

RNN : output=(2, 5, 6), h_n=(1, 2, 6)
LSTM: output=(2, 5, 6), h_n=(1, 2, 6), c_n=(1, 2, 6)
GRU : output=(2, 5, 6), h_n=(1, 2, 6)

暂时不用记住这些形状,只需要知道:

  • 2 表示一次输入了两句话;
  • 5 表示每句话有 5 个词;
  • 6 表示模型用 6 个数字保存当前状态;
  • LSTM 比另外两个模型多返回了一份 c_n,后面会解释它是什么。

把第一句话每个位置的隐藏状态画出来,可以看到模型每读一个词,6 个数都会发生变化:

RNN 隐藏状态随时间步变化

这个网络还没有训练,所以图中某条线并不代表固定的“情感”或“语法”。它只是直观地说明:RNN 读完一个词会更新状态,然后带着新状态继续阅读。

二、普通 RNN 为什么容易忘记很早以前的内容?

考虑下面这句话:

我去年在商场里反复比较了很久以后买下的那台笔记本电脑,现在仍然很好用。

读到“很好用”时,模型要知道被评价的是前面的“笔记本电脑”。中间隔得越远,模型越难保留这个信息。

可以把普通 RNN 想成一个不断改写的便签。每读一个词,它都要把“新词”和“旧便签”混在一起,重新写一张便签。改写次数太多以后,最早的内容就可能越来越模糊。

梯度消失,用一句话怎么理解?

模型训练时,最后的错误需要反向传回前面的每一步,告诉模型“当时应该怎样记”。这份纠错信号叫做梯度。

如果信号每向前传一步都只剩原来的八成,那么传 20 次以后就只剩一点点。距离太远时,开头几步几乎收不到纠错信号,模型自然很难学会“把这个信息一直记到最后”。这就是梯度消失最直观的含义。

反过来,如果信号每一步都被放大,也可能越来越大,形成梯度爆炸。梯度裁剪可以限制爆炸,但不能从根本上解决“记不久”的问题。

Jupyter 实验:第一个词还能收到多少学习信号?

Notebook 中放入一个 40 个词的序列,再让最后的分类结果对每个位置求梯度。这个过程由 PyTorch 自动完成,不需要我们手算微分。

embedded = model.embedding(sequence).detach().requires_grad_()
score = model.forward_from_embeddings(embedded)[0, 1]
score.backward()

gradient = embedded.grad.norm(dim=2)[0]

结果如下:

RNN、LSTM 与 GRU 的梯度传播对比

横轴越靠左,表示这个词离最终输出越远。纵轴使用对数刻度,每差一大格往往就是十倍甚至百倍的差距。

在这次固定初始化中:

  • RNN 传到第一个词的信号已经接近 0;
  • GRU 比 RNN 保留得更多;
  • LSTM 的远距离信号最平稳。

这不代表任何情况下 LSTM 都一定最好。它说明的是:门控结构为长期信息提供了一条更容易通过的道路。 接下来就看看这条道路是怎么设计的。

三、LSTM:像管理笔记本一样管理长期记忆

LSTM 的全称是 Long Short-Term Memory,中文通常译为长短期记忆网络。它仍然从左到右逐步处理序列,外表和 RNN 很像,主要变化发生在单元内部。

普通 RNN 只有一份隐藏状态。LSTM 增加了一条专门保存长期信息的细胞状态:

  • 隐藏状态 $h$:像当前放在桌面上的内容,对外提供输出;
  • 细胞状态 $c$:像一本持续向后传递的笔记本,保存更长期的信息。

为了管理这本笔记,LSTM 设计了三个“门”。门并不是物理零件,而是一组 0 到 1 之间的数字,用来控制信息通过多少。

1. 遗忘门:旧内容还要不要?

遗忘门回答的是:

上一步笔记中的哪些内容应该继续保留?

例如模型正在阅读一段人物介绍,原来讨论的是“张三”,后来话题转到了“李四”。这时,与旧人物相关的一些信息就可以逐渐忘掉。

遗忘门不是一个总开关,而是可以分别控制每个维度。例如:

[0.95, 0.10, 0.72, ...]

可以粗略理解为:第一类信息保留 95%,第二类只保留 10%,第三类保留 72%。模型不需要把整份记忆一起清空,而是可以有选择地遗忘。

2. 输入门:新内容要写多少?

输入门回答的是:

当前看到的新信息,有多少值得写进长期笔记?

模型会先整理出一份“候选新内容”,再由输入门决定每一部分写入多少。路过的修饰词可能只写一点,新的主语、否定词或关键实体则可能写得更多。

所以 LSTM 更新长期记忆的过程,用一句话就能概括:

新长期记忆 = 保留下来的旧内容 + 筛选后写入的新内容

这就是整套 LSTM 中最值得记住的逻辑。

3. 输出门:现在需要拿出什么?

笔记本中可以保存很多内容,但当前任务不一定全部需要。

输出门负责从长期记忆中挑选当前要展示的部分,形成隐藏状态 $h$。例如模型可能同时记得人物、时间、地点和情感,但在判断情感时,更需要把与态度相关的信息提供给分类器。

可以把三个门连起来记:

遗忘门:擦掉什么
输入门:写入什么
输出门:拿出什么

4. 为什么说门是“向量”?

假设细胞状态由 128 个数字组成,那么遗忘门、输入门和输出门也各有 128 个数字。每一个数字都在 0 到 1 之间,分别控制对应的那一维信息。

这和我们之后会接触的基础 Attention 示意有所不同:

  • LSTM 门控更像在问:“这份状态的每个维度保留多少?”
  • Attention 更像在问:“这么多个词的位置中,我现在更应该看谁?”

现代 Attention 还有多头等更复杂的设计,但对于初学阶段,先记住“门控主要筛选状态维度,Attention 主要选择位置”就够了。

5. 遗忘门和输入门为什么互不补偿?

LSTM 的遗忘门和输入门是独立判断的,并不要求一个增加时另一个必须减少。因此某一维可能出现四种情况:

旧内容新内容直观含义
保留多写入少主要沿用过去
保留少写入多主要换成新信息
都少这一维暂时几乎不保存信息
都多保留旧内容的同时继续补充

独立控制让 LSTM 很灵活,但也使它的结构和参数比较多。后面的 GRU 会把其中一部分控制合并起来。

6. LSTM 为什么更容易记得久?

关键不在于它“永远不会忘”,而在于它给长期记忆开辟了一条更平稳的通道。只要遗忘门判断某段信息仍然重要,就可以让它在细胞状态中继续向后传递,不必每一步都把所有内容彻底重算。

这也是 LSTM 能缓解长期依赖问题的根本原因。

至于输入是 256 维还是 128 维、输出要不要再做投影,都属于工程上的尺寸调整。它们不会改变“擦除、写入、输出”这套核心逻辑,初学时不用在这里停留太久。

四、GRU:用两个门完成更紧凑的记忆管理

GRU(Gated Recurrent Unit,门控循环单元)可以看成对 LSTM 的简化。

LSTM 同时维护细胞状态和隐藏状态,GRU 则只保留一份状态;LSTM 有三个门,GRU 主要使用两个门:

  • 重置门:生成新内容时,要参考多少过去的信息;
  • 更新门:最后要保留多少旧状态、采用多少新状态。

1. 重置门:需不需要参考过去?

当重置门接近 0 时,GRU 在准备新状态时会较少参考过去,更像重新开始;当它接近 1 时,就会充分利用旧信息。

例如句子进入一个全新话题时,减少对过去状态的依赖可能更合适。

2. 更新门:旧状态和新状态按比例混合

GRU 最有特点的地方,是旧状态与候选新状态使用互补比例。

如果某一维决定保留 80% 的旧信息,那么新信息就使用剩下的 20%。可以把它想成调果汁:杯子的总容量固定,旧果汁多一些,新果汁就只能少一些。

这与 LSTM 的遗忘门、输入门彼此独立不同。GRU 的做法更紧凑,也避免了同一维中“旧的和新的都大量写入”这种情况。

有些教材把更新门定义成“新信息比例”,另一些教材把它定义成“旧信息比例”,所以公式中两边可能刚好对调。遇到这种情况不用怀疑自己,只要先看作者如何定义更新门即可。

3. LSTM 和 GRU 怎么选?

对比项LSTMGRU
状态长期状态和隐藏状态两份只有一份状态
门遗忘、输入、输出三个门重置、更新两个门
新旧信息分别独立控制使用互补比例
参数量通常更多通常更少
直观特点控制更细结构更紧凑

GRU 通常参数更少、训练更快,在很多任务上能获得和 LSTM 接近的效果。但不存在“GRU 一定更好”或者“LSTM 一定更准确”的规则,真正的选择仍然要靠实验。

4. Jupyter 实验:让模型记住 40 步以前的第一个词

为了观察长期记忆,我们设计一个很简单的任务。每条序列有 40 个词:第一个词是“红”或“蓝”,后面 39 个都是无关词。

红  无关词  无关词  ……  无关词
蓝  无关词  无关词  ……  无关词

模型只能根据最后的状态回答:第一个词究竟是什么?

记住第一个词任务的训练曲线

50% 左右相当于随机猜测。在这次固定随机种子下:

  • LSTM 和 GRU 很快学会了任务;
  • 普通 RNN 在随机猜测附近停留了更久,到后面才突然学会。

这与前面的梯度图能够对应起来:门控模型更容易把第一个词的信息保留到最后,也更容易把最后的纠错信号传回开头。

不过,这只是帮助理解的教学实验。换一个随机种子、增加干扰词或改变隐藏层大小,曲线都可能变化。它能说明门控结构的优势,但不能作为正式的模型排名。

五、ELMo:让“苹果”在不同句子里变成不同向量

RNN、LSTM 和 GRU 解决的是怎样处理序列。ELMo 则把循环网络产生的状态拿来表示词,从而解决静态词向量的一项重要缺陷。

1. 静态词向量为什么不够?

Word2Vec、GloVe 等方法会在词表中为每个词保存一个固定向量。训练结束以后,无论“苹果”出现在哪句话中,查到的都是同一行数字。

这相当于把一个词的多种含义混合在一起:

我吃了一个苹果       → 苹果是水果
苹果发布了一台电脑   → 苹果是公司

人类会根据上下文自动切换含义,静态词向量却做不到。代词也有类似问题:“他”到底指谁,只看这个词本身无法判断。

2. ELMo 的核心想法

ELMo 的想法可以用一句话概括:

词表中查到的向量只作为输入,真正交给下游任务的,是语言模型读完上下文以后产生的状态。

这样一来,同一个“苹果”经过不同上下文的影响,就会得到不同表示。这种表示叫做上下文化词表示,也常被通俗地称为动态词向量。

3. 为什么要使用双向 LSTM?

只从左向右读时,模型在处理当前词时只能看到前文。但理解一个词有时也需要后文。

例如看到“苹果发布”时,后面的“电脑”能帮助模型确认这里说的是公司。因此标准 ELMo 使用两层双向 LSTM:

  • 前向 LSTM 从左向右读,利用左侧内容;
  • 反向 LSTM 从右向左读,利用右侧内容。
flowchart LR A["字符级 CNN
得到初始词表示"] --> F1["第 1 层前向 LSTM →"] A --> B1["第 1 层反向 LSTM ←"] F1 --> F2["第 2 层前向 LSTM →"] B1 --> B2["第 2 层反向 LSTM ←"] F2 --> C["上下文化词表示"] B2 --> C

标准 ELMo 的最底层使用字符级 CNN。初学时可以先把它理解成“根据词的字符组成得到初始向量”,重点仍然是后面的双向 LSTM。

4. 训练时为什么不能提前合并两个方向?

前向语言模型做的事情是“根据前文猜下一个词”:

a, b       → 猜 c
a, b, c    → 猜 d

反向语言模型正好相反,它根据后文猜前一个词:

e          → 猜 d
d, e       → 猜 c

两个方向可以一起训练,但在预测目标词时不能提前合并信息。

例如用 a, b 猜 c,如果同时拿到从右向左读出的状态,这份状态很可能已经看过 c。模型就像考试时提前看到了答案,这叫做信息泄漏。

所以要区分两个阶段:

训练语言模型:两个方向各自遵守预测方向,不能偷看答案
用于下游任务:整句话已经给出,可以合并两个方向的表示

5. ELMo 为什么还要组合不同层?

双向 LSTM 的不同层往往会学到不同层次的信息。较低层可能更关注词形和句法,较高层可能包含更多语义。

情感分析、问答、序列标注需要的信息并不完全相同,因此 ELMo 不强迫所有任务只使用最后一层。它让每个下游任务自己学习:输入层、第一层和第二层分别应该占多大比例。

可以把它理解成调音台:不同任务会把不同层的音量调高或调低,再组合成最终表示。

6. Jupyter 实验:训练一个迷你版动态词向量

Notebook 使用 16 个短句训练了一个很小的双向语言模型。语料中既有水果含义的“苹果”:

我 喜欢 吃 苹果 很 甜
今天 买 的 苹果 很 甜

也有公司含义的“苹果”:

我 使用 苹果 电脑 工作
苹果 公司 发布 新 手机

如果只是从词嵌入表中查找,“苹果”无论出现在哪里都得到同一行数字,两个向量完全相同。

经过前向和反向 LSTM 编码后,同一个词会受到左右上下文影响,于是得到不同向量:

迷你 ELMo 实验中的上下文化词向量

图中每一格表示两个“苹果”向量有多相似。对角线都是同一个向量与自己比较,所以等于 1;其他位置则会随句子变化。

这个实验的语料非常小,不能把具体数值当成真正的语义评测,更不能据此判断模型已经准确学会了水果和公司的概念。它只验证一个核心现象:

静态词向量只由“这个词是谁”决定,动态词向量还由“这个词出现在什么上下文中”决定。

7. 为什么说 ELMo 有些“生不逢时”?

Transformer 在 2017 年提出,ELMo、GPT 和 BERT 则先后在 2018 年出现。ELMo 很早就证明了上下文化表示和大规模预训练的价值,是自然语言处理发展中的重要一步。

但 ELMo 的主体仍然是 LSTM,必须一个时间步接一个时间步地计算,不容易大规模并行。基于 Transformer 的 GPT 和 BERT 更适合并行训练,也更容易直接连接距离很远的词,后来逐渐成为预训练语言模型的主流。

所以说 ELMo“生不逢时”,不是说它不重要,而是它刚展示出动态词向量的价值,底层架构就迎来了更适合大规模预训练的 Transformer。

六、把四个模型放在一起理解

模型它解决了什么问题最值得记住的做法主要局限
RNN让模型利用前面的信息把隐藏状态传给下一步长序列中容易忘记早期内容
LSTM更好地学习长期依赖用细胞状态和三个门管理记忆参数较多,仍要顺序计算
GRU简化门控循环网络用两个门和一份状态完成控制控制方式比 LSTM 更紧凑
ELMo让词向量随上下文变化使用双向 LSTM 生成词表示LSTM 主体不容易并行

把它们连起来看,演进逻辑非常清楚:

  1. 普通 MLP 没有时序记忆,于是 RNN 把上一步状态传给下一步;
  2. RNN 容易忘记很早的信息,于是 LSTM 增加长期状态和门控;
  3. LSTM 结构比较复杂,于是 GRU 用两个门进行简化;
  4. 静态词向量无法区分多义词,于是 ELMo 用双向 LSTM 生成动态表示;
  5. 循环网络不容易并行,于是预训练语言模型的主线继续转向 Transformer。

七、初学阶段最后记住这些就够了

  • RNN 的本质,是一边读当前输入,一边把过去的隐藏状态带过来。
  • 普通 RNN 不是完全没有记忆,而是距离太远时很难学会应该保留什么。
  • LSTM 的核心不是一堆公式,而是“忘掉什么、写入什么、输出什么”。
  • LSTM 的门是 0 到 1 的向量,可以分别控制状态的每一个维度。
  • GRU 把结构做得更紧凑,用互补比例混合旧状态和候选新状态。
  • ELMo 的关键贡献,是让同一个词根据上下文获得不同表示。
  • 实验曲线可以帮助理解现象,但一次小实验不能证明某个模型永远更好。

不必急着背四套公式。后面真正使用 PyTorch 搭模型时,再回头看每个参数和张量形状,理解成本会低很多。