令牌分类
我们将要探索的第一个应用是令牌分类。这个通用任务涵盖了任何可以表述为“将标签分配给句子中的每个令牌”的问题,例如
- 命名实体识别 (NER):找到句子中的实体(例如人、地点或组织)。这可以通过为每个令牌分配一个标签来表述,方法是为每个实体设置一个类别,并为“无实体”设置一个类别。
- 词性标注 (POS):将句子中的每个词标记为对应于特定的词性(例如名词、动词、形容词等)。
- 分块:找到属于同一实体的令牌。这个任务(可以与 POS 或 NER 结合)可以表述为为任何位于块开头的令牌分配一个标签(通常为
B-
),为位于块内部的令牌分配另一个标签(通常为I-
),并将第三个标签(通常为O
)分配给不属于任何块的令牌。
当然,还有许多其他类型的令牌分类问题;这些只是几个代表性的例子。在本节中,我们将对 NER 任务微调一个模型 (BERT),然后它将能够计算像这样预测
您可以在 Hub 中找到我们将训练并上传的模型,并仔细检查其预测 这里.
准备数据
首先,我们需要一个适合令牌分类的数据集。在本节中,我们将使用 CoNLL-2003 数据集,其中包含来自路透社的新闻报道。
💡 只要您的数据集包含分成单词及其对应标签的文本,您就可以将此处描述的数据处理程序调整到您自己的数据集。如果您需要复习如何在 Dataset
中加载自己的自定义数据,请参考 第 5 章。
CoNLL-2003 数据集
要加载 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'
我们可以看到,跨越两个词的实体(例如“欧洲联盟”和“Werner Zwingmann”)将第一个词分配为 B-
标签,将第二个词分配为 I-
标签。
✏️ 您的回合! 打印具有 POS 或分块标签的相同两个句子。
处理数据
像往常一样,我们的文本需要在模型能够理解之前转换为标记 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 章中所做的非常相似。
使用训练器 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
参数传递该数字,但如果我们想要一个像本节开头看到的那样工作的不错的推理小部件,最好设置正确的标签对应关系。
它们应该由两个字典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,
)
就像我们在第三章中定义AutoModelForSequenceClassification
一样,创建模型会发出警告,表明某些权重没有被使用(来自预训练头的权重),而其他一些权重是随机初始化的(来自新的标记分类头),并且此模型应该进行训练。我们将在稍后进行训练,但首先让我们仔细检查一下我们的模型是否具有正确的标签数量。
model.config.num_labels
9
⚠️ 如果你的模型具有错误的标签数量,则在稍后调用Trainer.train()
方法时会遇到一个模糊的错误(类似于“CUDA error: device-side assert triggered”)。这是用户报告此类错误的 bug 的首要原因,所以请确保你进行此检查以确认你拥有预期的标签数量。
微调模型
我们现在准备训练我们的模型!在定义我们的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 结束时保存模型并对其进行评估,并且我们希望将结果上传到模型中心。请注意,你可以使用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
还会草拟包含所有评估结果的模型卡并将其上传。在此阶段,你可以使用模型中心上的推理小部件测试你的模型并与你的朋友分享。你已经成功地针对标记分类任务微调了一个模型——恭喜!
如果你想更深入地了解训练循环,我们现在将向你展示如何使用 🤗 Accelerate 做同样的事情。
自定义训练循环
现在让我们看看完整的训练循环,这样你就可以轻松地自定义你需要的部分。它看起来很像我们在第三章中所做的事情,评估部分有一些变化。
准备所有内容进行训练
首先,我们需要从我们的数据集构建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 上进行训练,你需要将从上面单元格开始的所有代码移入一个专用的训练函数。有关更多详细信息,请参见第三章。
现在我们已经将train_dataloader
发送到accelerator.prepare()
,我们可以使用它的长度来计算训练步骤的数量。请记住,我们应该始终在准备 dataloader 后执行此操作,因为该方法将更改其长度。我们使用从学习率到 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,
)
最后,为了将我们的模型推送到 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()
。 - 保存和上传,我们首先保存模型和标记器,然后调用
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上查看我们使用此代码训练的模型。如果你想测试对训练循环的任何调整,你可以直接通过编辑上面显示的代码来实现它们!
使用微调模型
我们已经向您展示了如何使用我们在模型中心上微调的模型以及推断小部件。为了在 `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}]
太好了!我们的模型与默认模型一样有效地工作!