NLP 课程文档

令牌分类

Hugging Face's logo
加入 Hugging Face 社区

并获得增强文档体验

开始使用

令牌分类

Ask a Question Open In Colab Open In Studio Lab

我们将探索的第一个应用程序是令牌分类。此通用任务包含任何可以表述为“将标签分配给句子中的每个令牌”的问题,例如

  • 命名实体识别 (NER):在句子中找到实体(如人、地点或组织)。这可以表述为通过为每个实体分配一个类别并为“无实体”分配一个类别,将标签分配给每个令牌。
  • 词性标注 (POS):将句子中的每个词标记为对应于特定的词性(如名词、动词、形容词等)。
  • 分块:找到属于同一个实体的令牌。此任务(可以与 POS 或 NER 结合使用)可以表述为将一个标签(通常是B-)分配给任何处于块开始位置的令牌,将另一个标签(通常是I-)分配给处于块内部的令牌,并将第三个标签(通常是O)分配给不属于任何块的令牌。

当然,还有许多其他类型的令牌分类问题;这些只是一些有代表性的例子。在本节中,我们将对 NER 任务微调一个模型(BERT),然后该模型将能够计算出类似于以下的预测

One-hot encoded labels for question answering.

您可以找到我们将在 Hub 上进行训练并上传的模型,并仔细检查其预测 此处.

准备数据

首先,我们需要一个适合令牌分类的数据集。在本节中,我们将使用 CoNLL-2003 数据集,它包含来自路透社的新闻报道。

💡 只要您的数据集包含拆分为单词及其对应标签的文本,您就可以将此处描述的数据处理过程应用到您自己的数据集。如果您需要刷新有关如何在Dataset中加载自己的自定义数据的知识,请参阅 第 5 章

CoNLL-2003 数据集

要加载 CoNLL-2003 数据集,我们使用 🤗 数据集库中的load_dataset()方法

from datasets import load_dataset

raw_datasets = load_dataset("conll2003")

这将下载并缓存数据集,就像我们在 第 3 章 中为 GLUE MRPC 数据集所做的那样。检查此对象将显示存在的列以及训练、验证和测试集之间的分割

raw_datasets
DatasetDict({
    train: Dataset({
        features: ['chunk_tags', 'id', 'ner_tags', 'pos_tags', 'tokens'],
        num_rows: 14041
    })
    validation: Dataset({
        features: ['chunk_tags', 'id', 'ner_tags', 'pos_tags', 'tokens'],
        num_rows: 3250
    })
    test: Dataset({
        features: ['chunk_tags', 'id', 'ner_tags', 'pos_tags', 'tokens'],
        num_rows: 3453
    })
})

特别是,我们可以看到数据集包含我们之前提到的三个任务的标签:NER、POS 和分块。与其他数据集的一大区别是,输入文本不是以句子或文档的形式呈现,而是以单词列表的形式呈现(最后一列称为tokens,但它包含单词,因为它们是预先标记的输入,仍然需要通过标记器进行子词标记)。

让我们看看训练集中的第一个元素

raw_datasets["train"][0]["tokens"]
['EU', 'rejects', 'German', 'call', 'to', 'boycott', 'British', 'lamb', '.']

由于我们要执行命名实体识别,我们将查看 NER 标签

raw_datasets["train"][0]["ner_tags"]
[3, 0, 7, 0, 0, 0, 7, 0, 0]

这些标签是用于训练的整数,但在我们想要检查数据时,它们可能并不一定有用。与文本分类类似,我们可以通过查看数据集的features属性来访问这些整数与标签名称之间的对应关系

ner_feature = raw_datasets["train"].features["ner_tags"]
ner_feature
Sequence(feature=ClassLabel(num_classes=9, names=['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC', 'B-MISC', 'I-MISC'], names_file=None, id=None), length=-1, id=None)

因此,此列包含作为ClassLabel序列的元素。序列中元素的类型位于此ner_featurefeature属性中,我们可以通过查看该featurenames属性来访问名称列表

label_names = ner_feature.feature.names
label_names
['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC', 'B-MISC', 'I-MISC']

我们在 第 6 章 中深入研究token-classification管道时已经看到了这些标签,但为了快速回顾

  • O 表示该词不对应于任何实体。
  • B-PER/I-PER 表示该词对应于实体的开头/位于内部。
  • B-ORG/I-ORG 表示该词对应于组织实体的开头/位于内部。
  • B-LOC/I-LOC 表示该词对应于位置实体的开头/位于内部。
  • B-MISC/I-MISC 表示该词对应于杂项实体的开头/位于内部。

现在,解码我们之前看到的标签将得到以下结果

words = raw_datasets["train"][0]["tokens"]
labels = raw_datasets["train"][0]["ner_tags"]
line1 = ""
line2 = ""
for word, label in zip(words, labels):
    full_label = label_names[label]
    max_length = max(len(word), len(full_label))
    line1 += word + " " * (max_length - len(word) + 1)
    line2 += full_label + " " * (max_length - len(full_label) + 1)

print(line1)
print(line2)
'EU    rejects German call to boycott British lamb .'
'B-ORG O       B-MISC O    O  O       B-MISC  O    O'

对于混合了B-I-标签的示例,以下是同一代码在训练集索引为 4 的元素上给出的结果

'Germany \'s representative to the European Union \'s veterinary committee Werner Zwingmann said on Wednesday consumers should buy sheepmeat from countries other than Britain until the scientific advice was clearer .'
'B-LOC   O  O              O  O   B-ORG    I-ORG O  O          O         B-PER  I-PER     O    O  O         O         O      O   O         O    O         O     O    B-LOC   O     O   O          O      O   O       O'

正如我们所见,跨越两个单词的实体,例如“欧洲联盟”和“Werner Zwingmann”,第一个单词被分配了B-标签,第二个单词被分配了I-标签。

✏️ 轮到你了! 打印相同的两句话及其 POS 或分块标签。

处理数据

像往常一样,我们的文本需要先转换为token ID,模型才能理解。正如我们在第 6 章中看到的,在标记分类任务中一个很大的不同是,我们拥有预先标记化的输入。幸运的是,标记器 API 可以轻松处理这个问题;我们只需要用一个特殊的标志警告tokenizer

首先,让我们创建我们的tokenizer对象。如前所述,我们将使用 BERT 预训练模型,因此我们将从下载和缓存相关的标记器开始。

from transformers import AutoTokenizer

model_checkpoint = "bert-base-cased"
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)

您可以用您喜欢的任何其他模型从Hub替换model_checkpoint,或者用您保存了预训练模型和标记器的本地文件夹。唯一的约束是,标记器需要由 🤗 Tokenizers 库支持,因此有一个“快速”版本可用。您可以在这个大表中查看所有带有快速版本的架构,并且要检查您使用的tokenizer对象是否确实由 🤗 Tokenizers 支持,您可以查看其is_fast属性。

tokenizer.is_fast
True

要标记预先标记化的输入,我们可以像往常一样使用我们的tokenizer,并添加is_split_into_words=True

inputs = tokenizer(raw_datasets["train"][0]["tokens"], is_split_into_words=True)
inputs.tokens()
['[CLS]', 'EU', 'rejects', 'German', 'call', 'to', 'boycott', 'British', 'la', '##mb', '.', '[SEP]']

正如我们所看到的,标记器添加了模型使用的特殊标记(开头为[CLS],结尾为[SEP]),并将大多数单词保留原样。然而,单词lamb被标记成两个子词,la##mb。这会导致我们的输入和标签之间出现不匹配:标签列表只有 9 个元素,而我们的输入现在有 12 个标记。处理特殊标记很容易(我们知道它们在开头和结尾),但我们还需要确保将所有标签与正确的单词对齐。

幸运的是,由于我们使用的是快速标记器,因此我们可以使用 🤗 Tokenizers 的强大功能,这意味着我们可以轻松地将每个标记映射到其对应的单词(如第 6 章所示)。

inputs.word_ids()
[None, 0, 1, 2, 3, 4, 5, 6, 7, 7, 8, None]

通过一点工作,我们可以扩展我们的标签列表以匹配标记。我们将应用的第一个规则是,特殊标记的标签为-100。这是因为默认情况下,-100 是一个在我们将使用的损失函数(交叉熵)中被忽略的索引。然后,每个标记都与开始该标记所在的单词的标记具有相同的标签,因为它们属于同一个实体。对于单词内部但不在开头的标记,我们将B-替换为I-(因为标记不是实体的开头)。

def align_labels_with_tokens(labels, word_ids):
    new_labels = []
    current_word = None
    for word_id in word_ids:
        if word_id != current_word:
            # Start of a new word!
            current_word = word_id
            label = -100 if word_id is None else labels[word_id]
            new_labels.append(label)
        elif word_id is None:
            # Special token
            new_labels.append(-100)
        else:
            # Same word as previous token
            label = labels[word_id]
            # If the label is B-XXX we change it to I-XXX
            if label % 2 == 1:
                label += 1
            new_labels.append(label)

    return new_labels

让我们在第一句话上试一下。

labels = raw_datasets["train"][0]["ner_tags"]
word_ids = inputs.word_ids()
print(labels)
print(align_labels_with_tokens(labels, word_ids))
[3, 0, 7, 0, 0, 0, 7, 0, 0]
[-100, 3, 0, 7, 0, 0, 0, 7, 0, 0, 0, -100]

正如我们所看到的,我们的函数在开头和结尾添加了两个特殊标记的-100,以及被拆分成两个标记的单词的新的0

✏️ 轮到你了! 一些研究人员更喜欢为每个单词只分配一个标签,并将-100分配给给定单词中的其他子标记。这样做是为了避免被拆分成很多子标记的长单词对损失产生过大的影响。通过遵循此规则,更改之前的函数以使标签与输入 ID 对齐。

为了预处理整个数据集,我们需要标记所有输入,并将align_labels_with_tokens()应用于所有标签。为了利用快速标记器的速度优势,最好同时标记大量文本,因此我们将编写一个处理示例列表的函数,并使用Dataset.map()方法,并使用batched=True选项。唯一与之前示例不同的是,word_ids()函数需要在标记器输入为文本列表(或者在我们的例子中,为单词列表的列表)时获取我们想要的单词 ID 的示例索引,因此我们也添加了它。

def tokenize_and_align_labels(examples):
    tokenized_inputs = tokenizer(
        examples["tokens"], truncation=True, is_split_into_words=True
    )
    all_labels = examples["ner_tags"]
    new_labels = []
    for i, labels in enumerate(all_labels):
        word_ids = tokenized_inputs.word_ids(i)
        new_labels.append(align_labels_with_tokens(labels, word_ids))

    tokenized_inputs["labels"] = new_labels
    return tokenized_inputs

注意,我们还没有填充输入;我们将在稍后使用数据整理器创建批次时进行填充。

我们现在可以在数据集的其他拆分上一次性完成所有预处理。

tokenized_datasets = raw_datasets.map(
    tokenize_and_align_labels,
    batched=True,
    remove_columns=raw_datasets["train"].column_names,
)

我们已经完成了最困难的部分!现在数据已经预处理完毕,实际的训练将非常类似于我们在第 3 章中所做的。

使用 Trainer API 微调模型

使用Trainer的实际代码将与之前相同;唯一的变化是数据整理成批次的方式以及指标计算函数。

数据整理

我们不能像在第 3 章中那样直接使用DataCollatorWithPadding,因为那只填充输入(输入 ID、注意力掩码和标记类型 ID)。这里我们的标签应该与输入以完全相同的方式填充,以便它们保持相同的大小,使用-100作为值,以便在损失计算中忽略相应的预测。

所有这些都由DataCollatorForTokenClassification完成。与DataCollatorWithPadding一样,它接受用于预处理输入的tokenizer

from transformers import DataCollatorForTokenClassification

data_collator = DataCollatorForTokenClassification(tokenizer=tokenizer)

为了在一些样本上测试它,我们可以直接在标记化训练集的示例列表上调用它。

