调试训练管道
您已经编写了一个漂亮的脚本,用于在给定任务上训练或微调模型,并遵循了来自 第七章 的建议。 但是,当您启动命令 trainer.train()
时,发生了可怕的事情:您遇到了错误 😱! 或者更糟糕的是,一切似乎都很好,训练运行没有错误,但结果模型很糟糕。 在本节中,我们将向您展示如何调试这类问题。
调试训练管道
当您在 trainer.train()
中遇到错误时,问题在于它可能来自多个来源,因为 Trainer
通常会将很多东西整合在一起。 它将数据集转换为数据加载器,因此问题可能是您的数据集中存在问题,或者在尝试将数据集的元素一起批处理时出现了一些问题。 然后它获取一批数据并将其馈送到模型,因此问题可能出在模型代码中。 之后,它计算梯度并执行优化步骤,因此问题也可能出在您的优化器中。 即使训练一切顺利,如果您的指标存在问题,在评估期间仍然可能出现问题。
调试 trainer.train()
中出现的错误的最佳方法是手动遍历整个管道,以查看问题出在哪里。 然后,错误通常很容易解决。
为了演示这一点,我们将使用以下脚本(尝试)在 MNLI 数据集 上微调 DistilBERT 模型
from datasets import load_dataset
import evaluate
from transformers import (
AutoTokenizer,
AutoModelForSequenceClassification,
TrainingArguments,
Trainer,
)
raw_datasets = load_dataset("glue", "mnli")
model_checkpoint = "distilbert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)
def preprocess_function(examples):
return tokenizer(examples["premise"], examples["hypothesis"], truncation=True)
tokenized_datasets = raw_datasets.map(preprocess_function, batched=True)
model = AutoModelForSequenceClassification.from_pretrained(model_checkpoint)
args = TrainingArguments(
f"distilbert-finetuned-mnli",
evaluation_strategy="epoch",
save_strategy="epoch",
learning_rate=2e-5,
num_train_epochs=3,
weight_decay=0.01,
)
metric = evaluate.load("glue", "mnli")
def compute_metrics(eval_pred):
predictions, labels = eval_pred
return metric.compute(predictions=predictions, references=labels)
trainer = Trainer(
model,
args,
train_dataset=raw_datasets["train"],
eval_dataset=raw_datasets["validation_matched"],
compute_metrics=compute_metrics,
)
trainer.train()
如果您尝试执行它,您将遇到一个相当神秘的错误
'ValueError: You have to specify either input_ids or inputs_embeds'
检查您的数据
这不用说,但如果您的数据已损坏,Trainer
将无法形成批次,更不用说训练您的模型了。 因此,首先,您需要查看训练集中包含的内容。
为了避免花费无数小时试图修复不是错误来源的东西,我们建议您使用 trainer.train_dataset
进行检查,而不是使用其他任何东西。 因此,让我们在这里执行此操作
trainer.train_dataset[0]
{'hypothesis': 'Product and geography are what make cream skimming work. ',
'idx': 0,
'label': 1,
'premise': 'Conceptually cream skimming has two basic dimensions - product and geography.'}
您注意到有什么问题吗? 这与关于缺少 input_ids
的错误消息相结合,应该让您意识到这些是文本,而不是模型可以理解的数字。 在这里,原始错误非常具有误导性,因为 Trainer
会自动删除与模型签名不匹配的列(即模型期望的参数)。 这意味着在这里,除了标签以外的所有内容都被丢弃了。 因此,在创建批次并将其发送到模型时没有问题,而模型又抱怨它没有收到正确的输入。
为什么数据没有被处理? 我们确实在数据集上使用了 Dataset.map()
方法,以便在每个样本上应用分词器。 但是,如果您仔细查看代码,您会发现我们在将训练集和评估集传递给 Trainer
时犯了一个错误。 在这里,我们使用的是 raw_datasets
🤦,而不是使用 tokenized_datasets
。 因此,让我们修复它!
from datasets import load_dataset
import evaluate
from transformers import (
AutoTokenizer,
AutoModelForSequenceClassification,
TrainingArguments,
Trainer,
)
raw_datasets = load_dataset("glue", "mnli")
model_checkpoint = "distilbert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)
def preprocess_function(examples):
return tokenizer(examples["premise"], examples["hypothesis"], truncation=True)
tokenized_datasets = raw_datasets.map(preprocess_function, batched=True)
model = AutoModelForSequenceClassification.from_pretrained(model_checkpoint)
args = TrainingArguments(
f"distilbert-finetuned-mnli",
evaluation_strategy="epoch",
save_strategy="epoch",
learning_rate=2e-5,
num_train_epochs=3,
weight_decay=0.01,
)
metric = evaluate.load("glue", "mnli")
def compute_metrics(eval_pred):
predictions, labels = eval_pred
return metric.compute(predictions=predictions, references=labels)
trainer = Trainer(
model,
args,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["validation_matched"],
compute_metrics=compute_metrics,
)
trainer.train()
这段新代码现在将产生一个不同的错误(进展!)
'ValueError: expected sequence of length 43 at dim 1 (got 37)'
查看回溯,我们可以看到错误发生在数据整理步骤中
~/git/transformers/src/transformers/data/data_collator.py in torch_default_data_collator(features)
105 batch[k] = torch.stack([f[k] for f in features])
106 else:
--> 107 batch[k] = torch.tensor([f[k] for f in features])
108
109 return batch
因此,我们应该转移到那里。 但是,在我们这样做之前,让我们完成对数据的检查,以确保它 100% 正确。
在调试训练会话时,您始终应该做的一件事是查看模型的解码输入。 我们无法直接理解我们馈送给它的数字,因此我们应该查看这些数字代表什么。 例如,在计算机视觉中,这意味着查看传递的像素的解码图片,在语音中,这意味着收听解码的音频样本,而对于我们这里提供的 NLP 示例,则意味着使用我们的分词器来解码输入
tokenizer.decode(trainer.train_dataset[0]["input_ids"])
'[CLS] conceptually cream skimming has two basic dimensions - product and geography. [SEP] product and geography are what make cream skimming work. [SEP]'
因此,这似乎是正确的。 您应该对输入中的所有键执行此操作
trainer.train_dataset[0].keys()
dict_keys(['attention_mask', 'hypothesis', 'idx', 'input_ids', 'label', 'premise'])
请注意,与模型接受的输入不对应的键将被自动丢弃,因此在这里我们将只保留 input_ids
、attention_mask
和 label
(它将被重命名为 labels
)。 要仔细检查模型签名,您可以打印模型的类,然后检查其文档
type(trainer.model)
transformers.models.distilbert.modeling_distilbert.DistilBertForSequenceClassification
因此,在我们的示例中,我们可以检查 此页面 上接受的参数。 Trainer
还将记录它丢弃的列。
我们已经通过解码它们来检查输入 ID 是否正确。 下一个是 attention_mask
trainer.train_dataset[0]["attention_mask"]
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
由于我们在预处理中没有应用填充,因此这似乎是完全自然的。 为了确保注意力掩码没有问题,让我们检查它是否与我们的输入 ID 的长度相同
len(trainer.train_dataset[0]["attention_mask"]) == len(
trainer.train_dataset[0]["input_ids"]
)
True
很好! 最后,让我们检查我们的标签
trainer.train_dataset[0]["label"]
1
与输入 ID 一样,这只是一个本身没有意义的数字。 正如我们之前所见,整数和标签名称之间的映射存储在数据集相应特征的 names
属性中
trainer.train_dataset.features["label"].names
['entailment', 'neutral', 'contradiction']
因此 1
表示 neutral
,这意味着我们上面看到的两个句子没有矛盾,第一个句子也不意味着第二个句子。 这似乎是正确的!
我们这里没有令牌类型 ID,因为 DistilBERT 不需要它们; 如果您的模型中有一些,您还应该确保它们正确匹配第一个和第二个句子在输入中的位置。
✏️ 轮到你了! 检查训练数据集的第二个元素是否一切正常。
我们这里只对训练集进行了检查,但您当然应该以相同的方式仔细检查验证集和测试集。
既然我们知道我们的数据集看起来不错,那么是时候检查训练管道的下一步了。
从数据集到数据加载器
训练管道中可能出错的下一个地方是当Trainer
尝试从训练集或验证集中形成批次时。一旦确定Trainer
的数据集是正确的,就可以尝试通过执行以下代码手动形成一个批次(将train
替换为eval
以用于验证数据加载器)
for batch in trainer.get_train_dataloader():
break
此代码创建训练数据加载器,然后遍历它,在第一次迭代时停止。如果代码执行没有错误,则拥有可以检查的第一个训练批次;如果代码出错,则可以确定问题出在数据加载器中,就像这里的情况一样
~/git/transformers/src/transformers/data/data_collator.py in torch_default_data_collator(features)
105 batch[k] = torch.stack([f[k] for f in features])
106 else:
--> 107 batch[k] = torch.tensor([f[k] for f in features])
108
109 return batch
ValueError: expected sequence of length 45 at dim 1 (got 76)
检查回溯的最后一帧应该足以提供线索,但让我们进一步深入。在批次创建过程中,大多数问题都是由于将示例合并到单个批次中而引起的,因此,如果有疑问,首先要检查的是DataLoader
正在使用的collate_fn
data_collator = trainer.get_train_dataloader().collate_fn data_collator
<function transformers.data.data_collator.default_data_collator(features: List[InputDataClass], return_tensors='pt') -> Dict[str, Any]>
所以这是default_data_collator
,但这并不是我们想要的情况。我们需要将示例填充到批次中最长的句子,这是通过DataCollatorWithPadding
合并器完成的。此数据合并器应该由Trainer
默认使用,那么为什么它没有在这里使用呢?
答案是我们没有将tokenizer
传递给Trainer
,因此它无法创建我们想要的DataCollatorWithPadding
。实际上,不应该犹豫明确传递想要使用的数据合并器,以确保避免此类错误。让我们修改代码以完全这样做
from datasets import load_dataset
import evaluate
from transformers import (
AutoTokenizer,
AutoModelForSequenceClassification,
DataCollatorWithPadding,
TrainingArguments,
Trainer,
)
raw_datasets = load_dataset("glue", "mnli")
model_checkpoint = "distilbert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)
def preprocess_function(examples):
return tokenizer(examples["premise"], examples["hypothesis"], truncation=True)
tokenized_datasets = raw_datasets.map(preprocess_function, batched=True)
model = AutoModelForSequenceClassification.from_pretrained(model_checkpoint)
args = TrainingArguments(
f"distilbert-finetuned-mnli",
evaluation_strategy="epoch",
save_strategy="epoch",
learning_rate=2e-5,
num_train_epochs=3,
weight_decay=0.01,
)
metric = evaluate.load("glue", "mnli")
def compute_metrics(eval_pred):
predictions, labels = eval_pred
return metric.compute(predictions=predictions, references=labels)
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
trainer = Trainer(
model,
args,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["validation_matched"],
compute_metrics=compute_metrics,
data_collator=data_collator,
tokenizer=tokenizer,
)
trainer.train()
好消息是,我们不再遇到之前的错误,这绝对是进步。坏消息是,我们反而遇到了臭名昭著的 CUDA 错误
RuntimeError: CUDA error: CUBLAS_STATUS_ALLOC_FAILED when calling `cublasCreate(handle)`
这很糟糕,因为 CUDA 错误通常很难调试。我们将在下一分钟看到如何解决这个问题,但首先让我们完成对批次创建的分析。
如果确定数据合并器是正确的,则应该尝试将其应用于数据集的几个样本
data_collator = trainer.get_train_dataloader().collate_fn
batch = data_collator([trainer.train_dataset[i] for i in range(4)])
此代码将失败,因为train_dataset
包含字符串列,Trainer
通常会删除这些列。可以手动删除它们,或者如果想要完全复制Trainer
在幕后所做的事情,可以调用私有Trainer._remove_unused_columns()
方法来执行此操作
data_collator = trainer.get_train_dataloader().collate_fn
actual_train_set = trainer._remove_unused_columns(trainer.train_dataset)
batch = data_collator([actual_train_set[i] for i in range(4)])
如果错误仍然存在,则应该能够手动调试数据合并器内部发生的情况。
既然我们已经调试了批次创建过程,现在该将一个批次传递给模型了!
通过模型
应该能够通过执行以下命令获取一个批次
for batch in trainer.get_train_dataloader():
break
如果在笔记本中运行此代码,可能会遇到与之前类似的 CUDA 错误,在这种情况下,需要重新启动笔记本并重新执行最后一段代码,但没有trainer.train()
行。这是 CUDA 错误第二令人讨厌的地方:它们会不可挽回地破坏内核。最令人讨厌的是它们很难调试。
为什么?这与 GPU 的工作方式有关。它们在并行执行大量操作方面非常高效,但缺点是,当其中一条指令导致错误时,无法立即知道。只有当程序调用对 GPU 上多个进程的同步时,它才会意识到出现问题,因此错误实际上是在与创建错误无关的地方引发的。例如,如果查看之前的回溯,错误是在反向传播过程中引发的,但我们将在下一分钟看到,它实际上源于正向传播中的某些内容。
那么如何调试这些错误呢?答案很简单:不调试。除非 CUDA 错误是内存不足错误(这意味着 GPU 中没有足够的内存),否则应该始终回到 CPU 进行调试。
为了在我们的案例中做到这一点,我们只需要将模型放回 CPU 并对我们的批次进行调用——由DataLoader
返回的批次尚未移动到 GPU
outputs = trainer.model.cpu()(**batch)
~/.pyenv/versions/3.7.9/envs/base/lib/python3.7/site-packages/torch/nn/functional.py in nll_loss(input, target, weight, size_average, ignore_index, reduce, reduction)
2386 )
2387 if dim == 2:
-> 2388 ret = torch._C._nn.nll_loss(input, target, weight, _Reduction.get_enum(reduction), ignore_index)
2389 elif dim == 4:
2390 ret = torch._C._nn.nll_loss2d(input, target, weight, _Reduction.get_enum(reduction), ignore_index)
IndexError: Target 2 is out of bounds.
所以,情况越来越清楚。现在,我们不是遇到 CUDA 错误,而是遇到损失计算中的IndexError
(因此与前面提到的反向传播无关)。更确切地说,我们可以看到是目标 2 导致了错误,因此现在是检查模型标签数量的好时机
trainer.model.config.num_labels
2
使用两个标签,只允许 0 和 1 作为目标,但根据收到的错误消息,却得到了一个 2。得到 2 实际上很正常:如果还记得之前提取的标签名称,有三个,所以数据集中有索引 0、1 和 2。问题是我们没有将此告诉模型,模型应该使用三个标签创建。所以让我们修正一下!
from datasets import load_dataset
import evaluate
from transformers import (
AutoTokenizer,
AutoModelForSequenceClassification,
DataCollatorWithPadding,
TrainingArguments,
Trainer,
)
raw_datasets = load_dataset("glue", "mnli")
model_checkpoint = "distilbert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)
def preprocess_function(examples):
return tokenizer(examples["premise"], examples["hypothesis"], truncation=True)
tokenized_datasets = raw_datasets.map(preprocess_function, batched=True)
model = AutoModelForSequenceClassification.from_pretrained(model_checkpoint, num_labels=3)
args = TrainingArguments(
f"distilbert-finetuned-mnli",
evaluation_strategy="epoch",
save_strategy="epoch",
learning_rate=2e-5,
num_train_epochs=3,
weight_decay=0.01,
)
metric = evaluate.load("glue", "mnli")
def compute_metrics(eval_pred):
predictions, labels = eval_pred
return metric.compute(predictions=predictions, references=labels)
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
trainer = Trainer(
model,
args,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["validation_matched"],
compute_metrics=compute_metrics,
data_collator=data_collator,
tokenizer=tokenizer,
)
我们还没有包括trainer.train()
行,以便花时间检查一切看起来是否正常。如果请求一个批次并将其传递给模型,现在它就可以正常运行,没有任何错误!
for batch in trainer.get_train_dataloader():
break
outputs = trainer.model.cpu()(**batch)
下一步是回到 GPU,并检查一切是否仍然正常运行
import torch
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
batch = {k: v.to(device) for k, v in batch.items()}
outputs = trainer.model.to(device)(**batch)
如果仍然遇到错误,请确保重新启动笔记本,只执行脚本的最新版本。
执行一步优化
现在我们知道可以构建实际通过模型的批次,我们已经准备好进行训练管道的下一步:计算梯度并执行一步优化。
第一步只是对损失调用backward()
方法
loss = outputs.loss loss.backward()
在这个阶段遇到错误的情况很少见,但如果遇到错误,请确保回到 CPU 以获取有用的错误消息。
要执行优化步骤,只需要创建optimizer
并调用它的step()
方法
trainer.create_optimizer() trainer.optimizer.step()
同样,如果在Trainer
中使用默认优化器,则在这个阶段不应该遇到错误,但如果具有自定义优化器,则这里可能存在一些需要调试的问题。不要忘记,如果在这个阶段遇到奇怪的 CUDA 错误,请回到 CPU。
处理 CUDA 内存不足错误
每当遇到以RuntimeError: CUDA out of memory
开头的错误消息时,就表示 GPU 内存不足。这与代码本身无关,它可能会出现在完美运行的脚本中。此错误表示试图在 GPU 的内部内存中放入太多内容,导致出现错误。与其他 CUDA 错误一样,需要重新启动内核才能回到可以再次运行训练的位置。
要解决这个问题,只需要减少 GPU 空间的使用量——这通常说起来容易做起来难。首先,确保没有同时在 GPU 上使用两个模型(当然,除非问题需要这样做)。然后,可能需要缩小批次大小,因为它会直接影响模型所有中间输出及其梯度的大小。如果问题仍然存在,请考虑使用模型的较小版本。
在课程的下一部分,我们将介绍更多高级技术,这些技术可以帮助您减少内存占用并让您微调更大的模型。
评估模型
现在我们已经解决了代码中的所有问题,一切都很完美,训练应该顺利运行,对吧?没有那么快!如果运行trainer.train()
命令,最初一切都会看起来很好,但过一段时间后,会遇到以下情况
# This will take a long time and error out, so you shouldn't run this cell
trainer.train()
TypeError: only size-1 arrays can be converted to Python scalars
会发现此错误出现在评估阶段,所以这是最后需要调试的地方。
可以独立于训练运行Trainer
的评估循环,如下所示
trainer.evaluate()
TypeError: only size-1 arrays can be converted to Python scalars
💡 应该始终确保可以在启动trainer.train()
之前运行trainer.evaluate()
,以避免在遇到错误之前浪费大量的计算资源。
在尝试调试评估循环中的问题之前,您应该首先确保您已经查看了数据,能够正确地形成一个批次,并且可以在其上运行您的模型。我们已经完成了所有这些步骤,因此以下代码可以执行而不会出错
for batch in trainer.get_eval_dataloader():
break
batch = {k: v.to(device) for k, v in batch.items()}
with torch.no_grad():
outputs = trainer.model(**batch)
错误出现在评估阶段的最后,如果我们查看回溯,我们会看到这个
~/git/datasets/src/datasets/metric.py in add_batch(self, predictions, references)
431 """
432 batch = {"predictions": predictions, "references": references}
--> 433 batch = self.info.features.encode_batch(batch)
434 if self.writer is None:
435 self._init_writer()
这告诉我们错误起源于datasets/metric.py
模块——所以这是一个与我们的compute_metrics()
函数有关的问题。它接受一个包含 logits 和标签的元组作为 NumPy 数组,所以让我们尝试向它提供这些数据
predictions = outputs.logits.cpu().numpy()
labels = batch["labels"].cpu().numpy()
compute_metrics((predictions, labels))
TypeError: only size-1 arrays can be converted to Python scalars
我们得到了相同的错误,所以问题肯定在于那个函数。如果我们回顾它的代码,我们会看到它只是将predictions
和labels
转发给metric.compute()
。那么,那个方法有问题吗?其实没有。让我们快速看一下形状
predictions.shape, labels.shape
((8, 3), (8,))
我们的预测仍然是 logits,而不是实际的预测,这就是为什么指标会返回这个(有点模糊)错误。修复很简单;我们只需要在compute_metrics()
函数中添加一个 argmax
import numpy as np
def compute_metrics(eval_pred):
predictions, labels = eval_pred
predictions = np.argmax(predictions, axis=1)
return metric.compute(predictions=predictions, references=labels)
compute_metrics((predictions, labels))
{'accuracy': 0.625}
现在我们的错误修复了!这是最后一个错误,所以我们的脚本现在将正确地训练模型。
作为参考,以下是被完全修复的脚本
import numpy as np
from datasets import load_dataset
import evaluate
from transformers import (
AutoTokenizer,
AutoModelForSequenceClassification,
DataCollatorWithPadding,
TrainingArguments,
Trainer,
)
raw_datasets = load_dataset("glue", "mnli")
model_checkpoint = "distilbert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)
def preprocess_function(examples):
return tokenizer(examples["premise"], examples["hypothesis"], truncation=True)
tokenized_datasets = raw_datasets.map(preprocess_function, batched=True)
model = AutoModelForSequenceClassification.from_pretrained(model_checkpoint, num_labels=3)
args = TrainingArguments(
f"distilbert-finetuned-mnli",
evaluation_strategy="epoch",
save_strategy="epoch",
learning_rate=2e-5,
num_train_epochs=3,
weight_decay=0.01,
)
metric = evaluate.load("glue", "mnli")
def compute_metrics(eval_pred):
predictions, labels = eval_pred
predictions = np.argmax(predictions, axis=1)
return metric.compute(predictions=predictions, references=labels)
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
trainer = Trainer(
model,
args,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["validation_matched"],
compute_metrics=compute_metrics,
data_collator=data_collator,
tokenizer=tokenizer,
)
trainer.train()
在这种情况下,没有更多的问题,我们的脚本将微调一个应该给出合理结果的模型。但是,当训练在没有任何错误的情况下进行,并且训练的模型表现得非常糟糕时,我们该怎么办?这是机器学习中最难的部分,我们将向您展示一些可以帮助您的技术。
💡 如果您使用的是手动训练循环,则相同的步骤适用于调试您的训练管道,但更容易将它们分开。但是,请确保您没有在错误的位置忘记了model.eval()
或model.train()
,或者每个步骤的zero_grad()
!
调试训练过程中的静默错误
我们如何调试一个没有错误但没有获得良好结果的训练?我们将在这里为您提供一些提示,但请注意,这种类型的调试是机器学习中最难的部分,没有神奇的答案。
再次检查您的数据!
您的模型只有在能够从您的数据中学习到某些东西时才能学到东西。如果有错误破坏了数据或标签是随机分配的,那么您很可能不会得到任何在您的数据集上训练的模型。因此,总是从仔细检查您的解码输入和标签开始,并问问自己以下问题
- 解码后的数据是否可以理解?
- 您同意标签吗?
- 是否存在一个比其他标签更常见的标签?
- 如果模型预测一个随机答案/总是同一个答案,那么损失/指标应该是什么?
⚠️ 如果您正在进行分布式训练,请在每个进程中打印数据集的样本,并仔细检查您是否得到相同的结果。一个常见的错误是,在数据创建中存在一些随机性,使得每个进程拥有数据集的不同版本。
在查看完您的数据之后,再查看模型的一些预测结果并对其进行解码。如果模型总是预测相同的结果,可能是因为您的数据集偏向于一个类别(对于分类问题);诸如过采样稀有类别之类的技术可能会有所帮助。
如果在初始模型上得到的损失/指标与您对随机预测的期望损失/指标有很大不同,请仔细检查您的损失或指标的计算方式,因为那里可能存在错误。如果您使用的是几个最后要加起来的损失,请确保它们具有相同的尺度。
当您确定您的数据是完美的时,您可以通过一个简单的测试来查看模型是否能够在您的数据上进行训练。
在单个批次上过拟合您的模型
过拟合通常是我们训练时想要避免的东西,因为它意味着模型没有学会识别我们想要它识别的通用特征,而是仅仅记住了训练样本。但是,尝试反复在单个批次上训练您的模型是一个很好的测试,可以检查您所构建的问题是否可以通过您试图训练的模型解决。它还有助于您查看初始学习率是否过高。
在您定义了Trainer
之后,这样做非常容易;只需获取一批训练数据,然后运行一个小的手动训练循环,只使用该批次进行大约 20 步训练
for batch in trainer.get_train_dataloader():
break
batch = {k: v.to(device) for k, v in batch.items()}
trainer.create_optimizer()
for _ in range(20):
outputs = trainer.model(**batch)
loss = outputs.loss
loss.backward()
trainer.optimizer.step()
trainer.optimizer.zero_grad()
💡 如果您的训练数据不平衡,请确保构建一批包含所有标签的训练数据。
生成的模型应该对同一个batch
具有接近完美的训练结果。让我们计算一下结果预测的指标
with torch.no_grad():
outputs = trainer.model(**batch)
preds = outputs.logits
labels = batch["labels"]
compute_metrics((preds.cpu().numpy(), labels.cpu().numpy()))
{'accuracy': 1.0}
100% 的准确率,这是一个过拟合的很好的例子(这意味着,如果您在任何其他句子上尝试您的模型,它很可能给您错误的答案)!
如果您无法让您的模型获得这样的完美结果,则意味着您构建问题的方式或您的数据存在问题,因此您应该修复它。只有当您能够通过过拟合测试时,才能确定您的模型实际上能够学习到一些东西。
⚠️ 您将不得不在此测试后重新创建您的模型和Trainer
,因为得到的模型可能无法恢复并从您的完整数据集上学习到有用的东西。
在您获得第一个基线之前不要调整任何参数
超参数调整总是被强调为机器学习中最难的部分,但这只是帮助您在指标上稍微提升一下的最后一步。大多数情况下,Trainer
的默认超参数会很好地为您提供良好的结果,因此,在您获得一个在数据集上超过基线的模型之前,不要进行耗时且成本高昂的超参数搜索。
当您获得了一个足够好的模型时,您可以开始进行一些调整。不要尝试用不同的超参数启动一千次运行,而是比较几个用不同的超参数值进行的运行,以了解哪个超参数对结果的影响最大。
如果您正在调整模型本身,请保持简单,不要尝试任何您无法合理解释的东西。始终确保您回到过拟合测试,以验证您的更改是否产生了任何意外后果。
寻求帮助
希望您在本节中找到了一些对解决问题有帮助的建议,但如果没有,请记住您始终可以在论坛上向社区寻求帮助。
以下是一些可能对您有所帮助的其他资源
- “可重复性作为工程最佳实践的载体” by Joel Grus
- “调试神经网络的清单” by Cecelia Shao
- “如何对机器学习代码进行单元测试” by Chase Roberts
- “训练神经网络的配方” by Andrej Karpathy
当然,您在训练神经网络时遇到的并非所有问题都是您自己的过错!如果您在🤗 Transformers或🤗 Datasets库中遇到了看起来不对劲的东西,您可能遇到了错误。您应该告诉我们所有关于它的信息,在下一节中我们将解释如何做到这一点。