建模杂谈系列248 使用pydantic进行实体识别数据转换

说明

实体识别的数据标注方式和传统的机器学习0/1标注的方法差别比较大,一般采用标注工具,如LabelStudio来对原始数据进行打标,然后转换为标准的可训练模式;模型训练后,对数据进行预测,结果仍然需要重新转为LabelStudio的格式,方便检查。

LS在过程中起到的作用非常大,有许多高级功能值得探索和学习。以下是我快速看了一遍之后的总结

理解label studio

  • 1 general

    • 确定数据类型
  • 2 label interface

    • 1 参考模板-可以发现更多的机器学习模型
    • 2 就实体模型而言,只有三个抽象列:
      • 1 数据列:这个是要打标的主要对象
      • 2 实体列:这里可以选择在数据上可标注的实体类型
      • 3 勾选列:可以根据对一些选项进行勾选
  • 3 machine learning

    • 1 了解与model的对接方式
    • 2 允许标注与模型联动
  • 4 cloud storage

  • 5 webhooks

    • 1 可以在特定情况下发出告警

回到本篇的内容:解决pydantic与实体识别模型之间的关联。

内容

我采用BIO的标注方式。

在这里插入图片描述
原始的BIO标注方法是可以在同一个句子中标注多种不同的实体的,例如 ORG-B, ORG-I , PER-B, PER-I。但如果考虑这种标注方式,在标准化的时候会有一点麻烦。所以我做了一些简化,即每个实体识别模型仅标注一种实体,所以就只有B、I和O。

由于实体识别是一种贯序的分类任务,例如一个实体"张三", 是由“BI”两个分类标签才能表示的,所以可以先有一个ent_tuple的概念。即由实体、开始位置和结束位置共同构成一个实体的表达。

1 将原始标注数据转为BIO数据

以下是样例数据
在这里插入图片描述
其中content是原文主体,而highlight部分则是标注的结果,也就是ent_tuple_list,例子中列表的长度仅为1,实际长度可以为多个。

[{'text': '鞍山银行股份有限公司', 'labelType': 'ORG', 'start': 2, 'end': 12}]

1.1 提取ent_tuple

然后按照之前说的ent_tuple概念,我们可以建立数据模型

# 1 将标签转为ent_tuple_list
from typing import List, Optional
from pydantic import BaseModel,FieldValidationInfo, field_validator
class LabelStudioTag(BaseModel):
    text: str
    labelType: str
    start: int
    end: int

    @property
    def ls_tuple(self):
        return (self.text, self.start, self.end)

    def get_tuple(self):
        return self.ls_tuple

lst = LabelStudioTag(**{'text': '鞍山银行股份有限公司', 'labelType': 'ORG', 'start': 2, 'end': 12})
lst.get_tuple()
('鞍山银行股份有限公司', 2, 12)

这样就得到了一个ent_tuple。

针对实际给到的结果是列表形态,可以有

class LabelStudioTagList(BaseModel):
    tag_list : List[LabelStudioTag]

def convert_final_res(some_list):
    tem_lstl =  LabelStudioTagList(tag_list = some_list)
    ent_tuple_list = [x.get_tuple() for x in tem_lstl.tag_list]
    return ent_tuple_list

对整个df执行变换

# 【转换】
tem_df['ent_list'] = tem_df['final_res'].apply(convert_final_res)
tem_df['ent_tuple_list'] = tem_df['highlight'].apply(convert_final_res)

1.2 创建BIO标签

已知原文,标注实体(及其位置), 就可以创建BIO标签(与原文等长)

def make_BIO_by_len(some_len):
    default_str = 'I' * some_len
    str_list = list(default_str)
    str_list[0] ='B'
    return str_list
def gen_BIO_list2(some_dict):
    the_content = some_dict['clean_data']
    ent_list =  some_dict['ent_tuple_list']

    content_list = list(the_content)
    tag_list = list('O'* len(content_list))
    
    for ent_info in ent_list:
        start = ent_info[1]
        end = ent_info[2]
        label_len = end-start
        tem_bio_list = make_BIO_by_len(label_len)
        tag_list[start:end] = tem_bio_list

    res_dict = {}
    res_dict['x'] = ''.join(content_list)
    res_dict['y'] = ''.join(tag_list)
    return res_dict

