
简介一份面向Python开发者和NLP初学者的中文命名实体识别NER实战资源专注讲解如何借助预训练BERT模型完成中文人名、地名、组织名等实体抽取。资源使用Hugging Face Transformers与PyTorch生态覆盖从数据预处理、模型微调到评估部署的完整流程适合希望将BERT落地到中文文本分析任务的学习者。压缩包共9个文件以Python脚本、TXT数据文件、Markdown说明及评估工具为主总大小3.72MB包含BERT_NER.py主程序、tf_metrics.py评估脚本、conlleval.pl标准评测工具以及train/dev/test格式标注数据集结构清晰便于直接运行与二次开发。配套README和vocab词表可辅助理解模型配置整体能帮助读者快速搭建中文NER实验环境掌握实体识别任务的实现细节与调优思路。已有3331人学习下载适合NLP入门及信息抽取项目参考。 做中文命名实体识别NER这件事放在三年前还是一套让人头疼的特征工程流程分词、词性标注、手工特征模板、CRF解码每次换一个领域就要重复造一遍轮子。现在用预训练语言模型BERT做中文NER本质上就是“一个分类任务加上一点序列对齐的细节”上手门槛远没有很多人想象得高。我最近用一个公开中文数据集完整跑了一遍基于BERT的中文NER流程从环境配置、数据预处理、模型微调到实体抽取踩了几个文档里不会写的坑也整理出一套能直接复现的方案。这篇就当作一份从零到一的实践笔记适合刚接触NLP、想用Python跑通预训练模型做实体识别的朋友参考。1. 为什么中文NER绕不开BERT1.1 传统NER的痛点和BERT的解决思路过去做中文NER最主流的套路是“字向量或词向量 BiLSTM CRF”。这套方案在当时已经是工业界主力但它有几个绕不过去的麻烦分词误差会直接传导到实体边界判断上一个实体被分错词后面全乱词向量是静态的同一个词在不同语境下的含义被压成一个固定向量“苹果”在“苹果公司”和“吃苹果”里向量一模一样模型只能靠大量标注数据硬掰。CRF虽然能约束标签序列的转移关系但前面喂给它的特征质量如果不高解码出来的结果也不会好。BERT之所以能颠覆这个局面核心在于它用大规模语料预训练出来的上下文相关表示。同一个“苹果”在“苹果公司发布了新品”和“他拿起苹果咬了一口”两个句子里BERT输出的向量完全不同模型天然知道这里有一个机构实体和一个水果。这种“下游任务只做微调”的思路把之前手工设计特征的工作直接干掉了。中文NER从此变成一件很纯粹的事把句子切成一串token给每个token打一个标签整理成分类任务就能训练。1.2 中文场景下BERT的特殊性BERT在中文上有一个先天优势bert-base-chinese这个预训练模型的词典是基于汉字构建的分词器基本按“字”切分绝大多数情况下一个汉字对应一个token。这意味着中文NER的标签对齐比英文简单得多英文里一个词可能被WordPiece切成三个token标签要跟着换中文直接给每个字打标签就行。但注意这只是“大多数情况”当句子中混入英文、数字、特殊符号时tokenizer还是会把它们拆成子词比如“iPhone14”可能被切出好几个片段对齐逻辑不能想当然。另外BERT的输入必须带[CLS]和[SEP]两个特殊token它们对应的位置没有实体标签训练时要把这些位置的标签忽略掉。预训练模型在中文文本上的泛化能力很好哪怕标注数据只有几千条也能训练出一个能用的NER模型这是静态词向量时代很难做到的事。2. 环境准备、模型选型与标签体系2.1 环境与依赖安装我在Windows和Linux上都跑过这套流程核心依赖其实就三个transformers、torch和seqeval。如果你的Python环境还没配好先去把Python 3.8以上版本装好再按下面的命令安装依赖pip install transformers4.30.2 pip install torch --index-url https://download.pytorch.org/whl/cu118 pip install seqeval datasetstorch的安装要注意CUDA版本。纯CPU环境也能跑通流程但训练速度会慢到让人怀疑人生我有一次用纯CPU训练一个小数据集一个epoch跑了将近四十分钟换到单张消费级显卡后不到三分钟。如果你用的是VSCode或者PyCharm记得把Python解释器切到你安装依赖的这个虚拟环境里我身边不少朋友卡在“明明pip install成功了运行还是ModuleNotFoundError”基本就是解释器没选对。数据集我建议用开源的CLUENER2020它有十类实体标注数据量适中格式规范非常适合用来练手。不过它给的标签比较细十类实体对新手来说打印出来的预测结果会有点乱拿它做训练没问题如果只想快速看效果也可以自己标一个很小的“人名/地名/机构名”三类数据集几十条就能跑通。2.2 模型选型中文NER选预训练模型入门选bert-base-chinese是最稳的。它是一个base体量的中文BERTHugging Face上可以直接下载权重大概400多MB单卡能跑部署也不费劲。Hugging Face上有几个和它类似的候选RoBERTa-wwm-ext中文模型用了全词掩码策略在一些中文任务上表现稍好MacBERT等模型也有不少社区反馈不错。但第一次跑通流程时我不建议在这些模型之间纠结因为它们的接口用法几乎一样只要把预训练模型名换一下就行。技术选型的判断标准应该是“先跑通再优化”等你的评估指标、数据管线、推理逻辑都稳定了再去对比不同模型在同一个验证集上的F1分数这样才有意义。2.3 标签体系选择NER标签体系常见的有两种BIO和BIOES。BIO用B表示实体开始I表示实体内部O表示非实体BIOES在BIO基础上增加了E实体结束和S单字实体。对中文来说单字实体很常见比如人名“李白”如果只有“李”一个字符BIO只能标为B-PER模型需要靠后续标签判断它是不是一个单字实体BIOES就直接标为S-PER信息更完整。我的建议是第一版先把BIO跑通因为大部分开源数据集和教程默认BIO资料多、好排查问题。等你把整个流程跑顺了想提升效果可以再换BIOES。这里要记得先构建一个标签表并保存成JSON文件训练和推理都要用同一个映射一个字符大小写不对就会导致标签错乱。以“人名/地名/机构名”三类实体为例我的标签列表是这样组织的labels [O, B-PER, I-PER, B-LOC, I-LOC, B-ORG, I-ORG] label2id {label: i for i, label in enumerate(labels)} id2label {i: label for label, i in label2id.items()}3. 数据预处理把中文句子变成BERT的输入3.1 分词与标签对齐数据预处理是整条流程里最需要细心的地方一句话说错就可能让模型在训练时“看到”错位的标签。流程大致是用tokenizer把句子转成input_ids同时保持每个token的标签对齐。对于bert-base-chinese常规中文句子几乎是一个字对应一个token所以一个最直接的做法是把句子拆成字符列表把每个字符的标签转成id再在前面和后面补上特殊token对应的标签一般用-100表示忽略。但要注意bert-base-chinese的tokenizer并不是百分百按字切分的。句子中出现英文单词或数字时它可能被切分成多个WordPiece片段出现未登录符号时还可能变成[UNK]。一旦发生这种情况标签和token的数量就对不上了。比较稳妥的做法是用tokenizer返回的offset_mapping或者word_ids来对齐。简单地说拿到每个token在原始句子中对应的字符范围把该token的标签设为这个范围对应标签中的第一个字符的标签其余特殊token一律设为-100。用transformers的tokenizer配合return_offsets_mappingTrue就能拿到这个映射关系。下面是一段我实际用的编码函数处理了上述对齐问题from transformers import BertTokenizerFast tokenizer BertTokenizerFast.from_pretrained(bert-base-chinese) def encode_example(chars, tags, label2id, max_len128): # chars: 句子拆成的字符列表, tags: 与字符一一对应的标签字符串列表, 如 [B-PER, I-PER, O] enc tokenizer(chars, is_split_into_wordsTrue, truncationTrue, paddingmax_length, max_lengthmax_len, return_offsets_mappingTrue) label_ids [] for offset in enc[offset_mapping]: if offset[0] 0 and offset[1] 0: label_ids.append(-100) # 特殊token [CLS] [SEP] [PAD] else: char_idx offset[0] label_ids.append(label2id.get(tags[char_idx], 0)) enc[labels] label_ids return enc这个函数里有个细节char_idx offset[0]对普通单字token来说offset的起始就是那个字在字符列表中的下标对英文子词来说比如“iPhone”的第2个token“phone”偏移量指向的字符位置是“iPhone”这个整体在原始列表中的位置所以标签不会错位。3.2 数据集加载与批次构建数据处理好之后需要把它转成PyTorch Dataset再交给DataLoader构造批次。训练时要设置attention_mask让模型忽略padding位置标签里的-100会被PyTorch的交叉熵损失函数自动识别为忽略位置。下面是我的Dataset实现import torch from torch.utils.data import Dataset class NERDataset(Dataset): def __init__(self, encodings): self.encodings encodings def __getitem__(self, i): return { input_ids: torch.tensor(self.encodings[input_ids][i]), attention_mask: torch.tensor(self.encodings[attention_mask][i]), labels: torch.tensor(self.encodings[labels][i]), } def __len__(self): return len(self.encodings[input_ids])批次大小根据显存来定。我的经验是12GB显存跑bert-base-chinesebatch_size设16比较舒服设32会偶尔OOM。如果你显存不够可以把max_len从128降到64或者改用gradient_accumulation_steps来模拟更大的批次。这里有个小坑paddingmax_length会把所有样本都padding到128数据量大的时候会浪费很多算力。如果句子普遍不长建议用paddinglongest在每个batch内做动态padding训练速度会明显提升。4. 模型搭建、训练与评估4.1 模型搭建Hugging Face的Transformers库提供了开箱即用的BertForTokenClassification它会自动在BERT输出之上加一层线性分类器输出维度等于标签数量。加载方式非常简单from transformers import BertForTokenClassification model BertForTokenClassification.from_pretrained( bert-base-chinese, num_labelslen(labels), id2labelid2label, label2idlabel2id )如果你的电脑网络无法直接访问Hugging Face可以在能联网的机器上先把模型下载到本地目录然后from_pretrained(./bert-base-chinese)加载。这也是我在内网环境里常用的做法。4.2 训练参数配置与循环训练配置有几个关键参数新手容易选错。学习率建议用5e-5这是BERT微调通用的起点再配合warmup比例0.1也就是前10%的训练步数内学习率从0线性升到5e-5后面再线性衰减。优化器用AdamWweight_decay设0.01。不要使用SGDBERT微调用SGD不仅收敛慢效果也明显更差。我偏好用Hugging Face的Trainer它把训练循环、评估、日志都封装好了适合快速跑通。下面是一个直接用训练集和验证集构造Trainer的例子from transformers import Trainer, TrainingArguments training_args TrainingArguments( output_dir./ner_bert_output, evaluation_strategyepoch, save_strategyepoch, learning_rate5e-5, per_device_train_batch_size16, per_device_eval_batch_size32, num_train_epochs3, weight_decay0.01, warmup_ratio0.1, logging_dir./logs, logging_steps50, load_best_model_at_endTrue, metric_for_best_modeleval_f1, greater_is_betterTrue, ) trainer Trainer( modelmodel, argstraining_args, train_datasettrain_dataset, eval_datasetvalid_dataset, tokenizertokenizer, compute_metricscompute_metrics, ) trainer.train()训练轮数我建议先从3个epoch开始。BERT微调不是训越多越好中文数据量小的情况下训练轮数过多容易过拟合验证集的F1在高点之后很快往下掉我在CLUENER2020上实测3到4个epoch是比较舒服的范围。4.3 评估指标与边界NER的评估不像普通分类任务只算acc就行。实体识别更关心实体级别的精确率、召回率和F1一个实体被判定正确要求它的类型和边界都对。这意味着“李小明”三个字标签预测成了“李/小明”两个实体片段即使标签类型都对在整个实体级别的评估里也只能算既丢了召回又丢了精确。所以评估时我直接用seqeval它按实体级别的token序列计算指标from seqeval.metrics import classification_report, f1_score import numpy as np def compute_metrics(eval_pred): predictions, labels eval_pred predictions np.argmax(predictions, axis-1) true_labels [[id2label[int(l)] for l in label_seq if l ! -100] for label_seq in labels] true_predictions [] for pred_seq, label_seq in zip(predictions, labels): pred_label_seq [] for p, l in zip(pred_seq, label_seq): if l ! -100: pred_label_seq.append(id2label[int(p)]) true_predictions.append(pred_label_seq) return { precision: classification_report(true_labels, true_predictions, output_dictTrue)[micro avg][precision], recall: classification_report(true_labels, true_predictions, output_dictTrue)[micro avg][recall], f1: f1_score(true_labels, true_predictions) }注意这里必须把-100的标签位置过滤掉否则id2label[-100]直接KeyError这个错我见过很多次了。5. 推理与实体抽取落地5.1 预测与标签还原模型训练完后推理阶段的核心就是把输出转成实体。先加载模型把输入句子编码得到每个token的标签预测然后过滤掉特殊token再拼回原始序列。这里有个容易犯的错误模型预测的标签id需要过滤padding后按token顺序排好但BERT的tokenizer对中文句子可能加入了[UNK]或拆分token所以最好用offset_mapping把预测结果准确映射回原始字符位置而不是简单挑出非特殊token否则带英文或数字的句子还原出来会错位。推理代码看起来是这样的from transformers import BertForTokenClassification, BertTokenizerFast model BertForTokenClassification.from_pretrained(./ner_bert_output/checkpoint-best) tokenizer BertTokenizerFast.from_pretrained(bert-base-chinese) def predict(text): chars list(text) inputs tokenizer(chars, is_split_into_wordsTrue, return_tensorspt, truncationTrue, max_length128) with torch.no_grad(): logits model(**inputs).logits pred_ids torch.argmax(logits, dim-1)[0].tolist() offsets tokenizer(text, return_offsets_mappingTrue)[offset_mapping] labels [] for offset, pid in zip(offsets, pred_ids): if offset ! (0, 0): labels.append(id2label[pid]) char_labels [] idx 0 for offset in offsets: if offset (0, 0): continue end offset[1] while idx end: char_labels.append(labels[len(char_labels)] if len(char_labels) len(labels) else O) idx 1 return list(zip(chars, char_labels))5.2 实体抽取逻辑拿到每个字符的预测标签之后还需要把它们合并成完整的实体片段。核心逻辑就是扫描序列碰到B-XXX就开启一个新实体记录起始位置和类型后面连续的I-XXX如果有相同类型就继续追加直到遇到O或者新实体开始。这里要注意如果出现B-PER后面跟着I-ORG这种异常序列属于模型预测的非法转移稳妥做法是忽略这个I标签不把它并入当前实体这样至少不会产生一个混搭的错误实体。实体抽取函数实现如下def extract_entities(char_labels): entities [] current_type None current_entity [] for char, label in char_labels: if label.startswith(B-): if current_entity: entities.append((current_type, .join(current_entity))) current_type label[2:] current_entity [char] elif label.startswith(I-) and current_type label[2:]: current_entity.append(char) else: if current_entity: entities.append((current_type, .join(current_entity))) current_type None current_entity [] if current_entity: entities.append((current_type, .join(current_entity))) return entities这个函数虽然不长却直接影响落地的可用性。我测试一个句子“李明到北京出差参观了百度公司”输出大致是[(PER, 李明), (LOC, 北京), (ORG, 百度公司)]基本符合预期。6. 常见问题与避坑记录6.1 常见问题速查表我把实操中比较容易踩到的问题整理成一张表方便你排错时直接对照。现象可能原因解决办法训练时loss一直是常数标签没有对齐模型没有学到有效信息检查tokenizer的offset_mapping对齐逻辑确认特殊token位置标签是-100验证集F1为0标签体系不一致推理时映射错乱检查id2label和label2id是否互相对应模型加载的配置文件是否指向旧标签表预测结果全是O训练数据太少或训练轮数不足检查数据量可适度增加epoch或调低学习率显存OOMbatch_size过大或max_len过长降低batch_size或用gradient_accumulation_steps补偿英文单词预测不对bert-base-chinese对英文子词切分导致对齐错位用offset_mapping按字符位置回填不要直接按顺序贴标签加载AutoModel报错transformers版本过低或过高固定使用transformers 4.30.2左右的稳定版本seqeval报标签不一致传入了数值标签而非字符串标签确保传给seqeval的是字符串列表如B-PER6.2 实操心得与更深一层的优化思路第一版跑通已经算完成了70%的技术工作剩下的优化要基于验证集指标来做而不是盲目调参。我在实际项目里常用的优化顺序是先把数据清洗干净确认标签对齐没问题然后检查类别不平衡如果某些实体类别占比特别低模型倾向于把它们预测成O可以考虑调整损失函数权重简单做法是将loss_fct CrossEntropyLoss(ignore_index-100, weightclass_weights)替换默认的损失函数最后才是调学习率、轮数这些超参数。不要在数据都没确认干净的时候就去搜索参数组合那是在浪费时间。从业务落地角度看BERT做NER还有一个经常被忽视的问题——推理速度和部署资源。如果只是小批量跑加载完整BERT模型完全没问题但如果要应对高并发请求就应该考虑蒸馏一个小的NER模型或者把BERT的输出特征导出后交给更轻量的分类器。先用BERT拿到效果上限再考虑性能优化这是我比较推荐的一条路径。最后再分享一个我自己实践中的体会中文BERT在做实体识别时很多“烂效果”其实不是模型不行而是数据管线没对好。尤其是标签对齐这类细节如果不亲手写一遍并画出每个token对应的标签你很难意识到里面有多少隐含假设。跑通这套流程之后你会发现迁移到阅读理解、文本分类等任务时大量代码都是相通的——预训练模型的路径一致难点永远是“任务结构和数据怎么映射到模型输入”这件事。希望这篇笔记能帮你少踩几个坑把注意力放在真正的业务问题上。本文还有配套的精品资源点击获取