转载自:https://huggingface.co/learn/nlp-course/zh-CN/

原中文文档有很多地方翻译的太敷衍了,因此才有此系列文章。

NLP课程(三)- 微调预训练模型

预处理数据

1. 加载数据集

以MRPC(微软研究释义语料库)数据集作为示例,该数据集由威廉·多兰和克里斯·布罗克特在这篇文章发布。该数据集由5801对句子组成,每个句子对带有一个标签,指示它们是否为同义(即,如果两个句子的意思相同)。

使用MRPC数据集中的GLUE 基准测试数据集,它是构成MRPC数据集的10个数据集之一,这是一个学术基准,用于衡量机器学习模型在10个不同文本分类任务中的性能。

通过以下的代码下载MRPC数据集:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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, and idx)以及一个代表行数的变量,即每个集合中的行的个数(因此,训练集中有3668对句子,验证集中有408对,测试集中有1725对)。

可以访问数据集中的每一个raw_train_dataset对象,如使用字典:

1
2
3
4
5
6
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_datasetfeatures. 这将告诉我们每列的类型:

1
2
3
4
5
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)}

在上面的例子之中,Label(标签) 是一种ClassLabel(分类标签),使用整数建立起到类别标签的映射关系。0对应于not_equivalent1对应于equivalent

2.预处理数据集

为了预处理数据集,我们需要将文本转换为模型能够理解的数字。正如你在第二章上看到的那样

1
2
3
4
5
6
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模型所期望的输入进行处理:

1
2
3
4
5
6
7
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]
}

我们在第二章 讨论了输入词id(input_ids)注意力遮罩(attention_mask) ,但我们在那个时候没有讨论类型标记ID(token_type_ids)。在这个例子中,**类型标记ID(token_type_ids)**的作用就是告诉模型输入的哪一部分是第一句,哪一部分是第二句。

如果我们将input_ids中的id转换回文字:

1
tokenizer.convert_ids_to_tokens(inputs["input_ids"])

我们将得到(如上一讲所示,tokenizer处理后会多出来标识符)

1
['[CLS]', 'this', 'is', 'the', 'first', 'sentence', '.', '[SEP]', 'this', 'is', 'the', 'second', 'one', '.', '[SEP]']

所以我们看到模型需要输入的形式是 [CLS] sentence1 [SEP] sentence2 [SEP]。因此,当有两句话的时候。类型标记ID(token_type_ids) 的值是:

