1.1.1 分词器
在正式介绍Seq2Seq结构的模型之前,我们需要先了解“分词器”。人类之间的有效沟通完全依赖自然语言,这种语言包含无数复杂的词汇,而这些词汇对计算机而言却是完全陌生的。计算机是无法直接处理一个单词或者一个汉字的,因此在进行模型训练前,需要把人类能够理解的元素转化成计算机可以计算的向量。分词器正是为模型准备输入内容的,它可以将语料数据集预处理为模型可以接收的输入格式。对于文本格式的数据来说,分词器的作用就是将文本转换为词元序列,一个词元可以是一个字母、一个单词、一个标点符号或者一个其他符号,而这个过程也被称为分词(tokenization)。
1.什么是分词
词元(token)可以理解为最小的语义单元,分词的目的是将输入文本转换为一系列的词元,并且还要保证每个词元拥有相对完整的独立语义。举个简单的例子,比如“Hello World!”这句话,可以将其分为4个词元,即["Hello", " ", "World", "!"],然后把每个词元转换成一个数字,后续我们就用这个数字来表示这个词元,这个数字就被称为词元ID,也叫token ID。例如,我们可以用表1-1来表示上面那句话的词元以及对应的ID,而最终“Hello World!”这句话就可以转换为“1 2 3 4”的数字序列。
表1-1 “Hello World!”的分词示例
那么,分词应该分到什么粒度呢?对于英文来说,分词的粒度从细到粗依次是character、subword、word。其中character表示的是单个字符,例如a、b、c、d。而word表示的是整个单词,例如water表示水的意思。而subword相当于英文中的词根、前缀、后缀等,例如unfortunately中的un、fortun(e)、ly等就是subword,它们都是有含义的。
对于中文来说,分词算法也经历了按词语分、按字分和按子词分三个阶段。按词语分和按字分比较好理解,也有一些工具包可以使用,例如jieba分词。如果是按照子词分,那么词元就可以是偏旁部首,当然对于比较简单的字,一个词元也可以是一个完整的字。例如,“江”“河”“湖”“海”这四个字都跟水有关,并且它们都是三点水旁,那么在分词的时候,“氵”很可能会作为一个词元,“工”“可”“胡”“每”是另外的词元。假如“氵”的词元ID为1,“工”“可”“胡”“每”的词元ID分别是2、3、4、5,那么“江”“河”“湖”“海”的词元序列就可以表示为1 2、13、1 4、1 5。这样做的好处是,只要字中带有三点水旁,或者词元序列中含有词元ID为1的元素,那么我们就可以认为这个字或者这个词元序列跟水有关。即使是沙漠的“沙”字,是由“氵”和“少”组成的,也可以理解为水很少的地方。
2.BPE分词算法
使用单个字母或单词作为词元会带来一些问题。首先,不同语言的词汇量不同,如果每个单词都需要分配唯一的词元ID,那么会占用大量内存空间。其次,一些单词可能很少出现或是新造词,如专有名词、缩写、网络用语等,如果要让模型一直能够处理这些单词,就需要不断更新词元ID表格并重新训练模型。
为了解决这些问题,GPT模型使用了BPE(Byte Pair Encoding,字节对编码)方法来分割文本。BPE是一种数据压缩技术,将文本分割为更小的子单元(subword),它们可以是单个字母、字母组合、部分单词或完整单词。BPE基于统计频率合并最常见的字母对或子单元对,这种方法能够更有效地处理不同语言的词汇量和新出现的单词。
接下来我们举一个例子来介绍BPE分词算法。首先我们需要知道,基于子词的分词算法有一个基本原则:常用字词尽量表示为单个词元,罕见词分解为多个词元。如果我们有一个语料库,要从头开始通过分词构建词表,首先要统计所有语料的词频,也就是计算每个单词在语料库中出现的频率,假如得到如下结果:{"class</w>": 1, "classified</w>": 2, "classification</w>": 3, "create</w>": 4, "created</w>": 5, "creation</w>": 6}。其中“</w>”是为了让算法知道每个单词的结束位置而在末尾添加的一个特殊符号。接下来将每个单词拆分成字符并再次统计其出现次数,特殊符号也被看作一个字符,可以得到如表1-2所示的词元频次表。
表1-2 词元频次表1
接下来我们要寻找出现得最频繁的字符对,然后进行合并操作。我们从出现次数最多的字符开始,即“e”。通过计数可以发现,“ea”出现的次数最多,出现了4+5+6=15次,因此我们将“e”和“a”合并为“ea”,之后更新词元频次表,得到表1-3。
表1-3 词元频次表2
继续合并,此时出现次数最多的是字符“c”,可以发现合并字符“cr”出现的次数最多,因此将这两个字符合并,更新词元频率表,得到表1-4。
表1-4 词元频次表3
再进一步合并,就能发现BPE编码的奇妙之处。接下来我们将“cr”和“ea”合并为“crea”,并且删除表中出现次数为0的词元,更新词元频次表,得到表1-5。
表1-5 词元频次表4
一直重复以上步骤,单个词元会被逐渐合并,这也是BPE算法的主要目标——数据压缩。我们可以在开始时设置一个阈值,每次更新后词元的总数可能会发生变化,当词元总数达到设定的阈值之后,就停止合并,此时的词元频次表中所有的词元就是我们最终要用的词表。
可以发现,当我们合并词元并更新词表的时候,总的词元数可能增加,可能减少,也可能保持不变。一般来说,当迭代次数较小时,大部分词元还是字母,基本上没有什么实质含义,但是当迭代次数增大时,常用词开始慢慢合并到一起。如图1-2所示,这是我们在一次实验中统计的词元总数的变化,通常来说,随着合并次数的增加,词元总数是先增大后减小的,最终停止变化的条件就是达到我们设置的词元总数的阈值,因此,合适的停止标准是需要调整的一个参数。
图1-2 BPE分词算法的词元总数随着迭代次数的增大而先增大后减小
词表建立了一个从词元到ID的对应关系。在模型处理输入时,将输入序列中的每一个元素的词元都映射为ID,然后根据ID找到词元对应的特征向量。模型最终的输出结果也是词元ID,再根据对应关系,将ID映射为词元进行输出。
BPE在分词时具有一些显著的优势。首先,BPE能够通过减少词元的数量,节省内存以及计算资源。其次,BPE能有效地处理未遇见或罕见的词汇,它会将这些词汇分解为已知的子单元。此外,它能捕获词语的形态变化,例如复数、时态和派生形式,这使模型能够学习词语之间的关联。然而,BPE也有一些缺点。例如,BPE可能会分割一些有语义意义的子单元,使完整的词汇被拆分为多个部分。再者,BPE可能会影响对特定符号或标记的处理,如HTML标签、URL、电子邮件地址等,这可能导致原始含义或格式的丧失。因此,BPE并不是一种完美无缺的方法,而是一种在减少词元数量与保留词元含义之间达成平衡的折中策略。
BPE是一种比较常用的分词算法,GPT-2、BART和Llama模型都采用了BPE分词算法。除此之外,还有其他一些分词算法。WordPiece的核心思想与BPE分词算法类似,但在子词合并的选择上并不是直接选择出现频率最高的相邻子词合并,而是选择能够提升语言模型概率的相邻子词进行合并。BERT模型采用的就是WordPiece算法。SentencePiece是另外一种分词算法,它将空格视作一种特殊字符进行处理,然后再用BPE算法来进行分词。ChatGLM、BLOOM和PaLM模型采用了SentencePiece分词算法。
另外,在数据集的处理过程中,加入StartToken、PadToken、EndToken等词元,可以帮助模型更好地理解数据,或者帮助下游应用进行编码。以Vicuna模型为例,将对话结束标记</s>加入微调数据集中,能使模型更有效地预测何时停止生成字符。