LLM 课程文档
Token 分类
并获得增强的文档体验
开始使用
我们将探索的第一个应用是 token 分类。这个通用任务涵盖了任何可以表述为“为句子中的每个 token 赋予标签”的问题,例如
当然,还有许多其他类型的 token 分类问题;这些只是一些有代表性的例子。在本节中,我们将在一个 NER 任务上微调模型 (BERT),然后该模型将能够计算如下的预测


您可以在 这里 找到我们将要训练并上传到 Hub 的模型,并双重检查其预测。
首先,我们需要一个适合 token 分类的数据集。在本节中,我们将使用 CoNLL-2003 数据集,其中包含路透社的新闻报道。
💡 只要您的数据集由分成单词的文本及其对应的标签组成,您就可以将此处描述的数据处理过程应用到您自己的数据集。如果您需要复习如何在 Dataset
中加载您自己的自定义数据,请回顾 第 5 章。
为了加载 CoNLL-2003 数据集,我们使用 🤗 Datasets 库中的 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_feature
的 feature
属性中,我们可以通过查看该 feature
的 names
属性来访问名称列表
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'
正如我们所见,跨越两个词的实体,例如“European Union”和“Werner Zwingmann”,第一个词被赋予 B-
标签,第二个词被赋予 I-
标签。
✏️ 轮到你了! 打印相同的两个句子及其 POS 或组块分析标签。
与往常一样,我们的文本需要转换为 token ID,模型才能理解它们。正如我们在 第 6 章 中看到的,在 token 分类任务的情况下,一个很大的不同是我们有预分词的输入。幸运的是,分词器 API 可以很容易地处理这个问题;我们只需要使用一个特殊的标志来警告 tokenizer
。
首先,让我们创建我们的 tokenizer
对象。正如我们之前所说,我们将使用 BERT 预训练模型,因此我们将首先下载并缓存相关的分词器
from transformers import AutoTokenizer
model_checkpoint = "bert-base-cased"
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)
您可以将 model_checkpoint
替换为您在 Hub 中喜欢的任何其他模型,或者替换为您已保存预训练模型和分词器的本地文件夹。唯一的限制是分词器需要由 🤗 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]']
正如我们所见,分词器添加了模型使用的特殊 token(开头的 [CLS]
和结尾的 [SEP]
),并保留了大多数单词不变。然而,单词 lamb
被分词为两个子词,la
和 ##mb
。这在我们的输入和标签之间引入了不匹配:标签列表只有 9 个元素,而我们的输入现在有 12 个 token。考虑特殊 token 很容易(我们知道它们在开头和结尾),但我们还需要确保我们将所有标签与正确的单词对齐。
幸运的是,由于我们使用的是快速分词器,我们可以访问 🤗 Tokenizers 的超能力,这意味着我们可以轻松地将每个 token 映射到其对应的单词(如 第 6 章 中所示)
inputs.word_ids()
[None, 0, 1, 2, 3, 4, 5, 6, 7, 7, 8, None]
通过少量的工作,我们可以扩展我们的标签列表以匹配 token。我们将应用的第一个规则是特殊 token 获得 -100
的标签。这是因为默认情况下,-100
是在我们将要使用的损失函数(交叉熵)中被忽略的索引。然后,每个 token 都获得与其内部单词的起始 token 相同的标签,因为它们是同一实体的一部分。对于单词内部但不在开头的 token,我们将 B-
替换为 I-
(因为 token 不开始实体)
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]
正如我们所见,我们的函数为开头和结尾的两个特殊 token 添加了 -100
,并为我们分成两个 token 的单词添加了一个新的 0
。
✏️ 轮到你了! 一些研究人员更喜欢每个单词只分配一个标签,并将 -100
分配给给定单词中的其他子 token。这是为了避免长单词分裂成大量子 token,从而对损失产生很大影响。更改之前的函数,通过遵循此规则将标签与输入 ID 对齐。
为了预处理我们的整个数据集,我们需要对所有输入进行分词,并在所有标签上应用 align_labels_with_tokens()
。为了利用我们快速分词器的速度,最好同时对大量文本进行分词,因此我们将编写一个处理示例列表的函数,并使用带有选项 batched=True
的 Dataset.map()
方法。与我们之前的示例唯一不同的是,当分词器的输入是文本列表(或在我们的例子中,单词列表的列表)时,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
的实际代码将与之前相同;唯一的更改是将数据整理成批次的方式和指标计算函数。
我们不能像 第 3 章 中那样直接使用 DataCollatorWithPadding
,因为这只会填充输入(输入 ID、注意力掩码和 token 类型 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()
函数,该函数接受预测和标签的数组,并返回一个包含指标名称和值的字典。
用于评估 token 分类预测的传统框架是 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
来微调!
由于我们正在处理 token 分类问题,我们将使用 AutoModelForTokenClassification
类。定义此模型时要记住的主要事项是传递有关我们拥有的标签数量的一些信息。执行此操作的最简单方法是使用 num_labels
参数传递该数字,但是如果我们想要一个像我们在本节开头看到的那么好的推理小部件,最好设置正确的标签对应关系。
它们应该由两个字典 id2label
和 label2id
设置,其中包含从 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
时一样,创建模型会发出警告,提示某些权重未使用(来自预训练头的权重),而另一些权重是随机初始化的(来自新的 token 分类头的权重),并且应该训练此模型。我们将在稍后执行此操作,但首先让我们仔细检查我们的模型是否具有正确数量的标签
model.config.num_labels
9
⚠️ 如果您的模型标签数量不正确,则稍后在调用 Trainer.train()
方法时会收到一个模糊的错误(类似于“CUDA error: device-side assert triggered”)。这是用户报告此类错误的首要原因,因此请务必执行此检查以确认您具有预期数量的标签。
我们现在准备好训练我们的模型了!在定义我们的 Trainer
之前,我们只需要做最后两件事:登录 Hugging Face 并定义我们的训练参数。如果您在笔记本中工作,则有一个方便的功能可以帮助您完成此操作
from huggingface_hub import notebook_login
notebook_login()
这将显示一个小部件,您可以在其中输入您的 Hugging Face 登录凭据。
如果您不在笔记本中工作,只需在终端中键入以下行
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,
)
您之前已经看过其中的大部分内容:我们设置了一些超参数(例如学习率、要训练的 epoch 数和权重衰减),并且我们指定 push_to_hub=True
以指示我们想要保存模型并在每个 epoch 结束时对其进行评估,并且我们想要将我们的结果上传到 Model Hub。请注意,您可以使用 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()
请注意,在训练发生时,每次保存模型时(此处为每个 epoch),它都会在后台上传到 Hub。这样,如果需要,您将能够在另一台机器上恢复训练。
训练完成后,我们使用 push_to_hub()
方法确保我们上传模型的最新版本
trainer.push_to_hub(commit_message="Training complete")
如果您想检查它,此命令将返回它刚刚执行的提交的 URL
'https://huggingface.co/sgugger/bert-finetuned-ner/commit/26ab21e5b1568f9afeccdaed2d8715f571d786ed'
Trainer
还会起草一个模型卡,其中包含所有评估结果并上传它。在此阶段,您可以使用 Model Hub 上的推理小部件来测试您的模型并与您的朋友分享。您已成功在一个 token 分类任务上微调了一个模型 — 恭喜!
如果您想更深入地了解训练循环,我们现在将向您展示如何使用 🤗 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 的经典线性 schedule。
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,
)
最后,要将我们的模型推送到 Hub,我们需要在工作文件夹中创建一个 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
中保存的任何内容。 这将帮助我们在每个 epoch 结束时上传中间模型。
训练循环
现在我们准备好编写完整的训练循环。 为了简化其评估部分,我们定义了这个 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()
。 - 保存和上传,我们首先保存模型和 tokenizer,然后调用
repo.push_to_hub()
。 请注意,我们使用参数blocking=False
来告诉 🤗 Hub 库在异步进程中推送。 这样,训练可以正常继续,并且此(长时间的)指令在后台执行。
这是训练循环的完整代码。
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 查看我们使用此代码训练的模型。 如果你想测试对训练循环的任何调整,你可以通过编辑上面显示的代码直接实现它们!
使用微调后的模型
我们已经向你展示了如何使用我们在 Model Hub 上微调的模型以及推理小部件。 要在本地的 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}]
太棒了! 我们的模型运行效果与此 pipeline 的默认模型一样好!
< > 在 GitHub 上更新