简介:基于BERT+BiLSTM+CRF实现中文命名实体识别的Python源码,面向需要完成课程设计或期末大作业的高校学生,也适合正在学习自然语言处理与序列标注的开发者。项目覆盖数据预处理、模型训练到指标评估的完整流程,下载解压即可运行,无需修改配置。资源包共18个文件,核心为9个Python脚本,分别对应模型封装、数据处理、训练入口、验证评估等模块,同时附带Markdown说明文档、XML配置及3张结构示意图,便于对照代码理解整体架构。整个压缩包仅134KB,体量紧凑但内容完整,已有691人学习使用。代码封装了BERT_BiLSTM_CRF模型类、数据加载、训练及评估脚本,并配有模型结构图与运行效果图,目录清晰,适合快速上手和二次开发,也能帮助初学者深入理解预训练模型与条件随机场结合的NER实现思路,可作为课程设计答辩成果。
1. BERT+BiLSTM+CRF 的中文命名实体识别:直接能跑的 Python 课程设计源码
又到了课程设计周,题目是「基于深度学习的命名实体识别」。如果你抽到中文场景,大概率会在 BERT、BiLSTM、CRF 这三件套里打转。基本原理翻翻论文都能懂,但真要写一版能训练、能出 F1、能撑住答辩现场运行的代码,没两天时间下不来。这个源码包的价值就在这:模型结构、数据预处理、训练验证评估脚本全给齐了,下载下来把路径一配就能跑,适合把它当课程设计和期末大作业的主干工程,也适合拿来做 baseline 往自己的数据集上改。下面我按文件结构、数据流、训练复现、坑点排查、进阶改动的顺序,把这个包的代码拆开讲清楚。
2. 源码包拆解:六个关键模块,各管哪一段活
2.1 从文件清单看项目的运行主线
解压之后目录不长,但每个文件都有明确的职责边界。先看主路径上的核心文件:
| 文件/目录 | 职责 | 在整个流程中的位置 |
|---|---|---|
run.py | 推理入口,加载已有模型对新句子做预测 | 训练完成之后,演示/验收阶段 |
train_val_test.py | 训练、验证、测试三合一入口 | 使用阶段的最主要入口 |
data_helper.py | 数据读取、标签编码、BERT 分词、padding | 数据进入模型前的最后一站 |
models/BERT_BiLSTM_CRF.py | 完整网络结构定义 | 模型核心 |
models/rnncell.py | 自定义 LSTM/RNN 单元 | 被 BERT_BiLSTM_CRF 引用 |
base_config.py | 全局参数配置 | 所有脚本共用 |
utils.py | 小工具函数 | 各脚本通用辅助 |
conlleval.py | CONLL 标准评估脚本 | 训练后计算 P/R/F1 |
log/ result/ ckpt/ | 训练日志、预测结果、模型权重目录 | 输出产物 |
这个分工方式是典型的「课程设计友好型」:训练和测试合成一个入口,推理单独拆出来,这样答辩演示时运行run.py输入一句话就能看标注结果,不用重新走训练流程。train_val_test.py承担了三个职责,内部应该有一组 mode 参数来控制当前行为。
2.2 BERT_BiLSTM_CRF.py 的模型结构逻辑
BERT_BiLSTM_CRF.py是整个项目的核心文件,它的结构可以概括成三个层次。
第一层是 BERT 编码:中文文本经过 BERT 分词后得到 token ids,送入预训练模型拿到每个 token 的上下文表示。第二层是 BiLSTM 序列建模:把 BERT 的输出接一个双向 LSTM,由 LSTM 对每个 token 的上下文特征再提炼一轮。第三层是 CRF 序列解码:BiLSTM 的输出进线性层得到发射分数,CRF 利用转移矩阵约束标签序列的合法性。
模型对预训练部分和下游部分应该采用了不同的学习率。常见做法是 BERT 层用 2e-5 量级,因为微调幅度要小;BiLSTM 和 CRF 的线性层可以用 1e-3 量级。这样能避免预训练权重被大步长破坏,也保证下游新增层能够快速收敛。源码里默认冻结或不冻结 BERT 层,取决于 base_config.py 里的参数设定,训练前务必检查这个开关。
2.3 base_config.py 与 rnncell.py 的配角作用
base_config.py是全局参数出口。这类代码包我一般会先打开它,把所有路径、维度、学习率、迭代次数过一遍。典型配置项包括max_seq_len、batch_size、epochs、learning_rate、hidden_dim、num_labels、bert_pretrained_path等。num_labels一定要和数据标注的标签数量对齐,否则 CRF 层初始化就崩。
rnncell.py提供了 LSTM 单元的实现。这个文件的作用有两个:一是避免某些环境下torch.nn.LSTM的初始化方式不符合项目预期时不好排查,二是方便在 BiLSTM 的隐藏状态复用、dropout 位置上做微调。课程设计答辩时,这个文件很适合拿来解释「双向循环网络如何拼接前向和后向隐藏状态」,属于典型的答辩得分点。
整个源码包最重要的一条主线是:base_config定参数 →data_helper整理数据 →BERT_BiLSTM_CRF完成前向计算 →train_val_test.py控制训练循环 →conlleval.py出指标。后面所有复现步骤,都在这条线上展开。
3. 数据流设计:中文文本如何变成模型能吃的输入
3.1 标签体系与数据文件组织
课程设计的 NER 任务,数据格式通常遵循两个约定:一是标签体系,二是文件与字段排列。这个项目大概率使用的标签体系是 BIO 标注,B-表示实体开始、I-表示实体中间、O表示非实体。
一份标准的训练数据通常是三列:字符、标签、句子分隔符。类似下面这种组织方式:
李 B-PER 小 I-PER 明 I-PER 的 O 爸 O 爸 O 去 O 北 B-LOC 京 I-LOC 旅 O 游 O 空 O 调 O 维 O 修 O第一列为中文字符,第二列是 BIO 标签,空行表示句子结束。注意:这里数据预处理的工作量集中在「BERT tokenizer 切出来的 token 和标签如何一一对应」。BERT 的中文模型虽然基本按字切分,但遇到 [CLS]、[SEP]、特殊符号和英文片段时,token 数量会变化,标签对齐就成了一道边界问题。
3.2 data_helper.py 里的对齐逻辑
data_helper.py的处理流程,一般分成五步:读原始文本、生成 BIO 标签序列、调 BERT tokenizer 转 token ids、标签序列同步到 token 级别、padding 到固定长度。
def encode_example(sentence, labels, tokenizer, max_seq_len): # 第一步:BERT 分词,得到 token 级别的 ids 和实际 token 数 tokens = list(sentence) token_ids = tokenizer.convert_tokens_to_ids(['[CLS]'] + tokens + ['[SEP]']) # 第二步:标签也要带上 [CLS] 和 [SEP] 对应的占位 label_ids = [-1] + labels + [-1] # -1 在 loss 计算时会被忽略 # 第三步:截断到 max_seq_len - 1,留一个位置给 [SEP] token_ids = token_ids[:max_seq_len - 1] label_ids = label_ids[:max_seq_len - 1] # 第四步:padding 到固定长度,attention_mask 标记真实 token attention_mask = [1] * len(token_ids) padding_len = max_seq_len - len(token_ids) token_ids += [0] * padding_len label_ids += [-1] * padding_len attention_mask += [0] * padding_len return { 'input_ids': token_ids, 'attention_mask': attention_mask, 'label_ids': label_ids }这个函数的关键点在于:label_ids里用-1填充的位置,在损失计算时会被 mask 掉,不参与交叉熵计算。如果你自己在写类似逻辑时把 padding 位置的标签设成了 0(也就是O),CRF 会学习到一堆全是O的转移特征,指标看着高,实际换数据就翻车。
3.3 CRF 层在数据流里的位置
models/BERT_BiLSTM_CRF.py拿到data_helper的输出之后,前向计算分三段。BERT 部分产出sequence_output,BiLSTM 部分产出双向拼接后的lstm_output,线性层把隐藏维度映射到标签数量,产出emissions发射分数。CRF 层再接收emissions和真实标签label_ids,计算负对数似然损失。
import torch import torch.nn as nn def forward(self, input_ids, attention_mask, label_ids=None): # BERT 编码:shape = [batch_size, seq_len, hidden_dim] bert_output = self.bert(input_ids, attention_mask=attention_mask) sequence_output = bert_output[0] # BiLSTM 编码:前向和后向隐藏状态在最后一维拼接 lstm_output, _ = self.bilstm(sequence_output) # 线性层映射到 num_labels,得到发射分数 emissions = self.fc(lstm_output) # [batch_size, seq_len, num_labels] if label_ids is not None: # CRF 负对数似然损失 loss = self.crf(emissions, label_ids, mask=attention_mask.bool()) return loss else: # 推理模式:viterbi 解码 return self.crf.decode(emissions, mask=attention_mask.bool())attention_mask在这里是双向作用:BERT 端控制哪些 token 参与注意力计算,CRF 端控制哪些位置参与路径分数累计。我在调试中经常遇到一个现象:padding 位置的 mask 没传对,CRF 解码结果最后多出一串无意义的标签。排查方法很简单,打印一次attention_mask和label_ids,检查后面 padding 段的数值是不是 0 和 -1。
4. 从零到有:train_val_test.py 训练与评估完整流程
4.1 环境准备与依赖安装
这个项目依赖 PyTorch 和 Hugging Face Transformers。安装时要注意版本匹配:transformers 4.x 和 3.x 的 API 有差异,如果包里的代码用的是早期写法,装了最新版会报AttributeError。
# 创建虚拟环境,Python 建议 3.8 或 3.9 conda create -n ner python=3.9 conda activate ner # 安装核心依赖 pip install torch==1.13.0 pip install transformers==4.10.0 pip install tqdm scikit-learn如果网速不理想,可以给 pip 换国内源。我一般在命令行追加-i https://pypi.tuna.tsinghua.edu.cn/simple,速度会快很多。训练完成后的评估不需要 sklearn,但数据预处理阶段可能会用到,装上没坏处。
4.2 数据放置与路径配置
项目训练前必须先把数据放到train_val_test.py或base_config.py指定的路径下。默认结构通常是data/目录下放三个文件:train.txt、dev.txt、test.txt。打开base_config.py,找到路径相关的配置项:
# base_config.py 里的典型配置项 class Config: # 数据路径 train_data_path = "data/train.txt" dev_data_path = "data/dev.txt" test_data_path = "data/test.txt" # 模型参数 max_seq_len = 128 hidden_dim = 768 num_labels = 7 # 取决于你的标签体系 # 训练参数 batch_size = 16 epochs = 20 learning_rate = 2e-5 bert_learning_rate = 2e-5 crf_learning_rate = 1e-3 # 预训练模型 bert_pretrained_path = "bert-base-chinese"hidden_dim设为 768 是因为中文 BERT base 的输出维度就是 768,如果换用了 larger 版本,这个数字要跟着改。num_labels的赋值是最容易出错的,完整体验课设数据集的标签一般是B-PER/I-PER/B-LOC/I-LOC/B-ORG/I-ORG/O再加 padding 占位,具体以data_helper.py里的标签映射字典为准。
4.3 训练命令与运行参数说明
环境装好、路径配好之后,训练就一条命令:
python train_val_test.py --mode train训练过程会输出每个 epoch 的 loss,以及在验证集上的 P/R/F1。如果想调整训练精度,可以用--batch_size、--epochs、--learning_rate这些命令行参数覆盖base_config.py里的默认值。内存不足时优先把batch_size降到 8 或者 4,同时检查max_seq_len是否被设成了过长。
训练完成后,ckpt/目录下会生成模型权重文件,通常是pytorch_model.bin或者model.pt。log/目录下会有每个 epoch 的训练日志,答辩的时候直接贴 F1 曲线就行。
4.4 测试与评估:conlleval.py 怎么用
验证集上效果不错之后,跑一次正式测试:
# 测试模式:加载最佳模型,输出预测结果到 result 目录 python train_val_test.py --mode test # 用 conlleval 评估预测结果 python conlleval.py < result/pred_test.txtconlleval.py是经典的 CONLL 评估脚本,它会计算三种实体级别的指标:精确率、召回率、F1 值。这里有个细节:conlleval 对输入格式有严格要求,每行必须是「字符 正确标签 预测标签」的结构,三个字段用空格切分,句子之间用空行分隔。result/目录下生成的预测文件一般已经按这个格式排版了,但如果你自己写输出代码,一定要保证这三个字段不要换顺序。
评估列表参数的含义:
| 指标 | 含义 | 实体系别要求 |
|---|---|---|
| precision | 预测为实体的 token 中,真正是实体的比例 | 只看实体类型和边界都正确的 |
| recall | 真实实体中被预测出来的比例 | 漏报越少越好 |
| F1 | 两者的调和平均 | 课程设计一般看综合分 |
如果test.txt是未标注数据,那run.py就是演示入口。它的用法是在交互式命令行里输入一句话:
python run.py 输入:李小明在北京上班 输出:李小明/B-PER 北京/B-LOC5. 避坑清单:BERT 版本、标签错位和显存溢出的排查记录
5.1 transformers 版本升级后报 AttributeError
现象:加载 BERT 时提示AttributeError: 'BertModel' object has no attribute 'from_pretrained'或者module 'transformers' has no attribute 'BertForTokenClassification'。
原因:transformers 3.x 和 4.x 的 API 变化很大,最明显的区别是 4.x 之后一些类被移动到transformers.models子模块下,而代码里用的是旧版from transformers import BertForTokenClassification这种最原始的写法。升级到 5.x 之后,from_pretrained内部的参数签名也有调整。
解决:固定安装兼容版本。我装的是transformers==4.10.0,这个版本同时兼容BertModel和BertForTokenClassification的旧式导入。建议看一遍BERT_BiLSTM_CRF.py头部 import 语句,确认它用的是哪种写法,再对着写 requirements。
5.2 tokenizer 分词后标签数量对不上
现象:训练时 loss 是nan,或者在data_helper.py里报label length does not match。
原因:中文 BERT 基本按字切分,但遇到括号、数字、英文字母时可能切成子词,导致 token 数量大于原始字符数。这时候如果只按字符维度扩展标签,就会出现错位。
解决:在data_helper.py里加入对齐校验,tag 序列的每个位置都用tokenizer.convert_tokens_to_ids的结果长度做一次 assert。推荐用tokenizer.tokenize返回的 token 列表长度来生成标签序列,不要在外部手动拼接。做完这一层校验,模型训练阶段的绝大多数玄学问题都能排除。
5.3 显存溢出 OOM
现象:训练启动后没跑几步,报CUDA out of memory,特别是 batch_size 设得比较大时。
原因:BERT 系列模型显存占用本来就高,加上 BiLSTM 的中间激活值,显存峰值会出现在反向传播阶段。max_seq_len设成 256 时,每句 token 占用的显存几乎是 128 的四倍。
解决:按显存大小做一个经验对照,8G 显存用batch_size=8、max_seq_len=128,16G 显存用batch_size=16、max_seq_len=128。如果显存还是不够,可以把数据截断阈值调到 64。另一招是gradient_accumulation_steps,把一次大 batch 拆成多个小 batch 累计梯度,效果接近但显存压力低很多。
5.4 conlleval 脚本报错或指标全为 0
现象:conlleval.py输出Token number error,或者 P/R/F1 全部为 0。
原因:conlleval 严格要求每行三个字段,且字段间用空格分隔。很多同学在写结果文件时用了制表符,或者把 BIO 标签写成了B-PER/李小明这种拼接格式,conlleval 解析时完全无法识别。
解决:先检查result/pred_test.txt的前三行,确保格式和下面一致:
李 B-PER B-PER 小 I-PER I-PER 明 I-PER I-PER 的 O O字段之间只用空格。conlleval 第二个指标全为 0 还可能是标签名称不一致,比如模型输出了B-PER而评估脚本期望PER,打开脚本头部的实体类别集合看一眼就清楚了。
6. 进阶改动:换自己的数据、换标签体系、做对比实验的取舍
6.1 换自己的数据集,三个要点
课程设计如果不想用原始数据集,可以自己标注。数据文件保持三列结构,注意三个约定:标签用 BIO 体系、字符和标签之间用空格、句子之间用空行。换数据后要改的是data_helper.py底部的标签字典,保证label2id和num_labels同步变化。
6.2 BIO 换 BIOES:只改数据不换模型
如果想要提升效果,常见做法是 BIO 升级成 BIOES。训练数据和label2id里把I-拆成I-和E-,开头单实体用S-。CRF 对E-和S-的位置约束比I-更严格,对长实体的收尾判断更准。
6.3 对比实验:关掉 CRF 当消融基线
答辩时最稳妥的一步是做个消融对比。改法很简单,在BERT_BiLSTM_CRF.py的训练分支里,跳过 CRF 的负对数似然计算,直接把emissions和label_ids做 CrossEntropyLoss。拿这个结果和完整模型的 F1 对比,能清晰说明 CRF 带来的收益。
从那以后,我每次拿到新的课程设计或 GitHub 项目,第一件事不是训练,而是把base_config.py和data_helper.py里所有路径、标签数、padding 方式手动画一遍流程图,确认数据从文本变成 tensor 的每一步都在掌控中。这不是什么高深技巧,纯粹是踩多了坑之后的肌肉记忆。这个源码包整体结构完整,训练、验证、推理都有独立入口,适合直接跑通之后再做二次改造。希望帮到你。
本文还有配套的精品资源,点击获取