第 5 讲:语言模型与循环神经网络

Natural Language Processing with Deep Learning · Lecture 05。个人学习讲义与配套实验,按主题整理并持续修订。

课程源码 ZIP ↓

个人整理的学习笔记,非官方讲义;课程来源与整理说明见课程介绍。配套示例未逐一运行验证。

本篇目录

对应:Stanford CS224N Winter 2026 Lecture 4 前半(Language Models and RNNs);Winter 2025 Lecture 5(Basic Sequence Models to RNNs);Manning 主讲的 Winter 2021 / 2023 版第 5 讲(Language Models and Recurrent Neural Networks),录像在 YouTube 上。 配套代码:code/lecture05/ 预计阅读时间:4–5 小时(第一遍通读 2 小时,第二遍推导 BPTT、对照代码 2–3 小时)


0. 本讲导航

0.1 学完本讲,你应该能够

  1. 给出语言模型的定义,用链式法则把句子的概率拆成”预测下一个词”的乘积,并说明语言模型为什么重要。
  2. 解释 n-gram 语言模型的马尔可夫假设、最大似然估计,以及它的两个根本问题:稀疏和存储;说明加 k 平滑、插值的作用。
  3. 定义困惑度,解释它和交叉熵的关系与直观含义。
  4. 描述固定窗口的神经语言模型,说明它解决了什么、没解决什么。
  5. 写出循环神经网络(RNN)语言模型的公式,说出 RNN 的优点和缺点。
  6. 推导时间反向传播(BPTT):为什么”重复使用的参数的梯度是每次使用的梯度之和”。
  7. 解释 teacher forcing、填充与批处理、梯度裁剪,以及几种解码策略(贪心、温度、top-k、top-p)。
  8. 用主谓一致实验说明 n-gram 和 RNN 在长距离依赖上的差别,并初步理解梯度消失的后果。

0.2 先修知识

知识点用在哪里最低要求
第 1、3 讲全讲softmax、交叉熵、反向传播、“梯度要累加”、PyTorch 训练循环
第 4 讲第 4 节固定窗口特征的局限(神经依存分析器只看栈和缓冲区的前几个位置)
条件概率第 1、2 节P(A,B)=P(A) P(B∣A)P(A, B) = P(A)\, P(B \mid A)
对数与指数第 3 节log⁡∏=∑log⁡\log \prod = \sum \log,elog⁡x=xe^{\log x} = x

0.3 本讲地图

第1节  什么是语言模型?
第2节  n-gram 语言模型:计数、稀疏、平滑                ← 代码 01
第3节  评估:困惑度                                     ← 代码 01
第4节  固定窗口的神经语言模型                           ← 代码 02
第5节  循环神经网络                                     ← 代码 03、04(本讲重点)
第6节  训练 RNN:时间反向传播(BPTT)                   ← 代码 03、04
第7节  从语言模型生成文本:解码策略                     ← 代码 05
第8节  RNN 能学会长距离依赖吗?—— 主谓一致实验          ← 代码 01、02、04
第9节  RNN 的其他用途
第10–14节 小结、与作业的关系、练习、延伸阅读、下一讲预告

本讲的主线是一条熟悉的”发现问题—改进”链:

n-gram⏟稀疏、存储、窗口有限  →  固定窗口神经网络⏟解决了稀疏,窗口仍有限  →  RNN⏟任意长度、参数共享;但梯度会消失  →  LSTM(第 6 讲)⏟缓解梯度消失\underbrace{\text{n-gram}}_{\text{稀疏、存储、窗口有限}} \;\to\; \underbrace{\text{固定窗口神经网络}}_{\text{解决了稀疏,窗口仍有限}} \;\to\; \underbrace{\text{RNN}}_{\text{任意长度、参数共享;但梯度会消失}} \;\to\; \underbrace{\text{LSTM(第 6 讲)}}_{\text{缓解梯度消失}}

1. 什么是语言模型?

1.1 定义

语言模型(language model,LM)做的事情是预测下一个词:给定前面的词 x(1),…,x(t)x^{(1)}, \dots, x^{(t)},计算下一个词 x(t+1)x^{(t+1)} 在词表 VV 上的概率分布

P(x(t+1)∣x(t),…,x(1))P\big(x^{(t+1)} \mid x^{(t)}, \dots, x^{(1)}\big)

等价地,语言模型给任意一段文本赋予一个概率。根据概率的链式法则:

P(x(1),…,x(T))=P(x(1))×P(x(2)∣x(1))×⋯×P(x(T)∣x(T−1),…,x(1))=∏t=1TP(x(t)∣x(t−1),…,x(1))P\big(x^{(1)}, \dots, x^{(T)}\big) = P\big(x^{(1)}\big) \times P\big(x^{(2)} \mid x^{(1)}\big) \times \cdots \times P\big(x^{(T)} \mid x^{(T-1)}, \dots, x^{(1)}\big) = \prod_{t=1}^{T} P\big(x^{(t)} \mid x^{(t-1)}, \dots, x^{(1)}\big)

所以”给句子打分”和”预测下一个词”是一回事:会预测下一个词,就能算出整个句子的概率;反之亦然。

1.2 为什么语言模型重要?

你每天都在用语言模型:手机输入法的联想、搜索框的自动补全、邮件的智能回复……在深度学习之前,语言模型就已经是很多系统的关键组件:

  • 语音识别:声学模型给出几个读音相近的候选(“识别语音” vs “石壁鱼音”),语言模型判断哪个更像正常的句子;
  • 机器翻译:在多个候选译文中挑选最通顺的;
  • 拼写纠错、手写识别、文本输入……

更深刻的一点是:预测下一个词是一个极其通用的任务。要准确地预测”巴黎是法国的___“,模型需要知识;要预测”我把钥匙忘在家里了,所以我只好___“,模型需要常识和推理;要预测长句子末尾的动词形式,模型需要句法。所以语言模型一直被看作衡量语言理解能力的基准任务。今天的大语言模型(GPT 系列等)本质上就是规模极大的语言模型(第 9 讲)。


2. n-gram 语言模型

2.1 马尔可夫假设与计数

怎样估计 P(x(t+1)∣x(t),…,x(1))P(x^{(t+1)} \mid x^{(t)}, \dots, x^{(1)})?前文可以任意长,完全相同的前文在语料中几乎不会重复出现,没法直接估计。

深度学习之前的标准答案是 n-gram 模型。n-gram 指连续 nn 个词组成的片段(1-gram 是单个词,2-gram 是两个词……)。n-gram 模型做了一个简化,叫马尔可夫假设:下一个词只依赖于前面 n−1n - 1 个词。

