NLP Course documentation

调试训练管道

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

调试训练管道

Open In Colab Open In Studio Lab

假如你已经尽可能地遵循 第七章 中的建议,编写了一段漂亮的代码来训练或微调给定任务的模型。但是当你启动命令 trainer.train() 时,可怕的事情发生了:你得到一个错误😱!或者更糟糕的是,虽然看起来一切似乎都正常,训练运行没有错误,但生成的模型很糟糕。在本节中,我们将向你展示如何调试此类问题。

调试训练管道

当你在 trainer.train() 中遇到错误时,它有可能来自多个不同的来源,因为 Trainer 会将很多模块放在一起组合运行。首先它会将 datasets 转换为 dataloaders 因此问题可能出在 datasets 中,或者在尝试将 datasets 的元素一起批处理时出现问题。接着它会准备一批数据并将其提供给模型,因此问题可能出在模型代码中。之后,它会计算梯度并执行优化器,因此问题也可能出在你的优化器中。即使训练一切顺利,如果你的评估指标有问题,评估期间仍然可能出现问题。

调试 trainer.train() 中出现的错误的最佳方法是手动检查整个管道,看看哪里出了问题。通常情况下,错误很容易解决。

为了讲解这个过程,我们将尝试使用以下脚本在 MNLI 数据集 上微调 DistilBERT 模型:

from datasets import load_dataset
from transformers import (
    AutoTokenizer,
    AutoModelForSequenceClassification,
    TrainingArguments,
    Trainer,
)

raw_datasets = evaluate.load("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 将无法将数据整理成 batch,更不用说训练你的模型了。因此,首先需要检查一下你的训练集里面的内容。

为了避免花费无数小时试图修复不是错误来源的问题,避免多次封装的干扰,我们建议你只使用 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 会自动删除与模型配置中不需要的列 (即模型预期的输入参数)。这意味着在这里,除了标签之外的所有东西都被丢弃了。因此,创建 batch 然后将它们发送到模型时没有问题,但是模型会提示没有收到正确的输入。

为什么模型没有得到 input_ids 这一列呢?我们确实在数据集上调用了 Dataset.map() 方法,并使用 tokenizer 处理了每个样本。但是如果你仔细看代码,你会发现我们在将训练和评估集传递给 Trainer 时犯了一个错误。我们在这里没有使用 tokenized_datasets ,而是使用了 raw_datasets 🤦。所以让我们来解决这个问题!

from datasets import load_dataset
from transformers import (
    AutoTokenizer,
    AutoModelForSequenceClassification,
    TrainingArguments,
    Trainer,
)

raw_datasets = evaluate.load("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)'

查看 traceback,我们可以看到错误发生在数据整理步骤中:

~/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 解码后的原始输入文本:

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_idsattention_masklabel (它将被重命名为 labels )。为了防止记错,可以先打印模型的类,然后在其文档中看看它需要输入哪些列。

type(trainer.model)
transformers.models.distilbert.modeling_distilbert.DistilBertForSequenceClassification

所以在我们的这个例子中,我们可以在 模型文档 中查看这个模型接受的参数。同样的 Trainer 也会记录它丢弃的列。

我们通过解码检查了 inputs 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]

由于我们没有在预处理中使用填充,这看起来没什么问题。为确保该注意掩码没有问题,让我们检查它与 inputs ID 的长度是否相同:

len(trainer.train_dataset[0]["attention_mask"]) == len(
    trainer.train_dataset[0]["input_ids"]
)
True

那挺好的!最后,让我们检查一下我们的标签:

trainer.train_dataset[0]["label"]
1

与inputs ID 一样,这是一个本身并没有真正意义的数字,我们需要检查的是它所对应的真实的标签名称是否是正确的。正如我们之前学到的,标签 ID 和标签名之间的映射存储在数据集 features 里的 names 属性中:

trainer.train_dataset.features["label"].names
['entailment', 'neutral', 'contradiction']

所以 1 表示 neutral ,表示我们上面看到的两句话并不矛盾,也没有包含关系。这也看起来是正确的!

我们这里没有 token 类型 ID,因为 DistilBERT 不需要它们;如果你的模型中有,你还应该确保 token 类型 ID 可以正确匹配输入中第一句和第二句的位置。

✏️ 轮到你了! 检查训练数据集的第二个条数据是否正确。

我们在这里只对训练集进行检查,但你当然应该以同样的方式仔细检查验证集和测试集。

现在我们知道我们的数据集看起来不错,下一步是时候检查训练管道了。

从 datasets 到 dataloaders

训练管道中可能出错的下一个操作发生在 Trainer 尝试从训练或验证集中生成 batch 时。当你确定 Trainer 的数据集是正确的后,你可以尝试通过执行以下代码手动形成一个 batch(当要测试验证集的 dataloaders 时,可以将 train 替换为 eval ):

for batch in trainer.get_train_dataloader():
    break

上述代码会创建训练集的数据加载器,然后对其进行迭代一次。如果代码执行没有错误,那么你就有了可以检查的第一个 batch,如果代码出错,你可以确定问题出在数据加载器中,如下所示:

~/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)

