跳转到内容

第 4 章 从 N-gram 到 Bigram:手算语言模型

4.1 看过的文本,怎样变成预测的依据?

Section titled “4.1 看过的文本,怎样变成预测的依据?”

前面两章讲了文本怎样变成 Token 和数值表示,但还有一个问题没回答:下一 Token 的概率是从哪里来的?神经网络内部保存了大量数值,不容易一眼看清。我们先搭建一个简单模型,把它学到的全部内容放进一张小表。

想象你要补全“猫坐在……上”,手里有一些现成的例句。一种起步办法是数一数:“在”后面接过哪些片段,各出现了多少次?常出现的,就多给一些被选中的机会。这个办法的好处是,每次预测依据了哪些例子,都能查清。

为了把计算走完,我们先用几个字母代替日常文字。这些字母没有暗指什么含义,只是让我们专心看顺序和次数,不必同时处理词表和事实问题。按顺序写在一起的一串字符,叫作字符串。训练数据只有三条:

样例 1:ab
样例 2:ab
样例 3:ac

ab 出现了两次,就要数两次,不能去重。本章把每个字母当作一个 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,含结束标记预测次数
第一条 aba、b、EOS3
第二条 aba、b、EOS3
aca、c、EOS3
合计a:3;b:2;c:1;EOS:39

每次预测,模型都要给四个候选分配概率。组成概率分布的各项不能小于零,加起来必须等于一。上下文怎么取、概率怎么估计,一旦规定下来,每一步都按同样的规则处理。

例 4.1——用 Unigram 建立基线。先不看前文,只统计单个 Token,这种办法叫作一元模型(Unigram)。表 4.1 一共列出九次预测,其中 b 出现两次,所占比例就是 2/9,称为 b 的频率。我们的第一种估计办法很直接:就用观察到的这个比例,作为以后预测时给 b 的概率。其余候选也一样,分别用出现次数除以总次数九。无论预测哪个位置,都用同一组概率:

下一 Token次数概率
a33/9
b22/9
c11/9
EOS33/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 末尾时,取什么上下文
1Unigram0空
2Bigram1b
3Trigram2a b
NN-gramN−1最后 N−1 个 Token

Trigram 虽然叫“三元”,每次仍然只预测一个 Token,另外两个来自前文。序列刚开始时,前面还没有足够的 Token,就用 N−1 个 BOS 补齐。这些标记只提供上下文,不算预测目标。Unigram 不看前文,也就不需要补 BOS。

4.4 用前一个 Token 作条件:Bigram 模型

Section titled “4.4 用前一个 Token 作条件:Bigram 模型”

把前一个 Token 也写出来,三条训练字符串就成为:

BOS → a → b → EOS
BOS → a → b → EOS
BOS → a → c → EOS

第一行可以读成三道已经知道答案的题:开头接什么?a。a 后面接什么?b。b 后面接什么?结束。一个箭头记下“在这个上下文之后,出现了这个 Token”,称为一次转移。预测总共仍是九次,只是现在每次都记着对应的上下文。

例 4.2——从零开始建立计数表。先把计数全部设为零,再依次处理这些箭头。

表 4.3——每读到一次转移,就给对应位置加一。

次序读到的转移该项累积次数
1BOS→a1
2a→b1
3b→EOS1
4BOS→a2
5a→b2
6b→EOS2
7BOS→a3
8a→c1
9c→EOS1

预测时需要方便地找到这些次数,所以把它们整理成一张表。行写“前面是什么”,列写“后面是什么”,交叉的格子记次数。例如,a 行与 b 列相交的位置,填入 a→b 出现的两次。某个格子仍为零,就表示从未见过这一对。这样得到完整计数表。

表 4.4——Bigram 的完整计数表。

前一个 Token后面是 a后面是 b后面是 c后面是 EOS行合计
BOS30003
a02103
b00022
c00011

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,接下来各项的概率是多少?所以必须先选出符合条件的那一行,再在这一行内归一化。

用 C(h,x)C(h,x) 表示上下文 h 后面出现 x 的次数,用 C(h)C(h) 表示这一行的计数总和。根据出现次数估计条件概率,公式就是:

P0(x∣h)=C(h,x)C(h),C(h)>0.P_0(x\mid h)=\frac{C(h,x)}{C(h)},\qquad C(h)>0.

竖线表示“在给定条件下”:左边 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.3——计算 ab 的概率。现在给定字符串 ab,不让模型自由生成,而是算它完整生成这条字符串的概率。它必须先选 a,再选 b,最后结束,也就是沿 BOS→a→b→EOS 走完三步。

必须经过的转移条件概率
BOS→a1
a→b2/3
b→EOS1

三步都按要求完成的概率,是这些条件概率的乘积:

