当前位置:首页 > 技术 > 正文内容

基于预训练模型的Token分类与命名实体识别微调指南

访客 技术 2026年10月16日 1

Token分类任务概述

Token分类是自然语言处理中的核心序列标注任务,其本质是为文本序列中的每一个词元(Token)分配特定的类别标签。常见的应用场景包括:

  • 命名实体识别(NER):识别文本中的专有名词,如人名、地名、机构名等。
  • 词性标注(POS):为每个单词标注其词性,如名词、动词、形容词。
  • 分块(Chunking):将属于同一短语或实体的词元组合在一起,通常结合BIO(Begin, Inside, Outside)标注体系来实现。

本文将详细讲解如何基于预训练语言模型(如BERT)微调一个用于命名实体识别的Token分类模型,涵盖从数据预处理、子词标签对齐到模型训练与推理的完整流程。

数据集加载与结构探索

我们将使用经典的CoNLL-2003数据集进行演示。该数据集包含了新闻语料,并提供了NER、POS和Chunking三种标注。通过datasets库可以轻松加载并查看其结构:

from datasets import load_dataset

ner_corpus = load_dataset("conll2003")
print(ner_corpus)

输出结果会显示数据集包含训练集、验证集和测试集,且特征列中包含tokens(已预分词的单词列表)以及对应的ner_tags、pos_tags和chunk_tags。需要注意的是,这里的tokens是单词级别的,在输入模型前仍需经过Tokenizer进行子词(Subword)拆分。

在NER任务中,标签通常采用BIO格式。我们可以通过数据集的features属性来解析整数标签与字符串标签的映射关系:

ner_feature = ner_corpus["train"].features["ner_tags"]
tag_names = ner_feature.feature.names
print(tag_names)
# 输出: ['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC', 'B-MISC', 'I-MISC']

其中,O代表非实体,B-前缀表示实体的起始词,I-前缀表示实体的内部词。

子词对齐与数据预处理

由于预训练模型使用的Tokenizer会将单词拆分为更小的子词(例如将"lamb"拆分为"la"和"##mb"),这会导致输入Token的数量与原始单词标签的数量不一致。此外,Tokenizer还会添加[CLS]和[SEP]等特殊Token。因此,我们必须将原始单词级别的标签对齐到子词级别。

对齐的核心规则如下:

  1. 特殊Token(如[CLS]、[SEP])的标签设置为-100,以便在计算交叉熵损失时被自动忽略。
  2. 单词拆分后的第一个子词保留原始单词的标签。
  3. 单词拆分后的后续子词,如果原标签是B-开头,则需转换为I-开头,以保证实体边界的逻辑正确性。

以下是实现标签对齐的函数:

def map_tags_to_subwords(original_tags, word_id_mapping):
    aligned_tags = []
    prev_word_idx = None
    for current_word_idx in word_id_mapping:
        if current_word_idx is None:
            # 特殊Token标记为-100
            aligned_tags.append(-100)
        elif current_word_idx != prev_word_idx:
            # 单词的第一个子词,保留原标签
            aligned_tags.append(original_tags[current_word_idx])
        else:
            # 单词的后续子词
            tag = original_tags[current_word_idx]
            # 在标准BIO编码中,B-标签通常为奇数,I-标签为偶数
            aligned_tags.append(tag + 1 if tag % 2 != 0 else tag)
        prev_word_idx = current_word_idx
    return aligned_tags

接下来,我们将Tokenizer与对齐逻辑结合,编写批量处理函数,并利用Dataset.map对整个数据集进行预处理:

from transformers import AutoTokenizer

model_id = "bert-base-cased"
text_tokenizer = AutoTokenizer.from_pretrained(model_id)

def process_batch(batch_data):
    tokenized = text_tokenizer(
        batch_data["tokens"], 
        truncation=True, 
        is_split_into_words=True
    )
    updated_labels = []
    for idx, tags in enumerate(batch_data["ner_tags"]):
        w_ids = tokenized.word_ids(batch_index=idx)
        updated_labels.append(map_tags_to_subwords(tags, w_ids))
    
    tokenized["labels"] = updated_labels
    return tokenized

tokenized_corpus = ner_corpus.map(
    process_batch,
    batched=True,
    remove_columns=ner_corpus["train"].column_names,
)

数据整理器与评估指标

在构建Batch时,除了对输入ID进行填充,还需要对标签进行同步填充。我们可以使用DataCollatorForTokenClassification,它会自动使用-100对标签进行填充:

from transformers import DataCollatorForTokenClassification

data_collator = DataCollatorForTokenClassification(tokenizer=text_tokenizer)

对于Token分类任务,标准的评估库是seqeval。由于该库要求输入字符串格式的标签,我们需要在评估函数中将模型输出的Logits转换为字符串标签,并过滤掉-100的占位符:

import evaluate
import numpy as np

seqeval_metric = evaluate.load("seqeval")