1
2
['[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.

请注意,如果选择其他的检查点,则不一定具有类型标记ID(token_type_ids)(例如,如果使用DistilBERT模型,就不会返回它们)。只有当它在预训练期间使用过这一层,模型在构建时依赖它们,才会返回它们。

用类型标记ID对BERT进行预训练,并且使用第一章的遮罩语言模型,还有一个额外的应用类型,叫做下一句预测. 这项任务的目标是建立成对句子之间关系的模型。

在下一个句子预测任务中,会给模型输入成对的句子(带有随机遮罩的标记),并被要求预测第二个句子是否紧跟第一个句子。为了提高模型的泛化能力,数据集中一半的两个句子在原始文档中挨在一起,另一半的两个句子来自两个不同的文档。

一般来说,你不需要担心是否有类型标记ID(token_type_ids)。在您的标输入中:只要您对标记器和模型使用相同的检查点,一切都会很好,因为标记器知道向其模型提供什么。

现在我们已经了解了标记器如何处理一对句子,我们可以使用它对整个数据集进行处理:如之前的章节,我们可以给标记器提供一组句子,第一个参数是它第一个句子的列表,第二个参数是第二个句子的列表。这也与我们在第二章中看到的填充和截断选项兼容. 因此,预处理训练数据集的一种方法是:

1
2
3
4
5
6
tokenized_dataset = tokenizer(
raw_datasets["train"]["sentence1"],
raw_datasets["train"]["sentence2"],
padding=True,
truncation=True,
)

这很有效,但它的缺点是返回字典(字典的键是输入词id(input_ids)注意力遮罩(attention_mask)类型标记ID(token_type_ids),字典的值是键所对应值的列表)。而且只有当您在转换过程中有足够的内存来存储整个数据集时才不会出错。

为了将数据保存为数据集,我们将使用Dataset.map()方法,如果我们需要做更多的预处理而不仅仅是标记化,那么这也给了我们一些额外的自定义的方法。这个方法的工作原理是在数据集的每个元素上应用一个函数,因此让我们定义一个标记输入的函数:

1
2
def tokenize_function(example):
return tokenizer(example["sentence1"], example["sentence2"], truncation=True)

此函数的输入是一个字典(与数据集的项类似),并返回一个包含输入词id(input_ids) , 注意力遮罩(attention_mask)类型标记ID(token_type_ids) 键的新字典。请注意,如果像上面的示例一样,如果键所对应的值包含多个句子(每个键作为一个句子列表),那么它依然可以工作,就像前面的例子一样标记器可以处理成对的句子列表。这样的话我们可以在调用map()使用该选项 batched=True ,这将显著加快标记与标记的速度。

请注意,我们现在在标记函数中省略了padding参数。这是因为在标记的时候将所有样本填充到最大长度的效率不高。一个更好的做法:在构建批处理时填充样本更好,因为这样我们只需要填充到该批处理中的最大长度,而不是整个数据集的最大长度。当输入长度变化很大时,这可以节省大量时间和处理能力!

下面是我们如何在所有数据集上同时应用标记函数。我们在调用map时使用了batch =True,这样函数就可以同时应用到数据集的多个元素上,而不是分别应用到每个元素上。这将使我们的预处理快许多。

1
2
tokenized_datasets = raw_datasets.map(tokenize_function, batched=True)
tokenized_datasets

🤗Datasets库应用这种处理的方式是向数据集添加新的字段,每个字段对应预处理函数返回的字典中的每个键:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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参数使用并行处理。我们在这里没有这样做,因为🤗标记器库已经使用多个线程来更快地标记我们的样本,但是如果您没有使用该库支持的快速标记器,使用num_proc可能会加快预处理。

我们的标记函数(tokenize_function)返回包含输入词id(input_ids)注意力遮罩(attention_mask)类型标记ID(token_type_ids) 键的字典,所以这三个字段被添加到数据集的标记的结果中。注意,如果预处理函数**map()**为现有键返回一个新值,那将会修改原有键的值。

最后当我们一起批处理元素时,将所有示例填充到最长元素的长度——我们称之为动态填充

3.动态填充

负责在批处理中将数据整理为一个batch的函数称为collate函数。它是你可以在构建DataLoader时传递的一个参数,默认是一个函数,它将把你的数据集转换为PyTorch张量,并将它们拼接起来(如果你的元素是列表、元组或字典,则会使用递归)。这在我们的这个例子中下是不可行的,因为我们的输入不是都是相同大小的。

我们故意在之后每个batch上进行填充,避免有太多填充的过长的输入。这将大大加快训练速度,但请注意,如果你在TPU上训练,这可能会导致问题——TPU喜欢固定的形状,即使这需要额外的填充。

为了解决句子长度统一的问题,我们必须定义一个collate函数,该函数会将每个batch句子填充到正确的长度。幸运的是,🤗transformer库通过DataCollatorWithPadding为我们提供了这样一个函数。当你实例化它时,需要一个标记器(用来知道使用哪个词来填充,以及模型期望填充在左边还是右边),并将做你需要的一切:

1
2
3
from transformers import DataCollatorWithPadding

data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

为了测试这个新玩具,让我们从我们的训练集中抽取几个样本。这里,我们删除列idx, sentence1sentence2,因为不需要它们,并查看一个batch中每个条目的长度:

1
2
3
4
samples = tokenized_datasets["train"][:8] # 抽取处理过的训练集的前八行
samples = {k: v for k, v in samples.items() if k not in ["idx", "sentence1", "sentence2"]}#删除列idx, sentence1和sentence2
[len(x) for x in samples["input_ids"]]
[50, 59, 47, 67, 59, 50, 62, 32]

毫无疑问,我们得到了不同长度的样本,从32到67。动态填充意味着该批中的所有样本都应该填充到长度为67这是该批中的最大长度。如果没有动态填充,所有的样本都必须填充到整个数据集中的最大长度,或者模型可以接受的最大长度。让我们再次检查data_collator是否正确地动态填充了这批样本:

1
2
3
4
5
6
7
8
```py
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])}

微调模型

Transformers提供了一个 Trainer 类来帮助您在自己的数据集上微调任何预训练模型。您只需要执行几个步骤来创建 Trainer .最难的部分可能是为**Trainer.train()**配置运行环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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) # 动态填充

1.训练

**第一步:**在我们定义我们的 Trainer 之前首先要定义一个 TrainingArguments 类,它将包含 Trainer用于训练和评估的所有超参数。您唯一必须提供的参数是保存训练模型的目录,以及训练过程中的检查点。对于其余的参数,您可以保留默认值,

1
2
3
from transformers import TrainingArguments

training_args = TrainingArguments("test-trainer")

第二步:定义模型

1
2
3
from transformers import AutoModelForSequenceClassification

model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)

这时会注意到,和第二章不一样的是,在实例化此预训练模型后会收到警告。这是因为 BERT 没有在句子对分类方面进行过预训练,所以预训练模型的头部已经被丢弃,而是添加了一个适合句子序列分类的新头部。警告表明一些权重没有使用(对应于丢弃的预训练头的那些),而其他一些权重被随机初始化(新头的那些)。

第三步:定义一个 Trainer 通过将之前构造的所有对象传递给它

包括:

  • model
  • training_args
  • 训练和验证数据集
  • data_collator
  • tokenizer
1
2
3
4
5
6
7
8
9
10
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,
)

注意,data_collator=data_collator这一行代码可写可不写,因为默认Trainer使用 的data_collator会使用之前预定义的 DataCollatorWithPadding

第四步:调用Trainer的train() 方法

1
trainer.train()

每500步报告一次训练损失。但是,它不会告诉您模型的性能如何(或质量如何)。这是因为:

  1. 我们没有通过将evaluation_strategy设置为“steps”(在每次更新参数的时候评估)或“epoch”(在每个epoch结束时评估)来告诉Trainer在训练期间进行评估。
  2. 我们没有为Trainer提供一个**compute_metrics()**函数来直接计算模型的好坏(否则评估将只输出loss,这不是一个非常直观的数字)。

2.评估

定义一个 compute_metrics() 函数并在我们下次训练时使用它。该函数必须采用 EvalPrediction 对象(带有 predictionslabel_ids 字段的参数元组)并将返回一个字符串到浮点数的字典(字符串是返回的指标的名称,而浮点数是它们的值)。

第一步:使用 Trainer.predict() 命令来使用我们的模型进行预测:

1
2
3
4
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)(正如你在之前的章节看到的情况)。

第二步:要将我们的预测的可以与真正的标签进行比较,我们需要在第二个轴上取最大值的索引

1
2
3
import numpy as np

preds = np.argmax(predictions.predictions, axis=-1) # 每行取最大值

第三步:现在建立我们的 compute_metric() 函数来较为直观地评估模型的好坏

我们将使用 🤗 Evaluate 库中的指标。我们可以像加载数据集一样轻松加载与 MRPC 数据集关联的指标,这次使用 evaluate.load() 函数。返回的对象有一个 **compute()**方法我们可以用来进行度量计算的方法:

1
2
3
4
5
6
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() 函数

1
2
3
4
5
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

1
2
3
4
5
6
7
8
9
10
11
12
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 并创建了一个新模型。如果不创建新的模型就直接训练,就只会继续训练之前我们已经训练过的模型。要启动新的训练运行,我们执行:

1
trainer.train()

这一次,它将在训练loss之外,还会输出每个 epoch 结束时的验证loss和指标。同样,由于模型的随机头部初始化,您达到的准确率/F1 分数可能与我们发现的略有不同,但它应该在同一范围内。

Trainer 将在多个 GPU 或 TPU 上开箱即用,并提供许多选项,例如混合精度训练(在训练的参数中使用 fp16 = True )。

完整的训练过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)

1.训练前的准备

在实际编写我们的训练循环之前,我们需要定义一些对象。第一个是我们将用于迭代批次的数据加载器。我们需要对我们的tokenized_datasets做一些处理,来处理Trainer自动为我们做的一些事情。具体来说,我们需要:

  • 删除与模型不期望的值相对应的列(如sentence1sentence2列)。
  • 将列名label重命名为labels(因为模型期望参数是labels)。
  • 设置数据集的格式,使其返回 PyTorch 张量而不是列表。

针对上面的每个步骤,我们的 tokenized_datasets 都有一个方法:

1
2
3
4
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

然后,我们可以检查结果中是否只有模型能够接受的列:

1
["attention_mask", "input_ids", "labels", "token_type_ids"]

至此,我们可以轻松定义数据加载器:

1
2
3
4
5
6
7
8
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
)

为了快速检验数据处理中没有错误,我们可以这样检验其中的一个批次:

1
2
3
4
5
6
7
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 从业者来说都是一个令人满意但难以实现的目标),让我们将注意力转向模型。我们完全像在上一节中所做的那样实例化它:

1
2
3
from transformers import AutoModelForSequenceClassification

model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)

为了确保训练过程中一切顺利,我们将batch传递给这个模型:

1
2
3
4
outputs = model(**batch)
print(outputs.loss, outputs.logits.shape)

tensor(0.5441, grad_fn=<NllLossBackward>) torch.Size([8, 2])

当我们提供 labels 时, 🤗 Transformers 模型都将返回这个batchloss,我们还得到了 logits(batch中的每个输入有两个,所以张量大小为 8 x 2)。

我们几乎准备好编写我们的训练循环了!我们只是缺少两件事:优化器学习率调度器。由于我们试图自行实现 Trainer的功能,我们将使用相同的优化器和学习率调度器。Trainer 使用的优化器是 AdamW , 与 Adam 相同,但在权重衰减正则化方面有所不同(参见“Decoupled Weight Decay Regularization”作者:Ilya Loshchilov 和 Frank Hutter):

1
2
3
from transformers import AdamW

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

最后,默认使用的学习率调度器只是从最大值 (5e-5) 到 0 的线性衰减。 为了定义它,我们需要知道我们训练的次数,即所有数据训练的次数(epochs)乘以的数据量(这是我们所有训练数据的数量)。Trainer默认情况下使用三个epochs,因此我们定义训练过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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

2.训练循环

最后一件事:如果我们可以访问 GPU,我们将希望使用 GPU(在 CPU 上,训练可能需要几个小时而不是几分钟)。为此,我们定义了一个 device,它在GPU可用的情况下指向GPU 我们将把我们的模型和batche放在device上:

1
2
3
4
5
6
7
import torch

device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
model.to(device)
device

device(type='cuda')

我们现在准备好训练了!为了了解训练何时结束,我们使用 tqdm 库,在训练步骤数上添加了一个进度条:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)

您可以看到训练循环的核心与介绍中的非常相似。我们没有要求任何检验,所以这个训练循环不会告诉我们任何关于模型目前的状态。我们需要为此添加一个评估循环。

3.评估循环

正如我们之前所做的那样,我们将使用 🤗 Evaluate 库提供的指标。我们已经了解了 metric.compute() 方法,当我们使用 add_batch()方法进行预测循环时,实际上该指标可以为我们累积所有 batch 的结果。一旦我们累积了所有 batch ,我们就可以使用 metric.compute() 得到最终结果 .以下是在评估循环中实现所有这些的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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}

同样,由于模型头部初始化和数据改组的随机性,您的结果会略有不同,但它们应该在同一个范围内。

4.使用🤗 Accelerate加速您的训练循环

我们之前定义的训练循环在单个 CPU 或 GPU 上运行良好。但是使用🤗 Accelerate库,只需进行一些调整,我们就可以在多个 GPU 或 TPU 上启用分布式训练。从创建训练和验证数据加载器开始,我们的手动训练循环如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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)

以下是变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
+ 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对象 ,它将查看环境并初始化适当的分布式设置。 🤗 Accelerate 为您处理数据在设备间的传递,因此您可以删除将模型放在设备上的那行代码(或者,如果您愿意,可使用 accelerator.device 代替 device )。

然后大部分工作会在将数据加载器、模型和优化器发送到的accelerator.prepare()中完成。这将会把这些对象包装在适当的容器中,以确保您的分布式训练按预期工作。要进行的其余更改是删除将batch放在 device 的那行代码(同样,如果您想保留它,您可以将其更改为使用 accelerator.device ) 并将 loss.backward() 替换为accelerator.backward(loss)

⚠️ 为了使云端 TPU 提供的加速发挥最大的效益,我们建议使用标记器(tokenizer)的 padding=max_lengthmax_length 参数将您的样本填充到固定长度。

如果您想复制并粘贴来直接运行,以下是 🤗 Accelerate 的完整训练循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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 文件中,可以让它在任何类型的分布式设置上运行。要在分布式设置中试用它,请运行以下命令:

1
accelerate config

这将询问您几个配置的问题并将您的回答转储到此命令使用的配置文件中:

1
accelerate launch train.py

这将启动分布式训练

这将启动分布式训练。如果您想在 Notebook 中尝试此操作(例如,在 Colab 上使用 TPU 进行测试),只需将代码粘贴到 training_function() 并使用以下命令运行最后一个单元格:

1
2
3
from accelerate import notebook_launcher

notebook_launcher(training_function)

您可以在🤗 Accelerate repo找到更多的示例。

5.采用Accelerate的完全流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
from datasets import load_dataset
from transformers import AutoTokenizer, DataCollatorWithPadding
from transformers import AdamW, AutoModelForSequenceClassification, get_scheduler
import evaluate
# 1.训练前准备
raw_datasets = load_dataset("glue", "mrpc") # 使用MRPC数据集中的[GLUE 基准测试数据集]
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)

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)

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