Huggingface 核心模块(一): Tokenizer
huggingface
本文字数:2.9k 字 | 阅读时长 ≈ 13 min

Huggingface 核心模块(一): Tokenizer

huggingface
本文字数:2.9k 字 | 阅读时长 ≈ 13 min

本文的内容和核心代码参考Huggingface Tokenizer

1. Tokenizer 的基本方法

Tokenizer(标记器)是 NLP 中的一个核心模块,它将文本转换为 token,token 是 NLP 中的基本单位,可以是 单词、子词、字符 等。在模型的训练和预测过程中,模型只能处理数字,因此需要将文本转换为数字,这个过程就是 tokenization

下面我们看这样一段文本

I like computer vision and mechanism learning.

模型并不能直接对上述文本进行处理,因为模型只能处理数字信息,相反的人只能看懂文本信息,对于数字化的二进制信息,人是无法读懂的。因此我们需要有一个能将文本转为数字的方法,这就是 Tokenizer 所做的事情了,在 tokenizer 的发展过程中,人们创造了许多分词的方法,下面我们一一介绍这些方法

1.1 基于词(Word-based)

基于词的方法很容易理解,就是将一段 文本 转换为 单词,例如上述文本经过基于词的方法处理后,得到的结果如下

>>> text = "I like computer vision and transformers."
>>> tokenized_text = text.split()
>>> print(tokenized_text)
['I', 'like', 'computer', 'vision', 'and', 'transformers.']

上述结果是基于 空格 的分词,还可以根据 标点符号进行分词,这样上述结果的 transformers. 就会被分为 transformers.

除此之外还有一些单词的变体,例如 happyhappily,他们有额外的标点符号规则,但是不论使用哪种 tokenizer,最终我们都会得到一个 词汇表 ,他记录着每一个词汇的 id 和词汇本身。每种语言都有非常多的词汇,例如英语中有超过 50w 个单词,如果每个单词都有一个 id,那么词汇表的大小就会非常大,所以我们需要想办法对其进行压缩,下面基于字符的分词方法就解决了这个问题

1.2 基于字符(Character-based)

基于字符的方法将文本拆为字符,而不是单词,有下面两个好处

>>> print(list(text))
['I', ' ', 'l', 'i', 'k', 'e', ' ', 'c', 'o', 'm', 'p', 'u', 't', 'e', 'r', ' ', 'v', 'i', 's', 'i', 'o', 'n', ' ', 'a', 'n', 'd', ' ', 't', 'r', 'a', 'n', 's', 'f', 'o', 'r', 'm', 'e', 'r', 's', '.']