P0(ab,EOS∣BOS)=1×23×1=23.P_0(\text{ab,EOS}\mid\text{BOS})=1\times\frac23\times1=\frac23.

为什么相乘?第一步一定到 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,则是模型另作的假设,并不是链式法则要求的。用 xtx_t 表示第 t 个 Token,x<tx_{<t} 表示它前面的全部 Token,hth_t 表示 N-gram 取出的末尾片段。模型用这段有限的前文来近似完整历史:

P(xt∣x<t)≈PN(xt∣ht),∣ht∣=N−1.P(x_t\mid x_{<t})\approx P_N(x_t\mid h_t),\qquad |h_t|=N-1.

下标中的 t 表示第几个位置,不是相乘;前文下标中的 < 表示“在这个位置之前”;h 两边的竖线表示上下文有多长。对 Bigram 和字符串 ab 来说,三次预测所看的上下文依次是 BOS、a、b;换成 Trigram,则依次是 BOS BOS、BOS a、a b。变的是每一步参考多少前文。

一条序列若有 T 个普通 Token,再以 xT+1=EOSx_{T+1}=\mathrm{EOS} 结束,那么这个模型给出的完整序列概率为:

PN(x1,…,xT,EOS)=∏t=1T+1PN(xt∣ht).P_N(x_1,\ldots,x_T,\mathrm{EOS})=\prod_{t=1}^{T+1}P_N(x_t\mid h_t).

符号 ∏ 表示把列出的各项全部相乘,这里每个位置对应一项。它下面的 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 后面接什么

样例 1BOS → a → b → EOS
样例 2BOS → a → b → EOS
样例 3BOS → a → c → EOS
a 后面依次是:bbc
b: 2 / 3三张中的两张
c: 1 / 3三张中的一张

计数表不再改动。接下来有两种用法 ↓

生成

按这里的概率选 b 或 c,接到序列末尾,再查下一行。

评测

下一项已知是 b,就取概率 2/3;已知是 c,就取 1/3。

两种用法都只读取计数,不增加训练样本。

图 4.1——同一张计数表,既可用于生成,也可用于评测;两者都只读表。 本书原创图示 · CC BY-SA 4.0。

图 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、未做平滑时的生成过程。

步骤上下文抽样值,已舍入选中什么
1BOS0.252345a;其概率为 1
2a0.088125b;抽样值小于 2/3
3b0.577281EOS;其概率为 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],合计 30/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 用平滑给未见组合留一点余地”

沿用抽签袋的想法,可以往每个袋子里给每个候选各加一张纸条,空袋子也照样加。这样,每个候选都有机会被抽到,每个袋子也都有纸条可抽。换成数值计算,就是先给每个候选的计数加一个正数,再归一化。如果都加一,就叫加一平滑(add-one smoothing)。仍以 a 行为例。

例 4.5——a 行加一后,概率怎样变化。

候选实际出现次数加一后的数值调整后概率
a011/7
b233/7
c122/7
EOS011/7
合计371

加进去的四个一,是估计方法规定的调整,不是又观察到了四条数据。候选有四个,所以分母从三变成七。BOS 不参与预测,不能把它算作第五个候选。分子都加了,分母也必须重算,否则概率之和会超过一。

用 α\alpha 表示给每项增加的数,用 VV 表示可预测的候选总数。本章比较 α 为零和一的情况,V 为四。一般的加 α 平滑写作:

Pα(x∣h)=C(h,x)+αC(h)+αV.P_\alpha(x\mid h)=\frac{C(h,x)+\alpha}{C(h)+\alpha 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)
BOS4/71/71/71/7
a1/73/72/71/7
b1/61/61/63/6
c1/51/51/52/5

例如,b 行原来是 [0,0,0,2],加一后成为 [1,1,1,3],总数六。到 b 后面结束的概率,就从一降为 3/6。接下来算完整字符串时,每一步都要查这张新表。

例 4.6——重算平滑后的完整序列概率。要生成 aa,先从 BOS 选 a,再从 a 选 a,最后从 a 选 EOS:

P1(aa,EOS)=47×17×17=4343≈0.011662.P_1(\text{aa,EOS})=\frac47\times\frac17\times\frac17=\frac4{343}\approx0.011662.

对于训练中出现过的两条字符串,分别得到:

P1(ab,EOS)=47×37×36=649≈0.122449,P_1(\text{ab,EOS})=\frac47\times\frac37\times\frac36=\frac6{49}\approx0.122449, P1(ac,EOS)=47×27×25=16245≈0.065306.P_1(\text{ac,EOS})=\frac47\times\frac27\times\frac25=\frac{16}{245}\approx0.065306.

