对应: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 学完本讲,你应该能够
- 给出语言模型的定义,用链式法则把句子的概率拆成”预测下一个词”的乘积,并说明语言模型为什么重要。
- 解释 n-gram 语言模型的马尔可夫假设、最大似然估计,以及它的两个根本问题:稀疏和存储;说明加 k 平滑、插值的作用。
- 定义困惑度,解释它和交叉熵的关系与直观含义。
- 描述固定窗口的神经语言模型,说明它解决了什么、没解决什么。
- 写出循环神经网络(RNN)语言模型的公式,说出 RNN 的优点和缺点。
- 推导时间反向传播(BPTT):为什么”重复使用的参数的梯度是每次使用的梯度之和”。
- 解释 teacher forcing、填充与批处理、梯度裁剪,以及几种解码策略(贪心、温度、top-k、top-p)。
- 用主谓一致实验说明 n-gram 和 RNN 在长距离依赖上的差别,并初步理解梯度消失的后果。
0.2 先修知识
| 知识点 | 用在哪里 | 最低要求 |
|---|---|---|
| 第 1、3 讲 | 全讲 | softmax、交叉熵、反向传播、“梯度要累加”、PyTorch 训练循环 |
| 第 4 讲 | 第 4 节 | 固定窗口特征的局限(神经依存分析器只看栈和缓冲区的前几个位置) |
| 条件概率 | 第 1、2 节 | |
| 对数与指数 | 第 3 节 | , |
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节 小结、与作业的关系、练习、延伸阅读、下一讲预告
本讲的主线是一条熟悉的”发现问题—改进”链:
1. 什么是语言模型?
1.1 定义
语言模型(language model,LM)做的事情是预测下一个词:给定前面的词 ,计算下一个词 在词表 上的概率分布
等价地,语言模型给任意一段文本赋予一个概率。根据概率的链式法则:
所以”给句子打分”和”预测下一个词”是一回事:会预测下一个词,就能算出整个句子的概率;反之亦然。
1.2 为什么语言模型重要?
你每天都在用语言模型:手机输入法的联想、搜索框的自动补全、邮件的智能回复……在深度学习之前,语言模型就已经是很多系统的关键组件:
- 语音识别:声学模型给出几个读音相近的候选(“识别语音” vs “石壁鱼音”),语言模型判断哪个更像正常的句子;
- 机器翻译:在多个候选译文中挑选最通顺的;
- 拼写纠错、手写识别、文本输入……
更深刻的一点是:预测下一个词是一个极其通用的任务。要准确地预测”巴黎是法国的___“,模型需要知识;要预测”我把钥匙忘在家里了,所以我只好___“,模型需要常识和推理;要预测长句子末尾的动词形式,模型需要句法。所以语言模型一直被看作衡量语言理解能力的基准任务。今天的大语言模型(GPT 系列等)本质上就是规模极大的语言模型(第 9 讲)。
2. n-gram 语言模型
2.1 马尔可夫假设与计数
怎样估计 ?前文可以任意长,完全相同的前文在语料中几乎不会重复出现,没法直接估计。
深度学习之前的标准答案是 n-gram 模型。n-gram 指连续 个词组成的片段(1-gram 是单个词,2-gram 是两个词……)。n-gram 模型做了一个简化,叫马尔可夫假设:下一个词只依赖于前面 个词。
然后用计数来估计(最大似然估计,MLE):
例子(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 次,那么 、。可是被丢掉的前文 “proctor”(监考员)明明强烈暗示应该是 exams!这就是马尔可夫假设的代价。
动手:代码
01_ngram_lm.py使用一门玩具语言(toy_language.py):75 个词,带有可控的长距离主谓一致(第 8 节详细介绍)。训练集 20,000 句、开发集和测试集各 2,000 句。演示 1:上下文 “the dog” 在训练集中出现了 1312 次,后面跟 that 311 次()、near 254 次(0.194)、is 110 次(0.084)……句子 “the dog runs .” 在三元模型下的概率是 。
(句子开头用两个 <s> 填充,结尾加一个 </s>,这样”句子在这里结束”也是一个要预测的事件。)
2.2 两个根本问题:稀疏和存储
稀疏: 越大,模型越”细致”,但很多 -gram 在训练语料中从没出现过。
- 分子为 0:测试时遇到没见过的 -gram,最大似然估计给它概率 0。只要测试集里有一个这样的 -gram,整个测试集的概率就是 0。
- 分母为 0:连上下文都没见过,概率根本无法计算。
动手:代码 01 演示 2:
不同 -gram 的种数 测试集中没见过的比例 最大似然的困惑度 1 76 0.00% 31.76 2 1,168 0.00% 5.97 3 7,939 0.89% ∞ 4 20,178 3.45% ∞ 5 44,315 11.53% ∞ 我们的玩具语言只有 75 个词、结构非常规则,即便如此,5-gram 中也有超过 11% 在训练集中没见过。真实语言(几十万词表、无穷的句式)中这个问题严重得多。
存储:要存所有见过的 -gram 的计数。 每加 1,需要存的计数就多很多(代码 01 演示 6:从 1-gram 的 76 个到 5-gram 的 44,315 个)。在几十亿词的真实语料上,5-gram 模型要存几十亿个计数。
Manning 的总结是: 太小,模型看不到足够的上下文; 太大,稀疏问题让模型失效。实践中 很少超过 5。
2.3 平滑与插值
加 k 平滑:给每个可能的词都”假装”多见过 次。
叫加一平滑(拉普拉斯平滑)。这样没见过的 -gram 也有一点概率,代价是从见过的事件那里”挪走”了一些概率。
回退(backoff):如果 “students opened their” 从没出现过,就退一步用 “opened their”;还没有,再退到 “their”。
插值(interpolation):把不同阶的模型按权重混合:
高阶模型有信息时它起作用,没信息(概率为 0)时由低阶模型兜底。权重 要在开发集上估计(在训练集上估计的话,最高阶永远”最好”),常用 EM 算法:反复地”对每个词计算它由哪个子模型生成的后验概率,再用平均值更新 ”。
更精巧的方法是 Kneser–Ney 平滑,它是 n-gram 时代效果最好的方法。它的一个关键直觉是:低阶模型不应该用”词出现了多少次”,而应该用”这个词跟在多少种不同的词后面出现过”。例如 Francisco 出现得很频繁,但几乎只出现在 San 后面;当高阶模型没有信息、需要回退时,Francisco 就不该得到很高的概率。
动手:代码 01 演示 3(测试集困惑度,参数在开发集上选):
加 1 加 0.1 加 0.01 插值(EM) 1 31.76 31.76 31.76 31.78 2 6.11 5.98 5.97 5.97 3 6.42 5.42 5.34 5.26 4 8.70 5.78 5.31 4.91 加 1 平滑分给”没见过的事件”的概率太多了, 越大越糟; 取小一些好得多。插值最好:4-gram 插值模型的权重是 、、——主要依靠 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):
也就是测试集概率的倒数,再按词数开 次方根(几何平均)。取对数就能看出它和交叉熵的关系:
困惑度 = exp(平均交叉熵损失)。训练时最小化交叉熵,就是在最小化困惑度。
直观含义:困惑度可以理解为”模型在每一步平均要在多少个词之间犹豫”(有效分支数)。
- 一个对所有词一视同仁的模型,困惑度等于词表大小 ;
- 一个完美的模型(每一步都以概率 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 讲的窗口分类器一模一样:
它相对于 n-gram 的改进:
- 没有稀疏问题:任何窗口都能算出一个概率分布,不会有”没见过就是 0”;
- 不用存计数:只需要存固定数量的参数;
- 能泛化:相似的词有相似的向量,“the dog runs”学到的东西能迁移到”the cat runs”。
这篇论文在 word2vec 之前十年就提出了”同时学习词向量和语言模型”,是神经语言模型的开山之作。
4.2 仍然存在的问题
- 窗口仍然是固定的,窗口外的信息完全看不到;
- 窗口越大, 越大: 的列数是 窗口大小 × 词向量维度;
- 不同位置的权重互不共享: 中乘以 的那一块和乘以 的那一块是不同的参数。模型要分别学习”前 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.92 73,676 个计数 固定窗口神经模型(窗口 4) 4.55 29,231 个参数 神经模型用更少的存储得到了更低的困惑度。第 8 节会看到它在长距离依赖上的表现。
5. 循环神经网络
5.1 核心想法
我们想要一个能处理任意长度输入、并且在所有位置共享参数的结构。循环神经网络(Recurrent Neural Network,RNN)的想法是:用同一组权重,一步一步地反复处理。
它维护一个隐藏状态 ,作为”到目前为止读过的所有内容的总结”。每读入一个新词,就用同一个公式更新隐藏状态:
其中 是第 个词的词向量, 是非线性函数(通常是 tanh), 是初始隐藏状态(通常全为 0)。“循环”指的是 依赖于 :同一个网络被反复应用,输出又作为下一步的输入。
把它沿时间”展开”来看:
ŷ(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
每一列用的都是同一组 、、。
5.2 RNN 语言模型
把 RNN 用作语言模型,就是在每一步用隐藏状态预测下一个词:
是 的概率分布。和固定窗口模型相比, 理论上包含了从句首到第 个词的全部信息,而不仅仅是最近的几个词。
动手:代码
03_rnn_from_scratch.py用 numpy 从零实现了 RNN 语言模型。演示 1 逐步打印了读入 “<s> the dog runs .” 时的隐藏状态。随机初始化的模型平均交叉熵是 4.33,接近 (“什么都不懂”)。参数一共 840 个(词向量 4 维、隐藏状态 5 维时),和句子长度无关。代码
04_rnn_lm.py演示 1 验证了 PyTorch 的nn.RNN计算的正是 (PyTorch 用了两个偏置,加起来就是公式里的 )。
5.3 RNN 的优点与缺点
| 优点 | 缺点 |
|---|---|
| 能处理任意长度的输入 | 计算是串行的: 必须等 算完,无法在时间维度上并行,在 GPU 上训练长序列很慢 |
| 第 步的计算理论上可以利用很多步之前的信息 | 实际上很难利用很多步之前的信息(梯度消失,第 6.4 节、第 6 讲) |
| 模型大小不随输入长度增长 | |
| 每一步用同样的权重,对不同位置的处理是对称的 |
右边这两个缺点,第一个导致了 2017 年 Transformer 的诞生(第 7 讲),第二个导致了 LSTM 的普及(第 6 讲)。
6. 训练 RNN:时间反向传播
6.1 损失函数与 teacher forcing
训练 RNN 语言模型的步骤:
- 取一大段文本 ;
- 送进 RNN,每一步都计算下一个词的预测分布 ;
- 每一步的损失是预测分布和真实下一个词之间的交叉熵:;
- 整体损失是所有时间步的平均:
注意训练时,每一步的输入是真实的上一个词,而不是模型自己上一步预测的词。这叫 teacher forcing(“老师强制”:不管学生上一步答得对不对,下一步都把正确答案告诉他)。它让训练更稳定、更容易并行地计算所有时间步的损失。代价是训练和生成时的输入分布不一致:生成时模型只能看到自己生成的词,一旦生成了一个训练时没见过的错误,后面可能越错越远(“暴露偏差”,和第 4 讲转移分析器的问题是同一回事)。
实践中不会把整个语料当成一个序列:通常把文本切成句子或固定长度的片段,再组成小批量。同一批中长短不一的序列用 <pad> 补齐,计算损失时忽略填充位置(代码 04 的 make_batches)。
6.2 时间反向传播(BPTT)
怎样求 ?难点在于 在每个时间步都被用了一次。
回忆第 3 讲第 5.4 节的多元链式法则:一个变量被用了多次,损失对它的梯度是沿每条路径的梯度之和。把 RNN 展开后, 就是”一个被用了 次的变量”。设想我们给每次使用起个不同的名字 (它们的值都等于 ),那么:
被重复使用的参数,梯度等于它在每个时间步的梯度之和。 这就是时间反向传播(Backpropagation Through Time,BPTT):把 RNN 沿时间展开成一个很深的前馈网络(每个时间步一层,各层共享参数),然后做普通的反向传播,最后把各层对共享参数的梯度加起来。
具体地,从最后一个时间步往前走。在时间步 :
- 输出层:,于是 ;
- 传到 的梯度有两个来源:这一步的输出 ,以及下一个时间步传回来的 。两者相加;
- 穿过 tanh:;
- 累加共享参数的梯度:,;
- 传给上一个时间步:。
动手:代码 03 演示 2 对一个 8 个词的句子做了 BPTT 的梯度检验:、、、、、 六个参数的解析梯度与数值梯度的相对误差都在 量级。演示 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 一个隐患:梯度随时间的传播
反向传播时,梯度每往前传一步,都要乘一次 。传 步就是 个这样的矩阵连乘。
动手:代码 03 演示 4 在一个未训练的 RNN(隐藏状态 32 维)上,看最后一步的损失 对前面各个时间步隐藏状态的梯度大小 :
的初始化尺度 1.05 3.8e-2 8.7e-4 3.3e-7 3.3e-10 4.4e-13 1.05 0.70 0.29 0.10 0.077 0.068 1.14 4.1 5.8 134 973 23,600
偏小时梯度指数衰减(40 步之前只剩 ):模型几乎不可能为了 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 RNN 4.33 困惑度的差距看起来不大,因为这门玩具语言的大部分词都能由最近的几个词决定;RNN 的优势集中在少数需要远距离信息的位置上——第 8 节专门测量这些位置。
7. 从语言模型生成文本:解码策略
7.1 生成的过程
用 RNN 语言模型生成文本,和 n-gram 一样是一个词一个词地进行:
- 从
<s>和初始隐藏状态出发; - 计算下一个词的概率分布 ,从中选出一个词;
- 把选出的词作为下一步的输入(注意:这里用的是模型自己生成的词,不再是 teacher forcing);
- 重复,直到生成
</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 除以温度 : | 让分布更”尖”(更保守), 更”平”(更随机); 退化成贪心 |
| top-k | 只在概率最高的 个词中采样 | 砍掉了长尾;但 固定,不管分布是尖还是平 |
| top-p(核采样) | 只在累积概率刚达到 的最小词集合中采样 | 集合大小随分布自适应:分布尖时只留几个词,平时留很多 |
还有一种重要的策略叫束搜索(beam search),它试图找出整个句子概率最高的输出,而不是每一步贪心地选择。它在机器翻译中非常重要,第 6 讲详细讲。
动手:代码
05_sampling.py演示 1 展示了温度对 “the dog” 之后的分布的影响:
下一个词 that 0.446 0.293 0.183 near 0.366 0.265 0.174 is 0.107 0.143 0.128 runs 0.020 0.062 0.084 分布的熵分别相当于”在约 3.7、6.6、11.7 个词之间均匀选择”。
演示 3 让每种策略各生成 300 个句子:
策略 distinct-1 distinct-2 不同句子的比例 参考困惑度 贪心 0.003 0.003 0.3% 4.13 纯采样() 0.030 0.248 94.0% 5.57 温度 0.023 0.182 99.0% 4.86 温度 0.034 0.273 94.0% 12.47 top-k() 0.022 0.132 86.0% 4.75 top-p() 0.029 0.234 96.3% 4.70 distinct-n 是”不同 -gram 占全部 -gram 的比例”(越高越多样);参考困惑度是用代码 01 的 4-gram 模型给生成的句子打分(越低越像真实语料)。
几点观察:
- 贪心每次都生成同一个句子(“the cat that the girl likes near the houses is happy .”),完全没有多样性。
- 温度: 更保守(distinct-n 下降、参考困惑度降低); 的参考困惑度高达 12.47,不通顺的组合明显增多。有意思的是 时”不同句子”反而更多:在这门语言里,最可能的下一个词常常是 that、near(继续加修饰语),压低温度会让句子变长,长句子自然不容易重复。温度的效果取决于模型把概率放在了哪里。
- 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 0 0 100.0% 100.0% 100.0% 1 3.7 58.0% 81.0% 100.0% 2 7.5 62.0% 62.3% 100.0% 3 11.2 63.0% 66.5% 100.0% 4(训练时没见过) 14.8 59.2% 62.7% 100.0% 8(训练时没见过) 29.7 67.5% — 100.0%
- n-gram 和固定窗口模型:主语一旦跑出窗口,准确率就掉到六成左右(随机猜是 50%,剩下的优势来自窗口里的间接线索,例如 “that likes the cats ___” 中的 likes 暴露了主语是单数)。固定窗口模型在 1 个修饰语时还有 81%,是因为平均距离 3.7 时主语还经常落在 4 个词的窗口里。
- RNN 全对,甚至在比训练时长得多的句子上(8 个修饰语,主语和动词相距约 30 个词)也是如此。它确实把”主语是单数还是复数”这一信息带过了中间所有的修饰语——这是任何固定窗口的模型都做不到的。
动手:代码 02 演示 4 把固定窗口开大:
窗口 参数量 困惑度 一致(1 个修饰语) 一致(2 个修饰语) 2 21,039 5.18 52.8% 51.0% 4 29,231 4.55 81.0% 62.3% 8 45,615 4.47 100.0% 90.2% 12 61,999 4.40 100.0% 99.8% 窗口越大越好,但参数量随窗口线性增长;而且无论开多大,总有更长的句子。
8.3 但 RNN 学到的机制稳健吗?
上面的 RNN 在训练时见过最多 3 个修饰语的句子。如果训练时只见过短依赖呢?
动手:代码 04 演示 3(b) 用只含 0~1 个修饰语的语料(最远距离约 5 个词)训练同样的 RNN:
修饰语个数 1 2 3 4 6 8 平均距离 3.7 7.5 11.2 14.8 22.3 29.7 RNN(只见过短依赖) 100.0% 99.0% 94.8% 90.0% 80.2% 76.0%
距离越远,准确率越低。RNN 学到的”记住主语单复数”的机制并不稳健:信息每传一步就损耗一点。这和第 6.4 节的梯度消失是同一件事的两面——梯度传不回去,模型就学不会在长距离上可靠地保持信息;训练时没有长依赖的例子”逼”它学,它就只学会了短距离够用的机制。第 6 讲会在同样的设置下训练 LSTM:它几乎不受影响。
在真实英语上,Linzen 等人发现,即使是 LSTM 语言模型,在主语和动词之间有干扰名词时也会明显犯错,而且干扰名词越多错得越多。真实语言比我们的玩具语言复杂得多:句子结构千变万化,主谓一致的信号在训练数据中相对稀少。
8.4 用探针看隐藏状态
模型”知道”主语是单数还是复数吗?可以用一个探针(probe)来检查:取主动词之前那个位置的隐藏状态 ,训练一个线性分类器(逻辑回归)去预测主语的单复数。如果线性分类器就能准确地读出来,说明这个信息被线性地编码在隐藏状态里。
动手:代码 04 演示 4(一半题目训练探针,另一半测试):
修饰语个数 0 1 2 4 8 完整训练的 RNN 100.0% 100.0% 100.0% 100.0% 100.0% 只见过短依赖的 RNN 100.0% 100.0% 100.0% 94.0% 79.5%
完整训练的 RNN 的隐藏状态无论隔多远都保存着主语的单复数;只见过短依赖的 RNN,离得越远越难读出——和 8.3 节的行为一致。探针是研究”模型内部学到了什么”的常用工具,第 17 讲讨论可解释性时会再谈它的用法和局限。
9. RNN 的其他用途
RNN 不只能做语言模型,它是一个通用的”序列编码器”:
- 序列标注:在每个时间步输出一个标签,例如词性标注、命名实体识别(第 3 讲的窗口分类器只看固定窗口,RNN 可以利用整个前文)。
- 句子分类:例如情感分析。用最后一个隐藏状态 作为整个句子的表示,或者对所有隐藏状态取平均 / 最大值(通常效果更好),再接一个分类器。
- 作为编码器:把一个句子编码成向量(或向量序列),交给别的模块使用,例如问答系统、第 6 讲机器翻译中的编码器。
- 条件语言模型:在某个”条件”下生成文本——语音识别(条件是音频)、机器翻译(条件是源语言句子)、摘要(条件是原文)、看图说话(条件是图片)。第 6 讲的 seq2seq 就是一个条件语言模型。
做分类、标注时,每个位置往往既需要左边的信息,也需要右边的信息(“the movie was terribly exciting”中 terribly 的情感取决于后面的 exciting)。这时会用双向 RNN:一个 RNN 从左往右读,一个从右往左读,把两个隐藏状态拼接起来。语言模型不能用双向(预测下一个词时不能偷看后面的词)。第 6 讲会详细讲双向和多层 RNN。
10. 本讲小结
- 语言模型预测下一个词,等价于给文本赋予概率(链式法则)。预测下一个词是一个极其通用的任务,语言模型是衡量语言理解的基准,也是今天大模型的基础。
- n-gram 模型:马尔可夫假设 + 计数。问题是稀疏(没见过的 n-gram 概率为 0)和存储;平滑(加 k)、回退、插值(EM 估计权重)、Kneser–Ney 缓解稀疏。
- 困惑度 = exp(平均交叉熵),可以理解为每一步的”有效分支数”,越低越好;只有在相同词表和分词下才能比较。
- 固定窗口神经语言模型(Bengio 2003):解决了稀疏和存储,但窗口有限、不同位置的权重不共享。
- RNN:,同一组参数反复使用,能处理任意长度、理论上能利用任意远的信息;缺点是计算串行、实际上难以利用很远的信息。
- 训练:每一步交叉熵的平均;teacher forcing;BPTT:被重复使用的参数的梯度 = 各时间步梯度之和;截断 BPTT;梯度裁剪。梯度随时间连乘,可能消失或爆炸。
- 解码策略:贪心单调;温度控制随机性;top-k / top-p 砍掉长尾,在多样性和质量之间折中。
- 主谓一致实验: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(链式法则):用二元模型写出 的分解式(包括句子开始和结束符号)。三元模型呢?
参考答案
二元:。 三元(开头用两个 ):。
练习 2(困惑度)★:(a) 一个模型对测试集中的每个词都给出概率 0.25,困惑度是多少?(b) 如果一半的词概率 0.5、一半 0.125 呢?(c) 证明对所有词一视同仁(均匀分布)的模型困惑度等于 。
参考答案
(a) 。 (b) 平均对数概率 ,困惑度仍然是 4(困惑度是几何平均意义下的”有效分支数”)。 (c) 每个词的概率都是 ,平均交叉熵是 ,困惑度 。
练习 3(BPTT)★★:对 RNN ,设 只依赖于 。写出 的展开式,说明它由几项相加而成,每一项代表什么。
参考答案
记 ( 是 tanh 之前的加权和)。 在 各用了一次: , 其中 ,,。 三项分别是”第 3、2、1 步对 的使用”带来的梯度。越早的项要经过越多次 和 tanh 导数的连乘——这就是梯度消失 / 爆炸的来源。
练习 4(梯度的连乘):设隐藏状态是标量,。 是什么?如果 且所有 tanh 的导数都约为 0.8, 时这个值大约是多少?
参考答案
。每个因子约为 , 时约为 。只要每个因子小于 1,结果就随 指数衰减。
练习 5(参数量):词表 ,词向量 ,隐藏状态 。分别计算 (a) RNN 语言模型、(b) 窗口大小为 5 的固定窗口神经语言模型(隐藏层 200)的参数量。如果窗口增大到 10 呢?
参考答案
(a) : 万;: 万;: 万;:200;: 万;:1 万。共约 316 万,与句子长度无关。 (b) :100 万;: 万;:200;:200 万;:1 万。共约 311 万。窗口 10 时 变成 20 万,共约 321 万。(在这个例子里输出层 和词向量 占了绝大部分参数,窗口带来的增长看起来不大;但隐藏层的参数随窗口线性增长,而且不同位置的权重互不共享。)
练习 6(top-p):下一个词的分布是 A 0.5、B 0.2、C 0.15、D 0.1、E 0.05。top-p()会在哪些词中采样?重新归一化后各自的概率是多少?top-k()呢?
参考答案
按概率从大到小累加: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()保留 A、B:A 0.714,B 0.286。
练习 7(温度):logits 为 。分别计算 、、、 时的分布。
参考答案
:softmax。 :softmax。 :(贪心)。 :(均匀)。
练习 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 时,用 低阶得分。它的困惑度和插值相比如何?(注意:愚蠢回退给出的不是严格的概率分布。)
(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。