过拟合和欠拟合

在 TensorFlow.org 上查看 在 Google Colab 中运行 在 GitHub 上查看源代码 下载笔记本

与往常一样,此示例中的代码将使用 tf.keras API,您可以在 TensorFlow Keras 指南 中了解更多信息。

在之前的两个示例中 - 文本分类预测燃油效率 - 模型在验证数据上的准确率在经过一定数量的 epoch 训练后会达到峰值,然后停滞或开始下降。

换句话说,您的模型会过拟合训练数据。学习如何处理过拟合非常重要。虽然通常可以在训练集上获得很高的准确率,但您真正想要的是开发能够很好地泛化到测试集(或它们以前从未见过的数据)的模型。

过拟合的反面是欠拟合。欠拟合发生在训练数据上还有改进空间时。这可能是由于多种原因造成的:如果模型不够强大,过度正则化,或者训练时间不够长。这意味着网络没有从训练数据中学习到相关模式。

但是,如果您训练时间过长,模型将开始过拟合并从训练数据中学习到无法泛化到测试数据的模式。您需要找到平衡点。了解如何在下面将探索的适当数量的 epoch 内进行训练是一项有用的技能。

为了防止过拟合,最好的解决方案是使用更完整的数据集进行训练。数据集应该涵盖模型预期处理的全部输入范围。只有当额外数据涵盖新的和有趣的案例时,它才可能有用。

在更完整的数据集上训练的模型自然会更好地泛化。当这不再可能时,下一个最佳解决方案是使用正则化技术。这些技术对模型可以存储的信息量和类型施加约束。如果一个网络只能负担得起记忆少量模式,那么优化过程将迫使它专注于最突出的模式,这些模式更有可能很好地泛化。

在本笔记本中,您将探索几种常见的正则化技术,并使用它们来改进分类模型。

设置

在开始之前,导入必要的包

import tensorflow as tf

from tensorflow.keras import layers
from tensorflow.keras import regularizers

print(tf.__version__)
!pip install git+https://github.com/tensorflow/docs

import tensorflow_docs as tfdocs
import tensorflow_docs.modeling
import tensorflow_docs.plots
from  IPython import display
from matplotlib import pyplot as plt

import numpy as np

import pathlib
import shutil
import tempfile
logdir = pathlib.Path(tempfile.mkdtemp())/"tensorboard_logs"
shutil.rmtree(logdir, ignore_errors=True)

希格斯数据集

本教程的目标不是做粒子物理,所以不要纠结于数据集的细节。它包含 11,000,000 个示例,每个示例包含 28 个特征和一个二进制类标签。

gz = tf.keras.utils.get_file('HIGGS.csv.gz', 'http://mlphysics.ics.uci.edu/data/higgs/HIGGS.csv.gz')
FEATURES = 28

tf.data.experimental.CsvDataset 类可用于直接从 gzip 文件读取 csv 记录,无需中间解压缩步骤。

ds = tf.data.experimental.CsvDataset(gz,[float(),]*(FEATURES+1), compression_type="GZIP")

该 csv 读取器类为每条记录返回一个标量列表。以下函数将该标量列表重新打包成一个 (特征向量,标签) 对。

def pack_row(*row):
  label = row[0]
  features = tf.stack(row[1:],1)
  return features, label

TensorFlow 在处理大量数据时效率最高。

因此,与其单独重新打包每一行,不如创建一个新的 tf.data.Dataset,它以 10,000 个示例的批次进行处理,将 pack_row 函数应用于每个批次,然后将批次重新拆分为单个记录

packed_ds = ds.batch(10000).map(pack_row).unbatch()

检查来自这个新的 packed_ds 的一些记录。

这些特征并非完全标准化,但这对于本教程来说已经足够了。

for features,label in packed_ds.batch(1000).take(1):
  print(features[0])
  plt.hist(features.numpy().flatten(), bins = 101)

为了使本教程相对简短,只使用前 1,000 个样本进行验证,使用接下来的 10,000 个样本进行训练

