Transformers 文档

tokenizer 概述

Hugging Face's logo
加入 Hugging Face 社区

并获得增强的文档体验

开始使用

tokenizer 概述

在本页中,我们将更仔细地研究分词。

正如我们在预处理教程中看到的,tokenizer 文本是将其拆分为单词或子词,然后通过查找表将其转换为 ID。将单词或子词转换为 ID 很简单,因此在本概述中,我们将重点关注将文本拆分为单词或子词(即 tokenizer 文本)。更具体地说,我们将查看 🤗 Transformers 中使用的三种主要 tokenizer 类型:字节对编码 (BPE)WordPieceSentencePiece,并展示示例说明哪些模型使用哪种 tokenizer 类型。

请注意,在每个模型页面上,您可以查看关联的 tokenizer 的文档,以了解预训练模型使用了哪种 tokenizer 类型。例如,如果我们查看 BertTokenizer,我们可以看到该模型使用 WordPiece

简介

将文本拆分为更小的块是一项比看起来更难的任务,并且有多种方法可以做到这一点。例如,让我们看一下句子 "Don't you love 🤗 Transformers? We sure do."

tokenizer 此文本的简单方法是按空格拆分,这将给出

["Don't", "you", "love", "🤗", "Transformers?", "We", "sure", "do."]

这是明智的第一步,但如果我们查看 token "Transformers?""do.",我们注意到标点符号附加到单词 "Transformer""do" 上,这是次优的。我们应该考虑标点符号,以便模型不必学习单词的不同表示形式以及可能跟随它的每个可能的标点符号,这将爆炸模型必须学习的表示形式的数量。考虑标点符号,tokenizer 我们的示例文本将给出

["Don", "'", "t", "you", "love", "🤗", "Transformers", "?", "We", "sure", "do", "."]

更好。但是,tokenizer 如何处理单词 "Don't" 是不利的。"Don't" 代表 "do not",因此最好将其 tokenizer 为 ["Do", "n't"]。这就是事情开始变得复杂的地方,也是每个模型都有自己的 tokenizer 类型的部分原因。根据我们应用于 tokenizer 文本的规则,将为同一文本生成不同的 tokenizer 输出。只有当您向预训练模型提供使用与其训练数据 tokenizer 时使用的相同规则 tokenizer 的输入时,它才能正常执行。

spaCyMoses 是两个流行的基于规则的 tokenizer。将它们应用于我们的示例,spaCyMoses 将输出类似以下内容

["Do", "n't", "you", "love", "🤗", "Transformers", "?", "We", "sure", "do", "."]

可以看出,这里使用了空格和标点符号 tokenizer,以及基于规则的 tokenizer。空格和标点符号 tokenizer 以及基于规则的 tokenizer 都是单词 tokenizer 的示例,单词 tokenizer 粗略地定义为将句子拆分为单词。虽然这是将文本拆分为更小块的最直观的方法,但这种 tokenizer 方法可能会给大型文本语料库带来问题。在这种情况下,空格和标点符号 tokenizer 通常会生成非常大的词汇表(使用的所有唯一单词和 token 的集合)。例如Transformer XL 使用空格和标点符号 tokenizer,导致词汇量达到 267,735!

如此大的词汇量迫使模型在输入和输出层都拥有巨大的嵌入矩阵,这会导致内存和时间复杂度都增加。一般来说,transformers 模型很少有大于 50,000 的词汇量,特别是如果它们仅在单一语言上进行预训练。

因此,如果简单的空格和标点符号 tokenizer 不令人满意,为什么不简单地在字符上 tokenizer 呢?

虽然字符 tokenizer 非常简单,并且会大大降低内存和时间复杂度,但它使模型更难学习有意义的输入表示。例如,学习字母 "t" 的有意义的上下文无关表示比学习单词 "today" 的上下文无关表示要困难得多。因此,字符 tokenizer 通常伴随着性能的损失。因此,为了获得两全其美的效果,transformers 模型使用单词级和字符级 tokenizer 之间的混合,称为 子词 tokenizer。

子词 tokenizer