P(x(t+1)∣x(t),…,x(1))≈P(x(t+1)∣x(t),…,x(t−n+2))P\big(x^{(t+1)} \mid x^{(t)}, \dots, x^{(1)}\big) \approx P\big(x^{(t+1)} \mid x^{(t)}, \dots, x^{(t-n+2)}\big)

然后用计数来估计(最大似然估计,MLE):

P(w∣x(t),…,x(t−n+2))=count(x(t−n+2),…,x(t),w)count(x(t−n+2),…,x(t))P\big(w \mid x^{(t)}, \dots, x^{(t-n+2)}\big) = \frac{\text{count}\big(x^{(t-n+2)}, \dots, x^{(t)}, w\big)}{\text{count}\big(x^{(t-n+2)}, \dots, x^{(t)}\big)}

例子(Manning 课件):“as the proctor started the clock, the students opened their ___“。4-gram 模型只看最后 3 个词 “students opened their”。如果语料中 “students opened their” 出现了 1000 次,其中 “students opened their books” 400 次、“students opened their exams” 100 次,那么 P(books∣…)=0.4P(\text{books} \mid \ldots) = 0.4、P(exams∣…)=0.1P(\text{exams} \mid \ldots) = 0.1。可是被丢掉的前文 “proctor”(监考员)明明强烈暗示应该是 exams!这就是马尔可夫假设的代价。

动手:代码 01_ngram_lm.py 使用一门玩具语言(toy_language.py):75 个词,带有可控的长距离主谓一致(第 8 节详细介绍)。训练集 20,000 句、开发集和测试集各 2,000 句。演示 1:上下文 “the dog” 在训练集中出现了 1312 次,后面跟 that 311 次(P=0.237P = 0.237)、near 254 次(0.194)、is 110 次(0.084)……句子 “the dog runs .” 在三元模型下的概率是 1.0×0.0360×0.0343×0.6714×1.0≈0.000831.0 \times 0.0360 \times 0.0343 \times 0.6714 \times 1.0 \approx 0.00083。

(句子开头用两个 <s> 填充,结尾加一个 </s>,这样”句子在这里结束”也是一个要预测的事件。)

2.2 两个根本问题:稀疏和存储

稀疏:nn 越大,模型越”细致”,但很多 nn-gram 在训练语料中从没出现过。

  • 分子为 0:测试时遇到没见过的 nn-gram,最大似然估计给它概率 0。只要测试集里有一个这样的 nn-gram,整个测试集的概率就是 0。
  • 分母为 0:连上下文都没见过,概率根本无法计算。

动手:代码 01 演示 2:

nn不同 nn-gram 的种数测试集中没见过的比例最大似然的困惑度
1760.00%31.76
21,1680.00%5.97
37,9390.89%∞
420,1783.45%∞
544,31511.53%∞

我们的玩具语言只有 75 个词、结构非常规则,即便如此,5-gram 中也有超过 11% 在训练集中没见过。真实语言(几十万词表、无穷的句式)中这个问题严重得多。

存储:要存所有见过的 nn-gram 的计数。nn 每加 1,需要存的计数就多很多(代码 01 演示 6:从 1-gram 的 76 个到 5-gram 的 44,315 个)。在几十亿词的真实语料上,5-gram 模型要存几十亿个计数。

Manning 的总结是:nn 太小,模型看不到足够的上下文;nn 太大,稀疏问题让模型失效。实践中 nn 很少超过 5。

2.3 平滑与插值

加 k 平滑:给每个可能的词都”假装”多见过 kk 次。

Padd-k(w∣h)=count(h,w)+kcount(h)+k∣V∣P_{\text{add-}k}(w \mid h) = \frac{\text{count}(h, w) + k}{\text{count}(h) + k |V|}

k=1k = 1 叫加一平滑(拉普拉斯平滑)。这样没见过的 nn-gram 也有一点概率,代价是从见过的事件那里”挪走”了一些概率。

回退(backoff):如果 “students opened their” 从没出现过,就退一步用 “opened their”;还没有,再退到 “their”。

插值(interpolation):把不同阶的模型按权重混合:

Pinterp(w∣h)=λ01∣V∣+λ1P1(w)+λ2P2(w∣前 1 个词)+⋯+λnPn(w∣前 n−1 个词)P_{\text{interp}}(w \mid h) = \lambda_0 \frac{1}{|V|} + \lambda_1 P_1(w) + \lambda_2 P_2(w \mid \text{前 1 个词}) + \cdots + \lambda_n P_n(w \mid \text{前 } n-1 \text{ 个词})

高阶模型有信息时它起作用,没信息(概率为 0)时由低阶模型兜底。权重 λ\lambda 要在开发集上估计(在训练集上估计的话,最高阶永远”最好”),常用 EM 算法:反复地”对每个词计算它由哪个子模型生成的后验概率,再用平均值更新 λ\lambda”。

更精巧的方法是 Kneser–Ney 平滑,它是 n-gram 时代效果最好的方法。它的一个关键直觉是:低阶模型不应该用”词出现了多少次”,而应该用”这个词跟在多少种不同的词后面出现过”。例如 Francisco 出现得很频繁,但几乎只出现在 San 后面;当高阶模型没有信息、需要回退时,Francisco 就不该得到很高的概率。

动手:代码 01 演示 3(测试集困惑度,参数在开发集上选):

nn加 1加 0.1加 0.01插值(EM)
131.7631.7631.7631.78
26.115.985.975.97
36.425.425.345.26
48.705.785.314.91

加 1 平滑分给”没见过的事件”的概率太多了,nn 越大越糟;kk 取小一些好得多。插值最好:4-gram 插值模型的权重是 λ2=0.09\lambda_2 = 0.09、λ3=0.23\lambda_3 = 0.23、λ4=0.68\lambda_4 = 0.68——主要依靠 4-gram,3-gram、2-gram 兜底。(作为参照,对所有词一视同仁的模型困惑度等于词表大小 76。)

2.4 从 n-gram 模型生成文本

语言模型还可以用来生成文本:从 <s> 开始,按模型给出的概率分布采样下一个词,接到后面,再采样下一个……直到生成 </s>。

动手:代码 01 演示 4 从不同阶数的模型中采样(以下是实际生成的句子):

1-gram:like the boys the quickly . that house the students . near the birds dogs the
2-gram:the kings near the farmer sings .
3-gram:the tall teachers that the teacher sees runs .
4-gram:the cats that like the birds help smiles .

1-gram 是一堆随机词;阶数越高,局部越通顺。但稍长一点的句子就会出问题:2-gram 的 “the kings near the farmer sings”、3-gram 的 “the tall teachers that the teacher sees runs” 都是复数主语配单数动词——模型被最近的单数名词(farmer、teacher)“带偏”了,真正的主语早已跑出了它的视野。4-gram 的 “the cats that like the birds help smiles” 则连结构都乱了。