N_VALIDATION = int(1e3)
N_TRAIN = int(1e4)
BUFFER_SIZE = int(1e4)
BATCH_SIZE = 500
STEPS_PER_EPOCH = N_TRAIN//BATCH_SIZE

Dataset.skipDataset.take 方法使这变得很容易。

同时,使用 Dataset.cache 方法确保加载器不需要在每个 epoch 中从文件重新读取数据

validate_ds = packed_ds.take(N_VALIDATION).cache()
train_ds = packed_ds.skip(N_VALIDATION).take(N_TRAIN).cache()
train_ds

这些数据集返回单个示例。使用 Dataset.batch 方法创建适合训练的适当大小的批次。在批处理之前,还要记住对训练集使用 Dataset.shuffleDataset.repeat

validate_ds = validate_ds.batch(BATCH_SIZE)
train_ds = train_ds.shuffle(BUFFER_SIZE).repeat().batch(BATCH_SIZE)

演示过拟合

防止过拟合最简单的方法是从一个小的模型开始:一个具有少量可学习参数的模型(由层数和每层单元数决定)。在深度学习中,模型中可学习参数的数量通常被称为模型的“容量”。

直观地说,一个具有更多参数的模型将具有更多的“记忆能力”,因此能够轻松地学习训练样本与其目标之间的完美字典式映射,这种映射没有任何泛化能力,但在对以前从未见过的数据进行预测时将毫无用处。

始终牢记这一点:深度学习模型往往擅长拟合训练数据,但真正的挑战是泛化,而不是拟合。

另一方面,如果网络的记忆资源有限,它将无法轻松地学习映射。为了最小化损失,它必须学习具有更多预测能力的压缩表示。同时,如果将模型做得太小,它将难以拟合训练数据。在“容量过大”和“容量不足”之间存在平衡。

不幸的是,没有神奇的公式来确定模型的正确大小或架构(就层数或每层的大小而言)。您将不得不尝试使用一系列不同的架构。

要找到合适的模型大小,最好从相对较少的层和参数开始,然后开始增加层的大小或添加新层,直到您看到验证损失的收益递减。

从一个简单的模型开始,只使用密集连接层 (tf.keras.layers.Dense) 作为基线,然后创建更大的模型,并将它们进行比较。

训练过程

如果在训练期间逐渐降低学习率,许多模型的训练效果会更好。使用 tf.keras.optimizers.schedules 随着时间的推移降低学习率

lr_schedule = tf.keras.optimizers.schedules.InverseTimeDecay(
  0.001,
  decay_steps=STEPS_PER_EPOCH*1000,
  decay_rate=1,
  staircase=False)

def get_optimizer():
  return tf.keras.optimizers.Adam(lr_schedule)

上面的代码设置了一个 tf.keras.optimizers.schedules.InverseTimeDecay,以双曲线方式将学习率降低到 1,000 个 epoch 时为基本速率的 1/2,2,000 个 epoch 时为 1/3,依此类推。

step = np.linspace(0,100000)
lr = lr_schedule(step)
plt.figure(figsize = (8,6))
plt.plot(step/STEPS_PER_EPOCH, lr)
plt.ylim([0,max(plt.ylim())])
plt.xlabel('Epoch')
_ = plt.ylabel('Learning Rate')

本教程中的每个模型都将使用相同的训练配置。因此,以可重用的方式设置这些配置,从回调列表开始。

本教程的训练运行了多个短 epoch。为了减少日志噪音,使用 tfdocs.EpochDots,它只是为每个 epoch 打印一个 .,并在每 100 个 epoch 打印一组完整的指标。

接下来,包括 tf.keras.callbacks.EarlyStopping,以避免长时间且不必要的训练时间。请注意,此回调设置为监控 val_binary_crossentropy,而不是 val_loss。这种差异在后面很重要。

使用 callbacks.TensorBoard 为训练生成 TensorBoard 日志。

