分词器概述
在本页中,我们将更详细地了解分词。
正如我们在预处理教程中看到的,对文本进行分词是将其拆分为单词或子词,然后通过查找表将其转换为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"
会出现的频率更高,同时 "annoyingly"
的意义通过 "annoying"
和 "ly"
的组合意义得以保留。这在粘着语(如土耳其语)中尤其有用,因为你可以通过将子词串联起来形成(几乎)任意长的复杂词。
子词分词允许模型拥有合理的词汇量,同时能够学习有意义的上下文无关表示。此外,子词分词使模型能够处理它以前从未见过的单词,通过将它们分解为已知的子词。例如,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" 和 "##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 次(在 10 次 "hug"
中出现 10 次,在 5 次 "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)
假设,字节对编码训练在此停止,那么学习到的合并规则将应用于新词(只要这些新词不包含基础词汇表中没有的符号)。例如,单词 "bug"
将被分词为 ["b", "ug"]
,但 "mug"
将被分词为 ["
,因为符号 "m"
不在基础词汇表中。通常,像 "m"
这样的单个字母不会被 "
符号替换,因为训练数据通常至少包含每个字母的一次出现,但对于像表情符号这样的非常特殊的字符,这种情况可能会发生。
如前所述,词汇量大小,即基础词汇量大小加上合并次数,是一个需要选择的超参数。例如,GPT的词汇量大小为40,478,因为他们有478个基础字符,并选择在40,000次合并后停止训练。
字节级BPE
一个包含所有可能基础字符的基础词汇表可能会非常大,如果例如所有Unicode字符都被视为基础字符。为了拥有一个更好的基础词汇表,GPT-2使用字节作为基础词汇表,这是一个巧妙的技巧,可以强制基础词汇表的大小为256,同时确保每个基础字符都包含在词汇表中。通过一些额外的规则来处理标点符号,GPT-2的分词器可以分词每个文本,而不需要
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 et al., 2018) 将输入视为原始输入流,从而将空格包含在使用的字符集中。然后它使用 BPE 或 unigram 算法来构建适当的词汇表。
XLNetTokenizer 使用 SentencePiece 作为示例,这也是为什么在之前的示例中词汇表中包含了 "▁"
字符。使用 SentencePiece 进行解码非常简单,因为所有标记都可以直接连接起来,并且 "▁"
会被替换为空格。
库中所有使用SentencePiece的transformer模型都将其与unigram结合使用。使用SentencePiece的模型示例包括ALBERT、XLNet、Marian和T5。
< > Update on GitHub