少女祈祷中...

友情提示:由于HuggingFace社区触犯了天朝的某些法律,有关HuggingFace系列的内容中,提到“冲浪板”就指科学上网,需要借助国外旅游工具。

加载编码工具

下面这段代码加载了一个名为”bert-base-chinese”的中文编码工具。

1
2
3
4
5
6
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained(
pretrained_model_name_or_path='bert-base-chinese', #工具名称。
cache_dir=None, #缓存路径,不指定就为默认环境变量的路径,可查找相关资料修改环境变量。
force_download=False, #是否强制下载,建议设定为否,如果选是的话每次使用都要重新下载。
)

注意:如果是第一次使用,那么需要先下载,下载时需要借助冲浪板。后续再使用时可以不使用冲浪板,也可以使用。如果不使用,可能会出现下面这些输出,但是不影响使用。

1
'(MaxRetryError("HTTPSConnectionPool(host='huggingface.co', port=443): Max retries exceeded with url: /bert-base-chinese/resolve/main/tokenizer_config.json (Caused by ConnectTimeoutError(<urllib3.connection.HTTPSConnection object at 0x000002677A291C10>, 'Connection to huggingface.co timed out. (connect timeout=10)'))"), '(Request ID: fe978ef6-5237-46ce-b0bb-fb6e0290fe6f)')' thrown while requesting HEAD https://huggingface.co/bert-base-chinese/resolve/main/tokenizer_config.json

准备数据

现在准备一些中文的句子。

1
2
3
4
sents = ['你站在桥上看风景',
'看风景的人在楼上看你',
'明月装饰了你的窗子',
'你装饰了别人的梦',]

编码函数

有了编码工具和数据以后就可以使用编码函数对数据进行编码。

基本的编码函数

这是最简单的编码函数。虽说是最简单的编码工具,但也是一堆的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
#调用encode函数。
out = tokenizer.encode(
text=sents[0], #text和text_pair分别为两个句子,text_pair=None可以只编码一个句子。
text_pair=sents[1],
truncation=True, #当句子长度大于max_length时截断。
padding='max_length', #当句子长度不足时补PAD直到达到max_length。
add_special_tokens=True, #表示需要在句子中添加特殊符号。
max_length=25, #设定句子最大长度。
return_tensors=None, #return_tensors=None表示返回数据类型为list格式,
#可以赋值为tf,pt,bp,分别代表TensorFlow,Pytorch,Numpy的数据格式。
)
out
print(tokenizer.decode(out))
1
2
[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]

解释:
输出的第一行就是中文数据转为数字后的结果,因为在使用编码函数的时候没有指定返回类型,所以这里默认返回了列表,实际使用时可能会返回张量。中文文字一共是18个,而列表的长度是25。这其中补充了’[CLS]’(表示句子开头)’[SEP]’(句子分隔符)’[PAD]’(补足长度)。
输出的第二行是解码,就是把数字再转为文本。

进阶编码函数

这是一个稍微复杂一些的编码函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
out = tokenizer.encode_plus(
text=sents[0],
text_pair=sents[1],
truncation=True, #当句子长度大于max_length时截断。
padding='max_length', #当句子长度小于max_length补PAD。
max_length=25,
add_specidal_tokens=True, #句子中添加特殊符号。
return_tensors=None, #默认返回list,可取值tf,pt,np。
return_attention_mask=True, #返回attention_mask。
return_special_tokens_mask=True, #返回special_tokens_mask特殊符号标识。
return_length=True #返回length表示长度。
)

for k,v in out.items():
print(k,':',v)

tokenizer.decode(out['input_ids'])
1
2
3
4
5
6
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:中文文本编码后的结果。
token_type_ids:在编码函数中给了两个编码句子,这里的0和1分别指该处的文字属于第1个句子或第2个句子。具体的,1代表第二个句子,其余均为0。
special_tokens_mask:标注句子中的特殊符号,1代表特殊符号,其余均为0。
attention_mask:标注句子中的补足符号[PAD],1代表[PAD],其余均为0。
length:编码后的长度。

批量编码函数