子词 tokenizer 算法依赖于以下原则:常用单词不应拆分为更小的子词,但稀有单词应分解为有意义的子词。例如,"annoyingly" 可能被认为是稀有词,可以分解为 "annoying""ly""annoying""ly" 作为独立的子词会更频繁地出现,同时 "annoyingly" 的含义通过 "annoying""ly" 的复合含义来保留。这在粘着性语言(如土耳其语)中尤其有用,在土耳其语中,您可以通过将子词串在一起来形成(几乎)任意长的复合词。

子词 tokenizer 允许模型拥有合理的词汇量,同时能够学习有意义的上下文无关表示。此外,子词 tokenizer 使模型能够通过将单词分解为已知的子词来处理以前从未见过的单词。例如,BertTokenizer"I have a new GPU!" tokenizer 如下

>>> 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"] 存在于 tokenizer 的词汇表中,但单词 "gpu" 不存在。因此,tokenizer 将 "gpu" 拆分为已知的子词:["gp" 和 "##u"]"##" 表示 token 的其余部分应附加到前一个 token,没有空格(用于解码或反转 tokenizer)。

作为另一个示例,XLNetTokenizer 将我们之前示例文本 tokenizer 如下

>>> 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"

现在让我们看看不同的子词 tokenizer 算法是如何工作的。请注意,所有这些 tokenizer 算法都依赖于某种形式的训练,这种训练通常在相应模型将要训练的语料库上进行。

字节对编码 (BPE)

字节对编码 (BPE) 在 使用子词单元进行稀有词神经机器翻译(Sennrich 等人,2015 年)中介绍。BPE 依赖于一个预 tokenizer,该 tokenizer 将训练数据拆分为单词。预tokenizer 可以像空格 tokenizer 一样简单,例如 GPT-2RoBERTa。更高级的预 tokenizer 包括基于规则的 tokenizer,例如 XLMFlauBERT,它对大多数语言使用 Moses,或 GPT,它使用 spaCy 和 ftfy 来计算训练语料库中每个单词的频率。

预 tokenizer 后,创建了一组唯一单词,并确定了每个单词在训练数据中出现的频率。接下来,BPE 创建一个基本词汇表,其中包含唯一单词集中出现的所有符号,并学习合并规则以从基本词汇表的两个符号形成一个新符号。它这样做直到词汇表达到所需的词汇量。请注意,所需的词汇量是在训练 tokenizer 之前定义的超参数。

作为一个例子,让我们假设在预 tokenizer 之后,已经确定了以下包含其频率的单词集