Manning 在课上展示过一个在真实新闻语料上训练的 trigram 模型生成的文本:读起来每三五个词都很通顺,但整体完全不知所云。要生成连贯的文本,模型需要考虑多得多的上下文。


3. 评估:困惑度

怎样衡量一个语言模型好不好?标准指标是困惑度(perplexity):

perplexity=∏t=1T(1PLM(x(t+1)∣x(t),…,x(1)))1/T\text{perplexity} = \prod_{t=1}^{T} \left(\frac{1}{P_{\text{LM}}\big(x^{(t+1)} \mid x^{(t)}, \dots, x^{(1)}\big)}\right)^{1/T}

也就是测试集概率的倒数,再按词数开 TT 次方根(几何平均)。取对数就能看出它和交叉熵的关系:

perplexity=exp⁡(1T∑t=1T−log⁡PLM(x(t+1)∣x(t),…,x(1)))=exp⁡(J(θ))\text{perplexity} = \exp\left(\frac{1}{T} \sum_{t=1}^{T} -\log P_{\text{LM}}\big(x^{(t+1)} \mid x^{(t)}, \dots, x^{(1)}\big)\right) = \exp(J(\theta))

困惑度 = exp(平均交叉熵损失)。训练时最小化交叉熵,就是在最小化困惑度。

直观含义:困惑度可以理解为”模型在每一步平均要在多少个词之间犹豫”(有效分支数)。

  • 一个对所有词一视同仁的模型,困惑度等于词表大小 ∣V∣|V|;
  • 一个完美的模型(每一步都以概率 1 预测对),困惑度为 1;
  • 困惑度为 5 的模型,在每一步的不确定性相当于”在 5 个等可能的词之间选一个”。

越低越好。在真实的英文基准上,困惑度随着方法的进步大幅下降:在一个十亿词规模的基准上,插值 Kneser–Ney 5-gram 的困惑度大约是 68,大型 LSTM 语言模型降到了 30 左右,今天的 Transformer 语言模型更低。

一个重要的注意事项:困惑度只有在相同的词表、相同的分词方式、相同的测试集下才能比较。字符级模型和词级模型的困惑度不能直接比较(预测一个字符和预测一个词的难度完全不同);子词模型之间比较时,也常换算成”每字节比特数”(bits per byte)。


4. 固定窗口的神经语言模型

4.1 Bengio 等人(2003)的模型

n-gram 的稀疏问题,根源是”词是孤立的符号”——又是第 1 讲的老问题。Bengio 等人(2003)的解决办法和第 3 讲的窗口分类器一模一样:

e=[ e(t−3); e(t−2); e(t−1); e(t) ]窗口内词向量的拼接h=f(We+b1)隐藏层y^=softmax(Uh+b2)∈R∣V∣下一个词的概率分布\begin{aligned} e &= [\, e^{(t-3)};\ e^{(t-2)};\ e^{(t-1)};\ e^{(t)} \,] &&\text{窗口内词向量的拼接} \\ h &= f(W e + b_1) &&\text{隐藏层} \\ \hat{y} &= \text{softmax}(U h + b_2) \in \mathbb{R}^{|V|} &&\text{下一个词的概率分布} \end{aligned}

它相对于 n-gram 的改进:

  • 没有稀疏问题:任何窗口都能算出一个概率分布,不会有”没见过就是 0”;
  • 不用存计数:只需要存固定数量的参数;
  • 能泛化:相似的词有相似的向量,“the dog runs”学到的东西能迁移到”the cat runs”。

这篇论文在 word2vec 之前十年就提出了”同时学习词向量和语言模型”,是神经语言模型的开山之作。

4.2 仍然存在的问题

  • 窗口仍然是固定的,窗口外的信息完全看不到;
  • 窗口越大,WW 越大:WW 的列数是 窗口大小 × 词向量维度;
  • 不同位置的权重互不共享:WW 中乘以 e(t−3)e^{(t-3)} 的那一块和乘以 e(t)e^{(t)} 的那一块是不同的参数。模型要分别学习”前 1 个词是 dog 意味着什么”和”前 4 个词是 dog 意味着什么”,学习效率很低。

动手:代码 02_fixed_window_lm.py 训练了一个窗口大小为 4 的神经语言模型(词向量 32 维,隐藏层 128,约 2.9 万个参数)。窗口 4 意味着看前 4 个词,相当于一个 5-gram 模型:

模型测试集困惑度存储
4-gram(插值)4.91
5-gram(插值)4.9273,676 个计数
固定窗口神经模型(窗口 4)4.5529,231 个参数

神经模型用更少的存储得到了更低的困惑度。第 8 节会看到它在长距离依赖上的表现。


5. 循环神经网络

5.1 核心想法

我们想要一个能处理任意长度输入、并且在所有位置共享参数的结构。循环神经网络(Recurrent Neural Network,RNN)的想法是:用同一组权重,一步一步地反复处理。

它维护一个隐藏状态 h(t)h^{(t)},作为”到目前为止读过的所有内容的总结”。每读入一个新词,就用同一个公式更新隐藏状态:

h(t)=σ(Whh(t−1)+Wee(t)+b1)h^{(t)} = \sigma\big(W_h h^{(t-1)} + W_e e^{(t)} + b_1\big)

其中 e(t)=E x(t)e^{(t)} = E\, x^{(t)} 是第 tt 个词的词向量,σ\sigma 是非线性函数(通常是 tanh),h(0)h^{(0)} 是初始隐藏状态(通常全为 0)。“循环”指的是 h(t)h^{(t)} 依赖于 h(t−1)h^{(t-1)}:同一个网络被反复应用,输出又作为下一步的输入。

把它沿时间”展开”来看:

            ŷ(1)           ŷ(2)           ŷ(3)           ŷ(4)
             ↑ U            ↑ U            ↑ U            ↑ U
  h(0) ──W_h→ h(1) ──W_h→ h(2) ──W_h→ h(3) ──W_h→ h(4)
             ↑ W_e          ↑ W_e          ↑ W_e          ↑ W_e
            e(1)           e(2)           e(3)           e(4)
            the           students       opened          their

每一列用的都是同一组 WhW_h、WeW_e、UU。

5.2 RNN 语言模型

把 RNN 用作语言模型,就是在每一步用隐藏状态预测下一个词:

y^(t)=softmax(Uh(t)+b2)∈R∣V∣\hat{y}^{(t)} = \text{softmax}\big(U h^{(t)} + b_2\big) \in \mathbb{R}^{|V|}

y^(t)\hat{y}^{(t)} 是 x(t+1)x^{(t+1)} 的概率分布。和固定窗口模型相比,h(t)h^{(t)} 理论上包含了从句首到第 tt 个词的全部信息,而不仅仅是最近的几个词。