转换时,要求给到clean_data 和 ent_tuple_list。

_s = cols2s(title_df, cols=['content', 'ent_tuple_list'], cols_key_mapping=['clean_data', 'ent_tuple_list'])
_s1 = _s.apply(gen_BIO_list2)
_tem_df = pd.DataFrame(_s1.to_list())

然后数据就转为了BIO,也就是模型的y
在这里插入图片描述

BIO的"反函数"

写到这里,既然我们将标注数据标准化为了ent_tuple_list(与标注关联,人可识别), 进而将数据转为了BIO格式(模型可使用),那么就一定会有一个反过来的过程。即模型给出了BIO,然后再转为人可识别的ent_tuple_list。

import re
# 提取实体的位置列表[(start,end)]
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

extract_bio_positions('OOOOOOOOOOOOOOBIIIIIIIIIIIIOOOOOOOOO')
[(14, 27)]

def bio2ent_tuple_list(some_dict):
    text = some_dict['text']
    bio = some_dict['bio']

    res_list = []
    bio_pos_list = extract_bio_positions(bio)
    for bio_pos_tuple in bio_pos_list:
        bio_start, bio_end = bio_pos_tuple
        the_ent = text[bio_start:bio_end]

        res_list.append( (the_ent,bio_start,bio_end) )
    return res_list
# 反函数
_s00 = cols2s(_tem_df, cols = ['x','y'] , cols_key_mapping= ['text', 'bio'])
_s01 = _s00.apply(bio2ent_tuple_list)
_s01.head()

假设输入x,模型给到了y,那么就可以这样重新变换为 ent_tuple_list。

1.2 长句切短句

原始的文本可能是一篇文章,长度很长。这样的数据,模型是没法训练的。
考虑模型核心运算是矩阵计算,所以输入数据要想办法塑造为豆腐块的形状。

很显然,我们正常的表达不会有超长的句子,在需要分割和停顿的地方我们会有分隔符,例如句号和逗号。很显然,我们要识别的主体里不会包含这些分隔符,所以可以将一个大任务(一篇文章)分解为若干的小任务(由分隔符分开的短句)。

# 段落分割句子
import re

strong_punctuation =  r'([。?!?!\n])'
weak_punctuation  = r'([,,。!??、.;;ˎ̥”\ue000《》=><{}::\n\r\t\s])'

# 这个是针对x的
def split_sentences_with_punctuation_01(text, punctuation = r'([。?!?!\n])'):
    # 定义句子分隔符
    
    # 根据句子分隔符进行分割,并保留分隔符
    parts = re.split(punctuation, text)
    # 将分隔符与句子重新组合
    sentences = []
    for i in range(0, len(parts), 2):
        sentence = parts[i].strip()
        if i + 1 < len(parts):
            sentence += parts[i + 1]
        sentences.append(sentence)
    return [x for x in sentences if len(x)]

split_sentences_with_punctuation_01('蔚蓝的天空下稻浪翻滚,微风拂过,空气中弥漫着沁人心脾的稻香。', weak_punctuation )
['蔚蓝的天空下稻浪翻滚,', '微风拂过,', '空气中弥漫着沁人心脾的稻香。']

因此一篇文章可以按此重塑为

def make_sentences(some_dict = None):
    doc_id = some_dict['doc_id']
    sentences = some_dict['sentences']

    res_list = []
    for i, v in enumerate(sentences):
        tem_dict = {}
        tem_dict['doc_id'] = doc_id
        tem_dict['s_ord'] = i+1
        tem_dict['sentence'] = v
        res_list.append(tem_dict)
    return res_list

some_dict = {'doc_id':'some_id',
             'sentences':['蔚蓝的天空下稻浪翻滚,', '微风拂过,', '空气中弥漫着沁人心脾的稻香。']}
make_sentences(some_dict)