("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 次。因此,tokenizer 学习的第一个合并规则是将所有后跟 "g" 符号的 "u" 符号组合在一起。接下来,将 "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" 将被 tokenizer 为 ["b", "ug"],但 "mug" 将被 tokenizer 为 ["<unk>", "ug"],因为符号 "m" 不在基本词汇表中。一般来说,像 "m" 这样的单个字母不会被 "<unk>" 符号替换,因为训练数据通常至少包含每个字母的一次出现,但对于非常特殊的字符(如表情符号)来说,这种情况很可能发生。

如前所述,词汇量, 基本词汇量 + 合并次数,是一个要选择的超参数。例如,GPT 的词汇量为 40,478,因为它们有 478 个基本字符,并选择在 40,000 次合并后停止训练。

字节级 BPE

如果 例如 将所有 unicode 字符都视为基本字符,那么包含所有可能的基本字符的基本词汇表可能会非常大。为了获得更好的基本词汇表,GPT-2 使用字节作为基本词汇表,这是一个巧妙的技巧,可以强制基本词汇表的大小为 256,同时确保每个基本字符都包含在词汇表中。通过一些处理标点符号的附加规则,GPT2 的 tokenizer 可以 tokenizer 每个文本,而无需 <unk> 符号。GPT-2 的词汇量为 50,257,对应于 256 个字节基本 token、一个特殊的文本结束 token 和通过 50,000 次合并学习到的符号。

WordPiece

WordPiece 是用于 BERTDistilBERTElectra 的子词 tokenizer 算法。该算法在 日语和韩语语音搜索(Schuster 等人,2012 年)中概述,并且与 BPE 非常相似。WordPiece 首先初始化词汇表以包含训练数据中存在的每个字符,并逐步学习给定数量的合并规则。与 BPE 相比,WordPiece 不选择最频繁的符号对,而是选择一旦添加到词汇表中,最大化训练数据似然性的符号对。

那么这到底意味着什么呢?参考前面的例子,最大化训练数据的似然性等同于找到符号对,其概率除以其第一个符号后跟其第二个符号的概率在所有符号对中是最大的。例如,只有当 "ug" 的概率除以 "u""g" 的概率大于任何其他符号对时,"u" 后跟 "g" 才会被合并。直观地说,WordPiece 与 BPE 的略有不同之处在于,它评估合并两个符号所损失的内容,以确保它是值得的

Unigram

Unigram 是在 子词正则化:使用多个子词候选改进神经网络翻译模型(Kudo,2018 年)中引入的子词 tokenizer 算法。与 BPE 或 WordPiece 相比,Unigram 将其基本词汇表初始化为大量符号,并逐步修剪每个符号以获得更小的词汇表。基本词汇表例如可以对应于所有预 tokenizer 单词和最常见的子字符串。Unigram 没有直接用于 transformers 中的任何模型,但它与 SentencePiece 结合使用。

在每个训练步骤中,Unigram 算法根据当前词汇表和 unigram 语言模型定义训练数据上的损失(通常定义为对数似然)。然后,对于词汇表中的每个符号,该算法计算如果从词汇表中删除该符号,总体损失会增加多少。然后,Unigram 删除损失增加最小的符号的 p(p 通常为 10% 或 20%)百分比, 那些对训练数据总体损失影响最小的符号。重复此过程,直到词汇表达到所需大小。Unigram 算法始终保留基本字符,以便可以 tokenizer 任何单词。

由于 Unigram 不基于合并规则(与 BPE 和 WordPiece 相反),因此该算法在训练后有多种 tokenizer 新文本的方法。例如,如果训练有素的 Unigram tokenizer 显示词汇表

["b", "g", "h", "n", "p", "s", "u", "ug", "un", "hug"],

"hugs" 可以 tokenizer 为 ["hug", "s"]["h", "ug", "s"]["h", "u", "g", "s"]。那么选择哪一个呢?Unigram 在保存词汇表的基础上,还保存了每个 token 在训练语料库中的概率,以便在训练后可以计算每个可能的 tokenizer 的概率。该算法实际上只是选择最可能的 tokenizer,但也提供了根据其概率对可能的 tokenizer 进行抽样的可能性。

这些概率由 tokenizer 训练的损失定义。假设训练数据由单词组成x1,,xNx_{1}, \dots, x_{N}并且单词的所有可能的 tokenizer 的集合xix_{i}定义为S(xi)S(x_{i}),那么总体损失定义为L=i=1Nlog(xS(xi)p(x))\mathcal{L} = -\sum_{i=1}^{N} \log \left ( \sum_{x \in S(x_{i})} p(x) \right )

SentencePiece

到目前为止,所有描述的分词算法都有相同的问题:它们都假设输入文本使用空格来分隔单词。然而,并非所有语言都使用空格来分隔单词。一种可能的解决方案是使用特定于语言的预分词器,例如 XLM 使用了特定的中文、日文和泰文预分词器。为了更通用地解决这个问题,SentencePiece:一种用于神经文本处理的简单且与语言无关的子词分词器和反分词器(Kudo et al., 2018) 将输入视为原始输入流,因此将空格包括在要使用的字符集中。然后,它使用 BPE 或 unigram 算法来构建适当的词汇表。

XLNetTokenizer 例如使用了 SentencePiece,这也是为什么在之前的示例中 " " 字符包含在词汇表中的原因。使用 SentencePiece 进行解码非常容易,因为所有 tokens 都可以直接连接,并且 " " 会被替换为空格。

库中所有使用 SentencePiece 的 transformers 模型都将其与 unigram 结合使用。 使用 SentencePiece 的模型示例包括 ALBERTXLNetMarianT5

< > GitHub 上更新