调试训练流水线
您已经编写了一个漂亮的脚本,用于在给定任务上训练或微调模型,并认真遵循了第 7 章中的建议。但是,当您启动命令 model.fit()
时,发生了一些可怕的事情:您遇到了错误 😱!或者更糟糕的是,一切似乎都很好,训练在没有错误的情况下运行,但生成的模型很糟糕。在本节中,我们将向您展示如何调试此类问题。
调试训练流水线
当您在 model.fit()
中遇到错误时,问题在于它可能来自多个来源,因为训练通常会将您之前一直在处理的许多事物整合在一起。问题可能是您的数据集存在问题,或者是在尝试将数据集的元素组合成批次时出现了一些问题。或者它可能是模型代码、损失函数或优化器中存在一些问题。即使一切训练都顺利进行,如果您的指标存在问题,在评估期间仍然可能出现问题。
调试 model.fit()
中出现的错误的最佳方法是手动遍历整个流水线,以查看问题出在哪里。然后,错误通常很容易解决。
为了演示这一点,我们将使用以下脚本(尝试)在 MNLI 数据集 上微调 DistilBERT 模型
from datasets import load_dataset
import evaluate
from transformers import (
AutoTokenizer,
TFAutoModelForSequenceClassification,
)
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)
train_dataset = tokenized_datasets["train"].to_tf_dataset(
columns=["input_ids", "labels"], batch_size=16, shuffle=True
)
validation_dataset = tokenized_datasets["validation_matched"].to_tf_dataset(
columns=["input_ids", "labels"], batch_size=16, shuffle=True
)
model = TFAutoModelForSequenceClassification.from_pretrained(model_checkpoint)
model.compile(loss="sparse_categorical_crossentropy", optimizer="adam")
model.fit(train_dataset)
如果您尝试执行它,在执行数据集转换时可能会收到一些 VisibleDeprecationWarning
警告 - 这是一个已知的用户体验问题,请忽略它。如果您在 2021 年 11 月之后阅读本课程,并且问题仍然存在,请向 @carrigmat 发送愤怒推文,直到他修复它。
不过,更严重的问题是我们遇到了一个彻底的错误。而且它真的很长,令人恐惧
ValueError: No gradients provided for any variable: ['tf_distil_bert_for_sequence_classification/distilbert/embeddings/word_embeddings/weight:0', '...']
这是什么意思?我们尝试使用我们的数据进行训练,但没有获得梯度?这令人费解;我们甚至如何开始调试这样的问题?当您收到的错误没有立即表明问题出在哪里时,最佳解决方案通常是按顺序逐步检查,确保每个阶段都看起来正确。当然,开始的地方始终是……
检查您的数据
不用说,如果您的数据已损坏,Keras 将无法为您修复它。所以,首先,您需要查看训练集中包含的内容。
虽然查看 raw_datasets
和 tokenized_datasets
很诱人,但我们强烈建议您在数据即将进入模型的点查看它。这意味着读取使用 to_tf_dataset()
函数创建的 tf.data.Dataset
输出!那么我们如何做到这一点呢?tf.data.Dataset
对象一次提供整个批次,并且不支持索引,因此我们不能只请求 train_dataset[0]
。但是,我们可以礼貌地请求一个批次
for batch in train_dataset:
break
break
在一次迭代后结束循环,因此这会获取从 train_dataset
中输出的第一个批次并将其保存为 batch
。现在,让我们看看里面有什么
{'attention_mask': <tf.Tensor: shape=(16, 76), dtype=int64, numpy=
array([[1, 1, 1, ..., 0, 0, 0],
[1, 1, 1, ..., 0, 0, 0],
[1, 1, 1, ..., 0, 0, 0],
...,
[1, 1, 1, ..., 1, 1, 1],
[1, 1, 1, ..., 0, 0, 0],
[1, 1, 1, ..., 0, 0, 0]])>,
'label': <tf.Tensor: shape=(16,), dtype=int64, numpy=array([0, 2, 1, 2, 1, 1, 2, 0, 0, 0, 1, 0, 1, 2, 2, 1])>,
'input_ids': <tf.Tensor: shape=(16, 76), dtype=int64, numpy=
array([[ 101, 2174, 1010, ..., 0, 0, 0],
[ 101, 3174, 2420, ..., 0, 0, 0],
[ 101, 2044, 2048, ..., 0, 0, 0],
...,
[ 101, 3398, 3398, ..., 2051, 2894, 102],
[ 101, 1996, 4124, ..., 0, 0, 0],
[ 101, 1999, 2070, ..., 0, 0, 0]])>}
这看起来没错,不是吗?我们将 labels
、attention_mask
和 input_ids
传递给模型,这应该是它计算输出和计算损失所需的一切。那么为什么我们没有梯度呢?仔细看看:我们传递了一个单一的字典作为输入,但训练批次通常是一个输入张量或字典,加上一个标签张量。我们的标签只是我们输入字典中的一个键。
这是一个问题吗?实际上,并非总是如此!但这是在使用 TensorFlow 训练 Transformer 模型时遇到的最常见问题之一。我们的所有模型都可以在内部计算损失,但要做到这一点,标签需要在输入字典中传递。当我们没有为 compile()
指定损失值时,这是使用的损失。另一方面,Keras 通常期望标签与输入字典分开传递,并且如果您不这样做,损失计算通常会失败。
现在问题变得更清楚了:我们传递了一个 loss
参数,这意味着我们要求 Keras 为我们计算损失,但我们将我们的标签作为输入传递给模型,而不是 Keras 期望它们所在的位置!我们需要选择一个:要么我们使用模型的内部损失并将标签保留在它们所在的位置,要么我们继续使用 Keras 损失,但我们将标签移动到 Keras 期望它们所在的位置。为简单起见,让我们采用第一种方法。将对 compile()
的调用更改为
model.compile(optimizer="adam")
现在我们将使用模型的内部损失,这个问题应该得到解决!
✏️ 轮到你了!在解决其他问题之后,作为一个可选的挑战,您可以尝试回到这一步,让模型使用原始的 Keras 计算的损失而不是内部损失来工作。您需要将 "labels"
添加到 to_tf_dataset()
的 label_cols
参数中,以确保标签被正确输出,这将为您提供梯度 - 但我们指定的损失还有一个问题。训练仍然会在这个问题下运行,但学习速度会非常慢,并且会在较高的训练损失处达到平台期。你能弄清楚它是什么吗?
如果您卡住了,这里有一个 ROT13 编码的提示:Vs lbh ybbx ng gur bhgchgf bs FrdhraprPynffvsvpngvba zbqryf va Genafsbezref, gurve svefg bhgchg vf ybtvgf
。Jung ner ybtvgf?
第二个提示:Jura lbh fcrpvsl bcgvzvmref, npgvingvbaf be ybffrf jvgu fgevatf, Xrenf frgf nyy gur nethzrag inyhrf gb gurve qrsnhygf. Jung nethzragf qbrf FcnefrPngrtbevpnyPebffragebcl unir, naq jung ner gurve qrsnhygf?
现在,让我们尝试训练。我们现在应该获得梯度了,所以希望(这里播放不祥的音乐)我们可以直接调用 model.fit()
,一切都会正常工作!
246/24543 [..............................] - ETA: 15:52 - loss: nan
哦,不。
nan
不是一个非常令人鼓舞的损失值。尽管如此,我们已经检查了我们的数据,它看起来还不错。如果不是问题所在,我们接下来可以去哪里?下一步很明显是……
检查您的模型
Keras 中的 model.fit()
函数确实非常方便,但它会帮你处理很多事情,这使得查找问题的确切位置变得更加棘手。如果你正在调试你的模型,一个非常有帮助的策略是只向模型传递一个批次,并详细查看该批次的输出。另一个非常有用的提示是,如果模型抛出错误,可以使用 run_eagerly=True
来 compile()
模型。这会使速度大大降低,但会使错误消息更容易理解,因为它们会准确地指出模型代码中出现问题的位置。
不过,目前我们还不需要 run_eagerly
。让我们将之前得到的 batch
通过模型运行,看看输出是什么样的。
model(batch)
TFSequenceClassifierOutput(loss=<tf.Tensor: shape=(16,), dtype=float32, numpy=
array([nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan,
nan, nan, nan], dtype=float32)>, logits=<tf.Tensor: shape=(16, 2), dtype=float32, numpy=
array([[nan, nan],
[nan, nan],
[nan, nan],
[nan, nan],
[nan, nan],
[nan, nan],
[nan, nan],
[nan, nan],
[nan, nan],
[nan, nan],
[nan, nan],
[nan, nan],
[nan, nan],
[nan, nan],
[nan, nan],
[nan, nan]], dtype=float32)>, hidden_states=None, attentions=None)
好吧,这有点棘手。所有输出都是 nan
!但这很奇怪,不是吗?我们的所有 logits 怎么会变成 nan
?nan
代表“非数字”。nan
值通常出现在执行禁止操作时,例如除以零。但关于机器学习中 nan
的一个非常重要的知识是,这个值往往会传播。如果你将一个数字乘以 nan
,输出也是 nan
。如果你在输出、损失或梯度中的任何位置得到 nan
,它就会迅速传播到整个模型中——因为当这个 nan
值反向传播到你的网络时,你会得到 nan
梯度,当用这些梯度计算权重更新时,你会得到 nan
权重,而这些权重会计算出更多 nan
输出!很快整个网络就会变成一大块 nan
。一旦发生这种情况,就很难看出问题是从哪里开始的。我们如何隔离 nan
最初出现的位置呢?
答案是尝试重新初始化我们的模型。一旦我们开始训练,就会在某个地方得到一个 nan
,并且它迅速传播到整个模型。所以,让我们从检查点加载模型,并且不做任何权重更新,看看我们在哪里得到了 nan
值。
model = TFAutoModelForSequenceClassification.from_pretrained(model_checkpoint) model(batch)
当我们运行它时,我们得到
TFSequenceClassifierOutput(loss=<tf.Tensor: shape=(16,), dtype=float32, numpy=
array([0.6844486 , nan, nan, 0.67127866, 0.7068601 ,
nan, 0.69309855, nan, 0.65531296, nan,
nan, nan, 0.675402 , nan, nan,
0.69831556], dtype=float32)>, logits=<tf.Tensor: shape=(16, 2), dtype=float32, numpy=
array([[-0.04761693, -0.06509043],
[-0.0481936 , -0.04556257],
[-0.0040929 , -0.05848458],
[-0.02417453, -0.0684005 ],
[-0.02517801, -0.05241832],
[-0.04514256, -0.0757378 ],
[-0.02656011, -0.02646275],
[ 0.00766164, -0.04350497],
[ 0.02060014, -0.05655622],
[-0.02615328, -0.0447021 ],
[-0.05119278, -0.06928903],
[-0.02859691, -0.04879177],
[-0.02210129, -0.05791225],
[-0.02363213, -0.05962167],
[-0.05352269, -0.0481673 ],
[-0.08141848, -0.07110836]], dtype=float32)>, hidden_states=None, attentions=None)
现在我们找到了一些线索!我们的 logits 中没有 nan
值,这令人放心。但是我们确实在损失中看到了一些 nan
值!是这些特定样本中的某些东西导致了这个问题吗?让我们看看它们是哪些(请注意,如果你自己运行这段代码,你可能会得到不同的索引,因为数据集已经被洗牌了)。
import numpy as np
loss = model(batch).loss.numpy()
indices = np.flatnonzero(np.isnan(loss))
indices
array([ 1, 2, 5, 7, 9, 10, 11, 13, 14])
让我们看看这些索引来自的样本。
input_ids = batch["input_ids"].numpy()
input_ids[indices]
array([[ 101, 2007, 2032, 2001, 1037, 16480, 3917, 2594, 4135,
23212, 3070, 2214, 10170, 1010, 2012, 4356, 1997, 3183,
6838, 12953, 2039, 2000, 1996, 6147, 1997, 2010, 2606,
1012, 102, 6838, 2001, 3294, 6625, 3773, 1996, 2214,
2158, 1012, 102, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0],
[ 101, 1998, 6814, 2016, 2234, 2461, 2153, 1998, 13322,
2009, 1012, 102, 2045, 1005, 1055, 2053, 3382, 2008,
2016, 1005, 2222, 3046, 8103, 2075, 2009, 2153, 1012,
102, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0],
[ 101, 1998, 2007, 1996, 3712, 4634, 1010, 2057, 8108,
2025, 3404, 2028, 1012, 1996, 2616, 18449, 2125, 1999,
1037, 9666, 1997, 4100, 8663, 11020, 6313, 2791, 1998,
2431, 1011, 4301, 1012, 102, 2028, 1005, 1055, 5177,
2110, 1998, 3977, 2000, 2832, 2106, 2025, 2689, 2104,
2122, 6214, 1012, 102, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0],
[ 101, 1045, 2001, 1999, 1037, 13090, 5948, 2007, 2048,
2308, 2006, 2026, 5001, 2043, 2026, 2171, 2001, 2170,
1012, 102, 1045, 2001, 3564, 1999, 2277, 1012, 102,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0],
[ 101, 2195, 4279, 2191, 2039, 1996, 2181, 2124, 2004,
1996, 2225, 7363, 1012, 102, 2045, 2003, 2069, 2028,
2451, 1999, 1996, 2225, 7363, 1012, 102, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0],
[ 101, 2061, 2008, 1045, 2123, 1005, 1056, 2113, 2065,
2009, 2428, 10654, 7347, 2030, 2009, 7126, 2256, 2495,
2291, 102, 2009, 2003, 5094, 2256, 2495, 2291, 2035,
2105, 1012, 102, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0],
[ 101, 2051, 1010, 2029, 3216, 2019, 2503, 3444, 1010,
6732, 1996, 2265, 2038, 19840, 2098, 2125, 9906, 1998,
2003, 2770, 2041, 1997, 4784, 1012, 102, 2051, 6732,
1996, 2265, 2003, 9525, 1998, 4569, 1012, 102, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0],
[ 101, 1996, 10556, 2140, 11515, 2058, 1010, 2010, 2162,
2252, 5689, 2013, 2010, 7223, 1012, 102, 2043, 1996,
10556, 2140, 11515, 2058, 1010, 2010, 2252, 3062, 2000,
1996, 2598, 1012, 102, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0],
[ 101, 13543, 1999, 2049, 6143, 2933, 2443, 102, 2025,
13543, 1999, 6143, 2933, 2003, 2443, 102, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0]])
好吧,这里有很多东西,但没有什么看起来不寻常的。让我们看看标签。
labels = batch['labels'].numpy()
labels[indices]
array([2, 2, 2, 2, 2, 2, 2, 2, 2])
啊!nan
样本都具有相同的标签,并且是标签 2。这是一个非常强烈的提示。我们只有在标签为 2 时才会得到 nan
的损失,这表明现在是检查模型中标签数量的好时机。
model.config.num_labels
2
现在我们看到了问题:模型认为只有两个类别,但标签最多为 2,这意味着实际上有三个类别(因为 0 也是一个类别)。这就是我们如何得到 nan
的——试图计算不存在类别的损失!让我们尝试更改它并再次拟合模型。
model = TFAutoModelForSequenceClassification.from_pretrained(model_checkpoint, num_labels=3)
model.compile(optimizer='adam')
model.fit(train_dataset)
869/24543 [>.............................] - ETA: 15:29 - loss: 1.1032
我们正在训练!不再有 nan
了,我们的损失正在下降……有点。如果你观察一段时间,你可能会开始有点不耐烦,因为损失值仍然顽固地保持在高位。让我们在这里停止训练,并尝试思考可能导致此问题的原因。在这一点上,我们非常确定数据和模型都没有问题,但我们的模型学习效果不佳。还有什么剩下的呢?是时候……
检查你的超参数
如果你回顾上面的代码,你可能根本看不到任何超参数,除了可能 batch_size
,但这看起来不太可能是罪魁祸首。不过不要被骗了;超参数总是存在的,如果你看不到它们,那只是意味着你不知道它们的设置值。特别是,请记住 Keras 的一个关键点:如果你用字符串设置损失、优化器或激活函数,它的所有参数都将被设置为默认值。这意味着尽管使用字符串非常方便,但在这样做时应该非常小心,因为它很容易隐藏关键信息。(尝试上面可选挑战的任何人都应该仔细注意这一点。)
在这种情况下,我们在哪里用字符串设置了参数?我们最初是用字符串设置损失的,但现在不再这样做了。但是,我们正在用字符串设置优化器。这会隐藏任何信息吗?让我们看看它的参数。
这里有什么突出的吗?没错——学习率!当我们只使用字符串 'adam'
时,我们将获得默认的学习率,即 0.001 或 1e-3。这对于 Transformer 模型来说太高了!一般来说,我们建议尝试 1e-5 到 1e-4 之间的学习率用于你的模型;这比我们这里实际使用的值小 10 倍到 100 倍。这听起来可能是一个主要问题,所以让我们尝试降低它。为此,我们需要导入实际的 optimizer
对象。趁此机会,让我们从检查点重新初始化模型,以防使用高学习率训练损坏了它的权重。
from tensorflow.keras.optimizers import Adam
model = TFAutoModelForSequenceClassification.from_pretrained(model_checkpoint)
model.compile(optimizer=Adam(5e-5))
💡 你也可以从 🤗 Transformers 导入 create_optimizer()
函数,它会为你提供一个具有正确权重衰减以及学习率预热和衰减的 AdamW 优化器。与使用默认 Adam 优化器获得的结果相比,此优化器通常会产生略好一些的结果。
现在,我们可以尝试使用新的、改进的学习率拟合模型。
model.fit(train_dataset)
319/24543 [..............................] - ETA: 16:07 - loss: 0.9718
现在我们的损失真的开始下降了!训练终于看起来像是在工作了。这里有一个教训:当你的模型正在运行但损失没有下降,并且你确定你的数据没问题时,最好检查一下超参数,比如学习率和权重衰减。将这两个参数中的任何一个设置得太高都非常有可能导致训练在高损失值处“停滞”。
其他潜在问题
我们已经涵盖了上面脚本中的问题,但你可能会遇到其他一些常见错误。让我们看一下(非常不完整)的列表。
处理内存不足错误
内存不足的明显标志是类似“分配张量时内存不足”的错误——OOM 是“out of memory”(内存不足)的缩写。在处理大型语言模型时,这是一个非常常见的危险。如果遇到这种情况,一个好的策略是将批次大小减半,然后重试。但请记住,有些模型非常大。例如,完整尺寸的 GPT-2 有 15 亿个参数,这意味着你只需要 6GB 的内存来存储模型,另外 6GB 用于它的梯度!训练完整的 GPT-2 模型通常需要超过 20GB 的显存,无论你使用什么批次大小,只有少数 GPU 才能做到这一点。像 distilbert-base-cased
这样的更轻量级的模型更容易运行,并且训练速度也快得多。
在本课程的下一部分,我们将了解更多可以帮助你减少内存占用并让你微调最大模型的高级技术。
饥饿的 TensorFlow 🦛
你应该了解 TensorFlow 的一个特殊特性是,它会在你加载模型或进行任何训练时立即将所有 GPU 内存分配给自己,然后根据需要划分该内存。这与其他框架(如 PyTorch)的行为不同,后者使用 CUDA 根据需要分配内存,而不是在内部进行。TensorFlow 方法的一个优点是,当内存不足时,它通常会给出有用的错误,并且它可以从该状态恢复而不会导致整个 CUDA 内核崩溃。但它也存在一个重要的缺点:如果你同时运行两个 TensorFlow 进程,那么你将会遇到麻烦。
如果你在 Colab 上运行,则无需担心这个问题,但如果在本地运行,则绝对应该小心。特别要注意,关闭笔记本标签页并不一定意味着关闭该笔记本!你可能需要选择正在运行的笔记本(带有绿色图标的笔记本),并在目录列表中手动关闭它们。任何使用 TensorFlow 的正在运行的笔记本都可能仍然占用大量 GPU 内存,这意味着你启动的任何新笔记本都可能会遇到一些非常奇怪的问题。
如果你开始收到有关 CUDA、BLAS 或 cuBLAS 的错误,而这些错误之前在代码中是正常的,那么这通常就是罪魁祸首。你可以使用类似 nvidia-smi
的命令进行检查——当你关闭或重新启动当前笔记本时,大部分内存是否已释放,或者是否仍在使用?如果仍在使用,则说明其他东西正在占用它!
再次检查你的数据!
只有当你的数据实际上能够学习到东西时,你的模型才能学习到东西。如果存在损坏数据的错误或标签是随机分配的,那么你很可能无法在你的数据集上进行任何模型训练。这里一个有用的工具是 tokenizer.decode()
。这会将 input_ids
转换回字符串,以便你可以查看数据并确认你的训练数据是否在教授你想要教授的内容。例如,在你从 tf.data.Dataset
获取 batch
之后,就像我们在上面做的那样,你可以像这样解码第一个元素
input_ids = batch["input_ids"].numpy()
tokenizer.decode(input_ids[0])
然后,你可以将其与第一个标签进行比较,如下所示
labels = batch["labels"].numpy()
label = labels[0]
一旦你可以像这样查看你的数据,就可以问自己以下问题
- 解码后的数据是否可理解?
- 你是否同意这些标签?
- 是否存在一个标签比其他标签更常见?
- 如果模型预测随机答案/始终相同的答案,损失/指标应该是什么?
查看完数据后,浏览模型的一些预测——如果你的模型输出标记,请尝试解码它们!如果模型总是预测相同的结果,可能是因为你的数据集偏向于一个类别(对于分类问题),因此像过采样稀有类别这样的技术可能会有所帮助。或者,这也可能是由训练问题引起的,例如超参数设置不当。
如果你在任何训练之前获得的初始模型上的损失/指标与你对随机预测的预期损失/指标非常不同,请仔细检查损失或指标的计算方式,因为那里可能存在错误。如果你使用多个最终相加的损失,请确保它们具有相同的量级。
当你确定你的数据完美无缺时,你可以通过一个简单的测试来查看模型是否能够在其上进行训练。
在一个批次上过拟合你的模型
过拟合通常是我们训练时试图避免的事情,因为它意味着模型没有学习识别我们想要它识别的通用特征,而是仅仅记住了训练样本。但是,尝试一遍又一遍地在单个批次上训练你的模型是一个很好的测试,以检查你所提出的问题是否可以通过你试图训练的模型来解决。它还有助于你了解你的初始学习率是否过高。
在你定义了 model
之后,执行此操作非常容易;只需获取一批训练数据,然后将该 batch
视为你的整个数据集,在其上进行大量轮次的训练
for batch in train_dataset:
break
# Make sure you have run model.compile() and set your optimizer,
# and your loss/metrics if you're using them
model.fit(batch, epochs=20)
💡 如果你的训练数据不平衡,请确保构建一个包含所有标签的训练数据批次。
生成的模型应该在 batch
上获得接近完美的结果,损失迅速下降到 0(或你使用的损失的最小值)。
如果你没有让你的模型获得这样的完美结果,这意味着你提出问题或数据的方式存在问题,因此你应该解决它。只有当你通过过拟合测试时,才能确保你的模型实际上能够学习到东西。
⚠️ 你将不得不重新创建你的模型并在进行此过拟合测试后重新编译,因为获得的模型可能无法恢复并在你的完整数据集上学习有用的东西。
在获得第一个基线之前不要调整任何内容
人们总是强调密集的超参数调整是机器学习中最难的部分,但它只是最后一步,可以帮助你在指标上获得一些提升。非常糟糕的超参数值,例如在 Transformer 模型中使用 1e-3 的默认 Adam 学习率,当然会导致学习速度非常慢或完全停滞,但在大多数情况下,“合理”的超参数,例如 1e-5 到 5e-5 的学习率,可以很好地为你提供良好的结果。因此,在获得一个优于你在数据集上获得的基线的结果之前,不要进行耗时且代价高昂的超参数搜索。
一旦你拥有了一个足够好的模型,就可以开始进行一些微调。不要尝试运行数千次具有不同超参数的实验,而是比较几个具有不同值的超参数的运行,以了解哪个超参数对结果的影响最大。
如果你要调整模型本身,请保持简单,不要尝试任何你无法合理证明的事情。始终确保返回到过拟合测试,以验证你的更改是否产生了任何意外后果。
寻求帮助
希望你在本节中找到了一些帮助你解决问题的建议,但如果情况并非如此,请记住,你始终可以在 论坛 上向社区寻求帮助。
以下是一些可能会有帮助的其他资源
- Joel Grus 的“可重复性作为工程最佳实践的载体”
- Cecelia Shao 的“神经网络调试清单”
- Chase Roberts 的“如何对机器学习代码进行单元测试”
- Andrej Karpathy 的“神经网络训练秘诀”
当然,你在训练神经网络时遇到的并非所有问题都是你自己的错!如果你在 🤗 Transformers 或 🤗 Datasets 库中遇到了一些看起来不对劲的问题,你可能遇到了 bug。你绝对应该告诉我们所有相关信息,在下一节中,我们将详细解释如何做到这一点。