[{'doc_id': 'some_id', 's_ord': 1, 'sentence': '蔚蓝的天空下稻浪翻滚,'},
 {'doc_id': 'some_id', 's_ord': 2, 'sentence': '微风拂过,'},
 {'doc_id': 'some_id', 's_ord': 3, 'sentence': '空气中弥漫着沁人心脾的稻香。'}]

清洗数据

虽然在这里清洗稍微晚了点,但有时候也没办法,因为前序标注以及更前序的清洗可能不在我们控制范围内。我们需要清洗以保证模型之后拿到的输入都是干净的半角字符。

import re

def extract_utf8_chars(input_string = None):
    # 定义一个正则表达式,用于匹配所有的UTF-8字符
    utf8_pattern = re.compile(r'[\u0000-\U0010FFFF]')
    
    # 使用findall方法找到所有匹配的字符
    utf8_chars = utf8_pattern.findall(input_string)
    
    return ''.join(utf8_chars)

def toDBC(some_char):
    tem_str_ord = ord(some_char)
    res = None 
    if tem_str_ord >65280 and tem_str_ord < 65375:
        res =tem_str_ord - 65248
    # 12288全角空格,160 &nbsp空格
    if tem_str_ord in [12288,160]:
        res = 32
    res_var_ord = res or tem_str_ord
    return chr(res_var_ord)
def tranform_half_widh(some_str = None):
    res_list = []
    return ''.join([toDBC(x) for x in some_str])

上面已经将训练数据转为了x,y这样的形态,以下进行半角转换,并检查转换后长度是否一致。

_tem_df['x1'] = _tem_df['x'].apply(tranform_half_widh)
_is_ok = _tem_df['x1'].apply(len) ==  _tem_df['x'].apply(len)
_is_ok.sum()

然后将长度一致的数据提取出来,进行下一步

clean_bio_df = _tem_df[_is_ok]

下面就将“干净”的数据x和y进行对应的短句拆分

 def make_sentences_with_bio(some_dict = None, puncs = None):
    doc_id = some_dict['doc_id']
    text = some_dict['text']
    bio = some_dict['bio']

    sentences = split_sentences_with_punctuation_01(text,puncs)

    res_list = []
    start_len = 0
    for i, v in enumerate(sentences):
        tem_dict = {}
        tem_dict['doc_id'] = doc_id
        tem_dict['s_ord'] = i+1
        tem_dict['sentence'] = v
        sentence_len = len(v)
        tem_dict['bio'] = bio[start_len:start_len + sentence_len]
        start_len += sentence_len
        res_list.append(tem_dict)
    return res_list

将数据进行转换

# 将doc_id挂回来
# clean_bio_df['doc_id'] = list(title_df['doc_id'])
clean_bio_df['doc_id'] = list(range(len(clean_bio_df)))
_s20 = cols2s(clean_bio_df, cols=['doc_id' ,'x1', 'y'], cols_key_mapping= ['doc_id', 'text', 'bio'])
all_lod = _s20.apply(lambda x: make_sentences_with_bio(x, puncs = weak_punctuation))
all_lod[0]
[{'doc_id': 0,
  's_ord': 1,
  'sentence': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'(Mock掉了), 
  'bio': 'OOBIIIIIIIIIIIOOOOOOOOOOOOOOOO'}]

将数据进行扁平化,组成df,然后再进行必要的筛选

all_lod1 = flatten_list(all_lod)
output_df = pd.DataFrame(all_lod1)
# 训练时选择包含实体的短句
is_ent_sel = output_df['bio'].apply(lambda x: True if 'B' in x else False)
# 长度小于最大限定长度
is_len_198 = output_df['sentence'].apply(lambda x: True if len(x) <198 else False)
# 最终
is_sel = is_ent_sel & is_len_198
train_df = output_df[is_sel]

最后,可以保存为一个excel,这一步就算结束了。

后面还有两部分,为了避免内容太长混在一起,就单独再开两篇

  • 1 怎么基于拿到的数据进行模型训练
  • 2 怎么利用模型训练,然后再返回对应的结果。
  • 18
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值