使用UER进行开庭公告命名实体识别
项目概述
上篇文章已经讲述了,如何使用UER进行NER任务微调训练,接下来咱就开始实战了,我这边要做的一个任务是司法文书的信息抽取,简单描述就是抽取出案号、案由、当事人、法院、法庭等信息,不过整体的任务还是比较复杂的,并且司法文书种类也比较多,有裁判文书这种长文本,也有开庭公告、法院公告这种短文本,我这边打算先从开庭公告的一些短文本作为切入点,先做一些尝试,看看效果。
我这边的一个实验计划暂定为:
- 先使用一定量的数据,直接进行NER微调训练,记录训练效果,如precision、recall和f1的值;
- 在语料集上先进行词向量微调(也就是bert finetune),并生成自己的词表;
- 基于步骤2得到的模型再次进行NER微调训练,对比与步骤1的评测值是否有提升;
- 如果方案可行,会再扩充数据集再次重复步骤2、3。
数据预处理
现在手头上有这么一批样例数据,现在需要做的第一项工作是进行数据集格式转换和预处理操作,整个项目的思路是通过开发结构化的程序自动的去生成一定的标注数据,主要是提高标注效率,不过还需要对程序的标注结果进行人工校验,当然还存在很多无法通过程序生成的数据(会提取出错),也是需要人工去标注一部分。1
{"case_no":"(2016)渝01民终3652号","case_reason":"买卖合同纠纷","tribunal":"第十二审判庭","related_companies":"[{\"item\":\"63bd7827-e519-44bf-b050-3a56977c76df\",\"pure_role\":\"原告\",\"role\":\"原告\",\"name\":\"甘肃金创绿丰环境技术有限公司\",\"type\":\"E\"},{\"item\":\"\",\"pure_role\":\"被告\",\"role\":\"被告\",\"name\":\"甘肃省危险废物处置中心\",\"type\":\"E\"},{\"item\":\"991889cb-f858-45ca-9ec1-03cdb1730723\",\"pure_role\":\"被告\",\"role\":\"被告\",\"name\":\"重庆智得热工工业有限公司\",\"type\":\"E\"}]","hearing_date":"2016-06-23 00:00:00.000","content":"重庆市第一中级人民法院定于二零一六年六月二十三日15:30到17:30在第十二审判庭开庭审理(2016)渝01民终3652号原告甘肃金创绿丰环境技术有限公司诉被告甘肃省危险废物处置中心,重庆智得热工工业有限公司买卖合同纠纷一案"}
处理过程中需要注意的点:
- 这里的hearing_date是标准化后的时间,所以这里是没办法直接使用的,因为NER抽取是从原文抽取,所以这里需要重新提取一下时间;
- 法院名称信息缺失,所以需要在这里提取一下,提取不到的也影响不大;
- 案号的中英文括号要统一;
- 因为tsv格式中,数据和标签之间使用”\t”分割,所以要求文本中不能存在”\t”;
- 因为tsv格式中,数据之间、标签之间使用空格分割,所以要求文本中不能存在空格。
预处理代码:(只是一个初步的方法,后续还会不断迭代)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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def regular_brackets(content: str):
# 将content中各种形式的括号转变成统一的中文括号
# 去除\t和空格
return (content.replace('(', '(').replace(')', ')').replace('【', '(').replace('】', ')').replace('<', '(')
.replace('>', ')').replace('〔', '(').replace('〕', ')').replace('\t', '').replace(' ', '').strip())
def find_first(pattern, target_text, default_value=''):
"""
从目标文本根据正则进行匹配,返回第一个匹配的结果,未匹配到返回默认值
:param pattern:
:param target_text:
:param default_value:
:return:
"""
res = re.search(pattern, target_text)
if not res:
return default_value
return res.group(1)
def find_date(content):
pattern = [
r"(.{4}年.{1,2}月.{1,3}[日号]\d{1,2}[::]\d{1,2})"
]
for regex in pattern:
date = find_first(regex, content)
if date:
return date
return ''
def get_english_role(role):
role_dict = {
"原告": "plaintiff", "被告": "defendant", "当事人": "litigant", "原审第三人": "origin_third",
"第三人": "third", "上诉人": "appellant", "被上诉人": "appellee", "被告人": "accused",
"申请人": "appellant_c", "被申请人": "appellee_c", "被告单位": "defendant_c", "罪犯": "criminal",
"原审被告": "origin_defendant", "原审上诉人": "origin_appellant",
# "原审被上诉人": "origin_appellee", "原审原告": "origin_plaintiff"
}
assert role in role_dict.keys(), f"该角色不在映射列表中:{role}"
return role_dict.get(role)
# 将原始的ktgg数据转为另一种json数据
def convert_origin(origin_data_path, output_data_path):
fr = open(origin_data_path, 'r', encoding='utf-8')
fw = open(output_data_path, 'w', encoding='utf-8')
data_list = fr.readlines()
role_set = set() # 收集一下总共有多少种角色
for each in data_list:
each = json.loads(each)
case_reason = regular_brackets(each.get('case_reason', ''))
case_no = regular_brackets(each.get('case_no', ''))
tribunal = regular_brackets(each.get('tribunal', ''))
related_companies = json.loads(each.get('related_companies', []))
content = regular_brackets(each.get('content', ''))
if not content:
continue
label_data = {
"text": content,
"label": {}
}
if case_reason:
index = content.find(case_reason)
if index != -1:
reason = {case_reason: [[index, index+len(case_reason)-1]]}
label_data["label"]["reason"] = reason
if case_no:
index = content.find(case_no)
if index != -1:
number = {case_no: [[index, index+len(case_no)-1]]}
label_data["label"]["number"] = number
if tribunal:
index = content.find(tribunal)
if index != -1:
tribunal = {tribunal: [[index, index+len(tribunal)-1]]}
label_data["label"]["tribunal"] = tribunal
court = find_first(r'([^。]{2,10}?人民法院)', content)
if court:
court = {court: [[content.find(court), content.find(court)+len(court)-1]]}
label_data["label"]["court"] = court
else:
court = find_first(r'。(.{2,10}?(?:人民|运输)法院)', content)
if court:
court = {court: [[content.find(court), content.find(court) + len(court) - 1]]}
label_data["label"]["court"] = court
time = find_date(content)
if time:
time = {time: [[content.find(time), content.find(time)+len(time)-1]]}
label_data["label"]["time"] = time
else:
print(content)
role_label = {}
for litigant in related_companies:
role = regular_brackets(litigant.get('role'))
role_set.add(role)
role = get_english_role(role)
name = regular_brackets(litigant.get('name'))
index = content.find(name)
if index != -1:
if role in role_label.keys():
role_label[role].update({name: [[index, index+len(name)-1]]})
else:
role_label[role] = {name: [[index, index+len(name)-1]]}
label_data["label"].update(role_label)
# print(label_data)
fw.write(json.dumps(label_data, ensure_ascii=False))
fw.write('\n')
print(role_set)
fr.close()
fw.close()
这里数据集转换完成后的数据集格式为:1
{"text": "重庆市第一中级人民法院定于二零一六年六月二十三日15:30到17:30在第十二审判庭开庭审理(2016)渝01民终3652号原告甘肃金创绿丰环境技术有限公司诉被告甘肃省危险废物处置中心,重庆智得热工工业有限公司买卖合同纠纷一案", "label": {"reason": {"买卖合同纠纷": [[105, 110]]}, "number": {"(2016)渝01民终3652号": [[46, 61]]}, "tribunal": {"第十二审判庭": [[36, 41]]}, "court": {"重庆市第一中级人民法院": [[0, 10]]}, "time": {"二零一六年六月二十三日15:30": [[13, 28]]}, "plaintiff": {"甘肃金创绿丰环境技术有限公司": [[64, 77]]}, "defendant": {"甘肃省危险废物处置中心": [[81, 91]], "重庆智得热工工业有限公司": [[93, 104]]}}}
格式与上篇文章提到的CLUENER的数据集格式是一样的,之后再通过已经开发的转换脚本,转为UER的所需的输入数据格式即可。
划分数据集:这里采用一种完全随机的方式1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23def partition():
fw_train = open(r'../../datasets/ktgg/train.json', 'w', encoding='utf-8')
fw_test = open(r'../../datasets/ktgg/test.json', 'w', encoding='utf-8')
fw_dev = open(r'../../datasets/ktgg/dev.json', 'w', encoding='utf-8')
fr_chongqing = open(r'../../datasets/ktgg/train_chongqing.json', 'r', encoding='utf-8')
fr_beijing = open(r'../../datasets/ktgg/train_beijing.json', 'r', encoding='utf-8')
for fr in [fr_chongqing, fr_beijing]:
data_list = fr.readlines()
length = len(data_list)
test_list = random.sample(range(length), round(length*0.1))
dev_list = random.sample(range(length), round(length*0.1))
for i, each in enumerate(data_list):
if i in test_list:
fw_test.write(each)
elif i in dev_list:
fw_dev.write(each)
else:
fw_train.write(each)
fw_train.close()
fw_test.close()
fw_dev.close()
fr_beijing.close()
fr_chongqing.close()
实体种类:
court(法院名称)、reason(案由)、number(案号)、time(开庭时间)、tribunal(开庭地点)、plaintiff(原告)、defendant(被告)、litigant(当事人)、origin_third(原审第三人)、third(第三人)、appellant(上诉人)、appellee(被上诉人)、accused(被告人)、appellant_c(申请人)、appellee_c(被申请人)、defendant_c(被告单位)、criminal(罪犯)、origin_defendant(原审被告)、origin_appellant(原审上诉人)
不确定label2id.json中的标签顺序是否有影响?
label2id.json的内容1
{"O": 0, "B-accused": 1, "I-accused": 2, "B-appellant": 3, "I-appellant": 4, "B-appellant_c": 5, "I-appellant_c": 6, "B-appellee": 7, "I-appellee": 8, "B-appellee_c": 9, "I-appellee_c": 10, "B-court": 11, "I-court": 12, "B-criminal": 13, "I-criminal": 14, "B-defendant": 15, "I-defendant": 16, "B-defendant_c": 17, "I-defendant_c": 18, "B-litigant": 19, "I-litigant": 20, "B-number": 21, "I-number": 22, "B-origin_appellant": 23, "I-origin_appellant": 24, "B-origin_defendant": 25, "I-origin_defendant": 26, "B-origin_third": 27, "I-origin_third": 28, "B-plaintiff": 29, "I-plaintiff": 30, "B-reason": 31, "I-reason": 32, "B-third": 33, "I-third": 34, "B-time": 35, "I-time": 36, "B-tribunal": 37, "I-tribunal": 38}
目前在数据这块还存在一些问题:
- 存在一些结构化提取存在问题的数据,后期需要人工校正;
如案由提取出错,当事人角色提取错误
当前已经见过的案例:
申请宣告
尚海与中国社会科学院民族学与人类学研究所人事争议 - 数据集划分或许有更科学合理的划分方法;
- 还不能覆盖所有情况的数据,比如当事人角色除此之外还有更多。
不过当前属于试错期,先使用一批数据试试效果如何,如果效果好,再去提高数据集的质量。
finetune与测试
在训练开始前还有个重要的超参数需要调整,在UER中就是seq_length,其默认值为128,虽然BERT的word embedding的max_seq_length是512,但是针对一些短文本的任务,句子长度并没有达到512,所以在构建词向量时,可以根据句子长度的范围来设置seq_length,这样可以减少一定的计算量,我这里的数据集存在大量长度超过128的句子,如果使用seq_length=128进行训练,超过长度的部分是无法进行预测的,这一点实际上也是我训练过一次之后在做模型测试的时候发现的。所以这里我先看一下我的数据集句子长度分布,从而寻找一个合适的seq_length值。
图中可以看到绝大部分的数据长度都分布在<=250的区间,图片看不出来实际上>250的区间上也存在少量数据,约几十条。所以这里,为了尽可能覆盖数据集并且保持性能在一个不错的水平,可以将seq_length设置为256。
训练集总共169566条数据,验证集总共18850条数据,一开始我在自己的电脑上训练(无GPU)且CPU也很垃圾,训练三轮花了两天半,后来换到一台有两张NVIDIA GeForce RTX 2080 Ti
显卡的机器训练,同时用两张卡训练,训练一轮约需要1个小时,速度快的飞起!
1 | [2022-07-14 11:38:52,151 INFO] Report precision, recall, and f1: |
根据precision、recall和f1的值,我们看到效果好像还是相当不错的,损失值也围绕在0.000~0.002区间(有的批次是全部正确的)波动,推测那些预测不准确的极有可能是因为长度超过256的,下面我要在测试集上去实际测试下提取的效果。
分为两个步骤:
- 将测试集转换为tsv格式,使用模型预测得到prediction.tsv;
- 处理prediction.tsv,跟测试集中的标签进行对比,计算准确率。
测试集转换为tsv格式,直接使用上篇文章的方法即可1
convert_no_label_data("test.json", "test_nolabel.tsv")
得到的prediction.tsv的数据形式为:1
B-court I-court I-court I-court I-court I-court I-court I-court I-court I-court O O B-time I-time I-time I-time I-time I-time I-time I-time I-time I-time I-time I-time I-time I-time I-time I-time O O O O O O O B-tribunal I-tribunal I-tribunal I-tribunal I-tribunal O O O O B-number I-number I-number I-number I-number I-number I-number I-number I-number I-number I-number I-number I-number I-number I-number I-number I-number I-number O O B-plaintiff I-plaintiff I-plaintiff I-plaintiff I-plaintiff I-plaintiff I-plaintiff I-plaintiff I-plaintiff I-plaintiff I-plaintiff I-plaintiff O O O B-defendant I-defendant I-defendant O B-defendant I-defendant I-defendant B-reason I-reason I-reason I-reason I-reason I-reason I-reason I-reason O O
预测一条数据的时间小于0.01s,不过这个速度也是与你的机器配置有关。
我们需要开发相关的评估方法,评估方法可以分为三种
- 最严格的评估方法,对比实体边界和实体类别是否均提取正确以及实体是否均被提取出来了;
- 准确率,针对提取出来的实体是否边界和类别均提取正确;
- 召回率,针对测试集中所有的实体是否边界和种类均正确提取了出来。
以上的方法均以单条数据为一个样本,并非以单个实体为单个样本。
现在我们的测试集数据格式为1
{"text": "重庆市江北区人民法院定于二零一六年六月二十三日09:20到11:00在第十审判庭开庭审理(2016)渝0105民初7407号原告重庆澄海物业管理有限公司诉被告石以强,陶克群物业服务合同纠纷一案", "label": {"reason": {"物业服务合同纠纷": [[86, 93]]}, "number": {"(2016)渝0105民初7407号": [[44, 61]]}, "tribunal": {"第十审判庭": [[35, 39]]}, "court": {"重庆市江北区人民法院": [[0, 9]]}, "time": {"二零一六年六月二十三日09:20": [[12, 27]]}, "plaintiff": {"重庆澄海物业管理有限公司": [[64, 75]]}, "defendant": {"石以强": [[79, 81]], "陶克群": [[83, 85]]}}}
我们可以直接也把标签转为prediction.tsv的格式,然后直接逐行对比两个文件即可
严格评估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
26def evaluate_strict():
fr_label = open(r'../../datasets/ktgg/test_label.tsv', 'r', encoding='utf-8')
fr_prediction = open(r'../../datasets/ktgg/prediction.tsv', 'r', encoding='utf-8')
labels = fr_label.readlines()
predictions = fr_prediction.readlines()
total = 0
correct = 0
for label, prediction in zip(labels, predictions):
label = label.strip()
prediction = prediction.strip()
if label == "label":
continue
total += 1
label = label.split(' ')
prediction = prediction.split(' ')
if label == prediction:
correct += 1
fr_label.close()
fr_prediction.close()
print(f'correct/total: {correct}/{total}')
print(f'accuracy: {correct/total}')
correct/total: 20801/20935
accuracy: 0.9935992357296394
测试集20935条数据,仅134条预测有问题,按照最严格的评估准确率高于99.3%
评估准确率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
29def evaluate_precision():
fr_label = open(r'../../datasets/ktgg/test_label.tsv', 'r', encoding='utf-8')
fr_prediction = open(r'../../datasets/ktgg/prediction.tsv', 'r', encoding='utf-8')
labels = fr_label.readlines()
predictions = fr_prediction.readlines()
total = 0
error = 0
for label, prediction in zip(labels, predictions):
label = label.strip()
prediction = prediction.strip()
if label == "label":
continue
total += 1
label = label.split(' ')
prediction = prediction.split(' ')
for i, p in enumerate(prediction):
if p == 'O':
continue
if label[i] != p:
error += 1
break
fr_label.close()
fr_prediction.close()
print(f'correct/total: {total-error}/{total}')
print(f'precision: {(total-error)/total}')
correct/total: 20826/20935
precision: 0.9947934081681394
如果仅评估预测结果中得到的实体边界和类别是否正确,仅有109条预测结果有问题。
评估召回率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
33def evaluate_recall():
fr_label = open(r'../../datasets/ktgg/test_label.tsv', 'r', encoding='utf-8')
fr_prediction = open(r'../../datasets/ktgg/prediction.tsv', 'r', encoding='utf-8')
labels = fr_label.readlines()
predictions = fr_prediction.readlines()
total = 0
error = 0
for label, prediction in zip(labels, predictions):
label = label.strip()
prediction = prediction.strip()
if label == "label":
continue
total += 1
label = label.split(' ')
prediction = prediction.split(' ')
for i, p in enumerate(label):
if p == 'O':
continue
if i > 255:
error += 1
break
if prediction[i] != p:
error += 1
break
fr_label.close()
fr_prediction.close()
print(f'correct/total: {total-error}/{total}')
print(f'precision: {(total-error)/total}')
correct/total: 20820/20935
precision: 0.9945068067828995
如果仅评估召回率,有115条未被正确召回,召回率在99.4%。
现在总体看来,在测试集上的效果也是不错的,现在主要就是来看下预测有问题的这些数据的情况。我把出错的情形主要分为以下几类
- 案由提取出错或未提取出来,推测原因为训练数据中未覆盖到该类案由;
- 当事人提取出错,一般最后一个当事人出错的情况较多,与案由未正确提取有较大关联;
- 当事人遗漏,因母公司与分公司名称存在重合导致的遗漏情况较多;
- 时间未提取出来,推测原因为训练数据中未覆盖到该类时间格式;
- 法庭未提取出来,推测原因为训练数据中未覆盖到该类法庭形式;
- 法院提取错误;
- 当事人角色提取有问题,如被告人,推测是缺乏此类训练数据;
- 存在一些特殊数据。
第1、2点样例数据(为了方便看数据,对预测结果做了格式上的转化):
1 |
|
第3点样例数据:
1 | { |
第4点样例数据:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15{
"text": "我院定于二〇一九年九月十八日上午十时三十分,在本院第十八法庭依法公开开庭审理北京朝电长虹电力设备有限公司与北京华瑞欣曼电力设备安装公司买卖合同纠纷一案。\n",
"第十八法庭": "tribunal",
"北京朝电长虹电力设备有限公司": "litigant",
"北京华瑞欣曼电力设备安装公司": "litigant",
"买卖合同纠纷": "reason"
} // 时间未提取出来
{
"text": "我院定于二〇一九年十月二十二日下午十四时整,在本院东1-5法庭依法公开开庭审理北京东方中青投资发展有限公司与北京市工商局行政管理东城分局其他一案。\n",
"东1-5法庭": "tribunal",
"北京东方中青投资发展有限公司": "litigant",
"北京市工商局行政管理东城分局": "litigant",
"其他": "reason"
} // 时间未提取出来
第5点样例数据: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{
"text": "我院定于二零一六年十月十三日上午九时整,在北京市高级人民法院12法庭依法公开开庭审理王朝阳与中地不动产评估有限公司、尤孝明、钱海滨、王朝霞损害公司利益责任纠纷上诉一案。\n",
"时整,在北京市高级人民法院12法庭": "court",
"王朝阳": "plaintiff",
"中地不动产评估有限公司": "defendant",
"尤孝明": "defendant",
"钱海滨": "defendant",
"王朝霞": "defendant",
"损害公司利益责任纠纷上诉": "reason"
} // 12法庭
{
"text": "我院定于二〇一七年二月二十八日上午九时三十五分,在本院三区第4法庭依法公开开庭审理原告温德乙与被告证监会其他一案。\n",
"温德乙": "plaintiff",
"证监会": "defendant",
"其他": "reason"
} // 三区第4法庭
{
"text": "我院原定于二零一六年七月十四日下午十四时整,在北京市东城区人民法院(北区)第11法庭依法公开开庭审理广发银行股份有限公司北京分行与张树长金融借款合同纠纷一案,现取消开庭。\n",
"时整,在北京市东城区人民法院(北区)第11法庭": "court",
"广发银行股份有限公司北京分行": "plaintiff",
"张树长": "defendant",
"金融借款": "reason",
"现": "reason"
}// 时间、法院、法庭、案由
第6点样例数据:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"text": "我院定于二零一六年十月二十日上午十时整,在北京市第一中级人民法院三区第19法庭依法公开开庭审理原告达马股份有限公司与被告中华人民共和国国家工商行政管理总局商标评审委员会、第三人香港星光大道文化艺术传播有限公司其他一案。\n",
"时整,在北京市第一中级人民法院三区第19法庭": "court",
"达马股份有限公司": "plaintiff",
"中华人民共和国国家工商行政管理总局商标评审委员会": "defendant",
"第三人香港星光大道文化艺术传播有限公司": "defendant",
"其他": "reason"
} // 法院提取有问题,当事人提取有问题
{
"text": "我院定于二零一六年八月九日上午九时整,在北京市东城区人民法院(北区)第28法庭依法公开开庭审理满毅与北京首都开发股份有限公司北京圣泽宏房地产经纪有限公司、杨佳茗房屋买卖合同纠纷一案。\n",
"时整,在北京市东城区人民法院(北区)第28法庭": "court",
"满毅": "plaintiff",
"北京首都开发股份有限公司北京圣泽宏房地产经纪有限公司": "defendant",
"房屋买卖合同纠纷": "reason"
}// 法院名称提取有问题
第7点样例数据: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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52{
"text": "(2016)渝0151刑初167号本院定于二〇一六年九月二十七日09:30到10:10在第二审判庭开庭审理(2016)渝0151刑初167号被告人周容川,谭昌健涉嫌犯故意毁坏财物罪一案。铜梁区人民法院承办人:贾兆强\n",
"(2016)渝0151刑初167号": "number",
"二〇一六年九月二十七日09:30": "time",
"第二审判庭": "tribunal",
"人周容川": "defendant",
"谭昌健涉嫌犯故意毁坏财物罪": "defendant",
"铜梁区人民法院": "court"
} // 角色(被告人)、当事人、案由提取都有问题
{
"text": "(2017)渝0153刑初362号本院定于二〇一七年八月二十四日09:00到10:00在第十四法庭开庭审理(2017)渝0153刑初362号被告人徐继英,欧孟华,娄方南,张华涉嫌犯伪造、变造、买卖国家机关公文、证件、印章罪一案。荣昌区人民法院承办人:黄常菊\n",
"(2017)渝0153刑初362号": "number",
"二〇一七年八月二十四日09:00": "time",
"第十四法庭": "tribunal",
"人徐继英": "defendant",
"欧孟华": "defendant",
"娄方南": "defendant",
"张华": "defendant",
"伪造、变造、买卖国家机关公文、证件、印章罪": "reason",
"荣昌区人民法院": "court"
} // 被告人角色问题
{
"text": "(2017)渝0153刑初335号本院定于二〇一七年八月二十八日16:00到17:30在第十四法庭开庭审理(2017)渝0153刑初335号被告人刘从卓,黄体栋,邹家文涉嫌犯盗窃罪一案。荣昌区人民法院承办人:王小辉\n",
"(2017)渝0153刑初335号": "number",
"二〇一七年八月二十八日16:00": "time",
"第十四法庭": "tribunal",
"人": "defendant",
"刘从卓": "defendant",
"黄体栋": "defendant",
"邹家文": "defendant",
"涉嫌犯盗窃罪": "reason",
"荣昌区人民法院": "court"
} // 被告人问题
{
"text": "我院定于二〇一六年十一月九日上午九时三十分,在本院15法庭依法公开开庭审理上诉人中国医药保健品有限公司、上诉人上海咸池实业有限公司与被上诉人北京御盛隆堂科技发展有限公司、被上诉人大庆乳品厂有限责任公司、原审第三人北京中宇金达贸易有限公司委托合同纠纷上诉一案。\n",
"15法庭": "tribunal",
"上诉人中国医药保健品有限公司": "plaintiff",
"上诉人上海咸池实业有限公司": "plaintiff",
"被上": "defendant",
"被上诉人大庆乳品厂有限责任公司": "defendant",
"北京中宇金达贸易有限公司": "origin_third",
"委托合同纠纷上诉": "reason"
} // 当事人和案由提取都有问题
{
"text": "我院定于二〇一八年六月二十日下午十五时三十分,在本院第六法庭依法公开开庭审理英大泰和财产保险股份有限公司北京分公司与苑延忠保险人代位求偿权纠纷一案。\n",
"第六法庭": "tribunal",
"英大泰和财产保险股份有限公司北京分公司": "defendant",
"苑延忠": "defendant",
"保险人代位求偿权纠纷": "reason"
} // 角色(应该是当事人)
第8点特殊数据: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
38
39
40
41
42
43
44
45
46
47
48
49
50
51{
"text": "我院定于二〇一八年三月六日上午九时三十分,在本院(北区)第4法庭依法公开开庭审理王杰组织、领导传销活动罪、张丽艳组织、领导传销活动罪一案。\n",
"第4法庭": "tribunal",
"王杰组织、": "defendant",
"领导传销活动罪": "defendant",
"张丽艳": "defendant"
} // 每个当事人后面跟着一个案由
{
"text": "我院定于二〇一八年三月二十二日下午十六时四十五分,在本院第二十七法庭依法公开开庭审理原告惠普慧与集团有限责任公司与被告国家工商行政管理总局商标评审委员会其他一案。\n",
"第二十七法庭": "tribunal",
"惠普慧": "plaintiff",
"集团有限责任公司": "defendant",
"国家": "defendant",
"工商行政管理": "reason"
} // 每个当事人后面跟着一个案由
{
"text": "我院定于二〇一八年三月二十一日上午九时整,在本院一区第22法庭依法公开开庭审理李灵虚开增值税专用发票、用于骗取出口退税、抵扣税款发票罪、李维虚开增值税专用发票、用于骗取出口退税、抵扣税款发票罪、王茹莹虚开增值税专用发票、用于骗取出口退税、抵扣税款发票罪、周昊虚开增值税专用发票、用于骗取出口退税、抵扣税款发票罪、赖斌全虚开增值税专用发票、用于骗取出口退税、抵扣税款发票罪、王光辉虚开增值税专用发票、用于骗取出口退税、抵扣税款发票罪、北京辉创盈科信息技术有限公司虚开增值税专用发票、用于骗取出口退税、抵扣税款发票罪一案。\n",
"第22法庭": "tribunal",
"李灵": "defendant",
"虚开增值税专用发票、": "reason",
"用于骗取出口退税、": "defendant",
"抵扣税款发票罪": "defendant",
"李维": "defendant",
"王茹莹": "defendant",
"周昊": "defendant",
"赖斌全": "defendant",
"王光辉": "defendant",
"北京辉创盈科信息技术有限公司": "defendant"
} // 这个数据比较特殊
{
"text": "我院定于二〇一八年三月二十三日上午九时整,在本院一区第22法庭依法公开开庭审理李灵虚开增值税专用发票、用于骗取出口退税、抵扣税款发票罪、李维虚开增值税专用发票、用于骗取出口退税、抵扣税款发票罪、王茹莹虚开增值税专用发票、用于骗取出口退税、抵扣税款发票罪、周昊虚开增值税专用发票、用于骗取出口退税、抵扣税款发票罪、赖斌全虚开增值税专用发票、用于骗取出口退税、抵扣税款发票罪、王光辉虚开增值税专用发票、用于骗取出口退税、抵扣税款发票罪、北京辉创盈科信息技术有限公司虚开增值税专用发票、用于骗取出口退税、抵扣税款发票罪一案。\n",
"第22法庭": "tribunal",
"李灵": "defendant",
"虚开增值税专用发票、": "reason",
"用于骗取出口退税、": "defendant",
"抵扣税款发票罪": "defendant",
"李维": "defendant",
"王茹莹": "defendant",
"周昊": "defendant",
"赖斌全": "defendant",
"王光辉": "defendant",
"北京辉创盈科信息技术有限公司": "defendant"
} // 特殊数据
{
"text": "我院定于二〇一八年三月十四日下午十四时整,在本院第十三法庭(北区)依法公开开庭审理朱文翔盗窃罪、吴倩掩饰、隐瞒犯罪所得、犯罪所得收益罪一案。\n",
"第十三法庭": "tribunal",
"朱文翔": "defendant",
"吴倩": "defendant",
"掩饰、隐瞒犯罪所得、犯罪所得收益罪": "reason"
} // 特殊数据
另外还有几个有趣的问题
1、一个句子中有两个案号(案号内容一致,位置不同)1
(2016)渝0104民初2238号本院定于二〇一六年八月二十四日15:30到17:30在第十五审判庭开庭审理(2016)渝0104民初2238号原告黎淑梅诉被告重庆茂园物业管理有限公司,重庆金字塔房地产开发有限公司房屋租赁合同纠纷一案。重庆市大渡口区人民法院承办人:刘冰颖
这段文本中实际是有两个案号的,但是在训练数据集中并没有两处都给标注,预测的时候也确实没有把第二处作为案号提取(这有点不科学),如果尝试删掉最开始的案号再次预测,可以提取到第二处的案号,但是会导致法院提取出错,好像模型只是记住了一些位置信息,有可能是数据集太多,导致了模型过拟合?
2、存在比较多,标注数据错误,但是模型预测正确的情况。
1 | 测试集数据 |
预测的结果1
2
3
4
5
6
7
8
9
10{
"text": "重庆市渝中区人民法院定于二零一六年六月二十三日10:15到10:30在第十三审判庭开庭审理(2016)渝0103民初6196号原告王倩诉被告安诚保险销售有限公司福利待遇纠纷一案\n",
"重庆市渝中区人民法院": "court",
"二零一六年六月二十三日10:15": "time",
"第十三审判庭": "tribunal",
"(2016)渝0103民初6196号": "number",
"王倩": "plaintiff",
"安诚保险销售有限公司": "defendant",
"福利待遇纠纷": "reason"
}
针对以上问题,我们要做的是去查看训练数据集是否存在这样的问题,导致这样了预测结果,一开始我们也说了训练集存在一些问题,后续是需要修正和扩充的,现在第一步只是测试此方案是否可行,经过测试可以训练达到99%的准确率(基于当前数据集),表明方案可行,所以后面的重点工作是尽可能的保证训练数据集的准确,并且尽可能的涵盖各种类型的数据,以提高模型的泛化性。
另外还有这种情况也需要在数据集中解决,存在少量名称重合,导致的标注错误。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// 测试集数据
{
"text": "(2016)渝0114民初6340号本院定于二〇一六年十一月十日14:30到17:30在第四审判庭开庭审理(2016)渝0114民初6340号原告魏民诉被告卢小曼,重庆叶丹商贸有限公司,郑晓永,李清兰,郑晓勇,叶丹民间借贷纠纷一案。黔江区人民法院承办人:孙文敏",
"label": {
"reason": {"民间借贷纠纷": [[107, 112]]},
"number": {"(2016)渝0114民初6340号": [[0, 17]]},
"tribunal": {"第四审判庭": [[44, 48]]},
"court": {"黔江区人民法院": [[116, 122]]},
"time": {"二〇一六年十一月十日14:30": [[22, 36]]},
"plaintiff": {"魏民": [[73, 74]]},
"defendant": {
"卢小曼": [[78, 80]],
"重庆叶丹商贸有限公司": [[82, 91]],
"郑晓永": [[93, 95]],
"李清兰": [[97, 99]],
"郑晓勇": [[101, 103]],
"叶丹": [[84, 85]] // 这里标注错误
}
}
}
// 预测数据:正确
{
"text": "(2016)渝0114民初6340号本院定于二〇一六年十一月十日14:30到17:30在第四审判庭开庭审理(2016)渝0114民初6340号原告魏民诉被告卢小曼,重庆叶丹商贸有限公司,郑晓永,李清兰,郑晓勇,叶丹民间借贷纠纷一案。黔江区人民法院承办人:孙文敏\n",
"(2016)渝0114民初6340号": "number",
"二〇一六年十一月十日14:30": "time",
"第四审判庭": "tribunal",
"魏民": "plaintiff",
"卢小曼": "defendant",
"重庆叶丹商贸有限公司": "defendant",
"郑晓永": "defendant",
"李清兰": "defendant",
"郑晓勇": "defendant",
"叶丹": "defendant",
"民间借贷纠纷": "reason",
"黔江区人民法院": "court"
}
针对上述问题,我排查了问题原因,关于法院提取出错的问题,经过核查实际是数据集中出现了类似的标注错误,所以造成了这样的错误预测结果;时间未提取出来的问题,实际上是训练数据中有类似的时间,却没有标注出来;母公司与分公司的问题,大概率也是存在重复文本时的标注程序有bug。
当前很明确的可以进行以下优化:
- 优化法院标注;
- 优化时间标注;
- 优化一段文本中包含多个案号的标注逻辑;
- 优化存在重复文本时的标注逻辑;
- 增加其他种类案由的训练数据;
- 增加其他当事人角色的训练数据;
- 增加其他法庭形式的训练数据。
如果继续针对真实数据去做标注优化,这些点就是之后的优化方向,但是根据我的个人经验萌生一个想法,自己生成训练数据,根据一定的规则排列时间、角色、当事人名称、案由、案号、法院和法庭,然后加上一些其他的介词、标点符号等,在生成数据的时候直接获取标注数据,可以基本解决上述所有的待优化问题,并且大大提高效率。
另外使用当前的NER方法来做这个任务,我也发现了一些非常重要问题:
- 对于当事人来说,既可以划分为不同的角色,又可以分为个人、公司、组织机构等类型,没办法表示其多重身份,比如“甘肃金创绿丰环境技术有限公司”,既是被告,也是公司,这里只能把其分类到被告;
- 少量句子长度特别长,达到1000~2000字,如果后续扩展到裁判文书,那这种长度的文本更是比比皆是,所以针对长文本如何抽取也是问题所在。