在本教程中,您将学习在训练自己的自定义深度神经网络时,验证损失可能低于训练损失的三个主要原因。
我的验证损失低于训练损失!
怎么可能呢?
- 我是否意外地将训练和验证loss绘图的标签切换了? 潜在地。 我没有像matplotlib这样的绘图库,因此将丢失日志通过管道传输到CSV文件,然后在Excel中进行绘图。 绝对容易发生人为错误。
- 我的代码中有错误吗? 几乎可以确定。 我同时在自学Java和机器学习-该代码中肯定存在某种错误。
- 我只是因为太疲倦而无法理解我的大脑吗? 也很有可能。 我一生中的睡眠时间不多,很容易错过一些明显的事情。
但是,事实证明,上述情况都不是——我的验证损失确实比我的训练损失低。
要了解您的验证loss可能低于训练loss的三个主要原因,请继续阅读!
为什么我的验证loss低于训练loss?
在本教程的第一部分中,我们将讨论神经网络中“loss”的概念,包括loss代表什么以及我们为什么对其进行测量。
在此,我们将实现一个基础的CNN和训练脚本,然后使用新近实现的CNN进行一些实验(这将使我们的验证损失低于我们的训练损失)。
根据我们的结果,我将解释您的验证loss可能低于训练loss的三个主要原因。
训练神经网络时的“loss”是什么?
[1] 机器/深度学习的中的“loss”是什么? 为什么我的验证loss低于训练loss?
在最基本的层次上,loss函数可量化给定预测变量对数据集中输入数据点进行分类的“好”或“坏”程度。
loss越小,分类器在建模输入数据和输出目标之间的关系方面的工作就越好。
就是说,在某种程度上我们可以过度拟合我们的模型-通过过于紧密地建模训练数据(modeling the training data too closely),我们的模型将失去泛化的能力。
因此,我们寻求:
- 尽可能降低loss,从而提高模型精度。
- 尽可能快地这样子做,并减少超参数的更新/实验次数。
- 所有这些都没有过度拟合我们的网络,也没有将训练数据建模得过于紧密。。
这是一种平衡,我们选择loss函数和模型优化器会极大地影响最终模型的质量,准确性和通用性。
典型的损失函数(也称为“目标函数”或“评分函数”)包括:
- Binary cross-entropy
- Categorical cross-entropy
- Sparse categorical cross-entropy
- Mean Squared Error (MSE)
- Mean Absolute Error (MAE)
- Standard Hinge
- Squared Hinge
对loss函数的全面回顾不在本文的范围内,但就目前而言,只需了解对于大多数任务:
- loss衡量你的模型的“好(goodness)”
- loss越小越好
- 但你要小心别过拟合
要了解在训练自己的自定义神经网络时loss函数的作用,请确保:
- 阅读参数化学习和线性分类简介。https://www.pyimagesearch.com/2016/08/22/an-intro-to-linear-classification-with-python/
- 请阅读以下有关SoftMax分类器的教程。https://www.pyimagesearch.com/2016/09/12/softmax-classifiers-explained/
- 关于多类SVM损失,请参阅本指南。https://www.pyimagesearch.com/2016/09/05/multi-class-svm-loss/
文件结构
从那里,通过tree命令检查项目/目录结构:
$ tree --dirsfirst
.
├── pyimagesearch
│ ├── __init__.py
│ └── minivggnet.py
├── fashion_mnist.py
├── plot_shift.py
└── training.pickle
1 directory, 5 files
今天我们将使用一个称为MiniVGGNet的更小版本的vggnet。pyimagesearch模块包括这个CNN。
我们的fashion_mnist.py脚本在fashion MNIST数据集上训练MiniVGGNet。我在之前的一篇博文中写过关于在时尚mnist上训练MiniVGGNet,所以今天我们不会详细讨论。
https://www.pyimagesearch.com/2019/02/11/fashion-mnist-with-keras-and-deep-learning/
今天的训练脚本将生成一个training.pickle文件,其中包含训练精度/loss历史记录。在下面的原因部分中,我们将使用plot_shift.py将训练loss图移动半个epoch,以证明当验证loss低于训练loss时,测量loss的时间起作用。现在让我们深入探讨三个原因来回答这个问题:“为什么我的验证loss比训练loss低?“。
原因1:在训练中应用正则化,但在验证/测试中未应用正则化
[2] Aurélien在他的Twitter提要上回答了一个问题:“大家都想知道为什么验证loss>训练loss吗?”。 第一个原因是在训练过程中应用了正则化,但在验证/测试过程中未进行正则化。
在训练深度神经网络时,我们经常应用正则化来帮助我们的模型:
- 获得更高的验证/测试精度
- 理想情况下,为了更好地泛化验证和测试集之外的数据
正则化方法通常会牺牲训练准确性来提高验证/测试准确性——在某些情况下,可能导致您的验证loss低于训练loss。
其次,请记住,在验证/测试时不应用诸如dropout之类的正则化方法。
作为的Aurelien显示在图2中,原因验证loss应正则化(例如,在验证/测试时应用dropout)可以让你的训练/验证loss曲线看起来更相似。
原因2:训练loss是在每个epoch测量的,而验证loss是在每个epoch后测量的
[3] 验证loss的原因2有时小于训练损失,这与进行测量的时间有关
您可能会看到验证loss低于训练loss的第二个原因是由于如何测量和报告loss值:
- 训练loss在每个epoch过程中测量的
- 而验证loss是在每个epoch后测量的
在整个epoch内,您的训练loss将不断得到报告;但是,仅在当前训练epoch完成后,才根据验证集计算验证指标。
这意味着,平均而言,训练loss要提前半个epoch来衡量。
如果您将训练loss向左移动半个epoch,您会发现训练和验证loss值之间的差距要小得多。
有关此行为的示例,请阅读以下部分。
执行我们的训练脚本
我们将实现一个简单的Python脚本,以在Fashion MNIST数据集上训练类似于VGG的小型网络(称为MiniVGGNet)。在训练期间,我们会将训练和验证loss保存到磁盘中。然后,我们将创建一个单独的Python脚本,以比较未变动和变动后的loss图。
让我们开始执行loss脚本:
# import the necessary packages
from pyimagesearch.minivggnet import MiniVGGNet
from sklearn.metrics import classification_report
from tensorflow.keras.optimizers import SGD
from tensorflow.keras.datasets import fashion_mnist
from tensorflow.keras.utils import to_categorical
import argparse
import pickle
# construct the argument parser and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--history", required=True,
help="path to output training history file")
args = vars(ap.parse_args())
第2-8行导入了我们所需的包,模块,类和函数。 即,我们导入MiniVGGNet(我们的CNN),fashion_mnist(我们的数据集)和pickle(确保可以序列化我们的训练历史以使用单独的脚本来处理绘图)。
命令行参数--history指向单独的.pickle文件,该文件将很快包含我们的训练历史记录(第11-14行)。
然后,我们初始化一些超参数,即我们要训练的epoch数,初始学习率和批量大小:
# initialize the number of epochs to train for, base learning rate,
# and batch size
NUM_EPOCHS = 25
INIT_LR = 1e-2
BS = 32
然后,我们继续加载和预处理我们的Fashion MNIST数据:
# grab the Fashion MNIST dataset (if this is your first time running
# this the dataset will be automatically downloaded)
print("[INFO] loading Fashion MNIST...")
((trainX, trainY), (testX, testY)) = fashion_mnist.load_data()
# we are using "channels last" ordering, so the design matrix shape
# should be: num_samples x rows x columns x depth
trainX = trainX.reshape((trainX.shape[0], 28, 28, 1))
testX = testX.reshape((testX.shape[0], 28, 28, 1))
# scale data to the range of [0, 1]
trainX = trainX.astype("float32") / 255.0
testX = testX.astype("float32") / 255.0
# one-hot encode the training and testing labels
trainY = to_categorical(trainY, 10)
testY = to_categorical(testY, 10)
# initialize the label names
labelNames = ["top", "trouser", "pullover", "dress", "coat",
"sandal", "shirt", "sneaker", "bag", "ankle boot"]
第3-13行加载并预处理训练/验证数据。
第16和17行将我们的类别标签二值化,而第20和21行则列出了人类可读的类别标签名称,以供日后分类报告之用。
从这里,我们拥有编译和训练Fashion MNIST数据上的MiniVGGNet模型所需的一切:
# initialize the optimizer and model
print("[INFO] compiling model...")
opt = SGD(lr=INIT_LR, momentum=0.9, decay=INIT_LR / NUM_EPOCHS)
model = MiniVGGNet.build(width=28, height=28, depth=1, classes=10)
model.compile(loss="categorical_crossentropy", optimizer=opt,
metrics=["accuracy"])
# train the network
print("[INFO] training model...")
H = model.fit(trainX, trainY,
validation_data=(testX, testY),
batch_size=BS, epochs=NUM_EPOCHS)
第3-6行初始化并编译MiniVGGNet模型。
然后,第10-12行拟合/训练模型。
从这里我们将评估我们的模型并序列化我们的训练历史:
# make predictions on the test set and show a nicely formatted
# classification report
preds = model.predict(testX)
print("[INFO] evaluating network...")
print(classification_report(testY.argmax(axis=1), preds.argmax(axis=1),
target_names=labelNames))
# serialize the training history to disk
print("[INFO] serializing training history...")
f = open(args["history"], "wb")
f.write(pickle.dumps(H.history))
f.close()
第3-6行对测试集进行预测,并将分类报告打印到终端。
10-12行将我们的训练准确性/损失历史序列化为.pickle文件。 我们将在单独的Python脚本中使用训练历史记录来绘制损耗曲线,包括一个显示二分之一epoch偏移的图。
从那里打开一个终端,然后执行以下命令:
$ python fashion_mnist.py --history training.pickle
[INFO] loading Fashion MNIST...
[INFO] compiling model...
[INFO] training model...
Train on 60000 samples, validate on 10000 samples
Epoch 1/25
60000/60000 [==============================] - 200s 3ms/sample - loss: 0.5433 - accuracy: 0.8181 - val_loss: 0.3281 - val_accuracy: 0.8815
Epoch 2/25
60000/60000 [==============================] - 194s 3ms/sample - loss: 0.3396 - accuracy: 0.8780 - val_loss: 0.2726 - val_accuracy: 0.9006
Epoch 3/25
60000/60000 [==============================] - 193s 3ms/sample - loss: 0.2941 - accuracy: 0.8943 - val_loss: 0.2722 - val_accuracy: 0.8970
Epoch 4/25
60000/60000 [==============================] - 193s 3ms/sample - loss: 0.2717 - accuracy: 0.9017 - val_loss: 0.2334 - val_accuracy: 0.9144
Epoch 5/25
60000/60000 [==============================] - 194s 3ms/sample - loss: 0.2534 - accuracy: 0.9086 - val_loss: 0.2245 - val_accuracy: 0.9194
...
Epoch 21/25
60000/60000 [==============================] - 195s 3ms/sample - loss: 0.1797 - accuracy: 0.9340 - val_loss: 0.1879 - val_accuracy: 0.9324
Epoch 22/25
60000/60000 [==============================] - 194s 3ms/sample - loss: 0.1814 - accuracy: 0.9342 - val_loss: 0.1901 - val_accuracy: 0.9313
Epoch 23/25
60000/60000 [==============================] - 193s 3ms/sample - loss: 0.1766 - accuracy: 0.9351 - val_loss: 0.1866 - val_accuracy: 0.9320
Epoch 24/25
60000/60000 [==============================] - 193s 3ms/sample - loss: 0.1770 - accuracy: 0.9347 - val_loss: 0.1845 - val_accuracy: 0.9337
Epoch 25/25
60000/60000 [==============================] - 194s 3ms/sample - loss: 0.1734 - accuracy: 0.9372 - val_loss: 0.1871 - val_accuracy: 0.9312
[INFO] evaluating network...
precision recall f1-score support
top 0.87 0.91 0.89 1000
trouser 1.00 0.99 0.99 1000
pullover 0.91 0.91 0.91 1000
dress 0.93 0.93 0.93 1000
coat 0.87 0.93 0.90 1000
sandal 0.98 0.98 0.98 1000
shirt 0.83 0.74 0.78 1000
sneaker 0.95 0.98 0.97 1000
bag 0.99 0.99 0.99 1000
ankle boot 0.99 0.95 0.97 1000
accuracy 0.93 10000
macro avg 0.93 0.93 0.93 10000
weighted avg 0.93 0.93 0.93 10000
[INFO] serializing training history...
检查工作目录的内容,您应该有一个名为training.pickle的文件-该文件包含我们的训练历史日志。
$ ls *.pickle
training.pickle
在下一节中,我们将学习如何绘制这些值并将训练信息向左移动半个epoch,从而使我们的训练/验证loss曲线看起来更加相似。
平移我们的训练loss值
我们的plot_shift.py脚本用于绘制来自fashion_mnist.py的训练历史记录。 使用此脚本,我们可以研究将训练损失向左移动半个世纪如何使我们的训练/验证图看起来更相似。
打开plot_shift.py文件并插入以下代码:
# import the necessary packages
import matplotlib.pyplot as plt
import numpy as np
import argparse
import pickle
# construct the argument parser and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--input", required=True,
help="path to input training history file")
args = vars(ap.parse_args())
第2-5行导入matplotlib(用于绘制),NumPy(用于简单的数组创建操作),argparse(命令行参数)和pickle(加载我们的序列化训练历史记录)。
第8-11行解析--input命令行参数,该参数指向磁盘上的.pickle训练历史记录文件。
让我们继续加载数据并初始化绘图:
# load the training history
H = pickle.loads(open(args["input"], "rb").read())
# determine the total number of epochs used for training, then
# initialize the figure
epochs = np.arange(0, len(H["loss"]))
plt.style.use("ggplot")
(fig, axs) = plt.subplots(2, 1)
第2行使用--input命令行参数加载序列化的训练历史记录.pickle文件。
第6行为我们的x轴腾出了空间,该空间从零到训练历史中的epoch数。
第7行和第8行将我们的绘图图设置为同一图像中的两个堆叠绘图:
- top plot将按原样包含loss曲线。
- 另一方面,bottom plot将包括训练loss(但不包括验证loss)的偏移。 训练loss将按照Aurélien的推文向左移动半个epoch。 然后,我们将能够观察绘图线的排列是否更加紧密。
让我们生成top plot:
# plot the *unshifted* training and validation loss
plt.style.use("ggplot")
axs[0].plot(epochs, H["loss"], label="train_loss")
axs[0].plot(epochs, H["val_loss"], label="val_loss")
axs[0].set_title("Unshifted Loss Plot")
axs[0].set_xlabel("Epoch #")
axs[0].set_ylabel("Loss")
axs[0].legend()
然后绘制bottom plot:
# plot the *shifted* training and validation loss
axs[1].plot(epochs - 0.5, H["loss"], label="train_loss")
axs[1].plot(epochs, H["val_loss"], label="val_loss")
axs[1].set_title("Shifted Loss Plot")
axs[1].set_xlabel("Epoch #")
axs[1].set_ylabel("Loss")
axs[1].legend()
# show the plots
plt.tight_layout()
plt.show()
请注意,在第2行上,训练损失向左移动了0.5个epoch,即本例的核心。
现在,让我们分析我们的训练/验证图。
打开一个终端并执行以下命令:
python plot_shift.py --input training.pickle
[4] 将训练损失图向左移动1/2个epoch,可以得到更多类似的图。 显然,测量时间回答了一个问题:“为什么我的验证loss低于训练loss?”。
如您所见,将训练loss值向左(底部)移动一个半个epoch,使训练/验证曲线与未移动(顶部)图更加相似。
原因#3:验证集可能比训练集更容易(否则可能会泄漏(leaks))
[5] 考虑如何获取/生成验证集。 常见的错误可能导致验证loss少于训练loss。
验证loss低于训练loss的最终最常见原因是由于数据本身分布的问题。
考虑如何获取验证集:
- 您可以保证验证集是从与训练集相同的分布中采样的吗?
- 您确定验证示例与您的训练图像一样具有挑战性吗?
- 您是否可以确保没有“数据泄漏”(即训练样本与验证/测试样本意外混入)?
- 您是否确信自己的代码正确创建了训练集,验证集和测试集?
每位深度学习从业者在其职业中都至少犯过一次以上错误。
是的,它确实会令人尴尬-但这很重要-确实会发生,所以现在就花点时间研究您的代码。
BONUS: Are you training hard enough?
[6] 如果您想知道为什么验证损失低于训练loss,也许您没有“足够努力地训练”。
Aurélien在推文中没有提及的一个方面是“足够努力地训练(training hard enough)”的概念。
在训练深度神经网络时,我们最大的担心几乎总是过拟合——为了避免过拟合,我们引入了正则化技术(在上面的原因1中进行了讨论)。我们用以下形式应用正则化:
- Dropout
- L2权重衰减
- 减少模型容量(即更浅的模型)
我们的学习率也趋于保守一些,以确保我们的模型不会在亏损形势下超越亏损较低的领域。
一切都很好,但是有时候我们最终会过度规范我们的模型 (over-regularizing our models)。
如果您经历了验证loss低于上述详细说明的训练loss的所有三个原因,则可能是您的模型over-regularized了。通过以下方法开始放宽正则化约束:
- 降低L2权重衰减强度。
- 减少申请的dropout数量。
- 增加模型容量(即,使其更深)。
您还应该尝试以更高的学习率进行训练,因为您可能对此过于保守。
总结
今天的教程深受作者AurélienGeron的以下推文启发。
在线程中,Aurélien简洁明了地解释了训练深度神经网络时验证损失可能低于训练损失的三个原因:
- 原因1:在训练期间应用正则化,但在验证/测试期间未进行正则化。如果在验证/测试期间添加正则化损失,则损失值和曲线将看起来更加相似。
- 原因2:训练损失是在每个epoch期间测量的,而验证损失是在每个epoch后测量的。平均而言,训练损失的测量时间是前一个时期的1/2。如果将训练损失曲线向左移动半个epoch,则损失会更好。
- 原因3:您的验证集可能比训练集更容易,或者代码中的数据/错误泄漏。确保您的验证集大小合理,并且是从与您的训练集相同的分布(和难度)中抽取的。
- 奖励:您的模型可能over-regularizing 。尝试减少正则化约束,包括增加模型容量(即通过更多参数使其更深),减少dropout,降低L2权重衰减强度等。
希望这有助于消除对为什么您的验证损失可能低于培训损失的困惑!
英文原文链接:https://www.pyimagesearch.com/2019/10/14/why-is-my-validation-loss-lower-than-my-training-loss/