拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

BERT中文命名实体识别实操:从原理到避坑指南

BERT中文命名实体识别实操:从原理到避坑指南

简介:面向Python开发与自然语言处理学习者,这份资料聚焦如何利用预训练BERT模型完成中文命名实体识别(NER),涵盖人名、地名、组织名等实体提取。资源共9个文件,压缩包仅3.72MB,包含Python训练/评估脚本、划分好的中文语料数据、说明文档、评估脚本及效果示意图,结构紧凑,便于对照学习。已有3335人学习下载。内容从BERT的Transformer结构与掩码语言模型出发,延伸到中文输入的分词与特殊令牌处理,并讲解IOB标注格式、数据预处理、模型微调、优化器选择与Precision/Recall/F1评估指标等关键环节。配合自带的数据集与脚本,可系统掌握使用Python和Hugging Face Transformers库搭建中文NER模型的完整流程,适合有基础Python知识、希望快速上手NLP实战的开发者。

1. 用 BERT 做中文 NER:一份能直接复现的实操笔记

说实话,第一次接到“用 BERT 做中文 NER”这个需求时,我心里是拒绝的。领导丢来一批中文病历,让我把“症状”“药名”“检查项”这类实体自动捞出来,还说“用个预训练模型就行”。做过 NLP 的人都知道,中文命名实体识别不是套个模型那么简单,分词边界、标签对齐、OOV 人名,哪个都能让你调一整天。这篇文章把我自己拆过的一套完整流程写出来——从 BERT 原理讲清楚“为什么它能做”,到数据标注、微调、预测、部署,每一步给到可以直接抄的代码和参数,最后列出我实际踩过的五个坑。适合刚入门 Python NLP 的人照着复现,也适合已经跑过文本分类、想转序列标注的开发者查漏补缺。

2. 为什么中文 NER 首选 BERT:原理、分词与选型

2.1 BERT 到底靠什么认出实体

很多人把 BERT 当成一个“黑匣子”,跑通了就完事,但 NER 这种 token-level 的任务,不理解原理是真的会翻车的。BERT 的本质是一个多层双向 Transformer 编码器,它在大规模语料上做过两个预训练任务:掩码语言模型(随机盖住一部分 token,让模型根据上下文猜)和下一句预测。这两个任务让 BERT 学会的不是“这个词什么意思”,而是“这个词在当前上下文里是什么意思”。

这就和 NER 强绑定了。实体识别的核心是消歧:同样一个“苹果”,在“苹果公司发布新机”里是组织名,在“我吃了一个苹果”里是普通名词。BiLSTM 这类模型虽然也能看上下文,但受限于单向或浅层双向的结构,长距离依赖抓得不好。BERT 用自注意力机制,每个 token 都能直接看到句子里所有其他 token,而且是十几层叠着看,所以它输出的每个 token 的向量表示,天然携带了整个句子的语境信息。

NER 任务实际做的是序列标注:给每个 token 打个标签,比如“B-PER”表示人名开头,“I-LOC”表示地名中间。BERT 的最后一层隐藏状态(hidden state)就当作这个 token 的特征,后面接一个线性分类层,把特征映射到标签空间上。这就是BertForTokenClassification这个模型类做的事——它只在 BERT 输出层之上加了一个全连接层,没有改 BERT 本身的结构。

所以选型理由很直接:如果你没有海量领域数据重新预训练,直接用 BERT 的特征做微调,是效果和成本之间最平衡的方案。领域数据少的时候,BERT 在大规模通用语料上学到的语言知识能帮你兜底;领域数据多的时候,微调又能把通用知识对齐到你的实体类型上。

2.2 中文分词是第一个暗坑

BERT 用的分词器是 WordPiece,英文里它会把不认识的词拆成 subword,比如 “playing” 拆成 “play” 和 “##ing”。但中文不太一样。中文没有天然空格,hfl/rbt3、bert-base-chinese这些中文预训练模型的分词器,绝大多数情况下是“按单字切”的。“张三”会被切成['张', '三']两个 token,而不是一个整体。

这个特性对 NER 影响非常大。你拿到的原始标注往往是字符级的,比如张/B-PER 三/I-PER,而模型看到的是 token 级。单个汉字本身就是 token,这种情况下字符级标签可以直接平移到 token 级,不会错位。但如果你用的是bert-base-chinese之外的模型,或者你给的是词级标注,那就要小心了——某些中文模型会把常见词直接切成一个 token,比如“北京”可能是一个 token。这时候字符级标签就没法一一对应了,必须做标签对齐。

我自己一般会这样判断:先跑一遍tokenizer.tokenize("张三去北京"),看一眼输出。如果是['张', '三', '去', '北', '京'],那就按字符级标签走;如果是['张', '三', '去', '北京'],那“北京”这个 token 只能对应一个标签,你得决定它是取第一个字符的标签还是整个 token 的标签。没有统一标准,但必须在数据处理里保持一致。

2.3 BERT、BERT+BiLSTM、BERT+CRF 怎么选

中文 NER 的技术栈大概有三档。第一档是纯 BERT 微调,就是BertForTokenClassification,输出层直接接 Softmax,在公开数据集上效果已经不错,适合快速上线。第二档是 BERT + BiLSTM,在 BERT 输出上再接一层 BiLSTM 捕捉局部依赖,理论上能修正一些标签跳变,但训练时间和显存都上去了,提升幅度在 1~2 个点左右。第三档是 BERT + CRF,这是序列标注的“标配”,因为 Softmax 是逐 token 预测的,它不知道“B-PER 后面必须跟 I-PER 或 O”这种约束,而 CRF 层能学习标签之间的转移概率,让预测结果在整体路径上更合理。

我的建议是:项目时间紧、数据量不大,直接上纯 BERT 微调,跑通流程再说。如果你想要更高精度,且资愿意等训练时间,就加 CRF。transformers库里有BertForTokenClassification,但没有直接封装好的 BERT+CRF 类,CRF 一般用torchcrf这个库自己拼。我见过不少人在 ORG 这类多词实体上翻车——模型把“中”“国”“银”“行”预测成B-ORG I-ORG I-ORG B-ORG,这就是典型缺 CRF 的表现。不过这个阶段先别急着优化,先把流程跑通,后面避坑章节会具体说。

3. 跑通最小推理脚本:环境、目录与代码结构

3.1 环境准备与依赖清单

环境配置是 Python 入门者的第一道坎,我在这上面浪费过一下午,所以直接给你一份能用的清单。建议用 Python 3.8~3.10,装一个虚拟环境再动手,别直接往全局环境里塞依赖,不然以后pip install什么都是“已安装”但import报错,你会疯掉的。

requirements.txt 内容如下:

transformers==4.36.2 torch>=2.0.0 datasets==2.16.1 seqeval==1.2.2 numpy>=1.24.0

安装命令:

pip install -r requirements.txt python -c "from transformers import AutoTokenizer; print('ok')"

逻辑说明:transformers库负责加载预训练模型、分词器和微调用的模型类,前面写到的BertForTokenClassification就在里面。datasets用于加载和切分数据,seqeval是 NER 任务专用的评估工具,能直接算实体级别的精确率、召回率、F1。第一行命令执行完以后,第二行命令用来验证库是否装成功,如果打印出ok,说明基础环境没问题。

注意torch是跟着你的机器有无 GPU 走的。有 NVIDIA 显卡就装 CUDA 版,没有就装 CPU 版。CPU 版也能跑通流程,只是训练慢一些,推个小样本数据完全没问题。用pip install torch默认装的是 CPU 版,想装 CUDA 版要指定 index-url 或从官网下载,这里不展开,你按本机环境查一步就行。

3.2 项目目录与模型加载

我一般把项目的文件结构固定成这样,后期维护不用动脑:

my_ner/ ├── checkpoints/ # 微调后的模型保存目录 ├── data/ │ ├── train.txt # 训练数据,BIO标注 │ └── dev.txt # 验证数据 ├── src/ │ ├── predict.py # 推理脚本 │ ├── train.py # 微调脚本 │ └── utils.py # 标签对齐等工具函数 └── requirements.txt

模型加载这一步也有讲究。加载预训练权重时,AutoTokenizer和BertForTokenClassification必须用同一个模型名,比如都填hfl/rbt3。这两者像是一对配套零件,混搭会导致 tokenizer 切出来的 token 编号和模型词汇表对不上,推理结果全乱。

# -*- coding: utf-8 -*- # 文件:src/predict.py # 作用:加载预训练BERT,对单条文本做NER推理 import torch from transformers import AutoTokenizer, BertForTokenClassification MODEL_NAME = "hfl/rbt3" # 轻量中文BERT,下载快,适合CPU跑 label_list = ["O", "B-PER", "I-PER", "B-LOC", "I-LOC", "B-ORG", "I-ORG"] id2label = {i: label for i, label in enumerate(label_list)} label2id = {label: i for i, label in enumerate(label_list)} tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME) model = BertForTokenClassification.from_pretrained( MODEL_NAME, num_labels=len(label_list), id2label=id2label, label2id=label2id, ) model.eval() text = "张三在北京的阿里巴巴上班" inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=128) with torch.no_grad(): logits = model(**inputs).logits # 形状: [1, seq_len, num_labels] preds = logits.argmax(dim=-1).squeeze(0) # 去掉batch维 tokens = tokenizer.convert_ids_to_tokens(inputs["input_ids"].squeeze(0)) for token, pred in zip(tokens, preds): if pred.item() != 0: # 0 对应 O,跳过非实体 print(token, id2label[pred.item()])

这段代码是跑通 NER 的最小闭环,核心逻辑就三步:分词器把中文句子变成 token id 序列,BERT 模型对每个 token 输出一个在所有标签上的概率分布,argmax取概率最大的那个标签作为预测结果。注意logits.argmax(dim=-1)是在最后一个维度上取最大值,返回的是[seq_len]的索引数组,需要用id2label转回可读的标签字符串。

参数说明里最容易被忽略的是id2label和label2id。这两张映射表必须和训练时完全一致,否则模型输出索引为 3,你查表查到的是I-PER,但训练时索引 3 对应的是B-LOC,结果全部错位。我建议把这两张表固定写到一个.py文件里,训练和推理都从那里导入,不要各自维护一份。max_length=128表示超出部分截断,短于则填充到 128,这是控制显存和推理耗时最直接的参数,长文本调大到 256 或 512,但模型计算量会线性增长。

第一次跑这个脚本,transformers会自动从 HuggingFace 下载hfl/rbt3模型文件到缓存目录,国内网络可能需要配镜像源,见避坑章节第 3 条。

3.3 一段让新手迷惑的输出解析

跑完上面的脚本,打印结果是这样的:

张 B-PER 三 I-PER 北 B-LOC 京 I-LOC 阿 B-ORG 里 I-ORG 巴 I-ORG 巴 I-ORG

很多人会问:“为什么没有B-ORG的开头标记?” 因为“阿里巴巴”被分词器切成了['阿', '里', '巴', '巴']四个 token,第一个 token “阿”被模型预测为B-ORG,后面三个是I-ORG。这里有个细节要注意:convert_ids_to_tokens返回的列表里,第一个元素是[CLS],最后一个是[SEP],这两个特殊 token 也会参与预测,但它们的预测结果应该被忽略。上面代码里我用pred.item() != 0过滤掉了O,但如果[CLS]被预测成B-PER(模型抽风时确实可能),它就会出现在输出里。严谨的做法是循环时跳过tokens[0]和tokens[-1]。

这一段如果跑通了,说明环境、模型加载、推理链路都没问题。接下来要处理真正的难点——训练数据。

