ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

中文命名实体识别实战:BERT+BiLSTM+CRF课设指南

中文命名实体识别实战:BERT+BiLSTM+CRF课设指南 简介这份资源面向计算机相关专业的本科生与课程设计学习者提供一套基于BERTBiLSTMCRF的中文命名实体识别完整源码适合作为毕业设计、期末大作业或NLP入门实战项目。项目采用预训练语言模型提取语义特征结合双向LSTM与条件随机场完成序列标注覆盖数据预处理、模型训练、预测推理与服务部署等环节代码注释详尽新手也能理解整体流程。压缩包共54个文件以34个Python源码为核心辅以11张运行截图、3份说明文档、3个Markdown笔记及构建脚本与许可证文件整体约475KB结构紧凑便于快速部署。目前已有178人学习关注。读者可获得一套可直接运行的NER工程方案包括训练与预测脚本、服务端接口示例、依赖清单及环境配置说明便于在此基础上替换数据集、调整超参数或扩展实体类别也能借助截图与文档快速排查运行问题具备较高的课设与毕设参考价值。1. 中文命名实体识别为什么 BERTBiLSTMCRF 仍是课设首选做过中文 NER 的人都有一个共识数据集小、实体边界模糊、标注不一致这三件事凑在一起模型很容易学成“复读机”——把训练集里的实体背下来换一段新文本就崩。我带过几届毕业设计见过太多同学用纯 BiLSTMCRF 跑出 85% 的 F1答辩时被问“换个领域还能用吗”就答不上来。问题不在模型结构而在字向量太薄没有预训练语言模型的语义先验。BERTBiLSTMCRF 这套组合恰好卡在“效果够用”和“算力可承受”之间。BERT 负责把每个汉字映射成带上下文信息的向量BiLSTM 捕捉序列前后的长距离依赖CRF 层则保证输出的标签序列合法——比如“B-ORG”后面不会直接跟“I-PER”。对于中文命名实体识别这个任务标签之间的转移约束不是锦上添花而是刚需。你如果只做课设或毕业设计数据量通常在几千到几万条这套结构能在单卡 8G 显存内跑起来训练两三个小时就能看到收敛趋势。适合谁读正在做 Python 毕业设计或课程设计、需要一份能跑通的中文 NER 源码、对 BERT 微调流程不熟但想搞懂每一步在干什么的人。下面我会按“数据怎么进、模型怎么搭、参数怎么调、坑怎么避”的顺序把整套方案拆成可复现的步骤。你不需要先精通 Transformer但得会装 Python 环境、能看懂 PyTorch 的基础张量操作。2. 从原始文本到 BERT 输入数据预处理与标签对齐2.1 中文 NER 的数据格式与标签体系中文 NER 常见的数据格式有两种BIO 和 BIOES。BIO 用 B-X 表示实体开始I-X 表示实体内部O 表示非实体BIOES 多了 E-X实体结束和 S-X单字实体。课设里我一般推荐 BIO因为标注成本低而且 BERTBiLSTMCRF 对 BIO 的边界识别已经足够好。标签体系取决于你的数据集比如 MSRA 用 PER、LOC、ORG 三类人民日报语料用 Nh、Ns、Ni。你自己标数据的话先定好实体类型别中途加类否则前面标的全废。数据文件通常是一行一个字加标签空行分隔句子。下面是一个标准样例北 B-LOC 京 I-LOC 是 O 中 B-ORG 国 I-ORG 的 O 首 O 都 O读取时按空行切分句子每句变成一个(chars, labels)对。注意中文不需要分词BERT 的 tokenizer 对汉字基本是一字一 token但遇到英文或数字会拆成子词。如果你的数据里混了英文实体比如“Python”tokenizer 可能把它拆成“Py”“##thon”这时候标签对齐就会出问题。常见做法是在预处理阶段把英文和数字也按字符拆开或者直接用BertTokenizer的tokenize方法逐字处理保证一个汉字对应一个 token。2.2 用 BertTokenizer 做字符级编码与标签同步BERT 的输入需要三样东西input_ids、attention_mask、token_type_ids。对于单句 NERtoken_type_ids全零即可。关键难点在于tokenizer 可能会插入[CLS]和[SEP]还会对某些字符做拆分导致 token 数量与原始字符数不一致。你必须把标签序列同步扩展到 token 级别否则训练时 loss 会算错。我一般用is_split_into_wordsTrue模式先把句子拆成字符列表再传给 tokenizer。这样 tokenizer 会按字符对齐不会额外拆分汉字。代码示例如下from transformers import BertTokenizer tokenizer BertTokenizer.from_pretrained(bert-base-chinese) def encode_sentence(chars, labels, label2id, max_len128): # chars: [北, 京, 是, ...] # labels: [B-LOC, I-LOC, O, ...] encoding tokenizer( chars, is_split_into_wordsTrue, max_lengthmax_len, truncationTrue, paddingmax_length, return_tensorspt ) # 获取每个 token 对应的原始字符索引 word_ids encoding.word_ids(batch_index0) label_ids [] for word_idx in word_ids: if word_idx is None: label_ids.append(-100) # 特殊 token 忽略 loss else: label_ids.append(label2id[labels[word_idx]]) return encoding[input_ids], encoding[attention_mask], label_ids逻辑说明word_ids()返回每个 token 对应的原始字符位置None表示[CLS]、[SEP]或 padding。把这些位置的标签设为-100PyTorch 的CrossEntropyLoss会自动忽略。参数max_len根据你的数据长度分布来定中文 NER 句子通常不超过 128 个字设 128 能覆盖 95% 以上的样本。如果显存不够降到 64 再试但要注意截断会丢实体。注意is_split_into_wordsTrue时传入的必须是字符列表不能是整句字符串。否则 tokenizer 会按词切分中文会被拆成多字词标签对齐直接乱掉。3. 搭 BERTBiLSTMCRF模型结构拆解与 PyTorch 实现3.1 BERT 输出怎么接 BiLSTM维度、dropout 与残差BERT 的输出是[batch_size, seq_len, hidden_size]bert-base-chinese的hidden_size是 768。BiLSTM 的输入维度必须等于 768隐藏层维度一般设 128 或 256。双向 LSTM 会把每个时间步的输出拼接成[batch_size, seq_len, 2*hidden_dim]。如果你设hidden_dim128输出就是 256 维。然后接一个线性层映射到标签数量比如 7 个标签就是[batch_size, seq_len, 7]。这里有个细节BERT 最后一层的输出直接喂给 BiLSTM效果不一定最好。我习惯在 BERT 输出后加一个Dropout(0.3)再进 BiLSTM。原因是 BERT 微调时参数更新幅度大dropout 能防止过拟合。另外BiLSTM 的batch_firstTrue必须设否则维度顺序是[seq_len, batch, hidden]后面接 CRF 会报错。import torch import torch.nn as nn from transformers import BertModel class BertBiLSTMCRF(nn.Module): def __init__(self, bert_path, num_tags, lstm_hidden128, dropout0.3): super().__init__() self.bert BertModel.from_pretrained(bert_path) self.dropout nn.Dropout(dropout) self.bilstm nn.LSTM( input_size768, hidden_sizelstm_hidden, num_layers1, bidirectionalTrue, batch_firstTrue ) self.classifier nn.Linear(lstm_hidden * 2, num_tags) self.crf CRF(num_tags, batch_firstTrue) # 假设用 pytorch-crf def forward(self, input_ids, attention_mask, token_type_ids): outputs self.bert( input_idsinput_ids, attention_maskattention_mask, token_type_idstoken_type_ids ) sequence_output outputs.last_hidden_state # [B, L, 768] sequence_output self.dropout(sequence_output) lstm_output, _ self.bilstm(sequence_output) # [B, L, 256] emissions self.classifier(lstm_output) # [B, L, num_tags] return emissions参数说明lstm_hidden设 128 是课设里的甜点值再大显存吃紧再小欠拟合。dropout设 0.3 到 0.5 之间数据量小于 5000 条时用 0.5。num_layers1足够堆两层 BiLSTM 在中文 NER 上收益很小反而容易过拟合。3.2 CRF 层的作用与解码为什么不能直接用 softmaxCRF 的核心是学习标签之间的转移分数。比如在 BIO 标注下B-LOC后面跟I-PER的概率应该极低CRF 通过转移矩阵把这个约束学出来。如果没有 CRF模型可能输出B-LOC I-PER I-PER这种非法序列后处理还得写规则修得不偿失。训练时CRF 的 loss 是负对数似然计算的是所有合法路径的分数之和。解码时用 Viterbi 算法找最优路径。pytorch-crf库已经封装好了你只需要把 emissions 和标签传进去。from torchcrf import CRF # 训练阶段 crf CRF(num_tags7, batch_firstTrue) emissions model(input_ids, attention_mask, token_type_ids) loss -crf(emissions, tags, maskattention_mask.bool(), reductionmean) loss.backward() # 预测阶段 predictions crf.decode(emissions, maskattention_mask.bool())逻辑说明mask参数必须传否则 padding 位置会参与转移计算导致 loss 异常。reductionmean会对 batch 内所有有效 token 求平均比sum更稳定。解码返回的是每个样本的标签 id 列表长度等于实际 token 数不含 padding。注意pytorch-crf的decode返回的是 list of list不是张量。如果你要批量计算 F1得自己写对齐逻辑把预测标签和真实标签按有效长度截齐。4. 训练参数怎么设学习率、batch size 与早停策略4.1 BERT 微调的学习率分层设置BERT 微调最忌讳的就是全局用同一个学习率。BERT 本体已经预训练好了只需要微调学习率设 2e-5 到 5e-5 就够而 BiLSTM 和分类层是随机初始化的需要更大的学习率一般设 1e-3。如果你用 AdamW 统一设 2e-5BiLSTM 收敛会非常慢训练 10 个 epoch 可能还在震荡。我一般用参数分组的方式给 BERT 和新增层分别设学习率from transformers import AdamW bert_params list(model.bert.named_parameters()) new_params list(model.bilstm.named_parameters()) \ list(model.classifier.named_parameters()) \ list(model.crf.named_parameters()) optimizer AdamW([ {params: [p for n, p in bert_params], lr: 3e-5}, {params: [p for n, p in new_params], lr: 1e-3} ], weight_decay0.01)参数说明weight_decay0.01是 BERT 微调的常规值能抑制过拟合。如果你发现 BERT 层 loss 下降很慢可以把 BERT 学习率提到 5e-5但别超过 1e-4否则预训练知识会被冲掉。4.2 batch size 与梯度累积8G 显存下的可行配置bert-base-chinese模型本身约 400MB加上 BiLSTM 和 CRF推理时显存占用约 2GB。训练时还要存激活值和梯度8G 显存下batch_size设 16、max_len128基本能跑。如果显存不够用梯度累积batch_size8累积 2 步等效 batch size 还是 16。accum_steps 2 optimizer.zero_grad() for step, batch in enumerate(dataloader): loss model_train_step(batch) loss loss / accum_steps loss.backward() if (step 1) % accum_steps 0: optimizer.step() optimizer.zero_grad()逻辑说明梯度累积把多个小 batch 的梯度加起来再更新效果接近大 batch但显存占用按小 batch 算。注意 loss 要除以累积步数否则梯度会放大。早停策略每轮在验证集上算 F1如果连续 3 轮 F1 不升就停。课设数据量小通常 5 到 8 个 epoch 就能收敛。别训练太多轮BERT 微调过拟合很快训练集 F1 到 99% 时验证集可能已经开始掉了。5. 避坑与排查中文 NER 训练中最容易翻车的 5 个点5.1 标签对齐错位现象是 loss 不降预测全是 O现象训练几个 epoch 后 loss 卡在 2.0 左右不降预测结果全是 O 标签。原因tokenizer 的word_ids没处理好标签和 token 错位模型学到的全是噪声。解决在预处理阶段打印一条样本的chars、word_ids、label_ids逐字核对。确保[CLS]和[SEP]位置的标签是-100汉字位置的标签与原始标签一致。5.2 学习率过大导致 BERT 灾难性遗忘现象训练初期 loss 骤降但验证集 F1 从第 2 轮开始暴跌。原因BERT 学习率设成了 1e-3 或更高预训练权重被破坏。解决BERT 层学习率降到 3e-5新增层保持 1e-3。如果已经跑崩了重新加载bert-base-chinese从头训。5.3 CRF 的 mask 没传导致 loss 异常现象loss 出现 NaN 或异常大的值。原因crf()调用时没传maskpadding 位置的标签参与了转移计算。解决确保maskattention_mask.bool()传入并且attention_mask在 padding 位置是 0。5.4 实体类别不均衡导致小类 F1 极低现象PER 和 LOC 的 F1 有 90%ORG 只有 40%。原因ORG 实体在训练集中样本太少。解决在 loss 里给每个标签加权权重与类别频率成反比。或者用 focal loss 替代交叉熵。课设里如果 ORG 太少可以在数据增强时多造一些 ORG 样本。5.5 推理时 batch 内句子长度不一致导致解码错位现象单条推理正常批量推理时部分样本预测结果错乱。原因crf.decode返回的列表长度与输入长度一致但 padding 位置也被解码了。解决解码后按attention_mask的有效长度截取只保留真实 token 的标签。6. 进阶技巧用对抗训练和模型融合把 F1 再提 2 个点课设做到 85% F1 已经能交差但如果你想让答辩更稳可以加两个技巧FGM 对抗训练和 BERT 多层特征融合。FGM 的思路是在 embedding 层加一个扰动让模型对微小变化更鲁棒。实现很简单在训练循环里加一步class FGM: def __init__(self, model, epsilon1.0): self.model model self.epsilon epsilon self.backup {} def attack(self): for name, param in self.model.named_parameters(): if param.requires_grad and embedding in name: self.backup[name] param.data.clone() norm torch.norm(param.grad) if norm ! 0: r_at self.epsilon * param.grad / norm param.data.add_(r_at) def restore(self): for name, param in self.model.named_parameters(): if name in self.backup: param.data self.backup[name] self.backup {}用法正常前向传播算 loss反向传播后调用fgm.attack()再前向传播一次算对抗 loss累加后更新参数最后fgm.restore()。epsilon设 1.0 是常用值太大反而掉点。另一个技巧是取 BERT 最后四层的输出做平均再喂给 BiLSTM。bert-base-chinese有 12 层最后四层包含的语义信息最丰富平均后能缓解单层输出的噪声。代码上只需改output_hidden_statesTrue然后取hidden_states[-4:]求均值。outputs self.bert(..., output_hidden_statesTrue) hidden_states outputs.hidden_states # tuple of 13 tensors last_four torch.stack(hidden_states[-4:], dim0).mean(dim0) sequence_output self.dropout(last_four)这两个技巧叠加在 MSRA 数据集上通常能提 1.5 到 2.5 个 F1。但注意对抗训练会让训练时间增加约 50%课设如果时间紧只加多层融合就够了。我自己的习惯是先把 baseline 跑通确认数据管道没问题再加技巧。见过太多同学一上来就堆模块结果 loss 不降连问题出在哪都定位不到。先让模型能跑、能出结果再谈优化。希望帮到你。本文还有配套的精品资源点击获取
返回列表