
简介压缩包整合了法研杯2019相似案例匹配第二名解决方案及CAIL2020-2021司法考试赛道冠军团队的相关资源面向法律NLP、智慧司法研究的学生与竞赛工程师重点解决案例相似度匹配中的文本语义建模与复杂法律信息提取问题。全包共22个文件以Python脚本模型训练、推理预测、Shell脚本与Dockerfile环境构建、一键部署、Markdown/TXT文档使用说明与赛题描述为主压缩后仅192KB便于快速下载与二次开发。已有266人学习适合作为司法AI竞赛入门与复现的参考。内容涵盖BERT类预训练模型的微调、TF-IDF/词向量特征工程、XGBoost等机器学习算法融合以及数据划分、评价指标、调参与模型融合策略附带评测脚本、数据样例与多格式说明文档可帮助读者从数据预处理到结果评测快速跑通完整链路深入理解法研杯2019第二名方案的工程化细节与调优思路。1. 法研杯2019与CAIL司法考试双赛道这一份方案包到底在解决什么问题一个压缩包里同时出现“法研杯2019相似案例匹配第二名解决方案”和“CAIL2020/2021司法考试赛道冠军团队”第一眼会让人以为这是两个无关方案被随手丢到一起。实际上在裁判文书AI这个领域它们是同一套能力的两种考核方式一个判断两份案情描述是不是同一类法律争议一个判断一个法律问题哪个选项站得住脚。更关键的是这套包不是给你看刷榜成绩的里面的数据集、训练脚本和文档是按“能复现”的标准整理的。这份笔记不替你朗读paper直接拆两套方案的训练逻辑、关键参数和踩坑点并给出一份可抄作业的最小实现。2. 相似案例匹配第二名方案拆解把长文本塞进交互式BERT的完整训练路线2.1 从任务定义到数据形态为什么这个赛道不适合普通文本分类套路法研杯2019相似案例匹配赛道的输入是三元组一个查询案情A两个候选案情B和C要求判断B和C哪一个与A在“法律意义上”更相似。注意这里强调的是法律意义不是字面相似。两个案子都在讲盗窃但一个涉及入户盗窃一个是扒窃在罪名和量刑逻辑上完全不同反过来一个讲故意伤害一个讲寻衅滋事但都围绕“酒后冲突引发互殴”的事实结构在法律争议上反而更接近。数据给的是裁判文书的事实描述部分去掉了法院认为、判决结果等字段避免模型直接抄答案。这样的任务形态决定了三件事。第一它是pairwise的二分类问题和文本匹配里的NLI任务结构一样可以直接套 [CLS] A [SEP] B [SEP] 这种交互式输入。第二不能把它简单当成余弦相似度回归来做因为赛道要的不是绝对相似分数而是在同一查询下对候选做相对排序。第三案情描述平均长度远超BERT的512 token上限任何不做长文本处理的方案都会在验证集上立刻露馅。我一般把业务痛点拆成“数据形态、任务结构、文本长度”三层来看。数据形态决定了输入怎么组织任务结构决定了输出头怎么设计文本长度决定了主干网络怎么改。这个赛道的第二梯队方案和前排方案之间的差距往往就压在第三点上。2.2 第二梯队的差距在哪预训练模型、长文本编码和训练细节的三级跳先说结论如果直接拿BERT-wwm-ext在512截断的输入上跑到验证集80%左右接下来提升空间最大的地方不在换更大模型而在把长文本编码方式和训练稳定性补起来。多数方案最后都落在一个组合上中文预训练模型BERT-wwm-ext或RoBERTa-wwm-ext做主干输入侧采用滑窗切段输出侧做segment级attention pooling加上FGM对抗训练和伪标签半监督最后多seed融合。滑窗切段的做法是把长案情按固定长度切成多段每段保留部分上下文重叠再让各段独立过BERT得到各自的[CLS]表示最后用一层attention pooling把多个段整合成一个定长向量。这样做的好处是模型能看到完整案情而不是只看头512个字的“案发经过”却丢掉后面的“作案工具”和“财物价值”细节。代价是推理速度几乎翻倍因为一条文本要过不止一次BERT。训练细节上相似案例匹配有一个容易忽略的点样本类别没那么不平衡但相似对与不相似对的信息量极不均衡。因此除了标准的cross entropy不少前排方案会加一个排序损失让A与B的相似分数和A与C的相似分数拉开margin。这个trick对榜单的排序指标特别有效因为官方评测是按候选间的排序关系打的不是按单独分类准确率。排名靠前的方案不会完整公开参数但按行业里可复现的主流配置来跑下面这几个数是常见的max_seq_len512作为单段上限、滑窗stride256、batch_size16等效、learning_rate2e-5、warmup_ratio0.1、epoch3。显存不够时batch减半并开梯度累积比把学习率一并调小更稳。2.3 核心训练代码Pairwise交互匹配的最小可复现实现import torch from torch.utils.data import Dataset, DataLoader from transformers import BertTokenizer, BertForSequenceClassification, AdamW tokenizer BertTokenizer.from_pretrained(hfl/chinese-bert-wwm-ext) model BertForSequenceClassification.from_pretrained( hfl/chinese-bert-wwm-ext, num_labels2 ).cuda() def sliding_window(text, max_len256, stride128): tokens tokenizer.tokenize(text) segments [] for i in range(0, max(len(tokens), 1), stride): seg tokens[i: i max_len] segments.append(seg) if i max_len len(tokens): break return segments def encode_pair(query, candidate): q_tokens tokenizer.tokenize(query) c_tokens sliding_window(candidate) # 每条输入查询截断到128token候选按256token滑窗 input_ids_list, seg_ids_list [], [] for c_seg in c_tokens: tokens [[CLS]] q_tokens[:128] [[SEP]] c_seg [[SEP]] segment_ids [0] * (len(q_tokens) 2) [1] * (len(c_seg) 1) ids tokenizer.convert_tokens_to_ids(tokens) input_ids_list.append(ids) seg_ids_list.append(segment_ids) return input_ids_list, seg_ids_list代码逻辑上滑窗函数把长候选切成若干段每一段和查询构成一条BERT输入。查询侧只保留前128个token候选侧用256的段长去滑避免一条输入被截断到只剩头部。这种“查询截断候选滑窗”是这类任务最常见的选择查询通常比候选短也通常是单焦点文本截断对它的损失比截断候选小得多。如果候选长度分布比较平均可以适当调大stride来减少段数、加快训练但stride调到256之后段间重叠太少切点如果恰好落在关键情节上信息会断。一般建议stride取max_len的一半这个比例在速度和效果之间最稳。训练侧需要把pairwise数据组织成batch并对同一个A下的B、C样本做排序约束def train_step(batch, model, optimizer, margin0.1): input_ids, attn_mask, seg_ids, labels batch out model( input_idsinput_ids, attention_maskattn_mask, token_type_idsseg_ids, labelslabels, ) # 排序loss同query下正候选得分要高于负候选至少margin scores out.logits[:, 1] # 假设batch内每两行为同一query的一组对比 pos_scores scores[::2] neg_scores scores[1::2] rank_loss torch.relu(neg_scores - pos_scores margin).mean() loss out.loss 0.5 * rank_loss loss.backward() return loss.item()这段代码有两个关键点。第一分类loss还是主力但加了0.5权重的rank loss去约束“正候选得分高于负候选”因为榜单按候选对排序计分这个约束直接对排序指标生效。margin设0.1就够设大了会让模型过度关注边界样本训练后期容易出现损失震荡。第二batch的组织方式很讲究我按“同一个查询的两个候选排在相邻两行”来组batch这样scores[::2]和scores[1::2]天然形成对比对。如果你的业务里候选数量不定改成把同一query的所有候选一起过模型、在输出侧做pairwise比较也行原理一样。2.4 给方案加分的三个边际技巧伪标签、对抗训练、模型融合第一梯队方案和第二梯队拉开差距靠的往往不是主干网络而是三个边际技巧的组合。伪标签用训练好的模型对无标注案情对打标签加进训练集做第二轮微调难点在于阈值怎么设。置信度低于0.85的样本直接丢弃且只在训练后半段加入伪标签否则模型会被早期错误累积带偏。FGM对抗训练对长文本分类特别有效因为案情描述里同义词、近义表达特别多对抗扰动能逼模型学到更鲁棒的事实结构特征。模型融合方面常见组合是“两个不同随机种子的BERT-wwm-ext 一个基于滑窗多段向量平均的文本CNN”后者捕捉局部n-gram特征和BERT的全局语义形成互补投票权重按验证集调一般BERT类占0.7到0.8。这三个技巧的作用边界也要说清楚。伪标签在数据量小不足1万对时收益大数据量大了反而容易饱和FGM只对微调阶段的BERT有效对CNN这类小模型意义不大融合如果只在种子之间做提升有限真正有效的是跨模型结构融合。这些技巧单独拎出来都只有零点几个点叠在一起就是榜单上从十几名到前五名的差距。3. 司法考试赛道冠军方案拆解检索式知识注入和多任务学习怎么搭3.1 司法考试任务的三个特殊性长选项、强推理、知识外延CAIL2020与2021司法考试赛道的目标比较直观给一段法律情境描述比如合同履行事实四个选项都是完整句子模型要选出唯一正确的一项。表面上是选择题实际和法律文本分类完全不同。第一个特殊性是选项本身很长四个选项加起来可能超过2000字有的选项本身就是一个小案情直接拼接会导致题干和选项之间的交互被长文本稀释。第二个特殊性是强推理题目经常考“这个行为构成合同诈骗还是普通违约”事实几乎一样考的是法律要件是否齐备模型必须跨句子对齐要件而不是找关键词。第三个特殊性是知识外延很多题不把答案藏在题干和选项里而是要求你知道某个法条的构成要件这迫使方案必须引入外部知识源。冠军方案在公开分享里通常会强调“知识增强”和“多任务学习”本质上是在回答一个问题模型怎么把法条知识变成推理依据。做法上先用检索器从法条库和法律百科里召回与题目相关的知识段落拼到模型输入里再让模型同时预测“选项是否正确”和“本题涉及的法律领域”用后者做多任务正则。这个赛道的核心不是选择题框架本身而是“检索-融合-推理”三段式的工程质量。选择题框架是现成的检索召回和法律知识库的建设才是拉开差距的地方。3.2 冠军方案的总体架构检索式知识注入 多任务输出冠军方案按公开分享的通用结构可以拆成三层。数据层把法条库、法律百科、历年真题法律知识点整理成可检索的知识库检索层用BM25或向量召回对每一道题召回topK知识段落模型层用预训练模型做多选推理辅助任务预测法条领域类别。之所以用检索而不是直接把全部法条塞给模型是因为法律知识库太大全量拼接会超出输入长度而且无关法条会成为强噪声。检索层让模型只看到与当前题目最相关的知识相当于给模型配了一个开卷考试的资料检索工具。多任务输出是一个容易被人忽略的细节。只让模型判断选项对错时模型学到的是“这道题的语义特征”但司法考试题目里刑民行知识的分布差异极大。多任务让模型同时输出“题目所属法律领域”刑事、民事、行政、程序法等于强制模型先做一次案件定性这个中间目标对最终的选项判断有明显帮助。我倾向于把它理解为一种结构化正则而不是真正的多任务——它不追求领域分类的准确率只为了让主干抽取到更法律化的特征。3.3 关键代码BM25知识召回、知识拼接与多任务训练下面给的是这套架构里最关键的一环对每个题目召回法条知识并构造训练样本。这里看的是“知识怎么进到模型里”这一步。import jieba from rank_bm25 import BM25Okapi # 假设law_corpus是全部法条文本law_corpus_tokens是预先分词好的 law_corpus_tokens [jieba.lcut(doc) for doc in law_corpus] bm25 BM25Okapi(law_corpus_tokens) def retrieve_knowledge(question, option, topk3): query_tokens jieba.lcut(question option) scores bm25.get_scores(query_tokens) top_idx scores.argsort()[-topk:][::-1] # 返回每题的召回段落最多保留200token return [law_corpus[i][:200] for i in top_idx]这段代码是标准的BM25召回对“题干选项”分词和法条库比对取top3。注意检索查询里把选项也拼进去了因为有的题只看题干无法判断知识归属选项里反而藏着关键法律概念。比如题干只描述“某公司延期交货”选项里出现“不可抗力”和“情势变更”这两者对应的法条完全不同拼上选项才能召回正确知识。召回之后是拼接和训练。知识段落拼在题干之后、选项之前效果最好因为模型在阅读选项前先建立了法律概念选项中再出现相关表述能形成对照。# 构造输入question knowledge option四选项共享同一份知识 def build_input(question, knowledge_paras, option): q question .join(knowledge_paras) return q [SEP] option # 多任务训练时的forward部分 import torch.nn as nn def forward_with_mt(model, batch): input_ids, seg_ids, label, domain_label batch logits model(input_idsinput_ids, token_type_idsseg_ids).logits # 主任务选项对错 loss_main nn.CrossEntropyLoss()(logits, label) # 辅助任务法律领域复用同一个表示 domain_logits model.cls_head(logits) loss_domain nn.CrossEntropyLoss()(domain_logits, domain_label) return loss_main 0.3 * loss_domain代码的要点是主任务和辅助任务共享同一个主干编码器辅助任务的loss权重只给0.3避免领域分类喧宾夺主。domain_label来自数据集的标注字段如果业务数据里没有这个字段可以用案由或法条类别做粗粒度替代。训练时主任务是选项对错分类每道题四个option分别过模型取分数最高的一个为最终答案。3.4 多模型投票的工程细节三个模型各自的角色分配司法考试赛道前排方案基本都会做模型融合但融合不是简单bagging。我见过最有效的组合是三个不同认知层的模型。模型输入擅长题型投票权重参考知识增强BERT题干召回法条选项需要法条依据的推理题0.4纯文本BERT题干选项语言形式直接可判的题0.35TF-IDFGBDT/LR术语重叠特征法律概念记忆题0.25投票方式上不要直接用argmax投票而是用每个模型对四个选项的分数归一化后相加保留模型内部的置信度差异。有一个细节值得注意如果某个模型对四个选项的分数都特别接近最大和次大差距小于0.05说明它在这道题上基本是瞎猜这时候应该降低它的权重甚至把它从这道题的投票中剔除。这种“按题置信度动态调权重”的规则比固定权重更稳。4. 法律文本训练的五个常见坑从数据泄露到长文本截断的排查笔记4.1 坑一相似案例匹配里的数据泄漏让验证集变成自欺欺人现象用随机行划分训练集和验证集本地验证准确率92%提交到官方评测只有74%。原因裁判文书库里同一个案件可能存在多篇文书比如一审判决书、二审判决书、同一团伙多个被告人的判决书事实描述高度重合。随机划分会把同一案件的文书同时分到训练集和验证集模型见过几乎一样的文本验证分虚高。这是法律AI里最典型、也最容易忽略的泄漏。解决划分数据前按案号或文书编号去重保证训练集和验证集没有同一案件的文本。做法是先对所有样本按“案号”字段分组同一案号全放进训练集或全放进验证集再按罪名分布和时间维度做分层采样。4.2 坑二长文本直接截断到512模型只学到了开头2000字的偏见现象基线在验证集上能到80%但错误样本分析发现模型对“案情开头提到严重情节”的案例有系统性偏好哪怕后面的段落明显削弱了这个结论。原因案情描述的前几百字通常是案发经过的概括后面才是具体细节、手段、财物损失。直接截断让模型只能看到前面后面的关键细节完全丢失。更糟的是这样截断容易让模型学到“开头出现某个词就判相似”的捷径。解决不要用截断用滑窗切段。让模型分段读完整篇文章再做segment级融合。如果推理资源有限至少做到“开头一段结尾一段”的拼接因为裁判文书的事实描述里结尾往往有行为定性和损失统计信息密度不比开头低。4.3 坑三伪标签不加阈值等于把噪声当老师现象第一轮伪标签加入后验证集指标不升反降而且越训练降得越多。原因伪标签的数据都是模型自己打的置信度低的样本错误率极高。比如模型对一对完全不相干的案情打出0.6的相似概率阈值设在0.5这个错误标签就进训练集了。低置信度噪声样本在训练中被反复强化最后模型被带偏。解决伪标签必须设高阈值一般在0.85到0.9之间且只在训练中后期加入。加入后要监控验证集指标如果连续两个epoch下降就减小伪标签样本量或直接去掉。另外伪标签只加易分样本不加难样本否则等于给模型喂重复的困难噪声。4.4 坑四司法考试知识召回回了噪声段落比不召回更差现象给BERT加了BM25召回的知识段落之后选择题准确率反而下降了两到三个点。原因BM25召回的是字面重合度高的法条但司法考试题里的法条引用往往是间接的。比如题干讲合同履行纠纷召回的一篇合同法总则里正好提到合同解除看起来相关实际对判断“是否构成违约”毫无帮助。这些噪声知识被拼进输入后占用了宝贵的上下文长度还干扰了模型对题干和选项的注意力。解决先做一次候选过滤只保留与“题干正确选项”相关性都较高的段落召回数量宁可少top3够了。同时把召回段落按与题干的BM25分数排序截断到不超过150个token。如果线上条件允许可以把BM25换成向量召回对语义相关性更敏感减少字面噪声。4.5 坑五多模型投票的权重在测试集上调再玄学也会翻车现象拿着验证集调好的三个模型权重到测试集一跑涨点消失了。原因验证集和测试集的题型分布、难度分布不完全一致。当在验证集上把权重调到过拟合状态时遇到分布漂移优势就反转了。法律文书数据集尤其明显——不同年份的真题考点不一样验证集调出来的权重在测试集上失效。解决权重搜索范围要粗0.05的网格就够避免在验证集上做精细化调参。更稳的做法是用时间切分验证集2019年之前的数据训练2019年的数据验证模拟真实的时间外推场景。权重选定之后不要因为测试集分数波动再回头调调了就是自欺欺人。5. 数据集与文档的正确用法复现验证、指标对齐与迁移到业务5.1 拿到数据集先做三件事读schema、跑baseline、定指标不管方案包里的数据集是哪个赛道第一件事永远是读字段说明。法研杯相似案例匹配数据集的核心字段一般是案例编号、案情描述、候选关系司法考试赛道的数据集核心字段是题干、四个选项、答案可能附带法条或知识标注。先把每个字段的类型、长度分布、缺失比例跑一遍统计不要急着上模型。第二件事是用官方评测脚本跑一个最朴素的baseline。很多竞赛包会提供eval脚本建议直接用官方脚本对baseline的输出做评分确认自己的评分方式和官方口径一致。最容易翻车的点在于“模型输出什么格式”——是输出相似度分数还是直接输出胜负判断评测脚本的预期格式不同评分结果会完全不同。先让baseline分数在官方脚本上跑通再开始微调模型可以避免后面所有实验的对比基准是错的。第三件事是定指标的可信区间。竞赛数据集通常不大一次训练受随机种子影响明显。我会用三到五个种子跑同一个配置取平均和方差。两个配置之间的准确率差要大于一个标准差才认为是有效提升。否则你花一周调出来的0.5个点可能只是随机波动。5.2 复现时最容易忽略的“环境对齐”版本、种子、显存策略复现别人的竞赛方案最耗时的往往不是改模型而是对齐环境。预训练模型建议按方案文档里的版本号下载如果文档没写优先用当时时间点的主流版本比如BERT-wwm-ext或RoBERTa-wwm-ext。不要贪新换一个更新的模型——下游任务微调的结果对预训练版本很敏感换了模型就要重新调全部超参。随机种子对齐是个大坑。只固定torch和numpy的seed不够要连DataLoader的shuffle顺序、CUDA的cudnn.benchmark都固定。我的习惯是固定一个master_seed并为每个实验记录完整的seed序列方便复现。显存策略上batch_size和梯度累积步数要配套调整。方案里写batch_size16、epoch3你只有单卡12G显存可以把batch降到8、梯度累积设2等效batch_size不变这是唯一能和原配置对齐又不吃显存的做法。5.3 把竞赛方案迁移到真实业务三种常见落地路径竞赛方案迁移到业务不是解压跑通就完事而是要改掉竞赛里“合法但现实中不存在”的假设。常见有三种路径。第一种是把相似案例匹配模型放到检索系统里做重排先用ES或向量检索召回100个候选再用交互式BERT对候选打分取top10。第二种是把司法考试的多选题框架改造成法律咨询的答案验证用户问一个问题用模型从法条库里选出三个可能依据再用选项判断框架验证哪个依据最合理。第三种是把两个赛道的通用能力——长法律文本编码和知识增强——直接作为特征抽取器接到案由分类或风险识别任务上只训练一个小的分类头。迁移的时候有一个统一原则先确认业务数据分布和竞赛数据的差异再决定哪些模块要重训。如果业务文书比法研杯的裁判文书更长滑窗参数就要重调如果业务问题的知识来源不是法条而是企业内部规章制度知识库要整体替换。永远不要带一个在竞赛上千锤百炼、但从没接触过业务数据的模型直接上线。6. 从刷榜到落地把相似案例匹配改造成可用服务的一个关键技巧竞赛里大家直接对全文做交互匹配因为离线评测不在乎耗时。但上了线用户等不了你把一篇8000字的案情和20个候选依次过BERT。我做的处理是加一层“判决要点块”的粗筛把长文书先切块再检索最后只对少量候选做精细匹配。具体来说用案由和裁判文书结构知识把案情切成“案件起因、行为经过、危害结果、量刑情节”四类语义块每块用一个轻量编码器产出向量。召回阶段用向量或BM25在候选库中按“查询块和候选块的最高相似度”粗筛出top10重排阶段再把查询的完整案情和这10个候选的对应细节块做交互式BERT打分。和直接在全文上做最近邻检索相比块级别的粗筛能把召回相关度提升不少因为当事人的争议焦点通常只落在全文的一两个块上全文向量会把焦点稀释掉。这个做法对司法考试方案同样适用知识库侧把法条预先切成条文块检索粒度从“整篇法条”降到“单个条文”再拼接选项判断框架。上线前我会做一次回归测试拿200道业务真题分别用全文方案和切块方案跑一遍确认切块不损失头部效果再看P99延迟能不能砍掉一半。在我处理过的法律文本项目里延迟基本能从1000ms以上降到400ms以下效果不掉点。这里有一个教训方案包再完整也不等于业务效果。所有竞赛里练出来的技巧最终都要以自己业务的三个词来验收——召回率、准确性、延迟。希望帮到你。本文还有配套的精品资源点击获取