HuggingFace自然语言处理详解:基于BERT中文模型的任务实战
上QQ阅读APP看书,第一时间看更新

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]也被正确识别。