def get_callbacks(name):
  return [
    tfdocs.modeling.EpochDots(),
    tf.keras.callbacks.EarlyStopping(monitor='val_binary_crossentropy', patience=200),
    tf.keras.callbacks.TensorBoard(logdir/name),
  ]

类似地,每个模型都将使用相同的 Model.compileModel.fit 设置

def compile_and_fit(model, name, optimizer=None, max_epochs=10000):
  if optimizer is None:
    optimizer = get_optimizer()
  model.compile(optimizer=optimizer,
                loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
                metrics=[
                  tf.keras.metrics.BinaryCrossentropy(
                      from_logits=True, name='binary_crossentropy'),
                  'accuracy'])

  model.summary()

  history = model.fit(
    train_ds,
    steps_per_epoch = STEPS_PER_EPOCH,
    epochs=max_epochs,
    validation_data=validate_ds,
    callbacks=get_callbacks(name),
    verbose=0)
  return history

微型模型

首先训练一个模型

tiny_model = tf.keras.Sequential([
    layers.Dense(16, activation='elu', input_shape=(FEATURES,)),
    layers.Dense(1)
])
size_histories = {}
size_histories['Tiny'] = compile_and_fit(tiny_model, 'sizes/Tiny')

现在检查模型的表现

plotter = tfdocs.plots.HistoryPlotter(metric = 'binary_crossentropy', smoothing_std=10)
plotter.plot(size_histories)
plt.ylim([0.5, 0.7])

小型模型

为了检查是否可以超越小型模型的性能,逐步训练一些更大的模型。

尝试两个隐藏层,每个层有 16 个单元

small_model = tf.keras.Sequential([
    # `input_shape` is only required here so that `.summary` works.
    layers.Dense(16, activation='elu', input_shape=(FEATURES,)),
    layers.Dense(16, activation='elu'),
    layers.Dense(1)
])
size_histories['Small'] = compile_and_fit(small_model, 'sizes/Small')

中等模型

现在尝试三个隐藏层,每个层有 64 个单元

medium_model = tf.keras.Sequential([
    layers.Dense(64, activation='elu', input_shape=(FEATURES,)),
    layers.Dense(64, activation='elu'),
    layers.Dense(64, activation='elu'),
    layers.Dense(1)
])

并使用相同的数据训练模型

size_histories['Medium']  = compile_and_fit(medium_model, "sizes/Medium")

大型模型

作为练习,您可以创建一个更大的模型,并检查它开始过拟合的速度。接下来,在这个基准中添加一个容量大得多的网络,远远超过问题的需要

large_model = tf.keras.Sequential([
    layers.Dense(512, activation='elu', input_shape=(FEATURES,)),
    layers.Dense(512, activation='elu'),
    layers.Dense(512, activation='elu'),
    layers.Dense(512, activation='elu'),
    layers.Dense(1)
])

并且,再次使用相同的数据训练模型

size_histories['large'] = compile_and_fit(large_model, "sizes/large")

绘制训练损失和验证损失

实线显示训练损失,虚线显示验证损失(记住:较低的验证损失表示更好的模型)。

虽然构建一个更大的模型会赋予它更多能力,但如果这种能力没有得到某种约束,它很容易过拟合训练集。

在这个例子中,通常只有 "Tiny" 模型设法完全避免过拟合,而每个更大的模型都更快地过拟合数据。对于 "large" 模型来说,这种情况变得如此严重,以至于您需要将绘图切换到对数刻度才能真正弄清楚发生了什么。

如果您绘制并比较验证指标和训练指标,这一点很明显。

  • 存在微小的差异是正常的。
  • 如果两个指标都朝着相同的方向移动,那么一切正常。
  • 如果验证指标开始停滞,而训练指标继续改进,那么您可能接近过拟合。
  • 如果验证指标朝着错误的方向移动,那么模型显然过拟合了。
plotter.plot(size_histories)
a = plt.xscale('log')
plt.xlim([5, max(plt.xlim())])
plt.ylim([0.5, 0.7])
plt.xlabel("Epochs [Log Scale]")

