2.3 使用编码工具
经过以上示例,可以知道编码的过程中要经历哪些工作步骤了。现在就来看一看如何使用HuggingFace提供的编码工具。
1. 加载编码工具
首先需要加载一个编码工具,这里使用bert-base-chinese的实现,代码如下:
#第2章/加载编码工具 from transformers import BertTokenizer tokenizer = BertTokenizer.from_pretrained( pretrained_model_name_or_path='bert-base-chinese', cache_dir=None, force_download=False, )
参数pretrained_model_name_or_path='bert-base-chinese'指定要加载的编码工具,大多数模型会把自己提交的编码工具命名为和模型一样的名字。
模型和它的编码工具通常是成对使用的,不会出现张冠李戴的情况,建议调用者也遵从习惯,成对使用。
参数cache_dir用于指定编码工具的缓存路径,这里指定为None(默认值),也可以指定想要的缓存路径。
参数force_download为True时表明无论是否已经有本地缓存,都强制执行下载工作。建议设置为False。
2. 准备实验数据
现在有了一个编码工具,让我们来准备一些句子,以测试编码工具,代码如下:
#第2章/准备实验数据 sents = [ '你站在桥上看风景', '看风景的人在楼上看你', '明月装饰了你的窗子', '你装饰了别人的梦', ]
这是一些中文的句子,后面会用这几个句子做一些实验。
3. 基本的编码函数
首先从一个基本的编码方法开始,代码如下:
#第2章/基本的编码函数 out = tokenizer.encode( text=sents[0], text_pair=sents[1], #当句子长度大于max_length时截断 truncation=True, #一律补PAD,直到max_length长度 padding='max_length', add_special_tokens=True, max_length=25, return_tensors=None, ) print(out) print(tokenizer.decode(out))
这里调用了编码工具的encode()函数,这是最基本的编码函数,一次编码一个或者一对句子,在这个例子中,编码了一对句子。
不是每个编码工具都有编码一对句子的功能,具体取决于不同模型的实现。在BERT中一般会编码一对句子,这和BERT的训练方式有关系,具体可参见第14章。
(1)参数text和text_pair分别为两个句子,如果只想编码一个句子,则可让text_pair传None。
(2)参数truncation=True表明当句子长度大于max_length时,截断句子。
(3)参数padding= 'max_length'表明当句子长度不足max_length时,在句子的后面补充PAD,直到max_length长度。
(4)参数add_special_tokens=True表明需要在句子中添加特殊符号。
(5)参数max_length=25定义了max_length的长度。
(6)参数return_tensors=None表明返回的数据类型为list格式,也可以赋值为tf、pt、np,分别表示TensorFlow、PyTorch、NumPy数据格式。
运行结果如下:
[101, 872, 4991, 1762, 3441, 677, 4692, 7599, 3250, 102, 4692, 7599, 3250, 4638, 782, 1762, 3517, 677, 4692, 872, 102, 0, 0, 0, 0] [CLS] 你 站 在 桥 上 看 风 景 [SEP] 看 风 景 的 人 在 楼 上 看 你 [SEP] [PAD] [PAD] [PAD] [PAD]
可以看到编码的输出为一个数字的list,这里使用了编码工具的decode()函数把这个list还原为分词前的句子。这样就可以看出编码工具对句子做了哪些预处理工作。
从输出可以看出,编码工具把两个句子前后拼接在一起,中间使用[SEP]符号分隔,在整个句子的头部添加符号[CLS],在整个句子的尾部添加符号[SEP],因为句子的长度不足max_length,所以补充了4个[PAD]。
另外从空格的情况也能看出,编码工具把每个字作为一个词。因为每个字之间都有空格,表明它们是不同的词,所以在BERT的实现中,中文分词处理比较简单,就是把每个字都作为一个词来处理。
4. 进阶的编码函数
完成了上面最基础的编码函数,现在来看一个稍微复杂的编码函数,代码如下:
#第2章/进阶的编码函数 out = tokenizer.encode_plus( text=sents[0], text_pair=sents[1], #当句子长度大于max_length时截断 truncation=True, #一律补零,直到max_length长度 padding='max_length', max_length=25, add_special_tokens=True, #可取值tf、pt、np,默认为返回list return_tensors=None, #返回token_type_ids return_token_type_ids=True, #返回attention_mask return_attention_mask=True, #返回special_tokens_mask 特殊符号标识 return_special_tokens_mask=True, #返回length 标识长度 return_length=True, ) #input_ids 编码后的词 #token_type_ids 第1个句子和特殊符号的位置是0,第2个句子的位置是1 #special_tokens_mask 特殊符号的位置是1,其他位置是0 #attention_mask PAD的位置是0,其他位置是1 #length 返回句子长度 for k, v in out.items(): print(k, ':', v) tokenizer.decode(out['input_ids'])
和之前不同,这里调用了encode_plus()函数,这是一个进阶版的编码函数,它会返回更加复杂的编码结果。和encode()函数一样,encode_plus()函数也可以编码一个句子或者一对句子,在这个例子中,编码了一对句子。
参数return_token_type_ids、return_attention_mask、return_special_tokens_mask、return_length表明需要返回相应的编码结果,如果指定为False,则不会返回对应的内容。
运行结果如下:
input_ids : [101, 872, 4991, 1762, 3441, 677, 4692, 7599, 3250, 102, 4692, 7599, 3250, 4638, 782, 1762, 3517, 677, 4692, 872, 102, 0, 0, 0, 0] token_type_ids : [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0] special_tokens_mask : [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1] attention_mask : [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] length : 25 '[CLS] 你 站 在 桥 上 看 风 景 [SEP] 看 风 景 的 人 在 楼 上 看 你 [SEP] [PAD] [PAD] [PAD] [PAD]'
首先看最后一行,这里把编码结果中的input_ids还原为文字形式,可以看到经过预处理的原文本。预处理的内容和encode()函数一致。
这次编码的结果和encode()函数不一样的地方在于这次返回的不是一个简单的list,而是4个list和1个数字,见表2-2。
表2-2 进阶的编码函数结果
续表
接下来对编码的结果分别进行说明。
(1)输出input_ids:编码后的词,也就是encode()函数的输出。
(2)输出token_type_ids:因为编码的是两个句子,这个list用于表明编码结果中哪些位置是第1个句子,哪些位置是第2个句子。具体表现为,第2个句子的位置是1,其他位置是0。
(3)输出special_tokens_mask:用于表明编码结果中哪些位置是特殊符号,具体表现为,特殊符号的位置是1,其他位置是0。
(4)输出attention_mask:用于表明编码结果中哪些位置是PAD。具体表现为,PAD的位置是0,其他位置是1。
(5)输出length:表明编码后句子的长度。
5. 批量的编码函数
以上介绍的函数,都是一次编码一对或者一个句子,在实际工程中需要处理的数据往往是成千上万的,为了提高效率,可以使用batch_encode_plus ()函数批量地进行数据处理,代码如下:
#第2章/批量编码成对的句子 out = tokenizer.batch_encode_plus( #编码成对的句子 batch_text_or_text_pairs=[(sents[0], sents[1]), (sents[2], sents[3])], add_special_tokens=True, #当句子长度大于max_length时截断 truncation=True, #一律补零,直到max_length长度 padding='max_length', max_length=25, #可取值tf、pt、np,默认为返回list return_tensors=None, #返回token_type_ids return_token_type_ids=True, #返回attention_mask return_attention_mask=True, #返回special_tokens_mask 特殊符号标识 return_special_tokens_mask=True, #返回offsets_mapping 标识每个词的起止位置,这个参数只能BertTokenizerFast使用 #return_offsets_mapping=True, #返回length 标识长度 return_length=True, ) #input_ids 编码后的词 #token_type_ids 第1个句子和特殊符号的位置是0,第2个句子的位置是1 #special_tokens_mask 特殊符号的位置是1,其他位置是0 #attention_mask PAD的位置是0,其他位置是1 #length 返回句子长度 for k, v in out.items(): print(k, ':', v) tokenizer.decode(out['input_ids'][0])
参数batch_text_or_text_pairs用于编码一批句子,示例中为成对的句子,如果需要编码的是一个一个的句子,则修改为如下的形式即可。
batch_text_or_text_pairs=[sents[0], sents[1]]
运行结果如下:
input_ids : [[101, 872, 4991, 1762, 3441, 677, 4692, 7599, 3250, 102, 4692, 7599, 3250, 4638, 782, 1762, 3517, 677, 4692, 872, 102, 0, 0, 0, 0], [101, 21128, 21129, 749, 872, 4638, 21130, 102, 872, 21129, 749, 1166, 782, 4638, 3457, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0]] token_type_ids : [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]] special_tokens_mask : [[1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1], [1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]] length : [21, 16] attention_mask : [[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], [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]] '[CLS] 你 站 在 桥 上 看 风 景 [SEP] 看 风 景 的 人 在 楼 上 看 你 [SEP] [PAD] [PAD] [PAD] [PAD]'
可以看到,这里的输出都是二维的list了,表明这是一个批量的编码。这个函数在后续章节中会多次用到。
6. 对字典的操作
到这里,已经掌握了编码工具的基本使用,接下来看一看如何操作编码工具中的字典。首先查看字典,代码如下:
#第2章/获取字典 vocab = tokenizer.get_vocab() type(vocab), len(vocab), '明月' in vocab
运行后输出如下:
(dict, 21128, False)
可以看到,字典本身是个dict类型的数据。在BERT的字典中,共有21 128个词,并且“明月”这个词并不存在于字典中。
既然“明月”并不存在于字典中,可以把这个新词添加到字典中,代码如下:
#第2章/添加新词 tokenizer.add_tokens(new_tokens=['明月', '装饰', '窗子'])
这里添加了3个新词,分别为“明月”“装饰”和“窗子”。也可以添加新的符号,代码如下:
#第2章/添加新符号 tokenizer.add_special_tokens({'eos_token': '[EOS]'})
接下来试试用添加了新词的字典编码句子,代码如下:
#第2章/编码新添加的词 out=tokenizer.encode( text='明月装饰了你的窗子[EOS]', text_pair=None, #当句子长度大于max_length时截断 truncation=True, #一律补PAD,直到max_length长度 padding='max_length', add_special_tokens=True, max_length=10, return_tensors=None, ) print(out) tokenizer.decode(out)
输出如下:
[101, 21128, 21129, 749, 872, 4638, 21130, 21131, 102, 0] '[CLS] 明月 装饰 了 你 的 窗子 [EOS] [SEP] [PAD]'
可以看到,“明月”已经被识别为一个词,而不是两个词,新的特殊符号[EOS]也被正确识别。