当前位置: 首页 > 工具软件 > Tokenizer > 使用案例 >

tokenizer简介

习宸
2023-12-01

原文链接:https://huggingface.co/docs/transformers/master/en/tokenizer_summary

1、前言

众所周知,在NLP任务中,原始文本需要处理成数值型字符才能够被计算机处理,我们熟悉的one-hot编码就是一种转换方式。但这种方式有两个弊端:向量维度太高,且丢失了语义信息。后来人们发明了词向量(或称之为词嵌入,word embedding),它在一定程度了解决了one-hot的上述两个问题。

从「词向量」这个名字上就可以看出,其基本单元是词。因此,要想得到词向量,首先要对句子进行分词,所以,我们需要一个分词工具,简称之为“分词器”。在现代自然语言中,分词器的作用不再是仅仅将句子分成单词,更进一步的,它还需要将单词转化成一个唯一的编码,以便下一步在词向量矩阵中查找其对应的词向量。本文主要介绍一下现代NLP是如何将句子切分为词的。

在中文里,一般将tokenizer直接译为“分词器”,但正如上文所述,这其实只翻译出了其第一层含义。因此,我认为将其翻译为“符化器”——将句子分词并转化为唯一编码——更能体现其作用。本文不对该单词进行翻译。

2、引例

分词的任务看似简单,实际上却大有文章。首先,给一个例句:Don’t you love 珞 Transformers? We sure do.

一种最简单的分词方式,就是按照空格来对这个句子进行分词,可以得到如下内容:

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

分词成功。但是,我们注意到,对于"Transformers?""do.",标点符号与单词分到了一起。这其实并不好,因为如果采用这种方式,那么对每一种标点与单词的组合,模型都要学习到一种向量表示,这无疑会增加模型训练的复杂度。因此,我们可以对上述分词方法进行优化,把标点单独分出来:

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

尽管“?”和“.”被成功分离出来,但是,缩写单词"Don’t"也被拆分了。我们知道,"Don’t""do not"的缩写,因此,将其分为["Do", "n't"]显然更合理一些。

到这里,事情开始变得复杂起来,不同的模型对于上述情况的处理采用了不同的方式。也就是说,即使是对于同一段文本,由于采用的切分规则不同,我们也可能得到不同的输出。因此,要想正确地使用一个预训练模型,首先需要注意的就是,模型输入所用的切分规则,必须与模型的训练数据所采用的切分规则相同。换句话说,一个预训练模型,对应一个与之匹配的tokenizer。

spaCyMoses是两个比较流行的基于规则的tokenizer。将其应用于上述例句,输出如下:

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

可以看出,句子被基于空格、标点和缩写词规则进行了切分。这种直观的分词方式非常易于理解,但在面对海量文本语料的场景时,会出现一些问题。在这些场景里,基于空格和标点的切分方式通常会生成非常大的词表(词表指对所有单词和标点去重后得到的集合)。例如,TransformerXL使用这种方式进行分词,生成的词表的规模为267,735!

如此规模的词表,将迫使模型维护一个极其庞大的词向量矩阵,这将会导致运算中的时间复杂度和内存占用的上升。实际上,transformers 模型的词汇量很少超过 50,000,尤其是当它们仅在一种语言上进行预训练时。

既然这种符合直觉的简单分词方式并不令人满意,那为何不将句子直接拆分为字母(字符级)呢?

虽然将句子直接拆分为字母非常简单并且会大大减少内存占用和时间复杂度,但它使模型学习有意义的输入表示变得更加困难。例如,对于字母"t" ,学习它的上下文无关且有意义的向量表示比学习单词"today"的向量表示要难得多。这也就是说,直接字母表示通常会导致性能下降。

既然如此,为了两全其美,transformers 模型使用了词级切分和字符级切分的混合,称为子词(subword)切分。

3、子词切分

子词切分算法依赖这样一种原则:常用词不应该被切分;罕见词应该被切分为更有意义的子词。例如,单词"annoyingly"是一个罕见词,因此,可以被切分为"annoying""ly"——"annoying""ly"更加常见,且"annoyingly"的含义可以由"annoying""ly"组合生成。

子词切分允许模型保持一个规模相对合理的词汇量,同时能够学习有意义的上下文无关表示。此外,子词切分还可以使模型能够处理它以前从未见过的词,将它们分解为已知的子词。例如,使用BertTokenizer来切分句子"I have a new GPU!",其结果如下:

>>> from transformers import BertTokenizer
>>> tokenizer = BertTokenizer.from_pretrained("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上(这将会被用于解码层)。

另外一个例子,我们使用XLNetTokenizer:

>>> from transformers import XLNetTokenizer
>>> tokenizer = XLNetTokenizer.from_pretrained("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”

现在,让我们看看不同的子词切分算法是如何工作的。请注意,所有这些切分算法都依赖于某种形式的训练,这些训练使用的语料一般情况下和训练与之对应的pretrained model所使用的语料相同。

3.1 字节对编码(Byte-Pair Encoding, BPE)

BPE首次在论文Neural Machine Translation of Rare Words with Subword Units(Sennrich et al., 2015)中被提出。BPE首先需要依赖一个可以预先将训练数据切分成单词的tokenizer,它们可以一些简单的基于空格的tokenizer,如GPT-2Roberta等;也可以是一些更加复杂的、增加了一些规则的tokenizer,如XLMFlauBERT

在使用了这些tokenizer后,我们可以得到一个在训练数据中出现过的单词的集合以及它们对应的频数。下一步,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计算任意两个字母(符号)拼接到一起时,出现在语料中的频数,然后选择频数最大的字母(符号)对。接上例,"hu"组合的频数为15("hug"出现了10次,“hugs"中出现了5次)。在上面的例子中,频数最高的符号对是"ug”,一共有20次。因此,tokenizer学习到的第一个合并规则就是将所有的"ug"合并到一起。于是,基本词表变为:

("h" "ug", 10), ("p" "ug", 5), ("p" "u" "n", 12), ("b" "u" "n", 4), ("h" "ug" "s", 5)

应用相同的算法,下一个频数最高的组合是"un",出现了16次,于是"un"被添加到词表中;接下来是"hug",即"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"将会被切分为["", “ug”]——这是因为"m"不在词表中。

正如之前提到的,词表的规模——也就是基本词表的大小加上合并后的单词的数量——是一个超参数。例如,对于GPT而言,其词表的大小为40,478,其中,基本字符一共有478个,合并后的词有40,000个。

字节级的BPE

一个包含所有基本字符的词表也可以非常的大,例如,如果将所有的unicode字符视为基本字符。针对这一问题,GPT-2使用字节来作为基本词表,这种方法保证了所有的基本字符都包含在了词表中,且基本词表的大小被限定为256。再加上一些用于处理标点的规则,GPT2的tokenizer可以切分任何英文句子,而不必引入符号。GPT-2的词表大小为50,257,包含了256个字节级的基本词表,一个用于标识句子结尾的特殊字符,以及学习到的50,000个合并单词。

3.2 WordPiece

WordPiece是一种被应用于BERTDistilBERTElectra中的子词切分算法,它与BPE算法非常像。首先,它初始化一个包含所有出现在训练数据中的字符的词表,然后递归地学习一些合并规则。与BPE不同的是,WordPiece并不选择出现的频数最高的组合,而是选择可以最大化训练数据可能性的组合。

仍然拿上面的例子来说明。最大化训练数据的可能性就等同于找到这样的符号对,它的概率除以其第一个符号的概率与其第二个符号的概率的乘积,所得的值是所有可能的符号对中最大的。例如,如果"ug"的概率除以"u"的概率再除以"g"的概率得到的结果比其他任何一种组合的结果都大,那么就将"ug"组合起来。从直觉上说,WordPiece在合并两个符号前,评估了一下这种合并所带来的损失(合并后的概率除以合并前的概率),以确保这种合并是有价值的。

3.3 Unigram

论文Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates提出了基于子词的Unigram的切分方法。与BPE和WordPiece不同的是,Unigram以一个大规模的符号库作为其初始词表,然后采用迭代的方式来不断降低词表的规模。例如,初始词表可以是一个包含所有的预先切分的单词和所有常见子词的集合。

在每一个训练步中,Unigram算法在给定当前词表和一元语言模型的情况下定义了训练数据的损失函数(通常为对数似然函数)。然后,对于词表中的每一个符号,算法都会计算出如果将该符号从词表中移除,全局损失会增加多少。随后,Unigram会移除一定百分比(通常是10%或20%)的那些使损失增加最少的符号。

重复上述训练过程,直到词表达到预定的规模。由于Unigram总是保留基本字符,因此它可以切分任何单词。

由于Unigram并不像BPE和WordPiece那样依赖合并规则,所以,经过训练的Unigram在切分文本时可能会有多种选择。假设一个Unigram tokenizer的词表如下:

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

单词"hugs"可以被切分为[“hug”, “s”][“h”, “ug”, “s”][“h”, “u”, “g”, “s”]。那么,究竟该选择哪一种呢?Unigram在保存词表的基础上,也保存了训练语料库中每个token的概率,以便在训练后计算每一种可能的切分的概率。Unigram在实践中只是简单地选择概率最大的切分作为最终结果,但也提供了根据概率对可能的切分进行采样的能力。

3.4 SentencePiece

上文介绍了所有的切分算法都有一个共同的问题:它们都假定输入的文本使用空格进行单词划分。然而,并不是所有的语言都是这样的。针对这一点,一种解决方案是,使用基于特定语言的tokenizer进行预先分词,例如,XLM使用了针对中文、日文和泰文的tokenizer。一个更一般的解决方案,在论文SentencePiece: A simple and language independent subword tokenizer and detokenizer for Neural Text Processing(Kudo et al., 2018)中被提出。这种方法将输入的文本视为元输入流,进而将空格包含在要使用的字符集中。然后,它再使用BPE或Unigram算法来构建一个合适的词表。

XLNetTokenizer使用了SentencePiece,这也就是为什么在上面的例子中,"_ “出现在其切分结果中。基于SentencePiece的解码非常容易:将各个token拼接起来,把”_ "替换为空格即可。

 类似资料: