本文的内容和核心代码参考Huggingface Tokenizer
1. Tokenizer的基本方法
Tokenizer(标记器)是NLP中的一个核心模块,它将文本转换为token,token是NLP中的基本单位,可以是单词、子词、字符等。在模型的训练和预测过程中,模型只能处理数字,因此需要将文本转换为数字,这个过程就是tokenization
下面我们看这样一段文本
1 | I like computer vision and mechanism learning. |
模型并不能直接对上述文本进行处理,因为模型只能处理数字信息,相反的人只能看懂文本信息,对于数字化的二进制信息,人是无法读懂的。因此我们需要有一个能将文本转为数字的方法,这就是Tokenizer所做的事情了,在tokenizer的发展过程中,人们创造了许多分词的方法,下面我们一一介绍这些方法
1.1 基于词(Word-based)
基于词的方法很容易理解,就是将一段文本转换为单词,例如上述文本经过基于词的方法处理后,得到的结果如下
1 | text = "I like computer vision and transformers." |
上述结果是基于空格的分词,还可以根据标点符号进行分词,这样上述结果的transformers.就会被分为transformers和.
除此之外还有一些单词的变体,例如happy和happily,他们有额外的标点符号规则,但是不论使用哪种tokenizer,最终我们都会得到一个词汇表,他记录着每一个词汇的id和词汇本身。每种语言都有非常多的词汇,例如英语中有超过50w个单词,如果每个单词都有一个id,那么词汇表的大小就会非常大,所以我们需要想办法对其进行压缩,下面基于字符的分词方法就解决了这个问题
1.2 基于字符(Character-based)
基于字符的方法将文本拆为字符,而不是单词,有下面两个好处
- 词汇表要小得多
- 词汇外(未知)标记(token)也要少得多,因为每个单词都是由字符组成的
1 | print(list(text)) |
这种方法虽然大大减少了词汇表的数量,但是由于现在是字符不是单词,直觉上讲,这种做法的意义不大,因为每个字符本身并没有意义,单词才具有意义,因此我们需要一种方法,既能减少词汇表的数量,又能保留单词的意义,就是第三种的基于子词(Subword-based的分词方法
1.3 基于子词(Subword-based)
基于子词的分词方法依赖这样一个原则:不应将常用词拆分为更小的子词,而应将稀有词分解为有意义的子词。
例如,“annoyingly”可能被认为是一个罕见的词,可以分解为“annoying”和“ly”。这两者都可能作为独立的子词出现的更频繁,同时“annoyingly”的含义由“annoying”和“ly”的复合含义保持。
这里我们使用transformers库中的AutoTokenizer初始化一个Bert的Tokenizer,并用它对上述文本进行分词
1 | text = "I like computer vision and transformers." |
AutoTokenizer.from_pretrained("bert-base-cased")这一步会自动从Huggingface官网下载相应的bert分词模型,随后我们调用tokenizer的tokenize方法就能够对文本进行分词了,其中transformers被分为了transform和##ers,即将一个不常用的词分为两个更有意义的常用词,这种分词方式可以减少词汇表的数量,同时保留了单词的含义
1.4 Huggingface中Tokenizer的使用
对于一段文本,我们的目的是对其进行编码,使得模型能够读懂它,这个过程主要有两步:标记(token)化,转换为输入id。这两步在Huggingface中只需要调用tokenizer即可解决,如下所示
1 | text = "I like computer vision and transformers." |
除此之外我们还可以通过几个tokenizer的方法来查看他的分词过程
1 | tokens = tokenizer.tokenize(text) |
这里我们调用了三个tokenizer的方法,分别是tokenize,convert_tokens_to_ids和decode,其中tokenize方法将文本分词,convert_tokens_to_ids方法将分词后的文本转换为id,decode方法将id转换为文本。
注意这里直接调用tokenizer和调用其方法进行分词结果略有不同,直接调用的前后加入了两个id:101和102。这是给文本前后加入启示和终止符,这样我们就知道句子什么时候开始什么时候结束了,后面会详细介绍这些方法
2. Tokenizer与Model
如上所述,Tokenizer将文本处理数字形式,转化为模型可以处理的形式,下面我们看一下Huggingface中的Tokenizer和Model是如何配合使用的
2.1 将文本输入到模型中
下面我们使用一个分类模型来演示如何将文本输入到模型中
1 | from transformers import AutoTokenizer, AutoModelForSequenceClassification |
tokenizer在将文本转换为id之后我们需要确保输入到模型中的input_ids是二维的,因为模型通常需要处理多个句子,所以这里我们将单个句子拓展为2维后进行了输入
2.2 多序列处理
在示例中我们能够对单个序列进行处理了,但是当处理多序列时会遇到以下问题
- 序列长度不一致,如何对齐?
- 序列过长如何截断?
假设我们有两个句子,他们的id如下
1 | sequence1_ids = [[200, 200, 200]] |
他们合起来的输入到网络中为batched_ids,但是这无法输入到网络中,因为这个矩阵同一维度的大小是不一样的,所需要对齐操作,如下
1 | sequence1_ids = [[200, 200, 200]] |
注意这里sequence2和batched_ids的第二行结果应该是一样的,但是为什么得到的值不同呢?因为Transformer的注意力机制会考虑当前序列的所有token,因此会考虑pad_token_id。为了在填充与不填充句子时获得相同的结果,我们需要告诉Transformer哪一部分被padding了,这通过attention_mask参数来实现,如下
1 | batched_ids = [[200, 200, 200], [200, 200, tokenizer.pad_token_id]] |
这里的结果就和上述单序列输入的结果相同了
2.3 Tokenizer的参数使用
上面我们介绍了多序列的处理方法,幸运的是,在Huggingface中并不需要我们手动去padding,我们只需要直接调用tokenizer分词器即可,下面介绍在tokenizer中的常用方法
1. 直接调用tokenizer分词器
Huggingface中实例化的tokenizer本身就是可调用对象,可以直接对文本进行分词
1 | sequence = "I've been waiting for a HuggingFace course my whole life." |
2. padding参数
上面例子tokenizer并没有进行填充等操作,下面我们加入padding参数
1 | sequences = ["I've been waiting for a HuggingFace course my whole life.", "So have I!"] |
3. truncation参数
truncation参数表示当序列长度超过max_length时,是否进行截断,Huggingface中默认是False,即不进行截断
1 | tokenizer(sequences, padding=True) |
4. return_tensors参数
return_tensors参数表示返回的数据类型,pt返回PyTorch张量,tf返回TensorFlow张量,np返回NumPy数组
1 | # Returns PyTorch tensors |