在 TensorBoard 中查看

这些模型在训练期间都写入了 TensorBoard 日志。

在笔记本中打开嵌入式 TensorBoard 查看器(抱歉,这在 tensorflow.org 上不会显示)

# Load the TensorBoard notebook extension
%load_ext tensorboard

# Open an embedded TensorBoard viewer
%tensorboard --logdir {logdir}/sizes

您可以在 TensorBoard.dev 上查看 本笔记本先前运行的结果

防止过拟合的策略

在进入本节内容之前,复制上面 "Tiny" 模型的训练日志,用作比较的基线。

shutil.rmtree(logdir/'regularizers/Tiny', ignore_errors=True)
shutil.copytree(logdir/'sizes/Tiny', logdir/'regularizers/Tiny')
regularizer_histories = {}
regularizer_histories['Tiny'] = size_histories['Tiny']

添加权重正则化

您可能熟悉奥卡姆剃刀原理:对于某件事的两种解释,最有可能正确的解释是“最简单”的解释,即假设最少的解释。这也适用于神经网络学习的模型:给定一些训练数据和网络架构,存在多个权重值集(多个模型)可以解释数据,而更简单的模型比复杂的模型不太可能过拟合。

在这种情况下,“简单模型”是指参数值分布的熵较小的模型(或者具有更少参数的模型,如上面部分所示)。因此,减轻过拟合的一种常见方法是对网络的复杂性施加约束,迫使它的权重只取较小的值,这使得权重值的分布更加“规则”。这被称为“权重正则化”,它是通过在网络的损失函数中添加与具有较大权重相关的成本来实现的。这种成本有两种形式

  • L1 正则化,其中添加的成本与权重系数的绝对值成正比(即与权重的“L1 范数”成正比)。

  • L2 正则化,其中添加的成本与权重系数值的平方成正比(即与权重的平方“L2 范数”成正比)。在神经网络的背景下,L2 正则化也称为权重衰减。不要让不同的名称迷惑你:权重衰减在数学上与 L2 正则化完全相同。

L1 正则化将权重推向精确的零,鼓励稀疏模型。L2 正则化会惩罚权重参数,但不会使它们稀疏,因为对于较小的权重,惩罚会趋于零——这也是 L2 更常见的原因。

tf.keras 中,权重正则化是通过将权重正则化实例作为关键字参数传递给层来添加的。添加 L2 权重正则化

l2_model = tf.keras.Sequential([
    layers.Dense(512, activation='elu',
                 kernel_regularizer=regularizers.l2(0.001),
                 input_shape=(FEATURES,)),
    layers.Dense(512, activation='elu',
                 kernel_regularizer=regularizers.l2(0.001)),
    layers.Dense(512, activation='elu',
                 kernel_regularizer=regularizers.l2(0.001)),
    layers.Dense(512, activation='elu',
                 kernel_regularizer=regularizers.l2(0.001)),
    layers.Dense(1)
])

regularizer_histories['l2'] = compile_and_fit(l2_model, "regularizers/l2")

l2(0.001) 表示层权重矩阵中的每个系数都会在网络的总 **损失** 中添加 0.001 * weight_coefficient_value**2

这就是我们直接监控 binary_crossentropy 的原因。因为它没有混合这种正则化组件。

因此,具有 L2 正则化惩罚的相同 "Large" 模型表现得更好

plotter.plot(regularizer_histories)
plt.ylim([0.5, 0.7])

如上图所示,"L2" 正则化模型现在与 "Tiny" 模型相比具有更强的竞争力。尽管具有相同数量的参数,但该 "L2" 模型也比它所基于的 "Large" 模型更能抵抗过拟合。

更多信息

关于这种正则化,有两点需要注意

  1. 如果您正在编写自己的训练循环,那么您需要确保向模型询问其正则化损失。
result = l2_model(features)
regularization_loss=tf.add_n(l2_model.losses)
  1. 此实现通过将权重惩罚添加到模型的损失中,然后在之后应用标准优化过程来实现。

