前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一个值得深思的问题?为什么验证集的loss会小于训练集的loss

一个值得深思的问题?为什么验证集的loss会小于训练集的loss

作者头像
AI算法与图像处理
发布2019-11-07 16:07:52
7K0
发布2019-11-07 16:07:52
举报

在本教程中,您将学习在训练自己的自定义深度神经网络时,验证损失可能低于训练损失的三个主要原因。

我的验证损失低于训练损失!

怎么可能呢?

  • 我是否意外地将训练和验证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),我们的模型将失去泛化的能力。

因此,我们寻求:

  1. 尽可能降低loss,从而提高模型精度。
  2. 尽可能快地这样子做,并减少超参数的更新/实验次数。
  3. 所有这些都没有过度拟合我们的网络,也没有将训练数据建模得过于紧密。。

这是一种平衡,我们选择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命令检查项目/目录结构:

代码语言:javascript
复制
  $ 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低?“。

原因1:在训练中应用正则化,但在验证/测试中未应用正则化

[2] Aurélien在他的Twitter提要上回答了一个问题:“大家都想知道为什么验证loss>训练loss吗?”。第一个原因是在训练过程中应用了正则化,但在验证/测试过程中未进行正则化。

在训练深度神经网络时,我们经常应用正则化来帮助我们的模型:

  1. 获得更高的验证/测试精度
  2. 理想情况下,为了更好地泛化验证和测试集之外的数据

正则化方法通常会牺牲训练准确性来提高验证/测试准确性——在某些情况下,可能导致您的验证loss低于训练loss。

其次,请记住,在验证/测试时不应用诸如dropout之类的正则化方法。

作为的Aurelien显示在图2中,原因验证loss应正则化(例如,在验证/测试时应用dropout)可以让你的训练/验证loss曲线看起来更相似。

原因2:训练loss是在每个epoch测量的,而验证loss是在每个epoch后测量的

[3] 验证loss的原因2有时小于训练损失,这与进行测量的时间有关

您可能会看到验证loss低于训练loss的第二个原因是由于如何测量和报告loss值:

  1. 训练loss在每个epoch过程中测量的
  2. 而验证loss是在每个epoch后测量的

在整个epoch内,您的训练loss将不断得到报告;但是,仅在当前训练epoch完成后,才根据验证集计算验证指标。

这意味着,平均而言,训练loss要提前半个epoch来衡量。

如果您将训练loss向左移动半个epoch,您会发现训练和验证loss值之间的差距要小得多。

有关此行为的示例,请阅读以下部分。

执行我们的训练脚本

我们将实现一个简单的Python脚本,以在Fashion MNIST数据集上训练类似于VGG的小型网络(称为MiniVGGNet)。在训练期间,我们会将训练和验证loss保存到磁盘中。然后,我们将创建一个单独的Python脚本,以比较未变动和变动后的loss图。

让我们开始执行loss脚本:

代码语言:javascript
复制
  # 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数,初始学习率和批量大小:

代码语言:javascript
复制
  # 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数据:

代码语言:javascript
复制
  # 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模型所需的一切:

代码语言:javascript
复制
  # 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行拟合/训练模型。

从这里我们将评估我们的模型并序列化我们的训练历史:

代码语言:javascript
复制
  # 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偏移的图。

从那里打开一个终端,然后执行以下命令:

代码语言:javascript
复制
  $ 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的文件-该文件包含我们的训练历史日志。

代码语言:javascript
复制
  $ ls *.pickle
  training.pickle

在下一节中,我们将学习如何绘制这些值并将训练信息向左移动半个epoch,从而使我们的训练/验证loss曲线看起来更加相似。

平移我们的训练loss值

我们的plot_shift.py脚本用于绘制来自fashion_mnist.py的训练历史记录。使用此脚本,我们可以研究将训练损失向左移动半个世纪如何使我们的训练/验证图看起来更相似。

打开plot_shift.py文件并插入以下代码:

代码语言:javascript
复制
  # 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训练历史记录文件。

让我们继续加载数据并初始化绘图:

代码语言:javascript
复制
  # 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:

代码语言:javascript
复制
  # 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:

代码语言:javascript
复制
  # 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,即本例的核心。

现在,让我们分析我们的训练/验证图。

打开一个终端并执行以下命令:

代码语言:javascript
复制
  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. 原因1:在训练期间应用正则化,但在验证/测试期间未进行正则化。如果在验证/测试期间添加正则化损失,则损失值和曲线将看起来更加相似。
  2. 原因2:训练损失是在每个epoch期间测量的,而验证损失是在每个epoch后测量的。平均而言,训练损失的测量时间是前一个时期的1/2。如果将训练损失曲线向左移动半个epoch,则损失会更好。
  3. 原因3:您的验证集可能比训练集更容易,或者代码中的数据/错误泄漏。确保您的验证集大小合理,并且是从与您的训练集相同的分布(和难度)中抽取的。
  4. 奖励:您的模型可能over-regularizing 。尝试减少正则化约束,包括增加模型容量(即通过更多参数使其更深),减少dropout,降低L2权重衰减强度等。

希望这有助于消除对为什么您的验证损失可能低于培训损失的困惑!

当我刚开始研究机器学习和神经网络时,对我来说无疑是一个摇头丸,直到中级大学才使我确切地了解了发生这种情况的原因——当时的解释都没有Aurélien的清楚和简洁。

原文链接:https://www.pyimagesearch.com/2019/10/14/why-is-my-validation-loss-lower-than-my-training-loss/

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-11-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 AI算法与图像处理 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 为什么我的验证loss低于训练loss?
  • 训练神经网络时的“loss”是什么?
  • 文件结构
  • 原因1:在训练中应用正则化,但在验证/测试中未应用正则化
  • 原因2:训练loss是在每个epoch测量的,而验证loss是在每个epoch后测量的
  • 执行我们的训练脚本
  • 平移我们的训练loss值
  • 原因#3:验证集可能比训练集更容易(否则可能会泄漏(leaks))
  • BONUS: Are you training hard enough?
  • 总结
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档