说明
上一篇Python - 深度学习系列34 重塑实体识别介绍了如何进行训练,这篇讨论如何应用。
详细review了之后,发现其实早先的服务还是略有欠缺的。例如:
- 1 最早的时候好像还没有pipeline,我用DataFrame并行处理,然后用模型的裸方法,可能不一定那么优雅。(意味着性能或信息有丢失)
minput = torch.cat(the_ss_codecs_values)
if self.device == 'cuda':
if self.model.device.type != 'cuda':
self.model.to('cuda')
with torch.no_grad():
input_cuda = minput.to('cuda')
outputs_cuda = self.model(input_cuda).logits
predictions = torch.argmax(outputs_cuda, dim=2)
predictions_list = list(predictions.to('cpu').numpy())
- 2 在输出结果时丢掉了位置信息。最直观的问题是,如果产品需要进行高亮,那么是没办法的。
import numpy as np
@staticmethod
def inv_convert_chinese_word_tag_list2BIO_v3(data_dict):
data_list = data_dict['data_list']
tag_list = data_dict['tag_list']
res_data_list = []
data_list1 = copy.deepcopy(data_list)
# 如果tag_list都是无用词
if set(tag_list) == {'O'}:
return ','
else:
tag_arr = np.array(tag_list)
ner_start_idx_list = list(np.where(tag_arr=='B')[0])
for the_ner in ner_start_idx_list:
tem_data = ''
for i in range(198):
if the_ner+i == len(data_list1):
break
if the_ner+i >=198:
break
if tag_list[the_ner+i] != 'O':
tem_data += data_list1[the_ner+i]
else:
break
res_data_list.append(tem_data + ',')
return ','.join(res_data_list)
内容
1 目标
本次迭代之后可以直接看到的效果应该是:
- 1 模型的训练与调用方法更科学
- 2 输出的结果更完整(按序输出)
- 3 句子的分割更合理(仅以部分强分隔符切分句子)
- 4 初步形成流水线方法
下一阶段的目标则是不断用更多的模型类型以及尺寸去调教一套实体识别模型。之后至少会使用Electra, Roberta,DistilBert几类模型;并且除了实体识别,还需要有情感分析和局对匹配等内容。
2 使用实例
载入模型,注意device选项
from Basefuncs import *
# 载入模型
import transformers
import torch
from transformers import AutoModelForMaskedLM, AutoTokenizer
xmodel_path = 'model01'
from transformers import pipeline
# 加载中文 Electra 模型和 tokenizer
ner = pipeline("ner", model=xmodel_path, tokenizer=xmodel_path, device=0)
获取数据
host = 'xxxxxx.cn'
port = 19000
database = 'model_train_datasets'
user = 'xxx'
password = 'xxx'
name = 'tem'
chc = CHClient(host = host, port = port , database = database, user = user, password = password, name = name )
the_sql = 'show tables'
chc._exe_sql(the_sql)
# 取数
data_tuple_list = chc.get_table('train_ner_news_title_org_20240517')
df = pd.DataFrame(data_tuple_list, columns = ['mid', 'x', 'y'])
data_list = list(df['x'])
# sub_list
sub_list = data_list[:1000]
模型执行预测
sentences = sub_list
entities = ner(sentences)
在这里,CPU和GPU执行有巨大的时间差别。使用 timeit测试,CPU对100条数据的识别大约是6.9秒,而GPU对1000条数据的执行约为8.11秒。其中GPU的负载观察下来只有25%,所以还可以继续并行。
CPU的信息如下,因为是虚拟机切分,所以单精度浮点算力约为0.4T
而GPU单精约为30T。
理论上GPU应该快60倍,实际上如果可以满载的话大约是40倍。中间有部分因为流程原因,如tokenize或者内存-显存搬运消耗掉了。所以这类任务应该进行一些并行化,大约到30倍左右就比较理想了。
解析
通过pipeline可以很快获得实体的结果,但是无法直接使用。
每一个sentence的解析结果是listofdict, 序列分类的任务结果是每个token都有一个分类。我们需要从这些结果中解析出实体以及对应的起始位置。考虑到数据增强,位置现在变得更关键,我希望结果可以通过labelstudio进行有效的展示和标注,然后形成质量更好的回流数据。
解析依赖的功能有以下几个:
- 1 convert_entity_label 将LABEL_0,1,2转为最初的字符标签
- 2 detokenize 反令牌化,将一些因为tokenize而改变的字还原回来(这里我也理解了为什么是按单个字拆分令牌,因为模型会加上##作为特殊的拓展)
- 3 extract_bio_positions 抽出实体所在位置。这里将循环改为正则真的是灵感,快多了。
from datasets import ClassLabel
# 定义标签列表
label_list = ['B', 'I', 'O']
# 创建 ClassLabel 对象
class_label = ClassLabel(names=label_list)
def convert_entity_label(x):
x1 = int(x.split('_')[-1])
return class_label.int2str(x1)
def detokenize(word_piece):
"""
将 WordPiece 令牌还原为原始句子。
"""
if word_piece.startswith('##'):
x = word_piece[2:]
else:
x = word_piece
return x
import re
def extract_bio_positions(bio_string):
pattern = re.compile(r'B(I+)(O|$)')
matches = pattern.finditer(bio_string)
results = []
for match in matches:
start, end = match.span()
results.append((start, end - 1)) # end-1 to include the last 'I'
return results
# 示例字符串
bio_string = "BIIIIIIOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO"
# 提取位置
positions = extract_bio_positions(bio_string)
# 打印结果
for start, end in positions:
print(f"Entity starts at {start} and ends at {end}")
对于每一个句子的结果处理,有两种思路:
- 1 使用pandas + apply, 直观但是开销比较大
- 2 使用 map, 不直观,但是开销小
返回都是 (ent, start, end) tuple list
pandas apply方法
some_entity_list = entities[0]
def parse_ent_pos_pandas_apply(some_listofdict = None):
some_entity_list = some_listofdict
some_entity_df = pd.DataFrame(some_entity_list)
some_entity_df['label'] = some_entity_df['entity'].apply(convert_entity_label)
some_entity_df['ori_word'] = some_entity_df['word'].apply(detokenize)
label_list = list(some_entity_df['label'])
oriword_list = list(some_entity_df['ori_word'])
label_str = ''.join(label_list)
oriword_str = ''.join(oriword_list)
pos_list = extract_bio_positions(label_str)
part_ent_list = [(oriword_str[x[0]:x[1]] , *x) for x in pos_list]
return part_ent_list
%%timeit
parse_ent_pos_pandas_apply(some_entity_list)
1.15 ms ± 2.12 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
map方法: 快了10倍不止。
def parse_ent_pos_map(some_listofdict = None):
some_entity_list = some_listofdict
word_list = [x['word'] for x in some_entity_list]
label_list = [x['entity'] for x in some_entity_list]
label_str =''.join(map(convert_entity_label,label_list))
ori_word_str =''.join(map(detokenize,word_list))
pos_list = extract_bio_positions(label_str)
part_ent_list = [(ori_word_str[x[0]:x[1]] , *x) for x in pos_list]
return part_ent_list
%%timeit
parse_ent_pos_map(some_entity_list)
76.5 µs ± 137 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
最后进行批量生成
some_s = pd.Series(entities)
some_s2 =some_s.apply(parse_ent_pos_map)
res_df = pd.DataFrame()
res_df['clean_data'] = list(sub_list[:10])
res_df['ent_tuple_list0'] =list(some_s2)[:10]
res_df['ent_tuple_list'] = res_df['ent_tuple_list0'].apply(lambda x: [a + ('ORG',) for a in x])
res_df.head()
写到这里,暂时告一段落,先看看效果再进行下一步。