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()

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

相关推荐
一点媛艺2 小时前
Kotlin函数由易到难
开发语言·python·kotlin
姑苏风2 小时前
《Kotlin实战》-附录
android·开发语言·kotlin
奋斗的小花生3 小时前
c++ 多态性
开发语言·c++
魔道不误砍柴功3 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
闲晨3 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
_.Switch3 小时前
高级Python自动化运维:容器安全与网络策略的深度解析
运维·网络·python·安全·自动化·devops
老猿讲编程3 小时前
一个例子来说明Ada语言的实时性支持
开发语言·ada
阿_旭4 小时前
一文读懂| 自注意力与交叉注意力机制在计算机视觉中作用与基本原理
人工智能·深度学习·计算机视觉·cross-attention·self-attention
王哈哈^_^4 小时前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
Chrikk4 小时前
Go-性能调优实战案例
开发语言·后端·golang