这种方法虽然大大减少了 词汇表 的数量,但是由于现在是 字符不是 单词,直觉上讲,这种做法的意义不大,因为每个字符本身并没有意义,单词才具有意义,因此我们需要一种方法,既能减少词汇表的数量,又能保留单词的意义,就是第三种的 基于子词(Subword-based 的分词方法

1.3 基于子词(Subword-based)

基于子词的分词方法依赖这样一个原则:不应将常用词拆分为更小的子词,而应将稀有词分解为有意义的子词。

例如,“annoyingly”可能被认为是一个罕见的词,可以分解为“annoying”和“ly”。这两者都可能作为独立的子词出现的更频繁,同时“annoyingly”的含义由“annoying”和“ly”的复合含义保持。

这里我们使用 transformers 库中的 AutoTokenizer 初始化一个 Bert 的 Tokenizer,并用它对上述文本进行分词

>>> text = "I like computer vision and transformers."
>>> from transformers import AutoTokenizer
>>> tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
Downloading (…)okenizer_config.json: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 29.0/29.0 [00:00<00:00, 24.8kB/s]
Downloading (…)lve/main/config.json: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 570/570 [00:00<00:00, 809kB/s]
Downloading (…)solve/main/vocab.txt: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 213k/213k [00:00<00:00, 382kB/s]
Downloading (…)/main/tokenizer.json: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 436k/436k [00:00<00:00, 1.30MB/s]
>>> tokenizer.tokenize(text)
['I', 'like', 'computer', 'vision', 'and', 'transform', '##ers', '.']

AutoTokenizer.from_pretrained("bert-base-cased") 这一步会自动从 Huggingface 官网下载相应的 bert 分词模型,随后我们调用 tokenizer 的 tokenize 方法就能够对文本进行分词了,其中 transformers 被分为了 transform##ers,即将一个不常用的词分为两个更有意义的常用词,这种分词方式可以减少词汇表的数量,同时保留了单词的含义

1.4 Huggingface 中 Tokenizer 的使用

对于一段文本,我们的目的是对其进行编码,使得模型能够读懂它,这个过程主要有两步:标记(token)化,转换为输入 id。这两步在 Huggingface 中只需要调用 tokenizer 即可解决,如下所示

>>> text = "I like computer vision and transformers."
>>> tokenizer(text)
{'input_ids': [101, 146, 1176, 2775, 4152, 1105, 11303, 1468, 119, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}

除此之外我们还可以通过几个 tokenizer 的方法来查看他的分词过程

>>> tokens = tokenizer.tokenize(text)
>>> print(tokens)
['I', 'like', 'computer', 'vision', 'and', 'transform', '##ers', '.']
>>> ids = tokenizer.convert_tokens_to_ids(tokens)
>>> print(ids)
[146, 1176, 2775, 4152, 1105, 11303, 1468, 119]
>>> decoded_string = tokenizer.decode(ids)
>>> print(decoded_string)
I like computer vision and transformers.

这里我们调用了三个 tokenizer 的方法,分别是 tokenizeconvert_tokens_to_idsdecode,其中 tokenize 方法将文本分词,convert_tokens_to_ids 方法将分词后的文本转换为 id,decode 方法将 id 转换为文本。

注意这里直接调用 tokenizer 和调用其方法进行分词结果略有不同,直接调用的前后加入了两个 id:101 和 102。这是给文本前后加入启示和终止符,这样我们就知道句子什么时候开始什么时候结束了,后面会详细介绍这些方法

2. Tokenizer 与 Model

如上所述,Tokenizer 将文本处理数字形式,转化为模型可以处理的形式,下面我们看一下 Huggingface 中的 Tokenizer 和 Model 是如何配合使用的

2.1 将文本输入到模型中

下面我们使用一个分类模型来演示如何将文本输入到模型中

>>> from transformers import AutoTokenizer, AutoModelForSequenceClassification
>>> checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
>>> tokenizer = AutoTokenizer.from_pretrained(checkpoint)
Downloading (…)okenizer_config.json: 100%|████████████████████████████████████████████████████████████████████████████████████| 48.0/48.0 [00:00<00:00, 105kB/s]
Downloading (…)lve/main/config.json: 100%|█████████████████████████████████████████████████████████████████████████████████████| 629/629 [00:00<00:00, 1.46MB/s]
Downloading (…)solve/main/vocab.txt: 100%|████████████████████████████████████████████████████████████████████████████████████| 232k/232k [00:00<00:00, 546kB/s]
>>> model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
Downloading model.safetensors: 100%|█████████████████████████████████████████████████████████████████████████████████████████| 268M/268M [00:10<00:00, 24.5MB/s]
>>> sequence = "I've been waiting for a HuggingFace course my whole life."
>>> tokens = tokenizer.tokenize(sequence)
>>> ids = tokenizer.convert_tokens_to_ids(tokens)
>>> input_ids = torch.tensor([ids])
>>> model(input_ids)
>>> SequenceClassifierOutput(loss=None, logits=tensor([[-2.7276,  2.8789]], grad_fn=<AddmmBackward0>), hidden_states=None, attentions=None)


'''
tokens
['i', "'", 've', 'been', 'waiting', 'for', 'a', 'hugging', '##face', 'course', 'my', 'whole', 'life', '.']
ids
[1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012]
'''

tokenizer 在将文本转换为 id 之后我们需要确保输入到模型中的 input_ids二维的,因为模型通常需要处理多个句子,所以这里我们将单个句子拓展为 2 维后进行了输入

2.2 多序列处理

在示例中我们能够对单个序列进行处理了,但是当处理多序列时会遇到以下问题

假设我们有两个句子,他们的 id 如下

>>> sequence1_ids = [[200, 200, 200]]
>>> sequence2_ids = [[200, 200]]
>>> batched_ids = [[200, 200, 200], [200, 200]]

他们合起来的输入到网络中为 batched_ids,但是这无法输入到网络中,因为这个矩阵同一维度的大小是不一样的,所需要对齐操作,如下

>>> sequence1_ids = [[200, 200, 200]]
>>> sequence2_ids = [[200, 200]]
>>> batched_ids = [[200, 200, 200], [200, 200, tokenizer.pad_token_id]]
>>> model(torch.tensor(sequence1_ids)).logits
tensor([[ 1.5694, -1.3895]], grad_fn=<AddmmBackward0>)
>>> model(torch.tensor(sequence2_ids)).logits
tensor([[ 0.5803, -0.4125]], grad_fn=<AddmmBackward0>)
>>> model(torch.tensor(batched_ids)).logits
tensor([[ 1.5694, -1.3895],
        [ 1.3374, -1.2163]], grad_fn=<AddmmBackward0>)

注意这里 sequence2batched_ids 的第二行结果应该是一样的,但是为什么得到的值不同呢?因为 Transformer 的注意力机制会考虑当前序列的所有 token,因此会考虑 pad_token_id。为了在填充与不填充句子时获得相同的结果,我们需要告诉 Transformer 哪一部分被 padding 了,这通过 attention_mask 参数来实现,如下

>>> batched_ids = [[200, 200, 200], [200, 200, tokenizer.pad_token_id]]
>>> attention_mask  = [[1, 1, 1], [1, 1, 0]]
>>> model(torch.tensor(batched_ids), attention_mask=torch.tensor(attention_mask)).logits
tensor([[ 1.5694, -1.3895],
        [ 0.5803, -0.4125]], grad_fn=<AddmmBackward0>)

这里的结果就和上述单序列输入的结果相同了

2.3 Tokenizer 的参数使用

上面我们介绍了多序列的处理方法,幸运的是,在 Huggingface 中并不需要我们手动去 padding,我们只需要直接调用 tokenizer 分词器即可,下面介绍在 tokenizer 中的常用方法

1. 直接调用 tokenizer 分词器

Huggingface 中实例化的 tokenizer 本身就是可调用对象,可以直接对文本进行分词

>>> sequence = "I've been waiting for a HuggingFace course my whole life."
>>> tokenizer(sequence)
{'input_ids': [101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012, 102], 
'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}
>>> sequences = ["I've been waiting for a HuggingFace course my whole life.", "So have I!"]
>>> tokenizer(sequences)
{'input_ids': [[101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012, 102], [101, 2061, 2031, 1045, 999, 102]], 
'attention_mask': [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1]]}

2. padding 参数

上面例子 tokenizer 并没有进行填充等操作,下面我们加入 padding 参数

>>> sequences = ["I've been waiting for a HuggingFace course my whole life.", "So have I!"]
# 不加任何参数
>>> tokenizer(sequences)
{'input_ids': [[101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012, 102], [101, 2061, 2031, 1045, 999, 102]], 
'attention_mask': [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1]]}
# 按照序列中最长的序列进行填充,将所有短序列填充到最长序列的长度
# "longest"也可以用True代替
>>> tokenizer(sequences, padding="longest")
{'input_ids': [[101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012, 102], [101, 2061, 2031, 1045, 999, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], 'attention_mask': [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]}
# 按照能够容纳的最长序列进行填充,这里太长了展示不开
>>> tokenizer(sequences, padding="max_length")
{'input_ids': [[101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012, 102, 0, ..., 0], [101, 2061, 2031, 1045, 999, 102, 0, ..., 0]], 'attention_mask': [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, ..., 0], [1, 1, 1, 1, 1, 1, 0, ..., 0]]}
# 指定最长长度
>>> tokenizer(sequences, padding="max_length", max_length=8)
{'input_ids': [[101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012, 102], [101, 2061, 2031, 1045, 999, 102, 0, 0]], 'attention_mask': [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 0, 0]]}

3. truncation 参数

truncation 参数表示当序列长度超过 max_length 时,是否进行截断,Huggingface 中默认是 False,即不进行截断

>>> tokenizer(sequences, padding=True)

4. return_tensors 参数

return_tensors 参数表示返回的数据类型,pt 返回 PyTorch 张量,tf 返回 TensorFlow 张量,np 返回 NumPy 数组

# Returns PyTorch tensors
>>> model_inputs = tokenizer(sequences, padding=True, return_tensors="pt")
# Returns TensorFlow tensors
>>> model_inputs = tokenizer(sequences, padding=True, return_tensors="tf")
# Returns NumPy arrays
>>> model_inputs = tokenizer(sequences, padding=True, return_tensors="np")