如何用序列分类方式进行法律要素与当事人关联性分析

在智慧司法领域中,针对法律裁判文书的分析和挖掘已经成为计算法学的研究热点。目前公开的裁判文书资料大都以长篇文本的形式出现,内容主要包含案号、当事人、案由、审理过程、裁判结果、判决依据等,篇幅较长、表述复杂,无论对于普通民众或是司法领域从业人员而言,通过阅读裁判文书来准确、快速地了解案件要点信息,都是一项复杂、耗时的工作。因此,借助AI技术快速准确解构裁判文书,结构化展示文书中的关键信息,成为了大数据时代司法领域的迫切需求之一。

2020“睿聚杯”全国高校法律科技创新大赛,是面向全国高校开展的一场高水平的法律科创竞赛。本文介绍了比赛冠军团队采用的技术方案,该方案的优势在于其基于百度飞桨平台实现,使用ERNIE作为预训练模型,并以“序列分类”为主要思路完成比赛项目方案。该方案最终以F1=91.991的成绩取得了第一名,相比Baseline的分数提高了3.267。

赛题分析

在众多裁判文书信息挖掘与分析任务中,“法律要素与当事人关联性分析任务”因其对判决结果影响的重要性和算法设计技术难度,受到了越来越多法律科技研究人员的关注。举例而言,“多人多罪”在司法行业中是一种比较常见的现象,且在司法行业需要对每个人的不同罪名进行判断。本题目需要利用模型和算法对输入的文本、法律要素与当事人进行匹配判断,判断在当前输入文本中,法律要素与当事人之间的对应关系。

本次竞赛的主题是“法律要素与当事人的关联性分析”,核心是根据给定信息,判断要素与当事人是否匹配。

数据样例

首先,对比赛提供的数据进行分析,数据的内容和形式如下:

  • 文 号:(2016)豫1402刑初53号
  • 段落内容:商丘市梁园区人民检察院指控:1、2015年7月17日、18日,被告人刘磊、杜严二人分别在山东省单县中心医院和商丘市工行新苑盗窃现代瑞纳轿车两辆,共价值人民币107594元。其中将一辆轿车低价卖给被告人苗某某,被告人苗某某明知是赃车而予以收购。公诉机关向法庭提供了被告,是被告人供述、被害人陈述、证人证言、鉴定意见、有关书证等证据,认为被告刘磊、杜严的行为触犯了《中华人民共和国刑法》第二百六十四条之规定,构成盗窃罪。系共同犯罪。被害人苗某某的行为触犯了《中华人民共和国刑法》第二百一十二条第一款之规定,构成掩饰、隐瞒犯罪所得罪。请求依法判处。
  • 被告人集合:[“刘磊”,“杜严”,“苗某某”]
  • 句 子:1、2015年7月17日、18日,被告人刘磊、杜严二人分别在山东省单县中心医院和商丘市工行新苑盗窃现代瑞纳轿车两辆,共价值人民币107594元
  • 要素原始值:盗窃现代瑞纳轿车
  • 要素名称:盗窃、抢劫、诈骗、抢夺的机动车
  • 被告人:[“刘磊”]

这里给出了一条数据样例,每条数据中都包括以上字段。其中段落内容直接来自于公开法律文书,被告人集合是所有段落中提到的被告人。句子是段落中的某个片段,包含需要分析要素的原始表达。我们需要根据这些已知信息,预测出与要素名称相对应的被告人。

官方给定的数据集中的文本均来源于公开的法律文书,共包含6958条样本数据,模型最终评价指标是宏平均F1(Macro-averaging F1)。

Baseline(official)

图1. Baseline模型结构

我们对官方提供的Baseline方案进行了分析:官方提供的Baseline方案将这个任务定义为NER,将要素原始值和句子输入到模型中,在句子中标记出与该要素原始值对应的人名,模型结构如图1所示。

在本例中,句子包含多个人名(赵某甲、赵某、龙某),但与给定要素相关的只有赵某甲,因此模型只标出赵某甲。该方案难以应对句子中没有人或者包含多个人名的情况。

任务定义:序列分类

Baseline方案采用的NER形式对于句子中没有人名的情况和包含多个人名的情况效果较差,因此我们结合给定的数据重新构思赛题方案。考虑到数据中已经给定了被告人集合,我们将赛题任务重新定义为序列分类任务,如图2所示。将被告人、要素名称以及句子作为输入,判断输入的被告人是否与给定要素名称相关,若相关则模型预测1,否则预测0。

图2. Sequence Classification模型结构

模型描述


相比于BERT而言,ERNIE对中文实体更加敏感,因此本方案选取ERNIE作为主体。如图3所示,为了使输入更符合ERNIE的预训练方式,本方案将被告人和要素名称作为输入的sentence A,句子作为sentence B。将CLS位置的hidden state外接一层全连接网络,通过sigmoid函数将logit压缩到0到1之间。

