第 4 章 从 N-gram 到 Bigram:手算语言模型
4.1 看过的文本,怎样变成预测的依据?
Section titled “4.1 看过的文本,怎样变成预测的依据?”前面两章讲了文本怎样变成 Token 和数值表示,但还有一个问题没回答:下一 Token 的概率是从哪里来的?神经网络内部保存了大量数值,不容易一眼看清。我们先搭建一个简单模型,把它学到的全部内容放进一张小表。
想象你要补全“猫坐在……上”,手里有一些现成的例句。一种起步办法是数一数:“在”后面接过哪些片段,各出现了多少次?常出现的,就多给一些被选中的机会。这个办法的好处是,每次预测依据了哪些例子,都能查清。
为了把计算走完,我们先用几个字母代替日常文字。这些字母没有暗指什么含义,只是让我们专心看顺序和次数,不必同时处理词表和事实问题。按顺序写在一起的一串字符,叫作字符串。训练数据只有三条:
样例 1:ab样例 2:ab样例 3:acab 出现了两次,就要数两次,不能去重。本章把每个字母当作一个 Token,换用一套字符词表,不沿用第 3 章的分词器,也不用嵌入表。这样就能专心研究:只看过有限的几条序列,该怎样给今后可能出现的序列估计概率?
我们会先统计单个 Token,看看不记顺序会出什么问题;再加上前一个 Token 作为条件,最后处理训练中没出现过的组合。每一步都针对一个具体问题,也会引出新的选择和代价。整个过程始终使用这三条字符串,方便看清预测的变化究竟来自模型的哪项调整。
4.2 先说清预测什么、从哪里开始、在哪里结束
Section titled “4.2 先说清预测什么、从哪里开始、在哪里结束”如果模型只能选 a、b、c,那么每选一次就会多写一个字母。它要怎样表示“到这里就写完了”呢?我们得给“结束”单独留一个选项。另外,第一个字母前面还没有文字,也需要一个记号,说明现在是在开头作预测。我们用 BOS(beginning of sequence,序列开始)标记起点,把它放在上下文中;用 EOS(end of sequence,序列结束)作为一个可以预测的结果。选到 EOS 就表示这条字符串结束了,不会在显示的文字后再加一个字母。
模型可预测的候选按固定顺序排列为 [a,b,c,EOS],共四项。BOS、EOS 各是一个标记的名称,不要把名称里的三个字母拆成三个 Token 来数。BOS 只用作上下文,不参加预测。每条训练字符串单独处理:先回到起始上下文,依次预测字符,最后预测 EOS。不要把前一条的结尾和后一条的开头连起来算。
表 4.1——三条训练字符串包含哪些预测。
| 训练字符串 | 需要预测的 Token,含结束标记 | 预测次数 |
|---|---|---|
第一条 ab | a、b、EOS | 3 |
第二条 ab | a、b、EOS | 3 |
ac | a、c、EOS | 3 |
| 合计 | a:3;b:2;c:1;EOS:3 | 9 |
每次预测,模型都要给四个候选分配概率。组成概率分布的各项不能小于零,加起来必须等于一。上下文怎么取、概率怎么估计,一旦规定下来,每一步都按同样的规则处理。
4.3 先试一个不记顺序的模型
Section titled “4.3 先试一个不记顺序的模型”例 4.1——用 Unigram 建立基线。先不看前文,只统计单个 Token,这种办法叫作一元模型(Unigram)。表 4.1 一共列出九次预测,其中 b 出现两次,所占比例就是 2/9,称为 b 的频率。我们的第一种估计办法很直接:就用观察到的这个比例,作为以后预测时给 b 的概率。其余候选也一样,分别用出现次数除以总次数九。无论预测哪个位置,都用同一组概率:
| 下一 Token | 次数 | 概率 |
|---|---|---|
| a | 3 | 3/9 |
| b | 2 | 2/9 |
| c | 1 | 1/9 |
| EOS | 3 | 3/9 |
这个模型学到了 a 比 c 常见,也学到了 EOS 经常出现,但不会看前面已经有什么。所有训练字符串都以 a 开头,它却仍可能一开始就选 b、c 或 EOS。到了 a 后面,它给 a 和 EOS 的概率仍然最高,尽管训练中 a 后面实际出现的是 b、b、c。
明明有两次在 a 后面见到 b,为什么到了 a 后面,b 却没有得到最高概率?因为合并计数时,“在 a 后面”这个信息已经被扔掉了,模型没有单独的记录可查。问题就在它保存了什么:只记各个 Token 出现几次,不记出现顺序。如果另一组数据的次数相同、排列不同,Unigram 也会得到同一个分布。顺序在统计时已经丢了,预测时自然分不清。
我们可以只改一点:根据前一个 Token,分别统计后面出现什么。在 a 后面预测,就只看训练中 a 后面的内容;在 b 后面预测,就看 b 后面的内容。这便得到 Bigram 模型。训练数据和可预测的候选都不变,只是计数时多了一个条件。
这些名称说的都是一次预测涉及几个 Token:既算前面作为条件的 Token,也算接下来要预测的那一个。N-gram 指连续 N 个 Token 组成的片段。N-gram 语言模型取前 N−1 个 Token 作为上下文,预测接下来的一个 Token。连同这个预测目标,正好是 N 个。
表 4.2——不同阶数使用多长的上下文。
| 阶数 | 名称 | 使用前面几个 Token | 到 … a b 末尾时,取什么上下文 |
|---|---|---|---|
| 1 | Unigram | 0 | 空 |
| 2 | Bigram | 1 | b |
| 3 | Trigram | 2 | a b |
| N | N-gram | N−1 | 最后 N−1 个 Token |
Trigram 虽然叫“三元”,每次仍然只预测一个 Token,另外两个来自前文。序列刚开始时,前面还没有足够的 Token,就用 N−1 个 BOS 补齐。这些标记只提供上下文,不算预测目标。Unigram 不看前文,也就不需要补 BOS。
4.4 用前一个 Token 作条件:Bigram 模型
Section titled “4.4 用前一个 Token 作条件:Bigram 模型”4.4.1 逐个统计九次转移
Section titled “4.4.1 逐个统计九次转移”把前一个 Token 也写出来,三条训练字符串就成为:
BOS → a → b → EOSBOS → a → b → EOSBOS → a → c → EOS第一行可以读成三道已经知道答案的题:开头接什么?a。a 后面接什么?b。b 后面接什么?结束。一个箭头记下“在这个上下文之后,出现了这个 Token”,称为一次转移。预测总共仍是九次,只是现在每次都记着对应的上下文。
例 4.2——从零开始建立计数表。先把计数全部设为零,再依次处理这些箭头。
表 4.3——每读到一次转移,就给对应位置加一。
| 次序 | 读到的转移 | 该项累积次数 |
|---|---|---|
| 1 | BOS→a | 1 |
| 2 | a→b | 1 |
| 3 | b→EOS | 1 |
| 4 | BOS→a | 2 |
| 5 | a→b | 2 |
| 6 | b→EOS | 2 |
| 7 | BOS→a | 3 |
| 8 | a→c | 1 |
| 9 | c→EOS | 1 |
预测时需要方便地找到这些次数,所以把它们整理成一张表。行写“前面是什么”,列写“后面是什么”,交叉的格子记次数。例如,a 行与 b 列相交的位置,填入 a→b 出现的两次。某个格子仍为零,就表示从未见过这一对。这样得到完整计数表。
表 4.4——Bigram 的完整计数表。
| 前一个 Token | 后面是 a | 后面是 b | 后面是 c | 后面是 EOS | 行合计 |
|---|---|---|---|---|---|
| BOS | 3 | 0 | 0 | 0 | 3 |
| a | 0 | 2 | 1 | 0 | 3 |
| b | 0 | 0 | 0 | 2 | 2 |
| c | 0 | 0 | 0 | 1 | 1 |
a 行说明:训练中有三次在 a 后面预测,两次接 b,一次接 c。b 行和 c 行则说明它们之后都是结束标记。表中没有预测 BOS 的列,EOS 后也不再预测。每条字符串结束后重新开始,才不会把本来不存在的 EOS→a 也算进去。
这张计数表就是模型拟合后的状态。对它来说,训练就是在指定数据上完成计数,不需要求梯度。第 2 章的神经方法用另一种办法估计模型状态。两者都要把“从数据中拟合状态”和“使用已有状态作预测”分开。
4.4.2 选中一行,再把次数换成概率
Section titled “4.4.2 选中一行,再把次数换成概率”记完过去的情况,现在回到预测。可以为每一行想象一个抽签袋。a 这一行的袋子里有三张纸条:b、b、c。选这个袋子,表示我们已经知道前一个 Token 是 a;再从中等机会地抽一张,b 被选中的概率就是 2/3,c 是 1/3。抽完要把纸条放回去,因为预测只使用已有计数,不会改变它们。这就是条件概率的思路:先确定已知条件,再根据符合条件的那些情况作预测。
在 a 后面预测,就看 a 行:四个候选的次数为 [0,2,1,0],除以本行总数三,得到 [0,2/3,1/3,0]。分母之所以是三而不是九,是因为我们只考虑“前一个 Token 是 a”这三次。
如果改成除以九,这一行加起来只有 3/9,不能构成概率分布。它算的是各项占全部训练次数的比例,而我们要问的是:已经知道前面是 a,接下来各项的概率是多少?所以必须先选出符合条件的那一行,再在这一行内归一化。
用 表示上下文 h 后面出现 x 的次数,用 表示这一行的计数总和。根据出现次数估计条件概率,公式就是:
竖线表示“在给定条件下”:左边 x 是要预测的候选,右边 h 是已知上下文。拿 a→b 来对照,h 就是 a,x 就是 b,C(h,x) 是格子里的 2,C(h) 是这一行的合计 3。整条公式读下来,就是“a 后面接 b 的概率,等于 a→b 的次数除以在 a 后面预测的总次数”。下标 0 表示还没有做平滑,后面会介绍这个调整。右侧条件则要求分母大于零。
代入可得,a→b 的概率是 2/3,BOS→a 是 3/3 = 1,b→EOS 是 2/2 = 1,c→EOS 是 1/1 = 1。四行分别加起来都等于一。对于这个尚未平滑的模型,从 BOS 出发,每个能走到的位置都已有相应的预测规则。
这些概率只是根据当前样本作出的估计。a 后面接 b 的比例是 2/3,并不证明产生未来数据的真实过程也恰好使用 2/3。它只说明,我们用这份数据训练出的模型会怎样预测。
4.5 一整条序列的概率怎么算?
Section titled “4.5 一整条序列的概率怎么算?”4.5.1 按指定路径一步步相乘
Section titled “4.5.1 按指定路径一步步相乘”例 4.3——计算 ab 的概率。现在给定字符串 ab,不让模型自由生成,而是算它完整生成这条字符串的概率。它必须先选 a,再选 b,最后结束,也就是沿 BOS→a→b→EOS 走完三步。
| 必须经过的转移 | 条件概率 |
|---|---|
| BOS→a | 1 |
| a→b | 2/3 |
| b→EOS | 1 |
三步都按要求完成的概率,是这些条件概率的乘积:
为什么相乘?第一步一定到 a;到了 a 后面,只有 2/3 的机会接着选 b,而选到 b 的路径又都会结束,所以恰好写出 ab 的概率是 2/3。假如再增加一道限制,只有其中一半会在 b 后面结束,那么完整概率就会变成 2/3 的一半,也就是 1/3。这个假设说明了乘法的作用:每多一个要求,就从已满足前面要求的那些路径中,再取出符合新要求的一部分。把三个数相加,表达不了这层关系,本例的和甚至会超过一。
换成 ac,中间一项改为 1/3,最后一项仍是一,所以完整概率为 1/3。这个模型只能生成 ab 和 ac 两条完整字符串,两者的概率加起来正好为一。
把刚才的计算写成通用公式。如果不只计算 ab,而要处理任意长度的序列,就需要把具体字母换成表示位置的符号。概率链式法则(probability chain rule)可以把整条序列的联合概率写成条件概率的乘积。至于每一步只看最近几个 Token,则是模型另作的假设,并不是链式法则要求的。用 表示第 t 个 Token, 表示它前面的全部 Token, 表示 N-gram 取出的末尾片段。模型用这段有限的前文来近似完整历史:
下标中的 t 表示第几个位置,不是相乘;前文下标中的 < 表示“在这个位置之前”;h 两边的竖线表示上下文有多长。对 Bigram 和字符串 ab 来说,三次预测所看的上下文依次是 BOS、a、b;换成 Trigram,则依次是 BOS BOS、BOS a、a b。变的是每一步参考多少前文。
一条序列若有 T 个普通 Token,再以 结束,那么这个模型给出的完整序列概率为:
符号 ∏ 表示把列出的各项全部相乘,这里每个位置对应一项。它下面的 1 和上面的 T+1,说明从第 1 个位置一直乘到第 T+1 个位置。拿 ab 来说,T 是 2,第 1、2 个位置分别是 a、b,第 3 个位置是 EOS。把这个连乘符号展开,就是前面已经算过的 1 × 2/3 × 1。起始上下文用 BOS 补齐,BOS 本身不生成。要注意,近似的是“用多长的前文作条件”;一旦模型确定,用上述乘积计算它的路径概率就是精确的。相关计数、边界约定和链式法则,可以参阅 N-gram 的标准教材讲解。Jurafsky 与 Martin,§§3.1–3.3
4.5.2 “以它开头”与“到此结束”不同
Section titled “4.5.2 “以它开头”与“到此结束”不同”比较两个要求:“开头写 ab”和“只写 ab,到此结束”。它们都要求先写 a 再写 b,但后者还多了一项结束要求。前缀概率回答的是第一个问题:开头是不是这段文字,后面接什么并不限制。完整字符串概率则还要求恰好在这里结束。对于本例未平滑的 Bigram,选完 ab 后一定选 EOS,因此两种概率碰巧相同。这是本表的特殊情况,不是一般规律。
换用 Unigram,区别就显现出来了:它在 b 后面还可能接字母,所以写到 ab 并不一定就结束。以 ab 开头的概率是 3/9 × 2/9 = 2/27。若还要求接着结束,就要再乘 EOS 的概率 3/9,得到完整 ab 的概率 2/81。漏乘这一项,算的就不是同一件事。
同一个 Unigram 给完整 ac 的概率是 3/9 × 1/9 × 3/9 = 1/81,给完整 aa 的概率却是 3/9 × 3/9 × 3/9 = 1/27。训练数据里有 ac、没有 aa,它却给 aa 更高的概率。这再次暴露了忽略顺序的问题。Bigram 以前一个 Token 为条件,才在这些已见的路径上纠正了这种偏差。
4.6 按表中的概率走出一条生成路径
Section titled “4.6 按表中的概率走出一条生成路径”计算给定序列的概率和生成新序列,用的是同一张概率表,做法不同。前者已经知道下一项是什么,只需查它的概率;后者要从分布里选一项,加到序列末尾,再更新上下文。
只看 a 后面接什么
计数表不再改动。接下来有两种用法 ↓
按这里的概率选 b 或 c,接到序列末尾,再查下一行。
下一项已知是 b,就取概率 2/3;已知是 c,就取 1/3。
两种用法都只读取计数,不增加训练样本。
图 4.1 把训练和这两种用法分开了。无论给定字符串来算概率,还是按概率生成字符串,都只读表,不增加计数。如果后来把生成的文字放进训练集,那是另做了一次明确的训练。
例 4.4——让生成过程可以重现。从 BOS 出发,使用未平滑的 Bigram。候选顺序固定为 [a,b,c,EOS],每项对应一个左闭右开的区间,长度等于它的概率。到了上下文 a,a 和 EOS 的区间长度都是零,b 占前 2/3,c 占后 1/3。
图 4.1 中的 b、b、c 三张纸条,也表示同样的机会分配。下面记录了一次用数值抽样的过程。先顺着表看每个数选中了什么;这些抽样值怎样逐位算出来,放在过程之后交代。
表 4.5——种子为 42、N=2、未做平滑时的生成过程。
| 步骤 | 上下文 | 抽样值,已舍入 | 选中什么 |
|---|---|---|---|
| 1 | BOS | 0.252345 | a;其概率为 1 |
| 2 | a | 0.088125 | b;抽样值小于 2/3 |
| 3 | b | 0.577281 | EOS;其概率为 1 |
最终显示 ab,因为选到 EOS 而结束。计算时使用的是完整抽样值,表中只是为了便于阅读而舍入。模型、候选顺序、种子和算法都相同,从头重来就会得到同样的过程。反过来,不同种子也未必生成不同字符串:许多抽样值会落进同一个候选区间。
配套程序最多抽样 12 次,选中 EOS 也算一次。如果先选到 EOS,得到的就是完整结果;如果先用完次数,得到的只是截断的前缀;如果遇到无法定义的分布,则因另一种原因停止。要让别人重现实验,这些约定也得记下来,光记种子不够。
复现细节:抽样值是怎样得到的。程序可以按一套数值规则,依次生成一串能重复得到的抽样值,称为伪随机(pseudorandom)过程。规则的起始状态就是种子。它给模型提供选择用的数,并不训练模型。本书使用的 LCG32 从 42 开始:把当前状态乘以 1664525,再加 1013904223,最后取除以 4294967296 的余数,作为新状态。“余数”就是普通除法中的余数,例如 10 除以 3 余 1。前三个新状态依次为 1083814273、378494188、2479403867,各自除以 4294967296,就得到表 4.5 中舍入前的抽样值。这些很大的常数规定的是怎样生成随机数,并不是语言模型学到的参数。
4.7 没见过的组合,该给多少概率?
Section titled “4.7 没见过的组合,该给多少概率?”Bigram 已经学到了样本中的相邻顺序。但对完整 aa 会怎样?第一步 BOS→a 的概率为一,第二步 a→a 却为零,因为训练中没有 a 紧跟 a 的情况。这一步走不过去,整条路径的概率就是零。
可是,训练数据里没有 aa,并不证明 aa 永远不可能出现。是当前估计方法把“这次没见过”处理成了零概率。样本很少时,这样对待未来输入,往往过于武断。
用刚才的抽签袋来想,还容易看清另一种问题。a 的袋子不是空的,只是没有写着 a 的纸条;如果连袋子里都一张纸条没有,又该怎么抽?提高阶数后,就可能遇到这种情况。Trigram 分别记录 BOS BOS、BOS a、a b、a c 这些上下文。第一条 ab 对应 BOS BOS→a、BOS a→b、a b→EOS,三条训练字符串总共仍提供九次预测。
如果查 a a 后面的分布,这个上下文从未出现,四个计数全为零,分母也是零。因此,未平滑时这行分布未定义。不能说“四个候选的概率都是零”,因为四个零加起来不是一,根本不构成概率分布。
表 4.6——没见过下一项,与没见过整个上下文。
| 要查什么 | 对应的计数 | 未做平滑时的结果 |
|---|---|---|
| Bigram:a 后面接 a 的概率 | a 这一行是 [0,2,1,0],合计 3 | 0/3 = 0,概率有定义 |
| Trigram:a a 后面的分布 | a a 这一行是 [0,0,0,0],合计 0 | 要除以零,分布未定义 |
尽管如此,Trigram 仍然不可能从起点完整生成 aa,因为更早的 BOS a→a 这一步已有明确的零概率。前缀走不过去,无论怎样补后面那一行,仍然无法走完整条路径。这里是根据路径能不能到达作判断,不是把“零乘未定义”当成普通乘法来算。配套实现也分别保留这两个结果:完整序列不可达;单独查询后面的 a a 行,仍报告分布未定义。
4.8 用平滑给未见组合留一点余地
Section titled “4.8 用平滑给未见组合留一点余地”4.8.1 每个候选都分到一些概率
Section titled “4.8.1 每个候选都分到一些概率”沿用抽签袋的想法,可以往每个袋子里给每个候选各加一张纸条,空袋子也照样加。这样,每个候选都有机会被抽到,每个袋子也都有纸条可抽。换成数值计算,就是先给每个候选的计数加一个正数,再归一化。如果都加一,就叫加一平滑(add-one smoothing)。仍以 a 行为例。
例 4.5——a 行加一后,概率怎样变化。
| 候选 | 实际出现次数 | 加一后的数值 | 调整后概率 |
|---|---|---|---|
| a | 0 | 1 | 1/7 |
| b | 2 | 3 | 3/7 |
| c | 1 | 2 | 2/7 |
| EOS | 0 | 1 | 1/7 |
| 合计 | 3 | 7 | 1 |
加进去的四个一,是估计方法规定的调整,不是又观察到了四条数据。候选有四个,所以分母从三变成七。BOS 不参与预测,不能把它算作第五个候选。分子都加了,分母也必须重算,否则概率之和会超过一。
用 表示给每项增加的数,用 表示可预测的候选总数。本章比较 α 为零和一的情况,V 为四。一般的加 α 平滑写作:
alpha 读作“阿尔法”,表示给一个候选加多少。共有 V 个候选,所以总数一共增加 alpha 乘 V。拿 a→b 的加一计算来对照:分子是 2 + 1 = 3,分母是 3 + 1 × 4 = 7,仍然得到表中的 3/7。
当 α=0,且原行总数大于零时,就回到未平滑的公式。α=1 时,每个候选都能得到正概率,即使这个上下文以前从未出现过。
这也要付出代价:a 后面接 b 的概率从 2/3 降到了 3/7。总概率仍然只有一,给原先没见过的候选多分一些,就得从已见过的候选那里让出一些。所以,平滑是在证据不足时重新分配概率,并不是只补好处、不影响原有结果。
4.8.2 开始和结束的概率,也要一起重算
Section titled “4.8.2 开始和结束的概率,也要一起重算”平滑要应用到所有行,不能只改 a 行。怎样开始、何时结束,也都受影响。
表 4.7——Bigram 加一平滑后的完整分布。
| 上下文 | P₁(a) | P₁(b) | P₁(c) | P₁(EOS) |
|---|---|---|---|---|
| BOS | 4/7 | 1/7 | 1/7 | 1/7 |
| a | 1/7 | 3/7 | 2/7 | 1/7 |
| b | 1/6 | 1/6 | 1/6 | 3/6 |
| c | 1/5 | 1/5 | 1/5 | 2/5 |
例如,b 行原来是 [0,0,0,2],加一后成为 [1,1,1,3],总数六。到 b 后面结束的概率,就从一降为 3/6。接下来算完整字符串时,每一步都要查这张新表。
例 4.6——重算平滑后的完整序列概率。要生成 aa,先从 BOS 选 a,再从 a 选 a,最后从 a 选 EOS:
对于训练中出现过的两条字符串,分别得到:
原先概率为零的 aa 现在有可能生成了,ab 和 ac 的概率则都降低了。这三个值加起来不等于一,是因为模型现在还可能生成别的结果:一开始就结束、以 b 或 c 开头,或者接出更长的字符串。候选完整字符串变多了,但每一行的下一 Token 概率仍然加起来等于一。
对于 Trigram 中从未出现过的 a a 上下文,加一后得到 [1,1,1,1],合计四,四项概率便各为 1/4。这是因为没有样本时,我们仍给每一项加了相同的数,不是因为数据证明了它们在现实中一样常见。
例 4.7——为什么 Trigram 给 aa 的概率又不同?Trigram 用 BOS BOS 预测第一个 a,用 BOS a 预测第二个 a,再用 a a 预测 EOS。三项分别为 4/7、1/7、1/4,相乘得到 1/49。Bigram 最后只看一个 a,查出的 EOS 概率是 1/7,因此完整概率是 4/343。
本章选加一平滑,是因为每一步都容易看清,并不把它当作最好的估计方法。语言模型还有效果更好的平滑方法。这里先通过加一弄清:零概率怎样得到修正,其他候选的概率又怎样随之调整。Jurafsky 与 Martin,§3.6.1
4.9 比较模型之前,先定评测标准
Section titled “4.9 比较模型之前,先定评测标准”加一后,原来不可能生成的 aa 变得有可能了,但训练中见过的字符串反而降低了概率。这种交换值不值得?要回答它,就得回到第 1 章的问题:我们究竟按什么标准判断好坏?
评测(evaluation)是拿训练好的模型,按规定的标准检查指定样例,不把这些样例再拿去拟合。这里固定检查 ac 和 aa,计算它们的概率时不增加计数。前一个故意选了训练中已经出现的字符串,后一个选了未见组合。因此,这组对照用于排查模型行为,不能当作独立测量普通语言泛化能力的测试。
表 4.8——六种设置下的完整序列概率,均包含 EOS。
| 阶数与平滑 | P(ac, EOS) | P(aa, EOS) |
|---|---|---|
| Unigram,α=0 | 1/81 ≈ 0.012346 | 1/27 ≈ 0.037037 |
| Bigram,α=0 | 1/3 ≈ 0.333333 | 0 |
| Trigram,α=0 | 1/3 ≈ 0.333333 | 0 |
| Unigram,α=1 | 32/2197 ≈ 0.014565 | 64/2197 ≈ 0.029131 |
| Bigram,α=1 | 16/245 ≈ 0.065306 | 4/343 ≈ 0.011662 |
| Trigram,α=1 | 16/245 ≈ 0.065306 | 1/49 ≈ 0.020408 |
Bigram 和 Trigram 的几行,前面的例题已经算过。剩下的加一 Unigram,是把 [3,2,1,3] 改成 [4,3,2,4],总数 13。因此,ac 的概率为 4/13 × 2/13 × 4/13 = 32/2197,aa 的概率为 4/13 × 4/13 × 4/13 = 64/2197。
那么,哪个模型“更好”?如果只比较完整 ac 的概率,未平滑的 Bigram 比加一版本高。另一种标准是:把 ac 和 aa 看作从同一个固定模型中分别独立抽取的两个样本,比较这个设置给“两条都出现”分配了多少概率。这个数称为两条样本在该设置下的联合似然,算法就是把二者的概率相乘。这里固定的是已经看到的样本,比较的是哪种模型设置给它们更高的概率,并不是判断文字内容真假。按这个标准,结果就不同了:未平滑的 Bigram 乘出来是零,加一版本则大于零。同一项改动,可能提高某个明确的评测指标,同时降低某一条样例的概率。
这就是第 1 章为什么要求把分数和评测条件一起记录。反复看这两条公开的例子,确实有助于排查问题,但不能因此称它们为盲测,也不能说它们代表了真实任务。真正比较模型时,要根据研究的问题来选训练数据和评测数据。
还要注意,序列概率受分词方式和相乘项数影响,它本身不判断事实真假。不同分词器可能把文本拆成不同事件,不能把得到的原始概率直接当作同一口径来比。第 6 章会进一步介绍概率工具,以及怎样在评测时考虑序列长度。
4.10 N 越大,就一定越好吗?
Section titled “4.10 N 越大,就一定越好吗?”上下文更长,可以分清原先分不清的前文。例如,Trigram 把 a b 和 c b 视为不同上下文,Bigram 却都只看到最后的 b。保留的信息确实多了,但长度仍然有限。
例 4.8——线索落在窗口之外怎么办?这次把词当作 Token,比较 red key x x 和 blue key x x 两个前缀,任务规定下一步要说出前面的颜色。Bigram 两次都只看见最后一个 x,Trigram 两次都只看见 x x。同一个上下文查同一张表,就只能得到同一个分布。查表条件里根本没有颜色,分布也就无法随那次输入中的颜色而变。
对这些短上下文再多计数,也找不回这一次前面究竟是 red 还是 blue。增大 N 可以纳入更远的线索,但窗口只要固定,就总能遇到更远的内容。外部文档如果从没进入输入,窗口再长也用不上。
看得更远还可能带来另一个问题:每种具体情况能分到的依据更少了。就像一份调查虽然有不少受访者,但按多个细致条件再筛出一组人,人数可能所剩无几。文本计数也一样,样本没有增加,却被分得更细,这就是数据稀疏(data sparsity)。N 越大,可能出现的完整组合越多,每一种都需要数据来估计。V 个候选组成长度 N 的片段,形式上有 种组合。V=4 时,一阶、二阶、三阶分别是 4、16、64。上标 N 表示把 V 连乘 N 次:四个候选组成长度为一的片段,有四种;组成长度为二的片段,有 4 × 4 = 16 种;长度为三,就是 4 × 4 × 4 = 64 种。这个数还没有考虑遇到 EOS 就结束的限制,因此有些组合不会出现在合法字符串内部;含 BOS 的上下文又要另算。这里要看的是组合数成倍增长的趋势,并不是要求程序把所有组合都存一遍。
实际可以只保存见过的组合。本章这组小数据在三个阶数下,分别只有 4、5、5 种不同的预测事件,所以 N 增大,并不意味着每份数据的实际存储量都一定增加。不过,需要区分的上下文越多,同样的样本分散到各项后,每项往往就更少了。能看得远,和能估得准,是两个要求。
神经语言模型通过学到的表示和共用的数值函数,让不同输入利用相关的统计信息,不必把每一种具体上下文都孤立地估计一遍。Bengio 等人的工作介绍了这种设计动机。GPT 类因果语言模型和 N-gram 都预测下一 Token,但文本怎样表示、上下文怎样参与计算,差别很大。任务相同不等于机制相同;机制更复杂,也不保证总能答真话或用好每一条线索。Bengio 等,2003
4.11 本章小结:完成第一个纸笔模型
Section titled “4.11 本章小结:完成第一个纸笔模型”Unigram 统计单个 Token 的频率,不保留顺序;Bigram 多看前一个 Token,便能记下样本中的相邻转移。根据链式法则,各步条件概率相乘得到完整序列概率,最后的 EOS 表示在哪里结束。生成是按概率选一条路径,评测是检查给定路径,只有训练才把样本中的出现次数写进计数表。
没见过的组合还要分成两种情况:上下文见过,但某个后继没见过,概率可以明确为零;连上下文都没见过,未平滑的分布就无法定义。加一让每个候选都获得正概率,也挤占了原先其他候选的份额。增大 N 能保留更多前文,却增加了可能需要数据支持的组合。固定窗口之外的线索,则不是单靠多计数就能找回来的。
本书把第一个阶段成果记作 MG-0。它就是这样一个可以用纸笔完成的模型:重建表 4.3—4.4,分别计算未平滑和平滑后的完整序列概率,解释固定的生成过程,再分析未见组合和模型分不清的两段前文。把这些计算与第 1 章的评测、失败和风险表一同保存。这里不需要浏览器实验或编程环境。
第 5 章 开始补充向量、矩阵和形状的基础。完成这些核心内容及 Primer-PY 后,后续编程练习会再回来实现一个可以改变 N 的字符语言模型。第 6、7 章继续讲概率、损失和参数学习。第一部分已经让我们知道,为什么需要这些工具,以及希望用它们回答什么问题。
4.12 习题
Section titled “4.12 习题”- 阶数与边界。按本章约定,5-gram 使用前面几个 Token,开头需要补几个 BOS?一条三个字符的训练字符串提供几次预测?
- 更换训练数据。把训练字符串换成
ab、ac、ac,写出完整 Bigram 计数表,再算未平滑时完整 ac 的概率。与表 4.4 相比,哪些行变了? - 前缀与完整字符串。按表 4.7,分别计算以 ab 开头和恰好生成完整 ab 的概率,说明两者相差的那一项是什么。再算空字符串的完整概率。
- 改变平滑量。给原始 a 行的每个候选都加二,算出新分布,并检查总和。有人只改分子,分母仍用三,会错在哪里?
- 没见过的上下文。分别查看 Trigram 未平滑和加一平滑时的 a a 行。为什么原来四个零不能直接当作均匀分布?平滑后四项相等,又为什么只是估计方法的设定?
- 生成与训练。假设模型生成了 aa,原计数表为什么不会变?如果另把 aa 加为第四条训练字符串,再训练一次,a 行会怎样变,未平滑时完整 aa 的概率又是多少?
- 加入更长的字符串。回到原始三条训练数据,加入第四条 aba。写出新增加的所有转移、修改后的 a 行和 b 行,再算未平滑时完整 aba 的概率。
- 为什么没能重现?两个实现使用同一张计数表和种子 42,一个按
[a,b,c,EOS]分配区间,另一个按[c,b,a,EOS]分配。生成过程一定相同吗?只给种子,还缺了哪项约定? - 模型分不清的两段前文。为 5-gram 写出两个不同前缀:最后四个 Token 相同,但正确的下一项取决于更早的区别。为什么训练再多,也不能让它只凭这四个 Token 分清两种情况?
- 检查评测结论。一份报告说,加一让模型有可能生成 aa,所以加一总是更好。请用表 4.8 举出反例,并说明要作出有依据的比较,必须先规定哪些条件。
4.13 参考解答
Section titled “4.13 参考解答”- 使用前面四个 Token,开头补四个 BOS。三个字符加上 EOS,共提供四次预测。补在上下文中的 BOS 不算预测目标。
- BOS 这一行是
[3,0,0,0],a 行是[0,1,2,0],b 行是[0,0,0,1],c 行是[0,0,0,2],只有 BOS 行没变。完整 ac 的概率为 1 × 2/3 × 1 = 2/3。现在重复的是 ac,所以在 a 后面,c 的比例变大了,b 的比例变小了。 - 前缀 ab 的概率是 4/7 × 3/7 = 12/49;完整 ab 还要在 b 后选 EOS,概率为 12/49 × 3/6 = 6/49。多乘的这一项,表示恰好在这里结束。空字符串则从 BOS 直接选 EOS,概率为 1/7。
- 加二后是
[2,4,3,2],总数 11,分布为[2/11,4/11,3/11,2/11],相加等于一。若仍除以三,总和会是 11/3。分子变了,就必须按全部新数值的总和重新归一化。 - 未平滑时分母为零,分布没有定义。加一后四项各为一,除以总数四,得到四个 1/4。相等是因为我们在没有样本时给四项加了同样的数,不是因为观察到了相等的真实频率。
- 生成只读概率、选 Token,没有增加训练样本。只有明确把 aa 纳入训练,才新增 BOS→a、a→a、a→EOS。此时 BOS 行变为
[4,0,0,0],a 行变为[1,2,1,1],总数五。未平滑的完整 aa 概率为 1 × 1/5 × 1/5 = 1/25。 - 新增 BOS→a、a→b、b→a、a→EOS,总预测次数成为 13。a 行是
[0,3,1,1],合计五;b 行是[1,0,0,2],合计三。完整 aba 的概率为 1 × 3/5 × 1/3 × 1/5 = 1/25。最后一个字符是 a,所以结束概率要查 a 行,不能查 b 行。 - 不一定。到了上下文 a,抽样值 0.088125 按第一种顺序会选 b,按第二种顺序却选 c,因为这时 c 占据最前面的 1/3。种子只规定伪随机生成器的初始状态,不规定哪个候选对应哪个区间。还要记录词表顺序、生成器算法、参数、输入和停止规则。
- 例如
red key x x x x和blue key x x x x,预先规定下一步输出前面提到的颜色。两者最后四个 Token 都是 x,因此同一个固定函数只能返回相同分布。想区分颜色,就得保留四个 Token 之外的信息。这里的任务要求是事先规定的,不是在所列前缀末尾再接一条指令,否则末尾四个 Token 会变。 - 未平滑的 Bigram 给完整 ac 的概率是 1/3,加一后降为 16/245。如果指标只看 ac 的完整概率,这次调整反而更差。因此,要先说清用哪些数据、评测时如何避免重新拟合、Token 和边界怎么规定、用什么指标,以及结论针对哪类真实任务或哪一个诊断问题。
4.14 参考文献
Section titled “4.14 参考文献”- Jurafsky 与 Martin,Speech and Language Processing 第 3 章:N-gram Language Models。2026 年 8 月 19 日稿的 §§3.1–3.3 讲解 N-gram 计数、序列概率和评测,§3.6.1 讲解加一平滑,可以进一步了解本章的计数模型。本章全部数值例题均为原创。
- Bengio 等,A Neural Probabilistic Language Model(2003)。论文通过学习表示,让不同输入利用相关的统计信息。对于本章随着 N 增大而出现的数据稀疏问题,这提供了一条继续探索的思路。