batch = data_collator([tokenized_datasets["train"][i] for i in range(2)])
batch["labels"]
tensor([[-100,    3,    0,    7,    0,    0,    0,    7,    0,    0,    0, -100],
        [-100,    1,    2, -100, -100, -100, -100, -100, -100, -100, -100, -100]])

让我们将此与数据集中的第一个和第二个元素的标签进行比较。

for i in range(2):
    print(tokenized_datasets["train"][i]["labels"])
[-100, 3, 0, 7, 0, 0, 0, 7, 0, 0, 0, -100]
[-100, 1, 2, -100]

正如我们所看到的,第二组标签已经使用-100填充到第一个标签的长度。

指标

为了让Trainer在每个 epoch 计算指标,我们需要定义一个compute_metrics()函数,它接受预测和标签的数组,并返回一个包含指标名称和值的字典。

用于评估标记分类预测的传统框架是seqeval。要使用此指标,我们首先需要安装seqeval库。

!pip install seqeval

然后我们可以像在第 3 章中一样,通过evaluate.load()函数加载它。

import evaluate

metric = evaluate.load("seqeval")

此指标的行为不像标准的准确率:它实际上将接受标签列表作为字符串,而不是整数,因此我们需要在将它们传递给指标之前完全解码预测和标签。让我们看看它如何工作。首先,我们将获取第一个训练示例的标签。

labels = raw_datasets["train"][0]["ner_tags"]
labels = [label_names[i] for i in labels]
labels
['B-ORG', 'O', 'B-MISC', 'O', 'O', 'O', 'B-MISC', 'O', 'O']

然后,我们可以通过更改索引 2 处的数值来创建这些数值的伪预测。

predictions = labels.copy()
predictions[2] = "O"
metric.compute(predictions=[predictions], references=[labels])

注意,指标接受预测列表(不仅仅是一个)和标签列表。以下是输出。

{'MISC': {'precision': 1.0, 'recall': 0.5, 'f1': 0.67, 'number': 2},
 'ORG': {'precision': 1.0, 'recall': 1.0, 'f1': 1.0, 'number': 1},
 'overall_precision': 1.0,
 'overall_recall': 0.67,
 'overall_f1': 0.8,
 'overall_accuracy': 0.89}

这返回了大量信息!我们获得了每个单独实体的精度、召回率和 F1 分数,以及总体分数。对于我们的指标计算,我们只保留总体分数,但您可以随意调整compute_metrics()函数以返回您想要报告的所有指标。

compute_metrics()函数首先获取 logits 的 argmax 以将其转换为预测(像往常一样,logits 和概率按相同顺序排列,因此我们不需要应用 softmax)。然后,我们必须将标签和预测从整数转换为字符串。我们删除所有标签为-100的值,然后将结果传递给metric.compute()方法。

import numpy as np


def compute_metrics(eval_preds):
    logits, labels = eval_preds
    predictions = np.argmax(logits, axis=-1)

    # Remove ignored index (special tokens) and convert to labels
    true_labels = [[label_names[l] for l in label if l != -100] for label in labels]
    true_predictions = [
        [label_names[p] for (p, l) in zip(prediction, label) if l != -100]
        for prediction, label in zip(predictions, labels)
    ]
    all_metrics = metric.compute(predictions=true_predictions, references=true_labels)
    return {
        "precision": all_metrics["overall_precision"],
        "recall": all_metrics["overall_recall"],
        "f1": all_metrics["overall_f1"],
        "accuracy": all_metrics["overall_accuracy"],
    }

现在这已经完成了,我们几乎准备好定义我们的Trainer。我们只需要一个要微调的model

定义模型

由于我们在处理标记分类问题,因此我们将使用AutoModelForTokenClassification类。在定义此模型时要记住的最重要的事情是,要传递有关我们拥有的标签数量的一些信息。最简单的方法是使用num_labels参数传递该数量,但如果我们想要一个像本节开头看到的那样的不错的推理小部件,最好设置正确的标签对应关系。

它们应该由两个字典id2labellabel2id设置,其中包含从 ID 到标签和反之的映射关系。

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

现在,我们可以直接将它们传递给AutoModelForTokenClassification.from_pretrained()方法,它们将被设置在模型的配置中,然后正确地保存并上传到 Hub。

