工业界信息抽取之负样本构造
声明:欢迎转载,转载请注明出处以及链接,码字不易,欢迎小伙伴们点赞和分享。
一、前言
大家在做算法应该也意识到数据的重要性,数据量和数据标注准确性都会影响你最终的模型效果。一般来说大家可能最关心标注时正样本的质量,忽视负样本的质量。很多时候负样本的涵盖量和标注准确率可以很大程度上避免模型混淆,像很多任务在标注时可能并没有直接给出负样本,例如信息抽取或者命名实体识别任务就是只标注正样本。
信息抽取一般也分两种做法,一种端对端另一种是pipeline方法,端对端的模型不需要人工来构造负样本,但是工业界中用的最多却不是端对端模型,而是pipeline模型。原因显而易见主要是pipeline分段可控易于优化,所以信息抽取任务就被分为命名实体识别和关系分类,负样本构造主要是关系分类任务,下面会详细说说。
二、关系分类到底存在哪些负样本?
pipeline方法关系抽取大家都知道会存在累积误差,除了可以优化NER那块模型的输出之外,在关系分类上最好也能做出此类负样本做兜底工作。
关系分类常见负样本:
- 实体边界错误
- 实体类别错误
- 实体组合错误
- 笛卡尔积产生
其中比较麻烦的是笛卡尔积 产生的负样本,像实体边界问题 会比较容易识别,实体组合错误也可以通过schema 做相应的限制,避免出现实体组合错误 。如果不去构造合适的负样本,整个关系分类会呈现,低精确率 ,高召回率 ,我们去构造负样本的时候一定要考虑到实际生产场景产生的badcase。
笛卡尔积产生的负样本不仅很多,而且极难区分 ,因为他的组合是符合schema的,之所以不符合三元组是因为头尾实体位置问题 或者头尾实体顺序 以及文本中没有描述相应关系,这需要模型学到很深层的语义关系。
笛卡尔积
三、负样本构造方法
在数据样本构建时针对以上情况进行样本构造,以法研杯信息抽取数据集为例:
#分类数据集
import re
from secrets import choice
from tkinter import X
from xml.dom import xmlbuilder
import jsonlines
# from nlpcda import Similarword,Randomword,RandomDeleteChar
from random import choice
import random
import os
from itertools import product
def seed_everything(seed=0):
random.seed(seed)
os.environ['PYTHONHASHSEED'] = str(seed)
SEED = 0
seed_everything(SEED)
text_list = []
with jsonlines.open('./data/fayanbei/step2_train.json','r') as f:
for l in f:
text_list.append(l)
print(len(text_list))
train = jsonlines.open('./data/fayanbei/train_dense.json','w')
for l in text_list:#[301:]
re_result = [([x['e1start'],x['em1Text']],[x['e21start'],x['em2Text']]) for x in l['relationMentions']]
ner_result = [[x['start'],x['text'],x['label']] for x in l['entityMentions']]
ner_result = [x for x in ner_result if x[2] in ['Nh','NDR']]
ner_result = list(product(ner_result, repeat=2))
tmp_result = []
for x in ner_result:
if x[0][2]== 'Nh':
if x[0][1]!=x[1][1]:
tmp_result.append(([x[0][0],x[0][1]],[x[1][0],x[1][1]]))
ner_result = tmp_result
ret = []
for i in ner_result:
if i not in re_result:
ret.append(i)
tmp_list = []
for h in ret:
tmp_list.append({'e1start':h[0][0],'em1Text':h[0][1],'e21start':h[1][0],'em2Text':h[1][1],'label':'NA'})
if len(tmp_list)>5:
tmp_list = random.sample(tmp_list, 3)
l['relationMentions'].extend(tmp_list)
for s in l['relationMentions']:
text = list(l['sentText'])
e1_start_type = [k['label'] for k in l['entityMentions'] if k['start']==s['e1start'] and s['em1Text']==k['text']][0]
e21_start_type = [k['label'] for k in l['entityMentions'] if k['start']==s['e21start'] and s['em2Text']==k['text']][0]
if s['e1start']<s['e21start']:
text.insert(s['e1start'],'<S_'+e1_start_type+'>')
text.insert(s['e1start']+1+len(s['em1Text']),'</S_'+e1_start_type+'>')
text.insert(s['e21start']+2,'<O_'+e21_start_type+'>')
text.insert(s['e21start']+3+len(s['em2Text']),'</O_'+e21_start_type+'>')
else:
text.insert(s['e21start'],'<O_'+e21_start_type+'>')
text.insert(s['e21start']+1+len(s['em2Text']),'</O_'+e21_start_type+'>')
text.insert(s['e1start']+2,'<S_'+e1_start_type+'>')
text.insert(s['e1start']+3+len(s['em1Text']),'</S_'+e1_start_type+'>')
text = ''.join(text)
train.write({'data':text,'label':s['label']})
生成的数据集例子:
{"data": "经审理查明,2015年7月22日1时许,公安民警接到群众吴某某举报称贵阳市云岩区纯味园宿舍有一男子持有大量毒品。公安民警接警后前往举报地点搜查。在搜查过程中,从被告人<S_Nh>焦某某</S_Nh>身上查获毒品一包,经刑事科学技术鉴定检出<O_NDR>海洛因</O_NDR>计重120克。涉案毒品已上交省公安厅禁毒总队。", "label": "posess"}
{"data": "经审理查明,2015年7月22日1时许,公安民警接到群众吴某某举报称<O_Ns>贵阳市云岩区</O_Ns>纯味园宿舍有一男子持有大量毒品。公安民警接警后前往举报地点搜查。在搜查过程中,从被告人焦某某身上查获毒品一包,经刑事科学技术鉴定检出<S_NDR>海洛因</S_NDR>计重120克。涉案毒品已上交省公安厅禁毒总队。", "label": "NA"}
{"data": "经审理查明,<O_NT>2015年7月22日1时</O_NT>许,公安民警接到群众吴某某举报称贵阳市云岩区纯味园宿舍有一男子持有大量毒品。公安民警接警后前往举报地点搜查。在搜查过程中,从被告人<S_Nh>焦某某</S_Nh>身上查获毒品一包,经刑事科学技术鉴定检出海洛因计重120克。涉案毒品已上交省公安厅禁毒总队。", "label": "NA"}
{"data": "经审理查明,2015年7月22日1时许,公安民警接到群众<S_Nh>吴某某</S_Nh>举报称贵阳市云岩区纯味园宿舍有一男子持有大量毒品。公安民警接警后前往举报地点搜查。在搜查过程中,从被告人<O_Nh>焦某某</O_Nh>身上查获毒品一包,经刑事科学技术鉴定检出海洛因计重120克。涉案毒品已上交省公安厅禁毒总队。", "label": "NA"}
{"data": "经审理查明,2015年7月22日1时许,公安民警接到群众<S_Nh>吴某某</S_Nh>举报称贵阳市云岩区纯味园宿舍有一男子持有大量毒品。公安民警接警后前往举报地点搜查。在搜查过程中,从被告人焦某某身上查获毒品一包,经刑事科学技术鉴定检出<O_NDR>海洛因</O_NDR>计重120克。涉案毒品已上交省公安厅禁毒总队。", "label": "NA"}
{"data": "经审理查明,2015年7月22日1时许,公安民警接到群众<O_Nh>吴某某</O_Nh>举报称贵阳市云岩区纯味园宿舍有一男子持有大量毒品。公安民警接警后前往举报地点搜查。在搜查过程中,从被告人<S_Nh>焦某某</S_Nh>身上查获毒品一包,经刑事科学技术鉴定检出海洛因计重120克。涉案毒品已上交省公安厅禁毒总队。", "label": "NA"}
在头尾实体中加入标识符像头实体object中使用<o_实体类别>把实体框起来定位,尾实体subject也是如此。
但值得注意的一点就是笛卡尔积或者其他负样本类型不易过多,如果太多的负样本会影响正样本学习,需要控制采样的比例,一般来说笛卡尔积的负样本会很多,采样的比例一般在**10%~20%**之间。使得整体的召回率下降提高精确率。
四、机器学习在关系分类上探索
在NER之后进行笛卡尔积组合出关系分类的样本数量巨大,对于Bert之类的分类模型计算量很大,造成整体推理性能慢。如果机器老旧cpu性能羸弱的情况不能满足业务需求,这时候可以考虑机器学习模型来做关系分类,大家可能认为机器学习效果会在关系分类任务上效果不能满足业务指标,其实这与数据量 和特征工程有关系。
关系分类基础特征:
- 句子文本
- 句子文本长度
- 头实体名
- 头实体开始位置
- 头实体结束位置
- 头实体类别
- 尾实体名
- 尾实体开始位置
- 尾实体结束位置
- 尾实体类别
- 关系标签
在基础特征上我进行特征工程的扩展,在关系分类任务上头尾实体交互尤为重要,我主要是构建三种类型特征:上述基础特征 、头尾实体交互特征 、embedding特征。
头尾实体交互特征:
- 头尾实体距离(尾实体起始减去头实体起始)
- 头实体在文本中出现的频数
- 尾实体在文本中出现的频数
- 头尾实体编辑距离
- 头尾实体Jaro距离
- 头尾实体Jaro-Winkler距离
- 头尾实体头尾实体长度内积
- 头尾实体莱文斯坦比
- 头尾实体杰卡德相似系数
embedding特征处理相对麻烦点,需要把embedding每一个维度值都转化为特征,这样才能被机器学习算法学习到。
embedding特征:
- tf-idf表征句子文本,然后通过阶段奇异值分解降到30维
- 腾讯词向量对句子文本进行表征,其中选用的腾讯词向量模型是100维
- 腾讯词向量对头实体进行表征,其中选用的腾讯词向量模型是100维
- 腾讯词向量对尾实体进行表征,其中选用的腾讯词向量模型是100维
- 腾讯词向量对头尾实体进行表征进行余弦相似度
- 构造头尾实体交互描述文本embedding
实验结果:
使用基础特征的指标:
基础特征模型输出分类报告图
使用基础特征、头尾实体交互特征、embedding特征的指标:
三种类型特征模型输出分类报告图
从上图可以看出宏平均指标从43%提升到80%,微平均指标从63%提升到95%。这还只是我简单的构建了一些特征工程如果深挖特征效果会更好,推理性能会比Bert模型快上几十倍,能够满足线上的性能同时还可以达到不错的准确率。
五、总结
无论什么任务的负样本构造都存在很多技巧,有时候负样本的作用还会大于正样本。厉害的算法工程师除了对模型了解之外还需要对数据有敏锐的嗅觉,数据好就算是一般的模型效果也会很ok,数据差再厉害的模型也拉闸,善于发现数据中特点,针对数据进行优化才是工作中所需要的技能。