如何用序列分類方式進行法律要素與當事人關聯性分析

飛槳PaddlePaddle發表於2020-10-28
在智慧司法領域中,針對法律裁判文書的分析和挖掘已經成為計演算法學的研究熱點。目前公開的裁判文書資料大都以長篇文字的形式出現,內容主要包含案號、當事人、案由、審理過程、裁判結果、判決依據等,篇幅較長、表述複雜,無論對於普通民眾或是司法領域從業人員而言,透過閱讀裁判文書來準確、快速地瞭解案件要點資訊,都是一項複雜、耗時的工作。因此,藉助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算力資源,為我們模型的訓練和評估提供了必要的條件。

相關文章