ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

法律文书要素识别:基于BERT的序列标注实战与避坑指南

2026/9/28 7:57:47 拓冰建站 浏览量
法律文书要素识别:基于BERT的序列标注实战与避坑指南 简介这是一份面向计算机相关专业毕业设计及课程设计的法律文书要素识别完整项目资源融合BERT、BiLSTM、注意力机制、CRF与LSTM解码器等深度学习与自然语言处理技术适合人工智能、计算机科学与技术专业学生用于课题研究、模型复现与实验对比。压缩包共110个文件以83个Python脚本为核心涵盖模型构建、训练与推理流程另含12个Markdown文档用于记录实验思路与技术说明以及配置、图片、文本等辅助文件整体大小约620KB结构清晰便于定位代码与文档。资源内提供项目源码、实验结果与配套论文代码已经严格测试验证可正常运行使用时可先阅读README或Markdown说明了解模块组织。目前已有60人学习下载。通过对照论文分析与实际运行代码读者可深入理解序列标注、命名实体识别及相关模型融合方法为后续开展实验或完成课设提供参考。1. 法律文书要素识别不是“分类”是一份判决书到手 5 秒出结构化要点把一份刑事判决书丢给大模型传统的做法是问“这是诈骗还是盗窃”这是文本分类。但毕设和课设里真正要解决的问题更细当事人是谁、罪名是什么、主刑几年、罚金多少、引用了哪几条法律。这五个问题挤在同一段话里分类模型给不出位置只有序列标注能回答。所谓“要素识别”本质就是把一段判决书原文中的每个字打上标签标出哪几个字属于当事人、哪几个字属于罪名、哪几个字属于刑期。这类任务的标准做法是选一个预训练语言模型做底座再接一个 token 级分类头用标注好的语料微调。标题里的“特定模型”在毕设场景下通常指你选定要对比的那个基座模型最常见的就是中文 BERT 系模型。它不需要你从零训练一个 transformer而是站在别人预训练好的中文语义底座上只训练最后那层分类头。这篇文章按这个主流路线展开从标注数据怎么造、训练代码怎么写、参数怎么调到实验结果和论文怎么对齐。适合两类人一类是毕设/课设选了这个题需要一条能跑通的全流程另一类是法院信息化、法律科技公司的初级工程师想快速验证“要素识别”这个需求能不能用低成本方案解决。2. 把判决书变成训练数据标注方案与预处理才是真正的工作量2.1 为什么选 BIO/BIEO 标注而不是直接问大模型法律文书要素识别在国内的竞赛和公开论文里主流框架一直是“预训练模型 序列标注”不是现在流行的“直接把整篇文书丢给大模型让它抽取”。后者的问题是输出不稳定同一个罪名今天输出“诈骗罪”、明天输出“诈骗”实体边界忽宽忽窄论文里没法写定量结果。序列标注则把问题变成确定的数学形式给每个字一个标签训练目标就是最大化标签序列的概率。我一般用 BIEO 标注体系B 表示实体开头I 表示实体中间E 表示实体结尾O 表示非实体。比 BIO 多了一个 E好处是实体边界更明确评估的时候不用再靠后处理猜结尾。每个实体类型需要 B、I、E 三套标签加上一个 ON 类实体就是 3N1 个标签。如果你的课设只做当事人、罪名、刑期、罚金、法条这 5 类标签总数就是 16 个这个规模用 BERT-base 完全扛得住。2.2 标签集合怎么设计才不会被答辩老师挑毛病标签设计直接决定模型上限。我建议按“主体 行为 量化结果 法律依据”四个维度去拆而不是直接照抄论文里的标签。法律文书里最容易被混淆的是“刑期”和“罚金”前者单位是年/月后者单位是人民币如果两套标签都用 B-SENTENCE模型大概率会混。我一般拆成 6 类实体实体类型标签前缀示例原文片段说明当事人B-PER / I-PER / E-PER被告人张三不区分原被告统一用 PER罪名B-CRIME / I-CRIME / E-CRIME犯诈骗罪注意“罪”字算在实体内主刑B-SENTENCE / I-SENTENCE / E-SENTENCE判处有期徒刑三年只标刑种和刑期数字罚金B-FINE / I-FINE / E-FINE并处罚金人民币五千元数字和单位都算实体法条B-LAW / I-LAW / E-LAW依照《刑法》第二百六十六条引用的完整条款金额B-AMOUNT / I-AMOUNT / E-AMOUNT诈骗数额共计人民币十二万元注意和罚金区分这里最容易被低估的是标签一致性。如果标注规范里没有明确规定“罪”字是否进实体、金额数字和单位是否算一个实体两个人标同一份文书产出的标签序列对不上模型训练时同一个位置的同一个字一会儿是 O 一会儿是 B永远收敛不好。所以我建议在动手编训练集之前先写一页标注规范哪怕只是给标注的同伴看能省掉后面 70% 的返工。2.3 预处理脚本从原始文书到 jsonl 训练样本拿到裁判文书网的公开文书后第一步不是标数据而是把无用的头尾、案号、落款去掉再做分句。法律文书特点是句子特别长动辄一两百字直接整篇进模型不现实。我常用的预处理流程是按句号分句每句切成一个样本切完只保留含实体关键词的句子比如包含“判处”“犯”“依照”的句子没有这些词的段落直接丢。这一步能把训练集缩到原来的三分之一训练时间大幅下降。下面这段 python 源码是预处理的核心逻辑按句号分句、过滤无用行、输出成 jsonlimport re import json def split_and_filter(raw_text: str) - list[str]: # 去掉判决书头部和落款只保留“经审理查明”之后、“如不服本判决”之前 body re.split(r经审理查明|经查, raw_text)[-1] body re.split(r如不服本判决, body)[0] # 按句号、分号、问号切分 sentences re.split(r[。\n], body) # 只保留包含实体触发词的句子 keywords [判处, 犯, 依照, 罚金, 诈骗, 盗窃] filtered [] for sent in sentences: sent sent.strip() if not sent: continue if any(k in sent for k in keywords): filtered.append(sent) return filtered def to_jsonl(sentences: list[str], label: str, out_path: str): # label 是这条样本的人工标注结果格式为 list of (start, end, entity_type) with open(out_path, w, encodingutf-8) as f: for sent in sentences: record { text: sent, label: label, # 占位后续用标注工具替换 source: court_doc } f.write(json.dumps(record, ensure_asciiFalse) \n) if __name__ __main__: raw open(raw_judgment.txt, encodingutf-8).read() sents split_and_filter(raw) to_jsonl(sents, [], train_raw.jsonl) print(f过滤后剩余 {len(sents)} 条句子)这段代码里最关键的是触发词列表。刑事判决书的高频实体几乎都跟着“犯”“判处”“罚金”“依照”这几个动词走按触发词过滤能保留绝大多数正样本同时把“公诉机关指控”“辩护人认为”这类无实体段落丢掉。要注意re.split(r经审理查明|经查, raw_text)[-1]取的是最后一个分段如果一份文书里出现了两次“经查”可能会误取稳妥做法是取第一次出现后的所有文本代码里[-1]改成[1]更安全。2.4 数据量多少够用最小值不是硬指标实体覆盖才是很多同学问“我需要标多少条”这个问题其实没有绝对答案。我做过最小规模是 3000 句、约 12000 个实体BERT-base 微调出来整体 F1 能做到 82% 上下。但前提是 6 类实体都要有足够的样本特别是罚金和金额如果只有几十条F1 会掉到 60% 以下。所以准备数据时的检查点是每一类实体的标注条数是否超过 300。没超过就给这类实体加写规则数据或者去裁判文书网多捞几份。另外要注意样本的时间分布。法律用语这几年变化不大但罪名表述会随司法解释调整比如帮信罪相关判决书 2021 年后暴增。如果你的训练数据集中在 2019 年验证集用的是 2023 年的文书要素分布差异会导致成绩虚高或虚低。我一般会按年份切分训练集和测试集训练集用旧文书测试集用新文书这样论文里的评测结果更有说服力。3. 模型选型与训练脚本从“特定模型”落到可运行的 python 代码3.1 “特定模型”怎么选默认从中文 BERT 起步别一上来就追大模型标题里的“特定模型”在毕设答辩时会被追问“为什么选它”。我建议的默认答案是中文 BERThuggingface 上的bert-base-chinese理由有三条。第一它是在大规模中文语料上做过掩码语言模型预训练的对法律文书这种半文半白的文本已经有不错的语义理解微调只需要很少的标注数据。第二它是 transformer 结构的典型代表论文里画模型结构图方便老师容易看懂。第三显存门槛低6GB 显存就能训练实验室的老显卡也能跑。如果你想要更高上限可以用hfl/chinese-roberta-wwm-ext它用全词掩码训练对中文实体边界更友好训练成本几乎一样。不要一上来就追大参数模型。低显存运行模型这条路我走过用 7B 参数的大模型做零样本抽取推理一次要几十秒输出还不稳定论文里完全没法做消融。而 BERT-base 参数量 1.1 亿推理一条判决书只要 0.10.3 秒在 CPU 上也能跑这才是毕设和课设要的稳定性。3.2 最小训练脚本手写训练循环而不是套 Trainer很多教程让你直接用 Hugging Face 的Trainer几行代码就能跑。我不推荐在毕设场景用它原因是你需要控制每一轮的学习率、梯度累积、标签平滑Trainer封装太厚出了问题不好排查。手写一个训练循环40 行代码而已但你能在论文“实验设置”一节写清楚每一个参数的含义。下面是完整的最小训练代码import torch from torch.utils.data import DataLoader from transformers import ( AutoTokenizer, AutoModelForTokenClassification, AdamW, get_linear_schedule_with_warmup, ) from seqeval.metrics import classification_report def train_one_epoch(model, dataloader, optimizer, scheduler, device): model.train() total_loss 0.0 for batch in dataloader: input_ids batch[input_ids].to(device) attention_mask batch[attention_mask].to(device) labels batch[labels].to(device) outputs model( input_idsinput_ids, attention_maskattention_mask, labelslabels, ) loss outputs.loss loss.backward() # 梯度裁剪防止长句子的梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() scheduler.step() optimizer.zero_grad() total_loss loss.item() return total_loss / len(dataloader) if __name__ __main__: device torch.device(cuda if torch.cuda.is_available() else cpu) tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) model AutoModelForTokenClassification.from_pretrained( bert-base-chinese, num_labels16 ).to(device) # id2label 映射要和你标注规范里定义的顺序一致 id2label {0: O, 1: B-PER, 2: I-PER, 3: E-PER, 4: B-CRIME, 5: I-CRIME, 6: E-CRIME, 7: B-SENTENCE, 8: I-SENTENCE, 9: E-SENTENCE, 10: B-FINE, 11: I-FINE, 12: E-FINE, 13: B-LAW, 14: I-LAW, 15: E-LAW} model.config.id2label id2label model.config.label2id {v: k for k, v in id2label.items()} # dataloader 和 optimizerwarmup 设为总步数的 10% train_loader DataLoader(train_dataset, batch_size16, shuffleTrue) optimizer AdamW(model.parameters(), lr2e-5) total_steps len(train_loader) * 5 scheduler get_linear_schedule_with_warmup( optimizer, num_warmup_stepsint(total_steps * 0.1), num_training_stepstotal_steps ) for epoch in range(5): loss train_one_epoch(model, train_loader, optimizer, scheduler, device) print(fepoch {epoch 1}, loss {loss:.4f}) torch.save(model.state_dict(), fmodel_epoch_{epoch 1}.pt)这段代码有三个参数值得细说。clip_grad_norm_的max_norm1.0是经验值法律文书的句子长梯度范数经常超过 10不裁剪的话第三四个 epoch 会出现 loss 突然变成 NaN并且没有回滚手段。learning_rate2e-5是 BERT 微调的安全区超过 5e-5 后模型容易灾难性遗忘预训练知识表现在指标上就是第一轮掉点、后面再也涨不回去。num_labels16必须和你的标签集合数量严格一致少一个训练直接报错多一个会让标签分布统计出现偏差输出层也会多出无意义的概率分布。3.3 低显存运行模型的四个关键参数如果你只有 4GB 显存batch_size 16 直接 OOM。这时候不要换模型先调这四个参数max_length256截断长句、batch_size 降到 8、开启gradient_accumulation_steps2每两步累加一次梯度再更新、必要时用fp16True混合精度。这四招组合下来显存占用能砍掉 60% 以上而 F1 损失通常在 1 个百分点以内。注意 fp16 在旧版 transformers 上需要显存大于 3GB 才有收益太小的卡反而因为频繁精度转换变慢。3.4 训练中怎么判断模型该停了看 loss 不够看验证集实体级 F1很多同学只看训练 lossloss 降了就以为万事大吉。实际上序列标注的过拟合很有迷惑性训练 loss 一路下降但验证集实体级 F1 可能在第 3 个 epoch 就开始掉因为模型记住了训练集里“张三”这个人名遇到“李四”就不知所措。所以训练时每个 epoch 结束都要跑一次验证集用 span 级指标判断而不是 token 级准确率。具体怎么算下一章展开。4. 参数调优与实验结果呈现让答辩老师一眼看懂你做了什么4.1 五个必调参数先动学习率其他按表抄我把调参顺序固定下来避免每次训练全凭手感。第一步调学习率第二步调 epoch第三步动max_length第四步才考虑是否加数据增强。不要一开始就同时改三个参数不然论文里根本说不清是谁起了作用。下表是我在类似法律文本任务上的经验范围和推荐值参数经验范围推荐起点调参方向learning_rate1e-5 ~ 5e-52e-5loss 震荡就降到 1e-5收敛慢就涨到 3e-5epoch3 ~ 65验证 F1 在最后一个 epoch 还在涨就加 1max_length128 ~ 512256长句被截断导致实体边界残缺时上调batch_size8 ~ 3216显存不够时先降这个不要降 max_lengthwarmup_ratio0.05 ~ 0.20.1小数据集用 0.2防止第一轮把模型冲垮这里最容易踩的是max_length和法律文书句子长度的匹配。判决书里经常出现一个分号长句超过 300 字如果max_length128“判处有期徒刑三年”这个实体可能直接被截断成“判处有期徒刑”甚至只剩“判处”两个字模型再怎么调都学不出来。我建议先跑一段统计脚本看训练集句子的长度分布取第 95 百分位作为max_length而不是拍脑袋设 128。4.2 评估指标的坑token 级准确率好看但答辩老师只看实体级 F1序列标注的标准评估是计算每个实体类型的精确率、召回率、F1按实体边界对齐来算用seqeval库。touken 级准确率能到 95% 以上但实体级 F1 可能只有 80%两者之间差距大的原因大部分是实体边界错误——模型把“有期徒刑三年”里的“有期”识别成 B-SENTENCE剩下“徒刑三年”归为 Otoken 级只错了一个标签实体级整个实体都没了。所以我在实验章节永远只放实体级指标这才和“要素识别”这个任务目标对齐。from seqeval.metrics import classification_report def evaluate(model, dataloader, id2label, device): model.eval() true_labels [] pred_labels [] with torch.no_grad(): for batch in dataloader: input_ids batch[input_ids].to(device) attention_mask batch[attention_mask].to(device) labels batch[labels].to(device) logits model(input_idsinput_ids, attention_maskattention_mask).logits preds torch.argmax(logits, dim-1).cpu().numpy() for seq_pred, seq_true, mask in zip(preds, labels.cpu().numpy(), attention_mask.cpu().numpy()): pred_seq [] true_seq [] for p, t, m in zip(seq_pred, seq_true, mask): if m 0: continue pred_seq.append(id2label[p]) true_seq.append(id2label[t]) pred_labels.append(pred_seq) true_labels.append(true_seq) # classification_report 输出每一类实体的 P/R/F1 以及整体的 macro F1 report classification_report(true_labels, pred_labels, digits4) print(report)这段评估代码里有两个容易被忽略的设置。第一attention_mask为 0 的位置必须跳过否则 padding 部分会被算进标签序列拉高或拉低指标第二classification_report默认按实体级计算即只有当整段预测的实体和真实实体完全重合时才记为正确部分重合算错这和微平均的 F1 有本质区别。我建议把这段代码输出的报告直接写进论文附表包含 PER、CRIME、SENTENCE、FINE、LAW、AMOUNT 六行的 P、R、F1答辩时一目了然。4.3 实验结果表怎么排要有基线、有消融、有单类明细论文里的实验结果表要回答三个问题比随机好多少、每个模块有没有用、哪类实体最难。我通常排三张表。第一张是整体对比表列 baseline规则抽取、BERT-base、BERTCRF可选、本文模型四项的 macro F1 和推理耗时。第二张是消融表去掉预训练、去掉 CRF 层、去掉数据增强每个配置跑一遍证明每个环节都有贡献。第三张就是上面代码输出的单类明细表。单类 F1 有一个常见规律当事人的 F1 最高通常超过 90%罪名次之罚金和金额最容易混淆F1 可能只有 70% 出头。如果答辩时老师问“为什么罚金类 F1 低”标准回答是罚金的表述变体太多“罚金人民币五千元”“并处人民币五千元罚金”“罚金5000元”同一个意思三种词序训练数据里这类变体覆盖不足。这个回答比“模型不够好”有说服力得多也说明你在数据层面做过分析。5. 法律文书要素识别避坑指南现象、原因、解决5.1 长句子把“罪名”拦腰截断F1 直接掉 15 个点这是我在低显存跑模型时遇到的第一坑。为了把显存压到 4GB我把max_length设成 128训练集里的长句大多没截断但总有 10% 的句子超过 200 字结果就是“诈骗罪”的“诈”在 127 的位置“骗罪”在 128 之后全被扔掉。模型看到的是“被告人张 犯诈”标签是 B-CRIME语义残缺。解决方法是统计训练集长度分布把max_length调到覆盖 95% 句子的长度值同时配合truncation_sideleft因为判决书里实体通常出现在句子后段截断开头保留结尾比分两侧截更合理。如果显存实在不够用滑窗切片把长句切成重叠的两段保证实体不跨窗口这个技巧比直接截断多保住 3 个点的 F1。5.2 罚金和金额永远分不清原因是标注规范里没定义排除规则“并处罚金人民币五千元”里“五千元”既属于罚金又属于金额“赃款人民币十二万元”里“十二万元”是金额但不是罚金。如果标注规范里没有明确规定“罚金实体内部不再单独标金额”模型会学到“人民币”后面跟数字就是 B-FINE结果金额类的标签全部污染。解决方法是明确实体互斥规则金额实体只在非罚金语境下标注罚金实体内部不允许再出现 AMOUNT 标签。这个规则写进标注规范后把已有标注数据跑一遍校验脚本找出同时标了两个标签的样本批量清理。这类规则矛盾在数据准备阶段就要解决不要指望模型自己学会区分。5.3 同一份文书换个版本结果忽高忽低模型训练没固定随机种子有一次我连续跑了两次同样的训练脚本发现 F1 差了 4 个百分点一开始以为是数据加载顺序问题后来定位是没设随机种子。PyTorch 的 DataLoader 默认打乱顺序、Dropout 的随机性、甚至设备上的非确定性卷积算法都会让模型收敛到不同的局部最优。解决是在训练脚本开头固定三处种子torch.manual_seed(42)、random.seed(42)、np.random.seed(42)另外DataLoader里设置worker_init_fn用同一个种子。固定种子后多次运行结果波动应该控制在 0.5 个百分点以内。论文里一定要写明随机种子否则别人复现不出来答辩时解释不清。5.4 法条识别把“第二百六十六条”后半截丢掉标签没包含句子边界法条实体的边界不像人名那么清晰。“依照《刑法》第二百六十六条之规定”这个完整引用里模型容易只标出“第二百六十六条”而漏掉前面的《刑法》。原因是训练标注时不同标注员对“法条实体”的理解不一致有人标“《中华人民共和国刑法》第二百六十六条”全称有人标“第二百六十六条”简称标签序列对不齐。解决是在标注规范里强制要求法条实体必须包含法律名称和条号全称和简称都算合法实体但必须保持统一的边界约定。然后对训练集做一致性检查把“《刑法》第二百六十六条”这种只标条号不标法律名的样本全部重标。5.5 验证集 F1 高但实际文书效果差训练集和测试集来自同一批案件这是我自己踩过最深的坑。我从裁判文书网一次性下载了 2000 份文书随机切 80% 训练、20% 测试结果实体级 F1 高达 91%但拿另一批新下载的文书一测直接掉到 75%。原因是一批文书里的当事人、金额分布高度相似很多“实体”其实就是同一个字符串换着位置出现模型记住了字符串而不是语义。解决是严格按案号去重确保同一案件的文书不会同时出现在训练集和测试集最好按案号所在年份切分老案件训练、新案件测试。论文的实验设置里要把这个划分方式写清楚否则就是数据泄漏这是答辩老师最喜欢抓的问题。6. 最后一公里推理脚本加置信度阈值让要素识别结果可直接交差模型训好后交付给用户的是一个函数输入一段判决书原文输出结构化的要素列表。这里有个容易被忽略的点模型会给每个字预测一个标签但低置信度的位置不该硬着头皮输出。我一般在最后加一个置信度阈值softmax后最大概率低于 0.6 的 token 强制置为 O这样能过滤掉一大部分不稳定的边界字。阈值调高精度上升召回下降调低则反过来。我建议在验证集上画一条 P/R 随阈值变化的曲线取交叉点附近的值法律文书场景一般 0.550.65 之间。推理脚本和训练脚本的最大区别是输入不再是一句话而是一整篇判决书。所以先用第 2 章的split_and_filter切句逐句推理再把同一份文书的实体按出现顺序拼装这一步能避免长文本超长导致的截断问题。代码里我会顺手做两层兜底规则第一层正则匹配“判处有期徒刑X年”“罚金人民币X元”这类固定句式把模型的漏检补上第二层如果模型输出的实体和规则抽取冲突以两边边界较长的那个为准大部分情况下模型预测更准但金额和刑期这两类规则兜底反而更稳。最后一步是把结果序列化成表格。我建议导出成 JSON 或 CSV字段固定为文书ID、实体类型、实体文本、起始位置、置信度这样论文里的样例展示、后续人工复核、指标统计都用同一份文件避免“实验结果”和“代码实际输出”对不上。这个对齐问题在毕设答辩里出现过太多次论文里写了诈骗罪 F188.9%但演示时模型输出的是“诈骗”少了一个“罪”字当场被提问。所以我在实验阶段就开始统一口径凡是模型预测出的实体文本边界必须经过一个归一化函数把“诈骗”“诈骗罪”“诈骗罪一案”统一到标注规范里规定的标准形式再去算指标。做到这一步你的要素识别系统才算真正“能交差”。以上这些坑大部分是我自己在课设阶段一个脚印一个脚印踩出来的。序列标注的门槛不在模型而在数据和对齐这两件脏活上的耐心。希望帮到你。本文还有配套的精品资源点击获取