动手:代码 03_rnn_from_scratch.py 用 numpy 从零实现了 RNN 语言模型。演示 1 逐步打印了读入 “<s> the dog runs .” 时的隐藏状态。随机初始化的模型平均交叉熵是 4.33,接近 log⁡∣V∣=4.37\log |V| = 4.37(“什么都不懂”)。参数一共 840 个(词向量 4 维、隐藏状态 5 维时),和句子长度无关。

代码 04_rnn_lm.py 演示 1 验证了 PyTorch 的 nn.RNN 计算的正是 ht=tanh⁡(Wihxt+bih+Whhht−1+bhh)h_t = \tanh(W_{ih} x_t + b_{ih} + W_{hh} h_{t-1} + b_{hh})(PyTorch 用了两个偏置,加起来就是公式里的 b1b_1)。

5.3 RNN 的优点与缺点

优点缺点
能处理任意长度的输入计算是串行的:h(t)h^{(t)} 必须等 h(t−1)h^{(t-1)} 算完,无法在时间维度上并行,在 GPU 上训练长序列很慢
第 tt 步的计算理论上可以利用很多步之前的信息实际上很难利用很多步之前的信息(梯度消失,第 6.4 节、第 6 讲)
模型大小不随输入长度增长
每一步用同样的权重,对不同位置的处理是对称的

右边这两个缺点,第一个导致了 2017 年 Transformer 的诞生(第 7 讲),第二个导致了 LSTM 的普及(第 6 讲)。


6. 训练 RNN:时间反向传播

6.1 损失函数与 teacher forcing

训练 RNN 语言模型的步骤:

  1. 取一大段文本 x(1),…,x(T)x^{(1)}, \dots, x^{(T)};
  2. 送进 RNN,每一步都计算下一个词的预测分布 y^(t)\hat{y}^{(t)};
  3. 每一步的损失是预测分布和真实下一个词之间的交叉熵:J(t)(θ)=−log⁡y^x(t+1)(t)J^{(t)}(\theta) = -\log \hat{y}^{(t)}_{x^{(t+1)}};
  4. 整体损失是所有时间步的平均:
J(θ)=1T∑t=1TJ(t)(θ)J(\theta) = \frac{1}{T} \sum_{t=1}^{T} J^{(t)}(\theta)

注意训练时,每一步的输入是真实的上一个词,而不是模型自己上一步预测的词。这叫 teacher forcing(“老师强制”:不管学生上一步答得对不对,下一步都把正确答案告诉他)。它让训练更稳定、更容易并行地计算所有时间步的损失。代价是训练和生成时的输入分布不一致:生成时模型只能看到自己生成的词,一旦生成了一个训练时没见过的错误,后面可能越错越远(“暴露偏差”,和第 4 讲转移分析器的问题是同一回事)。

实践中不会把整个语料当成一个序列:通常把文本切成句子或固定长度的片段,再组成小批量。同一批中长短不一的序列用 <pad> 补齐,计算损失时忽略填充位置(代码 04 的 make_batches)。

6.2 时间反向传播(BPTT)

怎样求 ∂J/∂Wh\partial J / \partial W_h?难点在于 WhW_h 在每个时间步都被用了一次。

回忆第 3 讲第 5.4 节的多元链式法则:一个变量被用了多次,损失对它的梯度是沿每条路径的梯度之和。把 RNN 展开后,WhW_h 就是”一个被用了 tt 次的变量”。设想我们给每次使用起个不同的名字 Wh∣(1),Wh∣(2),…W_h|_{(1)}, W_h|_{(2)}, \dots(它们的值都等于 WhW_h),那么:

 ∂J(t)∂Wh=∑i=1t∂J(t)∂Wh∣(i) \boxed{\ \frac{\partial J^{(t)}}{\partial W_h} = \sum_{i=1}^{t} \left.\frac{\partial J^{(t)}}{\partial W_h}\right|_{(i)}\ }

被重复使用的参数,梯度等于它在每个时间步的梯度之和。 这就是时间反向传播(Backpropagation Through Time,BPTT):把 RNN 沿时间展开成一个很深的前馈网络(每个时间步一层,各层共享参数),然后做普通的反向传播,最后把各层对共享参数的梯度加起来。

具体地,从最后一个时间步往前走。在时间步 tt:

  1. 输出层:δout(t)=(y^(t)−y(t))/T\delta_\text{out}^{(t)} = (\hat{y}^{(t)} - y^{(t)}) / T,于是 ∂J∂U+=δout(t)h(t)⊤\frac{\partial J}{\partial U} \mathrel{+}= \delta_\text{out}^{(t)} h^{(t)\top};
  2. 传到 h(t)h^{(t)} 的梯度有两个来源:这一步的输出 U⊤δout(t)U^\top \delta_\text{out}^{(t)},以及下一个时间步传回来的 ∂J∂h(t)∣来自 t+1\frac{\partial J}{\partial h^{(t)}}\big|_\text{来自 } t+1。两者相加;
  3. 穿过 tanh:δ(t)=∂J∂h(t)∘(1−(h(t))2)\delta^{(t)} = \frac{\partial J}{\partial h^{(t)}} \circ \big(1 - (h^{(t)})^2\big);
  4. 累加共享参数的梯度:∂J∂Wh+=δ(t)h(t−1)⊤\frac{\partial J}{\partial W_h} \mathrel{+}= \delta^{(t)} h^{(t-1)\top},∂J∂We+=δ(t)e(t)⊤\frac{\partial J}{\partial W_e} \mathrel{+}= \delta^{(t)} e^{(t)\top};
  5. 传给上一个时间步:∂J∂h(t−1)∣来自 t=Wh⊤δ(t)\frac{\partial J}{\partial h^{(t-1)}}\big|_\text{来自 } t = W_h^\top \delta^{(t)}。

动手:代码 03 演示 2 对一个 8 个词的句子做了 BPTT 的梯度检验:EE、WeW_e、WhW_h、bb、UU、cc 六个参数的解析梯度与数值梯度的相对误差都在 10−910^{-9} 量级。演示 3 用纯 numpy 在 200 个玩具句子上训练了 15 轮,平均交叉熵从 2.44 降到 1.5 左右,生成的句子开始像样了(“the dogs smile .”、“the students wait .”),但也会出现 “the cats near the tables sings .” 这样的主谓不一致。

6.3 截断的 BPTT

如果训练文本是一整本书,展开成几十万步的网络再反向传播,内存和计算都受不了。实践中用截断的 BPTT(truncated BPTT):把长序列切成固定长度(比如 100 步)的片段,梯度只在片段内反向传播;但隐藏状态在片段之间继续传递(前向时不截断),这样模型仍然能”看到”片段之前的内容,只是不能为了片段之前的事情调整参数。

