看好的开源文档就像发现宝藏一般,决定记录一下不走丢。
这个应该会是一个系列 ,因为博主看的文档都是NLP方向的,所以称之为NLP文档挖宝,有内味了没 :-)
1 引言
此次推荐的文档是transformers包下tokenizer的综述性文档[1],它介绍了整个tokenizer的进化历程,以及tokenizer在不同预训练模型中使用的差异。对这个知识点感兴趣的朋友强烈推荐去看一下官方文档,会对整个tokenizer有更体系化的认识。
在nlp领域中tokenizer主要用于文本的预处理,能够将句子级文本转化为词级的文本,然后用于接下来的词向量转化,这个过程可以叫他token转化,或者直接叫tokenizer。总的来看,Tokenizer在转化策略上大致分为三种,可以按照最后token转化结果的粒度分为:词级(word level),字母级(character level)和子词级(subword level),目前主要流行的方法是子词级转化。
接下来,我们将总结在官方文档中的tokenizer的进化史部分,并展示一种使用tokenizer的最简单的方法。
2 Tokenizer进化史
2.1 word level 词级
使用词级分隔的一般是面向用空格作为分隔的语言,例如英语、俄语等。
-
直接使用空格作为分隔。
-
使用空格和标点作为分隔。
-
使用空格、标点和一定的分词规则。
2.2 character level 字级
词级处理能够松散地(loosely)完成分词,例如中文、日文就使用字作为单位进行词表示,但是同时带来了词表过大的问题,使得程序在训练时的内存和显存占用过多,所以需要一种生成更小词表的方法。于是有限的字母级处理方式被提出。
但是,从实验表现上看效果并不好,因为没有字母上下文而单看字母,其意义是模糊的,比如“t”相比“today”,就缺少了很多的含义。因此,一种能融合词级和字母级的处理方法子词级被提出。
2.3 subword level 子词级
子词级处理的目标是,将稀有词划分为更有意义的字词,在凝集性(agglutinative)的语言中更有效,例如土耳其语,一个词由多个词缀组成。
字词的处理分为以下几种
- Byte-Pair Encoding (BPE)。按照排列组合生成大量字母和词缀组成的词表,然后根据【频次】排序,获取规定大小的词表。
- Byte-level BPE(BBPE)。在分词和BPE不同,是对句子的比特序列(byte sequence)进行分割,分割成byte-level subwords,排列组合上和BPE相同。每个BBPE字符的分布概率相比BPE会更平均[2]。
- Wordpiece。通过概率来保留词表中的词。与BPE相比,WordPiece不会选择最频繁的符号对,而是会选择一种将训练数据添加到词汇表中的可能性最大化的符号对[3]。
- Unigram。通过删除某一个对整体词表完整性影响小的组合,获取目标大小的词表。与BPE或WordPiece相比,Unigram将其基本词汇表初始化为大量符号,并逐步修整每个符号以获得较小的词汇表。 基本词汇可以是所有预先分词后的单词和最常见的子字符串。 Unigram不能直接用于Transformer中的任何模型,但可以与SentencePiece结合使用[3]。
- SentencePiece。首先在输入的句子中把空格也当做一个词表中字符来处理,然后,它使用BPE或unigram算法构造适当的词汇表[3]。
3 Tokenizer简单的调用方法
Transformers包中有各种各样的tokenizer方法,通过from_pretrained方法可以快速获取词表配置。我们可以使用AutoTokenizer类来自动识别我们提供的预训练模型路径下的词表类别。下面以bert和roberta的预训练模型引入为例:
from transformers import AutoTokenizer # 引入AutoTokenizer
# bert
bert_pre_path = 'bert-base-uncased'
bert_tokenizer = AutoTokenizer.from_pretrained(bert_pre_path)
# roberta
roberta_pre_path = 'roberta_base'
roberta_tokenizer = AutoTokenizer.from_pretrained(roberta_pre_path)
·【注意】此处有坑。因为每种预训练模型有不同的tokenizer方法,所以通过AutoTokenizer加载出来的预训练模型还是有区别的,这里需要提前知道各种预训练模型使用的tokenizer方法,才能使用这种便捷方法。
""":class:`~transformers.AutoTokenizer` is a generic tokenizer class
that will be instantiated as one of the tokenizer classes of the library
when created with the `AutoTokenizer.from_pretrained(pretrained_model_name_or_path)`
class method.
The `from_pretrained()` method takes care of returning the correct tokenizer class instance
based on the `model_type` property of the config object, or when it's missing,
falling back to using pattern matching on the `pretrained_model_name_or_path` string:
- `t5`: T5Tokenizer (T5 model)
- `distilbert`: DistilBertTokenizer (DistilBert model)
- `albert`: AlbertTokenizer (ALBERT model)
- `camembert`: CamembertTokenizer (CamemBERT model)
- `xlm-roberta`: XLMRobertaTokenizer (XLM-RoBERTa model)
- `longformer`: LongformerTokenizer (AllenAI Longformer model)
- `roberta`: RobertaTokenizer (RoBERTa model)
- `bert`: BertTokenizer (Bert model)
- `openai-gpt`: OpenAIGPTTokenizer (OpenAI GPT model)
- `gpt2`: GPT2Tokenizer (OpenAI GPT-2 model)
- `transfo-xl`: TransfoXLTokenizer (Transformer-XL model)
- `xlnet`: XLNetTokenizer (XLNet model)
- `xlm`: XLMTokenizer (XLM model)
- `ctrl`: CTRLTokenizer (Salesforce CTRL model)
- `electra`: ElectraTokenizer (Google ELECTRA model)"""
比如上面的bert_tokenizer就有ids_to_tokens这个方法,可以获取对应的词表映射关系,但是roberta_tokenizer是通过decoder方法进行映射的。如果想写一个一体化的程序,就要在词表这里设置一个判断进行进一步的区分。
itos = tokenizer.ids_to_tokens if 'ids_to_tokens' in tokenizer.__dict__ \
else tokenizer.decoder
4 总结
之前看bert、albert的论文的时候,subword、wordpiece、BPE在脑子里散乱的存储着,现在总算成了体系。
小小的tokenizer都有这么多花样,妙啊。不由感叹知识让我自由,不受条框的限制,要不然就被一个Auto唬住了。
后续可以看一下tokenizer的源码,一个类两千多行,很壮观。内部使用了__call__方法直接让tokenizer类可以当成方法用,答案尽在:'./transformers/tokenization_utils_base.py' line 1551。核心还是调用了'batch_encode_plus'……不过这些都是后话了。
![tokenizer发展笔记](https://i-blog.csdnimg.cn/blog_migrate/3c6afc30c80c0969f15983df4a7b9603.png)
参考:
[1] transformers官网文档中的tokennizer进展总结:https://huggingface.co/transformers/tokenizer_summary.html
[2] 浅谈Byte-Level BPE:https://zhuanlan.zhihu.com/p/146114164
[3] Transformer 理解Tokenizer: https://blog.csdn.net/weixin_42167712/article/details/110727139