在本教程中,您将学习在训练自己的自定义深度神经网络时,验证损失可能低于训练损失的三个主要原因。
我的验证损失低于训练损失!
怎么可能呢?
但是,事实证明,上述情况都不是——我的验证损失确实比我的训练损失低。
要了解您的验证loss可能低于训练loss的三个主要原因,请继续阅读!
在本教程的第一部分中,我们将讨论神经网络中“loss”的概念,包括loss代表什么以及我们为什么对其进行测量。
在此,我们将实现一个基础的CNN和训练脚本,然后使用新近实现的CNN进行一些实验(这将使我们的验证损失低于我们的训练损失)。
根据我们的结果,我将解释您的验证loss可能低于训练loss的三个主要原因。
[1] 机器/深度学习的中的“loss”是什么?为什么我的验证loss低于训练loss?
在最基本的层次上,loss函数可量化给定预测变量对数据集中输入数据点进行分类的“好”或“坏”程度。
loss越小,分类器在建模输入数据和输出目标之间的关系方面的工作就越好。
就是说,在某种程度上我们可以过度拟合我们的模型-通过过于紧密地建模训练数据(modeling the training data too closely),我们的模型将失去泛化的能力。
因此,我们寻求:
这是一种平衡,我们选择loss函数和模型优化器会极大地影响最终模型的质量,准确性和通用性。
典型的损失函数(也称为“目标函数”或“评分函数”)包括:
对loss函数的全面回顾不在本文的范围内,但就目前而言,只需了解对于大多数任务:
要了解在训练自己的自定义神经网络时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,所以今天我们不会详细讨论。
pyimagesearch.com/2019/
今天的训练脚本将生成一个training.pickle文件,其中包含训练精度/loss历史记录。在下面的原因部分中,我们将使用plot_shift.py将训练loss图移动半个epoch,以证明当验证loss低于训练loss时,测量loss的时间起作用。现在让我们深入探讨三个原因来回答这个问题:“为什么我的验证loss比训练loss低?“。
[2] Aurélien在他的Twitter提要上回答了一个问题:“大家都想知道为什么验证loss>训练loss吗?”。第一个原因是在训练过程中应用了正则化,但在验证/测试过程中未进行正则化。
在训练深度神经网络时,我们经常应用正则化来帮助我们的模型:
正则化方法通常会牺牲训练准确性来提高验证/测试准确性——在某些情况下,可能导致您的验证loss低于训练loss。
其次,请记住,在验证/测试时不应用诸如dropout之类的正则化方法。
作为的Aurelien显示在图2中,原因验证loss应正则化(例如,在验证/测试时应用dropout)可以让你的训练/验证loss曲线看起来更相似。
[3] 验证loss的原因2有时小于训练损失,这与进行测量的时间有关
您可能会看到验证loss低于训练loss的第二个原因是由于如何测量和报告loss值:
在整个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曲线看起来更加相似。
我们的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:
# 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,使训练/验证曲线与未移动(顶部)图更加相似。
[5] 考虑如何获取/生成验证集。常见的错误可能导致验证loss少于训练loss。
验证loss低于训练loss的最终最常见原因是由于数据本身分布的问题。
考虑如何获取验证集:
每位深度学习从业者在其职业中都至少犯过一次以上错误。
是的,它确实会令人尴尬-但这很重要-确实会发生,所以现在就花点时间研究您的代码。
[6] 如果您想知道为什么验证损失低于训练loss,也许您没有“足够努力地训练”。
Aurélien在推文中没有提及的一个方面是“足够努力地训练(training hard enough)”的概念。
在训练深度神经网络时,我们最大的担心几乎总是过拟合——为了避免过拟合,我们引入了正则化技术(在上面的原因1中进行了讨论)。我们用以下形式应用正则化:
我们的学习率也趋于保守一些,以确保我们的模型不会在亏损形势下超越亏损较低的领域。
一切都很好,但是有时候我们最终会过度规范我们的模型 (over-regularizing our models)。
如果您经历了验证loss低于上述详细说明的训练loss的所有三个原因,则可能是您的模型over-regularized了。通过以下方法开始放宽正则化约束:
您还应该尝试以更高的学习率进行训练,因为您可能对此过于保守。
今天的教程深受作者AurélienGeron的以下推文启发。
在帖子中,Aurélien简洁明了地解释了训练深度神经网络时验证损失可能低于训练损失的三个原因:
希望这有助于消除对为什么您的验证损失可能低于培训损失的困惑!
当我刚开始研究机器学习和神经网络时,对我来说无疑是一个摇头丸,直到中级大学才使我确切地了解了发生这种情况的原因——当时的解释都没有Aurélien的清楚和简洁。
原文链接:https://www.pyimagesearch.com/2019/10/14/why-is-my-validation-loss-lower-than-my-training-loss/