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` 意味着 `中立`,这意味着我们上面看到的两句话没有矛盾,第一句话不暗示第二句话。这看起来是正确的!
这里没有 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` collator 完成。而且这个数据整理器应该由 `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()
💡 如果您的训练数据不平衡,请确保构建一个包含所有标签的训练数据批次。
由此产生的模型应该在同一 `批次` 上获得接近完美的结果。让我们计算结果预测的指标
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 上更新