在自然语言处理(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. 关键点总结

通过本文的实践,我们掌握了:

  1. NER的任务定义:对每个词元进行细粒度分类,使用BIO标注体系。

  2. 数据准备:使用CoNLL-2003数据集,理解标签映射。

  3. 分词对齐:由于子词分词的存在,必须将单词级标签对齐到词元级,并正确处理特殊词元和连续子词。

  4. 模型加载:使用 AutoModelForTokenClassification 加载预训练BERT,指定标签映射。

  5. 评估指标:采用实体级别的F1分数(seqeval),更符合NER的实际需求。

  6. 训练配置:使用 DataCollatorForTokenClassification 处理动态填充,用 Trainer 简化训练流程。

  7. 推理部署:通过 pipeline 快速应用模型,获取可解释的实体识别结果。

命名实体识别是许多NLP应用的基石。掌握这项技术后,你可以将其应用到更多领域,例如:

  • 从医疗文本中抽取疾病、药物名称

  • 从法律文档中识别当事人、日期、条款

  • 对社交媒体文本进行实体匿名化处理

本文参考:图解大模型:生成式AI原理与实战

书籍pdf免费下载地址:https://pan.baidu.com/s/1mTaUQ5czcfGpBM8KvJuS2g?pwd=un44

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