BERT-BiLSTM-CRF中文命名实体识别源码实战与排坑指南
简介面向计算机相关专业学生与算法入门者的中文命名实体识别NER项目源码基于BERT-BILSTM-CRF模型涵盖数据预处理、模型训练、实体预测的完整流程可直接用于毕业设计、课程设计或算法基线对比。压缩包共20个文件以Python脚本、JSON配置、TXT语料及说明文档为主整体约1.03MB目录包含checkpoint模型保存、model_hub预训练权重、data数据集与核心代码模块结构清晰易于检索。项目以dgre数据集为例提供了BIO标注格式的训练/验证样本和标签文件附带详细使用说明配置好预训练模型后运行main.py即可训练通过predict.py可加载模型完成中文实体识别预测。读者还可结合config.py调整最大序列长度、批次大小、训练轮数等超参数适配不同显存环境或进行扩展实验便于二次开发与功能改进。目前已有1250人学习下载适合希望快速搭建NER系统或完成课程项目初版演示的开发者参考使用。1. 基于BERT-BILSTM-CRF的中文命名实体识别这个项目包为什么值得你花一个晚上跑通做中文命名实体识别最怕的不是模型选型而是你折腾三天还在调环境。基于BERT-BILSTM-CRF进行中文命名实体识别python源码项目使用说明数据模型.zip这个标题听起来很“打包好、拿来即用”但如果你直接双击train.py大概率还是会翻车。我的经验是这类工程包最大的价值不在那几十行模型代码而在它把数据标注格式、预训练模型权重、超参数和推理脚本串成了一条完整链路。适合刚接触NER的Python开发者、需要给业务快速出基线的算法工程师以及想做消融实验但对PyTorch细节不熟的人。这篇文章我会顺着这套方案从头拆到尾告诉你每一步为什么这么做以及最容易被坑在哪。2. 从标注体系到模型结构BERT、BiLSTM、CRF 三层分别解决什么问题2.1 中文 NER 的标注体系BIO 还是 BIOES直接决定模型边界命名实体识别本质上是序列标注任务给句子里的每个字或词分配一个标签。最常见的标签体系是 BIOB 表示实体开头I 表示实体内部O 表示非实体。比如“张三去北京”标注结果可以写成张 B-PER 三 I-PER 去 O 北 B-LOC 京 I-LOC还有一种更细的 BIOES 体系额外增加 E实体结尾和 S单字实体。我的实际感受是BIO 已经够用但如果你的实体里大量出现单字人名或者实体边界本来就模糊BIOES 的学习信号会更清楚F1 大概能提升一到两个点。代价是标注人员更容易出错项目使用说明里如果没写清楚你可以直接看数据里有没有 E 和 S 标签来判断。中文 NER 还有一个容易忽略的选型点字符级标注还是词级标注。很多人习惯先用 jieba 分词再标实体但分词错误会一路传导到 NER模型完全没有机会纠错。我在项目里几乎一律用字符级标注即使拿到的原始语料是词级别的也会先拆成字。这个决定在后面数据预处理时会省掉很多麻烦。对应到源码里大多数中文 NER 模型用的是字级别输入BERT 的 tokenizer 天然按字切分中文正好匹配。2.2 BERT 层用上下文相关字向量替代静态词向量BERT 层承担的是“把每个字表示成向量”的职责。中文 BERT 在海量语料上预训练过所以“长”字在“长城”里和“长高”里会得到不同的表示这是静态词向量比如 word2vec做不到的。对于人名、地名里那些低频但信息量大的汉字这一点非常关键。选择底模时我一般先看项目里对模型路径的配置。如果使用说明里写的是 bert-base-chinese那就是最通用的版本如果写的是 chinese-roberta-wwm-ext效果通常更好一点但加载方式和前者的 api 完全一样。标题里的“模型.zip”大概率就是一个微调后的 checkpoint里面包含的是 BERT 权重加上下游 BiLSTM、CRF 的权重不包含原始预训练模型。这一点很多人搞混导致加载模型时报 “key not found”后面我会在推理部分详细讲。2.3 BiLSTM 层在 BERT 之上再做序列编码真的不是白加BiLSTM 层把 BERT 输出的每个位置表示从左到右、从右到左各跑一遍然后拼接起来。这样做的好处是在局部窗口内进一步强化相邻字符之间的顺序关系。BERT 的 Transformer 虽然是双向的但它对位置编码的依赖方式和 LSTM 归纳偏置不太一样在数据量只有几千条的中文 NER 任务里加上 BiLSTM 之后收敛更平滑验证集 F1 也更稳。有一个观点说“BERT 都强到不需要 BiLSTM 了”我不完全认同。我做消融实验时把 BiLSTM 拿掉、只保留 BERTsoftmax在通用领域数据集上 F1 掉了两个点左右在方言数据上掉得更多。原因可能是 BiLSTM 对实体边界的局部变化更敏感。尤其是在 BERT 被微调得不够充分时BiLSTM 相当于一个缓冲区能兜住一部分噪声。当然BiLSTM 也不是层数越多越好。我一般设置 1 层hidden_size 取 256。层数多了在小数据集上很容易过拟合而且训练时间几乎翻倍。2.4 CRF 层让模型学会标签合法性而不是靠运气CRF 层解决的是标签依赖问题。如果用 softmax 对每个位置独立分类模型可能预测出“B-PER 后面跟 I-LOC”这种荒谬的序列因为它在每个位置做决策时看不到上一个标签。而 CRF 会维护一个标签转移矩阵训练时学习每一种标签跳转的权重解码时用 Viterbi 算法找整条序列的最优路径。举个例子模型可能对“张三去北京”输出每个字属于各标签的分数如果单独看每个位置“京”是 I-LOC 的分很高但前面的“去”被预测成 O那么按 softmax 就是“O I-LOC”结尾CRF 会把这个序列的得分压下去因为 O 后面接 I-LOC 的转移非常不合常规。这就是 CRF 层的价值。在实践中我建议直接使用 torchcrf 这个库不要自己去写 Viterbi 解码自己实现时漏掉 mask 处理或者只返回一条路径的 bug 非常难查。torchcrf 提供的 decode 方法自动处理 padding 位置省心很多。2.5 三层组合的输入输出维度与消融参考三层网络放在一起后的数据流向是输入 ids 形状 [batch, seq_len]BERT 输出 [batch, seq_len, 768]BiLSTM 输出 [batch, seq_len, 512]因为双向 256*2线性投影到 [batch, seq_len, num_tags]CRF 基于这个发射分数计算损失或解码。你用源码时关心 num_tags 和标签映射表就够了其余维度都是由模型配置决定的。如果你拿到这份 python 源码想快速验证三层结构是否真的都必要我建议做一次消融固定数据和种子对比 BERTCRF 和 BERTBiLSTMCRF 的 F1。这个实验通常是一个晚上能跑完的。做完之后你会对这套组合的“常青树”属性有更直观的认识。3. 从解压到跑通环境准备、BIO 数据转换与标签对齐3.1 拿到源码包后先别急着运行按这个顺序检查项目我见过太多人拿到压缩包第一件事就是解压运行 train.py结果报错后连项目结构都没看过。正确做法是先看项目使用说明再确认三件事第一数据文件长什么样标签是 BIO 还是 BIOES第二模型权重路径指向哪里是预训练模型还是微调后的第三主入口是训练脚本还是推理脚本。很多“源码数据模型”的打包项目里真正的入口其实是一个 demo.ipynb 或 predict.py而不是 train.py。如果压缩包里没有 README 或项目使用说明那就从文件名和目录结构反推。常见做法是 src 里放模型代码data 里放 train.json 和 dev.jsonmodel 里放权重。你至少要确认标签集合和数据的长度再决定是否需要重新训练。不要一上来就要求模型跑出 90 分先让它跑通。3.2 Python 3.8 环境下的依赖安装torch、transformers、seqeval 的版本搭配环境这部分是新手踩坑重灾区。这个项目依赖 PyTorch、Transformers、seqeval、pandas 等。我建议直接建一个虚拟环境用 Python 3.8不要用 3.10 以上的版本因为一些旧版 torch 和 transformers 组合在 3.10 里会有 wheel 缺失问题。下面是我常用的安装命令# 创建虚拟环境指定 Python 3.8 python3.8 -m venv ner_env source ner_env/bin/activate # 先装 numpy版本别乱挑 pip install numpy1.24.3 pandas scikit-learn # 安装 GPU 版 torchcuda 11.7 对应 torch 1.13.1 pip install torch1.13.1 --index-url https://download.pytorch.org/whl/cu117 # 安装 transformers 和序列评估工具 pip install transformers4.26.1 seqeval参数说明如果你机器上没有 NVIDIA 显卡把 torch 的安装命令改成pip install torch1.13.1 --index-url https://download.pytorch.org/whl/cpu但训练速度会慢很多小数据集还能忍受。transformers 4.26.1 和 torch 1.13.1 这个组合我反复用没有 API 变化导致的兼容性错乱。seqeval 用于 F1 评估后面会讲。装完依赖后用下面这段代码验证关键库能正常 importpython -c import torch, transformers, seqeval; print(torch.__version__, transformers.__version__)如果 torch 能 import 但 transformers 报错多半是版本不匹配或缺少 tokenizers 的编译依赖。不要追着最新版装项目里使用说明写的版本能跑就优先用那个版本。3.3 把原始中文语料转成 BIO 标注一个词典匹配的转换脚本假设你手上有的是普通中文文本和一个实体词典并没有人工标注好的 NER 数据。这时你需要一个预处理脚本用词典匹配把实体边界转成 BIO 标签。下面是最小实现# dict_to_bio.py最简单词典匹配转 BIO 标注 def dict_to_bio(chars, entity_dict): chars: 句子字符列表例如 [张, 三, 去, 北, 京] entity_dict: 列表元素是 (实体文本, 实体类型)例如 [(北京, LOC)] 返回 tokens 和 labels 两个等长列表 tokens, labels [], [] i 0 while i len(chars): matched False for entity, etype in entity_dict: if .join(chars[i:]).startswith(entity): entity_chars list(entity) tokens.extend(entity_chars) labels.append(B- etype) labels.extend([I- etype] * (len(entity_chars) - 1)) i len(entity_chars) matched True break if not matched: tokens.append(chars[i]) labels.append(O) i 1 return tokens, labels # 示例用法 chars list(张三去北京) entity_dict [(北京, LOC)] tokens, labels dict_to_bio(chars, entity_dict) print(tokens) # [张, 三, 去, 北, 京] print(labels) # [O, O, O, B-LOC, I-LOC]代码逻辑不复杂用一个游标 i 在字符列表上走词典里谁在当前位置匹配上就整段输出实体标签并把游标跳过去没有匹配则输出 O游标前进一个字。这里有个细节词典匹配的优先级会直接影响标注质量我建议在实体字典里按文本长度降序排列优先匹配最长实体否则“北京大学”会被匹配成“北京”“大学”边界全乱。把这个逻辑扩展到整份语料后你还需要按句子粒度划分训练集、验证集和测试集。一般按 8:1:1 的比例随机切分但要注意不要让同一个文档里的句子同时出现在两个集合里。这个数据泄漏问题会让 F1 虚高后面排查章节还会提到。3.4 构造 Dataset 与标签对齐BERT tokenizer 是最大的标签错位源当你把文本转成 BIO 列表后还不能直接喂给 BERT。BERT tokenizer 会在句子首尾加入 [CLS] 和 [SEP]对英文或数字还可能切分成多个 token中文虽然大多是一个字一个 token但标签长度还是会对不上。如果你直接拿 BIO 标签列表和 input_ids 做 zip训练时 loss 不会报错但模型永远学不会正确边界。正确做法是用 tokenizer 返回的 word_ids 做映射。下面是一个标准的对齐函数# align_labels.pyBERT tokenizer 与 BIO 标签对齐 from transformers import BertTokenizerFast def tokenize_and_align_labels(examples, tokenizer, label2id, max_len128): examples: 包含 tokens 和 labels 字段的样本 label2id: {O: 0, B-PER: 1, ...} encoding tokenizer( examples[tokens], is_split_into_wordsTrue, truncationTrue, paddingmax_length, max_lengthmax_len ) word_ids encoding.word_ids() # 每个 token 对应原始字符位置 aligned_labels [] for word_id in word_ids: if word_id is None: # CLS, SEP, PAD 位置用 -100 忽略 aligned_labels.append(-100) else: aligned_labels.append(label2id[examples[labels][word_id]]) encoding[labels] aligned_labels return encoding参数说明is_split_into_wordsTrue是关键它告诉 tokenizer 你传入的是已经切好的 token 列表不要再做整体分词。word_ids()返回的是一个数组第 i 个值表示 input_ids 第 i 个 token 来自原始句子的第几个字符None 表示特殊 token。用这个映射去查原始标签就能保证每个 token 有自己对应的标签特殊 token 用 -100 在 loss 里被忽略。这个对齐逻辑是整套源码里最值得你读的代码也是很多“训练起来 loss 不降”的根因。如果你在预训练模型的使用说明里没有看到这段逻辑我强烈建议你把它补上并且在自己构造验证集时也走同样的对齐函数否则验证结果的错位会给你一种指标很高的错觉。4. 模型构建与训练网络定义、损失函数、超参数一次讲清4.1 网络结构定义BertEncoder BiLSTM CRF 的完整类模型代码是整个项目包的核心但大多数开源打包工程里的模型代码都是一两百行而已。下面这个类就是我常用的实现它把三层结构串起来# model.pyBERT-BiLSTM-CRF 模型定义 import torch import torch.nn as nn from transformers import BertModel from torchcrf import CRF class BertBiLSTMCRF(nn.Module): def __init__(self, bert_path, num_tags, lstm_hidden256, lstm_layers1, dropout0.5): super().__init__() self.bert BertModel.from_pretrained(bert_path) self.dropout nn.Dropout(dropout) self.bilstm nn.LSTM( input_sizeself.bert.config.hidden_size, # 768 hidden_sizelstm_hidden, # 256 num_layerslstm_layers, batch_firstTrue, bidirectionalTrue ) self.classifier nn.Linear(lstm_hidden * 2, num_tags) self.crf CRF(num_tags, batch_firstTrue) def forward(self, input_ids, attention_mask, labelsNone): outputs self.bert(input_idsinput_ids, attention_maskattention_mask) sequence_output self.dropout(outputs.last_hidden_state) lstm_out, _ self.bilstm(sequence_output) logits self.classifier(lstm_out) if labels is not None: mask attention_mask.bool() loss -self.crf(logits, labels, maskmask, reductionmean) return loss, logits else: predictions self.crf.decode(logits, maskattention_mask.bool()) return logits, predictions逻辑说明BERT 的输出 last_hidden_state 维度是 [batch, seq_len, 768]经过 dropout 后进入双向 LSTM输出维度变成 [batch, seq_len, 512]。classifier 把这 512 维投影到标签数量上得到每个 token 对每个标签的发射分数。训练时CRF 层计算负对数似然作为 loss推理时CRF 的 decode 方法返回每个序列的最优标签路径。参数说明lstm_hidden256是 LSTM 隐层维度双向后线性层输入就是 512。这个值你可以在 128 到 512 之间调太小欠拟合太大容易过拟合。dropout0.5是 BERT 输出后的丢弃比例如果你的训练集很小建议加到 0.6反之可以降到 0.3。注意 PyTorch 默认的 LSTM 第一维是 seq_len所以要设置batch_firstTrue和 transformers 的输出保持一致。4.2 损失函数与解码为什么用负对数似然而不是交叉熵如果你把 NER 当作普通多分类问题每个 token 独立用交叉熵就会丢掉标签依赖。CRF 的损失函数不是交叉熵而是整个标签序列的负对数似然。简单说CRF 会计算所有可能标签序列的总得分然后用正确序列的得分除以总得分再取负对数。torchcrf 把这个过程封装成了-crf(logits, labels, mask)。解码部分Viterbi 算法会动态规划寻找得分最高的标签序列。使用 torchcrf 时你不需要理解矩阵推导但要知道 decode 返回的是一个 listlist 里每个元素是当前 batch 中该样本的标签序列。由于 batch 里有 paddingdecode 返回的序列长度可能不一致直接强制转 tensor 会报错。我在推理时通常保留 list 形式再做后处理对齐到原句子长度。4.3 训练主循环与分层学习率让 BERT 和下游层各学各的训练 BERT 系模型时最忌讳所有参数用一个学习率。BERT 是预训练模型微调时学习率太大会覆盖掉原有知识BiLSTM 和 CRF 是从零开始太小又学不动。我一般把参数分为两组BERT 参数学习率 5e-5下游参数学习率 1e-3。下面是一个训练主循环的核心代码# train.py训练主循环核心部分 import transformers from torch.optim import AdamW # 分层设置学习率 bert_params [p for n, p in model.named_parameters() if bert in n] downstream_params [p for n, p in model.named_parameters() if bert not in n] optimizer_grouped_parameters [ {params: bert_params, lr: 5e-5}, {params: downstream_params, lr: 1e-3} ] optimizer AdamW(optimizer_grouped_parameters) # warmup 与总步数 total_steps len(train_dataloader) * num_epochs warmup_steps int(total_steps * 0.1) scheduler transformers.get_linear_schedule_with_warmup( optimizer, num_warmup_stepswarmup_steps, num_training_stepstotal_steps ) # 训练循环 for epoch in range(num_epochs): model.train() for batch in train_dataloader: input_ids batch[input_ids].to(device) attention_mask batch[attention_mask].to(device) labels batch[labels].to(device) loss, _ model(input_ids, attention_mask, labels) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) optimizer.step() scheduler.step() optimizer.zero_grad()参数说明warmup_steps设为总步数的 10% 到 15% 比较常见让模型在训练初期用一个很小的学习率稳定方向。clip_grad_norm_(5.0)用来防止梯度爆炸尤其是 BiLSTM 部分如果梯度没裁剪很容易在第三四个 batch 就让 loss 变 NaN。num_epochs在小数据集上一般设 10 到 20但我建议每个 epoch 后都跑一次验证集保存效果最好的权重。训练时如果你发现 loss 在第二个 epoch 开始反弹大概率是学习率太大或 warmup 太短。不要急着改模型结构先调学习率。4.4 加载微调后的模型做推理标题里“模型.zip”的正确用法拿到压缩包里已经训练好的模型你不需要重新训练。常见做法是先用项目使用说明确认模型保存的格式。如果是 PyTorch 的 state_dict那你要先初始化完整的 BertBiLSTMCRF 模型再 load_state_dict。下面是一个最小的推理脚本# infer.py加载微调模型并预测实体 import torch from transformers import BertTokenizerFast tokenizer BertTokenizerFast.from_pretrained(bert-base-chinese) model BertBiLSTMCRF(bert_pathbert-base-chinese, num_tagslen(label2id)) state torch.load(model.zip/ner_model.pt, map_locationcpu) model.load_state_dict(state[model_state_dict]) model.eval() text 我昨天去了北京大学参观 inputs tokenizer(list(text), is_split_into_wordsTrue, return_tensorspt) with torch.no_grad(): logits, predictions model( inputs[input_ids], attention_maskinputs[attention_mask] ) # predictions[0] 是标签 id 序列转回文本标签 pred_tags [id2label[t] for t in predictions[0]] for char, tag in zip(list(text), pred_tags): if tag ! O: print(char, tag)逻辑说明加载权重时bert_pathbert-base-chinese仍然是必须的因为 BertBiLSTMCRF 类在初始化时就要加载 BERT 基础权重。如果直接用随机初始化的 BERT 再 load_state_dict会产生 key 不匹配。如果你的压缩包里给的预训练模型不是 pytorch_model.bin而是说用 TensorFlow 训练出来的那需要先用transformers的转换脚本转一下这个坑在第 5 章会重点讲。5. 避坑与常见问题排查BERT 系中文 NER 最容易翻车的 5 个地方5.1 标签错位预测结果整体偏移一位训练损失却正常现象训练 loss 在下降验证集 F1 也不低但把预测结果打印出来时发现所有实体的边界都往后错了一个字。比如“北京”被预测成“北京”后面的“大”是 B-LOC。原因BERT tokenizer 加了 [CLS] 在句首很多初学者直接拿原始标签和 input_ids 对齐没有用 word_ids 映射导致模型学到的实际是偏移后的序列。更隐蔽的是如果标签列表里的元素个数比 tokenizer 输出少一个PyTorch 的 cross_entropy 不会报错只会默默错位。解决一定要用我前面写的对齐函数用 -100 标记特殊 token。并且在训练前打印出首条数据的前 10 个 token 和对应标签人工核对一遍不要直接开训练。这一点属于“已经不是坑而是习惯”的级别。5.2 学习率玄学统一用 5e-5 让 BiLSTM 和 CRF 学不动现象训练五个 epoch 后验证集 F1 停留在 70 左右模型大量输出 O只有 BERT 部分在变化。原因BiLSTM 和 CRF 是随机初始化的如果用 5e-5 这种专为 BERT 设计的微调学习率它们的参数更新幅度太小几十个 epoch 都学不出有效的转移矩阵。这类问题表面看是模型结构不对实际是学习率分配问题。解决采用分层学习率BERT 部分 5e-5下游 1e-3。如果用了分层学习率后训练波动太大再给下游层加一个单独的 warmup或者把下游层学习率降到 5e-4。我见过有项目使用说明里直接给了一套“祖传超参”照抄可以但要留个心眼人家的数据规模、标签数和你不同。5.3 CUDA OOM调大 batch_size 就崩溃回退策略是什么现象batch_size 从 16 调到 32 后直接报 CUDA out of memory哪怕数据量很小。原因BERT 模型本身占用显存很大序列长度 128 时一个 batch 的中间激活值可能占掉几 GB 显存。如果你的显卡只有 8GB 显存batch_size 到 16 基本就是极限。解决首先把 max_len 从 128 降到 64大部分中文命名实体的上下文窗口 64 足够。其次用梯度累积模拟大 batch每 4 个 batch 累积一次梯度再更新参数等效 batch 变成 4 倍但显存不变。最后可以考虑混合精度PyTorch 直接用torch.cuda.amp包一下 forward 和 backward。还有一种办法是把 BERT 换成更小的中文预训练模型但这种模型通常要单独下载权重格式不一定兼容非必要不要换。5.4 实体类别不均衡F1 看着还行小类实体全军覆没现象语料里 90% 都是 PER 和 LOCORG 只有几百条。训练完 PER 的 F1 有 85ORG 的 F1 是 0。模型在预测时几乎从不输出 B-ORG。原因数据不均衡在 NER 里是常态但模型对低频实体类的输出偏置非常敏感。CRF 层会学到“输出 O 或 PER 的转移更安全”于是低频实体被牺牲掉。解决最简单的做法是在训练时对 loss 中的标签做权重或者对低频实体做数据增强。我常用的是把低频实体的样本复制几份同时在实体文本上做轻微替换比如把机构名里的“公司”换成“集团”。注意不要简单地重复同一个句子否则会造成过拟合。另外一个建议是把标签分布打印出来如果某个类别占比不到 5%你要么加数据要么考虑把它和大类合并而不是硬训练。5.5 预训练模型下载慢或超时第一次运行卡在 from_pretrained现象代码执行到BertModel.from_pretrained(bert-base-chinese)就长时间没反应或者报连接超时。这不是你的代码问题而是模型权重要从外网下载国内访问经常失败。原因transformers 默认从官方模型库下载模型网络状况不好时几十到几百 MB 的权重很难拉下来。很多打包项目里所谓的“模型.zip”未必包含 BERT 基础权重只包含微调后的下游权重所以训练时还是要联网拉基础模型。解决优先使用国内镜像下载或者手动下载后放到本地目录。常见做法是设置环境变量HF_ENDPOINThttps://hf-mirror.com然后再跑 from_pretrained。如果项目使用说明里写了模型文件路径那就直接把bert_path改成本地权重目录目录里要有 config.json 和 pytorch_model.bin。手动下载时注意版本中文 BERT 的权重文件大小大概在 400MB 左右不要拿一个几 MB 的文件糊弄自己。6. 从能跑到能用严格 F1 评估、半精度推理和一个我保留的验证习惯6.1 用 seqeval 做严格 span 级评估分类准确率在 NER 上没有意义因为绝大部分标签是 O全预测 O 也能刷到高分。我评估时固定使用 seqeval并且用严格的 span 级别 F1也就是实体必须边界和类型同时完全正确才算对一个。代码很简单from seqeval.metrics import classification_report y_true [[B-PER, I-PER, O], [O, B-LOC]] y_pred [[B-PER, I-PER, O], [O, B-LOC]] print(classification_report(y_true, y_pred))seqeval 要求标签是字符串列表而不是 id 列表所以预测后一定要用 id2label 转回去。如果你的标签里有 -100在评估之前要过滤掉否则会把特殊 token 也放进 y_pred导致长度不匹配。6.2 半精度推理与批量解码模型上线时如果追求速度可以在 GPU 上用半精度预测。做法是model.half()同时输入也要.half()。但注意 CRF decode 对数值精度比较敏感半精度下偶发边界选择异常。我通常保留一个全精度模型做最终验证半精度只用于线上批量推理。批量解码时把所有句子 padding 到固定长度但 decode 返回的序列长度参差不齐你要用原始长度截断。6.3 保留“未见过语料”做人工复核我吃过一次亏模型在公开测试集上 F1 很高上线后却对“简称”识别得极差比如“北大”没有被识别成机构。后来我发现测试集里恰好没有这类写法。从那以后每个 NER 项目我都会额外留一份从业务场景找来的未标注语料人工标少量样本专门测试模型在改写、简称、中英混排上的表现。这个习惯帮我躲过了至少两次发布事故。如果你准备在这套源码上投入我建议你从这个动作开始把项目包跑通不算完拿你的真实数据样本去碾压一下这个模型看到预期外的结果再回头调数据。希望帮到你。本文还有配套的精品资源点击获取