原先概率为零的 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,α=01/81 ≈ 0.0123461/27 ≈ 0.037037
Bigram,α=01/3 ≈ 0.3333330
Trigram,α=01/3 ≈ 0.3333330
Unigram,α=132/2197 ≈ 0.01456564/2197 ≈ 0.029131
Bigram,α=116/245 ≈ 0.0653064/343 ≈ 0.011662
Trigram,α=116/245 ≈ 0.0653061/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 章会进一步介绍概率工具,以及怎样在评测时考虑序列长度。

上下文更长,可以分清原先分不清的前文。例如,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 的片段,形式上有 VNV^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 章继续讲概率、损失和参数学习。第一部分已经让我们知道,为什么需要这些工具,以及希望用它们回答什么问题。

  1. 阶数与边界。按本章约定,5-gram 使用前面几个 Token,开头需要补几个 BOS?一条三个字符的训练字符串提供几次预测?
  2. 更换训练数据。把训练字符串换成 ab、ac、ac,写出完整 Bigram 计数表,再算未平滑时完整 ac 的概率。与表 4.4 相比,哪些行变了?
  3. 前缀与完整字符串。按表 4.7,分别计算以 ab 开头和恰好生成完整 ab 的概率,说明两者相差的那一项是什么。再算空字符串的完整概率。
  4. 改变平滑量。给原始 a 行的每个候选都加二,算出新分布,并检查总和。有人只改分子,分母仍用三,会错在哪里?
  5. 没见过的上下文。分别查看 Trigram 未平滑和加一平滑时的 a a 行。为什么原来四个零不能直接当作均匀分布?平滑后四项相等,又为什么只是估计方法的设定?
  6. 生成与训练。假设模型生成了 aa,原计数表为什么不会变?如果另把 aa 加为第四条训练字符串,再训练一次,a 行会怎样变,未平滑时完整 aa 的概率又是多少?
  7. 加入更长的字符串。回到原始三条训练数据,加入第四条 aba。写出新增加的所有转移、修改后的 a 行和 b 行,再算未平滑时完整 aba 的概率。
  8. 为什么没能重现?两个实现使用同一张计数表和种子 42,一个按 [a,b,c,EOS] 分配区间,另一个按 [c,b,a,EOS] 分配。生成过程一定相同吗?只给种子,还缺了哪项约定?
  9. 模型分不清的两段前文。为 5-gram 写出两个不同前缀:最后四个 Token 相同,但正确的下一项取决于更早的区别。为什么训练再多,也不能让它只凭这四个 Token 分清两种情况?
  10. 检查评测结论。一份报告说,加一让模型有可能生成 aa,所以加一总是更好。请用表 4.8 举出反例,并说明要作出有依据的比较,必须先规定哪些条件。
  1. 使用前面四个 Token,开头补四个 BOS。三个字符加上 EOS,共提供四次预测。补在上下文中的 BOS 不算预测目标。
  2. 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 的比例变小了。
  3. 前缀 ab 的概率是 4/7 × 3/7 = 12/49;完整 ab 还要在 b 后选 EOS,概率为 12/49 × 3/6 = 6/49。多乘的这一项,表示恰好在这里结束。空字符串则从 BOS 直接选 EOS,概率为 1/7。
  4. 加二后是 [2,4,3,2],总数 11,分布为 [2/11,4/11,3/11,2/11],相加等于一。若仍除以三,总和会是 11/3。分子变了,就必须按全部新数值的总和重新归一化。
  5. 未平滑时分母为零,分布没有定义。加一后四项各为一,除以总数四,得到四个 1/4。相等是因为我们在没有样本时给四项加了同样的数,不是因为观察到了相等的真实频率。
  6. 生成只读概率、选 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。
  7. 新增 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 行。
  8. 不一定。到了上下文 a,抽样值 0.088125 按第一种顺序会选 b,按第二种顺序却选 c,因为这时 c 占据最前面的 1/3。种子只规定伪随机生成器的初始状态,不规定哪个候选对应哪个区间。还要记录词表顺序、生成器算法、参数、输入和停止规则。
  9. 例如 red key x x x x 和 blue key x x x x,预先规定下一步输出前面提到的颜色。两者最后四个 Token 都是 x,因此同一个固定函数只能返回相同分布。想区分颜色,就得保留四个 Token 之外的信息。这里的任务要求是事先规定的,不是在所列前缀末尾再接一条指令,否则末尾四个 Token 会变。
  10. 未平滑的 Bigram 给完整 ac 的概率是 1/3,加一后降为 16/245。如果指标只看 ac 的完整概率,这次调整反而更差。因此,要先说清用哪些数据、评测时如何避免重新拟合、Token 和边界怎么规定、用什么指标,以及结论针对哪类真实任务或哪一个诊断问题。