Reference:【HuggingFace
Transformers-入门篇】基础组件之Tokenizer,Huggingface NLP
Course
Transformer
模型只接受张量作为输入。与其他神经网络一样,Transformer
模型不能直接处理原始文本,因此的第一步是将文本输入转换为模型可以理解的数字。为此,我们使用一个分词器tokenizer,它将负责:
- 将输入拆分为称为标记的单词、子单词或符号(如标点符号)
- 将每个标记映射到整数
- 添加可能对模型有用的其他输入

Tokenizer基本使用
我们可以直接给tokenizer传入句子,随后会得到一个字典,该字典可以提供给我们的模型。剩下唯一要做的就是将输入
ID 列表转换为张量。 
加载与保存
AutoTokenizer
会根据传入的参数自动分配默认的tokenizer。加载的时候会用到from_pretrained()
方法,即从预训练阶段开始加载。
1 2 3 4
| from transformers import AutoTokenizer sen = "弱小的我也有大梦想!"
tokenizer = AutoTokenizer.from_pretrained("uer/roberta-base-finetuned-dianping-chinese")
PYTHON
|
可以看到这是一个基于Bert的模型。

默认的下载路径是C:\Users\Administrator\.cache\huggingface\hub\
,也可以指定路径存储。

模型文件里面的snapshots文件夹里是模型有用的文件。

使用save_pretrained()
方法来将tokenizer保存到指定路径。./roberta_tokenizer表示将路径设置为代码文件所在文件夹的roberta_tokenizer文件夹中。
1 2 3 4
| tokenizer.save_pretrained("./roberta_tokenizer")
tokenizer = AutoTokenizer.from_pretrained("./roberta_tokenizer/")
PYTHON
|

这里记一下从官方文档看到的两个英文词概念:
- Architecture:
模型的骨架,即模型中每个层和每个操作的定义。
- Checkpoints: 在给定架构中加载的权重。
例如,BERT是一个architecture,而 bert-base-cased(Google为 BERT
第一个版本训练的一组权重)则是一个checkpoint。
句子分词
加载完tokenizer之后需要对句子进行分词,可以使用tokenize()
方法。
1
| tokens = tokenizer.tokenize(sen)
PYTHON
|
当前选择的基于BERT的模型分词结果如下。

查看词典
可以查看这个分词器的词典,井号我觉得可以理解为占位符,意思是这个字通常出现在一个词的第二个位置,也就是说比起这个字单独出现,它和其他字一起出现的概率更高,具体可以看搜搜subword的概念。
BERT的分词方式是WordPiece,词表的构造是基于subword(子词)的,有助于缓解OOV(Out
of Vocabulary)问题,即测试集有很多模型训练时没见过的单词。
1 2
| print(tokenizer.vocab) print(tokenizer.vocab_size)
PYTHON
|

索引转换
使用convert_tokens_to_ids()
将分词后的token序列转换成id序列送入神经网络。
1 2 3 4 5 6
| ids = tokenizer.convert_tokens_to_ids(tokens)
tokens = tokenizer.convert_ids_to_tokens(ids)
str_sen = tokenizer.convert_tokens_to_string(tokens)
PYTHON
|

更简便的方法是使用encode()
一步实现句子序列到id序列的转换。这里有两个参数,sen即输入的句子,add_special_tokens用来控制是否给id序列添加特殊符号。
在BERT的分词方法中,会在id序列的前后或者中间加入一些特殊符号,如这里的[CLS]和[SEP],前者代表一句话的开始,后者标志着一句话的结束。
1 2 3 4
| ids = tokenizer.encode(sen, add_special_tokens=True)
str_sen = tokenizer.decode(ids, skip_special_tokens=False)
PYTHON
|
可以看到使用encode()
方法会在句子前后多了101和102,其实就对应特殊符号[CLS]和[SEP],因为add_special_tokens=True,不添加的话可以设为False。同理,解码decode()
也可以通过这个参数选择要不要将特殊符号及进行解码。

填充与截断
让模型处理大量数据时会涉及到数据的填充和截断,以统一每条数据长度。即短数据填充padding
,长数据截断truncation
。
1 2 3 4
| ids = tokenizer.encode(sen, padding="max_length", max_length=15)
ids = tokenizer.encode(sen, max_length=5, truncation=True)
PYTHON
|
以下是效果展示。需要注意的是每个处理后的句子前后都至少会有两个特殊符号,每条数据以5为最大长度截断但实际上只能存放三个词。

其他输入部分
因为存在填充,所以我们需要告诉模型哪些是填充的哪些是真实有效的输入,这就需要attention_mask
。此外,对于BERT模型,它需要token_type_ids
这个参数来区分哪些词属于第一句,哪些属于第二句,也就是段编码。
这是因为BERT的input由三部分组成,除了分词后的词向量(token
embeddings),还有段编码(segment embeddings)和位置编码(Position
Embedding),详见之前的PLMs的笔记。
1 2 3
| ids = tokenizer.encode(sen, padding="max_length", max_length=15) attention_mask = [1 if idx != 0 else 0 for idx in ids] token_type_ids = [0] * len(ids)
PYTHON
|
attention_mask
就是id为0记为0否则记为1。由于这是一句话的句间文本,所以token_type_ids
都是0,长度就是整个token的长度。
PLMs(GPT,
BERT)
快速调用方式
Huggingface
Transformers提供了快捷调用方式,可以一步实现上述步骤,也就是encode_plus()
方法。调用后返回一个dic,里面包含了id序列和attention_mask
。除此之外,直接调用tokenizer
(不是tokenize()
,注意区分)也可以得到一样的效果。
1 2
| inputs = tokenizer.encode_plus(sen, padding="max_length", max_length=15) inputs = tokenizer(sen, padding="max_length", max_length=15)
PYTHON
|

