分词器简介
在本篇教程中,我们将近距离观看分词。
正如我们在预处理教程中看到的那样,对文本分词就是将其切分成单词或子词,进而可用通过查表的方式获得ids。将单词或者子词转换为ids是非常直接的,因此在本篇中,我们主要关注将文本切分成单词或者子词。更具体来说,我们会看到在Transformers中使用的三种主要的分词器:BPE、WordPiece和SentencePiece,并展示它们分别使用在什么模型上面。
值得注意的是,在每个模型页面,你可以查看相关联分词器的文档,来了解模型到底使用的是哪种分词器。比如,当你查看BertTokenizer,你可以看到模型使用的是WordPiece。
简介
将文本切成小块比它看起来要难很多,现在有很多方法来做这件事情。比如,让我们看这个句子:"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"]会更好一些。这里就是事情开始变复杂的地方,一部分原因是每个模型拥有其自己的分词方式。对相同文本的不同分词结果,取决于使用的规则。一个预训练模型只有当你把用对应分词器处理后的输入喂进去时,才会表现正常,如果使用与训练时不同的分词规则,那么就会出问题。
spaCy和Moses是两款经典的基于规则的分词器。将其应用于我们的例子,输出结果是:
["Do", "n't", "you", "love", "", "Transformers", "?", "We", "sure", "do", "."]
我们可以看到,空格和标点分词,以及规则分词都用在了这里。空格和标点分词,以及规则分词都是单词分词的典型例子,可以简单定义为将句子切分成词汇。虽然这是将句字切分成小块的最直觉的方式,但是对于大规模语料库来说会出现问题。在这种情况下,空格和标点分词经常会产生一个巨大的词汇表。比如,Transformer XL使用空格和标点分词,产生的词汇表大小是267,735!
如此巨大的词汇表使得模型需要一个巨大的嵌入矩阵作为输入输出层,这会导致巨大的空间和时间复杂度。通常来讲,transformers模型拥有的词汇表大小很少超过50,000。尤其是当它们只在一种语言上预训练。
所以如果空格和标点不能让人满意,那么为什么不考虑基于字符进行分词呢?
尽管基于字符分词非常简单并且大大缩减了时间和空间复杂度,但是它使得模型更难学到有意义的表示。比如:学习到't'有意义的上下文独立表示比学到'today'的表示更难。因此,字符分词通常伴随着性能的损失。所以为了得到两者之间的平衡,transformers模型使用了词汇级和字符级的结合体——子词分词。
子词分词
子词分词算法基于“频繁使用的单词不能被分割成更小的子词,但是稀有词汇应该被分解成有意义的子词”原则。对于单词"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!")
'''
output:
["i", "have", "a", "new", "gp", "##u", "!"]
'''
因为我们使用的是大小写无关模型,因此句子首先要被小写化。我们可以看到单词["i", "have", "a", "new"]出现在了分词器的词汇表中,但是单词"gpu"没有。因此,分词器将"gpu"分割成了典型的子词:["gp", "##u"]。"##"的意思是剩下的token应该被连接在上一个的后面(中间没有空格)。
作为另一个案例,XLNetTokenizer对于我们之前的例子进行分词:
from transformers import XLNetTokenizer
tokenizer = XLNetTokenizer.from_pretrained("xlnet-base-cased")
tokenizer.tokenize("Don't you love Transformers? We sure do.")
'''
output:
["▁Don", "'", "t", "▁you", "▁love", "▁", "", "▁", "Transform", "ers", "?", "▁We", "▁sure", "▁do", "."]
'''
我们将会在SentencePiece中看到那些"_"的意思。正如你看到的那样,稀有词汇"Transformers"被分割成了两个常见子词"Transformer"和"ers"。
让我们现在看看不同子词分词算法是怎么运作的。请注意,所有这些分词算法都依赖于某种形式的训练,这种训练通常是在相应模型将要训练的语料库上进行的。
Byte-Pair Encoding(BPE)
(暂且搁置)