Python - 深度学习系列35 重塑实体识别2

说明

上一篇Python - 深度学习系列34 重塑实体识别介绍了如何进行训练,这篇讨论如何应用。

详细review了之后,发现其实早先的服务还是略有欠缺的。例如:

  • 1 最早的时候好像还没有pipeline,我用DataFrame并行处理,然后用模型的裸方法,可能不一定那么优雅。(意味着性能或信息有丢失)
python 复制代码
        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 在输出结果时丢掉了位置信息。最直观的问题是,如果产品需要进行高亮,那么是没办法的。
python 复制代码
    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选项

python 复制代码
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)  

获取数据

python 复制代码
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]

模型执行预测

python 复制代码
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 抽出实体所在位置。这里将循环改为正则真的是灵感,快多了。
python 复制代码
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方法

python 复制代码
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倍不止。

python 复制代码
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)

最后进行批量生成

python 复制代码
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()

写到这里,暂时告一段落,先看看效果再进行下一步。

相关推荐
卷心菜小温3 分钟前
【BUG】P-tuningv2微调ChatGLM2-6B时所踩的坑
python·深度学习·语言模型·nlp·bug
陈苏同学26 分钟前
4. 将pycharm本地项目同步到(Linux)服务器上——深度学习·科研实践·从0到1
linux·服务器·ide·人工智能·python·深度学习·pycharm
唐家小妹30 分钟前
介绍一款开源的 Modern GUI PySide6 / PyQt6的使用
python·pyqt
FL16238631291 小时前
[深度学习][python]yolov11+bytetrack+pyqt5实现目标追踪
深度学习·qt·yolo
XKSYA(小巢校长)1 小时前
NatGo我的世界联机篇
开发语言·php
羊小猪~~1 小时前
深度学习项目----用LSTM模型预测股价(包含LSTM网络简介,代码数据均可下载)
pytorch·python·rnn·深度学习·机器学习·数据分析·lstm
Cons.W1 小时前
Codeforces Round 975 (Div. 1) C. Tree Pruning
c语言·开发语言·剪枝
憧憬成为原神糕手1 小时前
c++_ 多态
开发语言·c++
VBA63371 小时前
VBA信息获取与处理第三个专题第三节:工作薄在空闲后自动关闭
开发语言
Marst Code1 小时前
(Django)初步使用
后端·python·django