简介:这份资源是中国法研杯司法人工智能挑战赛的完整参赛源码与项目说明,面向计算机、数学、电子信息等专业的学生及算法竞赛爱好者,可作为课程设计、期末大作业或毕业设计的参考案例,帮助理解司法领域自然语言处理任务的工程实现路径。压缩包共收录444个文件,约12.26MB,其中307个py文件构成核心算法与训练流程,52个dll、22个pyd及11个exe支撑运行环境,另有txt、md、json、xml等配置与说明文档,以及pth模型权重、png图示和bat脚本,目录结构完整,便于按模块查阅。目前已有195人学习下载,适合希望借鉴赛题方案、复现实验流程或学习司法AI建模思路的读者。项目涵盖数据预处理、模型构建、训练评估等环节,可作为算法竞赛与工程实践的学习范本,帮助读者快速把握整体架构并在此基础上调试与扩展。
1. 法研杯司法 AI 挑战赛源码拆解:从赛题到可复现的落地路径
拿到一份「中国法研杯-司法人工智能挑战赛参赛源码+项目说明.zip」,很多人第一反应是解压、找requirements.txt、pip install、跑main.py,然后被一堆路径报错和缺失权重劝退。这个比赛在国内法律 NLP 圈子里知名度不低,赛题通常围绕司法阅读理解、判决预测、法条推荐、争议焦点识别、类案检索这几类任务展开,数据以裁判文书、案情描述、法条文本为主。它解决的核心问题是:把非结构化的法律文本,转成可计算、可检索、可预测的结构化信号。适合谁?做法律科技产品的工程师、想入门法律 NLP 的学生、需要给律所或法院做智能辅助系统的团队。但源码包不等于能跑通的系统,它更像一份「半成品图纸」——赛题思路、模型结构、数据处理逻辑都在,缺的是环境对齐、数据补齐和参数调优。下面按「这是什么 → 怎么跑 → 坑在哪 → 怎么改」的顺序,把这份源码拆成能照着做的实战笔记。
2. 司法 AI 赛题的典型任务与源码结构:先看懂再动手
2.1 法研杯常见赛题类型与对应技术栈
法研杯历届赛题虽然每年有调整,但底层任务类型相对稳定。拿到源码后,先别急着跑,先判断它属于哪一类,因为不同任务的数据格式、评价指标、模型选型差异很大。
| 赛题类型 | 输入 | 输出 | 常用模型 | 评价指标 |
|---|---|---|---|---|
| 司法阅读理解 | 案情 + 问题 + 候选答案 | 答案片段 | BERT/RoBERTa + 指针网络 | EM / F1 |
| 判决预测 | 案情描述 | 罪名/法条/刑期 | 多任务 BERT + 分类头 | Accuracy / Macro-F1 |
| 法条推荐 | 案情描述 | 相关法条列表 | 文本多标签分类 | Micro-F1 / Recall@k |
| 争议焦点识别 | 原被告陈述 | 争议焦点 | 序列标注 / 抽取式 | F1 |
| 类案检索 | 查询案情 | 相似案例排序 | 双塔语义匹配 | MAP / NDCG |
源码包里一般会有data/、models/、utils/、config/、train.py、predict.py这几个部分。先打开config/或config.py,看task_type字段,这是判断任务类型最快的入口。如果配置里写的是multi_label,大概率是法条推荐或罪名多标签;如果是span_extraction,就是阅读理解类。
提示:不要一上来就改模型结构。先确认数据字段和标签体系,很多源码跑不通是因为数据路径写死或标签映射文件缺失。
2.2 源码目录的阅读顺序与关键文件定位
我一般按这个顺序读一份比赛源码,能最快判断它能不能复用:
README.md或项目说明.md:看赛题背景、数据来源、运行命令。注意,很多比赛源码的说明写得很简略,甚至只有一句话,别指望它把环境讲清楚。requirements.txt或environment.yml:看依赖版本。司法 AI 比赛常用transformers、torch、jieba、sklearn,版本冲突是翻车重灾区。config/或config.py:看数据路径、模型名称、超参数、任务类型。data/或dataset/:看数据格式。常见的是 JSON、JSONL、TSV,字段名可能是fact、question、answer、label。models/:看模型定义。如果是 BERT 类,重点看forward函数的输出维度和损失函数。train.py/main.py:看训练入口,重点看数据加载、优化器、学习率调度、保存逻辑。predict.py/inference.py:看推理入口,重点看输出格式和后处理。
# 先看目录结构,不要急着装依赖 find . -maxdepth 2 -type f | sort # 重点看配置文件和说明 cat README.md 2>/dev/null || cat 项目说明.md 2>/dev/null cat requirements.txt 2>/dev/null cat config.py 2>/dev/null || ls config/上面这段命令的作用是快速定位关键文件。find限制深度为 2,避免被深层缓存目录干扰;cat依次尝试读取说明和配置,如果文件不存在就跳过。参数上,-maxdepth 2可以根据实际目录深度调整,一般比赛源码不会超过三层。
2.3 数据格式对齐:比赛数据与源码期望的差距
比赛源码最常见的问题就是数据格式对不上。比如源码期望的 JSON 字段是{"fact": "...", "labels": [1,0,1]},但你手里的数据是{"案情": "...", "法条": "刑法第二百六十四条"}。这时候需要写一个转换脚本,而不是硬改源码里的字段名。
import json def convert_format(src_path, dst_path): """ 将比赛原始数据转换为源码期望的 JSONL 格式 src_path: 原始数据路径 dst_path: 转换后输出路径 """ with open(src_path, 'r', encoding='utf-8') as f: raw = json.load(f) with open(dst_path, 'w', encoding='utf-8') as f: for item in raw: # 字段映射:根据实际数据调整 key converted = { "fact": item.get("案情", "").strip(), "labels": item.get("法条标签", []), "accusation": item.get("罪名", "") } # 过滤空样本,避免训练时报错 if not converted["fact"]: continue f.write(json.dumps(converted, ensure_ascii=False) + "\n") if __name__ == "__main__": convert_format("raw_data.json", "train.jsonl")这段代码的核心是字段映射和空样本过滤。ensure_ascii=False保证中文正常写入;strip()去掉首尾空白;空fact直接跳过,因为 BERT 类模型遇到空输入会报维度错误。参数上,src_path和dst_path按实际路径改,如果原始数据是多个文件,可以用glob批量处理。
注意:转换后一定要统计样本数量和标签分布。如果某个标签只有个位数样本,训练时会出现严重类别不平衡,需要做重采样或调整损失函数权重。
3. 环境搭建与最小可运行闭环:把训练跑起来
3.1 依赖安装与版本锁定:避开 transformers 版本地狱
司法 AI 比赛源码大多基于 PyTorch + Transformers。2023 年之后的比赛源码,transformers版本通常在 4.20 到 4.40 之间。版本不匹配的典型报错是ImportError: cannot import name 'AutoModelForSequenceClassification'或者AttributeError: 'BertConfig' object has no attribute 'xxx'。
# 创建独立环境,避免污染全局 conda create -n legal_ai python=3.8 -y conda activate legal_ai # 先装 PyTorch,根据 CUDA 版本选择 pip install torch==1.13.1+cu117 -f https://download.pytorch.org/whl/torch_stable.html # 再装 transformers,不要直接 pip install transformers pip install transformers==4.28.1 pip install scikit-learn jieba tqdm numpy pandas这里把torch和transformers分开装,是因为transformers会自动拉取兼容的torch版本,但比赛源码往往对torch有特定要求。python=3.8是保险选择,3.9 以上有时会遇到tokenizers编译问题。如果源码的requirements.txt写了具体版本,优先按它来,但要注意torch的 CUDA 版本和本机驱动匹配。
提示:如果
pip install卡在tokenizers编译,直接装预编译包:pip install tokenizers==0.13.3 --no-build-isolation。
3.2 数据加载与预处理:从原始文书到模型输入
法律文本的预处理比通用 NLP 多几个步骤:去除文书中的隐私信息、统一法条引用格式、处理长文本截断。裁判文书动辄几千字,BERT 的 512 上限根本不够,常见做法是分段取前 N 段或使用 Longformer。
from transformers import BertTokenizer import re tokenizer = BertTokenizer.from_pretrained("bert-base-chinese") def clean_legal_text(text): """清洗法律文本:去隐私、去多余空白、统一标点""" # 去除身份证号、手机号等敏感信息 text = re.sub(r'\d{17}[\dXx]', '[ID]', text) text = re.sub(r'1[3-9]\d{9}', '[PHONE]', text) # 统一全角半角标点 text = text.replace('.', '.').replace(',', ',') # 合并多余空白 text = re.sub(r'\s+', ' ', text).strip() return text def encode_sample(fact, label, max_len=512): """将案情文本编码为模型输入""" fact = clean_legal_text(fact) encoding = tokenizer( fact, max_length=max_len, padding='max_length', truncation=True, return_tensors='pt' ) return { 'input_ids': encoding['input_ids'].squeeze(0), 'attention_mask': encoding['attention_mask'].squeeze(0), 'labels': label }clean_legal_text里的正则替换是法律 NLP 的常规操作,[ID]和[PHONE]占位符不会影响模型语义,还能避免隐私泄露。max_len=512是 BERT 的硬上限,如果案情超过 512 个 token,truncation=True会直接截断,可能丢失关键信息。改进方案是用滑动窗口取多段,或者换longformer-base-4096。
3.3 训练脚本关键参数:学习率、batch size 与 warmup
比赛源码的训练脚本通常有train.py或run_train.sh。重点看这几个参数:learning_rate、batch_size、num_epochs、warmup_ratio、max_grad_norm。法律文本分类任务,学习率一般设 2e-5 到 5e-5,batch size 根据显存调整,8GB 显存跑bert-base最多 16。
# 典型训练命令 python train.py \ --data_dir ./data \ --model_name bert-base-chinese \ --task_type multi_label \ --learning_rate 3e-5 \ --batch_size 16 \ --num_epochs 5 \ --warmup_ratio 0.1 \ --max_grad_norm 1.0 \ --output_dir ./output \ --seed 42learning_rate 3e-5是 BERT 微调的经典值,太大容易震荡,太小收敛慢。warmup_ratio 0.1表示前 10% 的步数做学习率预热,避免初期梯度爆炸。max_grad_norm 1.0是梯度裁剪,法律文本长句多,梯度容易偏大。seed 42固定随机种子,保证结果可复现。如果训练 loss 不下降,先检查数据标签是否和模型输出维度对齐。
注意:多标签任务的损失函数要用
BCEWithLogitsLoss,不是CrossEntropyLoss。源码里如果写错了,训练会报维度错误或 loss 异常。
4. 模型推理与结果后处理:从输出到可提交格式
4.1 推理脚本的输入输出对齐
训练跑通后,predict.py或inference.py负责生成提交文件。常见问题是推理时的数据处理和训练时不一致,比如训练用了clean_legal_text,推理时忘了调用,导致输入分布偏移。
import torch from transformers import BertForSequenceClassification def predict(model_path, test_file, output_file): """加载模型并对测试集推理""" model = BertForSequenceClassification.from_pretrained(model_path) model.eval() tokenizer = BertTokenizer.from_pretrained("bert-base-chinese") results = [] with open(test_file, 'r', encoding='utf-8') as f: for line in f: item = json.loads(line) # 必须和训练时用同样的清洗逻辑 fact = clean_legal_text(item['fact']) encoding = tokenizer(fact, max_length=512, padding='max_length', truncation=True, return_tensors='pt') with torch.no_grad(): logits = model(**encoding).logits # 多标签用 sigmoid,单标签用 softmax probs = torch.sigmoid(logits).squeeze().cpu().numpy() preds = (probs > 0.5).astype(int).tolist() results.append({"id": item["id"], "labels": preds}) with open(output_file, 'w', encoding='utf-8') as f: for r in results: f.write(json.dumps(r, ensure_ascii=False) + "\n")关键点是model.eval()和torch.no_grad(),前者关闭 dropout,后者节省显存。sigmoid用于多标签,阈值 0.5 是默认值,但法律任务里可以根据验证集调阈值,比如 0.3 能提高召回。输出格式要严格按比赛要求,有的比赛要求 JSON 数组,有的要求每行一个 JSON。
4.2 后处理:法条归一化与阈值调优
法条推荐任务的后处理很关键。模型输出的是法条 ID 或标签,但比赛提交可能要求法条名称,或者要求去重、排序。另外,多标签阈值直接影响 F1,需要在验证集上扫一遍。
import numpy as np from sklearn.metrics import f1_score def search_threshold(y_true, y_probs, thresholds=np.arange(0.1, 0.9, 0.05)): """在验证集上搜索最佳阈值""" best_th, best_f1 = 0.5, 0 for th in thresholds: y_pred = (y_probs > th).astype(int) f1 = f1_score(y_true, y_pred, average='micro') if f1 > best_f1: best_f1 = f1 best_th = th return best_th, best_f1这段代码在验证集上遍历 0.1 到 0.85 的阈值,取 Micro-F1 最高的。法律任务里,法条推荐通常更看重召回,阈值可以偏低;罪名预测更看重准确,阈值可以偏高。average='micro'适合多标签整体评估,如果关心中文每个法条的单独表现,用average='macro'。
提示:阈值调优必须在验证集上做,不要用测试集。比赛提交前,把验证集切分固定下来,避免数据泄露。
5. 避坑与排查:司法 AI 源码复现的 5 个血泪教训
5.1 现象:训练 loss 正常但验证 F1 极低
原因:标签映射错位。源码里的label2id和实际数据的标签顺序不一致,模型学的是错位的标签。
解决:打印label2id和数据的唯一标签集合,逐一对齐。如果源码用sklearn.preprocessing.LabelEncoder,注意它的排序是字典序,不是出现顺序。
5.2 现象:推理时报RuntimeError: expected scalar type Long but found Float
原因:输入input_ids被转成了 float,或者标签类型不对。
解决:检查encoding['input_ids']的 dtype,确保是torch.long。标签如果是多标签,用torch.float32;单标签用torch.long。
5.3 现象:显存溢出,batch size 降到 1 还是 OOM
原因:法律文本太长,512 token 的 BERT 在反向传播时显存占用远超预期;或者源码里没有释放中间变量。
解决:用gradient_accumulation_steps模拟大 batch,或者换albert、distilbert等轻量模型。另外检查max_len是否被设成了 1024 但模型不支持。
5.4 现象:提交结果全是同一个标签
原因:类别极度不平衡,模型退化成只预测多数类。
解决:在损失函数里加pos_weight,或者对少数类过采样。BCEWithLogitsLoss(pos_weight=torch.tensor([...]))是常用做法,权重设为多数类样本数除以少数类样本数。
5.5 现象:本地跑通但比赛提交格式报错
原因:输出 JSON 的字段名、编码、换行符不符合要求。
解决:用json.dumps(..., ensure_ascii=False)写文件,每行一个 JSON 对象,最后一行也要换行。提交前用head -1 output.json | python -m json.tool验证格式。
6. 进阶技巧:用交叉验证和模型融合把 F1 再提两个点
比赛源码通常只给单折训练,但司法 AI 任务数据量不大,交叉验证能显著提升稳定性。我一般用 5 折,每折训练一个 BERT,推理时取平均概率。如果显存够,可以同时跑bert-base-chinese和roberta-base-chinese,两个模型的预测结果加权融合。
import numpy as np from sklearn.model_selection import KFold def cross_validate(data, n_splits=5): """5 折交叉验证,返回每折的验证概率""" kf = KFold(n_splits=n_splits, shuffle=True, random_state=42) oof_probs = np.zeros((len(data), num_labels)) for fold, (train_idx, val_idx) in enumerate(kf.split(data)): train_data = [data[i] for i in train_idx] val_data = [data[i] for i in val_idx] # 训练模型,保存最佳权重 model = train_model(train_data, val_data, fold) # 对验证集推理,填充 oof_probs val_probs = predict_proba(model, val_data) oof_probs[val_idx] = val_probs print(f"Fold {fold} done") return oof_probsoof_probs是 out-of-fold 概率,可以用来找最佳阈值,也可以作为 stacking 的输入。融合时,bert和roberta的权重按验证集 F1 分配,比如 0.6 和 0.4。注意,融合前要确保两个模型的标签顺序一致,否则会得到错误结果。
另一个技巧是对抗验证:如果比赛数据分布和训练数据差异大,可以训练一个二分类器区分训练集和测试集,找出差异大的样本,针对性补充数据。法律任务里,不同年份的裁判文书用词差异明显,对抗验证能帮你发现这种偏移。
我自己的习惯是:每次跑完比赛源码,先不急着改模型,而是把数据清洗、标签对齐、阈值调优这三步做扎实。很多队伍输在数据预处理上,而不是模型不够新。希望帮到你。
本文还有配套的精品资源,点击获取