我们从一个任务开始:计算 $P(w|h)$,也就是在给定某个历史上下文 $h$ 的条件下,某个词 $w$ 出现的概率。
假设上下文 $h$ 是 The water of Walden Pond is so beautifully,我们想知道下一个词是“蓝色”的概率:
一种估算这个概率的方法是通过相对频率计数:找一个非常大的语料库,统计 The water of Walden Pond is so beautifully 这段上下文出现的次数,再统计它后面紧接着出现 blue 的次数。
这其实是在回答这样一个问题:“在我们看到 $h$ 历史中,有多少次后面紧接着出现了词 $w$?”如下所示:
如果有一个足够大的语料库,就可以通过这两个计数来估算这个概率。 但即使是整个互联网的文本也不足以提供对完整句子的准确估计。 这是因为语言具有创造性:人们不断在创造新的句子,我们无法指望对完整句子这样长的结构获得足够的统计次数。 因此,我们需要更聪明的方法来估计,在给定出现历史 $h$ 的条件下,某个词 $w$ 的概率,或者整个词序列 $W$ 的概率。
我们先从一些记号开始。 在本章中,我们继续使用“词”(word)这个术语,但在实际应用中,我们通常是基于**词元(token)**来建立语言模型的,比如上一章提到的 BPE 词元。 为了表示某个随机变量 $X_i$ 取值为“the”的概率,即 $P(X_i = \text{“the”})$,我们使用简化的记法 $P(\text{the})$。 一个由 $n$ 个词组成的序列,可以写成 $w_1 \cdots w_n$ 或者 $w_{1:n}$。 因此,$w_{1:n-1}$ 表示的是字符串 $w_1, w_2, \ldots, w_{n-1}$,也可以等价地写成 $w_{< n}$,可以理解为“从 $w_1$ 到 $w_{n-1}$ 的所有词”。 对于一个词序列中每个词都取特定值的联合概率 $P(X_1 = w_1, X_2 = w_2, X_3 = w_3, \ldots, X_n = w_n)$,我们简写为 $P(w_1, w_2, \ldots, w_n)$。
那么,如何计算像 $P(w_1, w_2, \ldots, w_n)$ 这样的整个序列的概率呢? 一个方法是使用概率链式法则(chain rule of probability)来分解这个联合概率:
$$ \begin{align*} P(X_1 \cdots X_n) &= P(X_1)P(X_2|X_1)P(X_3|X_{1:2}) \cdots P(X_n|X_{1:n-1}) \\ &= \prod_{k=1}^{n} P(X_k|X_{1:k-1}) \tag{3.3} \end{align*} $$将这个法则应用到词序列上,得到:
$$ \begin{align*} P(w_{1:n}) &= P(w_1)P(w_2|w_1)P(w_3|w_{1:2}) \cdots P(w_n|w_{1:n-1}) \\ &= \prod_{k=1}^{n} P(w_k|w_{1:k-1}) \tag{3.4} \end{align*} $$链式法则展示了计算一个词序列的联合概率,与给定上一个词计算当前词的条件概率之间的联系。 公式(3.4)表明,我们可以通过将一系列条件概率相乘,来估算整个词序列的联合概率。 但问题是,链式法则并没有真正帮我们解决困难! 我们并不知道如何准确计算一个词在很长的上下文之后出现的概率 $P(w_n|w_{1:n-1})$。 正如前面所说,不能简单地通过统计语料库中每个词在每种长字符串之后出现的次数来估计它。 因为语言是创造性的,很多特定的上下文可能从未在语料库中出现过!
3.1.1 马尔可夫假设
n元模型的依据是:与其基于一个词的全部历史来计算它的概率,不如用最近的几个词来近似这段历史。
以双词模型(bigram)为例,它将一个词在所有前面词语条件下的概率 $P(w_n|w_{1:n-1})$,近似为仅基于前一个词的条件概率 $P(w_n|w_{n-1})$。 换句话说,与其计算:
$$ P(\text{blue}|\text{The water of Walden Pond is so beautifully}) \tag{3.5} $$我们用下面这个概率来近似:
$$ P(\text{blue}|\text{beautifully}) \tag{3.6} $$当使用双词模型来预测下一个词的条件概率时,我们实际上是在做一个这样的近似:
$$ P(w_n|w_{1:n-1}) \approx P(w_n|w_{n-1}) \tag{3.7} $$这个假设认为,一个词的概率只依赖于它前面的一个词,这被称为马尔可夫假设(Markov assumption)。 马尔可夫模型是一类概率模型,它们假设我们不需要回顾太久的历史,就能预测未来某个单元(比如一个词)的概率。
我们可以将双词模型(只回顾一个词的历史)推广到三词模型(回顾两个词的历史),进而推广到更一般的n元模型(回顾 $n-1$ 个词的历史)。
下面我们来看一个用于序列中下一个词条件概率的通用n元模型近似公式。 这里我们用 $N$ 表示n元的大小,例如 $N=2$ 表示双词模型,$N=3$ 表示三词模型。 那么,对一个词在完整上下文中的概率做出如下近似:
$$ P(w_n|w_{1:n-1}) \approx P(w_n|w_{n-N+1:n-1}) \tag{3.8} $$在双词模型的假设下,我们可以通过将公式(3.7)代入公式(3.4),来计算一个完整词序列的概率:
$$ P(w_{1:n}) \approx \prod_{k=1}^{n} P(w_k|w_{k-1}) \tag{3.9} $$3.1.2 如何估计概率
我们该如何估计这些双词模型(bigram)或n元模型的概率呢? 一种直观的方法叫做最大似然估计(Maximum Likelihood Estimation,简称MLE)。 通过从语料库中统计词频来获得n元模型参数的MLE估计值,然后对这些词频进行归一化,使它们落在0到1之间,成为概率。 对于概率模型来说,归一化意味着将某个计数除以一个总数,这样得到的结果概率在 0 到 1 之间,而且所有概率之和为 1。
举个例子,要计算在给定前一个词 $w_{n-1}$ 的条件下,某个词 $w_n$ 出现的双词概率,可以先统计两个词同时出现的次数 $C(w_{n-1}w_n)$,然后用所有以 $w_{n-1}$ 开头的双词的总次数来归一化:
$$ P(w_n|w_{n-1}) = \frac{C(w_{n-1}w_n)}{\sum_w C(w_{n-1}w)} \tag{3.10} $$可以简化这个公式,因为所有以某个词 $w_{n-1}$ 开头的双词的总数,其实就等于这个词 $w_{n-1}$ 本身在语料中出现的次数(你可以稍作思考来确认这一点):
$$ P(w_n|w_{n-1}) = \frac{C(w_{n-1}w_n)}{C(w_{n-1})} \tag{3.11} $$我们来看一个例子,使用一个包含三句话的小型语料库。
为了能统计第一个词的双词上下文,需要在每句话的开头加上一个特殊符号 <s>;同时,还需要一个表示句子结束的特殊符号 </s>:
<s> I am Sam </s>
<s> Sam I am </s>
<s> I do not like green eggs and ham </s>
以下是基于这个语料库计算出的一些双词概率:
$$ \begin{align*} P(\texttt{I}|< \texttt{/s} >) &= \frac{2}{3} = 0.67 \\ P(< \texttt{/s} >|\texttt{Sam}) &= \frac{1}{2} = 0.5 \\ P(\texttt{Sam}|< \texttt{/s} >) &= \frac{1}{3} = 0.33 \\ P(\texttt{Sam}|\texttt{am}) &= \frac{1}{2} = 0.5 \\ P(\texttt{am}|\texttt{I}) &= \frac{2}{3} = 0.67 \\ P(\texttt{do}|\texttt{I}) &= \frac{1}{3} = 0.33 \end{align*} $$对于一般情况下的n元模型参数的最大似然估计,公式如下:
$$ P(w_n|w_{n-N+1:n-1}) = \frac{C(w_{n-N+1:n-1} w_n)}{C(w_{n-N+1:n-1})} \tag{3.12} $$公式 3.12(与公式 3.11 类似)通过将某个特定序列的出现频率除以它的前缀频率,来估计n元模型的概率。 这个比值被称为相对频率(relative frequency)。 我们上面提到的这种用相对频率来估计概率的方法,就是最大似然估计(MLE)的一个例子。 在 MLE 中,所得到的参数集合能够最大化训练语料 $T$ 在模型 $M$ 条件下的似然(即 $P(T | M)$)。 例如,假设在一个一百万词的语料库中,“Chinese”这个词出现了 400 次,那么从另一篇一百万词的文本中随机选一个词,它是“Chinese”的概率是多少呢? 它的 MLE 估计值是 $\frac{400}{1000000} = 0.0004$。 当然,0.0004 并不一定是“Chinese”这个词在所有场景下的最佳概率估计。 比如在另一个语料库或上下文中,“Chinese”可能是非常少见的词。 但这个估计值能使得“Chinese”在一百万词中出现 400 次的可能性最大。 在 3.6 节中,我们会介绍一些对MLE估计值进行微调的方法,从而获得更准确的概率估计。
让我们来看一些来自一个真实但规模很小的语料库的例子。这些数据来自已停止维护的伯克利餐馆项目(Berkeley Restaurant Project),这是上世纪的一个对话系统,能够回答有关加州伯克利市餐馆数据库的相关问题(Jurafsky 等,1994)。 以下是一些用户查询的样例(已经过文本标准化处理,包括转为小写字母,并去除了标点符号)(完整的 9332 个句子的样本可在本书网站上找到。):
can you tell me about any good cantonese restaurants close by
tell me about chez panisse
i’m looking for a good place to eat breakfast
when is caffe venezia open during the day
图 3.1 展示了部分来自伯克利餐馆项目语料库中双词模型(bigram)的一部分计数情况。 请注意,其中大多数单元格的值为0。 事实上,我们特意选择了这些彼此之间有一定关联的词;如果从随机选取的八个词中构建这样的矩阵,结果会更加稀疏。
| i | want | to | eat | chinese | food | lunch | spend | |
|---|---|---|---|---|---|---|---|---|
| i | 5 | 827 | 0 | 9 | 0 | 0 | 0 | 2 |
| want | 2 | 0 | 608 | 1 | 6 | 6 | 5 | 1 |
| to | 2 | 0 | 4 | 686 | 2 | 0 | 6 | 211 |
| eat | 0 | 0 | 2 | 0 | 16 | 2 | 42 | 0 |
| chinese | 1 | 0 | 0 | 0 | 0 | 82 | 1 | 0 |
| food | 15 | 0 | 15 | 0 | 1 | 4 | 0 | 0 |
| lunch | 2 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
| spend | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
图 3.1:伯克利餐馆项目语料库中 8 个词(共 1446 个词)的双词计数表。零计数用灰色表示。每个单元格表示列标签词出现在行标签词之后的次数。例如,行标签为 i、列标签为 want 的单元格值为 827,表示在语料库中,“want”出现在“i”之后的次数为 827 次。
图 3.2 展示了归一化后的双词概率(即将图 3.1 中每一行的数值除以该行对应的一元词频,这些一元词频来自以下词频统计):
| i | want | to | eat | chinese | food | lunch | spend |
| 2533 | 927 | 2417 | 746 | 158 | 1093 | 341 | 278 |
以下是一些其他有用的双词概率:
$$ \begin{align*} P(\texttt{i}|< \texttt{s} >) &= 0.25 \\ P(\texttt{english}|\texttt{want}) &= 0.0011 \\ P(\texttt{food}|\texttt{english}) &= 0.5 \\ P(< \texttt{/s} >|\texttt{food}) &= 0.68 \\ \end{align*} $$现在,我们可以计算像 I want English food 或 I want Chinese food 这类句子的概率,只需将对应的双词概率相乘即可。例如:
$$ \begin{align*} &P(< \texttt{s} > \texttt{i want english food} < \texttt{/s} >) \\ = &P(\texttt{i}|< \texttt{s} >)P(\texttt{want}|\texttt{i})P(\texttt{english}|\texttt{want})P(\texttt{food}|\texttt{english})P(< \texttt{/s} >|\texttt{food}) \\ = &0.25 \times 0.33 \times 0.0011 \times 0.5 \times 0.68 \\ = &0.000031 \end{align*} $$| i | want | to | eat | chinese | food | lunch | spend | |
|---|---|---|---|---|---|---|---|---|
| i | 0.002 | 0.33 | 0 | 0.0036 | 0 | 0 | 0 | 0.00079 |
| want | 0.0022 | 0 | 0.66 | 0.0011 | 0.0065 | 0.0065 | 0.0054 | 0.0011 |
| to | 0.00083 | 0 | 0.0017 | 0.28 | 0.00083 | 0 | 0.0025 | 0.087 |
| eat | 0 | 0 | 0.0027 | 0 | 0.021 | 0.0027 | 0.056 | 0 |
| chinese | 0.0063 | 0 | 0 | 0 | 0 | 0.52 | 0.0063 | 0 |
| food | 0.014 | 0 | 0.014 | 0 | 0.00092 | 0.0037 | 0 | 0 |
| lunch | 0.0059 | 0 | 0 | 0 | 0 | 0.0029 | 0 | 0 |
| spend | 0.0036 | 0 | 0.0036 | 0 | 0 | 0 | 0 | 0 |
图 3.2 伯克利餐馆项目语料库中 8 个词的双词概率(语料库包含 9332 个句子)。零概率用灰色表示。
我们把计算 i want chinese food 的概率作为练习 3.2 留给读者完成。
这些双词统计信息捕捉到了哪些语言现象呢? 其中一些双词概率反映了一些我们认为严格来说属于句法层面的事实。 例如,“eat”后面通常接名词或形容词,“to”后面通常接动词。 还有一些概率则可能与具体任务有关,比如在个人助理任务中,句子以“I”开头的概率很高。 甚至还有一些统计结果反映的是文化现象,而非纯粹的语言结构,比如人们更倾向于寻找“Chinese food”而不是“English food”。
3.1.3 在大规模n元模型中处理规模问题
在实际应用中,语言模型可能非常庞大,这会带来一些实际操作上的挑战。
对数概率(Log Probabilities) 语言模型中的概率通常在对数空间中,以对数概率进行存储和计算。 这是因为概率的定义本身小于或等于1,因此相乘的次数越多,结果就越趋近于零,最终可能导致数值下溢(numerical underflow)。 对数空间中的加法等价于线性空间中的乘法操作,所以我们把对数概率相加。 使用对数概率相加来代替概率惩罚,可以避免数值过小的问题。 所有计算和存储都在对数空间中进行,只有在最后需要输出概率值时,才通过取指数(exp)将其转换回普通概率值:
$$ \begin{align*} &p_1 \times p_2 \times p_3 \times p_4 \\ = &\exp(\log p_1 + \log p_2 + \log p_3 + \log p_4) \tag{3.13} \end{align*} $$在本书中,除非特别说明,我们提到的 $log$ 都指的是自然对数($ln$)。
更长的上下文 虽然为了教学目的我们目前只介绍了双词模型(bigram),但在训练数据足够的情况下,我们会使用三词模型(trigram):基于前两个词来预测当前词;或者四词模型(4-gram)、五词模型(5-gram)等。 对于这些更长的n元模型,我们需要在句子开头和结尾添加额外的上下文。 例如,在句子的最开始计算三词模型时,我们通常使用两个特殊的起始符号来构建第一个三词组合,例如 $P(\texttt{I}|< \texttt{s} >< \texttt{s} >)$。
目前已经构建了一些大规模的n元模型语料库,例如从《当代美国英语语料库》(Corpus of Contemporary American English,COCA)中提取百万个最高频n-gram。COCA是一个一个精选的、包含 10 亿词的美式英语语料库(Davies, 2020); 谷歌的网页五元语料库(Web 5-gram Corpus),基于1万亿词的英文网页文本(Franz 和 Brants, 2006); 谷歌图书n元语料库(Google Books Ngrams),包含8000亿词,涵盖中文、英文、法文、德文、希伯来文、意大利文、俄文和西班牙文(Lin 等,2012a)。
甚至可以使用非常长的n元上下文。 例如,无限n元模型(∞-gram,infini-gram)项目(Liu 等,2024)允许任意长度的n元组合。 其核心思想是避免使用(空间和时间上)昂贵的预计算来获取庞大的n-gram计数表。 而是使用一种高效的结构——缀数组(suffix arrays),在推理时快速计算任意长度的n元概率。 这种方法使得在包含 5 万亿词元的巨大语料库上,也能计算各种长度的n元组合。
构建大规模n元语言模型时,效率是一个关键因素。 通常会使用 4 到 8 位(bit)来表示概率,而不是 8 字节浮点数;将词字符串存储在磁盘上,在内存中仅使用 64 位哈希值表示;使用如“反向字典树”(reverse tries)等特殊结构来表示 n-gram。 对n元语言模型进行剪枝(Pruning)也很常见,例如只保留计数高于某个阈值的n元组合,或者使用熵(entropy)来剪除不太重要的n元组合(Stolcke, 1998)。 高效的工具包如 KenLM(Heafield, 2011;Heafield 等,2013)使用排序数组和归并排序算法,对大语料库以最少的遍历次数高效构建概率表。