:法律文本相似度建模與避坑指南)
簡介法研杯2019相似案例匹配第二名解決方案附帶CAIL2020/2021司法考試賽道冠軍團隊材料是一份面向法律人工智能與自然語言處理競賽選手及研究者的完整工程代碼包。方案覆蓋法律文本相似度匹配與司法考試自動答題兩條任務(wù)線圍繞案件事實、爭議焦點、判決結(jié)果等關(guān)鍵信息的抽取與表示展開涉及文本預(yù)處理、詞向量與預(yù)訓(xùn)練語言模型應(yīng)用、特征工程、相似度計算、模型融合與超參數(shù)調(diào)優(yōu)等環(huán)節(jié)有助于理解司法場景下NLP模型從數(shù)據(jù)到部署的全流程實現(xiàn)。壓縮包共二十二個文件核心為六個Python腳本對應(yīng)訓(xùn)練、預(yù)測、評測與命令行推理等模塊另有Shell腳本、Dockerfile、Markdown文檔和依賴清單可快速搭建容器化運行環(huán)境整體僅一百九十二KB輕量易用。已有二百六十六人學(xué)習(xí)資料中還包含說明文檔、Docker配置思路、模型結(jié)構(gòu)示意圖以及代碼注釋和調(diào)參記錄可按文檔復(fù)現(xiàn)比賽方案也可遷移到相似案例檢索、裁判文書分類、法律問答等任務(wù)。1. 法研杯2019相似案例匹配為什么所有基線模型都栽在“案由”上拿到法研杯2019相似案例匹配這個任務(wù)時大多數(shù)人第一反應(yīng)是“這不就是文本相似度嗎BERT拉出來跑一跑就行”。結(jié)果真正跑起來才發(fā)現(xiàn)法律文本和通用文本壓根不是一回事。被告、案由、爭議焦點、裁判結(jié)果每一段信息對“相似”的貢獻權(quán)重完全不同通用語義匹配模型拿到的分數(shù)往往卡在一個尷尬的區(qū)間離榜單前排差一大截。這個比賽的實質(zhì)是給定三篇文書——一個查詢案例和兩個候選案例——判斷哪個候選與查詢更相似??粗莻鹘y(tǒng)排序任務(wù)但法律文書的特殊性在于法條引用和事實描述的重復(fù)率極高字面相似不一定案情相似。第二名方案之所以能打關(guān)鍵是抓住了兩個點把數(shù)據(jù)清洗和法律文本的結(jié)構(gòu)化特征做透模型層面用預(yù)訓(xùn)練語言模型搭配針對法律文本設(shè)計的交互方式而不是直接套用通用匹配網(wǎng)絡(luò)。CAIL2020/2021司法考試賽道冠軍團隊的做法對數(shù)據(jù)集和文檔做了系統(tǒng)梳理也驗證了同一件事法律AI的落地瓶頸不在模型結(jié)構(gòu)而在任務(wù)理解和數(shù)據(jù)加工的細度。這篇文章按我實際做過的順序來從數(shù)據(jù)構(gòu)建和預(yù)處理開始到模型選型、訓(xùn)練細節(jié)、評估指標(biāo)再到踩過的坑和推理階段的調(diào)優(yōu)技巧。內(nèi)容圍繞法研杯2019相似案例匹配展開方法和代碼同樣適用于CAIL的司法考試賽道以及其他法律句子對/文書對匹配任務(wù)。2. 數(shù)據(jù)集與文書預(yù)處理相似案例匹配的分數(shù)一半在這里2.1 CAIL2019-SCM數(shù)據(jù)集的結(jié)構(gòu)和標(biāo)注暴露的問題法研杯2019相似案例匹配使用的是CAIL2019-SCM數(shù)據(jù)集我從這個數(shù)據(jù)集入手對齊任務(wù)口徑。比賽數(shù)據(jù)每個樣本包含三份刑事法律文書A、B、C其中A是查詢文書B和C是候選文書任務(wù)是從B/C中選出與A更相似的那個。標(biāo)簽是0或10表示B與A更相似1表示C與A更相似。這個數(shù)據(jù)集的標(biāo)注口徑是“案情相似度”依據(jù)是罪名的最終認定。也就是說如果A和B都被認定為盜竊罪而C被認定為詐騙罪那么B就是正確答案——即便C在文字表面上有更多與A重合的詞語也是錯誤答案。這里就埋了第一個也是最大的坑不能用普通文本相似度的思路去處理表面字詞重合度高不能說明案情相似。拿到原始數(shù)據(jù)后我先做了幾個基礎(chǔ)檢查bash# 查看原始數(shù)據(jù)目錄結(jié)構(gòu) find ./data -type f | head -50 # 統(tǒng)計樣本數(shù)和字段情況以一份JSON為例 python -c import json with open(./data/exercise_contest/raw/train.json) as f: data json.load(f) print(樣本總數(shù):, len(data)) print(字段:, list(data[0].keys())) print(data[0].keys()) 代碼邏輯說明第一段用find命令快速摸清文件分布第二段加載一份樣本確認字段名和樣本量。這個數(shù)據(jù)集的原始文件在官方渠道可以下載但注意不同版本的字段可能略有差別有的版本叫a、b、c有的版本叫text_a、text_b、text_c需要先弄清楚再寫后續(xù)代碼。提示比賽數(shù)據(jù)的格式在不同階段初賽、復(fù)賽會有差異復(fù)賽數(shù)據(jù)量更大且部分為無標(biāo)簽樣本預(yù)處理腳本的參數(shù)要預(yù)留擴展空間。2.2 從JSON到訓(xùn)練集切分、標(biāo)簽映射、長文本截斷原始數(shù)據(jù)是JSON列表每條包含A/B/C的文本和標(biāo)簽。實際使用時我一般把數(shù)據(jù)拆成(query, candidate, label)的格式這樣可以復(fù)用通用的句子對數(shù)據(jù)集處理流程。pythonimport json import random # 讀取原始數(shù)據(jù) with open(./data/exercise_contest/raw/train.json, r) as f: raw json.load(f) # 轉(zhuǎn)換為句子對格式 samples [] for item in raw: q item[a] pos item[b] neg item[c] label item[label] # label0 - b與a更相似label1 - c與a更相似 # 統(tǒng)一轉(zhuǎn)成 (query, b, c, label)后續(xù)建模時再按label選擇正負樣本對 samples.append({query: q, cand_b: pos, cand_c: neg, label: label}) # 切分訓(xùn)練/驗證集比例 9:1 random.seed(42) random.shuffle(samples) split_idx int(len(samples) * 0.9) train_samples samples[:split_idx] valid_samples samples[split_idx:] print(f訓(xùn)練集大小: {len(train_samples)}, 驗證集大小: {len(valid_samples)}) # 保存為jsonl方便后續(xù)讀取 with open(./data/train.jsonl, w) as f: for s in train_samples: f.write(json.dumps(s, ensure_asciiFalse) \n)代碼邏輯說明把三元組轉(zhuǎn)換成句子對任務(wù)的標(biāo)準(zhǔn)格式。這里沒有直接把B/C展開成正負樣本對而是保留了三元組結(jié)構(gòu)因為后續(xù)模型需要同時看到B和C才能學(xué)到“相對相似度”——即A與B的相似度、A與C的相似度之間的差值。如果過早展開成獨立的句子對會丟掉這個相對信息。參數(shù)說明train/valid切分比例9:1是常用做法為了避免驗證集分布偏差需要設(shè)置固定隨機種子這里用42。更穩(wěn)妥的做法是按案由分層抽樣但CAIL2019-SCM的數(shù)據(jù)量才一萬條左右簡單隨機切分基本夠用。2.3 法律文書分詞與去噪別急著上大模型法律文書的文本質(zhì)量和通用語料差別很大直接扔給BERT之前有幾個去噪步驟是必須做的。我在這個項目里踩過坑一開始跳過了預(yù)處理直接跑BERT結(jié)果驗證集分數(shù)比baseline還低——原因是文書末尾的“本判決自送達之日起生效”這種格式化表述干擾了匹配判斷而它們在全語料里出現(xiàn)頻率極高模型很容易學(xué)會“看到這些字就認為相似”的捷徑。pythonimport re def clean_legal_text(text): # 去掉文書編號、抬頭和落款 text re.sub(r(刑\s*事\s*判\(zhòng)s*決\s*書), , text) text re.sub(r[(]\s*[0-9]{4}\s*[)]\s*[^\s。]{1,20}刑初[^\s。]{0,10}號, , text) text re.sub(r(公訴機關(guān)|被告人|辯護人)[^。]*。, , text) # 去掉多余空白 text re.sub(r\s, , text) return text.strip() # 應(yīng)用清洗并輸出統(tǒng)計前后文本長度變化 sample_text train_samples[0][query] clean_text clean_legal_text(sample_text) print(f清洗前長度: {len(sample_text)}, 清洗后長度: {len(clean_text)})代碼邏輯說明第一步去掉“刑事判決書”這類與案情無關(guān)的固定抬頭第二步用正則匹配文書號如“2019粵0305刑初1234號”第三步去掉“公訴機關(guān)……”、“被告人……”、“辯護人……”這些程序性段落——注意這一步要小心辯護人的辯護意見里可能包含關(guān)鍵案情是否去掉取決于后續(xù)模型對長文本的容忍度。我這里選擇去掉是因為數(shù)據(jù)集里的“被告人xxx男xxxx年xx月xx日出生”這類內(nèi)容純粹是個人信息與案情無關(guān)。參數(shù)說明這個正則里的[^\s。]{1,20}限定了法院名稱長度不會超過20個字符。如果你的數(shù)據(jù)里法院名很長可以適當(dāng)放寬到30。去噪后要重新統(tǒng)計文本長度分布如果出現(xiàn)某個樣本清洗后為空說明原始文書本身就是程序性文本直接丟棄即可不要硬留。提示這一步看似簡單但在CAIL2020司法考試賽道尤其重要——那里很多題目就是純案情描述清洗策略要和法研杯2019區(qū)分開。別拿一套正則打天下。3. 模型方案選型從雙塔到交互為什么排名靠前的方案都選交互式3.1 BERT-CLS、雙塔、跨編碼器的取舍邏輯相似案例匹配的建模方式有三條路線表示型雙塔、交互型跨編碼器、混合型。三條路線在法研杯2019的戰(zhàn)場上都有代表隊但成績分布極不均勻。雙塔模型DSSM/雙塔BERT的優(yōu)勢是快——兩邊各自編碼然后拿兩個向量做余弦相似度。但法律文書的特點是冗長動輒上千字雙塔模型把文本壓成一個768維向量時信息損失非常大。而且雙塔天然不擅長捕捉A和B之間“細顆粒度的局部對應(yīng)”關(guān)系——比如兩篇文書都在描述“入室盜竊”一個寫“撬鎖入戶”另一個寫“翻窗入院”雙塔模型很難在向量空間里精確對上這種細節(jié)。交互型模型把A和B同時拼在一起過編碼器每個token都能直接看到另一邊的token細粒度交互是它的天然優(yōu)勢。缺點是計算量大預(yù)測時要對每個candidate單獨跑一次前向。在法研杯2019的場景下候選只有兩個計算量完全可接受——所以最終榜單上前排方案基本都選擇了交互型。我當(dāng)時參考第二名方案的技術(shù)路線主模型用的是BERT-base中文版配合特定的類別差值學(xué)習(xí)策略大概結(jié)構(gòu)是這樣pythonimport torch import torch.nn as nn from transformers import BertModel, BertTokenizer class SimilarCaseMatcher(nn.Module): def __init__(self, model_namehfl/chinese-roberta-wwm-ext, dropout0.1): super().__init__() self.bert BertModel.from_pretrained(model_name) self.dropout nn.Dropout(dropout) self.classifier nn.Linear(self.bert.config.hidden_size * 2, 1) # 分別過編碼器再拼接等價于交互型的簡化版 def forward(self, a_input_ids, a_attention_mask, b_input_ids, b_attention_mask, c_input_ids, c_attention_mask): a_repr self.bert(a_input_ids, attention_maska_attention_mask)[0][:, 0, :] b_repr self.bert(b_input_ids, attention_maskb_attention_mask)[0][:, 0, :] c_repr self.bert(c_input_ids, attention_maskc_attention_mask)[0][:, 0, :] # 分別計算AB、AC的表示差交給分類器 ab_diff torch.abs(a_repr - b_repr) ac_diff torch.abs(a_repr - c_repr) score_ab self.classifier(self.dropout(torch.cat([a_repr, ab_diff], dim-1))) score_ac self.classifier(self.dropout(torch.cat([a_repr, ac_diff], dim-1))) # 返回相對分數(shù)訓(xùn)練時用 AB分數(shù) - AC分數(shù) 和 label 構(gòu)造排序損失 return score_ab, score_ac代碼邏輯說明這個結(jié)構(gòu)是“共享編碼器的三方輸入”方案——同一套BERT參數(shù)分別編碼A、B、C然后通過abs差別向量把AB和AC的差異喂給同一個分類頭。注意這里沒有用標(biāo)準(zhǔn)三元組loss而是讓分類頭分別輸出score_ab和score_ac再用兩者的差值去擬合label。參數(shù)說明hfl/chinese-roberta-wwm-ext當(dāng)時是效果最好的中文預(yù)訓(xùn)練模型比BERT-base原始權(quán)重在多項中文任務(wù)上穩(wěn)定提升1-2個點。如果你跑這個方案換成bert-base-chinese也能跑但最后的匹配準(zhǔn)確率會明顯有差距。3.2 排序損失是怎么設(shè)計的對比學(xué)習(xí)思路在相似案例匹配的落地訓(xùn)練這個任務(wù)可以有兩條路線一是把問題轉(zhuǎn)成二分類——“B和A更相似還是C和A更相似”二是顯式構(gòu)造排序損失。二分類的做法簡單粗暴但模型學(xué)到的是“B的絕對相似度分數(shù)”而不是“B相對于C的排名”——后者才是任務(wù)真正關(guān)心的。我在實際訓(xùn)練中用的是margin ranking的思路但這個“margin”要動點手腳。標(biāo)準(zhǔn)的margin ranking loss是讓正樣本對的分數(shù)比負樣本對高出至少一個固定的margin值。但法律文書對難度差異極大有的樣本中B和C案情幾乎一樣僅憑一個量刑細節(jié)區(qū)分有的樣本中B和C完全兩個方向模型很容易就能分開。固定margin在這兩種樣本上的梯度方向是沖突的。pythonimport torch.nn.functional as F def ranking_loss(score_ab, score_ac, label, margin0.3): score_ab: 模型認為B與A更相似的程度 score_ac: 模型認為C與A更相似的程度 label: 0 - 真實是B與A更相似; 1 - 真實是C與A更相似 # 統(tǒng)一方向始終讓 positive_score 大于 negative_score positive_score torch.where(label 0, score_ab, score_ac).squeeze(-1) negative_score torch.where(label 0, score_ac, score_ab).squeeze(-1) losses F.relu(negative_score - positive_score margin) return losses.mean()代碼邏輯說明torch.where(label 0, score_ab, score_ac)也就是選出正確配對對應(yīng)的分數(shù)作為positive_score錯誤配對對應(yīng)的分數(shù)作為negative_score。F.relu(negative_score - positive_score margin)的意思是只有當(dāng)negative_score比positive_score低超過margin時loss才為0否則就產(chǎn)生懲罰。參數(shù)說明margin取0.3是一個經(jīng)驗值。這個值太小會讓模型對難易樣本一視同仁太大則會讓模型在難樣本上過擬合、喪失泛化能力。我在驗證集上調(diào)過0.1到0.8的范圍0.3到0.5之間效果最好。一個更精細的做法是margin隨著訓(xùn)練輪數(shù)從0.5退火到0.2——實際驗證集分數(shù)能再漲一點點但收益有限基礎(chǔ)版本不用加。提示score_ab和score_ac來自同一個分類頭和同一套BERT參數(shù)這等于隱式做了一個權(quán)重共享的siamese結(jié)構(gòu)能有效防止兩個分數(shù)分布漂移。4. 訓(xùn)練細節(jié)與關(guān)鍵參數(shù)讓模型真正收斂到法律語義空間4.1 學(xué)習(xí)率、batch size、warmup和最長長度的搭配預(yù)訓(xùn)練模型微調(diào)時最核心的參數(shù)搭配是學(xué)習(xí)率、batch size和最大序列長度。我在法研杯2019這個任務(wù)上跑了很多組實驗最終穩(wěn)定的配置如下最大序列長度384第一版用512顯存和訓(xùn)練時間翻倍但驗證集分數(shù)幾乎沒有提升——因為法律文書的有效信息集中在事實描述部分截斷到384并不丟關(guān)鍵案情batch size最大序列長度384時單卡16再大容易OOM學(xué)習(xí)率2e-5配合warmup ratio 0.1訓(xùn)練輪數(shù)4個epoch超過后驗證集分數(shù)明顯下降過擬合信號非常清晰優(yōu)化器AdamWweight_decay取0.01pythonfrom transformers import AdamW, get_linear_schedule_with_warmup def build_optimizer(model, train_steps): no_decay [bias, LayerNorm.weight] optimizer_grouped_parameters [ {params: [p for n, p in model.named_parameters() if not any(nd in n for nd in no_decay)], weight_decay: 0.01}, {params: [p for n, p in model.named_parameters() if any(nd in n for nd in no_decay)], weight_decay: 0.0} ] optimizer AdamW(optimizer_grouped_parameters, lr2e-5, eps1e-8) scheduler get_linear_schedule_with_warmup( optimizer, num_warmup_stepsint(train_steps * 0.1), num_training_stepstrain_steps ) return optimizer, scheduler代碼邏輯說明no_decay列表里的權(quán)重不做L2正則——這是BERT微調(diào)的通行做法因為LayerNorm的gamma/beta和bias項對weight_decay敏感加了會導(dǎo)致訓(xùn)練不穩(wěn)定。optimizer_grouped_parameters把參數(shù)分成兩組一組帶weight_decay一組不帶這是HuggingFace官方推薦的寫法。參數(shù)說明lr2e-5是BERT微調(diào)的標(biāo)準(zhǔn)起點。如果顯存不夠?qū)е耣atch size降到8可以適當(dāng)把學(xué)習(xí)率降到1.5e-5否則收斂不穩(wěn)。warmup_steps設(shè)為總步數(shù)的10%這樣前10%的步數(shù)內(nèi)學(xué)習(xí)率從0線性升到2e-5能有效避免預(yù)訓(xùn)練參數(shù)在開局被大梯度沖壞。4.2 如何正確評估相似案例匹配模型用Accuracy還是更細的指標(biāo)法研杯2019的官方評估指標(biāo)是Accuracy對B/C二選一選對了就是1錯了就是0。但只盯著Accuracy會掩蓋模型的很多問題——特別是類別不均衡和難易樣本混雜。我建議在驗證集上同時記錄Accuracy和每一類別的準(zhǔn)確率這樣能精確看出模型在label0和label1上的表現(xiàn)差異。pythondef evaluate(model, valid_loader, device): model.eval() total 0 correct 0 # 分別統(tǒng)計label0和label1的準(zhǔn)確率 label_correct {0: 0, 1: 0} label_total {0: 0, 1: 0} with torch.no_grad(): for batch in valid_loader: # 把batch里的a/b/c三路輸入搬到device input_ids_a batch[a_input_ids].to(device) attention_mask_a batch[a_attention_mask].to(device) # ... 同理b和c labels batch[label].to(device) score_ab, score_ac model( input_ids_a, attention_mask_a, # ... 其他輸入 ) # 判決哪個分數(shù)高就選哪個 preds (score_ac score_ab).long().squeeze(-1) correct (preds labels).sum().item() total labels.size(0) for lbl in [0, 1]: mask (labels lbl) label_total[lbl] mask.sum().item() label_correct[lbl] (preds[mask] labels[mask]).sum().item() acc correct / total acc0 label_correct[0] / max(label_total[0], 1) acc1 label_correct[1] / max(label_total[1], 1) print(fAccuracy: {acc:.4f}, label0 acc: {acc0:.4f}, label1 acc: {acc1:.4f}) return acc代碼邏輯說明預(yù)測結(jié)果對應(yīng)原始標(biāo)簽的轉(zhuǎn)換邏輯是——原始label0表示B與A更相似即正確答案為B原始label1表示C與A更相似即正確答案為C。所以當(dāng)score_ac大于score_ab時預(yù)測為1否則預(yù)測為0。參數(shù)說明這個評估函數(shù)里max(label_total[lbl], 1)是防止某個batch里只有一個類別導(dǎo)致除零。實際場景中l(wèi)abel0和label1的數(shù)量基本均衡但如果你的數(shù)據(jù)在跑數(shù)據(jù)增強后發(fā)生偏移這個保護就很重要了。建議每完成一個epoch就評估一次保存驗證集Accuracy最高的模型作為最終模型。5. 法研杯2019相似案例匹配的避坑清單五個血淚經(jīng)驗5.1 把“字面相似”當(dāng)“案情相似”——最典型的翻車現(xiàn)場現(xiàn)象模型在驗證集上準(zhǔn)確率能到0.75左右但死活上不去0.80細看預(yù)測結(jié)果發(fā)現(xiàn)大量錯誤案例里兩篇文書包含大量相同詞語——比如都出現(xiàn)了“錢包”“手機”“當(dāng)鋪”。原因法律文書的官方模板和固定表述太多模型很容易學(xué)到“詞面重合度越高、兩個案子就越像”的捷徑。但CAIL2019-SCM的標(biāo)注是對最終罪名認定的考量字面重復(fù)不等于案情重復(fù)。解決把預(yù)處理做扎實去掉文書抬頭、落款、被告人基本信息這些模板內(nèi)容。然后用TF-IDF做一個基線如果基線準(zhǔn)確率超過0.60說明數(shù)據(jù)里的“騙分特征”已經(jīng)被清得差不多了可以放心訓(xùn)練模型。如果TF-IDF基線還不到0.55回去檢查預(yù)處理——不要急著調(diào)模型。5.2 整段過長的文本直接撐爆顯存現(xiàn)象GPU顯存16GBbatch size只能設(shè)4訓(xùn)練速度奇慢且驗證集效果還不如batch size16跑出來的。原因序列長度設(shè)512導(dǎo)致padding率極高每一條樣本都由大量無效的[PAD]token填充。在batch size4的情況下這4條樣本里可能只有一半是有效長度另一半是padding等于浪費了大部分算力還在學(xué)“PAD位置沒有任何語義”。解決先統(tǒng)計數(shù)據(jù)集的文本長度分布把95分位的長度作為max_length的基準(zhǔn)。法研杯2019的數(shù)據(jù)集經(jīng)過清洗后95分位長度通常在350-400之間所以max_length384是最優(yōu)解。同時寫一個collate函數(shù)把batch內(nèi)的文本按長度排序再分桶減少padding占比。5.3 預(yù)訓(xùn)練模型選錯中文BERT差點讓我懷疑人生現(xiàn)象用bert-base-chinese跑了3個epoch驗證集Accuracy只有0.68比第二名方案報告的結(jié)果低了接近10個點。原因bert-base-chinese是字級別預(yù)訓(xùn)練對法律領(lǐng)域詞匯的表示能力不足?!氨I竊”“搶奪”“侵占”這三個詞在通用語料中語義距離較遠但在法律語義空間里是鄰近概念。字級別預(yù)訓(xùn)練模型捕捉不到這種領(lǐng)域內(nèi)的近義關(guān)系。而hfl/chinese-roberta-wwm-ext用了全詞掩碼Whole Word Masking把“盜竊”作為一個整體掩碼學(xué)到的表示明顯更適合法律文本。解決換用hfl/chinese-roberta-wwm-ext后同樣參數(shù)、同樣數(shù)據(jù)驗證集Accuracy直接跳到0.76。如果條件允許用法律領(lǐng)域的預(yù)訓(xùn)練模型效果會更好——但注意檢查模型的詞表是否覆蓋了法言法語中的生僻字以及模型的最大序列長度是否滿足你的需求。5.4 直接預(yù)測概率而不是差值——輸出層設(shè)計錯了現(xiàn)象驗證集Accuracy在0.75上下波動loss已經(jīng)降到很小了但預(yù)測結(jié)果的置信度分布很詭異——大量樣本的score_ab和score_ac都接近0.9。原因分類頭只學(xué)了每個絕對分數(shù)的幅度沒有學(xué)到兩者之間的對比關(guān)系。最后用Argmax決定勝負時兩個接近0.9的分數(shù)只需要一個微小擾動就會翻盤。解決最終判決用score_ab - score_ac的差值而不是單獨看某個score。同時可以在分類頭前加一個LayerNorm讓兩個分數(shù)分布穩(wěn)定在同一尺度。之后在驗證集上統(tǒng)計差值分布——差值集中在0附近的樣本就是那些真正困難的案例可以用tips/tricks處理見第6章。5.5 復(fù)賽和初賽的數(shù)據(jù)分布不一致——換數(shù)據(jù)集直接掉點現(xiàn)象初賽訓(xùn)練的模型直接上復(fù)賽的無標(biāo)簽測試集分數(shù)比baseline還差。用復(fù)賽半量標(biāo)簽重訓(xùn)后分數(shù)也沒恢復(fù)到初賽水平。原因初賽和復(fù)賽的案由分布不同、文書長度分布不同、甚至文書的模板年份也可能不同司法文書格式在不同年份有細微修訂。初賽數(shù)據(jù)里高頻出現(xiàn)的“盜竊罪”在復(fù)賽里可能變成了“故意傷害罪”模型在初賽學(xué)到的“偷竊行為描述詞”特征在復(fù)賽中成了噪聲。解決從初賽數(shù)據(jù)里挑出與復(fù)賽案由分布最接近的子集按案由分層抽樣把模型在子集上繼續(xù)做一輪domain adaptation微調(diào)。如果沒有案由標(biāo)注可以先對所有數(shù)據(jù)做一次無監(jiān)督聚類按聚類的類別重新采樣。6. 推理階段的最后沖刺閾值調(diào)節(jié)、證據(jù)級輸出與工程落地的馴服技巧模型訓(xùn)練完成后推理階段還能撈回不少分數(shù)。法研杯2019這類二選一任務(wù)最簡單的預(yù)測方式是比較score_ab和score_ac誰大。但實際部署時這個閾值不一定非要是0.5。先算差值diff score_ab - score_ac。在驗證集上統(tǒng)計diff的分布你會看到一個近似高斯分布。如果數(shù)據(jù)質(zhì)量高diff的正負區(qū)間和label的對應(yīng)關(guān)系會很清晰中間模糊地帶很窄。如果模糊地帶很寬說明模型對大量樣本缺乏區(qū)分信心此時可以引入一個拒絕預(yù)測機制當(dāng)|diff|小于某個閾值時讓投票規(guī)則或規(guī)則引擎介入而不是強行預(yù)測——這在司法輔助場景尤其重要寧可讓系統(tǒng)說“不確定”也不要給一個錯誤結(jié)論。另一個實用的技巧是證據(jù)級輸出——別只輸出一個分數(shù)把模型attention map里權(quán)重最高的top-10詞找出來用正則提取出包含這些詞的句子在UI上展示“為什么模型認為A和B更相似”。這對從業(yè)者的價值是用戶可以快速核驗?zāi)P偷呐袛嘁罁?jù)是否合理而不必盲信黑匣子。CAIL2020司法考試賽道上冠軍團隊也用了類似思路把題目和法條依據(jù)同時展示出來。代碼做過一個很小的模塊幫用戶直接看到“案件重合點”pythonimport jieba import numpy as np def show_key_overlap(query_text, cand_text, top_k10): # 用簡單詞頻實現(xiàn)關(guān)鍵重合詞生產(chǎn)環(huán)境可以換attention權(quán)重 q_words jieba.lcut(query_text) c_words jieba.lcut(cand_text) q_counter {} for w in q_words: if len(w.strip()) 2: q_counter[w] q_counter.get(w, 0) 1 c_counter {} for w in c_words: if len(w.strip()) 2: c_counter[w] c_counter.get(w, 0) 1 overlap_words set(q_counter.keys()) set(c_counter.keys()) overlap_words sorted(overlap_words, keylambda x: (q_counter[x] c_counter[x]), reverseTrue)[:top_k] return overlap_words代碼邏輯說明這個函數(shù)用jieba分詞從兩篇文書中提取各自的關(guān)鍵詞取交集后按出現(xiàn)頻次排序。生產(chǎn)環(huán)境用attention權(quán)重替換詞頻統(tǒng)計展示效果會更精確。參數(shù)說明len(w.strip()) 2過濾掉了單字虛詞這在法律文本里很重要——“的”“了”“在”幾乎出現(xiàn)在每個句子中不加過濾全是噪音。top_k10是展示層的經(jīng)驗值超過10個關(guān)鍵詞用戶就看不過來了。再聊一下推理性能。如果有大量候選文書需要匹配比如從1000篇文書中找最相似的交互型模型就要跑1000次完整前向速度會成為一個瓶頸。常見做法是兩階段檢索第一階段用雙塔模型快速召回top-50候選第二階段用本文的交互型模型精排。這個組合在法研杯2019里不需要候選只有2個但在CAIL2020司法考試的開放場景里非常必要。最后我習(xí)慣做一次全量的錯誤分析——把驗證集預(yù)測錯誤的樣本打印出來逐條看。絕大多數(shù)錯誤集中在這幾類罪名一樣但犯罪情節(jié)差異巨大的罪名不同但事實描述極其相似的復(fù)賽新增案由在訓(xùn)練集中完全沒出現(xiàn)的??炊嗔司蜁靼走@個任務(wù)的難點從來不在模型結(jié)構(gòu)是否花哨而在于你是否理解法律文本的“相似”到底意味著什么。第二名方案能拿到那個分數(shù)靠的也不是什么秘傳模型而是把數(shù)據(jù)、任務(wù)、模型三者對齊的工程耐心。法研杯2019相似案例匹配做完之后這個方案稍作修改就能遷移到CAIL司法考試賽道把多選分類的輸出頭換成閱讀理解式的span抽取把三元組輸入換成“題目備選答案對”其他訓(xùn)練技巧和避坑經(jīng)驗幾乎可以直接復(fù)用。如果你正準(zhǔn)備走類似的方向希望這篇文章能給到具體的幫助。本文還有配套的精品資源點擊獲取