6.4 一个隐患:梯度随时间的传播

反向传播时,梯度每往前传一步,都要乘一次 ∂h(t)∂h(t−1)=diag(1−(h(t))2)Wh\frac{\partial h^{(t)}}{\partial h^{(t-1)}} = \text{diag}\big(1 - (h^{(t)})^2\big) W_h。传 kk 步就是 kk 个这样的矩阵连乘。

动手:代码 03 演示 4 在一个未训练的 RNN(隐藏状态 32 维)上,看最后一步的损失 LTL_T 对前面各个时间步隐藏状态的梯度大小 ∥∂LT/∂h(t)∥\|\partial L_T / \partial h^{(t)}\|:

WhW_h 的初始化尺度t=Tt = TT−5T - 5T−10T - 10T−20T - 20T−30T - 30T−40T - 40
σ=0.5/H\sigma = 0.5/\sqrt{H}1.053.8e-28.7e-43.3e-73.3e-104.4e-13
σ=1/H\sigma = 1/\sqrt{H}1.050.700.290.100.0770.068
σ=3/H\sigma = 3/\sqrt{H}1.144.15.813497323,600

WhW_h 偏小时梯度指数衰减(40 步之前只剩 10−1310^{-13}):模型几乎不可能为了 40 个词之前的事情调整参数,这叫梯度消失。偏大时梯度指数增长,一次更新就可能把参数”炸飞”,这叫梯度爆炸。代码 04 训练时用了梯度裁剪(gradient clipping):梯度的总范数超过阈值就按比例缩小,是应对梯度爆炸的标准做法。第 6 讲会从数学上分析这两个问题,并介绍 LSTM。

6.5 训练结果

动手:代码 04 演示 2 用 PyTorch 在 20,000 个句子上训练 RNN 语言模型(词向量 32 维、隐藏状态 128 维、约 3.3 万个参数,Adam,梯度裁剪阈值 1.0,8 轮,在编写讲义的电脑上约 40 秒):

模型测试集困惑度
4-gram(插值)4.91
固定窗口神经模型(窗口 4)4.55
RNN4.33

困惑度的差距看起来不大,因为这门玩具语言的大部分词都能由最近的几个词决定;RNN 的优势集中在少数需要远距离信息的位置上——第 8 节专门测量这些位置。


7. 从语言模型生成文本:解码策略

7.1 生成的过程

用 RNN 语言模型生成文本,和 n-gram 一样是一个词一个词地进行:

  1. 从 <s> 和初始隐藏状态出发;
  2. 计算下一个词的概率分布 y^(t)\hat{y}^{(t)},从中选出一个词;
  3. 把选出的词作为下一步的输入(注意:这里用的是模型自己生成的词,不再是 teacher forcing);
  4. 重复,直到生成 </s> 或达到最大长度。

Karpathy 2015 年的博客《The Unreasonable Effectiveness of Recurrent Neural Networks》展示了字符级 RNN 语言模型的惊人效果:在莎士比亚全集上训练后能生成”像模像样”的剧本,在 Linux 源代码上训练后能生成括号匹配、缩进正确的 C 代码,还有人用它生成奥巴马演讲、哈利·波特、菜谱……这些文本局部非常流畅,但整体仍然缺乏连贯的意义。

动手:代码 04 演示 5 从训练好的 RNN 语言模型中采样(实际生成的句子):

the red farmers that the bird sees are ready .
the old girl near the young table sleeps again .
the cats that chase the kings near the garden near the river smile .

注意第三句:主语 cats 和动词 smile 之间隔了 9 个词,其中 garden、river 是单数(和主语单复数相反的”干扰名词”,第 8 节),RNN 仍然选对了复数形式 smile。

7.2 解码策略

第 2 步”从分布中选出一个词”有很多种做法,统称解码策略:

策略做法特点
贪心每一步选概率最高的词确定、单调,容易重复同样的句子或短语
纯采样按模型的分布随机抽多样,但偶尔会抽到概率很低、不合理的词
温度采样前把 logits 除以温度 τ\tau:softmax(z/τ)\text{softmax}(z / \tau)τ<1\tau < 1 让分布更”尖”(更保守),τ>1\tau > 1 更”平”(更随机);τ→0\tau \to 0 退化成贪心
top-k只在概率最高的 kk 个词中采样砍掉了长尾;但 kk 固定,不管分布是尖还是平
top-p(核采样)只在累积概率刚达到 pp 的最小词集合中采样集合大小随分布自适应:分布尖时只留几个词,平时留很多

还有一种重要的策略叫束搜索(beam search),它试图找出整个句子概率最高的输出,而不是每一步贪心地选择。它在机器翻译中非常重要,第 6 讲详细讲。

动手:代码 05_sampling.py 演示 1 展示了温度对 “the dog” 之后的分布的影响:

下一个词τ=0.5\tau = 0.5τ=1\tau = 1τ=2\tau = 2
that0.4460.2930.183
near0.3660.2650.174
is0.1070.1430.128
runs0.0200.0620.084

分布的熵分别相当于”在约 3.7、6.6、11.7 个词之间均匀选择”。

演示 3 让每种策略各生成 300 个句子:

策略distinct-1distinct-2不同句子的比例参考困惑度
贪心0.0030.0030.3%4.13
纯采样(τ=1\tau = 1)0.0300.24894.0%5.57
温度 τ=0.5\tau = 0.50.0230.18299.0%4.86
温度 τ=1.5\tau = 1.50.0340.27394.0%12.47
top-k(k=5k = 5)0.0220.13286.0%4.75
top-p(p=0.9p = 0.9)0.0290.23496.3%4.70

distinct-n 是”不同 nn-gram 占全部 nn-gram 的比例”(越高越多样);参考困惑度是用代码 01 的 4-gram 模型给生成的句子打分(越低越像真实语料)。

几点观察:

  1. 贪心每次都生成同一个句子(“the cat that the girl likes near the houses is happy .”),完全没有多样性。
  2. 温度:τ=0.5\tau = 0.5 更保守(distinct-n 下降、参考困惑度降低);τ=1.5\tau = 1.5 的参考困惑度高达 12.47,不通顺的组合明显增多。有意思的是 τ=0.5\tau = 0.5 时”不同句子”反而更多:在这门语言里,最可能的下一个词常常是 that、near(继续加修饰语),压低温度会让句子变长,长句子自然不容易重复。温度的效果取决于模型把概率放在了哪里。
  3. top-k、top-p 砍掉了分布的长尾,参考困惑度比纯采样低,多样性只略有下降,是比较好的折中。今天的大语言模型最常用的就是 top-p(或 top-k)配合一个温度。