def evaluate_predictions(eval_data):
    logits, true_labels = eval_data
    preds = np.argmax(logits, axis=-1)
    
    # 过滤-100并转换为字符串标签
    pred_strings = [[tag_names[p] for p, l in zip(pred, lbl) if l != -100] for pred, lbl in zip(preds, true_labels)]
    label_strings = [[tag_names[l] for l in lbl if l != -100] for lbl in true_labels]
    
    metrics = seqeval_metric.compute(predictions=pred_strings, references=label_strings)
    return {
        "f1_score": metrics["overall_f1"],
        "precision": metrics["overall_precision"],
        "recall": metrics["overall_recall"],
        "accuracy": metrics["overall_accuracy"]
    }

模型初始化与Trainer微调

使用AutoModelForTokenClassification加载预训练模型。为了确保模型在Hub上展示正确的标签映射,我们需要显式传入id2label和label2id字典:

from transformers import AutoModelForTokenClassification, TrainingArguments, Trainer

id2label = {str(i): label for i, label in enumerate(tag_names)}
label2id = {v: k for k, v in id2label.items()}

model = AutoModelForTokenClassification.from_pretrained(
    model_id,
    id2label=id2label,
    label2id=label2id,
)

training_args = TrainingArguments(
    output_dir="my-ner-model",
    evaluation_strategy="epoch",
    save_strategy="epoch",
    learning_rate=2e-5,
    num_train_epochs=3,
    weight_decay=0.01,
    push_to_hub=True,
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_corpus["train"],
    eval_dataset=tokenized_corpus["validation"],
    data_collator=data_collator,
    compute_metrics=evaluate_predictions,
    tokenizer=text_tokenizer,
)

trainer.train()
trainer.push_to_hub(commit_message="Training completed")

基于Accelerate的自定义训练循环

如果需要更细粒度的控制,可以使用Accelerate库构建原生PyTorch训练循环。以下是分布式环境下的完整训练逻辑:

from torch.utils.data import DataLoader
from torch.optim import AdamW
from transformers import get_scheduler
from accelerate import Accelerator
from tqdm.auto import tqdm
import torch

# 初始化Accelerator
accelerator = Accelerator()

train_loader = DataLoader(tokenized_corpus["train"], shuffle=True, collate_fn=data_collator, batch_size=8)
eval_loader = DataLoader(tokenized_corpus["validation"], collate_fn=data_collator, batch_size=8)

optimizer = AdamW(model.parameters(), lr=2e-5)

num_epochs = 3
num_training_steps = num_epochs * len(train_loader)
lr_scheduler = get_scheduler("linear", optimizer=optimizer, num_warmup_steps=0, num_training_steps=num_training_steps)

model, optimizer, train_loader, eval_loader = accelerator.prepare(model, optimizer, train_loader, eval_loader)

training_progress = tqdm(total=num_training_steps)

for current_epoch in range(num_epochs):
    # 训练阶段
    model.train()
    for batch in train_loader:
        outputs = model(**batch)
        loss = outputs.loss
        accelerator.backward(loss)
        
        optimizer.step()
        lr_scheduler.step()
        optimizer.zero_grad()
        training_progress.update(1)

    # 评估阶段
    model.eval()
    for batch in eval_loader:
        with torch.no_grad():
            outputs = model(**batch)
        
        preds = outputs.logits.argmax(dim=-1)
        targets = batch["labels"]
        
        # 跨进程填充以对齐形状,防止gather时维度不匹配
        preds = accelerator.pad_across_processes(preds, dim=1, pad_index=-100)
        targets = accelerator.pad_across_processes(targets, dim=1, pad_index=-100)
        
        gathered_preds = accelerator.gather(preds)
        gathered_targets = accelerator.gather(targets)
        
        # 此处需调用前文定义的格式转换逻辑将gathered结果转为seqeval所需格式
        # final_preds, final_targets = format_for_seqeval(gathered_preds, gathered_targets)
        # seqeval_metric.add_batch(predictions=final_preds, references=final_targets)

    # 保存模型
    accelerator.wait_for_everyone()
    base_model = accelerator.unwrap_model(model)
    base_model.save_pretrained("my-ner-model", save_function=accelerator.save)
    if accelerator.is_main_process:
        text_tokenizer.save_pretrained("my-ner-model")

模型推理与应用

微调完成后,可以通过pipeline快速加载模型进行推理。设置aggregation_strategy="simple"可以将子词级别的预测结果自动聚合成完整的实体词汇:

from transformers import pipeline

ner_pipeline = pipeline(
    "token-classification", 
    model="my-ner-model", 
    aggregation_strategy="simple"
)

text = "Apple is looking at buying U.K. startup for $1 billion"
results = ner_pipeline(text)
print(results)

输出结果将直接返回识别出的实体组、置信度分数以及在原句中的字符起止位置,方便直接集成到下游业务系统中。

相关文章

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

linux screen 用法详情 (nohup 的替代方案)

一、screen 是什么?能干嘛?screen 是一个终端复用器,可以:在一个 SSH 会话中开多个“虚拟终端”SSH 断线后,程序仍然在后台运行随时重新连接到原来的会话特别适合:nohup 的替代方案跑脚本 / 爬虫 / 训练模型运维、远程开发二、安装 screen# CentOS / Rocky / Almayum install -y screen# Debian / Ubuntuapt i...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。