Transformers 文档
分词器概述
并获得增强的文档体验
开始使用
分词器概述
在本页中,我们将更深入地了解分词。
正如我们在预处理教程中看到的,对文本进行分词是将其拆分为单词或子词,然后通过查找表将其转换为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", "!"]
因为我们考虑的是 uncased 模型,所以句子首先被小写。我们可以看到单词 `["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 次(在 `"hug"` 的 10 次出现中出现 10 次,在 `"hugs"` 的 5 次出现中出现 5 次)。然而,最频繁的符号对是 `"u"` 后跟 `"g"`,总共出现了 10 + 5 + 5 = 20 次。因此,分词器学习的第一个合并规则是将所有 `"u"` 符号后跟 `"g"` 符号的分词组合在一起。接下来,`"ug"` 被添加到词汇表中。单词集 then becomes
("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并未直接用于transformer中的任何模型,但它与SentencePiece结合使用。
在每个训练步骤中,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 模型都将其与 unigram 结合使用。使用 SentencePiece 的模型示例包括 ALBERT、XLNet、Marian 和 T5。
< > 在 GitHub 上更新