4. 数据准备与 BIO 标注:标签对齐是核心工程

4.1 BIO 标注格式与 label2id 设计

NER 的标注体系常见的有 BIO、BIOES、BMES 三种。BIO 最简单:B表示实体开始,I表示实体中间或结尾,O表示非实体。比如“张三在北京”标注成:

张 B-PER 三 I-PER 在 O 北 B-LOC 京 I-LOC

数据文件常见的存储格式是每行一个“字 标签”,空行分隔句子。这是很多公开数据集的标准格式,比如人民日报标注语料就是这样的。训练脚本读数据时,把每个句子整理成两个等长的列表:一个字列表和一个标签列表,然后让 tokenizer 把字列表拼接成字符串再做处理。

label2id映射表我一般这么设计:

label_list = ["O", "B-PER", "I-PER", "B-LOC", "I-LOC", "B-ORG", "I-ORG"] label2id = {label: i for i, label in enumerate(label_list)} # 输出:{'O': 0, 'B-PER': 1, 'I-PER': 2, 'B-LOC': 3, 'I-LOC': 4, 'B-ORG': 5, 'I-ORG': 6}

这个表的顺序就是模型输出层num_labels的维度顺序,一旦定了就不要改。你训练完模型、保存权重之后,这个表就是你和模型之间的“契约”。我见过有人训练时 label_list 按PER, LOC, ORG排,推理时按ORG, LOC, PER排,结果预测出的实体类型全乱了,这不是模型的问题,是映射表没对齐。

4.2 tokenizer 的 offset_mapping:标签对齐的救命稻草

前面说过,BERT 分词器对中文可能按字切,也可能把常见词切成一个 token。为了应对第二种情况,必须写一段对齐逻辑。transformers提供了offset_mapping,它返回每个 token 对应原始文本的起止字符位置,这正是对齐标签时要用的关键工具。

# -*- coding: utf-8 -*- # 文件:src/utils.py # 作用:将字符级BIO标签对齐到token级,供训练时使用 from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("hfl/rbt3") def align_labels_to_tokens(text, char_labels): """ text: 原始文本字符串,如 "张三在北京" char_labels: 字符级标签列表,长度与text一致 ['B-PER', 'I-PER', 'O', 'B-LOC', 'I-LOC'] """ encoded = tokenizer( text, return_offsets_mapping=True, truncation=True, max_length=128, ) token_labels = [] # offset_mapping[0]是[CLS],[-1]是[SEP],都跳过 for offset in encoded["offset_mapping"][1:-1]: start, end = offset # 某些tokenizer会产生(0,0)的空offset,跳过并标记为-100 if start == 0 and end == 0: token_labels.append(-100) continue # 取token第一个字符对应的标签,作为整个token的标签 token_labels.append(char_labels[start]) return token_labels # 测试 text = "张三在北京" char_labels = ["B-PER", "I-PER", "O", "B-LOC", "I-LOC"] result = align_labels_to_tokens(text, char_labels) print(result) # 如果tokenizer按字切,输出:[1, 2, 0, 3, 4]

这段代码的逻辑是:先拿到每个 token 在原始文本里的起止位置,然后取这个 token 第一个字符的标签作为整个 token 的标签。“北京”如果被切成一个 token,它的 offset 是(2, 4),start=2,对应 char_labels[2] =B-LOC,所以整个 token 的标签就是B-LOC。训练时模型看到这个 token 要预测B-LOC,“京”字虽然没有独立标签了,但信息还在——它的位置信息已经编码进 offset 里了。

-100这个值是个关键参数。在 PyTorch 的交叉熵损失函数CrossEntropyLoss里,默认ignore_index=-100,意思是标签为-100的位置不计算损失。那些因 tokenizer 产生的空 token、或者截断后 padding 出来的位置,都应该标记成-100,让模型在训练时忽略它们,不然模型会对着[PAD]和[CLS]学习毫无意义的标签,白白浪费参数更新。

