使用UER进行开庭公告命名实体识别-续2
再接着上篇博客,本次依然采用自己生成训练数据的方式来训练,经过上一次的实验和优化,本次生成的训练数据标注错误的问题将会大大减少,因为很多实体是根据已有数据进行收集的,没有耗费大量的人力去逐条核验,难免会存在噪音,这一次修复了上篇中存在的各种噪音问题,并且还做了较多优化,具体效果如何,继续来看看吧。
优化方案
这一次在上一次的训练结果上做了以下优化点:
- 修复了目前见过的在数据集生成方面的噪音;
- 生成的数据种类大幅增加,增加了6种模板;
- 大幅度减少每种模板生成的数量,防止过拟合;
- 通过在生成的文本中增加一定的冗余数据,来让模型学会识别哪些信息并不是我们需要的实体;
- 生成了一定比例当事人之间没有任何分隔符号的数据,以此来进一步提升训练模型的抽取能力;
- 修复了一个“意料之外,又在情理之中”的问题,这个问题下面会说明;
- 修复了有空白字符开头的句子预测实体的位置索引不准确的问题。
意料之外,又在情理之中”的问题
在做以上优化时,发现模型存在一个奇怪的问题,当被告放在前面,原告放在后面时,模型无法正确识别
案例1
2
3
4
5
6
7
8{'label': {'defendant': [{'春和集团有限公司': [55, 62]},
{'江苏太平洋造船集团股份有限公司': [64, 78]},
{'梁光夫': [80, 82]}],
'plaintiff': [{'国家开发银行股份有限公司': [40, 51]}],
'reason': [{'合同纠纷': [83, 86]}],
'time': [{'二〇一七年四月二十七日上午九时三十分': [4, 21]}],
'tribunal': [{'12法庭': [26, 29]}]},
'text': '我院定于二〇一七年四月二十七日上午九时三十分,在本院12法庭依法公开开庭审理被告国家开发银行股份有限公司与原告春和集团有限公司、江苏太平洋造船集团股份有限公司、梁光夫合同纠纷一案。'}
这里的抽取结果,原告与被告身份搞反了,推测是生成的训练数据,原告总是在前面,被告总是在后面,所以模型记住了位置信息,所以在这一版本中优化了这一点,随机改变原告、被告等的生成顺序。
有空白字符开头的句子,预测的实体位置索引不准确
1 | {'label': {'defendant': [{'告春和集团有限公': [55, 62]}, |
这里在开头加了一个空格,导致输出的实体边界全部错误,实际上模型预测的并没有错,只是因为模型在分词时就把句子首尾的空白字符去掉了,而我输出时依然把开头的空白字符算了进去,所以导致实体边界错乱,只要控制输出时也先把句子首尾的空白字符去掉即可。
注意:句子内部出现空白字符不会有任何影响
另外也要注意,训练数据(tsv文件中的text)也不要让首尾出现空白字符,否则的话,在模型读入数据时,会因为文本长度和标签长度对应不上而出错。所以在从我们利用模板生成的数据转换到tsv文件时,要注意这一点。
开始训练
1 | [2022-09-28 07:34:33,854 INFO] Epoch id: 5, Training steps: 100, Avg loss: 0.003 |
本次因为只是优化了数据集,关于一些常规性的预处理工作和前面都是一样的,训练了5轮,最终准确率也达到了99%(感觉验证集不应该有这么高,毕竟数据量大幅度减少,并且增加了难度)。
模型评估
测试集评估
在测试集上的评测指标如下:1
2
3
4
5
6correct/total: 1649/1700
accuracy: 0.97
correct/total: 1655/1700
precision: 0.9735294117647059
correct/total: 1651/1700
precision: 0.9711764705882353
经过对测试数据预测错误的原因分析,分为以下几种:
- 公司实体预测不准确:1
- 英文企业、人名提取不准确:1
- 人名实体预测不准确:1
- 角色预测不准确:23(因为数据集的生成存在问题,确实无法判断角色)
- 名字之间无分隔,导致识别出错:2
- 生成的训练数据有噪音:6
- 开庭地点中含有案由:1
- 法院名称有重合:1
- 法庭地点不准确:3
- 案由提取不准确:7
- 预测标签存在明显错误:3
针对问题1、2、4、5、7、9只能适当增加一些数据集,问题4已解决,问题6发现的噪音问题已经解决,问题8和10可能需要增加一些特定法院名称和案由的训练数据。
利用CRF层学习句子的约束条件
我这里以一条预测数据的标签的一部分来解释问题11的含义1
B-reason I-reason I-origin_appellee I-origin_appellee I-origin_appellee I-origin_appellee
我们都知道BIO标注规则的含义,这里”origin_appellee”直接跟在”I-reason”后面明显是不合理的,经过资料查询发现CRF可以解决这一问题,CRF层可以加入一些约束来保证最终预测结果是有效的。这些约束可以在训练数据时被CRF层自动学习得到。
可能的约束条件有:
- 句子的开头应该是“B-”或“O”,而不是“I-”。
- “B-label1 I-label2 I-label3…”,在该模式中,类别1,2,3应该是同一种实体类别。比如,“B-Person I-Person” 是正确的,而“B-Person I-Organization”则是错误的。
- “O I-label”是错误的,命名实体的开头应该是“B-”而不是“I-”。
另外还需要说明的是,一开始我自己也没想过会出现这种不合理的标签预测结果,我在将预测标签转为格式化数据时,是默认新的实体都是以B开头的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38def pretty_print2(text, labels):
data = {"text": text}
label_dict = {}
text = text.strip()
current_label = ''
start = -1
for i, j in enumerate(labels):
if j == 'O':
if start != -1:
end = i
entity = text[start:end]
temp = {entity: [start, end-1]}
if current_label in label_dict:
label_dict[current_label].append(temp)
else:
label_dict.update({current_label: [temp]})
start = -1
elif "B-" in j:
if start != -1:
end = i
entity = text[start:end]
temp = {entity: [start, end-1]}
if current_label in label_dict:
label_dict[current_label].append(temp)
else:
label_dict.update({current_label: [temp]})
start = i
current_label = j.strip('B-')
# 最末尾如果不是'O',提取最后的实体
if start != -1:
entity = text[start:len(text)]
temp = {entity: [start, len(text)-1]}
if current_label in label_dict:
label_dict[current_label].append(temp)
else:
label_dict.update({current_label: [temp]})
data["label"] = label_dict
pprint(data)
这样处理带来的问题就是,有的数据虽然预测标签有错误,但是转换为格式化数据时却难以发现错误。例如在测试数据就有这样的案例,其中一条数据的案由为:“其他(城建)”,但是预测的标签为:“B-reason I-reason I-origin_appellee I-origin_appellee I-origin_appellee I-origin_appellee”,由于”I-origin_appellee”没有以B开头,所以转换程序认为后续依然为reason实体,所以转换结果看起来完全正确。
真实数据及评估
1 | # 正确预测 |
通过在真实数据集上的测试,其实可以发现模型的准确率极高,虽然在测试集上存在各种问题,但是这些问题在真实数据集上极少出现(例如角色无法判断的问题,是因为生成的测试数据相比真实数据有一定的特殊性)。现在只需要将新增的劳动仲裁的数据再融入到模型的训练中,基本就把整个项目完成了。
把仲裁机构划分一个独立的实体类别是一个非常不错的选择,但是这样又要针对生成数据的模板进行较大的更改,可以将仲裁机构与法院认为是同一实体种类,后续交给业务方自行判断是一个比较简洁的方式,只需要在原法院的实体位置,增加一些仲裁机构样本即可,仲裁案号也是同理。