Trackback 的最后一个堆栈的输出应该足够给你一些线索,但让我们再深入挖掘一下创建 batch 的过程。创建 batch 过程中的大多数问题是在将数据整理到单个 batch 中时出现的, 因此在出现问题时首先要检查的是 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 ,但这不是我们在这个示例中想要使用的 collate_fn 。我们希望将 batch 中的每个句子填充到 batch 中最长的句子,这项功能是由 DataCollatorWithPadding 整理器实现的。而 Trainer 默认使用的数据整理器就是它,为什么这里没有使用呢?

答案是因为我们在上面的代码中并没有把 tokenizer 传递给 Trainer ,所以它无法创建我们想要的 DataCollatorWithPadding 。在实践中,你应该明确地传递你想要使用的数据整理器,以确保避免这些类型的错误。让我们修改代码以实现这一点:

from datasets import load_dataset
import evaluate
from transformers import (
    AutoTokenizer,
    AutoModelForSequenceClassification,
    DataCollatorWithPadding,
    TrainingArguments,
    Trainer,
)

raw_datasets = evaluate.load("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 错误通常很难调试。我们将在稍后再解决这个问题,现在让我们先完成对创建的 batch 的分析。

如果你确定你的数据整理器是正确的,那么应该尝试用它来处理数据集的几个样本,检查一下在创建样本的时候是否会出现错误:

data_collator = trainer.get_train_dataloader().collate_fn
batch = data_collator([trainer.train_dataset[i] for i in range(4)])

上述代码运行失败,因为 train_dataset 包含字符串列, 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)])

如果又出现了新的错误,则应该手动调试数据整理器以确定具体的问题。

现在我们已经调试了创建 batch 过程,是时候将数据传递给模型了!

检查模型

你可以通过执行以下命令来获得一个 batch 的数据:

for batch in trainer.get_train_dataloader():
    break

如果你在 notebook 中运行此代码,你可能会收到与我们之前看到的类似的 CUDA 错误,在这种情况下,你需要重新启动 notebook 并重新执行最后一段代码,但是暂时不要运行 trainer.train() 这一行命令。这是关于 CUDA 错误的第二个最烦人的事情:它们会破坏你的 Cuda 内核,而且无法恢复。它们最烦人的事情是它们很难调试。

这是为什么?它与 GPU 的工作方式有关。它们在并行执行大量操作方面非常有效,但缺点是当其中一条指令导致错误时,你不会立即知道。只有当程序在 GPU 上调用多个进程的同步处理时,才会输出一些错误信息,但事实上真实触发错误的地方往往与输出错误信息的地方并不一致。例如,如果我们查看之前的 Trackback,错误是在反向传播期间引发的,但我们会在一分钟后看到错误实际上源于前向传播的某些东西。