4.3 训练脚本的写法与关键参数

训练部分我建议直接用 transformers 的Trainer,它封装了训练循环、梯度累积、学习率调度、评估等一整套流程,省去自己写 for 循环的功夫,尤其适合刚接触微调的人。核心代码如下:

# -*- coding: utf-8 -*- # 文件:src/train.py # 作用:加载数据并微调BERT,保存checkpoint import torch from transformers import ( AutoTokenizer, BertForTokenClassification, Trainer, TrainingArguments, DataCollatorForTokenClassification, ) from datasets import Dataset # 1. 准备训练数据(这里仅示意,真实项目从文件读入) texts = ["张三在北京", "阿里巴巴在杭州"] label_lists = [ ["B-PER", "I-PER", "O", "B-LOC", "I-LOC"], ["B-ORG", "I-ORG", "I-ORG", "I-ORG", "O", "B-LOC", "I-LOC"], ] tokenized_data = {"input_ids": [], "attention_mask": [], "labels": []} for text, char_labels in zip(texts, label_lists): token_labels = align_labels_to_tokens(text, char_labels) encoded = tokenizer( text, truncation=True, max_length=128, is_split_into_words=False, ) # 把标签补成与input_ids等长,[CLS]和[SEP]位置填-100 labels = [-100] + token_labels + [-100] tokenized_data["input_ids"].append(encoded["input_ids"]) tokenized_data["attention_mask"].append(encoded["attention_mask"]) tokenized_data["labels"].append(labels) dataset = Dataset.from_dict(tokenized_data) # 2. 定义模型与训练参数 model = BertForTokenClassification.from_pretrained( "hfl/rbt3", num_labels=7, id2label=id2label, label2id=label2id, ) training_args = TrainingArguments( output_dir="./checkpoints", num_train_epochs=3, per_device_train_batch_size=16, learning_rate=3e-5, weight_decay=0.01, logging_steps=50, save_strategy="epoch", save_total_limit=2, ) trainer = Trainer( model=model, args=training_args, train_dataset=dataset, data_collator=DataCollatorForTokenClassification(tokenizer), tokenizer=tokenizer, ) # 3. 开始训练并保存 trainer.train() model.save_pretrained("./checkpoints/final_model") tokenizer.save_pretrained("./checkpoints/final_model") print("训练完成,模型已保存")

逻辑说明:TrainingArguments里的per_device_train_batch_size=16是单卡 batch size,显存不够就调到 8 或 4。learning_rate对 BERT 微调来说,3e-5到5e-5是常见区间,太大容易灾难性遗忘,太小收敛慢。save_strategy="epoch"表示每个 epoch 结束保存一次 checkpoint,save_total_limit=2控制只保留最近两个版本,防止磁盘被撑爆。

数据部分有个容易忽略的点:labels的长度必须和input_ids一致。上面我的写法是[-100] + token_labels + [-100],因为 tokenizer 处理时会自动在开头加[CLS]、结尾加[SEP],所以标签也要对应填充两个-100。如果这里不补,DataCollatorForTokenClassification会报维度不匹配的错误,或者更糟——静默地对齐错了位置。这也是为什么我建议用现成的DataCollatorForTokenClassification而不是自己写 padding 逻辑,它能自动处理 token 级别的标签 padding,把-100填到所有 padding 位置。

5. 避坑专题:训练与部署中必踩的五个点

5.1 训练损失降到很低,但预测结果全是 O

现象:训练集上 loss 掉到 0.1 以下,F1 看着也还行,但一到预测阶段,不管输入什么文本,模型输出的实体列表全是空的。

原因:这是典型的标签不均衡问题。NER 数据里O标签占比通常超过 80%,模型学到的最优策略是“全都预测成 O”,loss 也不会太大。如果你的验证集里实体样本本来就少,模型很容易走这条捷径。另一个常见原因是label2id和id2label在训练和推理时不一致,导致推理时查表全查到了O头上。

