
简介面向法律智能领域的算法研究与竞赛场景这份压缩包可复现法研杯2019相似案例匹配第二名的解决方案适合自然语言处理工程师、司法人工智能研究人员及竞赛选手参考。包内共22个文件主要由六个脚本、三个容器配置、三个说明文档、启动脚本与依赖清单等构成整体约192KB代码结构紧凑便于快速定位核心逻辑。内容覆盖案例文本预处理、命名实体识别、预训练模型应用、特征向量构建、模型训练与评估以及超参数调优与模型融合等关键环节并附带数据读取、命令行预测入口、模型定义与运行依赖文件可帮助读者理解相似案例匹配从数据到推理的完整实现流程。已有266人学习该资源对于希望从代码层面复盘比赛解法、快速搭建法律文本匹配基线模型的人来说是一份轻量且实用的参考其中的数据加载与评估思路也可迁移到其他法律文本任务中。1. 竞赛标题背后的两个赛道一条技术主线法研杯2019相似案例匹配第二名、CAIL2020/2021司法考试赛道冠军团队这两个名头放在一起很多人会以为是两个独立的项目压缩包。实际上它们共享同一条技术主线法律文本的语义匹配与语言模型的领域迁移。2019年的相似案例匹配任务需要判断两段案情描述是否指向同一类型的裁判结果2020/2021年的司法考试赛道则要把法条、案情和选项做多跳推理。前者是文本对匹配后者是文本集合上的判断核心模型骨架和训练套路高度重叠。本文不对标题里的 zip 文件做“解包式”解析只从工程视角讲清楚这套方案为什么能用 BERT 系模型在有限标注下做到前排数据集和文档应该怎么组织以及 CAIL 司法考试赛道里那些真正拉开差距的训练细节。目标读者是做过文本分类或语义相似度、想进入法律 NLP 竞赛或业务落地的工程师。看完你能复现一条“数据 → 预训练 → 微调 → 评估”的完整链路并知道失败时该看哪个环节。2. 法研杯2019相似案例匹配的数据集设计与评测口径2.1 任务定义不是句子相似度是裁判结果相似度法研杯2019的相似案例匹配任务给的是三元组A, B, C其中 B 和 C 各是一条案情描述需要判断哪一条与 A 更相似。乍一听是经典的文本对排序问题但注意它的标注标准不是语义上的“像不像”而是“裁判结果是否一致”。两者差异巨大两段案情可能都用到了同一个法条、同一个争议焦点但一个判赔、一个驳回也可能用词完全不同却在“民间借贷利率上限”这个点上结论相同。所以单纯用 BERT 计算余弦相似度会翻车。正确的做法是把问题重新定义为三分类或 pairwise 排序对A, B和A, C分别建模再比较两个相似度分数。下面是我常用的数据组织方式{ id: cail2019_0001, A: 原告张某诉称2018年5月其向被告李某出借人民币10万元约定月息2%……, B: 被告王某辩称其与原告赵某之间不存在借贷关系未出具借条……, C: 原告孙某诉称2017年3月其通过银行转账向被告周某支付货款8万元……, label: B }label标注的是“B 与 A 更相似”还是“C 与 A 更相似”而不是 B、C 各自与 A 的相似度数值。这意味着模型只需要学到相对顺序不需要拟合绝对分数训练目标上用 softmax 交叉熵即可不必做回归。2.2 数据集清洗的三个关键动作去重、截断、证据对齐CAIL 系列数据集全部来自裁判文书网特点是“脏”。常见问题包括缺字乱码OCR 或原始文本解析导致、当事人姓名地址被替换为“某某”、文书中的案号和时间戳干扰语义、案情描述动不动超过 512 token。处理顺序我一般固定为统一全角半角把数字、字母、标点归一化这一步对中文法律文本尤其重要因为裁判文书里“年”和“2018年”会同时出现。清洗案号与法院信息。正则匹配\d{4}\w民初\d号这类模式并替换为[案号]占位符避免模型把“北京市朝阳区人民法院”这种高频实体当作判别信号。按句切分后做证据对齐。如果 A 和 B 都提到“借条”而 C 提到“买卖合同”模型应该从证据类型上捕捉差异。切分后保留句号、分号级别的粒度用于后续的对抗训练和 R-Drop 增强。截断策略上直接[:512]会丢尾部信息。法律文书的判决依据往往在“本院认为”之后位置偏后。常见的做法是首尾拼接截断保留前 128 token 和后 380 token中间用[SEP]连接。这个操作在法研杯上是实打实涨点的比单纯前截断高 1-2 个点 MAP。2.3 评测指标MAP 与 MRR 的差异以及为何不能用准确率自欺法研杯2019初赛的官方评测指标是 MAPMean Average Precision复赛加入了 MRRMean Reciprocal Rank。但很多参赛队伍在本地只用 accuracy 看模型好坏这是典型的自欺式评估。指标计算口径对模型行为的约束Accuracypredict(B更相似) 是否等于 label只关心二分类对不对不关心置信度排序MAP对全部三元组按预测得分排序计算每个 A 对应的平均精度要求得分平滑、单调极端值会拉低分数MRR对每个 A看正确答案排在第几位取倒数只关心第一名适合“检索返回 Top-1”的实战场景实际训练时我会用triplet loss 排序损失联合而不是单纯分类。原因很简单分类损失只约束“B 比 C 高”这个二值关系不约束“B 比 C 高多少”。在评测时MAP 对分数差的大小非常敏感只有当正负样本的分数间隔稳定时排序才能抗扰动。下面的 PyTorch 片段展示如何在一个 batch 内同时计算分类损失和排序损失import torch import torch.nn.functional as F def compute_loss(score_ab, score_ac, labels): # score_ab, score_ac: (batch_size,) 模型输出的相似度分数 # labels: 1 表示 B 与 A 更相似0 表示 C 与 A 更相似 # 分类损失把两个分数拼成二分类 logits logits torch.stack([score_ab, score_ac], dim-1) # (batch, 2) ce_loss F.cross_entropy(logits, labels) # 排序损失期望正确项的分数比错误项高出一个 margin diff score_ab - score_ac # 当 label1 时希望为正 margin_loss F.relu(1.0 - diff * (labels * 2 - 1)) # labels转成±1 margin_loss margin_loss.mean() return ce_loss 0.5 * margin_losslabels * 2 - 1把 0/1 转成 -1/1diff * sign在正确时为正、错误时为负。F.relu(1.0 - x)是 hinge loss 的标准写法。这里0.5是排序损失的权重我试过 0.3 到 1.0 之间的范围0.5 在法研杯验证集上表现最稳。3. 基于 BERT 的法律文本匹配方案从双塔到交互式编码3.1 为什么双塔很快但输在“裁判结果相似”上双塔模型DSSM、Sentence-BERT 范式把 A、B、C 分别编码成向量再算点积或余弦。优势是速度快可以做向量索引召回但劣势正好打在法研杯的命门上“相似”的定义随案件类型变化。民间借贷案里“利息约定”是决定性证据交通事故案里“责任认定书”才是关键。双塔模型把每段文本压缩成一个固定向量交互只发生在最后一步模型无法动态对齐 A 中的“争议焦点”和 B 中的“对应证据”。法研杯的标注数量不大初赛约 9k 条三元组双塔在小数据上更容易欠拟合。3.2 交互式编码用[SEP]拼接让注意力自己找对应关系主流解法是把A, B拼成一个序列输入 BERT输出[CLS]向量再接分类头。这样 BERT 每一层的自注意力都会在 A 和 B 的词元之间建立对齐关系——A 里的“利率”能和 B 里的“月息”建立注意力连接这是双塔结构做不到的。同理会计算A, C最后把两个[CLS]分数做 softmax。但直接拼接有两个坑序列长度翻倍512 上限很快不够用A 和 B 是同等重要的输入[SEP]两侧没有显式的“哪边是查询、哪边是证据”区分。常见做法是在输入嵌入中加入一个段类型segment embedding的变体或者用两个不同的分类头分别处理“A,B”和“B,A”的顺序取平均。法研杯的 A 和 B 存在逻辑先后关系A 是待匹配样例B 是候选所以顺序不要随机交换固定把 A 放前。3.3 最小可复现的微调代码结构与参数表下面给出一套可以直接套用的脚本骨架使用 HuggingFace Transformers 加载中文法律预训练模型from transformers import AutoTokenizer, AutoModelForSequenceClassification from torch.utils.data import Dataset, DataLoader from transformers import get_linear_schedule_with_warmup import torch tokenizer AutoTokenizer.from_pretrained(hfl/chinese-roberta-wwm-ext) model AutoModelForSequenceClassification.from_pretrained( hfl/chinese-roberta-wwm-ext, num_labels2 ) class PairDataset(Dataset): def __init__(self, pairs, labels, tokenizer, max_len256): self.pairs pairs # [(a_b_text, a_c_text)] self.labels labels # [0 or 1] self.tokenizer tokenizer self.max_len max_len def __len__(self): return len(self.pairs) def __getitem__(self, idx): a_b, a_c self.pairs[idx] enc_b self.tokenizer( a_b, paddingmax_length, truncationTrue, max_lengthself.max_len, return_tensorspt) enc_c self.tokenizer( a_c, paddingmax_length, truncationTrue, max_lengthself.max_len, return_tensorspt) return { input_ids_b: enc_b[input_ids].squeeze(0), attention_mask_b: enc_b[attention_mask].squeeze(0), input_ids_c: enc_c[input_ids].squeeze(0), attention_mask_c: enc_c[attention_mask].squeeze(0), label: torch.tensor(self.labels[idx], dtypetorch.long), }PyTorch 的Dataset返回的是预处理后的张量AutoModelForSequenceClassification输出[CLS]经过分类头的 logits。训练循环中要分别对 (A,B) 和 (A,C) 做一次前向再拼接 logits 计算交叉熵for batch in dataloader: logits_b model( input_idsbatch[input_ids_b], attention_maskbatch[attention_mask_b], ).logits logits_c model( input_idsbatch[input_ids_c], attention_maskbatch[attention_mask_c], ).logits combined torch.stack([logits_b[:, 1], logits_c[:, 1]], dim-1) loss F.cross_entropy(combined, batch[label]) loss.backward()logits_b[:, 1]取的是正类相似的分数两个分数拼一起作为二分类 logits。这里不直接用logits_b[:, 0] - logits_b[:, 1]是因为拼接后 softmax 的梯度更平稳。关键超参数参考表法研杯验证集上的经验值参数推荐值说明batch_size8-16显存不够时梯度累积到 32learning_rate2e-5超过 5e-5 容易灾难性遗忘训练轮数3-4BERT 微调多于 5 轮会过拟合max_len256-384首尾截断后此区间最佳warmup_ratio0.1前 10% 步数线性热身weight_decay0.01仅对非 bias 和 LayerNorm 参数生效3.4 为什么不用更大的长文本模型法律文书动辄几千字一个自然的想法是换 Longformer 或 BigBird。实测下来这类模型在 3000 token 以上才有明显优势而法研杯给的单条案情描述平均在 300-500 字之间首尾拼接截断后 512 窗口覆盖了绝大多数信息。换大模型只带来 30% 的显存开销却只涨 0.2-0.4 个点 MAP性价比很低。真正值得做的是在编码之前用正则定位“本院认为”“判决如下”等关键段落把它们强制保留在窗口内。4. 从法研杯到CAIL司法考试赛道多任务学习与对抗训练4.1 CAIL2020/2021 司法考试赛道的任务拆解CAIL2020 司法考试赛道要求模型像人一样做选择题给一个案情描述从四个选项中选出正确的法律结论。CAIL2021 升级为“案例问题”的多选题选项可能不止一个正确。这两年的冠军方案有一个共同点不把问题当成“四个选项的分类”而是拆成**“案情-选项”的文本匹配任务**——分别计算案情与每个选项之间的语义关联度选择分数最高的或超过阈值的。这与法研杯 2019 的相似案例匹配在模型架构上惊人地一致。区别只在于法研杯里 A 是案情、B/C 是候选案情司法考试里 A 是案情候选是四个选项。甚至可以直接复用PairDataset只把pairs从两个改成四个。这种“文本匹配基座 任务定制头”的复用思路是冠军团队的真正资产。4.2 多任务学习法条预测作为辅助任务CAIL 数据集自带每个案例对应的法条信息司法考试赛道也是如此。冠军方案里最常见的一招是把法条预测当成辅助任务与选项匹配共享 BERT 编码器。多头结构如下主任务每个选项与案情拼接输出匹配分数softmax 交叉熵。辅助任务案情文本预测涉及的法条类别数千类用 BERT 的[CLS]过一个线性层。from transformers import AutoModel import torch.nn as nn class LegalModel(nn.Module): def __init__(self, model_name, num_law_classes): super().__init__() self.bert AutoModel.from_pretrained(model_name) self.match_head nn.Linear(self.bert.config.hidden_size, 1) self.law_head nn.Linear(self.bert.config.hidden_size, num_law_classes) def forward(self, input_ids, attention_mask, law_labelsNone): out self.bert(input_idsinput_ids, attention_maskattention_mask) cls_vec out.last_hidden_state[:, 0] # [CLS] 向量 match_score self.match_head(cls_vec).squeeze(-1) law_logits self.law_head(cls_vec) return match_score, law_logits辅助任务与主任务的 loss 按 $L L_{match} \lambda \cdot L_{law}$ 相加$\lambda$ 从 0.3 起步验证集上不涨就降到 0.1。多任务有效的原因在于法条类别提供了“争议焦点”级别的监督信号迫使[CLS]向量不只关注表面词汇相似还要捕捉法律意义上的归类。单任务模型很容易被“双方均有过错”这类高频短语带偏多任务则帮它锚定真正的法理依据。4.3 对抗训练与 R-Drop小数据下防过拟合的两板斧法研杯和 CAIL 的标注量在 NLP 竞赛里都算小的BERT 微调很容易记住训练集的措辞模式。冠军队伍普遍用的两个正则手段FGM 对抗训练。在 embedding 上加一个小的扰动 $\delta$使模型在“最坏情况下”仍然分类正确。实现核心代码class FGM: def __init__(self, model, epsilon0.5): self.model model self.epsilon epsilon self.backup {} def attack(self): for name, param in self.model.named_parameters(): if param.requires_grad and param.grad is not None: self.backup[name] param.data.clone() norm torch.norm(param.grad) if norm ! 0: r_at self.epsilon * param.grad / norm param.data.add_(r_at) def restore(self): for name, param in self.model.named_parameters(): if name in self.backup: param.data self.backup[name] self.backup.clear()训练循环里在loss.backward()之后、optimizer.step()之前调用fgm.attack()再算一次前向和反向最后fgm.restore()。注意 FGM 不是“加两次梯度”而是用扰动后的梯度修改原梯度所以第二次反向不需要 step。R-Drop。同一份输入过两次模型用 KL 散度约束两次输出的分布一致性。它对 NLP 小数据任务涨点稳定实现也简单def r_drop_loss(p, q): p_log_softmax F.log_softmax(p, dim-1) q_softmax F.softmax(q, dim-1) kl F.kl_div(p_log_softmax, q_softmax, reductionbatchmean) return kl两次前向的 dropout mask 不同分布就会有差异。强制它们一致等价于让模型不依赖特定神经元组合效果类似 dropout 的“集成版”。法研杯上我见过 R-Drop 带来 0.8-1.5 个 MAP 点提升是除数据增强外性价比最高的操作。4.4 伪标签与模型蒸馏的边界有人会用“训练集预测 高置信度样本”做伪标签扩充这招对 CAIL2020 的单选题有效但对法研杯存在风险相似案例匹配的标签本来就依赖人为判断模型高置信度不代表标签正确。我的建议是伪标签只用于预训练阶段的领域自适应不用于微调阶段。领域自适应做法很简单——用全部无标注裁判文书继续预训练 MLM这一步对任何 CAIL 下游任务都有提升代价是时间。5. 实战坑位与参数调优案例匹配任务最易翻车的四个环节5.1 坑一评测时分数没有归一化排序不稳定很多队伍训练时 loss 降到很低但验证集 MAP 忽高忽低。原因是last-layer dropout 没有关闭推理时模型每次前向的分数都不同。在 HuggingFace 里用model.eval()只能关掉 dropout 的局部作用正确做法是同时固定随机种子import numpy as np import random def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) set_seed(42) model.eval() with torch.no_grad(): pass # 推理代码如果你的代码中调用了torch.dropout自定义层推理前也得手动关闭。5.2 坑二首尾截断仍然丢关键句MAP 卡住不动首尾截断能解决大部分长度问题但有一种情况会失败争议焦点恰好落在文书中间段。解决思路是先定位“争议焦点”特征句把句首加一个[MARK]特殊 token在输入拼接时优先保留标记句。具体操作A 原文分段后用关键词表利息、违约金、责任认定、连带、时效命中前把命中句排在序列最前面截断发生在末尾。顺序变为[MARK句i] 开头128token 结尾200token。这一招在法研杯上比单纯首尾截断再涨 0.5-0.8 个点。5.3 坑三学习率调度器的 warmup 步数没配好BERT 微调里 warmup 不是为了“预热”而是为了避免预训练分布到任务分布的剧烈跳变。法研杯数据量小warmup 比例太高会浪费训练步数太低则 loss 起步爆炸。经验规则训练 3 轮以上warmup_ratio0.1训练 1-2 轮warmup_ratio0.2使用 AdamW 时epsilon1e-8不要用默认的1e-65.4 坑四只用验证集调参忽略“提交系统里的噪声”竞赛平台在复赛阶段往往会对提交结果做平滑或多次采样本地验证集 MAP 再高也可能对不上线上分数。验证技巧是做 5 折交叉验证取平均 MAP 作为最终指标。同时把预测分数的方差多次前向的标准差也纳入观察——方差大说明模型对输入扰动敏感排名在复赛里容易掉。本地现象可能原因动作训练 loss 降、验证 MAP 不涨分类头过拟合到措辞模式降 lr、加 R-Drop验证 MAP 高、线上 MAP 低验证集划分与官方不一致按案由类型分层抽样某一类案件全错该案由样本太少检查数据分布考虑案由作为特征预测全偏向 B正负样本不平衡加 focal loss 或重采样6. 复盘技巧从竞赛模型到司法检索的工程迁移无论你最终目的是打竞赛还是做落地有一个技巧是通用的把匹配模型输出保存成“可解释证据”。BERT 匹配分数是一个黑盒法务人员不会接受“模型说更像”。在 CAIL 司法考试解法里冠军团队通常会额外保存注意力矩阵中每个 token 对[CLS]的贡献输给前端展示成高亮句。实现上可以利用AutoModel返回的 attentionsoutputs model.bert(input_idsinput_ids, attention_maskattention_mask, output_attentionsTrue) attentions outputs.attentions # tuple, 每层 (batch, heads, seq, seq) # 取最后一层所有头的平均按 [CLS] 的注意力权重选 top-k 句 cls_weights attentions[-1].mean(dim1)[0, 0] # (seq_len,)cls_weights就是每个 token 对[CLS]的注意力权重。配合 tokenizer 的decode可以还原出高权重句子。这个“证据高亮”不仅提升了方案的说服力还能反向用于 Bad Case 分析如果模型把注意力放在“原告诉称”这种套话上说明截断策略或 MLM 预训练环节有问题。训练结束后建议再做两件事。第一把 5 折模型的预测分数取平均之前先做rank 平均而不是 score 平均——分数量纲不一致时rank 平均更稳。第二把最优超参数以 JSON 形式保存在模型目录旁包括 warmup、lr、batch_size、截断策略版本号。你永远不会记得哪一组参数跑了哪一版数据但排行榜会替你记得。CAIL 司法考试赛道对相似案例匹配方案的最大启示是不要为每个任务重新发明架构把“文本对匹配”做到极致法条预测、选项匹配、案例检索都是它的变体。竞赛名次会过期但这条“清洗 → 配对 → 交互编码 → 多任务 → 对抗训练 → 证据可视化”的链路换一个领域依然能打。本文还有配套的精品资源点击获取