前文我们探索了如何使用标记器(Tokenizer)和预训练模型进行预测。但是,如果您想为自己的数据集微调预训练模型,该怎么做呢?
处理数据
下面是我们用 PyTorch 在一个分批上训练句子分类器的例子:
import torch from transformers import AdamW, AutoTokenizer, AutoModelForSequenceClassification # Same as before checkpoint = "bert-base-uncased" tokenizer = AutoTokenizer.from_pretrained(checkpoint) model = AutoModelForSequenceClassification.from_pretrained(checkpoint) sequences = [ "I've been waiting for a HuggingFace course my whole life.", "This course is amazing!", ] batch = tokenizer(sequences, padding=True, truncation=True, return_tensors="pt") # This is new batch["labels"] = torch.tensor([1, 1]) optimizer = AdamW(model.parameters()) loss = model(**batch).loss loss.backward() optimizer.step()
当然,仅仅用两句话训练模型不会产生很好的效果。为了获得更好的结果,您需要准备一个更大的数据集。
在本节中,我们将使用MRPC(微软研究释义语料库)数据集作为示例,该数据集由威廉·多兰和克里斯·布罗克特在这篇论文发布。该数据集由5801对句子组成,每个句子对带有一个标签,指示它们是否为同义(即,如果两个句子的意思相同)。选择它是因为它是一个小数据集,所以很容易对它进行训练。
从 Hub 加载数据集
模型中心(Hub)不只是包含模型;它也有许多不同语言的数据集。点击数据集的链接即可进行浏览。现在,让我们使用 MRPC 数据集,它是构成GLUE 基准测试的10个数据集之一,这是一个学术基准,用于衡量机器学习模型在10个不同文本分类任务中的性能。
HuggingFace Datasets 库提供了一个非常便捷的命令,可以在模型中心上下载和缓存数据集。我们可以通过以下的代码下载 MRPC 数据集:
from datasets import load_dataset raw_datasets = load_dataset("glue", "mrpc") raw_datasets
DatasetDict({ train: Dataset({ features: ['sentence1', 'sentence2', 'label', 'idx'], num_rows: 3668 }) validation: Dataset({ features: ['sentence1', 'sentence2', 'label', 'idx'], num_rows: 408 }) test: Dataset({ features: ['sentence1', 'sentence2', 'label', 'idx'], num_rows: 1725 }) })
正如你所看到的,我们获得了一个DatasetDict
对象,其中包含训练集、验证集和测试集。每一个集合都包含几个列(sentence1
, sentence2
, label
, 和idx
)以及一个代表行数的变量,即每个集合中的行的个数(因此,训练集中有3668对句子,验证集中有408对,测试集中有1725对)。
默认情况下,此命令下载数据集并缓存到 ~/.cache/huggingface/datasets。您可以通过设置HF_HOME
环境变量来自定义缓存的文件夹。
我们可以访问我们数据集中的每一个raw_train_dataset
对象,如使用字典:
raw_train_dataset = raw_datasets["train"] raw_train_dataset[0]
{'idx': 0, 'label': 1, 'sentence1': 'Amrozi accused his brother , whom he called " the witness " , of deliberately distorting his evidence .', 'sentence2': 'Referring to him as only " the witness " , Amrozi accused his brother of deliberately distorting his evidence .'}
我们可以看到标签已经是整数了,所以我们不需要对标签做任何预处理。要知道哪个数字对应于哪个标签,我们可以查看raw_train_dataset
的features
。这将告诉我们每列的类型:
raw_train_dataset.features
{'sentence1': Value(dtype='string', id=None), 'sentence2': Value(dtype='string', id=None), 'label': ClassLabel(num_classes=2, names=['not_equivalent', 'equivalent'], names_file=None, id=None), 'idx': Value(dtype='int32', id=None)}
在上面的例子之中,标签是ClassLabel
类型,使用整数建立起到类别标签的映射关系。0对应于not_equivalent
,1对应于equivalent
。
预处理数据集
为了预处理数据集,我们需要将文本转换为模型能够理解的数字。正如你在前文看到的那样
from transformers import AutoTokenizer checkpoint = "bert-base-uncased" tokenizer = AutoTokenizer.from_pretrained(checkpoint) tokenized_sentences_1 = tokenizer(raw_datasets["train"]["sentence1"]) tokenized_sentences_2 = tokenizer(raw_datasets["train"]["sentence2"])
然而,我们不能直接将两个序列传递给模型,预测这两句话是否是同义。我们需要处理成序列对,并进行适当的预处理。幸运的是,标记器还可以输入序列对,并按照我们的 BERT 模型所期望的输入进行处理:
inputs = tokenizer("This is the first sentence.", "This is the second one.") inputs
{ 'input_ids': [101, 2023, 2003, 1996, 2034, 6251, 1012, 102, 2023, 2003, 1996, 2117, 2028, 1012, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }
我们在前文讨论过input_ids
和attention_mask
,但我们在那个时候没有讨论token_type_ids
。在这个例子中,类型标记 ID 的作用就是告诉模型输入的哪一部分是第一句,哪一部分是第二句。
如果我们将input_ids
中的 ID 转换回词:
tokenizer.convert_ids_to_tokens(inputs["input_ids"])
我们将得到:
['[CLS]', 'this', 'is', 'the', 'first', 'sentence', '.', '[SEP]', 'this', 'is', 'the', 'second', 'one', '.', '[SEP]']
所以我们看到模型需要输入的形式是 [CLS] sentence1 [SEP] sentence2 [SEP]
。因此,当有两句话的时候,token_type_ids
的值是:
['[CLS]', 'this', 'is', 'the', 'first', 'sentence', '.', '[SEP]', 'this', 'is', 'the', 'second', 'one', '.', '[SEP]'] [ 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1]
如您所见,输入中 [CLS] sentence1 [SEP]
的类型标记 ID 均为0,而其他部分,对应于sentence2 [SEP]
,所有的类型标记 ID 均为1。
请注意,如果选择其他的检查点,则不一定有token_type_ids
(例如,如果使用 DistilBERT 模型,就不会返回它们)。只有在预训练期间使用过这一层,模型在构建时依赖它们,才会返回它们。
BERT 用类型标记 ID 进行预训练,并且在使用的遮罩语言建模目标之上,还有一个额外的目标,叫做下一句预测。这项任务的目标是建立成对句子之间关系的模型。
在下一句预测任务中,会给模型输入成对的句子(带有随机遮罩的标记),并被要求预测第二个句子是否紧跟第一个句子。为了提高模型的泛化能力,数据集中一半情况两个句子在原始文档中挨在一起,另一半两个句子来自两个不同的文档。
一般来说,你不需要担心在您的标记输入中是否有token_type_ids
:只要您对标记器和模型使用相同的检查点,一切都会很好,因为标记器知道向其模型提供什么。
现在我们已经了解了标记器如何处理一对句子,我们可以使用它对整个数据集进行处理:我们可以给标记器提供一组句子,第一个参数是它第一个句子的列表,第二个参数是第二个句子的列表。这也与我们在前文看到的填充和截断选项兼容。因此,预处理训练数据集的一种方法是:
tokenized_dataset = tokenizer( raw_datasets["train"]["sentence1"], raw_datasets["train"]["sentence2"], padding=True, truncation=True, )
这很有效,但它的缺点是返回字典(字典的键是input_ids
,attention_mask
和token_type_ids
,字典的值是列表的列表)。而且只有当您在转换过程中有足够的内存来存储整个数据集时才不会出错。
为了将数据保存为数据集,我们将使用Dataset.map()
方法,如果我们需要做更多的预处理而不仅仅是标记化,那么这也给了我们一些额外的自定义的方法。这个方法的工作原理是在数据集的每个元素上应用一个函数,因此让我们定义一个标记输入的函数:
def tokenize_function(example): return tokenizer(example["sentence1"], example["sentence2"], truncation=True)
此函数的输入是一个字典(与数据集的项类似),并返回一个包含input_ids
,attention_mask
和token_type_ids
键的新字典。请注意,如果像上面的示例一样,如果键所对应的值包含多个句子(每个键有一个句子列表),那么它依然可以工作,就像前面的例子一样标记器可以处理成对的句子列表。我们可以在调用map()
时使用选项batched=True
,这将显著加快标记的速度。这个标记器来自 HuggingFace Tokenizers库,由 Rust 编写而成。当我们一次给它大量的输入时,这个标记器可以非常快。
请注意,我们现在在标记函数中省略了padding
参数。这是因为在标记的时候将所有样本填充到最大长度的效率不高,在构建分批时填充样本更好,因为这样我们只需要填充到该分批中的最大长度,而不是整个数据集的最大长度。当输入长度变化很大时,这可以节省大量时间和处理能力!
下面是我们如何在所有数据集上同时应用标记函数。我们在调用map
时使用了batch=True
,这样函数就可以同时应用到数据集的多个元素上,而不是分别应用到每个元素上。这将使我们的预处理快许多。
tokenized_datasets = raw_datasets.map(tokenize_function, batched=True) tokenized_datasets
HuggingFace Datasets 库应用这种处理的方式是向数据集添加新的字段,每个字段对应预处理函数返回的字典中的每个键:
DatasetDict({ train: Dataset({ features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'], num_rows: 3668 }) validation: Dataset({ features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'], num_rows: 408 }) test: Dataset({ features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'], num_rows: 1725 }) })
在使用预处理函数map()
时,甚至可以通过传递num_proc
参数使用并行处理。我们在这里没有这样做,因为 HuggingFace Tokenizers 库已经使用多个线程来更快地标记我们的样本,但是如果您没有使用该库支持的快速标记器,使用num_proc
可能会加快预处理。
我们的tokenize_function
返回包含input_ids
,attention_mask
和token_type_ids
键的字典,所以这三个字段被添加到数据集的标记的结果中。注意,如果预处理函数map()
为现有键返回一个新值,那将会修改原有键的值。
最后一件我们需要做的事情是,当我们把元素分批时,将所有示例填充到最长元素的长度——我们称之为动态填充。
动态填充
负责将数据整理为一个分批的函数称为collate函数。它是你可以在构建DataLoader
时传递的一个参数,默认是一个函数,它将把你的数据集转换为 PyTorch 张量,并将它们拼接起来(如果你的元素是列表、元组或字典,则会使用递归)。这在我们的这个例子中是不可行的,因为我们的输入不都是相同大小的。我们故意在之后每个分批上进行填充,避免有太多填充的过长的输入。这将大大加快训练速度,但请注意,如果你在TPU上训练,这可能会导致问题—— TPU 喜欢固定的形状,即使这需要额外的填充。
为了解决句子长度统一的问题,我们必须定义一个collate函数,该函数会将每个句子分批填充到正确的长度。幸运的是,HuggingFace Transformers 库通过DataCollatorWithPadding
为我们提供了这样一个函数。当你实例化它时,需要一个标记器(用来知道使用哪个词来填充,以及模型期望填充在左边还是右边),并将做你需要的一切:
from transformers import DataCollatorWithPadding data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
为了测试这个新玩具,让我们从我们的训练集中抽取几个样本。这里,我们删除列idx
, sentence1
和sentence2
,因为不需要它们,并查看一个分批中每个条目的长度:
samples = tokenized_datasets["train"][:8] samples = {k: v for k, v in samples.items() if k not in ["idx", "sentence1", "sentence2"]} [len(x) for x in samples["input_ids"]]
[50, 59, 47, 67, 59, 50, 62, 32]
毫无疑问,我们得到了不同长度的样本,从32到67。动态填充意味着该分批中的所有样本都应该填充到长度为67,这是该分批中的最大长度。如果没有动态填充,所有的样本都必须填充到整个数据集中的最大长度,或者模型可以接受的最大长度。让我们再次检查data_collator
是否正确地动态填充了这批样本:
batch = data_collator(samples) {k: v.shape for k, v in batch.items()}
{'attention_mask': torch.Size([8, 67]), 'input_ids': torch.Size([8, 67]), 'token_type_ids': torch.Size([8, 67]), 'labels': torch.Size([8])}
看起来不错!现在,我们已经将原始文本转化为了模型可以处理的数据,我们已准备好对其进行微调!
使用 Trainer API 微调模型
HuggingFace Transformers 提供了一个Trainer
类来帮助您在自己的数据集上微调任何预训练模型。完成上一节中的所有数据预处理工作后,您只需要执行几个步骤来创建Trainer
。最难的部分可能是为Trainer.train()
配置运行环境,因为它在 CPU 上运行速度会非常慢。如果您没有设置 GPU,您可以访问免费的 GPU 或 Google Colab的 TPU。
下面的示例假设您已经执行了前面的示例。下面这段代码,概括了您需要提前运行的代码:
from datasets import load_dataset from transformers import AutoTokenizer, DataCollatorWithPadding raw_datasets = load_dataset("glue", "mrpc") checkpoint = "bert-base-uncased" tokenizer = AutoTokenizer.from_pretrained(checkpoint) def tokenize_function(example): return tokenizer(example["sentence1"], example["sentence2"], truncation=True) tokenized_datasets = raw_datasets.map(tokenize_function, batched=True) data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
训练
在我们定义我们的Trainer
之前首先要定义一个TrainingArguments
类,它将包含Trainer
用于训练和评估的所有超参数。您唯一必须提供的参数是保存训练模型的目录,以及训练过程中的检查点。对于其余的参数,您可以保留默认值,这对于基本微调应该非常有效。
from transformers import TrainingArguments training_args = TrainingArguments("test-trainer")
第二步是定义我们的模型。我们将使用AutoModelForSequenceClassification
类,它有两个参数:
from transformers import AutoModelForSequenceClassification model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)
你会注意到,和前文不一样的是,在实例化此预训练模型后会收到警告。这是因为 BERT 没有在句子分类方面进行过预训练,所以预训练模型的头部已经被丢弃,添加了一个适合句子序列分类的新头部。警告表示一些权重没有使用(对应于丢弃的预训练头部的那些),而其他一些权重被随机初始化(新头部的那些)。最后鼓励您训练模型,这正是我们现在要做的。
一旦我们有了我们的模型,我们就可以定义一个Trainer
,将之前构造的所有对象传递给它——我们的model
、training_args
,训练和验证数据集,data_collator
,和tokenizer
:
from transformers import Trainer trainer = Trainer( model, training_args, train_dataset=tokenized_datasets["train"], eval_dataset=tokenized_datasets["validation"], data_collator=data_collator, tokenizer=tokenizer, )
请注意,当您在这里像上面那样传入tokenizer
,Trainer
默认使用的data_collator
会使用之前预定义的DataCollatorWithPadding
,因此您可以在这个例子中跳过data_collator=data_collator
。
为了让预训练模型在我们的数据集上微调,我们只需要调用Trainer
的train()
方法 :
trainer.train()
这将开始微调(在 GPU 上应该需要几分钟),并每500步报告一次训练损失。但是,它不会告诉您模型的性能如何(或质量如何)。这是因为:
- 我们没有通过将
evaluation_strategy
设置为“steps
”(在每次更新参数的时候评估)或“epoch
”(在每个epoch结束时评估)来告诉Trainer
在训练期间进行评估。 - 我们没有为
Trainer
提供一个compute_metrics()
函数来直接计算模型的好坏(评估将只输出loss,这不是一个非常直观的数字)。
评估
让我们看看如何构建一个有用的compute_metrics()
函数并在我们下次训练时使用它。该函数必须采用EvalPrediction
对象(带有 predictions
和 label_ids
字段的参数元组)并将返回一个字符串到浮点数的字典(字符串是返回的指标的名称,而浮点数是它们的值)。我们可以使用 Trainer.predict()
命令来用我们的模型进行预测:
predictions = trainer.predict(tokenized_datasets["validation"]) print(predictions.predictions.shape, predictions.label_ids.shape)
(408, 2) (408,)
predict()
的输出结果是具有三个字段的命名元组: predictions
, label_ids
, 和 metrics
。metrics
字段将只包含传递的数据集的loss,以及一些运行时间(预测所需的总时间和平均时间)。如果我们定义了自己的 compute_metrics()
函数并将其传递给 Trainer
,该字段还将包含compute_metrics()
的结果。
如你所看到的, predictions
是一个形状为 408 x 2 的二维数组(408 是我们使用的数据集中元素的数量)。这些是我们传递给predict()
的数据集的每个元素的logits。要将我们的预测的可以与真正的标签进行比较,我们需要在第二个轴上取最大值的索引:
import numpy as np preds = np.argmax(predictions.predictions, axis=-1)
现在建立我们的 compute_metric()
函数来较为直观地评估模型的好坏,我们将使用 HuggingFace Evaluate 库中的指标。我们可以像加载数据集一样轻松加载与 MRPC 数据集关联的指标,这次使用 evaluate.load()
函数。返回的对象有一个 compute()
方法我们可以用来进行度量计算的方法:
import evaluate metric = evaluate.load("glue", "mrpc") metric.compute(predictions=preds, references=predictions.label_ids)
{'accuracy': 0.8578431372549019, 'f1': 0.8996539792387542}
您获得的确切结果可能会有所不同,因为模型头的随机初始化可能会影响最终建立的模型。在这里,我们可以看到我们的模型在验证集上的准确率为 85.78%,F1 分数为 89.97。这是用于评估 GLUE 基准的 MRPC 数据集结果的两个指标。而在BERT 论文中展示的基础模型的 F1 分数为 88.9。那是 uncased
模型,而我们目前正在使用 cased
模型,通过改进得到了更好的结果。
最后将所有东西打包在一起,我们得到了我们的 compute_metrics()
函数:
def compute_metrics(eval_preds): metric = evaluate.load("glue", "mrpc") logits, labels = eval_preds predictions = np.argmax(logits, axis=-1) return metric.compute(predictions=predictions, references=labels)
为了查看模型在每个训练周期结束的好坏,下面是我们如何使用compute_metrics()
函数定义一个新的 Trainer
:
training_args = TrainingArguments("test-trainer", evaluation_strategy="epoch") model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2) trainer = Trainer( model, training_args, train_dataset=tokenized_datasets["train"], eval_dataset=tokenized_datasets["validation"], data_collator=data_collator, tokenizer=tokenizer, compute_metrics=compute_metrics, )
请注意,我们设置了了一个新的 TrainingArguments
它的evaluation_strategy
设置为 epoch
并创建了一个新模型。如果不创建新的模型就直接训练,就只会继续训练之前我们已经训练过的模型。要启动新的训练运行,我们执行:
trainer.train()
这一次,它将在训练loss之外,还会输出每个 epoch
结束时的验证loss和指标。同样,由于模型的随机头部初始化,您达到的准确率/F1 分数可能与我们发现的略有不同,但它应该在同一范围内。
Trainer
将在多个 GPU 或 TPU 上开箱即用,并提供许多选项,例如混合精度训练(在训练的参数中使用 fp16 = True
)。
使用Trainer API微调的介绍到此结束。
一个完整的训练
现在,我们将了解如何在不使用Trainer
类的情况下获得与上一节相同的结果。下面是一个简短的总结,涵盖了您需要的所有内容:
from datasets import load_dataset from transformers import AutoTokenizer, DataCollatorWithPadding raw_datasets = load_dataset("glue", "mrpc") checkpoint = "bert-base-uncased" tokenizer = AutoTokenizer.from_pretrained(checkpoint) def tokenize_function(example): return tokenizer(example["sentence1"], example["sentence2"], truncation=True) tokenized_datasets = raw_datasets.map(tokenize_function, batched=True) data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
训练前的准备
在实际编写我们的训练循环之前,我们需要定义一些对象。第一个是我们将用于迭代批次的数据加载器。我们需要对我们的tokenized_datasets
做一些处理,来处理Trainer
自动为我们做的一些事情。具体来说,我们需要:
- 删除与模型不期望的值相对应的列(如
sentence1
和sentence2
列)。 - 将列名label重命名为labels(因为模型期望参数是
labels
)。 - 设置数据集的格式,使其返回 PyTorch 张量而不是列表。
针对上面的每个步骤,我们的 tokenized_datasets
都有一个方法:
tokenized_datasets = tokenized_datasets.remove_columns(["sentence1", "sentence2", "idx"]) tokenized_datasets = tokenized_datasets.rename_column("label", "labels") tokenized_datasets.set_format("torch") tokenized_datasets["train"].column_names
然后,我们可以检查结果中是否只有模型能够接受的列:
["attention_mask", "input_ids", "labels", "token_type_ids"]
至此,我们可以轻松定义数据加载器:
from torch.utils.data import DataLoader train_dataloader = DataLoader( tokenized_datasets["train"], shuffle=True, batch_size=8, collate_fn=data_collator ) eval_dataloader = DataLoader( tokenized_datasets["validation"], batch_size=8, collate_fn=data_collator )
为了快速检验数据处理中没有错误,我们可以这样检验其中的一个批次:
for batch in train_dataloader: break {k: v.shape for k, v in batch.items()}
{'attention_mask': torch.Size([8, 65]), 'input_ids': torch.Size([8, 65]), 'labels': torch.Size([8]), 'token_type_ids': torch.Size([8, 65])}
请注意,实际的形状可能与您略有不同,因为我们为训练数据加载器设置了shuffle=True
,并且模型会将句子填充到batch中的最大长度。
现在我们已经完全完成了数据预处理(对于任何 ML 从业者来说都是一个令人满意但难以实现的目标),让我们将注意力转向模型。我们完全像在上一节中所做的那样实例化它:
from transformers import AutoModelForSequenceClassification model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)
为了确保训练过程中一切顺利,我们将batch传递给这个模型:
outputs = model(**batch) print(outputs.loss, outputs.logits.shape)
tensor(0.5441, grad_fn=<NllLossBackward>) torch.Size([8, 2])
当我们提供 labels
时, HuggingFace Transformers 模型都将返回这个batch的loss,我们还得到了 logits(batch中的每个输入有两个,所以张量大小为 8 x 2)。
我们几乎准备好编写我们的训练循环了!我们只是缺少两件事:优化器和学习率调度器。由于我们试图自行实现 Trainer
的功能,我们将使用相同的优化器和学习率调度器。Trainer
使用的优化器是 AdamW
, 与 Adam
相同,但在权重衰减正则化方面有所不同(参见“Decoupled Weight Decay Regularization”,作者 Ilya Loshchilov 和 Frank Hutter):
from transformers import AdamW optimizer = AdamW(model.parameters(), lr=5e-5)
最后,默认使用的学习率调度器只是从最大值 (5e-5) 到 0 的线性衰减。 为了定义它,我们需要知道我们训练的次数,即所有数据训练的次数(epoch)乘以的数据量(这是我们所有训练数据的数量)。Trainer
默认情况下使用三个epoch,因此我们定义训练过程如下:
from transformers import get_scheduler num_epochs = 3 num_training_steps = num_epochs * len(train_dataloader) lr_scheduler = get_scheduler( "linear", optimizer=optimizer, num_warmup_steps=0, num_training_steps=num_training_steps, ) print(num_training_steps)
1377
训练循环
最后一件事:如果我们可以访问 GPU,我们将希望使用 GPU(在 CPU 上,训练可能需要几个小时而不是几分钟)。为此,我们定义了一个 device
,它在GPU可用的情况下指向GPU,我们将把我们的模型和batch放在device
上:
import torch device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") model.to(device) device
device(type='cuda')
我们现在准备好训练了!为了了解训练何时结束,我们使用 tqdm
库,在训练步骤数上添加了一个进度条:
from tqdm.auto import tqdm progress_bar = tqdm(range(num_training_steps)) model.train() for epoch in range(num_epochs): for batch in train_dataloader: batch = {k: v.to(device) for k, v in batch.items()} outputs = model(**batch) loss = outputs.loss loss.backward() optimizer.step() lr_scheduler.step() optimizer.zero_grad() progress_bar.update(1)
您可以看到训练循环的核心与介绍中的非常相似。我们没有要求任何检验,所以这个训练循环不会告诉我们任何关于模型目前的状态。我们需要为此添加一个评估循环。
评估循环
正如我们之前所做的那样,我们将使用 HuggingFace Evaluate 库提供的指标。我们已经了解了 metric.compute()
方法,当我们使用 add_batch()
方法进行预测循环时,实际上该指标可以为我们累积所有 batch 的结果。一旦我们累积了所有 batch ,我们就可以使用 metric.compute()
得到最终结果。以下是在评估循环中实现所有这些的方法:
import evaluate metric = evaluate.load("glue", "mrpc") model.eval() for batch in eval_dataloader: batch = {k: v.to(device) for k, v in batch.items()} with torch.no_grad(): outputs = model(**batch) logits = outputs.logits predictions = torch.argmax(logits, dim=-1) metric.add_batch(predictions=predictions, references=batch["labels"]) metric.compute()
{'accuracy': 0.8431372549019608, 'f1': 0.8907849829351535}
同样,由于模型头部初始化和数据改组的随机性,您的结果会略有不同,但它们应该在同一个范围内。
使用 HuggingFace Accelerate 加速您的训练循环
我们之前定义的训练循环在单个 CPU 或 GPU 上运行良好。但是使用HuggingFace Accelerate库,只需进行一些调整,我们就可以在多个 GPU 或 TPU 上启用分布式训练。从创建训练和验证数据加载器开始,我们的手动训练循环如下所示:
from transformers import AdamW, AutoModelForSequenceClassification, get_scheduler model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2) optimizer = AdamW(model.parameters(), lr=3e-5) device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") model.to(device) num_epochs = 3 num_training_steps = num_epochs * len(train_dataloader) lr_scheduler = get_scheduler( "linear", optimizer=optimizer, num_warmup_steps=0, num_training_steps=num_training_steps, ) progress_bar = tqdm(range(num_training_steps)) model.train() for epoch in range(num_epochs): for batch in train_dataloader: batch = {k: v.to(device) for k, v in batch.items()} outputs = model(**batch) loss = outputs.loss loss.backward() optimizer.step() lr_scheduler.step() optimizer.zero_grad() progress_bar.update(1)
以下是变化:
+ from accelerate import Accelerator from transformers import AdamW, AutoModelForSequenceClassification, get_scheduler + accelerator = Accelerator() model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2) optimizer = AdamW(model.parameters(), lr=3e-5) - device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") - model.to(device) + train_dataloader, eval_dataloader, model, optimizer = accelerator.prepare( + train_dataloader, eval_dataloader, model, optimizer + ) num_epochs = 3 num_training_steps = num_epochs * len(train_dataloader) lr_scheduler = get_scheduler( "linear", optimizer=optimizer, num_warmup_steps=0, num_training_steps=num_training_steps ) progress_bar = tqdm(range(num_training_steps)) model.train() for epoch in range(num_epochs): for batch in train_dataloader: - batch = {k: v.to(device) for k, v in batch.items()} outputs = model(**batch) loss = outputs.loss - loss.backward() + accelerator.backward(loss) optimizer.step() lr_scheduler.step() optimizer.zero_grad() progress_bar.update(1)
要添加的第一行是导入Accelerator
。第二行实例化一个 Accelerator
对象 ,它将查看环境并初始化适当的分布式设置。HuggingFace Accelerate 为您处理数据在设备间的传递,因此您可以删除将模型放在设备上的那行代码(或者,如果您愿意,可使用 accelerator.device
代替 device
)。
然后大部分工作会在将数据加载器、模型和优化器发送到的accelerator.prepare()
中完成。这将会把这些对象包装在适当的容器中,以确保您的分布式训练按预期工作。要进行的其余更改是删除将batch放在 device
的那行代码(同样,如果您想保留它,您可以将其更改为使用 accelerator.device
) 并将loss.backward()
替换为accelerator.backward(loss)
。
如果您想复制并粘贴来直接运行,以下是 HuggingFace Accelerate 的完整训练循环:
from accelerate import Accelerator from transformers import AdamW, AutoModelForSequenceClassification, get_scheduler accelerator = Accelerator() model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2) optimizer = AdamW(model.parameters(), lr=3e-5) train_dl, eval_dl, model, optimizer = accelerator.prepare( train_dataloader, eval_dataloader, model, optimizer ) num_epochs = 3 num_training_steps = num_epochs * len(train_dl) lr_scheduler = get_scheduler( "linear", optimizer=optimizer, num_warmup_steps=0, num_training_steps=num_training_steps, ) progress_bar = tqdm(range(num_training_steps)) model.train() for epoch in range(num_epochs): for batch in train_dl: outputs = model(**batch) loss = outputs.loss accelerator.backward(loss) optimizer.step() lr_scheduler.step() optimizer.zero_grad() progress_bar.update(1)
把这个放在train.py
文件中,可以让它在任何类型的分布式设置上运行。要在分布式设置中试用它,请运行以下命令:
accelerate config
这将询问您几个配置的问题并将您的回答转储到此命令使用的配置文件中:
accelerate launch train.py
这将启动分布式训练。如果您想在 Notebook 中尝试此操作(例如,在 Colab 上使用 TPU 进行测试),只需将代码粘贴到 training_function()
并使用以下命令运行最后一个单元格:
from accelerate import notebook_launcher notebook_launcher(training_function)
您可以在HuggingFace Accelerate repo找到更多的示例。