文本分词器概述¶
在本页面中,我们将深入探讨文本分词。
在预处理教程中,我们了解到,分词是将文本拆分为单词或子词,然后通过查找表将其转换为ID。将单词或子词转换为ID的过程非常直观,所以在本文中,我们将重点介绍如何将文本拆分为单词或子词(即分词)。具体来说,我们将介绍在 🤗 Transformers 中使用的三种主要分词器:字节对编码(BPE)、WordPiece和SentencePiece,并展示不同模型使用哪种分词器类型的示例。
请注意,在每个模型页面上,您可以查看相关分词器的文档以了解预训练模型使用了哪种分词器类型。例如,查看BertTokenizer,可以看到该模型使用了WordPiece。
简介¶
将文本拆分为更小部分的任务比看起来要复杂得多,并且有多种方法可以实现。例如,考虑这句话:"Don't you love 🤗 Transformers? We sure do."
一种简单的分词方法是按空格拆分文本,结果会是:
["Don't", "you", "love", "🤗", "Transformers?", "We", "sure", "do."]
这是一个合理的初步步骤,但如果看分词"Transformers?"和"do.",我们会发现标点符号与单词"Transformers"和"do"连接在一起,这是不理想的。我们应该将标点符号与单词分开,以便模型不必为每个单词和可能跟随它的标点符号学习不同的表示法,这将使模型需要学习的表示法数量大幅增加。考虑标点符号后,我们的示例文本可以拆分为:
["Don", "'", "t", "you", "love", "🤗", "Transformers", "?", "We", "sure", "do", "."]
["Do", "n't", "you", "love", "🤗", "Transformers", "?", "We", "sure", "do", "."]
可以看到,这里使用了空格和标点分词以及基于规则的分词。空格和标点分词以及基于规则的分词都是词汇分词的示例,即大致定义为将句子拆分为单词。虽然这种方法最直观,但在处理海量文本语料库时可能会出现问题。例如,Transformer XL使用空格和标点分词,导致词汇量为267,735!
如此大的词汇量迫使模型需要一个巨大的嵌入矩阵作为输入和输出层,这不仅增加了内存,还增加了时间复杂度。通常,Transformer 模型的词汇量很少超过50,000,特别是在仅在一个语言上进行预训练的情况下。
因此,如果简单的空格和标点分词不令人满意,为什么不按字符进行分词呢?
虽然字符分词非常简单,并且可以大大减少内存和时间复杂度,但会使得模型更难学习有意义的输入表示。例如,学习"t"的有意义上下文无关表示比学习"today"的表示要困难得多。因此,字符分词通常伴随着性能损失。为了兼顾两者的优势,Transformer 模型使用介于词汇级和字符级分词之间的混合方法,称为子词分词。
子词分词¶
子词分词算法依赖于以下原则:常用单词不应拆分为更小的子词,而罕见单词应分解为有意义的子词。例如,"annoyingly"可能被视为一个罕见单词,可以拆分为"annoying"和"ly"。"annoying"和"ly"作为单独的子词会更频繁地出现,同时,"annoying"和"ly"的组合意义保留了"annoyingly"的含义。这对粘着语(如土耳其语)特别有用,因为可以使用子词连接形成几乎任意长的复杂单词。
子词分词使模型能够拥有合理的词汇量大小,同时学习有意义的上下文无关表示。此外,子词分词使模型能够处理从未见过的单词,通过将其分解为已知的子词。例如,BertTokenizer会将"I have a new GPU!"分词为:
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained("google-bert/bert-base-uncased")
tokenizer.tokenize("I have a new GPU!")
因为我们在使用小写模型,所以句子首先被转换为了小写。可以看到,词典中包含["i", "have", "a", "new"],但不包含"gpu",因此将其拆分为已知的子词["gp"和"##u"]。"##"表示该标记应连接到前一个标记,且中间没有空格(用于解码或逆转分词)。
作为另一个例子,XLNetTokenizer将我们之前的示例文本分词为:
from transformers import XLNetTokenizer
tokenizer = XLNetTokenizer.from_pretrained("xlnet/xlnet-base-cased")
tokenizer.tokenize("Don't you love 🤗 Transformers? We sure do.")
我们稍后会解释"▁"的含义(参见SentencePiece)。可以看到,罕见单词"Transformers"被拆分为了更常见的子词"Transform"和"ers"。
现在,让我们看看不同的子词分词算法是如何工作的。请注意,所有这些分词算法都依赖某种形式的训练,通常是在相关模型将要训练的语料库上进行训练。
字节对编码 (BPE)¶
字节对编码(BPE)首次引入于 Neural Machine Translation of Rare Words with Subword Units (Sennrich et al., 2015)。BPE依赖于预分词器将训练数据拆分为单词。预分词可以像空格分词一样简单,例如GPT-2、RoBERTa。更复杂的预分词包括基于规则的分词,例如XLM、FlauBERT使用Moses处理大多数语言,或GPT使用spaCy和ftfy,以统计训练语料库中每个单词的频率。
预分词后,会创建一组唯一的单词,并确定每个单词在训练数据中出现的频率。接下来,BPE创建一个包含所有唯一单词中出现的符号的基本词典,并学习合并规则,通过将两个基本词典中的符号组合为一个新符号。它会不断进行,直到词典达到所需的大小。请注意,所需的词典大小是需要在训练分词器之前定义的超参数。
例如,假设预分词后的词频如下:
("hug", 10), ("pug", 5), ("pun", 12), ("bun", 4), ("hugs", 5)
因此,基本词典是["b", "g", "h", "n", "p", "s", "u"]。将所有单词拆分为基本词典中的符号,我们得到:
("h" "u" "g", 10), ("p" "u" "g", 5), ("p" "u" "n", 12), ("b" "u" "n", 4), ("h" "u" "g" "s", 5)
BPE然后统计每个可能的符号对出现的频率,并选择最常出现的符号对。在上述例子中,"h"后面跟着"u"出现了10 + 5 = 15次(在"hug"的10次和"hugs"的5次中)。然而,最常出现的符号对是"u"后面跟着"g",总共出现了10 + 5 + 5 = 20次。因此,分词器学习的第一条合并规则是将所有"u"后面跟着"g"的符号组合在一起。接下来,"ug"被添加到词典中。单词集变为:
("h" "ug", 10), ("p" "ug", 5), ("p" "u" "n", 12), ("b" "u" "n", 4), ("h" "ug" "s", 5)
BPE然后识别下一个最常出现的符号对。它是"u"后面跟"n",出现了16次。"u"和"n"被合并为"un"并添加到词典中。下一个最常出现的符号对是"h"后面跟"ug",出现了15次。再次合并,"hug"可以被添加到词典中。
此时,词典是["b", "g", "h", "n", "p", "s", "u", "ug", "un", "hug"],我们唯一的单词集表示为:
("hug", 10), ("p" "ug", 5), ("p" "un", 12), ("b" "un", 4), ("hug" "s", 5)
假设BPE训练在此时停止,学习到的合并规则将应用于新单词(只要这些新单词不包含基本词典中不存在的符号)。例如,单词"bug"会被分词为["b", "ug"],但"mug"会被分词为["<unk>", "ug"],因为符号"m"不在基本词典中。通常,单个字母如"m"不会被替换为"<unk>"符号,因为训练数据通常至少包含每个字母的一次出现,但对于一些特殊字符(如表情符号),可能会发生这种情况。
如前所述,词典的大小,即基本词典大小加上合并次数,是需要选择的超参数。例如,GPT的词汇量大小为40,478,因为它们有478个基本字符,并选择在40,000次合并后停止训练。
字节级BPE¶
如果考虑所有可能的基本字符,如所有Unicode字符,则基本词典可能会非常大。为了获得更好的基本词典,GPT-2使用字节作为基本词典,这是一种巧妙的技巧,强迫基本词典的大小为256,同时确保每个基本字符都包含在词典中。通过一些额外的规则处理标点符号,GPT2的分词器可以分词任何文本,而无需使用<unk>符号。GPT-2的词汇量大小为50,257,对应256个字节基础标记、一个特殊结束文本标记以及通过50,000次合并学习到的符号。
WordPiece¶
WordPiece是用于BERT、DistilBERT和Electra的子词分词算法。该算法首次出现在Japanese and Korean Voice Search (Schuster et al., 2012)中,与BPE非常相似。WordPiece首先将词典初始化为包含训练数据中所有字符,并逐步学习指定数量的合并规则。与BPE不同的是,WordPiece不选择出现最频繁的符号对,而是选择能最大化训练数据似然度的符号对。
具体来说,最大化训练数据的似然度相当于找到一个符号对,其概率除以其第一个符号和第二个符号的概率之比在所有符号对中最大。例如,"u"后面跟"g"只会在"ug"的概率除以"u"和"g"的概率大于任何其他符号对时被合并。直观地说,WordPiece与BPE略有不同,因为它会评估合并两个符号所“损失”的内容,以确保“值得”。
Unigram¶
Unigram是一种子词分词算法,首次出现在Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates (Kudo, 2018)中。与BPE或WordPiece不同,Unigram将基本词典初始化为大量符号,并逐步修剪每个符号以获得较小的词典。基本词典可以对应所有预分词的单词和最常见子串。Unigram未直接用于transformers库中的任何模型,而是与SentencePiece结合使用。
在每次训练步骤中,Unigram算法在当前词典和单语模型下定义训练数据的损失(通常定义为对数似然)。然后,对于词典中的每个符号,计算如果将其从词典中移除,整体损失会增加多少。Unigram会移除p(通常是10%或20%)个损失增加最少的符号,即对训练数据整体损失影响最小的符号。这个过程重复进行,直到词典达到所需的大小。Unigram算法总是保留基本字符,以便任何单词都可以被分词。
由于Unigram不是基于合并规则(与BPE和WordPiece不同),因此在训练后有多种方式对新文本进行分词。例如,如果训练后的Unigram分词器词典为:
["b", "g", "h", "n", "p", "s", "u", "ug", "un", "hug"]
"hugs"可以被分词为["hug", "s"]、["h", "ug", "s"]或["h", "u", "g", "s"]。那么,应该选择哪一个呢?Unigram会保存训练语料库中每个标记的概率,从而在训练后计算每种可能分词的概率。实际上,算法会选择最可能的分词,但也提供了根据其概率采样可能分词的方法。
这些概率由分词器训练所用的损失定义。假设训练数据由单词x1,...,xN组成,单词xi的所有可能分词集定义为S(xi),则整体损失定义为:
SentencePiece¶
到目前为止,所有分词算法都有一个问题:假设输入文本使用空格分隔单词。然而,并不是所有语言都使用空格分隔单词。一个可能的解决方案是使用特定语言的预分词器,例如XLM使用特定的中文、日文和泰文预分词器。为了解决这一问题,SentencePiece: A simple and language independent subword tokenizer and detokenizer for Neural Text Processing (Kudo et al., 2018)将输入视为原始输入流,从而将空格包括在字符集中。然后使用BPE或Unigram算法构建适当的词典。
例如,XLNetTokenizer使用SentencePiece,这就是为什么在之前的例子中"▁"字符被包含在词典中。使用SentencePiece进行解码非常简单,因为所有标记可以简单地连接起来,并用空格替换"▁"。
transformers库中使用SentencePiece的所有模型都将其与Unigram结合使用。例如,ALBERT、XLNet、Marian和T5都使用SentencePiece。