解决:先检查两份映射表是否完全一致,这是最低成本的排查。如果一致,再看训练集里实体样本数量,少于几百条时建议用类别权重或者 Focal Loss。更简单的办法是训练时看每个 epoch 结束后的实体级 F1,不要只盯 loss。用seqeval在验证集上算一次 F1,如果实体 F1 低于 60%,基本可以断定模型还没学会识别实体,需要加大数据量或调学习率。

5.2 标签错位:B-PER 后面跟着 B-LOC

现象:预测出的实体序列出现B-PER I-PER B-LOC I-LOC这种标签跳变,或者I-PER出现在实体最开头。

原因:纯 BERT 微调是逐 token 独立预测,没有学习标签之间的转移约束。模型不知道I-PER前面必须要有B-PER或I-PER,所以会出现这种“非法序列”。

解决:最彻底的办法是加 CRF 层,但 CRF 训练慢,而且代码复杂度高。一个折中方案是后处理修复——推理时把非法序列的I标签改成它前面那个B的实体类型,比如B-PER I-PER B-LOC I-LOC修复成B-PER I-PER B-LOC I-LOC里把第二个I-LOC改成I-ORG这种规则,写起来很别扭。我一般建议数据量超过 5000 条再上 CRF,小于这个量就先用后处理兜底。后处理的规则比较简单:遍历预测序列,遇到I开头但不是B开头的情况,就把这个I的实体类型改成上一个非Otoken 的实体类型。

5.3 transformers 下载模型慢或超时

现象:第一次运行脚本时卡在下载模型的进度条上,或者直接报ConnectionError,重试几次都一样。

原因:transformers默认从 HuggingFace 官方域名下载模型权重,国内网络访问不稳定是常态。这不是你代码的问题,也不需要换模型。

解决:设置环境变量指向国内镜像站。在运行脚本前执行一行命令:

export HF_ENDPOINT=https://hf-mirror.com

或者写进.bashrc里持久生效。另外可以把模型预先下载好放到本地目录,然后代码里from_pretrained("./models/rbt3")直接走本地加载,工业环境下我都是这么干的——把模型文件随项目一起分发,避免每台新机器都去外网拉一次权重。这个镜像地址是我实际用过的,圈内常用的公开镜像,效果稳定。

5.4 训练时 OOM:明明很小的 batch size 也爆显存

现象:batch size 明明设置了 8,模型加载也成功,但训练一开始就报 CUDA out of memory。

原因:中文文本经过 tokenizer 后会被 padding 到max_length=128,如果你的数据里有特别长的句子,实际 padding 后的序列可能超过 128。而且 BERT 的显存占用是随序列长度平方级增长的,多几个长样本就爆了。另一个原因是 PyTorch 默认会缓存整个计算图,反向传播时才释放,显存峰值出现在 forward 结束到 backward 开始之前。

解决:先用tokenizer检查一下训练数据里最长的样本。如果超过 128,要么调大max_length同时调小 batch size,要么做动态 padding——DataCollatorForTokenClassification默认就是动态 padding 的,它是按 batch 内最长序列做 padding,而不是固定 128。前提是你的数据加载没有手动把所有样本都 pad 到 128。显存实在不够就开梯度累积:gradient_accumulation_steps=4,效果等价于 batch size 翻 4 倍,但显存不涨。我一般 batch size 设 8、看 GPU 利用率调整梯度累积步数。

5.5 预测阶段文本必须是整句

现象:训练时用的是完整的数据集句子,推理时我把用户输入按标点符号切成了碎片,每个碎片单独预测,结果实体识别率断崖式下降。

原因:BERT 依赖上下文做消歧,碎片断句把上下文切掉了。比如“张三毕业于北京大学”,如果你切成“张三毕业于”“北京大学”,前半句里的“张三”还能识别,但后半句“北京大学”在缺少“毕业”这个上下文时,可能被识别成地名而不是组织名。