处理batch数据
批量处理数据也可以用同样的方法,返回的值形如注释。
1 2 3 4 5 6 7 8
| sens = ["弱小的我也有大梦想", "有梦想谁都了不起", "追逐梦想的心,比梦想本身,更可贵"] res = tokenizer(sens)
''' {'input_id' : [[..1..],[..2..],[..3..]], 'token_type_ids' : [[..1..],[..2..],[..3..]], 'attention_mask' : [[..1..],[..2..],[..3..]]} '''
PYTHON
|
对比for循环与batch处理的时间,可以发现batch明显快于循环计算。

Fast/Slow Tokenizer
- FastTokenizer:基于Rust实现,速度块,有两个额外返回值
offsets_mapping
、word_ids
。
- SlowTokenizer:基于Python实现,速度较慢。
使用from_pretrained()
方法分配tokenizer时默认分配FastTokenizer,如果要设置成SlowTokenizer需括号内增加参数use_fast=False
。
1 2 3
| sen = "弱小的我也有大Dreaming!" fast_tokenizer = AutoTokenizer.from_pretrained("uer/roberta-base-finetuned-dianping-chinese") slow_tokenizer = AutoTokenizer.from_pretrained("uer/roberta-base-finetuned-dianping-chinese", use_fast=False)
PYTHON
|

FastTokenizer有个额外返回值offsets_mapping
,使用时将需要设置return_offsets_mapping=True。随之还会返回word_ids
,这两个值相互对应。
1 2
| inputs = fast_tokenizer(sen, return_offsets_mapping=True) print(inputs.word_ids)
PYTHON
|
- sen = "弱小的我也有大Dreaming!"。
- 'offset_mapping': [(0, 0), (0, 1), (1, 2), (2, 3), (3, 4), (4, 5),
(5, 6), (6, 7), (7, 12), (12, 15), (15, 16), (0, 0)]
- word_ids: [None, 0, 1, 2, 3, 4, 5, 6, 7, 7, 8, None]
word_ids会记录每个词在句子中的位置,None分别对应句首句尾的特殊字符[CLS]和[SEP],也就是offset_mapping里的(0,0)。(0,1)对应第一个字"弱"的位置,(6,7)对应第7个字"大"的位置。
因为截断,所以Dreaming被分为两个数据段,所以word_ids里有两个7,对应offset_mapping的(7,12)和(12,15),表示这两个部分合起来是一个完整的token。
这里我还试了其他的例子:
这个更多的会在QA中用到,因为回答问题时需要记录答案在原始文本中开始和结束的位置。
特定Tokenizer的加载
还可以加载特定模型的分词器(只要Huggingface
Model里有),比如这里想用天工的分词器。
找到模型名使用from_pretrained进行加载,加载时必须添加参数trust_remote_code=True
,不然会报错。
1 2 3 4 5 6 7
| from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("Skywork/Skywork-13B-base", trust_remote_code=True) tokenizer.save_pretrained("skywork_tokenizer") tokenizer = AutoTokenizer.from_pretrained("skywork_tokenizer", trust_remote_code=True) tokenizer.decode(tokenizer.encode(sen))
PYTHON
|

【代码汇总】
以换行为分割线,单独运行每块代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| from transformers import AutoTokenizer
sen = "弱小的我也有大梦想!"
tokenizer = AutoTokenizer.from_pretrained("uer/roberta-base-finetuned-dianping-chinese")
tokenizer.save_pretrained("./roberta_tokenizer")
tokenizer = AutoTokenizer.from_pretrained("./roberta_tokenizer/")
tokens = tokenizer.tokenize(sen) print(tokenizer.vocab) print(tokenizer.vocab_size)
ids = tokenizer.convert_tokens_to_ids(tokens)
tokens = tokenizer.convert_ids_to_tokens(ids)
str_sen = tokenizer.convert_tokens_to_string(tokens)
ids = tokenizer.encode(sen, add_special_tokens=True)
str_sen = tokenizer.decode(ids, skip_special_tokens=False)
ids = tokenizer.encode(sen, padding="max_length", max_length=15)
ids = tokenizer.encode(sen, max_length=5, truncation=True)
ids = tokenizer.encode(sen, padding="max_length", max_length=15) attention_mask = [1 if idx != 0 else 0 for idx in ids] token_type_ids = [0] * len(ids)
inputs = tokenizer.encode_plus(sen, padding="max_length", max_length=15) inputs = tokenizer(sen, padding="max_length", max_length=15)
sens = ["弱小的我也有大梦想", "有梦想谁都了不起", "追逐梦想的心,比梦想本身,更可贵"] res = tokenizer(sens)
sen = "弱小的我也有大Dreaming!" fast_tokenizer = AutoTokenizer.from_pretrained("uer/roberta-base-finetuned-dianping-chinese") slow_tokenizer = AutoTokenizer.from_pretrained("uer/roberta-base-finetuned-dianping-chinese", use_fast=False)
inputs = fast_tokenizer(sen, return_offsets_mapping=True) print(inputs.word_ids)
tokenizer = AutoTokenizer.from_pretrained("Skywork/Skywork-13B-base", trust_remote_code=True) tokenizer.save_pretrained("skywork_tokenizer") tokenizer = AutoTokenizer.from_pretrained("skywork_tokenizer", trust_remote_code=True) tokenizer.decode(tokenizer.encode(sen))
PYTHON
|