还有第二种方法,它只对原始损失运行优化器,然后在应用计算出的步骤时,优化器也会应用一些权重衰减。这种“解耦权重衰减”用于 tf.keras.optimizers.Ftrltfa.optimizers.AdamW 等优化器中。

添加 dropout

Dropout 是神经网络中最有效和最常用的正则化技术之一,由 Hinton 及其在多伦多大学的学生开发。

Dropout 的直观解释是,由于网络中的单个节点不能依赖于其他节点的输出,因此每个节点必须输出本身有用的特征。

Dropout 应用于层,包括在训练期间随机“丢弃”(即设置为零)层的多个输出特征。例如,给定层通常会在训练期间为给定输入样本返回向量 [0.2, 0.5, 1.3, 0.8, 1.1];应用 dropout 后,该向量将具有几个随机分布的零条目,例如 [0, 0.5, 1.3, 0, 1.1]

“dropout 率”是零掉的特征的比例;它通常设置为 0.2 到 0.5 之间。在测试时,不会丢弃任何单元,而是将层的输出值按等于 dropout 率的因子缩小,以平衡训练时活动单元比测试时更多的现象。

在 Keras 中,您可以通过 tf.keras.layers.Dropout 层在网络中引入 dropout,该层应用于紧接其前的层的输出。

在您的网络中添加两个 dropout 层,以检查它们在减少过拟合方面的效果。

dropout_model = tf.keras.Sequential([
    layers.Dense(512, activation='elu', input_shape=(FEATURES,)),
    layers.Dropout(0.5),
    layers.Dense(512, activation='elu'),
    layers.Dropout(0.5),
    layers.Dense(512, activation='elu'),
    layers.Dropout(0.5),
    layers.Dense(512, activation='elu'),
    layers.Dropout(0.5),
    layers.Dense(1)
])

regularizer_histories['dropout'] = compile_and_fit(dropout_model, "regularizers/dropout")
plotter.plot(regularizer_histories)
plt.ylim([0.5, 0.7])

从该图可以清楚地看出,这两种正则化方法都改善了 "Large" 模型的行为。但这仍然无法超越甚至 "Tiny" 基线。

接下来尝试同时使用它们,看看是否效果更好。

组合 L2 + dropout

combined_model = tf.keras.Sequential([
    layers.Dense(512, kernel_regularizer=regularizers.l2(0.0001),
                 activation='elu', input_shape=(FEATURES,)),
    layers.Dropout(0.5),
    layers.Dense(512, kernel_regularizer=regularizers.l2(0.0001),
                 activation='elu'),
    layers.Dropout(0.5),
    layers.Dense(512, kernel_regularizer=regularizers.l2(0.0001),
                 activation='elu'),
    layers.Dropout(0.5),
    layers.Dense(512, kernel_regularizer=regularizers.l2(0.0001),
                 activation='elu'),
    layers.Dropout(0.5),
    layers.Dense(1)
])

regularizer_histories['combined'] = compile_and_fit(combined_model, "regularizers/combined")
plotter.plot(regularizer_histories)
plt.ylim([0.5, 0.7])

具有 "Combined" 正则化的模型显然是迄今为止最好的模型。

在 TensorBoard 中查看

这些模型还记录了 TensorBoard 日志。

要打开嵌入式运行,请在代码单元格中执行以下操作(抱歉,这在 tensorflow.org 上不会显示)

%tensorboard --logdir {logdir}/regularizers

您可以在 TensorBoard.dev 上查看 本笔记本先前运行的结果

结论

回顾一下,以下是防止神经网络过拟合的最常见方法

  • 获取更多训练数据。
  • 降低网络容量。
  • 添加权重正则化。
  • 添加 dropout。

本指南中未涵盖的两种重要方法是

请记住,每种方法都可以单独提供帮助,但通常将它们组合起来会更有效。

# MIT License
#
# Copyright (c) 2017 François Chollet
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.