LLM 课程文档
调试训练流程
并获取增强的文档体验
开始使用
调试训练流程
您已经编写了一个漂亮的脚本,用于在给定任务上训练或微调模型,并忠实地遵循了第 7 章的建议。但是当您启动命令 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
,这意味着我们上面看到的两个句子并不矛盾,并且第一个句子并不意味着第二个句子。这似乎是正确的!
我们这里没有 token type IDs,因为 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 错误,我们之前提到了一个特例。现在让我们来看看。
处理 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
的默认超参数都可以很好地工作,为您提供良好的结果,因此在您获得一些超过数据集基线的结果之前,不要开始耗时且昂贵的超参数搜索。
一旦你拥有了一个足够好的模型,就可以开始进行一些调整。不要尝试用不同的超参数启动上千次运行,而是比较几次运行,每次运行中只改变一个超参数的值,以此来了解哪个超参数的影响最大。
如果你要调整模型本身,请保持简单,不要尝试任何你无法合理解释的东西。始终确保回到过拟合测试,以验证你的更改没有产生任何意想不到的后果。
寻求帮助
希望你能在本节中找到一些建议来帮助你解决问题,但如果没有,请记住你始终可以在论坛上向社区寻求帮助。
以下是一些可能对您有帮助的额外资源
- Joel Grus 的 “可重复性作为工程最佳实践的载体”
- Cecelia Shao 的 “调试神经网络的检查清单”
- Chase Roberts 的 “如何单元测试机器学习代码”
- Andrej Karpathy 的 “训练神经网络的秘诀”
当然,并非所有在训练神经网络时遇到的问题都是你自己的错误!如果你在 🤗 Transformers 或 🤗 Datasets 库中遇到似乎不正确的东西,你可能遇到了一个错误。你绝对应该告诉我们所有关于它的信息,在下一节中,我们将详细解释如何做到这一点。
< > 在 GitHub 上更新