调试训练管道
假如你已经尽可能地遵循 第七章 中的建议,编写了一个漂亮的脚本来训练或微调给定任务的模型。 但是当你启动命令 model.fit()
时,你得到一个错误😱! 或者更糟的是虽然看起来一切似乎都正常,训练运行没有错误,但生成的模型却很糟糕。 在本节中,我们将向你展示如何调试此类问题。
调试训练管道
当你在运行 model.fit()
中遇到错误时,它有可能来自多个不同的来源,因为 Trainer
通常将之前的许多工作汇集到一起。比如有可能是你的数据集有问题,或者可能是在尝试将数据集的元素汇集在一起做批处理时出现问题,又或者模型代码、损失函数或优化器中存在问题,另外即使训练过程一切顺利,如果选取的评估指标有问题,评估过程中仍然可能出现错误。
所以调试 model.fit()
中出现的错误的最佳方法是手动检查整个管道,看看哪里出了问题。
这里我们将使用以下脚本在 MNLI 数据集上微调 DistilBERT 模型:
from datasets import load_dataset
from transformers import (
AutoTokenizer,
TFAutoModelForSequenceClassification,
)
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)
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
——这是已知的 UX 问题,可以忽略。 如果你在 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
对象一次给我们整个 batch
的数据,并且不支持索引,所以不能使用 train_dataset[0]
来获取 batch
中的第一个数据。 但是我们可以先向它请求一个 batch
:
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
参数,并且确保 to_tf_dataset()
输出真实的标签来提供梯度,但是我们指定的损失还有一个问题。即使在这个问题上可以进行训练,学习速度仍然会非常慢,并且 loss 会达到一个较高的值。你能找出问题在哪里吗?
如果你卡住了,这是一个 ROT13 编码的提示(如果你不熟悉 ROT13,可以在这里解码。):Vs lbh ybbx ng gur bhgchgf bs FrdhraprPynffvsvpngvba zbqryf va Genafsbezref, gurve svefg bhgchg vf ybtvgf
. Jung ner ybtvgf?(查看 Transformers 中 SequenceClassification
模型的输出,第一个输出是“logits”。思考什么是 logits ?它所代表的实际含义是什么?)
还有一个提示:
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?(当训练模型时直接使用字符串告诉模型指定优化器、激活函数或损失函数时,Keras 会使用优化器、激活函数或损失函数参数值的默认值。
SparseCategoricalCrossentropy
损失函数有哪些参数,它们的默认值是什么?
)
现在让我们尝试继续进行训练。 如今已经得到梯度,所以希望(此处播放令人不安的音乐)只需调用model.fit()
,一切都会正常工作!
246/24543 [..............................] - ETA: 15:52 - loss: nan
哦不。
nan
不是一个正常的损失值。我们已经检查了我们的数据,看起来一切正常。如果问题不在数据,那么我们接下来检查哪里呢?很明显的下一步是…
检查模型
model.fit()
是 Keras 中一个很方便的函数,但这个函数一次性做了很多事情,这使准确定位问题发生的位置变得更加棘手。 如果你正在调试模型,那么一个明智的策略是考虑只将一个批次传递给模型,并查看该批次的详细输出。 如果模型抛出错误,另一个非常有用的提示是可以将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
值:
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
值,这些出现 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])
让我们看看这些来自样本的输入id:
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
时,我们会得到loss为 nan
,现在是时候检查一下模型的标签了:
model.config.num_labels
2
这表明模型认为只有两个类,但是标签的取值范围是从 0 到 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
,而 batch_size
似乎不太可能是问题的原因。不过,不要被表象迷惑;超参数始终存在,如果你看不到它们,那只意味着你不知道它们默认被设置成了什么。这里强调一个关于 Keras 的关键点:如果使用字符串设置损失函数、优化器或激活函数,“它的所有参数都将设置为默认值”。这意味着即使使用字符串非常方便,但在这样做时应该非常小心,因为它很容易隐藏一些关键的问题。(尝试上面的可选挑战的人应该注意到了这一点。)
在这个例子中,我们在哪里使用了字符串参数呢 ?最初我们使用字符串设置了损失函数,但现在我们已经去掉了。除此之外,我们还使用字符串设置了优化器。其中是否隐藏了什么信息呢?让我们查看一下优化器的参数:
这里需要注意学习率。当我们只使用字符串“adam”时,将使用默认的学习率 0.001(即 1e-3)。这对于transormer模型来说太高了,一般来说,我们建议尝试学习率在 1e-5 到 1e-4 之间的值;这比默认的值小 10倍 到 100倍。这听起来可能是一个导致loss下降很缓慢的原因,所以让我们尝试减小学习率。为此我们需要导入optimizer
对象,在optimizer
对象中设置学习率。让我们从checkpoint
重新初始化模型,以防刚刚过高的学习率的训练破坏了权重:
from tensorflow.keras.optimizers import Adam
model = TFAutoModelForSequenceClassification.from_pretrained(model_checkpoint)
model.compile(optimizer=Adam(5e-5))
💡除了从 Keras 中导入 Adam
优化器你还可以从🤗 Transformers 中导入 create_optimizer()
函数,这将提供具有正确的权重衰减和学习率预热和衰减的 AdamW 优化器。 此优化器通常会比使用默认 Adam
优化器的效果稍好一些。
现在,我们可以使用改进后的学习率来拟合模型:
model.fit(train_dataset)
319/24543 [..............................] - ETA: 16:07 - loss: 0.9718
现在训练终于看起来奏效了。当你的模型在正常训练但损失没有下降,同时确定数据没问题时,可以检查学习率和权重衰减等超参数,其中任何一个设置得太高很可能导致训练的损失值居高不下。
其他潜在问题
我们已经涵盖了上面脚本中存在的所有问题,但是还有其他一些常见错误可能会遇到。让我们来看一个(不太完整的)列表。
处理内存不足错误
内存不足指的是OOM when allocating tensor
之类的错误——OOM 是out of memory
的缩写。 在处理大型语言模型时,这是一个非常常见的错误。 如遇此种情况,可以将 batch size 减半并重试。 但有些尺寸非常大,比如全尺寸 GPT-2 的参数为 1.5B,这意味着你将需要 6 GB 的内存来存储模型,另外需要 6 GB 的内存用于梯度下降! 无论你使用什么 batch size ,训练完整的 GPT-2 模型通常都需要超过 20 GB 的 VRAM,然而这只有少数 GPU 才可以做到。 像distilbert-base-cased
这样更轻量级的模型更容易训练,并且训练速度也更快。
在课程的下一部分中,我们将介绍更先进的技术,这些技术可以帮助你减少内存占用并微调超大的模型。
TensorFlow 🦛饿饿
TensorFlow 的一个与众不同的特点是它会在加载模型或进行任何训练后会立即向系统请求所需的所有显存,然后在内部再将请求到的显存划分给不同的模块。这与其他框架的行为不同,例如 PyTorch根据 CUDA 的需要动态分配内存,而不是在内部进行。 TensorFlow 方法的一个优点是当内存耗尽时,它通常会给出可以帮助我们定位问题的错误信息,并且可以从错误状态恢复而不会导致整个 CUDA 内核崩溃。但是,如果同时运行两个 TensorFlow 进程,那么势必会因为预先分配内存导致内存爆炸。
如果你在 Google Colab 上运行则无需担心这一点,但如果在本地运行,则应该小心是否成功释放了显存。特别要注意,关闭Notebook选项卡并不一定会彻底关闭该 Notebook !需要选择正在运行的 Notebook (带有绿色图标的 Notebook )并在目录列表中手动关闭它们。任何使用 TensorFlow 的正在运行的 Notebook 都可能占用大量显存,这意味着启动的任何新的使用TensorFlow 的 Notebook 可能会遇到一些非常奇怪的问题。
如果在运行正确的代码后收到有关 CUDA、BLAS 或 CUBLAS 的错误,那么问题来源通常是类似的,可以使用类似 nvidia-smi
的命令来检查。当关闭或重新启动当前 Notebook 时,要查看大部分显存是否空闲或者仍在使用,如果仍在使用中,则代表仍然有其他东西在占用内存。
再次检查你的数据
理论上,只有数据中存在可以学习的知识,模型才会学到一些知识。如果数据已经被损坏了或标签是随机的,那么模型很可能无法从数据集中获得任何知识。这里一个有用的工具是解决该问题的一个有用的方法是使用 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]
在查看数据时,可以对以下问题进行检查。
- 解码后的文本数据你是否可以正常阅读和理解?
- 你认同这些标签对于文本的描述吗?
- 有没有一个标签比其他标签更常见?
- 如果模型预测的答案是随机的或总是相同的,那么 loss/ 评估指标应该是多少,是否模型根本没能学到任何知识?
在检查数据后,可以检查模型的一些预测并对其进行解码。 如果模型总是预测同样的类别,那么可能是因为这个类别在数据集中的比例比较高(针对分类问题); 过采样稀有类等技术可能会对解决这种问题有帮助。或者,这也可能是由训练的设置(如错误的超参数设置)引起的。
如果在初始模型上获得的 loss/ 评估指标与预估的随机时预测的 loss/ 评估指标非常不同,则应该仔细检查 loss/ 评估指标的计算方式,因为其中可能存在错误。如果使用多个 loss,并将其相加计算最后的loss,则应该确保它们具有相同的比例大小。
当你确定你的数据是完美的之后,则可以通过一个简单的过拟合测试来查看模型是否能够用其进行训练。
在一个 batch 上过拟合模型
过拟合通常是在训练时尽量避免的事情,因为这意味着模型没有识别并学习我们想要的一般特征,而只是记住了训练样本。 但一遍又一遍地尝试在一个 batch 上训练模型可以检查数据集所描述的问题是否可以通过训练的模型来解决, 它还将帮助查看你的初始学习率是否太高了。
在定义 Trainer 后,只需获取一个 batch 训练数据,然后将这个 batch 视为整个数据集,并在上面拟合大量 epoch 即可:
for batch in train_dataset:
break
# 确保已经运行了 model.compile() 并设置了优化器和损失/指标
model.fit(batch, epochs=20)
💡 如果训练数据不平衡,请确保构建的这个 batch 包含训练数据中所有的标签。
生成的模型在一个 batch 上应该有接近完美的结果,损失迅速下降到 0(或你正在使用的损失的最小值)。
如果你没有设法让你的模型获得这样的完美结果,这意味着构建问题的方式或数据有问题。只有当你通过了过拟合测试,才能确定你的模型理论上确实可以学到一些东西。
⚠️ 在此测试之后,你需要创建模型和 Trainer
,因为经过过拟合测试的模型可能无法恢复正常的参数范围,从而无法在完整数据集上学到有用的知识。
在你有第一个 baseline 模型之前不要调整任何东西
超参数调整总是被是为机器学习中最难的部分,但这只是帮助你在指标上获得一点点提升的最后一步。 例如,使用学习率为 1e-3 的Adam 优化器训练 Transformer 模型时,当然会使学习进行得非常缓慢或完全停止,在大多数时候,合理的超参数会带来更好的结果, 如 1e-5 到 5e-5 的学习率。不过,在你有了 baseline 模型之前,请不要试图进行耗时且昂贵的超参数搜索。
在有一个足够好的模型后,就可以开始微调了。尽量避免使用不同的超参数进行一千次运行,而要比较一个超参数取不同数值的几次运行,从而了解哪个超参数的影响最大,从而理解超参数值的改变与于模型训练之间的关系。
如果正在调整模型本身,请保持简单,不要直接对模型进行非常复杂的无法理解或者证明的修改,要一步步修改,同时尝试理解和证明这次修改对模型产生的影响,并且 确保通过过拟合测试来验证修改没有引发其他的问题。
请求帮忙
希望你会在本课程中找到一些可以帮助你解决问题的建议,除此之外可以随时在 论坛 上向社区提问。
以下是一些可能有用的额外资源:
-Joel Grus 的 “作为工程最佳实践工具的再现性”
- Cecelia Shao 的 “神经网络调试清单”
- Chase Roberts 的 “如何对机器学习代码进行单元测试”
- Andrej Karpathy 的 “训练神经网络的秘诀”
当然,并非你在训练神经网络时遇到的每个问题都是你自己的错!如果你在 🤗 Transformers 或 🤗 Datasets 库中遇到看起来不正确的内容而导致无法解决的问题,请及时告知我们。在下一节中,我们将准确解释如何进行这一步。
< > Update on GitHub