前言

现在我们将看到如何在不使用Trainer类的情况下,获得与上一部分相同的结果。再次提醒,我们假设你已经完成了第二部分的数据处理。下面是一个涵盖你所需要的一切的简短总结:

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)

src link: https://huggingface.co/learn/nlp-course/chapter3/4

Operating System: Ubuntu 22.04.4 LTS

参考文档

  1. NLP Course - A full training

准备训练

在实际编写我们的训练循环之前,我们需要定义一些对象。首先是我们将用来迭代批次的dataloader。但在定义这些dataloader之前,我们需要对我们的tokenized_datasets进行一些后处理,以处理Trainer自动为我们完成的一些事情。具体来说,我们需要:

  • 删除模型不期望的列(如sentence1和sentence2列)。
  • 将列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"]

现在这已经完成,我们可以轻松定义我们的dataloader:

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
for batch in train_dataloader:
break
{k: v.shape for k, v in batch.items()}
1
2
3
4
{'attention_mask': torch.Size([8, 65]),
'input_ids': torch.Size([8, 65]),
'labels': torch.Size([8]),
'token_type_ids': torch.Size([8, 65])}

注意,实际的形状可能对你来说略有不同,因为我们为训练dataloader设置了shuffle=True,并且我们在批次内进行了填充以适应最大长度。

现在我们已经完成了数据预处理(这是任何机器学习实践者都希望达到但往往难以实现的目标),让我们来看看模型。我们将其实例化的方式与上一部分完全相同:

1
2
3
from transformers import AutoModelForSequenceClassification

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

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

1
2
outputs = model(**batch)
print(outputs.loss, outputs.logits.shape)
1
tensor(0.5441, grad_fn=<NllLossBackward>) torch.Size([8, 2])

所有🤗 Transformers模型在提供标签时都会返回损失,同时我们还会得到logits(对于我们批次中的每个输入都有两个,所以是一个尺寸为8 x 2的张量)。

我们几乎准备好编写训练循环了!我们还缺两样东西:一个优化器和一个学习率调度器。由于我们试图手动复制Trainer所做的操作,我们将使用相同的默认设置。Trainer使用的优化器是AdamW,它与Adam相同,但对于权重衰减正则化有一个小变化(参见Ilya Loshchilov和Frank Hutter的“Decoupled Weight Decay Regularization”):

1
2
3
from transformers import AdamW

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

最后,默认使用的学习率调度器只是从最大值(5e-5)到0的线性衰减。为了正确定义它,我们需要知道我们将采取的训练步数,这是我们想要运行的epoch数乘以训练批次数(即我们训练dataloader的长度)。Trainer默认使用三个epoch,所以我们将遵循这个设置:

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

训练循环

最后一件事:如果我们能够访问GPU,我们会想要使用它(在CPU上,训练可能需要几个小时,而不是几分钟)。为了做到这一点,我们定义了一个设备,我们将把我们的模型和批次放在上面:

1
2
3
4
5
import torch

device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
model.to(device)
device
1
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)

你可以看到,训练循环的核心部分与简介中的非常相似。我们没有要求任何报告,所以这个训练循环不会告诉我们模型的表现如何。我们需要添加一个评估循环来实现这一点。

评估循环

就像我们之前所做的,我们将使用🤗 Evaluate库提供的指标。我们已经看到了metric.compute()方法,但实际上指标可以在我们遍历预测循环时使用add_batch()方法为我们累积批次。一旦我们累积了所有批次,我们就可以使用metric.compute()得到最终结果。以下是如何在评估循环中实现所有这些:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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()
1
{'accuracy': 0.8431372549019608, 'f1': 0.8907849829351535}

再次强调,由于模型头初始化和数据洗牌中的随机性,你的结果可能会有所不同,但它们应该在大致相同的范围内。

使用 🤗 Accelerate 超级加速你的训练循环

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

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 对象,它将检查环境并初始化适当的分布式设置。🤗 Accelerate 为你处理设备放置,所以你可以删除将模型放在设备上的行(或者,如果你愿意,可以将它们改为使用 accelerator.device 而不是 device)。

然后,主要的工作是在将dataloader、模型和优化器发送到 accelerator.prepare() 的行中完成的。这将把这些对象包装在适当的容器中,以确保你的分布式训练按预期工作。剩下的更改是删除将批次放在设备上的行(再次强调,如果你想要保留这个操作,你可以只是将其改为使用 accelerator.device),并将 loss.backward() 替换为 accelerator.backward(loss)。

⚠️ 为了利用 Cloud TPU 提供的速度提升,我们建议使用 tokenizer 的 padding=“max_length” 和 max_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

这将启动分布式训练。

如果你想在笔记本中尝试这个(例如,在Colab上测试TPU),只需将代码粘贴到一个 training_function() 中,并在最后一个单元格中运行:

1
2
3
from accelerate import notebook_launcher

notebook_launcher(training_function)

你可以在 🤗 Accelerate 仓库中找到更多示例。

结语

第二百一十八篇博文写完,开心!!!!

今天,也是充满希望的一天。