一个更一般的教训:多样性和”安全”之间存在权衡,而且自动指标(distinct-n、参考困惑度)只能反映其中一部分;真正评估生成质量往往需要人工评估(第 13 讲)。


8. RNN 能学会长距离依赖吗?—— 主谓一致实验

8.1 实验设计

困惑度是一个”平均”指标,很难单独看出模型能不能利用远处的信息。Linzen、Dupoux 和 Goldberg(2016)提出了一个干净的测试:主谓一致(subject–verb agreement)。

英语中动词要和主语保持单复数一致:the dog runs、the dogs run。如果主语和动词之间隔着很多词,模型要选对动词形式,就必须”记住”很久以前出现的主语是单数还是复数:

the dog  near the old trees  that the girls like  ___ 
主语(单数)  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  正确:runs   错误:run
            中间插入 9 个词,trees、girls 都是复数("干扰名词")

测试方法:把句子截到主动词之前,比较模型给正确形式(runs)和错误形式(run)的概率,正确形式更高就算对。中间与主语单复数相反的名词叫干扰名词(attractor),它们会”诱导”模型选错。

我们的玩具语言(代码 toy_language.py)就是为这个实验设计的:主语后面可以插入若干个修饰语(介词短语 “near the trees”、宾语关系从句 “that the girls like”、主语关系从句 “that likes the cats”),修饰语中名词的单复数随机。训练语料中最多插入 3 个修饰语。

8.2 结果:n-gram、固定窗口、RNN

动手:代码 01 演示 5、代码 02 演示 3、代码 04 演示 3 的结果汇总(每组 400 道题):

修饰语个数主语与动词的平均距离4-gram固定窗口(窗口 4)RNN
00100.0%100.0%100.0%
13.758.0%81.0%100.0%
27.562.0%62.3%100.0%
311.263.0%66.5%100.0%
4(训练时没见过)14.859.2%62.7%100.0%
8(训练时没见过)29.767.5%—100.0%
  1. n-gram 和固定窗口模型:主语一旦跑出窗口,准确率就掉到六成左右(随机猜是 50%,剩下的优势来自窗口里的间接线索,例如 “that likes the cats ___” 中的 likes 暴露了主语是单数)。固定窗口模型在 1 个修饰语时还有 81%,是因为平均距离 3.7 时主语还经常落在 4 个词的窗口里。
  2. RNN 全对,甚至在比训练时长得多的句子上(8 个修饰语,主语和动词相距约 30 个词)也是如此。它确实把”主语是单数还是复数”这一信息带过了中间所有的修饰语——这是任何固定窗口的模型都做不到的。

动手:代码 02 演示 4 把固定窗口开大:

窗口参数量困惑度一致(1 个修饰语)一致(2 个修饰语)
221,0395.1852.8%51.0%
429,2314.5581.0%62.3%
845,6154.47100.0%90.2%
1261,9994.40100.0%99.8%

窗口越大越好,但参数量随窗口线性增长;而且无论开多大,总有更长的句子。

8.3 但 RNN 学到的机制稳健吗?

上面的 RNN 在训练时见过最多 3 个修饰语的句子。如果训练时只见过短依赖呢?

动手:代码 04 演示 3(b) 用只含 0~1 个修饰语的语料(最远距离约 5 个词)训练同样的 RNN:

修饰语个数123468
平均距离3.77.511.214.822.329.7
RNN(只见过短依赖)100.0%99.0%94.8%90.0%80.2%76.0%

距离越远,准确率越低。RNN 学到的”记住主语单复数”的机制并不稳健:信息每传一步就损耗一点。这和第 6.4 节的梯度消失是同一件事的两面——梯度传不回去,模型就学不会在长距离上可靠地保持信息;训练时没有长依赖的例子”逼”它学,它就只学会了短距离够用的机制。第 6 讲会在同样的设置下训练 LSTM:它几乎不受影响。

在真实英语上,Linzen 等人发现,即使是 LSTM 语言模型,在主语和动词之间有干扰名词时也会明显犯错,而且干扰名词越多错得越多。真实语言比我们的玩具语言复杂得多:句子结构千变万化,主谓一致的信号在训练数据中相对稀少。

8.4 用探针看隐藏状态

模型”知道”主语是单数还是复数吗?可以用一个探针(probe)来检查:取主动词之前那个位置的隐藏状态 hh,训练一个线性分类器(逻辑回归)去预测主语的单复数。如果线性分类器就能准确地读出来,说明这个信息被线性地编码在隐藏状态里。

动手:代码 04 演示 4(一半题目训练探针,另一半测试):

修饰语个数01248
完整训练的 RNN100.0%100.0%100.0%100.0%100.0%
只见过短依赖的 RNN100.0%100.0%100.0%94.0%79.5%

完整训练的 RNN 的隐藏状态无论隔多远都保存着主语的单复数;只见过短依赖的 RNN,离得越远越难读出——和 8.3 节的行为一致。探针是研究”模型内部学到了什么”的常用工具,第 17 讲讨论可解释性时会再谈它的用法和局限。


9. RNN 的其他用途

RNN 不只能做语言模型,它是一个通用的”序列编码器”:

  • 序列标注:在每个时间步输出一个标签,例如词性标注、命名实体识别(第 3 讲的窗口分类器只看固定窗口,RNN 可以利用整个前文)。
  • 句子分类:例如情感分析。用最后一个隐藏状态 h(T)h^{(T)} 作为整个句子的表示,或者对所有隐藏状态取平均 / 最大值(通常效果更好),再接一个分类器。
  • 作为编码器:把一个句子编码成向量(或向量序列),交给别的模块使用,例如问答系统、第 6 讲机器翻译中的编码器。
  • 条件语言模型:在某个”条件”下生成文本——语音识别(条件是音频)、机器翻译(条件是源语言句子)、摘要(条件是原文)、看图说话(条件是图片)。第 6 讲的 seq2seq 就是一个条件语言模型。

做分类、标注时,每个位置往往既需要左边的信息,也需要右边的信息(“the movie was terribly exciting”中 terribly 的情感取决于后面的 exciting)。这时会用双向 RNN:一个 RNN 从左往右读,一个从右往左读,把两个隐藏状态拼接起来。语言模型不能用双向(预测下一个词时不能偷看后面的词)。第 6 讲会详细讲双向和多层 RNN。


