在第 2 章中,我们探讨了如何使用分词器和预训练模型进行预测。但是,如果您想为自己的数据集微调预训练模型怎么办?这就是本章的主题!您将学习:
用前面章节的例子,我们会用以下代码来训练一个文本分类模型
当然,仅仅在两个句子上训练模型不会产生很好的结果。为了获得更好的结果,您需要准备更大的数据集。
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()
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
})
})
我们可以raw_datasets通过索引来访问对象中的每对句子,就像使用字典一样:
>>>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 .'}
我们可以看到标签已经是整数,所以我们不必在那里做任何预处理。要知道哪个整数对应哪个标签,我们可以检查features我们的raw_train_dataset. 这将告诉我们每一列的类型:
>>>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)}
为了预处理数据集,我们需要将文本转换为模型可以理解的数字。正如您在前一章中看到的,这是通过分词器完成的。我们可以给分词器提供一个句子或一个句子列表,所以我们可以像这样直接分词每对的所有第一个句子和所有第二个句子:
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]
}
这里’token_type_ids’代表的是这是第几个句子的单词
如果我们将里面的 ID 解码input_ids回单词:
>>>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]all对应的输入部分的标记类型 ID 为0,而对应于 的其他部分sentence2 [SEP]的标记类型 ID 均为1。
为了将数据保留为数据集,我们将使用该Dataset.map()方法。如果我们需要完成更多的预处理而不仅仅是标记化,这也为我们提供了一些额外的灵活性。该map()方法通过在数据集的每个元素上应用一个函数来工作,所以让我们定义一个标记我们输入的函数:
def tokenize_function(example):
return tokenizer(example["sentence1"], example["sentence2"], truncation=True)
tokenized_datasets = raw_datasets.map(tokenize_function, batched=True)
tokenized_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
})
})
定义我们之前的第一步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)
Once we have our model, we can define a Trainer by passing it all the objects constructed up to now — the model, the training_args, the training and validation datasets, our data_collator, and our 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,
)
为了在我们的数据集上微调模型,我们只需要调用train()我们的方法Trainer:
trainer.train()
1.这将开始微调(在 GPU 上应该需要几分钟)并每 500 步报告一次训练损失。但是,它不会告诉您模型的性能如何(或差)。这是因为:
2.我们没有Trainer通过设置evaluation_strategy为"steps"(evaluate every eval_steps) 或"epoch"(evaluate at each epoch)来告诉训练期间进行评估。
在上述评估期间,我们没有提供计算指标Trainer的compute_metrics()函数(否则评估只会打印损失,这不是一个非常直观的数字)。
让我们看看如何构建一个有用的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,)
我们要评估预测的准确性需要每一行上最大值的索引
import numpy as np
preds = np.argmax(predictions.predictions, axis=-1)
然后就可以愉快的评估训练结果了
from datasets import load_metric
metric = load_metric("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 = load_metric("glue", "mrpc")
logits, labels = eval_preds
predictions = np.argmax(logits, axis=-1)
return metric.compute(predictions=predictions, references=labels)
并查看它在每个时期结束时用于报告指标的实际用途,以下是我们如何Trainer使用此compute_metrics()函数定义新的:
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,
)
接下来我们来进行一次完整的训练
在实际编写我们的训练循环之前,我们需要定义一些对象。第一个是我们将用于迭代批次的数据加载器。但是在我们定义这些数据加载器之前,我们需要对我们的 应用一些后处理tokenized_datasets,以处理一些Trainer自动为我们做的事情。具体来说,我们需要:
我们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
We can then check that the result only has columns that our model will accept:
["attention_mask", "input_ids", "labels", "token_type_ids"]
我们可以轻松定义我们的dataloaders:
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
)
为了快速检查数据处理中没有错误,我们可以这样检查一个batch:
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])}
现在我们已经完全完成了数据预处理,开始载入模型
from transformers import AutoModelForSequenceClassification
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)
为了确保训练过程中一切顺利,我们将批次传递给这个模型:
outputs = model(**batch)
print(outputs.loss, outputs.logits.shape)
输出:
tensor(0.5441, grad_fn=<NllLossBackward>) torch.Size([8, 2])
我们几乎准备好编写我们的训练循环了!我们只是缺少两件事:优化器和学习率调度器。由于我们正在尝试Trainer手动复制正在执行的操作,因此我们将使用相同的默认值。Traineris使用的优化器AdamW与 Adam 相同,但对权重衰减正则化有所不同 (see “Decoupled Weight Decay Regularization” by Ilya Loshchilov and Frank Hutter):
from transformers import AdamW
optimizer = AdamW(model.parameters(), lr=5e-5)
最后,默认使用的学习率调度器只是从最大值 (5e-5) 到 0 的线性衰减。 为了正确定义它,我们需要知道我们将采取的训练步骤数,即 epochs 数我们希望运行乘以训练批次的数量(这是我们训练数据加载器的长度)。在Trainer使用了三个时代的默认,因此我们将遵循:
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)
把我们的模型移动到GPU上更快速的跑起来:
>>>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)
您可以看到训练循环的核心与介绍中的核心非常相似。我们没有要求任何报告,所以这个训练循环不会告诉我们任何关于模型票价的信息。我们需要为此添加一个评估循环。
正如我们之前所做的那样,我们将使用 Datasets 库提供的指标。我们已经看到了该metric.compute()方法,但是当我们使用该方法进行预测循环时,指标实际上可以为我们累积批次add_batch()。一旦我们累积了所有批次,我们就可以得到最终结果metric.compute()。以下是在评估循环中实现所有这些的方法:
from datasets import load_metric
metric = load_metric("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}
鸽了,因为我没有多卡