解决:预测时保持原始文本的完整性。用户输入一段话,就整段丢给模型,让 tokenizer 自己处理截断,不要在外部先做句子切分。如果文本长度超过max_length,应该按段落切分,而不是按标点切分。切分时还要保证有重叠窗口,比如窗口大小 120、步长 110,这样实体不会恰好被截断成两半。

6. 把模型部署成服务:加载、推理与验收一条龙

模型训练完不是终点,能给别人用才算落地。我一般用 Flask 包一个轻量 HTTP 接口,把模型加载、预测、结果格式化封装成一个函数。加载模型时注意设置model.eval(),推理代码包在torch.no_grad()里,这样 PyTorch 不会保存计算图,显存占用低很多,CPU 推理也更快。另外每次请求都走一次完整的前向传播,如果 QPS 高,可以用torch.compile加速,或者把模型转成 ONNX,但这都是后话。

一个比较实用的做法是加一个输入兜底:如果文本长度小于 2 或者全是标点,直接返回空结果,不调模型。这样既省算力,也避免模型在短文本上输出奇怪的结果。

# -*- coding: utf-8 -*- # 文件:app.py # 作用:用Flask封装NER预测服务 from flask import Flask, request, jsonify import torch from transformers import AutoTokenizer, BertForTokenClassification app = Flask(__name__) model_name = "./checkpoints/final_model" # 微调后保存的本地模型 tokenizer = AutoTokenizer.from_pretrained(model_name) model = BertForTokenClassification.from_pretrained(model_name) model.eval() id2label = model.config.id2label # 直接用训练时保存的映射表 def predict(text): """输入文本,输出 [(实体, 类型), ...]""" if len(text.strip()) < 2: return [] inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=128) with torch.no_grad(): logits = model(**inputs).logits preds = logits.argmax(dim=-1).squeeze(0).tolist() tokens = tokenizer.convert_ids_to_tokens(inputs["input_ids"].squeeze(0)) entities = [] current_entity = "" current_type = "" for token, pred in zip(tokens[1:-1], preds[1:-1]): # 跳过CLS和SEP label = id2label[pred] if label.startswith("B-"): if current_entity: entities.append((current_entity, current_type)) current_entity = token current_type = label[2:] elif label.startswith("I-") and current_entity: current_entity += token else: if current_entity: entities.append((current_entity, current_type)) current_entity = "" current_type = "" if current_entity: entities.append((current_entity, current_type)) return entities @app.route("/ner", methods=["POST"]) def ner(): data = request.get_json() text = data.get("text", "") return jsonify({"entities": predict(text)}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000)

这段代码里model.config.id2label是关键——训练时我把映射表存进了模型配置,推理时直接在配置里读,两份映射表永远一致,不会出现前面避坑章节说的错位问题。实体拼接逻辑是按B开头、I续接的方式拼回完整实体字符串,current_entity在遇到新实体或O时清空。tokens[1:-1]和preds[1:-1]同步切片,跳过了[CLS]和[SEP]这两个特殊 token 的预测结果。

说到验证,我一直坚持一条习惯:每次改完数据处理或模型参数,强制自己跑一遍“三段验收”——先拿训练集里的一句话说,再看验证集里的一句话,最后编一句没见过的文本。三层都对了才敢往上交。这个习惯是我从第一次部署 NER 服务时学到的教训,那次我拿着一个在训练集上表现很好的模型直接上线,结果真实用户发来的一句口语里全是没见过的表述,模型预测结果惨不忍睹。从那以后,我每次上线前都强制走一遍“修改输入文本 → 重启服务 → 验证输出”的流程,宁可慢一点,也不让用户在线上帮我试错。希望这篇笔记能帮你把 BERT 中文 NER 这条路走顺——原理先立住,代码能复现,坑提前避开,剩下的就是多练了。

本文还有配套的精品资源,点击获取

返回列表