10. 本讲小结

  1. 语言模型预测下一个词,等价于给文本赋予概率(链式法则)。预测下一个词是一个极其通用的任务,语言模型是衡量语言理解的基准,也是今天大模型的基础。
  2. n-gram 模型:马尔可夫假设 + 计数。问题是稀疏(没见过的 n-gram 概率为 0)和存储;平滑(加 k)、回退、插值(EM 估计权重)、Kneser–Ney 缓解稀疏。
  3. 困惑度 = exp(平均交叉熵),可以理解为每一步的”有效分支数”,越低越好;只有在相同词表和分词下才能比较。
  4. 固定窗口神经语言模型(Bengio 2003):解决了稀疏和存储,但窗口有限、不同位置的权重不共享。
  5. RNN:h(t)=σ(Whh(t−1)+Wee(t)+b)h^{(t)} = \sigma(W_h h^{(t-1)} + W_e e^{(t)} + b),同一组参数反复使用,能处理任意长度、理论上能利用任意远的信息;缺点是计算串行、实际上难以利用很远的信息。
  6. 训练:每一步交叉熵的平均;teacher forcing;BPTT:被重复使用的参数的梯度 = 各时间步梯度之和;截断 BPTT;梯度裁剪。梯度随时间连乘,可能消失或爆炸。
  7. 解码策略:贪心单调;温度控制随机性;top-k / top-p 砍掉长尾,在多样性和质量之间折中。
  8. 主谓一致实验:n-gram 和固定窗口模型在主语跑出窗口后接近瞎猜;RNN 能跨越很长的距离,但如果训练时只见过短依赖,远距离的表现会下降。

11. 与作业的关系

Winter 2026 版的作业中没有单独的 RNN 作业(作业 3 直接跳到自注意力和 Transformer),但本讲的内容是后面所有序列模型的基础:

  • 语言模型的训练目标(交叉熵、teacher forcing、困惑度)在作业 3 的 Transformer 语言模型、默认期末项目的 GPT-2 中原封不动地出现;
  • 解码策略(贪心、采样、温度、top-k、top-p)在生成类任务中都要用到;
  • Winter 2025 及更早的作业 3(神经机器翻译)用 LSTM 实现编码器—解码器,第 6 讲会讲;
  • 作业 2 中关于梯度、反向传播的推导,可以直接迁移到 BPTT(练习 3、4)。

12. 练习

练习 1(链式法则):用二元模型写出 P(the dog runs)P(\text{the dog runs}) 的分解式(包括句子开始和结束符号)。三元模型呢?

参考答案

二元:P(the∣⟨s⟩) P(dog∣the) P(runs∣dog) P(⟨/s⟩∣runs)P(\text{the} \mid \langle s \rangle)\, P(\text{dog} \mid \text{the})\, P(\text{runs} \mid \text{dog})\, P(\langle /s \rangle \mid \text{runs})。 三元(开头用两个 ⟨s⟩\langle s \rangle):P(the∣⟨s⟩⟨s⟩) P(dog∣⟨s⟩ the) P(runs∣the dog) P(⟨/s⟩∣dog runs)P(\text{the} \mid \langle s \rangle \langle s \rangle)\, P(\text{dog} \mid \langle s \rangle\, \text{the})\, P(\text{runs} \mid \text{the dog})\, P(\langle /s \rangle \mid \text{dog runs})。

练习 2(困惑度)★:(a) 一个模型对测试集中的每个词都给出概率 0.25,困惑度是多少?(b) 如果一半的词概率 0.5、一半 0.125 呢?(c) 证明对所有词一视同仁(均匀分布)的模型困惑度等于 ∣V∣|V|。

参考答案

(a) exp⁡(−log⁡0.25)=4\exp(-\log 0.25) = 4。 (b) 平均对数概率 =12(log⁡0.5+log⁡0.125)=log⁡0.0625=log⁡0.25= \frac{1}{2}(\log 0.5 + \log 0.125) = \log \sqrt{0.0625} = \log 0.25,困惑度仍然是 4(困惑度是几何平均意义下的”有效分支数”)。 (c) 每个词的概率都是 1/∣V∣1/|V|,平均交叉熵是 log⁡∣V∣\log |V|,困惑度 =exp⁡(log⁡∣V∣)=∣V∣= \exp(\log |V|) = |V|。

练习 3(BPTT)★★:对 RNN h(t)=tanh⁡(Whh(t−1)+Wee(t)+b)h^{(t)} = \tanh(W_h h^{(t-1)} + W_e e^{(t)} + b),设 J(3)J^{(3)} 只依赖于 h(3)h^{(3)}。写出 ∂J(3)∂Wh\frac{\partial J^{(3)}}{\partial W_h} 的展开式,说明它由几项相加而成,每一项代表什么。

参考答案

记 δ(t)=∂J(3)∂z(t)\delta^{(t)} = \frac{\partial J^{(3)}}{\partial z^{(t)}}(z(t)z^{(t)} 是 tanh 之前的加权和)。WhW_h 在 t=1,2,3t = 1, 2, 3 各用了一次: ∂J(3)∂Wh=δ(3)h(2)⊤+δ(2)h(1)⊤+δ(1)h(0)⊤\frac{\partial J^{(3)}}{\partial W_h} = \delta^{(3)} h^{(2)\top} + \delta^{(2)} h^{(1)\top} + \delta^{(1)} h^{(0)\top}, 其中 δ(3)=∂J(3)∂h(3)∘(1−h(3)2)\delta^{(3)} = \frac{\partial J^{(3)}}{\partial h^{(3)}} \circ (1 - h^{(3)2}),δ(2)=(Wh⊤δ(3))∘(1−h(2)2)\delta^{(2)} = (W_h^\top \delta^{(3)}) \circ (1 - h^{(2)2}),δ(1)=(Wh⊤δ(2))∘(1−h(1)2)\delta^{(1)} = (W_h^\top \delta^{(2)}) \circ (1 - h^{(1)2})。 三项分别是”第 3、2、1 步对 WhW_h 的使用”带来的梯度。越早的项要经过越多次 Wh⊤W_h^\top 和 tanh 导数的连乘——这就是梯度消失 / 爆炸的来源。

练习 4(梯度的连乘):设隐藏状态是标量,h(t)=tanh⁡(wh(t−1)+x(t))h^{(t)} = \tanh(w h^{(t-1)} + x^{(t)})。∂h(t)∂h(t−k)\frac{\partial h^{(t)}}{\partial h^{(t-k)}} 是什么?如果 ∣w∣=0.9|w| = 0.9 且所有 tanh 的导数都约为 0.8,k=20k = 20 时这个值大约是多少?

参考答案

∂h(t)∂h(t−k)=∏j=t−k+1tw⋅(1−h(j)2)\frac{\partial h^{(t)}}{\partial h^{(t-k)}} = \prod_{j=t-k+1}^{t} w \cdot (1 - h^{(j)2})。每个因子约为 0.9×0.8=0.720.9 \times 0.8 = 0.72,k=20k = 20 时约为 0.7220≈1.4×10−30.72^{20} \approx 1.4 \times 10^{-3}。只要每个因子小于 1,结果就随 kk 指数衰减。

