ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

BERT-BiLSTM-CRF中文命名实体识别实战:从数据加工到模型训练全流程

BERT-BiLSTM-CRF中文命名实体识别实战:从数据加工到模型训练全流程 简介面向高校计算机相关专业学生和中文自然语言处理入门者该资源交付基于BERT-BILSTM-CRF的中文命名实体识别完整方案涵盖数据预处理、标签生成、模型训练与预测推理全流程。项目以dgre等数据集为例提供数据转换脚本process.py、模型结构定义model.py、训练验证模块main.py、预测接口predict.py并配有依赖清单和使用说明可直接服务于毕业设计、课程设计或项目初期演示。资源压缩包共20个文件其中6个Python脚本承载核心逻辑8个JSON配置便于调整参数5个文本文件提供标签与数据样例1份说明文档讲解运行步骤整体仅1.03MB小巧易用。该资源已有1250人学习下载代码经过运行测试适合计算机、人工智能、通信、自动化等专业学生使用。即使基础一般也可按照说明完成从环境安装、预训练模型下载到自定义语料训练的完整链路配置文件集中管理序列长度、batch size、epochs等关键参数方便在有限GPU资源下调整优化适合作为工程原型和进阶学习参考。1. 基于BERT-BILSTM-CRF的中文命名实体识别从训练代码到可复现的完整闭环还没正式跑通这个源码包之前我一度怀疑“BERT-BILSTM-CRF”这个组合是不是被毕业论文写烂了网络上铺天盖地的教程十个里有八个卡在预训练模型下载这一步剩下两个又死在标签映射错乱上。真正把这个包完整跑下来之后我的看法变了。这份资源把中文命名实体识别从理论拆成了一个可以照着改的闭环DGRE与DuIE两套数据、BIO标注的加工脚本、可调的config参数、训练到预测的完整流程都有了。它适合两类人一类是把毕业设计或课程大作业当正式工程做的学生另一类是手头有一批中文文本、想快速出一版NER基线验证效果的在职开发者。下面不聊框架宏观意义直接讲这个包怎么拆、怎么用、坑在哪。2. 认识这份NER资源目录结构、BIO数据格式与模型管线选型2.1 目录结构与各模块的职责先把整个包的结构摊开看这份资源的目录设计其实非常贴近我平时做 NLP 工程的习惯数据预处理、模型定义、训练入口、预测入口四个部分完全分离改任何一个模块都不需要动其他文件。下面是解压后的核心目录树--checkpoint模型和配置保存位置 --model_hub预训练模型 ----chinese-bert-wwm-ext: --------vocab.txt --------pytorch_model.bin --------config.json --data存放数据 ----dgre --------ori_data原始的数据 --------ner_data处理之后的数据 ------------labels.txt标签 ------------train.txt训练数据 ------------dev.txt测试数据 --config.py配置 --model.py模型 --process.py处理ori数据得到ner数据 --predict.py加载训练好的模型进行预测 --main.py训练和测试这个结构里有一个容易被忽略的点model_hub下的chinese-bert-wwm-ext是哈工大发布的基于全词掩码Whole Word Masking的中文 BERT 预训练模型。它不是简单的中文 BERT-base而是在预训练阶段把“一个词的所有字同时掩码”对中文这种词边界模糊的语言来说下游 NER 任务的语义表征会比原始 BERT 更稳。vocab.txt是词表config.json是模型结构参数pytorch_model.bin是权重文件三者缺一不可。data下按数据集分目录每个数据集内部又分ori_data和ner_data。ori_data是原始语料格式比较杂ner_data是经过process.py加工后的标准 BIO 格式。checkpoint目录存放训练好的模型和ner_args.json配置快照这个 JSON 很实用因为模型训练完后还能看到当时用了哪些超参数。2.2 BIO标注train.txt里的每一行JSON这份资源里 NER 数据采用 BIO 标注体系B 表示实体的开始BeginI 表示实体的内部InsideO 表示非实体。以 dgre 数据集为例标签只涉及两类实体故障设备和故障原因。训练文件里每一行是一条完整的样本以 JSON 格式存储字符序列和标签序列严格对齐{id: AT0001, text: [6, 2, 号, 汽, 车, 故, 障, 报, 告, 综, 合, 情, 况, :, 故, 障, 现, 象, :, 加, 速, 后, , 丢, 开, 油, 门, , 发, 动, 机, 熄, 火, 。], labels: [O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, O, B-故障设备, I-故障设备, I-故障设备, B-故障原因, I-故障原因, O]}这里text是按字拆开的列表labels与之一一对应。“发动机”三个字被打成了B-故障设备, I-故障设备, I-故障设备“熄火”则是B-故障原因, I-故障原因。labels.txt文件内容只有两行“故障设备”和“故障原因”它是标签集合训练时标签到 ID 的映射顺序完全由这个文件决定。我在做 NER 数据验收时有个习惯拿到train.txt先随机抽几行检查每个非 O 标签对应的字符是不是一个完整词。这份资源的样本做得比较规整但如果你要换自己的数据集这一步必须做。原因很简单BIO 标注里如果 B 后面跟的不是 I 而是 O模型拿这种数据训练CRF 层的转移矩阵会学出一个稀烂的概率分布。2.3 为什么偏偏是BERT-BILSTM-CRF这套组合这套结构在深度学习 NER 里几乎是教科书级的配置但它之所以被反复使用是因为三个模块各自解决了不同层面的问题。BERTDevlin 等人在 2019 年提出的预训练模型负责把每个字编码成包含上下文信息的向量表示对于“发动机”这种在故障报告里语义相对固定的词BERT 能根据整句话的语境给出动态词向量而不是像 Word2Vec 那样不管放哪里都是同一个向量。BILSTM 接在 BERT 后面作用是双向建模。字级别的 BERT 输出已经带了上下文信息但 BiLSTM 会再按时间顺序把序列从左到右、从右到左各扫一遍把当前位置的前后文信息进一步融合相当于在 BERT 之上加了一层序列建模的“精修”。CRF 层放在最后它不改变每个位置的标签概率而是学习标签之间的转移约束。举个例子CRF 会自动学到“B-故障设备”后面大概率接“I-故障设备”但基本不可能接一个“O”这种约束让预测出来的标签序列整体合法而不是每个位置单独取最大概率导致 B 后面跟 I 的实体断成两截。对比一下用 BERT Softmax 做序列标注的常见误用Softmax 在每个位置独立分类模型预测出“B-故障设备/I-故障原因”这种非法组合都没人拦着CRF 相当于加了一道全局保险。尤其对于“故障设备”这种经常连续占三到四个字的实体CRF 对标签序列合法性的约束能实打实拉高 F1 值。这也是为什么即便 BERT 本身已经很强大这份资源仍然保留了 BiLSTM 和 CRF 两个模块。3. 从原始语料到训练数据process.py的加工逻辑与参数调整3.1 原始数据长什么样原始语料放在data/dgre/ori_data下格式是带实体标注的自然语言报告不是现成的 BIO 序列。例如一份汽车故障报告文本中包含故障现象、故障设备、故障原因等描述原始文件里用类似 JSON 或键值对的方式记录了“哪句话里哪个实体属于什么类型”。任务就是把这些实体和类型转成与文本字符逐位对齐的 BIO 标签。另一份duie数据集的原始格式略有不同它是面向关系抽取构建的中文数据集实体类别和 dgre 不一样但process.py的处理框架是通用的只需要在脚本里修改实体读取规则即可。项目说明里特意强调“这里以dgre数据为例其余数据类似”我实际测试下来duie 数据的处理逻辑比 dgre 还要简单因为它的实体边界在原始标注里给得更清晰。3.2 process.py加工流程详解process.py的核心功能是把原始语料转换成第 2.2 节展示的 JSON 行格式。下面这段代码是处理 dgre 原始数据时最核心的转换逻辑我为了说明在原来脚本基础上做了精简def convert_ner_data(ori_path, out_path): # 读取原始数据每行一条样本 with open(ori_path, r, encodingutf-8) as f: lines f.readlines() samples [] for line in lines: item json.loads(line) text item[text] # 原始文本 entities item.get(entities, []) # 实体列表每个实体包含类型和位置 # 初始化标签序列全部置为 O长度与字符数严格一致 chars list(text) labels [O] * len(chars) for ent in entities: entity_text ent[entity] # 实体的字面内容 entity_type ent[type] # 实体类型如故障设备 start text.find(entity_text) # 找到实体起始位置 # 从第二个字符开始打 I- 标签第一个字符打 B- 标签 labels[start] B- entity_type for i in range(start 1, start len(entity_text)): labels[i] I- entity_type samples.append({ id: item[id], text: chars, labels: labels }) # 写出为 JSON 行格式 with open(out_path, w, encodingutf-8) as f: for s in samples: f.write(json.dumps(s, ensure_asciiFalse) \n)这段代码的逻辑分四步先把文本拆成字符列表并生成全 O 标签再遍历样本里的实体信息用text.find定位实体在句子里的起始下标然后从起始位置开始连续打 B- 和 I- 标签最后把所有字符和标签打包成一条样本写入文件。实际使用时有几个参数需要按自己的数据改。entity_text来自原始标注如果原始数据的实体字段名不叫entity比如叫name或value就需要在读取时对应修改。text.find(entity_text)得的结果是第一个匹配位置如果同一实体在句子中出现多次这段代码只能标注第一处后面出现的全部漏标我在第 5 章避坑部分会细讲这个坑。另外labels.txt是从这里生成还是手工维护我一般建议用脚本扫描所有实体类型自动生成并排序保证 ID 映射稳定。3.3 数据加工后必须做的三件事跑完process.py后别急着训练我每次都会顺手做三个动作。首先是检查train.txt和dev.txt的行数比例NLP 任务里 8:2 或 9:1 的训练验证划分是常见做法如果原始脚本是 9:1但你的语料更少建议改成 8:2 让验证集多一些样本。其次是验证字符和标签长度一致。写一段简单的代码遍历train.txt对每条样本断言len(text) len(labels)长度不一致的样本直接打印出来并剔除。这个操作看似多余实际上 BIO 数据里大量训练崩溃都源于某一条脏数据的标签比文本短一截模型训练时 embedding lookup 直接越界。最后是统计标签分布。用collections.Counter扫一遍所有标签看 B- 和 I- 标签数量是否在同一量级如果 B-故障设备有 500 个但 I-故障设备只有 200 个说明很多实体只有一个字长对于中文实体来说不太正常大概率是标注遗漏。这个统计同时也是在下游训练前对数据质量的一轮体检。4. 训练与验证config.py参数配置与main.py启动细节4.1 关键参数怎么调显存、序列长度与批次大小的权衡config.py是整个训练流程的“控制台”所有影响模型行为和显存占用的参数都集中在这里。我把它最核心的几个参数整理成了表格方便逐项对照调整参数名推荐初始值说明max_seq_len128句子最大长度超过则截断不足则填充显存不足优先调小epochs5训练轮数数据量大时 3 轮足够小数据可加到 8 轮train_batch_size16训练批次大小OOM 时降到 8 或 4dev_batch_size32验证批次大小显存占用比训练小learning_rate5e-5BERT 微调常用学习率太大会冲乱预训练权重save_step500每多少步保存一次模型 checkpoint关于max_seq_len有两个不同的代价方向。设得太大比如 512显存占用会成倍上升因为 BERT 的 self-attention 复杂度是序列长度的平方设得太小比如 64句子被截断的部分实体直接丢失召回率明显下降。项目默认给 128 对 dgre 这种短文本故障报告比较合适我测试时发现 99% 的句子长度都在 80 字以内。train_batch_size和max_seq_len是显存的直接消耗者如果 GPU 显存只有 6G建议按“max_seq_len 降到 96、batch_size 降到 8”的组合先跑通再看 loss 曲线决定是否加回去。learning_rate 这里特别提醒BERT 微调的常见做法是从 5e-5 往下调而不是往上加一群人会把 learning_rate 设成 1e-3 然后发现 loss 训练一会儿就变成 NaN那是因为预训练权重被一步更新冲坏了。4.2 训练启动改data_name与运行main.py训练入口在main.py但跑之前必须修改一个关键变量data_name。它要和data目录下的数据集目录名保持一致比如要用 dgre 数据就设成dgre要用 duie 数据就设成duie。很多第一次跑这个包的人会忘记改这个变量结果训练循环已经在跑 downstream 任务了数据加载器读的还是上一个数据集。# main.py 开头部分 if __name__ __main__: # 修改 data_name 为数据集名称必须和 data 下的目录名一致 data_name dgre # dgre 或 duie train(data_name) test(data_name)修改完成后直接执行python main.py训练正式开始后日志会周期性地打印 loss 和当前 step 数。训练过程中save_step控制的是 checkpoint 保存频率每 500 步会把当前模型权重保存到checkpoint目录同时写一个ner_args.json记录当时的参数配置。这里有个细节checkpoint里保存的模型是多个每个对应不同的 step最后选哪个用于预测需要在预测阶段指定不能默认用最新的因为训练后期 loss 可能已经在验证集上开始反弹。验证集的评测指标由seqeval这个库计算它会输出实体级别的 precision、recall 和 F1。我在实际跑 dgre 数据时训练到第 3 个 epoch 左右 F1 就能稳定在 85 上下如果训练 5 轮后 F1 还在 70 以下优先排查数据加工环节是否存在标签错位而不是怀疑模型结构。4.3 训练日志怎么读loss下降与checkpoint保存训练日志最常见的形态是“step 500, loss 0.4231”“step 1000, loss 0.3108”这种逐行输出。loss 的绝对值大小没有绝对标准它取决于标签数量和序列长度但下降趋势必须是单调的。loss 如果出现先降后升通常是 learning_rate 太大模型跨越了损失面的谷底如果 loss 在前几百步内纹丝不动先检查数据加载器是否正确把标签和文本配了对。checkpoint 目录里每个保存节点的文件结构通常是模型权重加ner_args.json。这里我强烈建议训练结束后把ner_args.json里的参数和最终验证集 F1 值一起记到实验笔记里。命名实体识别这个任务有个特点同一套代码、同一份数据只改一个max_seq_len都可能让 F1 波动两三个点不记录参数后面复现自己都看不懂自己当时是怎么跑出那个分数的。5. 避坑与常见问题从预训练模型下载到标签错位的五个踩坑记录5.1 HuggingFace模型下载慢或下载不完整现象按照项目说明去 HuggingFace 下载chinese-bert-wwm-ext的config.json、pytorch_model.bin、vocab.txt三个文件网络波动大下载到一半中断重新下载又要重来或者下载完成后model_hub里缺少pytorch_model.bin模型加载时报文件不存在的错。原因pytorch_model.bin的体积在 400MB 左右网络条件差时很容易下载超时浏览器默认的下载机制不支持断点续传中断后只能整个重来。解决不要用浏览器直接点下载我一般会把三个文件分开逐个下载config.json和vocab.txt很小先下pytorch_model.bin单独下下载完成后核对文件大小是否和网页标注一致。文件放到位后跑一句from transformers import BertModel; BertModel.from_pretrained(model_hub/chinese-bert-wwm-ext)能打印模型结构说明权重完整。注意预训练模型和代码包是配套使用的模型文件缺失时训练脚本会在加载阶段直接抛异常它不会用随机权重继续训练所以不用担心“模型不完整但训练照跑”的假象。5.2 labels.txt顺序导致标签ID映射错乱现象训练全程 loss 正常下降验证集 F1 却一直极低甚至预测出的标签全部是O或者O和B-故障设备交替出现。原因label 到 ID 的映射字典是按labels.txt中标签出现的顺序构建的labels.txt里如果新增了标签或者顺序和process.py生成 NER 数据时不一致模型输出层的维度虽然对得上但“故障设备”对应的 ID 换了数字预测结果自然全乱。解决强制保证labels.txt和训练脚本的标签映射来自同一个来源。我一般会写一个脚本扫描所有训练数据的 labels 字段动态生成labels.txt并排序这样不管数据集换成 dgre 还是 duie标签集合都是程序算出来的不会出现手写标签漏行或错序。5.3 GPU显存溢出OOM现象训练刚开始跑终端直接报CUDA out of memory甚至有时候一加载模型就爆了。原因显存占用由模型参数、max_seq_len、train_batch_size三者共同决定。BERT 本身就有 1 亿多参数稍微把序列长度和 batch size 调大一点6G 显存的卡就扛不住了。解决按“先降train_batch_size再降max_seq_len”的顺序处理。先把 batch size 从 16 降到 8还爆就把max_seq_len从 128 降到 96。这两个参数每降一档显存占用接近减半基本能解决 90% 的 OOM。如果显存实在紧张可以考虑在config.py里加入梯度累积相当于用时间换空间卡在 batch size 为 2 也能等效出 batch size 为 16 的更新效果。5.4 text.find只匹配实体的第一次出现现象训练数据里某个实体在句子中出现了两次甚至三次但模型预测时只认了第一次出现的位置其它实体位置全部漏标导致召回率偏低。原因第 3.2 节代码里用的是text.find(entity_text)它只返回字符串第一次出现的位置同一实体后续出现的位置不会被标注。解决换成循环查找每找到一个位置就记录并从下一个位置继续查找直到找完整个文本。我习惯封装一个find_all_occurrences(text, entity)辅助函数返回所有起始下标列表然后对每个下标依次打 B- 和 I- 标签。这个坑在 dgre 这种故障报告数据里尤其明显因为同一设备名称在“故障现象”和“故障原因”里可能各出现一次漏标就是信息漏失。5.5 save_step设置不当导致最佳模型丢失现象训练结束后用 checkpoint 里最后保存的模型做预测F1 反而比训练到一半时低或者验证集上最好的模型分数根本不在保存列表里。原因save_step控制的是每多少 step 保存一次模型但它并不保证保存的那个节点恰好是验证集分数最高的节点。如果save_step设得过大比如 2000 步一次而验证集最佳的 epoch 出现在 2600 步那么 2000 步的模型尚未收敛4000 步的模型已经过拟合最佳状态就被跳过了。解决把save_step调小我一般设置为 200 到 500 之间保证在验证集分数变动最剧烈的训练中期也能落地几个保存点。另外可以在训练循环里增加边训练边验证的逻辑保留每次验证集 F1 提升时的模型快照这样最后一个 checkpoint 就是历史最优而不是任意 step 的中间产物。6. 让模型真正可用predict.py的改造与seqeval指标解读6.1 预测脚本改造从测试集到任意一句中文原始predict.py是在测试集上做预测输出测试样本的预测标签并计算指标。但在毕业设计答辩或者实际业务里更多时候是想输入一句全新的话直接得到实体抽取结果。我通常会把predict.py改造成一个可复用的函数输入文本字符串输出实体列表# predict.py 中针对单句预测的核心逻辑 text 汽车在加速之后丢开油门发动机熄火 inputs tokenizer( list(text), is_split_into_wordsTrue, max_lengthconfig.max_seq_len, truncationTrue, paddingmax_length, return_tensorspt ).to(device) with torch.no_grad(): logits model(**inputs) predictions logits.argmax(dim-1) # 每个位置的标签ID # 把预测ID还原成标签并拼回实体 id2label {v: k for k, v in label2id.items()} pred_labels [id2label[idx] for idx in predictions[0].tolist()]这里return_tensorspt让 tokenizer 直接返回 PyTorch 张量is_split_into_wordsTrue表示传入的已经是字符级别的切分和训练时保持一致。预测结果还原成标签后按 B- 开头的位置切实体遇到 I- 标签就继续累加当前实体文本遇到 O 就断开。改造之后无论是写一个 web 接口还是批量处理文本文件调用起来都方便得多。我在实际使用时会在函数外层加一个格式判断输入是字符串就走单句预测输入是列表就走批量预测同一套内部逻辑复用。6.2 指标解读seqeval的F1到底在算什么训练结束后main.py会调用seqeval输出验证集的分类报告这个报告和 sklearn 的 classification_report 长得像但统计口径完全不同。seqeval 计算的是实体级别的指标不是字符级别的指标一个“发动机”被完整预测成“故障设备”才算一个正确的实体预测如果预测成了“发动机故”三个字带 B- 和两个 I-即便字符标签对了实体边界只要差一个字这个实体也记错。precision 是预测出的实体里预测对的占比recall 是真实实体里被找出来的占比F1 是两者的调和平均。看报告时优先看 micro avg 这一行的 F1而不是逐标签的 F1。如果某个实体类型比如故障原因的 F1 明显低于其它实体数据里这个类型的样本量大概率很少few-shot 场景下可以考虑在config.py层面给这个类型加权但要注意加权过大会过拟合。从那以后我每次训练 NER 模型都会强制走一遍固定的验收流程先跑process.py验证标签对齐再训练前打印一条样本的人工校对最后预测阶段拿到实体结果先看边界再看类型。这个习惯帮我挡掉了无数次数据错位和标签映射的坑。这份资源适合已经具备一定 Python 基础、想直接看完整 NER 工程实现细节的人解压后按第 2 章目录对好文件改一下data_name就能把整个流程跑通希望帮到你。本文还有配套的精品资源点击获取
返回列表