from transformers import AutoModelForTokenClassification

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

就像我们在第 3 章中定义AutoModelForSequenceClassification时一样,创建模型会发出警告,指出一些权重没有被使用(来自预训练头的权重),而另一些权重则是随机初始化的(来自新的标记分类头的权重),并且该模型应该被训练。我们将在稍后进行训练,但首先让我们仔细检查一下我们的模型是否具有正确的标签数量。

model.config.num_labels
9

⚠️ 如果您的模型具有错误的标签数量,则在稍后调用Trainer.train()方法时会收到一个不明确的错误(类似于“CUDA 错误:设备端断言触发”。这是用户报告此类错误的错误的主要原因,因此请确保您执行此检查,以确认您具有预期的标签数量。

微调模型

现在我们可以训练模型了!在定义 Trainer 之前,我们只需要做两件事:登录 Hugging Face 并定义训练参数。如果在 Notebook 中工作,可以使用一个便捷函数来帮助您完成此操作

from huggingface_hub import notebook_login

notebook_login()

这将显示一个窗口,您可以在其中输入您的 Hugging Face 登录凭据。

如果不在 Notebook 中工作,只需在终端中输入以下行

huggingface-cli login

完成此操作后,我们可以定义 TrainingArguments

from transformers import TrainingArguments

args = TrainingArguments(
    "bert-finetuned-ner",
    evaluation_strategy="epoch",
    save_strategy="epoch",
    learning_rate=2e-5,
    num_train_epochs=3,
    weight_decay=0.01,
    push_to_hub=True,
)

您之前已经见过大部分内容:我们设置了一些超参数(例如学习率、训练的轮数和权重衰减),并指定 push_to_hub=True,以表明我们希望在每个轮结束时保存模型并进行评估,以及将结果上传到模型中心。请注意,您可以使用 hub_model_id 参数指定要推送到哪个仓库的名称(特别是,您需要使用此参数才能推送到组织)。例如,当我们将模型推送到 huggingface-course 组织 时,我们在 TrainingArguments 中添加了 hub_model_id="huggingface-course/bert-finetuned-ner"。默认情况下,使用的仓库将位于您的命名空间中,并以您设置的输出目录命名,因此在我们的例子中将是 "sgugger/bert-finetuned-ner"

💡 如果您使用的输出目录已经存在,则它需要是您要推送到该仓库的本地克隆。如果不是,您将在定义 Trainer 时出现错误,并且需要设置一个新名称。

最后,我们只需将所有内容传递给 Trainer 并启动训练

from transformers import Trainer

trainer = Trainer(
    model=model,
    args=args,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["validation"],
    data_collator=data_collator,
    compute_metrics=compute_metrics,
    tokenizer=tokenizer,
)
trainer.train()

请注意,在训练过程中,模型每次保存时(这里是指每个轮次)都会在后台上传到模型中心。这样,您就可以在需要时在另一台机器上恢复训练。

训练完成后,我们将使用 push_to_hub() 方法确保上传模型的最新版本

trainer.push_to_hub(commit_message="Training complete")

此命令将返回它刚刚执行的提交的 URL(如果您想检查它)。

'https://huggingface.co/sgugger/bert-finetuned-ner/commit/26ab21e5b1568f9afeccdaed2d8715f571d786ed'

Trainer 还会使用所有评估结果起草一个模型卡片并将其上传。在此阶段,您可以使用模型中心的推理窗口测试您的模型并与您的朋友分享。您已成功在标记分类任务上微调了一个模型——恭喜!

如果您想更深入地了解训练循环,我们将向您展示如何使用 🤗 Accelerate 完成相同的事情。

自定义训练循环

现在让我们看一下完整的训练循环,这样您就可以轻松地自定义您需要的部分。它看起来很像我们在 第 3 章 中所做的,只是在评估方面有一些更改。

准备所有内容进行训练

首先,我们需要从我们的数据集中构建 DataLoader。我们将重新使用 data_collator 作为 collate_fn,并对训练集进行洗牌,但不对验证集进行洗牌

from torch.utils.data import DataLoader

train_dataloader = DataLoader(
    tokenized_datasets["train"],
    shuffle=True,
    collate_fn=data_collator,
    batch_size=8,
)
eval_dataloader = DataLoader(
    tokenized_datasets["validation"], collate_fn=data_collator, batch_size=8
)

接下来,我们将重新实例化我们的模型,以确保我们不是从之前开始微调,而是从 BERT 预训练模型开始

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

然后,我们将需要一个优化器。我们将使用经典的 AdamW,它类似于 Adam,但对权重衰减的应用方式进行了修正

from torch.optim import AdamW

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

有了这些对象后,我们可以将它们发送到 accelerator.prepare() 方法

from accelerate import Accelerator

accelerator = Accelerator()
model, optimizer, train_dataloader, eval_dataloader = accelerator.prepare(
    model, optimizer, train_dataloader, eval_dataloader
)

🚨 如果您正在 TPU 上进行训练,则需要将上面的单元格开始的所有代码移到一个专门的训练函数中。有关更多详细信息,请参阅 第 3 章

现在,我们已经将 train_dataloader 发送到 accelerator.prepare(),我们可以使用它的长度来计算训练步骤数。请记住,我们应该始终在准备数据加载器后执行此操作,因为该方法会更改其长度。我们使用从学习率到 0 的经典线性调度

from transformers import get_scheduler

num_train_epochs = 3
num_update_steps_per_epoch = len(train_dataloader)
num_training_steps = num_train_epochs * num_update_steps_per_epoch

lr_scheduler = get_scheduler(
    "linear",
    optimizer=optimizer,
    num_warmup_steps=0,
    num_training_steps=num_training_steps,
)

最后,为了将模型推送到模型中心,我们需要在工作文件夹中创建一个 Repository 对象。首先,如果您还没有登录,请登录 Hugging Face。我们将从要给模型提供的模型 ID 中确定仓库名称(可以随意替换 repo_name,它只需要包含您的用户名,这就是函数 get_full_repo_name() 的作用)

from huggingface_hub import Repository, get_full_repo_name

model_name = "bert-finetuned-ner-accelerate"
repo_name = get_full_repo_name(model_name)
repo_name
'sgugger/bert-finetuned-ner-accelerate'

然后,我们可以在本地文件夹中克隆该仓库。如果它已经存在,则此本地文件夹应该是我们正在使用的仓库的现有克隆

output_dir = "bert-finetuned-ner-accelerate"
repo = Repository(output_dir, clone_from=repo_name)

现在,我们可以通过调用 repo.push_to_hub() 方法上传在 output_dir 中保存的任何内容。这将帮助我们在每个轮次结束时上传中间模型。

训练循环

现在我们可以编写完整的训练循环。为了简化其评估部分,我们定义了这个 postprocess() 函数,它接收预测和标签并将它们转换为字符串列表,就像我们的 metric 对象所期望的那样

def postprocess(predictions, labels):
    predictions = predictions.detach().cpu().clone().numpy()
    labels = labels.detach().cpu().clone().numpy()

    # Remove ignored index (special tokens) and convert to labels
    true_labels = [[label_names[l] for l in label if l != -100] for label in labels]
    true_predictions = [
        [label_names[p] for (p, l) in zip(prediction, label) if l != -100]
        for prediction, label in zip(predictions, labels)
    ]
    return true_labels, true_predictions

然后,我们可以编写训练循环。在定义一个进度条以跟踪训练进度后,循环包含三个部分

  • 训练本身,它就是对 train_dataloader 的经典迭代,通过模型进行前向传递,然后进行反向传递和优化器步骤。
  • 评估,其中在获取模型在批次上的输出后出现了一个新颖之处:由于两个进程可能将输入和标签填充到不同的形状,我们需要使用 accelerator.pad_across_processes() 使预测和标签具有相同的形状,然后再调用 gather() 方法。如果我们不这样做,评估将会出错或永远挂起。然后,我们将结果发送到 metric.add_batch(),并在评估循环结束后调用 metric.compute()
  • 保存和上传,在这里我们首先保存模型和分词器,然后调用 repo.push_to_hub()。请注意,我们使用参数 blocking=False 来告诉 🤗 模型中心库以异步方式进行推送。这样,训练就可以正常进行,并且这个(很长的)指令在后台执行。

以下是训练循环的完整代码

from tqdm.auto import tqdm
import torch

progress_bar = tqdm(range(num_training_steps))

for epoch in range(num_train_epochs):
    # Training
    model.train()
    for batch in train_dataloader:
        outputs = model(**batch)
        loss = outputs.loss
        accelerator.backward(loss)

        optimizer.step()
        lr_scheduler.step()
        optimizer.zero_grad()
        progress_bar.update(1)

    # Evaluation
    model.eval()
    for batch in eval_dataloader:
        with torch.no_grad():
            outputs = model(**batch)

        predictions = outputs.logits.argmax(dim=-1)
        labels = batch["labels"]

        # Necessary to pad predictions and labels for being gathered
        predictions = accelerator.pad_across_processes(predictions, dim=1, pad_index=-100)
        labels = accelerator.pad_across_processes(labels, dim=1, pad_index=-100)

        predictions_gathered = accelerator.gather(predictions)
        labels_gathered = accelerator.gather(labels)

        true_predictions, true_labels = postprocess(predictions_gathered, labels_gathered)
        metric.add_batch(predictions=true_predictions, references=true_labels)

    results = metric.compute()
    print(
        f"epoch {epoch}:",
        {
            key: results[f"overall_{key}"]
            for key in ["precision", "recall", "f1", "accuracy"]
        },
    )

    # Save and upload
    accelerator.wait_for_everyone()
    unwrapped_model = accelerator.unwrap_model(model)
    unwrapped_model.save_pretrained(output_dir, save_function=accelerator.save)
    if accelerator.is_main_process:
        tokenizer.save_pretrained(output_dir)
        repo.push_to_hub(
            commit_message=f"Training in progress epoch {epoch}", blocking=False
        )

如果这是您第一次看到使用 🤗 Accelerate 保存的模型,让我们花点时间检查一下与之相关的三行代码

accelerator.wait_for_everyone()
unwrapped_model = accelerator.unwrap_model(model)
unwrapped_model.save_pretrained(output_dir, save_function=accelerator.save)

第一行不言自明:它告诉所有进程在继续之前等待每个人都到达该阶段。这样做是为了确保在保存之前,每个进程都具有相同的模型。然后,我们获取 unwrapped_model,它是我们定义的基本模型。accelerator.prepare() 方法会更改模型以在分布式训练中工作,因此它将不再具有 save_pretrained() 方法;accelerator.unwrap_model() 方法会撤消该步骤。最后,我们调用 save_pretrained(),但告诉该方法使用 accelerator.save() 而不是 torch.save()

完成此操作后,您应该得到一个与使用 Trainer 训练的模型结果非常相似的模型。您可以在 huggingface-course/bert-finetuned-ner-accelerate 中检查我们使用此代码训练的模型。如果您想测试对训练循环的任何调整,您可以直接通过编辑上面显示的代码来实现它们!

使用微调的模型

我们已经向您展示了如何在模型中心使用推理窗口使用我们微调的模型。为了在本地 pipeline 中使用它,您只需指定正确的模型标识符

from transformers import pipeline

# Replace this with your own checkpoint
model_checkpoint = "huggingface-course/bert-finetuned-ner"
token_classifier = pipeline(
    "token-classification", model=model_checkpoint, aggregation_strategy="simple"
)
token_classifier("My name is Sylvain and I work at Hugging Face in Brooklyn.")
[{'entity_group': 'PER', 'score': 0.9988506, 'word': 'Sylvain', 'start': 11, 'end': 18},
 {'entity_group': 'ORG', 'score': 0.9647625, 'word': 'Hugging Face', 'start': 33, 'end': 45},
 {'entity_group': 'LOC', 'score': 0.9986118, 'word': 'Brooklyn', 'start': 49, 'end': 57}]

太棒了!我们的模型与该管道的默认模型一样好用!