手把手教你微调BERT做命名实体识别(NER),从数据预处理到模型部署全解析
在自然语言处理(NLP)的诸多任务中,命名实体识别(Named Entity Recognition,NER)是一项基础且极具实用价值的技术。它能够从非结构化文本中自动识别人名、地名、组织机构名等特定实体,广泛应用于信息抽取、知识图谱构建、问答系统以及数据去标识化(匿名化)等场景。特别是当我们需要处理敏感数据时,NER可以帮助我们快速定位并替换个人身份信息,保护用户隐私。
本文将带你深入实践,使用预训练的BERT模型,在经典的CoNLL-2003英文数据集上微调一个NER模型。我们会从数据准备、分词对齐、模型加载、训练评估,到最终的推理部署,每一步都给出详细的解释和代码示例。无论你是NLP初学者,还是希望巩固NER技术的开发者,这篇文章都能让你收获满满。
1. 命名实体识别:从文档分类到词元分类
回顾我们之前接触过的文本分类任务(如情感分析、新闻分类),通常是对整个文档或句子打一个标签。而NER则要精细得多:它需要对文本中的每个词元(token) 进行分类,判断其是否属于预定义的实体类别,以及属于哪一类。
例如,给定句子:
I am Maarten and I live in the Netherlands.
一个微调后的BERT模型应该能够识别出“Maarten”是人名(PER),“Netherlands”是地点(LOC)。这种词元级别的分类方式,使得NER能够捕捉文本中的细粒度信息。
2. 数据准备:认识CoNLL-2003数据集
我们将使用英文版的CoNLL-2003数据集,这是NER任务最常用的基准数据集之一。它包含约14000个训练样本,涵盖四种实体类型:
-
PER:人名
-
ORG:组织机构
-
LOC:地点
-
MISC:其他实体(如事件、产品等)
以及一个非实体标签 O(表示“其他”)。
数据集中每个单词都标注了对应的NER标签,采用BIO标注体系:
-
B- (Begin) 表示实体的开始
-
I- (Inside) 表示实体的内部
-
O 表示非实体
例如,句子 “Dean Palmer hit his 30th homer for the Rangers!” 中的NER标签如下:
| 单词 | Dean | Palmer | hit | his | 30th | homer | for | the | Rangers | ! |
|---|---|---|---|---|---|---|---|---|---|---|
| NER标签 | B-PER | I-PER | O | O | O | O | O | O | B-ORG | O |
这里 “Dean Palmer” 是一个完整的人名,因此Dean用B-PER标记,Palmer用I-PER标记,表示它们属于同一个实体。同样,“Rangers”是一个组织,用B-ORG标记。
在Hugging Face的datasets库中,我们可以直接加载该数据集:
python
from datasets import load_dataset
dataset = load_dataset("conll2003")
查看一个训练样本:
python
example = dataset["train"][848] print(example)
输出包含tokens(单词列表)和ner_tags(标签ID列表)。标签ID与类别的对应关系如下:
python
label2id = {
"O": 0, "B-PER": 1, "I-PER": 2,
"B-ORG": 3, "I-ORG": 4,
"B-LOC": 5, "I-LOC": 6,
"B-MISC": 7, "I-MISC": 8
}
id2label = {v: k for k, v in label2id.items()}
3. 分词挑战:如何将单词标签对齐到子词元
预训练BERT模型使用子词分词(如WordPiece),它会将单词拆分为更小的子词单元。例如,单词“homer”可能被拆分为 home 和 ##r。这就产生了一个问题:我们原始的标签是基于完整单词的,如何将标签正确地传递给拆分后的子词元?
直接沿用单词的标签会导致错误:假设“Maarten”被拆分为 Ma、##arte、##n,如果都给它们打上B-PER,模型就会错误地认为这是三个独立的人名实体。正确的做法是:
-
第一个子词元(
Ma)保留B-PER,表示实体的开始。 -
后续子词元(
##arte、##n)应标记为I-PER,表示它们属于同一个实体内部。
此外,分词器还会自动添加特殊词元,如 [CLS] 和 [SEP]。这些词元不应该参与损失计算,因此我们需要将它们对应的标签设为 -100(PyTorch中忽略该位置的约定)。
实现标签对齐函数
下面的align_labels函数完成了上述对齐工作:
python
def align_labels(examples):
token_ids = tokenizer(
examples["tokens"],
truncation=True,
is_split_into_words=True # 表示输入已经按单词切分
)
labels = examples["ner_tags"]
updated_labels = []
for i, label in enumerate(labels):
word_ids = token_ids.word_ids(batch_index=i) # 每个词元对应的单词索引
previous_word_idx = None
label_ids = []
for word_idx in word_ids:
if word_idx is None:
# 特殊词元 [CLS], [SEP] 等
label_ids.append(-100)
elif word_idx != previous_word_idx:
# 新单词的开始
previous_word_idx = word_idx
label_ids.append(label[word_idx])
else:
# 同一个单词的内部(后续子词元)
# 如果原始标签是 B-XXX,则改为 I-XXX
original_label = label[word_idx]
if original_label % 2 == 1: # B类标签是奇数
label_ids.append(original_label + 1) # 转为对应的 I 类
else:
label_ids.append(original_label)
updated_labels.append(label_ids)
token_ids["labels"] = updated_labels
return token_ids
解释一下核心逻辑:
-
token_ids.word_ids()返回一个列表,每个元素表示当前词元属于原始句子中的第几个单词(从0开始),None表示特殊词元。 -
当遇到新单词的第一个词元时,直接使用该单词的原始标签。
-
如果后续词元属于同一个单词,则检查原始标签是否为B类(奇数ID),如果是,则将其改为对应的I类(原始ID+1);否则保持不变(如O类)。
-
特殊词元统一设为
-100。
应用此函数处理整个数据集:
python
tokenized_dataset = dataset.map(align_labels, batched=True)
现在,原始标签 [1, 2, 0, 0, 0, 0, 0, 3, 0] 就会变成类似 [-100, 1, 2, 0, 0, 0, 0, 0, 0, 3, 0, -100] 的对齐后标签。
4. 加载预训练模型与分词器
我们选择经典的 bert-base-cased 作为基础模型(注意保留大小写信息,NER中大小写有时很重要)。使用 AutoModelForTokenClassification 加载模型,并指定标签映射:
python
from transformers import AutoTokenizer, AutoModelForTokenClassification
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
model = AutoModelForTokenClassification.from_pretrained(
"bert-base-cased",
num_labels=len(id2label),
id2label=id2label,
label2id=label2id
)
5. 定义评估指标:使用seqeval
NER任务常用的评估指标是基于实体级别的精确率、召回率和F1分数。与逐词元准确率不同,实体级别评估需要正确识别实体的边界和类型。seqeval 库正是为此设计的。
我们定义一个 compute_metrics 函数,在训练时定期评估模型:
python
import evaluate
import numpy as np
seqeval = evaluate.load("seqeval")
def compute_metrics(eval_pred):
logits, labels = eval_pred
predictions = np.argmax(logits, axis=-1)
true_predictions = []
true_labels = []
for pred_seq, label_seq in zip(predictions, labels):
pred_entities = []
label_entities = []
for pred_id, label_id in zip(pred_seq, label_seq):
if label_id != -100: # 忽略特殊词元
pred_entities.append(id2label[pred_id])
label_entities.append(id2label[label_id])
true_predictions.append(pred_entities)
true_labels.append(label_entities)
results = seqeval.compute(predictions=true_predictions, references=true_labels)
return {"f1": results["overall_f1"]}
这个函数会收集所有非特殊词元的预测标签,以句子为单位传给 seqeval,最终返回整体的F1分数。
6. 数据整理器:处理动态填充
与普通文本分类不同,NER任务中每个样本的输入长度不一,我们需要动态地将一个批次内的序列填充到相同长度。Hugging Face 提供了 DataCollatorForTokenClassification,它不仅能填充输入,还会相应地填充标签(用 -100 填充,确保损失计算忽略填充部分)。
python
from transformers import DataCollatorForTokenClassification data_collator = DataCollatorForTokenClassification(tokenizer=tokenizer)
7. 设置训练参数与Trainer
接下来配置训练参数。这里使用 TrainingArguments 定义学习率、批次大小、轮数等,然后用 Trainer 整合模型、数据集、数据整理器和评估函数。
python
from transformers import TrainingArguments, Trainer
training_args = TrainingArguments(
output_dir="./ner_model",
learning_rate=2e-5,
per_device_train_batch_size=16,
per_device_eval_batch_size=16,
num_train_epochs=3, # 通常3轮效果较好
weight_decay=0.01,
evaluation_strategy="epoch",
save_strategy="epoch",
load_best_model_at_end=True,
report_to="none"
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_dataset["train"],
eval_dataset=tokenized_dataset["validation"], # CoNLL-2003有验证集
tokenizer=tokenizer,
data_collator=data_collator,
compute_metrics=compute_metrics,
)
启动训练:
python
trainer.train()
训练完成后,在测试集上评估:
python
trainer.evaluate(tokenized_dataset["test"])
8. 推理:使用pipeline快速部署
训练好的模型可以保存并在推理中使用。Hugging Face 的 pipeline 提供了便捷的接口:
python
from transformers import pipeline
# 保存模型
trainer.save_model("my_ner_model")
# 加载推理pipeline
token_classifier = pipeline(
"token-classification",
model="my_ner_model",
tokenizer="my_ner_model" # 会自动加载保存的tokenizer
)
# 测试
result = token_classifier("My name is Maarten.")
print(result)
输出会列出每个识别出的实体词元及其分数、位置和标签。例如:
text
[
{'entity': 'B-PER', 'score': 0.995, 'word': 'Ma', 'start': 11, 'end': 13},
{'entity': 'I-PER', 'score': 0.992, 'word': '##arte', 'start': 13, 'end': 17},
{'entity': 'I-PER', 'score': 0.995, 'word': '##n', 'start': 17, 'end': 18}
]
可以看到,模型成功地将“Maarten”拆分成的三个子词元正确标注为人名实体。
9. 关键点总结
通过本文的实践,我们掌握了:
-
NER的任务定义:对每个词元进行细粒度分类,使用BIO标注体系。
-
数据准备:使用CoNLL-2003数据集,理解标签映射。
-
分词对齐:由于子词分词的存在,必须将单词级标签对齐到词元级,并正确处理特殊词元和连续子词。
-
模型加载:使用
AutoModelForTokenClassification加载预训练BERT,指定标签映射。 -
评估指标:采用实体级别的F1分数(
seqeval),更符合NER的实际需求。 -
训练配置:使用
DataCollatorForTokenClassification处理动态填充,用Trainer简化训练流程。 -
推理部署:通过
pipeline快速应用模型,获取可解释的实体识别结果。
命名实体识别是许多NLP应用的基石。掌握这项技术后,你可以将其应用到更多领域,例如:
-
从医疗文本中抽取疾病、药物名称
-
从法律文档中识别当事人、日期、条款
-
对社交媒体文本进行实体匿名化处理
本文参考:图解大模型:生成式AI原理与实战
书籍pdf免费下载地址:https://pan.baidu.com/s/1mTaUQ5czcfGpBM8KvJuS2g?pwd=un44
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)