可以一次性编码多个句子的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
out = tokenizer.batch_encode_plus(
batch_text_or_text_pairs=[(sents[0],sents[1]),(sents[2],sents[3])],
#参数batch_text_or_text_pairs用于编码一批句子,在本例中为成对的句子,
#如果需要编码单个的句子,则修改为
#batch_text_or_text_pairs=[sents[0],sents[1]]

add_special_tokens=True,
truncation=True, #当句子长度大于max_length时截断。
padding='max_length', #当句子长度小于max_length时补PAD。
max_length=25,
return_tensors=None,
return_token_type_ids=False, #简洁一下输出,这里就不返回这三样了。
return_attention_mask=False,
return_special_tokens_mask=False,
return_offsets_mapping=True, #返回offsets_mapping标识每个词的起止位置,
#这个参数只能BertTokenizerFast使用。
return_length=True
)
out
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, 3209, 3299, 6163, 7652, 749, 872, 4638, 4970, 2094, 102, 872, 6163, 7652, 749, 1166, 782, 4638, 3457, 102, 0, 0, 0, 0, 0]], 'length': [21, 20]}

可以看到input_ids中包含的两个列表,分别是两组句子编码后的结果。可以查看一下解码的结果。

1
2
tokenizer.decode(out['input_ids'][0])
tokenizer.decode(out['input_ids'][1])
1
2
'[CLS] 你 站 在 桥 上 看 风 景 [SEP] 看 风 景 的 人 在 楼 上 看 你 [SEP] [PAD] [PAD] [PAD] [PAD]'
'[CLS] 明 月 装 饰 了 你 的 窗 子 [SEP] 你 装 饰 了 别 人 的 梦 [SEP] [PAD] [PAD] [PAD] [PAD] [PAD]'

注意:
batch_text_or_text_pairs=[(sents[0],sents[1]),(sents[2],sents[3])]
在这个参数中,给定的是一个列表,列表中包含了文本。如果是成对的文本要带上括号用元组打包起来,并且一个元组只能打包最多两个文本。列表中可以同时含有成对的文本和单个文本。

编码工具的成分

虽然我们通过编码函数可以轻松地得到文本编码后的结果,也可以轻松地将数字解码成文本,但还不清楚编码工具都包含了什么。

字典

1
2
3
vocab = tokenizer.get_vocab()
type(vocab)
len(vocab)
1
2
<class 'dict'>
21128

这是一个获取字典的方法,可以看到vocab的类型就是字典,长度是21128,也就是说包含了21128个汉字及其对应的编码。同时也就是说,它只认识单个的汉字,多个字组成的词就不认识了。

1
vocab['你'],vocab['我'],vocab['[CLS]'],vocab['氟'],vocab[','],vocab[',']
1
(872, 2769, 101, 3703, 8024, 117)

查找某个字或符号的方法就是像用字典一样。最后两个符号是不一样的,第一个是中文逗号,第二个是英文逗号,可以看到它们的编码也不一样。
不过,对于某些生僻字,字典中没有收录。

1
vocab['燚'] #念yi,第四声。
1
2
3
Traceback (most recent call last):
File "<string>", line 1, in <module>
KeyError: '燚'

向字典中添加内容

原先的字典虽然收录了两万多个汉字,但是它不认识词语和一些生僻字。我们可以手动添加这些内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
'明月' in vocab
#加入内容,用列表打包。
tokenizer.add_tokens(new_tokens=['明月', '装饰', '窗子','燚'])
#添加新符号。
tokenizer.add_special_tokens({'eos_token':'[EOS]'})
vocab = vocab=tokenizer.get_vocab()
#再次查看。
'明月' in vocab
vocab['燚']
len(vocab)

#编码新添加的词。
out = tokenizer.encode(
text='明月装饰了你的窗子[EOS]',
text_pair=None,
truncation=True,
padding='max_length',
max_length=10,
return_tensors=None
)
out
tokenizer.decode(out)
1
2
3
4
5
6
7
8
False
4
1
True
21131
21133
[101, 21128, 21129, 749, 872, 4638, 21130, 21132, 102, 0]
'[CLS] 明月 装饰 了 你 的 窗子 [EOS] [SEP] [PAD]'

添加时tokenizer.add_tokens()会返回成功添加的数量,添加完成之后需要重新获取一下字典。如果需要添加的内容本来就存在于字典中,那么就不会添加。
在编码结果和解码结果里,可以看出编码工具已经可以识别词语了。