练习 5(参数量):词表 ∣V∣=10000|V| = 10000,词向量 d=100d = 100,隐藏状态 H=200H = 200。分别计算 (a) RNN 语言模型、(b) 窗口大小为 5 的固定窗口神经语言模型(隐藏层 200)的参数量。如果窗口增大到 10 呢?

参考答案

(a) EE:10000×100=10010000 \times 100 = 100 万;WeW_e:200×100=2200 \times 100 = 2 万;WhW_h:200×200=4200 \times 200 = 4 万;b1b_1:200;UU:10000×200=20010000 \times 200 = 200 万;b2b_2:1 万。共约 316 万,与句子长度无关。 (b) EE:100 万;WW:200×(5×100)=10200 \times (5 \times 100) = 10 万;b1b_1:200;UU:200 万;b2b_2:1 万。共约 311 万。窗口 10 时 WW 变成 20 万,共约 321 万。(在这个例子里输出层 UU 和词向量 EE 占了绝大部分参数,窗口带来的增长看起来不大;但隐藏层的参数随窗口线性增长,而且不同位置的权重互不共享。)

练习 6(top-p):下一个词的分布是 A 0.5、B 0.2、C 0.15、D 0.1、E 0.05。top-p(p=0.8p = 0.8)会在哪些词中采样?重新归一化后各自的概率是多少?top-k(k=2k = 2)呢?

参考答案

按概率从大到小累加:A 0.5,A+B 0.7,A+B+C 0.85 ≥ 0.8,所以保留 A、B、C。重新归一化:A 0.5/0.85 ≈ 0.588,B ≈ 0.235,C ≈ 0.176。 top-k(k=2k = 2)保留 A、B:A 0.714,B 0.286。

练习 7(温度):logits 为 [2,1,0][2, 1, 0]。分别计算 τ=1\tau = 1、τ=0.5\tau = 0.5、τ→0\tau \to 0、τ→∞\tau \to \infty 时的分布。

参考答案

τ=1\tau = 1:softmax([2,1,0])≈[0.665,0.245,0.090]([2, 1, 0]) \approx [0.665, 0.245, 0.090]。 τ=0.5\tau = 0.5:softmax([4,2,0])≈[0.867,0.117,0.016]([4, 2, 0]) \approx [0.867, 0.117, 0.016]。 τ→0\tau \to 0:[1,0,0][1, 0, 0](贪心)。 τ→∞\tau \to \infty:[1/3,1/3,1/3][1/3, 1/3, 1/3](均匀)。

练习 8(Kneser–Ney 的直觉):“San Francisco” 在语料中出现了 1000 次,Francisco 从不单独出现;“glasses” 出现了 300 次,跟在 50 种不同的词后面。如果高阶模型对 “I can’t see without my reading ___” 没有信息、需要回退到低阶模型,按普通的 1-gram 频率和按”跟在多少种不同的词后面”,哪个词得分更高?哪个更合理?

参考答案

按 1-gram 频率,Francisco(1000)高于 glasses(300);按”跟在多少种词后面”(延续计数),glasses(50)远高于 Francisco(1)。后者更合理:Francisco 几乎只出现在 San 后面,在一个全新的上下文中出现的可能性很小。这正是 Kneser–Ney 平滑的核心想法。

练习 9(动手):

(a) 在代码 01 中实现一个简单的”愚蠢回退”(stupid backoff):高阶计数为 0 时,用 0.4×0.4 \times 低阶得分。它的困惑度和插值相比如何?(注意:愚蠢回退给出的不是严格的概率分布。) (b) 在代码 04 中把隐藏状态维度从 128 改成 16、32,主谓一致的表现如何变化? (c) 在代码 04 中去掉梯度裁剪(把 clip 设得很大),训练还稳定吗? (d) 在 toy_language.py 中把训练语料改成最多 6 个修饰语,代码 04 演示 3(b) 中”只见过短依赖”的问题还存在吗?

练习 10(思考):RNN 的隐藏状态是一个固定大小的向量(比如 128 维)。用它来总结任意长的前文,会有什么问题?你能想到什么改进办法?(这是第 6 讲”注意力”的动机。)

参考答案

固定大小的向量容量有限,而前文的信息量随长度增长;模型只能保留”它认为重要的”信息,越早的信息越容易被覆盖或丢失。而且每一步都要把新信息”挤进”同一个向量。改进的思路包括:(1) 更好地控制信息的写入和保留(LSTM 的门控);(2) 不只依赖最后一个隐藏状态,而是保留所有时间步的隐藏状态,在需要时”回头查看”相关的那几个——这就是注意力机制。


13. 延伸阅读

  • CS224N 课程笔记 Part V(Language Models, RNN, GRU and LSTM)。
  • Jurafsky & Martin,Speech and Language Processing 第 3 版,第 3 章(N-gram Language Models)和第 8 章(RNNs and LSTMs)。
  • Bengio, Ducharme, Vincent & Jauvin,A Neural Probabilistic Language Model(2003)。
  • Mikolov et al.,Recurrent Neural Network Based Language Model(2010):RNN 语言模型的经典工作。
  • Andrej Karpathy,The Unreasonable Effectiveness of Recurrent Neural Networks(博客,2015):字符级 RNN 生成的大量有趣例子。
  • Linzen, Dupoux & Goldberg,Assessing the Ability of LSTMs to Learn Syntax-Sensitive Dependencies(2016):本讲第 8 节实验的原型。
  • Holtzman et al.,The Curious Case of Neural Text Degeneration(2020):提出 top-p(核采样)。
  • Chen & Goodman,An Empirical Study of Smoothing Techniques for Language Modeling(1998):n-gram 平滑方法的经典比较。

14. 下一讲预告

本讲看到 RNN 有两个问题:梯度会消失或爆炸,导致很难学会可靠的长距离依赖;隐藏状态是一个固定大小的向量,要装下全部前文。下一讲(梯度消失、LSTM、seq2seq 与注意力)会讲:

  • 梯度消失和爆炸的数学原因,以及梯度裁剪;
  • LSTM 和 GRU:用”门”来控制信息的写入、保留和读出,让信息(和梯度)能沿着时间畅通地流动;
  • 机器翻译与 seq2seq(编码器—解码器)模型、束搜索、BLEU 评估;
  • 信息瓶颈问题与注意力机制:解码时”回头看”编码器的所有隐藏状态——这是通往 Transformer 的关键一步。

下一讲之前,建议你:跑通 code/lecture05/ 下的 5 个脚本(先运行 04 号文件生成检查点,05 号文件和第 6 讲会用到它);推一遍练习 3 的 BPTT;思考练习 10。

← 返回课程目录