那么我们如何调试这些错误呢?答案很简单:不调试。除非你的 CUDA 错误是内存不足错误(这意味着你的 GPU 中没有足够的内存),除此之外你应该始终返回到 CPU 进行调试。

为此,我们只需将模型放回 CPU 然后将一个 batch 的数据送入模型。现在 DataLoader 返回的那批数据还尚未移动到 GPU,因此可以直接送入这个 batch

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 (因此与反向传播无关)。我们可以看到是 Target 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 = evaluate.load("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() 命令。先请求一个 batch 的数据并将其传递给我们的模型,如果它现在可以正常工作了!

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)

如果仍然出现错误,请确保重新启动 notebook 并仅执行最后一版的代码(如果之前出现了CUDA 错误,那么CUDA 内核就会被破坏,之后哪怕执行正确的代码也会出现错误)。

执行一个优化器步骤

现在我们已经可以构建通过模型检查的成批次的数据,我们已经为训练管道的下一步做好准备:接下来是计算梯度并执行优化器迭代。

第一部分是在 loss 上调用 backward() 方法:

loss = outputs.loss
loss.backward()

在这个阶段很少出现错误,但如果确实出现错误,那么需要返回 CPU 来获取更有用的错误消息。

要执行优化步骤,我们只需要创建 optimizer 并调用它的 step() 方法:

trainer.create_optimizer()
trainer.optimizer.step()

同样,如果你在 Trainer 中使用默认优化器,那么在此阶段你不应该收到错误,但如果你有自定义优化器,则可能会出现一些问题,需要在这里调试。如果你在此阶段遇到奇怪的 CUDA 错误,请不要忘记返回 CPU。说到 CUDA 错误,前面我们提到了一个特殊情况。现在让我们来看看这种情况。

处理 CUDA out-of-memory 错误

每当你收到以 RuntimeError: CUDA out of memory 开头的错误消息时,这表明你的显存不足。这与你的代码没有直接关系,并且它也可能发生在运行良好的代码中。此错误意味着你试图在 GPU 的显存中放入太多东西,这导致了错误。与其他 CUDA 错误一样,你需要重新启动内核才能再次运行训练。

要解决这个问题,你只需要使用更少的显存—这往往说起来容易做起来难。首先,确保你没有同时在 GPU 上运行两个模型(当然,除非在解决问题时必须要这样做)。然后,你可能应该减少 batch 的大小,因为它直接影响模型的所有中间输出的大小及其梯度。如果问题仍然存在,请考虑使用较小版本的模型,或者更换有更大显存的设备。

在课程的下一部分中,我们将介绍更先进的技术,这些技术可以帮助你减少内存占用并让你微调超大的模型。

评估模型

现在我们已经解决了所有的代码问题,一切都很完美,训练应该可以顺利进行,对吧?没那么快!如果你运行 trainer.train() 命令,一开始一切看起来都不错,但过一会儿你会得到以下信息:

# 这将花费很长时间并且会出错,所以不要直接运行这个单元
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() 是可以运行的,以避免在遇到错误之前浪费大量计算资源。

在尝试调试评估循环中的问题之前,你应该首先确保你已经检查了数据,能够正确地形成了 batch 并且可以在其上运行你的模型。我们已经完成了所有这些步骤,因此可以执行以下代码而不会出错:

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)

稍等一会儿,错误就会出现,在评估阶段结束时输出了一个错误,如果我们查看 Trackback,我们会看到:

~/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() 函数时出现的问题。它需要输入 NumPy 数组格式的 logits 值和标签的元组,所以让我们尝试将其提供给它:

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

我们得到了同样的错误,所以问题肯定出在那个函数上。如果我们回顾它的代码,我们会发现它只是将 predictionslabels 转发到 metric.compute() 。那么这种方法有问题吗?不一定。让我们快速浏览一下输入的形状:

predictions.shape, labels.shape
((8, 3), (8,))

