预训练BERT的Tokenizer有着强大的embedding的表征能力,基于BERT的Tokenizer的特征矩阵可以进行下游任务,包括文本分类,命名实体识别,关系抽取,阅读理解,无监督聚类等。由于最近的工作涉及到了Tokenizer,利用hugging face的transformers学习了Tokenizer,整理了这篇博客,如有理解表达不当,欢迎大家指正。
加载预训练BERT的Tokenizer,这里采用的bert-base-chinese预训练模型,代码如下:
from transformers import BertTokenizerFast
tokenizer = BertTokenizerFast.from_pretrained(
"D:/Spyder/pretrain_model/transformers_torch_tf/bert_base_chinese/",
add_special_tokens=False, # 不添加CLS,SEP
do_lower_case=True) # 区分大小写字母,
注意:vocab.txt中英文是小写,设置为do_lower_case=True,如果do_lower_case=False,中文中英文会变成[UNK]
对文本进行tokenizer,得到文本的tokens,token_span,token_ids,token_mask等
text = "30%~50%患儿血清IgA浓度升高;HSP急性期血循环中表面IgA阳性的B淋巴细胞数、IgA类免疫复合物或冷球蛋白均增高;"
tokens = tokenizer.tokenize(text)
token_span = tokenizer.encode_plus(text, return_offsets_mapping=True, add_special_tokens=False)["offset_mapping"]
tokens的输出的结果为
tokens: ['30', '%', '~5', '##0', '%', '患', '儿', '血', '清', '[UNK]', '浓', '度', '升', '高', ';', '[UNK]', '急', '性', '期', '血', '循', '环', '中', '表', '面', '[UNK]', '阳', '性', '的', '[UNK]', '淋', '巴', '细', '胞', '数', '、', '[UNK]', '类', '免', '疫', '复', '合', '物', '或', '冷', '球', '蛋', '白', '均', '增', '高']
token_span的输出结果为
token_span: [(0, 2), (2, 3), (3, 5), (5, 6), (6, 7), (7, 8), (8, 9), (9, 10), (10, 11), (11, 14), (14, 15), (15, 16), (16, 17), (17, 18), (18, 19), (19, 22), (22, 23), (23, 24), (24, 25), (25, 26), (26, 27), (27, 28), (28, 29), (29, 30), (30, 31), (31, 34), (34, 35), (35, 36), (36, 37), (37, 38), (38, 39), (39, 40), (40, 41), (41, 42), (42, 43), (43, 44), (44, 47), (47, 48), (48, 49), (49, 50), (50, 51), (51, 52), (52, 53), (53, 54), (54, 55), (55, 56), (56, 57), (57, 58), (58, 59), (59, 60), (60, 61)]
tokens的30对应原文中的text的索引位置为[0,1]
通常在实际任务中,需要将文本中字与tokenizer的编码对应起来会使用到token_span,有很多时候,文本中的文字的位置index与tokenizer中的tokens的index不是一一对应的,一个字符经过tokenizer后得到多个字符,下面通过两个代码说明tokens和文本的字符建议一一对应的关系:
方法1
# 获取文本的长度
char_num = None
for tok_ind in range(len(token_span) - 1, -1, -1):
if token_span[tok_ind][1] != 0:
char_num = token_span[tok_ind][1]
break
# 建立文本与tokens之间的对应关系
char2tok_span = [[-1, -1] for _ in range(char_num)] # [-1, -1] is whitespace
for tok_ind, char_sp in enumerate(token_span):
for char_ind in range(char_sp[0], char_sp[1]):
tok_sp = char2tok_span[char_ind]
# 因为char to tok 也可能出现1对多的情况,比如韩文。所以char_span的pos1以第一个tok_ind为准,pos2以最后一个tok_ind为准
if tok_sp[0] == -1:
tok_sp[0] = tok_ind
tok_sp[1] = tok_ind + 1
方法2
context_tokens = tokenizer.encode(context, add_special_tokens=False)
tokens = query_context_tokens.ids
type_ids = query_context_tokens.type_ids
offsets = query_context_tokens.offsets
origin_offset2token_idx_start = {}
origin_offset2token_idx_end = {}
for token_idx in range(len(tokens)):
# skip query tokens
if type_ids[token_idx] == 0:
continue
token_start, token_end = offsets[token_idx]
# skip [CLS] or [SEP]
if token_start == token_end == 0:
continue
origin_offset2token_idx_start[token_start] = token_idx
origin_offset2token_idx_end[token_end] = token_idx
start_positions = [1, 3, 5, 6]
new_positions = [origin_offset2token_idx_start[start] for start in start_positions]
方法2的处理方式只争对纯中文,如果文本中包含中文,英文,数字,将文本的字符与tokens对应出现偏差,因为tokenizer的结果可能不是一个字符。而方法1可以避免出现上述偏差的现象。
basic tokenzier和上文使用的BertTokenizerFast不一样,如果中文文本中含有英文,basic tokenizer 会将英文识别为单词,BertTokenizerFast会将英文识别为英文单词本身,或者##xxx之类,详细看下面的例子
(1)basic tokenizer
from transformers import BasicTokenizer
basic_tokenizer = BasicTokenizer(do_lower_case=True)
text = "临时用电“三省”fighting服务相关说明"
basic_token1 = basic_tokenizer.tokenize(text)
print("basic token2: ", basic_token1)
text = "掌上营业厅2019APP充值卡APP充值"
basic_token2 = basic_tokenizer.tokenize(text)
print("basic token2: ", basic_token2)
tokens的结果为
basic token1: ['临', '时', '用', '电', '“', '三', '省', '”', 'fighting', '服', '务', '相', '关', '说', '明']
basic token2: ['掌', '上', '电', '力', '2019app', '充', '值', '卡', 'app', '充', '值']
(2)tokenzier
这里使用transformers中的BertTokenizerFast,
from transformers import BertTokenizerFast
tokenizer = BertTokenizerFast.from_pretrained(bert_path,
add_special_tokens=False,
do_lower_case=True)
text = "临时用电“三省”fighting服务相关说明"
tokens1 = tokenizer.tokenize(text)
text = "掌上营业厅2019APP充值卡APP充值"
token2 = tokenizer.tokenize(text)
print("tokens2:", tokens2)
tokens的结果为
tokens1: ['临', '时', '用', '电', '[UNK]', '三', '省', '[UNK]', 'fi', '##ght', '##ing', '服', '务', '相', '关', '说', '明']
tokens2: ['掌', '上', '电', '力', '2019', '##app', '充', '值', '卡', 'app', '充', '值']
transformers中的tokenizer可以添加vocab.txt不包含的词,从而实现了对特殊指定的词进行tokens编码。详细看代码
from transformers import BertTokenizer
pretrain_model_path = "D:/Spyder/pretrain_model/transformers_torch_tf/bert-base-chinese/"
tokenizer = BertTokenizer.from_pretrained(pretrain_model_path)
# 注意这里对 的tokenizer的结果
text = "舅父赵叔孺 亦为闽中大收藏家,林寿图 从小耳濡目染,对书画颇有灵性。"
ori_tokens = tokenizer.tokenize(text)
print("ori_tokens: ", ori_tokens)
ori_tokens 结果为:
ori_tokens: ['舅', '父', '<', 'e1', '>', '赵', '叔', '孺', '<', '/', 'e1', '>', '亦', '为', '闽', '中', '大', '收', '藏', '家', ',', '<', 'e2', '>', '林', '寿', '图', '<', '/', 'e2', '>', '从', '小', '耳', '濡', '目', '染', ',', '对', '书', '画', '颇', '有', '灵', '性', '。']
在tokenizer 中添加额外特殊的tokens, 例如添加 < e 1 >
add_special_tokens = ["" , "", "" , ""]
tokenizer.add_special_tokens({"additional_special_tokens": add_special_tokens})
special_tokens_text = " "
special_tokens = tokenizer.tokenize(special_tokens_text)
print("special tokens: ", special_tokens)
special_input_ids = tokenizer(special_tokens_text, max_length=64, truncation=True,
add_special_tokens=False).input_ids
print("special tokens input ids: ", special_input_ids)
输出结果如下:
special tokens: ['' , '', '' , '']
special tokens input ids: [21128, 21129, 21130, 21131]
备注:给添加的 < e 1 >
下面添加tokens 对text进行tokenizer
tokens = tokenizer.tokenize(text)
print("add special tokens: ", tokens)
print("add special tokens size: ", len(tokens))
输出结果为:
add special tokens: ['舅', '父', '' , '赵', '叔', '孺', '', '亦', '为', '闽', '中', '大', '收', '藏', '家', ',', '' , '林', '寿', '图', '', '从', '小', '耳', '濡', '目', '染', ',', '对', '书', '画', '颇', '有', '灵', '性', '。']
add special tokens size: 36
tokenizer提供了batch encoder的api,同时多个句子进行编码,获取特征。
text_list = ["今天天气温度很高", "来一场大暴雨"]
features = tokenizer.batch_encode_plus(text_list, add_special_tokens=False,
padding=True, truncation=True, max_length=16)
print("features: ", features["input_ids"])
结果如下:
input ids: [[791, 1921, 1921, 3698, 3946, 2428, 2523, 7770],
[3341, 671, 1767, 1920, 3274, 7433, 0, 0]]