讨论 2023-10-18 = ###### tags: `tutorials` `Fudan` `2023` 自然语言的数字化 = ## 前文回顾 我们上次介绍了如何利用全概率公式对一段文本进行拆解,$$P(X) = P(x_1, ..., x_T) = P(x_1)\prod_i P(x_i | x_{<i}) $$ 以及在模型建模时的关键优化,参数在时间维度共享。$$ P_\theta (x_1 | <bos>) \prod_i P_\theta (x_i | x_{<i}) $$ 本次我们将进一步介绍上述思想的具体实现,同时我们是怎样在模型中存储和表达自然语言的。 ## 文本序列的单位 在全概率公式中,一段文本或字符串被查分成多个部分,我们的第一个问题就是如何拆分,以什么单位去拆分?考虑一段文本,“今天不下雨。”,我们可以按汉字,把它拆解成 “今 天 不 下 雨 。”,也可以按词,“今天 不 下雨”,还可以按照在机器中的unicode编码或其他各种方式进行拆分。 直观地讲,很难评价这些分解方法的优劣,我们通常会从以下几个角度来评判一种分解方法: - 是否覆盖全部输入文本,比如分解后的每个元素是否在一个预先设定的词表中。 - 分解过程把原始文本压缩了多少?给定一个分解方法及语料,就确定了一种编码方式,我们可以计算出和原始编码(GBK或Unicode)相比的信息压缩率。 - 长度问题,分解方法直接决定了文本序列的长度,长度对语言模型的建模有多方面的影响,通常来讲,模型能处理的长度是有限的。 - 组合问题,有些分解方法会把一个完整的概念分成多个部分,比如把“麦克风”拆分成“麦 克 风”,那么就需要模型把三个字的意思再组合成做正确的意思,但很多语义是难以根据它的组成部分进行推测的。 目前最为常用的分解方法有三种,字,词,和词元(token)。字和词容易理解,而词元是用统计方法在预料中学习得到一种分解方法,通常它的粒度介于字和词之间,可以认为是词的组成部分。 词元的寻找过程其实是一个编码构建的过程,我们通常采用 Byte-Pair Encoding (BPE)方法来进行构建。 在介绍BPE算法之前,我们可以回顾一下经典的霍夫曼编码,如下图所示。 ![](https://hackmd.io/_uploads/ByzFNDJk6.png) 霍夫曼编码是把每个元素用不同长度的编码进行表示。而我们构建词元的过程是把不同长度的连续元素块用相同的编码长度进行表示。我们希望构建许多词元,每个词元都用一个整数表示,但对应了不同长度的文本片段。比如词元可以表示“今天”,“不”,“下”,“雨”等。 类比霍夫曼编码,因为每个词元的编码长度相同,我们希望词元尽可能覆盖那些高频的文本片段。BPE算法就是遵循这一原则进行贪心寻找最佳编码方案,具体为 1. 统计语料中所有相邻两个元素的共同出现频率 2. 选取频率最高的相邻元素,组成一个词元,并且当作一个新元素放回语料 3. 重复第一步知道词元数量达到预设的值 实践中,大部分词元构建是从字开始的,并且规定不能超过一个词,即不允许词组作为一个词元。 我们可以看一个例子 |Step|Seq|New Token| |-|-|-| |1|aaabaaaccde|aa->A| |2|AabAaccde|Aa->B| |3|BbBccde|Bb->C| |4|CBccde|...| ## N元语言模型 在确定分解方法后,我们可以清晰地确定$P_\theta \prod_i P_\theta (x_i | x_{<i})$ 中的 $x_i$具体是什么了。在讲解神经网络之前,有一个简单的方法来构建语言模型,即$P_\theta (x_i | x_{<i})\approx P_\theta (x_i | x_{i-n+1},...,x_{i-1})$,这种方法把问题近似为文本序列中的一个元素和之前n个元素间的关系。如果n的值很小,那么我们可以直接在一个语料中统计出对应的频率。我们通常把这种方法称之为n元模型,并且$n\leq5$。 具体来讲$P(x_i|x_{i-n+1:i-1}) \triangleq \frac{C(x_{i-n+1:i})}{\sum_\hat{x} C(x_{i-n+1:i-1}\oplus \hat{x}) }\quad,$ 其中C表示在语料中的统计次数。 但就算n很小,我们还是会经常碰到某一组$x_{i-n:i-1}$没有在语料中出现过的情况,那么这时候n-gram语言模型就会认为相应的概率为0,但这很多时候都是不正确的。为此,过去的经典应对方法就是引入平滑函数,即对于没见过的文本给予一个很小的概率。 比如+1方法, $P(x_i|x_{i-n+1:i-1}) \triangleq \frac{1+C(x_{i-n+1:i})}{V+\sum_\hat{x} C(x_{i-n+1:i-1}\oplus \hat{x}) }\quad$ 其中$V$为词表大小,也就是所有出现过的N元组种类。 回退方法,如果n-gram不存在,那么n-1 gram如果存在,可以用相应的概率作为替代,即用 $x_{i-n+2:i-1}$去替代 $x_{i-n+1:i-1}$,如果n-1没有,那么就n-2,以此类推。 插值方法,回退方法的延伸,如果n-gram不存在,用1~n-1 gram线性组合来估计缺少的概率。 归类方法,假如我们有一个同义词词典,或通过其他手段对n-gram进行聚类,根据聚类结果形成一些等价类,即等价类中的每个元素的含义相同。这样,我们就可以统计每个等价类的频率而不是具体n-gram的频率了,只要我们可以对没见过的n-gram进行归类,就可以得到正确的概率。 其实插值方法和归类方法已经展现了神经N元模型的雏形,我们把语言模型进行抽象,可以得到如下一种形式: - 我们求得了一个前文表示, $r_i=prev(x_{i-n+1:i-1})$ - 我们根据训练语料中的所有前文表示以及下一个词元,$(r_j, x_j) \in D$,学习了一个函数$f(r_j) \rightarrow x_j$ - 我们根据认为$f$适用于任意$r$的,所以可以通过$f(r_i) \rightarrow x_i$,以此生成文本 而大部分语言模型(特例会在以后讨论)都在研究不同形式的$f$ ## 自回归生成 ![](https://hackmd.io/_uploads/ByCOIyl16.png) 语言模型的一个重要用途就是文本生成,在拥有了一个建模好的语言模型后,我们可以利用自回归生成的方式来进行文本生成。比如我们有了一个N元语言模型,可以按照上图所示,逐个从左到右生成每一个字,最后组成一句话。 以N=3的三元语言模型为例, |Step|Prob|Seq| |-|-|-| |1|P(x)->自|自| |2|P(x\|自)->回| 自回| |3|P(x\|自回)->归| 自回归| |4|P(x\|回归)->生|自回归生| |5|P(x\|归生)->成|自回归生成| |6|P(x\|生成)->是|自回归生成是| 在上面的例子中,我们可以看到每一步都只生成了一个汉字,并且生成的内容拼接起来重新作为下一步的输入。 这里其实可以引出文本拆解成字,词,词元的另一面影响,比如P(x=生成|回归)和P(x=生|回归) P(x=成|归生) 的区别。因为每一步概率估计都是有误差的,拆分成两个概率就意味着会有累积误差。 ### 采样过程的重要性 通常,语言模型在每一步的预测都是有很强的不确定性的,比如一个前文后面有三种可能的说法,这些不确定性的存在导致语言模型有多种多样的采样方法,它们的适用场景也不相同。但需要注意的是自回归生成是非常强调临近元素之间的一致性的,因为每一个元素是根据采样出的前文来确定的。所以这种方式很适合生成等差数列等递推序列。但是比如让一段很长文本的首尾呼应,或者干脆生成回文字符串,对于自回归生成都是有难度的。 在研究N元模型过程中,人们发现N元模型已经可以生成流畅的文本了,并且消减了大部分的不确定性,随之发现了自然语言的关联主要是临近关联。 ![](https://hackmd.io/_uploads/r1ZDpdTZT.png)