简介:本资源面向自然语言处理方向的学生与开发者,提供一套基于BERT+BiLSTM+CRF的中文法律文书命名实体识别完整源码,聚焦交通肇事案件的事件要素抽取任务,可作为课程设计、期末大作业或NLP入门实战项目使用。压缩包共48个文件,约694KB,以21个Python脚本为核心,涵盖模型定义、数据加载、训练与预测流程,并配有xml配置、train/test/dev数据集、日志、pkl映射文件及README说明文档,结构清晰便于二次开发。目前已有190人学习下载。项目将预训练语言模型与序列标注架构结合,读者可据此掌握法律领域实体抽取的建模思路、数据预处理方式与训练评估流程,理解BERT词向量、BiLSTM上下文编码与CRF约束解码的协同机制,并可直接运行验证效果,快速完成从环境搭建到结果复现的完整实践。
1. 交通肇事案要素抽取:一份能直接跑起来的 BERT+BiLSTM+CRF 源码包
交通肇事案的卷宗里,办案人员真正关心的其实就那么几类东西:肇事时间、肇事地点、涉事车辆、伤亡人数、责任认定。人工从几百份判决书里抠这些字段,一天下来眼睛发花还容易漏。这份资源干的就是把这件事自动化——用 BERT 做字向量、BiLSTM 抓上下文、CRF 约束标签转移,把法律文书里的交通肇事要素按序列标注的方式抽出来。它是一份完整的 Python 工程,带训练、预测、评估脚本和预训练权重加载逻辑,不是只丢一个模型文件让你自己拼。适合正在做 NLP 课程设计、需要一份能跑通的法律领域 NER 基线、或者想拿交通肇事数据练序列标注的人。下面我按自己拆包复现的顺序,把这份源码从结构到跑通再到踩坑讲一遍。
2. 拆开压缩包先看什么:目录结构与模块职责
2.1 从文件清单反推工程分层
拿到一个陌生工程,我习惯先ls一遍再决定从哪个文件读起。这份包的顶层文件大致能分成四层:数据层、模型层、训练层、工具层。data目录放原始语料和标注文件,bert目录是预训练模型相关,model.py和rnncell.py定义网络结构,train.py、predict.py、load_pretrain_test.py是三个入口脚本,utils.py、data_utils.py、loader.py负责数据读取和批处理,conlleval.py是序列标注的标准评估脚本,config_file存超参,maps.pkl是标签到 id 的映射缓存。nerhup.py、nerhup_ori.py、nerhup_ori1.py是几个版本的训练主程序,LTP_NER.py看起来是接 LTP 做对比或辅助的脚本,download_electra.py是下载 ELECTRA 权重的辅助脚本。
先理清这个分层,后面改代码时才知道该动哪个文件。比如你要换标签体系,改的是maps.pkl的生成逻辑和data_utils.py;要换模型结构,动的是model.py;要调训练轮数和学习率,看config_file。
2.2 三个入口脚本的分工
train.py是训练入口,读配置、建模型、跑 epoch、存 checkpoint。predict.py是推理入口,加载训练好的权重对单条或批量文本做标注。load_pretrain_test.py是加载预训练权重做验证的脚本,通常用来确认 BERT 权重加载正确、标签映射没串。这三个脚本共用data_utils.py里的数据处理函数,所以数据格式只要对齐一次,三个入口都能用。
我一般先跑load_pretrain_test.py,因为它不涉及长时间训练,能快速验证环境、权重、标签映射三件事是否都对。这一步过了,再跑train.py才有意义。很多人上来就python train.py,结果报错在数据加载阶段,白白等半天。
2.3 依赖与运行环境确认
requirement.txt里列了依赖,常见的是torch、transformers、numpy、tqdm这几类。先建虚拟环境再装,别污染系统 Python。命令如下:
python -m venv venv_ner source venv_ner/bin/activate # Windows 用 venv_ner\Scripts\activate pip install -r requirement.txt逻辑说明:用独立虚拟环境隔离依赖,避免和系统里已有的 torch 版本冲突。参数说明:venv_ner是环境名,可自定义;source在 Linux/macOS 下激活,Windows 用对应脚本。装完后python -c "import torch; print(torch.__version__)"确认 torch 能导入。如果 requirement 里没锁版本,torch 和 transformers 的版本兼容要自己盯一下,这是后面避坑章会展开的点。
3. 数据怎么进模型:标签体系与批处理管线
3.1 法律文书的序列标注格式
序列标注任务的数据格式通常是「字 + 标签」逐行对齐,句子之间用空行分隔。交通肇事要素抽取的标签体系一般是 BIO 或 BMES 标注,比如B-TIME、I-TIME、B-LOC、I-LOC、B-VEH、I-VEH、B-CASUALTY、I-CASUALTY、O。data目录里的语料应该就是这个格式,data_utils.py负责把它读成 id 序列。
读数据前先确认两件事:标签集合是否和maps.pkl一致,句子长度是否超过 BERT 的最大长度限制。法律文书句子往往很长,一段事实描述可能几百字,直接截断会丢要素。常见做法是按句切分或滑窗,把长文本拆成不超过 512 的子段,再分别标注后合并。
3.2 标签映射与 maps.pkl 的生成
maps.pkl存的是标签到 id 的双向映射。如果语料标签变了而maps.pkl没重新生成,训练时会出现标签越界或全部预测成O。重新生成映射的典型逻辑如下:
# 从语料中收集所有标签,生成 label2id 和 id2label labels = set() with open("data/train.txt", encoding="utf-8") as f: for line in f: line = line.strip() if not line: continue parts = line.split() if len(parts) >= 2: labels.add(parts[-1]) label_list = sorted(labels) label2id = {lab: i for i, lab in enumerate(label_list)} id2label = {i: lab for lab, i in label2id.items()} import pickle with open("maps.pkl", "wb") as f: pickle.dump({"label2id": label2id, "id2label": id2label}, f)逻辑说明:遍历训练语料每一行,取最后一列作为标签,去重排序后建映射。参数说明:data/train.txt是训练语料路径,按实际改;parts[-1]假设标签在最后一列,如果你的格式标签在中间要调整索引。生成后训练和预测都用同一份maps.pkl,保证 id 对齐。这一步不做,后面评估出来的 F1 再高也是假的。
3.3 批处理与 padding 对齐
loader.py和data_utils.py负责把变长句子组成 batch。BERT 输入需要input_ids、attention_mask、token_type_ids,标签序列要和输入对齐,padding 位置用-100或忽略索引,避免参与 loss 计算。常见做法是用torch.nn.utils.rnn.pad_sequence或自定义 collate 函数。
from torch.nn.utils.rnn import pad_sequence def collate_fn(batch): input_ids = [torch.tensor(x["input_ids"]) for x in batch] labels = [torch.tensor(x["labels"]) for x in batch] input_ids = pad_sequence(input_ids, batch_first=True, padding_value=0) labels = pad_sequence(labels, batch_first=True, padding_value=-100) attention_mask = (input_ids != 0).long() return {"input_ids": input_ids, "attention_mask": attention_mask, "labels": labels}逻辑说明:把 batch 内变长序列 pad 到同一长度,padding_value=0对应 BERT 的[PAD],标签用-100让 CrossEntropy 忽略。参数说明:batch_first=True让输出维度是[batch, seq_len];attention_mask由非 pad 位置生成。这里如果 padding 值和 BERT 词表里的[PAD]id 不一致,attention 会算错,是隐蔽的翻车点。
4. 模型结构:BERT 出字向量,BiLSTM 抓上下文,CRF 管转移
4.1 为什么是 BERT+BiLSTM+CRF 这个组合
BERT 本身已经带上下文信息,为什么还要接 BiLSTM?因为 BERT 的输出是每个字的上下文表示,但序列标注需要的是「标签序列」的全局最优,BiLSTM 能在 BERT 输出之上再抽一层序列特征,CRF 则保证标签转移合法,比如I-TIME不会直接跟在B-LOC后面。这个组合在法律领域 NER 里是成熟基线,比单用 BERT+softmax 在实体边界上更稳。
model.py里应该能看到 BERT 输出接 BiLSTM 再接 CRF 的 forward 逻辑。CRF 层通常用torchcrf或自己实现转移矩阵。如果包里没带 CRF 实现,rnncell.py或model.py里会有转移分数计算。
4.2 模型 forward 的关键参数
class BertBiLSTMCRF(nn.Module): def __init__(self, bert_path, num_tags, lstm_hidden=256): super().__init__() self.bert = BertModel.from_pretrained(bert_path) self.lstm = nn.LSTM(self.bert.config.hidden_size, lstm_hidden, bidirectional=True, batch_first=True) self.fc = nn.Linear(lstm_hidden * 2, num_tags) self.crf = CRF(num_tags, batch_first=True) def forward(self, input_ids, attention_mask, labels=None): outputs = self.bert(input_ids, attention_mask=attention_mask) seq_out = outputs.last_hidden_state lstm_out, _ = self.lstm(seq_out) emissions = self.fc(lstm_out) if labels is not None: loss = -self.crf(emissions, labels, mask=attention_mask.bool()) return loss return self.crf.decode(emissions, mask=attention_mask.bool())逻辑说明:BERT 输出last_hidden_state送进双向 LSTM,再经全连接映射到标签数,最后 CRF 算 loss 或解码。参数说明:lstm_hidden=256是 LSTM 单向隐藏维度,双向拼接后是 512;num_tags要和maps.pkl的标签数一致;mask用 attention_mask 屏蔽 padding。这里num_tags对不上是最常见的报错来源,改标签体系后必须同步。
4.3 训练循环与 checkpoint 保存
train.py里的训练循环一般包含前向、loss 反传、优化器 step、定期评估。关键是把验证集 F1 作为保存 checkpoint 的依据,而不是只看 loss。
best_f1 = 0.0 for epoch in range(epochs): model.train() for batch in train_loader: optimizer.zero_grad() loss = model(batch["input_ids"], batch["attention_mask"], batch["labels"]) loss.backward() optimizer.step() f1 = evaluate(model, dev_loader, id2label) if f1 > best_f1: best_f1 = f1 torch.save(model.state_dict(), "result/best_model.pt")逻辑说明:每个 epoch 后在验证集上算 F1,只保存最优模型。参数说明:epochs从config_file读;evaluate用conlleval.py或自己实现,按实体级别算 P/R/F1。注意result目录要提前存在,否则torch.save会报路径错误。
5. 避坑与排查:跑这份源码最容易翻车的五个点
5.1 现象:训练 loss 一直不降,预测全是 O
原因:maps.pkl和当前语料标签不一致,或者标签 id 映射错位,模型学到的是错的对应关系。解决:删掉旧maps.pkl,用 3.2 的脚本重新生成,确认num_tags和模型输出维度一致,再重跑训练。
5.2 现象:报错 CUDA out of memory
原因:法律文书句子长,batch 内 padding 到最长序列,显存被撑爆。解决:把 batch size 调小,或按长度分桶(bucket)组 batch,让同 batch 内序列长度接近;也可以把 BERT 最大长度从 512 降到 256 配合滑窗。
5.3 现象:BERT 权重加载报 missing keys 或 unexpected keys
原因:bert目录里的权重和transformers版本不匹配,或权重文件不完整。解决:确认bert目录下有config.json、pytorch_model.bin、vocab.txt三件套;用BertModel.from_pretrained时路径指向目录而非单个文件;版本不匹配就按 requirement 锁定的 transformers 版本重装。
5.4 现象:评估 F1 很高但实际预测乱标
原因:评估时用了训练集,或者conlleval.py的输入格式和预测输出没对齐,标签和字错位。解决:评估必须用独立验证集;检查conlleval.py读入的每行是否是「字 预测标签 真实标签」三列,错位会导致虚高。
5.5 现象:predict.py 对单句预测结果为空
原因:输入文本没经过和训练一致的分字/编码流程,或者长度超过模型最大长度被截断到没有实体。解决:复用data_utils.py里的编码函数,保证input_ids生成方式和训练一致;长文本先切句再逐句预测,最后合并结果。
6. 进阶用法:换标签体系与接自己的数据
跑通默认流程后,真正有价值的是把它改成你自己的要素体系。比如你不想只抽时间地点车辆,还想抽「是否逃逸」「是否酒驾」,那就改标签集、重生成maps.pkl、改num_tags,再在data里按新标签重新标注一批语料。标注量不用很大,每个实体几百条就能看到效果,因为 BERT 预训练已经提供了很强的先验。
验证改动是否生效,我一般走三步:先用load_pretrain_test.py确认权重和标签映射加载正常;再用小样本过拟合测试,拿 20 条语料训练几十轮,看能不能在训练集上把 F1 打到 0.95 以上,打不到说明模型或数据管线有问题;最后才上全量数据训练。这个过拟合测试是我踩过坑之后养成的习惯,能快速区分「模型没学好」和「数据有问题」。
| 改动项 | 需要动的文件 | 验证方式 |
|---|---|---|
| 新增实体类型 | maps.pkl、data_utils.py、model.py 的 num_tags | 小样本过拟合 F1 |
| 换预训练模型 | config_file 里的 bert_path | load_pretrain_test.py |
| 调 LSTM 维度 | model.py 的 lstm_hidden | 验证集 F1 对比 |
| 改最大序列长度 | config_file、data_utils.py | 长文本预测完整性 |
从那以后我每次换标签体系,都强制先跑一遍小样本过拟合,确认管线通了再上全量,省下不少白等的训练时间。希望帮到你。
本文还有配套的精品资源,点击获取