我们的模型预测的输出是三个标签的 logits 值,而不是概率最高的标签id,这就是 metrics 返回这个(有点模糊)错误的原因。修复很简单;我们只需要在 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 = evaluate.load("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()

在训练期间调试静默(没有任何错误提示)错误

如何调试一个没有错误但也没有得到好的结果的训练?接下来会给出一些可以参考的做法,但请注意,这种调试是机器学习中最难的部分,并且没有万能的灵丹妙药。

再次检查你的数据

理论上,只有数据中存在可以学习的知识,模型才会学到一些知识。如果数据已经被损坏了或标签是随机的,那么模型很可能无法从数据集中获得任何知识。因此,始终首先仔细检查你的解码后的输入和真实的标签,然后问自己以下问题:

  • 解码后的文本数据你是否可以正常阅读和理解?
  • 你认同这些标签对于文本的描述吗?
  • 有没有一个标签比其他标签更常见?
  • 如果模型预测的答案是随机的或总是相同的,那么 loss/ 评估指标应该是多少,是否模型根本没能学到任何知识?

⚠️ 如果你正在进行分布式训练,请在每个进程中打印数据集的样本并仔细核对,确保你得到的是相同的内容。一个常见的错误是在数据创建过程中有一些随机性,导致每个进程具有不同版本的数据集。

在检查数据后,可以检查模型的一些预测并对其进行解码。 如果模型总是预测同样的类别,那么可能是因为这个类别在数据集中的比例比较高(针对分类问题); 过采样稀有类等技术可能会对解决这种问题有帮助。或者,这也可能是由训练的设置(如错误的超参数设置)引起的。

如果在初始模型上获得的 loss/ 评估指标与预估的随机时预测的 loss/ 评估指标非常不同,则应该仔细检查 loss/ 评估指标的计算方式,因为其中可能存在错误。如果使用多个 loss,并将其相加计算最后的loss,则应该确保它们具有相同的比例大小。

当你确定你的数据是完美的之后,则可以通过一个简单的过拟合测试来查看模型是否能够用其进行训练。

在一个 batch 上过拟合模型

过拟合通常是在训练时尽量避免的事情,因为这意味着模型没有识别并学习我们想要的一般特征,而只是记住了训练样本。 但一遍又一遍地尝试在一个 batch 上训练模型可以检查数据集所描述的问题是否可以通过训练的模型来解决, 它还将帮助查看你的初始学习率是否太高了。

在定义好 Trainer 之后,这样做真的很容易;只需获取一批训练数据,然后仅使用这个 batch 运行一个小型手动训练循环,大约 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 ,因为获得的模型可能无法在你的完整数据集上恢复和学习有用的东西。

在你有第一个 baseline 模型之前不要调整任何东西

超参数调优总是被强调为机器学习中最难的部分,但这只是帮助你在指标上有所收获的最后一步。大多数情况下, Trainer 的默认超参数可以很好地为你提供良好的结果,因此在你拥有数据集上的 baseline 模型之前,不要急于进行耗时和昂贵的超参数搜索。

在有一个足够好的模型后,就可以开始微调了。尽量避免使用不同的超参数进行一千次运行,而要比较一个超参数取不同数值的几次运行,从而了解哪个超参数的影响最大,从而理解超参数值的改变与于模型训练之间的关系。

如果正在调整模型本身,请保持简单,不要直接对模型进行非常复杂的无法理解或者证明的修改,要一步步修改,同时尝试理解和证明这次修改对模型产生的影响,并且 确保通过过拟合测试来验证修改没有引发其他的问题。

请求帮忙

希望你会在本课程中找到一些可以帮助你解决问题的建议,除此之外可以随时在 论坛 上向社区提问。

以下是一些可能有用的额外资源:

-Joel Grus 的 “作为工程最佳实践工具的再现性”

当然,并不是你在训练神经网络时遇到的每个问题都是你自己的错!如果你在🤗 Transformers 或🤗 Datasets 库中遇到了似乎不正确的东西,你可能遇到了一个错误。你应该告诉我们所有这些问题,在下一节中,我们将详细解释如何做到这一点。

< > Update on GitHub