分词器摘要
在本页中,我们将仔细了解分词。
正如我们在 预处理教程 中看到的,分词文本是将其拆分为单词或子词,然后通过查找表将其转换为 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."
,我们注意到标点符号附着在单词"Transformer"
和"do"
上,这并不是最佳情况。我们应该考虑标点符号,这样模型就不需要学习一个单词的不同表示及其之后可能出现的每个标点符号,这将导致模型必须学习的表示数量激增。考虑到标点符号,我们对示例文本进行分词将会得到以下结果:
["Don", "'", "t", "you", "love", "🤗", "Transformers", "?", "We", "sure", "do", "."]
更好。但是,分词对"Don't"
的处理方式存在缺点。"Don't"
代表"do not"
,因此最好将其分词为["Do", "n't"]
。这就是事情开始变得复杂的地方,也是每个模型拥有自己的分词器类型的原因之一。根据我们对文本分词应用的规则,对于相同的文本,会生成不同的分词结果。只有当您提供使用与训练数据相同的规则进行分词的输入时,预训练模型才能正常执行。
spaCy和Moses是两个流行的基于规则的分词器。将它们应用于我们的示例,spaCy和Moses将输出类似以下内容:
["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", "gp", "##u", "!"]
由于我们考虑的是非大小写模型,因此句子首先被转换为小写。我们可以看到单词["i", "have", "a", "new"]
存在于分词器的词汇表中,但单词"gpu"
不存在。因此,分词器将"gpu"
分割成已知的子词:["gp" and "##u"]
。"##"
表示该标记的剩余部分应该附加到前面的标记,不带空格(用于解码或分词的反转)。
另一个例子,XLNetTokenizer将我们之前的示例文本分词如下:
>>> from transformers import XLNetTokenizer
>>> tokenizer = XLNetTokenizer.from_pretrained("xlnet/xlnet-base-cased")
>>> tokenizer.tokenize("Don't you love 🤗 Transformers? We sure do.")
["▁Don", "'", "t", "▁you", "▁love", "▁", "🤗", "▁", "Transform", "ers", "?", "▁We", "▁sure", "▁do", "."]
当我们查看SentencePiece时,我们将回到那些" "
的含义。可以看到,罕见词"Transformers"
已经被拆分成了更常见的子词"Transform"
和"ers"
。
现在让我们看看不同的子词分词算法是如何工作的。注意,所有这些分词算法都依赖于某种形式的训练,这种训练通常在相应模型将要训练的语料库上进行。
字节对编码 (BPE)
字节对编码 (BPE) 出现在使用子词单元进行罕见词的神经机器翻译 (Sennrich 等人,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 次出现中出现了 10 次,在 "hugs"
的 5 次出现中出现了 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)
假设字节对编码训练在此阶段停止,那么学习到的合并规则将应用于新单词(只要这些新单词不包含不在基本词汇表中的符号)。例如,单词 "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 的子词分词算法。该算法在 日语和韩语语音搜索 (Schuster 等人,2012) 中进行了概述,与 BPE 非常相似。WordPiece 首先将词汇表初始化为包含训练数据中存在的每个字符,并逐步学习给定数量的合并规则。与 BPE 相比,WordPiece 不会选择最频繁的符号对,而是选择在添加到词汇表后最大化训练数据似然的符号对。
那么这到底意味着什么?参考前面的例子,最大化训练数据的似然等效于找到符号对,其概率除以其第一个符号后跟其第二个符号的概率在所有符号对中最大。例如,"u"
后面跟着 "g"
只有在 "ug"
的概率除以 "u"
、"g"
的概率大于其他任何符号对时才会合并。直观地说,WordPiece 与 BPE 有细微的差别,因为它会评估合并两个符号所损失的,以确保它是值得的。
Unigram
Unigram 是一种子词分词算法,在 子词正则化:使用多个子词候选词改进神经网络翻译模型 (Kudo,2018) 中提出。与 BPE 或 WordPiece 相比,Unigram 将其基本词汇表初始化为大量的符号,并逐步修剪每个符号以获得更小的词汇表。例如,基本词汇表可以对应于所有预先分词的词和最常见的子字符串。Unigram 未直接用于 transformers 中的任何模型,但它与 SentencePiece 结合使用。
在每个训练步骤中,Unigram 算法定义了给定当前词汇表和 Unigram 语言模型的训练数据的损失(通常定义为对数似然)。然后,对于词汇表中的每个符号,该算法计算如果从词汇表中删除该符号,整体损失会增加多少。然后,Unigram 删除 p(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 在保存词汇表的基础上还保存了训练语料库中每个标记的概率,以便在训练后可以计算每个可能分词的概率。该算法在实践中只选择最可能的词,但同时也提供了根据概率对可能词进行采样的可能性。
这些概率由分词器训练所依据的损失定义。假设训练数据由词组成并且一个词的所有可能分词的集合为定义为, 那么整体损失定义为
SentencePiece
到目前为止,所有描述的标记化算法都有一个共同的问题:它们假设输入文本使用空格来分隔单词。然而,并非所有语言都使用空格来分隔单词。一个可能的解决方案是使用特定于语言的预标记器,例如,XLM 使用了专门针对中文、日语和泰语的预标记器。为了更普遍地解决这个问题,SentencePiece: 一种简单且与语言无关的子词标记器和解标记器,用于神经文本处理 (Kudo 等人,2018) 将输入视为原始输入流,因此包括空格在内,作为要使用的字符集。然后它使用 BPE 或 unigram 算法来构建相应的词汇表。
XLNetTokenizer 例如使用 SentencePiece,这也是前面示例中 " "
字符被包含在词汇表中的原因。使用 SentencePiece 解码非常简单,因为所有标记都可以直接拼接,然后将 " "
替换为空格。
库中所有使用 SentencePiece 的 Transformer 模型都将 SentencePiece 与 unigram 相结合使用。使用 SentencePiece 的模型示例包括 ALBERT、XLNet、Marian 和 T5。
< > Update on GitHub