为了增强关键部分的信息,我们在被告人和要素原始值两端各添加了四个特殊标记[PER_S]和[PER_E]分别表示句中“被告人(person)”的起始位置start和end,[OVS]和[OVE]分别表示“要素原始值(ovalue)”的起始位置start和end,以期望模型能够学习到这种范式,更多地关注到这两部分信息。

图3. Model Description

Model核心代码:

class ErnieForElementClassification(ErnieModel):     def __init__(self, cfg, name=None):         super(ErnieForElementClassification, self).__init__(cfg, name=name)         initializer = F.initializer.TruncatedNormal(scale=cfg['initializer_range'])         self.classifier = _build_linear(cfg['hidden_size'], cfg['num_labels'], append_name(name, 'cls'), initializer)           prob = cfg.get('classifier_dropout_prob', cfg['hidden_dropout_prob'])         self.dropout = lambda i: L.dropout(i, dropout_prob=prob,   dropout_implementation="upscale_in_train",) if self.training else i     @add_docstring(ErnieModel.forward.__doc__)     def forward(self, *args, **kwargs):         labels = kwargs.pop('labels', None)         pooled, encoded = super(ErnieForElementClassification, self).forward(*args, **kwargs)         hidden = self.dropout(pooled)         logits = self.classifier(hidden)         logits = L.sigmoid(logits)         sqz_logits = L.squeeze(logits, axes=[1])         if labels is not None:             if len(labels.shape) == 1:                 labels = L.reshape(labels, [-1, 1])             part1 = L.elementwise_mul(labels, L.log(logits))             part2 = L.elementwise_mul(1-labels, L.log(1-logits))             loss = - L.elementwise_add(part1, part2)             loss = L.reduce_mean(loss)             return loss, sqz_logits         else:             return sqz_logits

数据去噪

在本地实验阶段,我们将官方提供的6958条原始数据(train.txt)按照以上说明的形式处理后得到31030条新数据,并按照8:2的比例划分训练集和测试集。通过分析官方给定的数据,我们发现给定的训练数据中部分数据存在以下两个问题:

(1) sentence不包含被告人集合中的任意一个名称(sentence中找不到被告人)

(2) sentence不是段落内容的一部分(段落中找不到sentence)

若数据存在问题(1),则只通过给定的sentence无法判断要素名称对应的被告人,需要在段落中定位到sentence并根据其前后的信息进一步判断。若一条数据同时存在问题(1)和问题(2),那么根据该条数据给定的信息将不足以判断要素对应的被告人是哪一个。

本方案将同时满足问题(1)和问题(2)的数据当作噪声数据,在训练过程中将这部分数据剔除。处理后数据集信息如下表:

注释:
  • Original:官方提供的原始数据集train.txt。
  • Preprocessed:将Original数据重新整理,将“被告人集合”拆分成单独的“被告人”。
  • Denoised:去除Preprocessed中,同时满足问题(1)和(2)的样本。
  • Denoised_without_no_person:去除Denoised中,存在问题(1)的样本。
按照模型的输入形式,我们结合官方提供的数据形式,对数据进行批处理,核心代码如下:

def pad_data(file_name, tokenizer, max_len):     """         This function is used as the Dataset Class in PyTorch     """     # configuration:     file_content = json.load(open(file_name, encoding='utf-8'))     data = []     for line in file_content:         paragraph = line['paragraph']         person = line['person']         element = line['element_name']         sentence = line['sentence']         ovalue = line["ovalue"]         label = line['label']         sentence_a = add_dollar2person(person) + element         sentence_b = add_star2sentence(sentence, ovalue)         src_id, sent_id = tokenizer.encode(sentence_a, sentence_b, truncate_to=max_len-3)      # 3 special tokens         # pad src_id and sent_id (with 0 and 1 respectively)         src_id = np.pad(src_id, [0, max_len-len(src_id)], 'constant', constant_values=0)         sent_id = np.pad(sent_id, [0, max_len-len(sent_id)], 'constant', constant_values=1)         data.append((src_id, sent_id, label))     return data def make_batches(data, batch_size, shuffle=True):     """         This function is used as the DataLoader Class in PyTorch     """     if shuffle:         np.random.shuffle(data)     loader = []     for j in range(len(data)//batch_size):         one_batch_data = data[j * batch_size:(j + 1) * batch_size]         src_id, sent_id, label = zip(*one_batch_data)         src_id = np.stack(src_id)         sent_id = np.stack(sent_id)         label = np.stack(label).astype(np.float32)  # change the data type to compute BCELoss conveniently         loader.append((src_id, sent_id, label))     return loader

在数据处理完成之后,我们开始模型的训练,模型训练的核心代码如下:

def train(model, dataset, lr=1e-5, batch_size=1, epochs=10):     max_steps = epochs * (len(dataset) // batch_size)     # max_train_steps = args.epoch * num_train_examples // args.batch_size  // dev_count     optimizer = AdamW(         # learning_rate=LinearDecay(lr, int(0), max_steps),         learning_rate=lr,         parameter_list=model.parameters(),         weight_decay=0)     model.train()     logging.info('start training process!')     for epoch in range(epochs):         # shuffle the dataset every epoch by reloading it         data_loader = make_batches(dataset, batch_size=batch_size, shuffle=True)         running_loss = 0.0         for i, data in enumerate(data_loader):             # prepare inputs for the model             src_ids, sent_ids, labels = data             # convert numpy variables to paddle variables             src_ids = D.to_variable(src_ids)             sent_ids = D.to_variable(sent_ids)             labels = D.to_variable(labels)             # feed into the model             outs = model(src_ids, sent_ids, labels=labels)             loss = outs[0]             loss.backward()             optimizer.minimize(loss)             model.clear_gradients()             running_loss += loss.numpy()[0]             if i % 10 == 9:                 print('epoch: ', epoch + 1, '\tstep: ', i + 1, '\trunning_loss: ', running_loss)                 running_loss = 0.0         state_dict = model.state_dict()         F.save_dygraph(state_dict, './saved/plan3_all/model_'+str(epoch+1)+'epoch')         print('model_'+str(epoch+1)+'epoch saved')     logging.info('all model parameters saved!')

效果对比

最终与baseline相比,我们的方案在F1、Precision和Recall三项指标上都有明显的提升。在所有25支参赛队伍中排名第一,其中F1和Precision值均为所有参赛队伍最好成绩。

方案总结

本方案将比赛任务重新定义为序列分类任务,这一任务形式将判断要素名称与被告人之间关系所需的关键信息直接作为模型的输入,并且在关键信息处添加了特殊符号,有效增强了关键信息,降低了模型判断的难度。在训练数据方面,本方案剔除了部分噪声数据。

实验结果也表明这一操作能够提升模型的预测表现。在测试阶段,本方案对于句子中没有被告人的情况采取了向前扩一句的方式。这一方式能够解决部分问题,但对于前一句仍不包含被告人的情况效果较差。并且在扩句后,输入序列的长度增加,而输入序列的最大长度不能超过512。因此,本方案仍需解决以下两种情况:

(1) 向前扩句后,句子中仍不包含被告人的情况;

(2) 输入序列较长的情况(分词之后达到1000个token以上)

方案改进

针对上一节总结的两个问题,我们有如下的方案,但由于时间原因未能完全实现。以下是我们的思路:

(1) 滑窗策略:若句子中不包含被告人,则使用该句之前的所有信息(或者直接输入段落)。这样输入序列的长度会大幅增加,这时采用多个ERNIE 512窗口,stride=128,对完整序列进行滑窗,不同窗口重叠的地方采用pooling的方式获取最终隐藏状态。这样就打破了ERNIE输入512长度的限制;

(2) 拼接关键向量:在滑窗策略中,输入序列增加之后,相应的冗余信息也会增加。因此我们将进一步对【被告人】和【要素原始值】的信息进行增强。现有的方案是使用[CLS]位置的最终隐藏层向量连接全连接层进行二分类,我们可以将【被告人】和【要素原始值】每个token位置的最终隐层向量进行取平均,然后和[CLS]位置的向量进行拼接,将原先768维的向量扩展到2304维,使用新的向量进行二分类。

本项目基于飞桨深度学习框架完成,作为首次接触Paddle的新手,在使用动态图ERNIE代码过程中领略到了其独特的魅力!这一切都得益于百度为Paddle的使用者开发了详细的使用手册和丰富的学习资料。当然,也要感谢AI Studio提供的GPU算力资源,为我们模型的训练和评估提供了必要的条件。
飞桨PaddlePaddle
飞桨PaddlePaddle

飞桨(PaddlePaddle)是中国首个自主研发、功能完备、开源开放的产业级深度学习平台。

https://www.paddlepaddle.org
专栏二维码
工程大数据技术智能法务
相关数据
深度学习技术

深度学习(deep learning)是机器学习的分支,是一种试图使用包含复杂结构或由多重非线性变换构成的多个处理层对数据进行高层抽象的算法。 深度学习是机器学习中一种基于对数据进行表征学习的算法,至今已有数种深度学习框架,如卷积神经网络和深度置信网络和递归神经网络等已被应用在计算机视觉、语音识别、自然语言处理、音频识别与生物信息学等领域并获取了极好的效果。

百度机构

百度是全球最大的中文搜索引擎,是一家互联网综合信息服务公司,更是全球领先的人工智能平台型公司。2000年1月1日创立于中关村,公司创始人李彦宏拥有“超链分析”技术专利,也使中国成为美国、俄罗斯、和韩国之外,全球仅有的4个拥有搜索引擎核心技术的国家之一。

https://www.baidu.com/
暂无评论
暂无评论~