与往常一样,此示例中的代码将使用 tf.keras
API,你可以在 TensorFlow Keras 指南中了解更多信息。
在之前的两个例子 —— 分类电影评论和预测住房价格 —— 中,我们看到模型对验证数据的准确率在经过多个周期的训练后会达到峰值,然后开始下降。
换句话说,我们的模型会过度拟合训练数据。学习如何处理过拟合很重要,尽管通常可以在训练集上获得高准确率,但我们真正想要的是开发能够很好地泛化至测试数据(或之前未见过的数据)的模型。
与过拟合相反的是欠拟合。当模型在测试数据上仍有改进空间时,会发生欠拟合。出现这种情况的原因有很多:如果模型不够强大、过度正则化、或者根本没有经过足够长时间的训练。这意味着网络尚未学习训练数据中的相关模式。
如果训练时间过长,模型将开始过度拟合并从训练数据中学习无法泛化至测试数据的模式。我们需要取得平衡,了解如何训练适当数量的周期是一项有用的技能。
为了防止过拟合,最好的解决方案是使用更多的训练数据。使用更多数据训练的模型自然会更好地泛化。当该方案不再可行时,下一个最佳解决方案是使用正则化等技术。正则化限制了模型可以存储的信息的数量和类型,如果一个网络只能记住少量模式,那么优化过程将迫使它专注于最突出的模式,这样模型可以更好地进行泛化。
本教程中,我们将探索两种常见的正则化技术 —— 权重正则化和 dropout —— 并使用它们来改进我们的 IMDB 电影评论分类模型。
import tensorflow as tf
from tensorflow import keras
import numpy as np
import matplotlib.pyplot as plt
print(tf.__version__)
下载 IMDB 数据集
我们不会像之前的教程一样使用嵌入,而是对句子进行 multi-hot 编码。该模型将很快对训练集过拟合,它将用于演示何时发生过度拟合,以及如何对抗它。
对我们的列表进行 multi-hot 编码意味着将它们转换为 0 和 1 的向量。具体地,例如将序列 [3, 5]
转换为 10,000 维向量,除了索引 3 和 5 为 1 之外,其它将是全 0。
NUM_WORDS = 10000
(train_data, train_labels), (test_data, test_labels) = keras.datasets.imdb.load_data(num_words=NUM_WORDS)
def multi_hot_sequences(sequences, dimension):
# 创建大小为 (len(sequences), dimension) 的全零矩阵
results = np.zeros((len(sequences), dimension))
for i, word_indices in enumerate(sequences):
results[i, word_indices] = 1.0 # 将 results[i] 的索引设为 1
return results
train_data = multi_hot_sequences(train_data, dimension=NUM_WORDS)
test_data = multi_hot_sequences(test_data, dimension=NUM_WORDS)
Downloading data from https://s3.amazonaws.com/text-datasets/imdb.npz
17465344/17464789 [==============================] - 2s 0us/step
让我们看一下生成的 multi-hot 矢量。单词索引按频率排序,因此索引零附近有更多的 1 值,我们可以在下图中看到:
plt.plot(train_data[0])
演示过拟合
防止过拟合的最简单方法是减小模型的大小,即模型中可学习参数的数量(由层数和每层单元数决定)。在深度学习中,模型中可学习参数的数量通常被称为模型的“容量”。直观上,具有更多参数的模型将具有更多“记忆容量”,因此将能够更容易地学习训练样本与其目标之间的完美的字典式映射,但是该映射没有任何泛化能力,在对未见过的数据做预测时这将是无用的。
始终牢记这一点:深度学习模型往往善于拟合训练数据,但真正的挑战是泛化,而不是拟合。
另一方面,如果网络具有有限的记忆资源,则将不能轻易地学习映射。为了最小化损失,它必须学习具有更强预测能力的压缩表征。同时,如果模型太小,则难以拟合训练数据。“容量太多”和“容量不足”之间存在一定的平衡。
不幸的是,没有神奇的公式来确定模型的正确大小或架构(就层数而言,或每层的大小)。你将不得不尝试使用一系列不同的架构。
要找到合适的模型大小,最好从相对较少的网络层和参数开始,然后开始增加网络层的大小或添加新网络层,直到你看到验证损失递减为止。让我们在电影评论分类网络上进行尝试。
我们将仅使用 Dense
层作为基线创建一个简单模型,然后创建更小和更大的版本,并进行比较。
创建基线模型
baseline_model = keras.Sequential([
# `input_shape` 只在这里需要,以便 `.summary` 可以工作
keras.layers.Dense(16, activation=tf.nn.relu, input_shape=(NUM_WORDS,)),
keras.layers.Dense(16, activation=tf.nn.relu),
keras.layers.Dense(1, activation=tf.nn.sigmoid)
])
baseline_model.compile(optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy', 'binary_crossentropy'])
baseline_model.summary()
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense (Dense) (None, 16) 160016
_________________________________________________________________
dense_1 (Dense) (None, 16) 272
_________________________________________________________________
dense_2 (Dense) (None, 1) 17
=================================================================
Total params: 160,305
Trainable params: 160,305
Non-trainable params: 0
_________________________________________________________________
baseline_history = baseline_model.fit(train_data,
train_labels,
epochs=20,
batch_size=512,
validation_data=(test_data, test_labels),
verbose=2)
Train on 25000 samples, validate on 25000 samples
Epoch 1/20
- 4s - loss: 0.4830 - acc: 0.8082 - binary_crossentropy: 0.4830 - val_loss: 0.3383 - val_acc: 0.8758 - val_binary_crossentropy: 0.3383
Epoch 2/20
- 4s - loss: 0.2494 - acc: 0.9114 - binary_crossentropy: 0.2494 - val_loss: 0.2851 - val_acc: 0.8862 - val_binary_crossentropy: 0.2851
...
Epoch 19/20
- 4s - loss: 0.0037 - acc: 1.0000 - binary_crossentropy: 0.0037 - val_loss: 0.8185 - val_acc: 0.8536 - val_binary_crossentropy: 0.8185
Epoch 20/20
- 4s - loss: 0.0031 - acc: 1.0000 - binary_crossentropy: 0.0031 - val_loss: 0.8396 - val_acc: 0.8522 - val_binary_crossentropy: 0.8396
创建一个较小的模型
让我们创建一个隐藏单元较少的模型,与我们刚刚创建的基线模型进行比较:
smaller_model = keras.Sequential([
keras.layers.Dense(4, activation=tf.nn.relu, input_shape=(NUM_WORDS,)),
keras.layers.Dense(4, activation=tf.nn.relu),
keras.layers.Dense(1, activation=tf.nn.sigmoid)
])
smaller_model.compile(optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy', 'binary_crossentropy'])
smaller_model.summary()
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense_3 (Dense) (None, 4) 40004
_________________________________________________________________
dense_4 (Dense) (None, 4) 20
_________________________________________________________________
dense_5 (Dense) (None, 1) 5
=================================================================
Total params: 40,029
Trainable params: 40,029
Non-trainable params: 0
_________________________________________________________________
并使用相同的数据训练模型:
smaller_history = smaller_model.fit(train_data,
train_labels,
epochs=20,
batch_size=512,
validation_data=(test_data, test_labels),
verbose=2)
Train on 25000 samples, validate on 25000 samples
Epoch 1/20
- 4s - loss: 0.5875 - acc: 0.7590 - binary_crossentropy: 0.5875 - val_loss: 0.4841 - val_acc: 0.8534 - val_binary_crossentropy: 0.4841
Epoch 2/20
- 4s - loss: 0.3949 - acc: 0.8851 - binary_crossentropy: 0.3949 - val_loss: 0.3738 - val_acc: 0.8733 - val_binary_crossentropy: 0.3738
...
Epoch 19/20
- 4s - loss: 0.0659 - acc: 0.9857 - binary_crossentropy: 0.0659 - val_loss: 0.4142 - val_acc: 0.8666 - val_binary_crossentropy: 0.4142
Epoch 20/20
- 4s - loss: 0.0606 - acc: 0.9877 - binary_crossentropy: 0.0606 - val_loss: 0.4292 - val_acc: 0.8656 - val_binary_crossentropy: 0.4292
创建一个更大的模型
作为练习,你可以创建一个更大的模型,并观察它开始过拟合的速度。接下来,让我们在这个基准测试中添加一个容量大得多的网络,远远超出问题的需要:
bigger_model = keras.models.Sequential([
keras.layers.Dense(512, activation=tf.nn.relu, input_shape=(NUM_WORDS,)),
keras.layers.Dense(512, activation=tf.nn.relu),
keras.layers.Dense(1, activation=tf.nn.sigmoid)
])
bigger_model.compile(optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy','binary_crossentropy'])
bigger_model.summary()
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense_6 (Dense) (None, 512) 5120512
_________________________________________________________________
dense_7 (Dense) (None, 512) 262656
_________________________________________________________________
dense_8 (Dense) (None, 1) 513
=================================================================
Total params: 5,383,681
Trainable params: 5,383,681
Non-trainable params: 0
_________________________________________________________________
并且,再次使用相同的数据训练模型:
bigger_history = bigger_model.fit(train_data, train_labels,
epochs=20,
batch_size=512,
validation_data=(test_data, test_labels),
verbose=2)
Train on 25000 samples, validate on 25000 samples
Epoch 1/20
- 8s - loss: 0.3478 - acc: 0.8486 - binary_crossentropy: 0.3478 - val_loss: 0.2999 - val_acc: 0.8788 - val_binary_crossentropy: 0.2999
Epoch 2/20
- 8s - loss: 0.1424 - acc: 0.9483 - binary_crossentropy: 0.1424 - val_loss: 0.3631 - val_acc: 0.8639 - val_binary_crossentropy: 0.3631
...
Epoch 19/20
- 8s - loss: 1.6204e-05 - acc: 1.0000 - binary_crossentropy: 1.6204e-05 - val_loss: 0.8572 - val_acc: 0.8722 - val_binary_crossentropy: 0.8572
Epoch 20/20
- 8s - loss: 1.4384e-05 - acc: 1.0000 - binary_crossentropy: 1.4384e-05 - val_loss: 0.8645 - val_acc: 0.8723 - val_binary_crossentropy: 0.8645
绘制训练和验证损失
实线表示训练损失,虚线表示验证损失(记住:较低的验证损失表示更好的模型)。在这里,较小的网络开始过拟合晚于基线模型(在 6 个周期之后而不是 4 个周期),并且一旦开始过拟合,其性能下降得慢得多。
def plot_history(histories, key='binary_crossentropy'):
plt.figure(figsize=(16,10))
for name, history in histories:
val = plt.plot(history.epoch, history.history['val_'+key],
'--', label=name.title()+' Val')
plt.plot(history.epoch, history.history[key], color=val[0].get_color(),
label=name.title()+' Train')
plt.xlabel('Epochs')
plt.ylabel(key.replace('_',' ').title())
plt.legend()
plt.xlim([0,max(history.epoch)])
plot_history([('baseline', baseline_history),
('smaller', smaller_history),
('bigger', bigger_history)])
注意,较大的网络在仅仅一个周期之后,几乎立即开始过拟合,并且过拟合得更加严重。网络容量越大,能够越快地对训练数据进行建模(导致训练损失低),但过拟合的可能性越大(导致训练和验证损失之间的差异很大)。
策略
添加权重正则化
你可能熟悉奥卡姆剃刀原则:给出某个东西的两种解释,最可能是正确的解释是“最简单”的解释,即做出最少量假设的解释。这也适用于使用神经网络学习的模型:给定一些训练数据和网络架构,有多组权重值(多个模型)可以解释数据,而简单模型比复杂模型更不容易过拟合。
在这种情况下,“简单模型”是一个参数值的分布具有较少的熵(或者具有较少参数的模型,如我们在上面的部分中所见)的模型。因此,减轻过拟合的常见方法是通过强制其权重仅采用较小的值来对网络的复杂性施加约束,这使得权重值的分布更“规则”。这被称为“权重正则化”,通过向网络的损失函数添加与大权重相关联的成本来完成。这个成本有两种:
- L1 正则化:添加的成本与权重系数的绝对值(即权重的“L1 范数”)成比例。
- L2 正则化:添加的成本与权重系数的值的平方成比例(即权重的“L2 范数”)。L2 正则化在神经网络的背景下也称为权重衰减。不要让不同的名字让你感到困惑:权重衰减在数学上与 L2 正则化完全相同。
在 tf.keras
中,通过将权重正则化实例作为关键字参数传递给网络层来添加权重正则化。现在让我们添加 L2 权重正则化。
l2_model = keras.models.Sequential([
keras.layers.Dense(16, kernel_regularizer=keras.regularizers.l2(0.001),
activation=tf.nn.relu, input_shape=(NUM_WORDS,)),
keras.layers.Dense(16, kernel_regularizer=keras.regularizers.l2(0.001),
activation=tf.nn.relu),
keras.layers.Dense(1, activation=tf.nn.sigmoid)
])
l2_model.compile(optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy', 'binary_crossentropy'])
l2_model_history = l2_model.fit(train_data, train_labels,
epochs=20,
batch_size=512,
validation_data=(test_data, test_labels),
verbose=2)
Train on 25000 samples, validate on 25000 samples
Epoch 1/20
- 4s - loss: 0.5401 - acc: 0.7926 - binary_crossentropy: 0.5019 - val_loss: 0.3785 - val_acc: 0.8760 - val_binary_crossentropy: 0.3386
Epoch 2/20
- 4s - loss: 0.2991 - acc: 0.9103 - binary_crossentropy: 0.2543 - val_loss: 0.3339 - val_acc: 0.8870 - val_binary_crossentropy: 0.2856
...
Epoch 19/20
- 4s - loss: 0.1466 - acc: 0.9748 - binary_crossentropy: 0.0778 - val_loss: 0.5389 - val_acc: 0.8589 - val_binary_crossentropy: 0.4700
Epoch 20/20
- 4s - loss: 0.1451 - acc: 0.9755 - binary_crossentropy: 0.0757 - val_loss: 0.5499 - val_acc: 0.8557 - val_binary_crossentropy: 0.4800
l2(0.001)
意味着网络层权重矩阵中的每个系数都会增加 0.001 * weight_coefficient_value
至网络的总损失中。请注意,由于此惩罚仅在训练时添加,因此网络在训练时的损失将远高于在测试时的损失(这句感觉有问题)。
这是 L2 正则化惩罚的影响:
plot_history([('baseline', baseline_history),
('l2', l2_model_history)])
正如你所看到的,L2 正则化模型比基线模型更能抵抗过拟合,即使两个模型具有相同数量的参数。
添加 dropout
Dropout 是由 Hinton 和他在多伦多大学的学生提出的最有效和最常用的神经网络正则化技术之一。Dropout 应用于网络层,包括在训练期间随机“失活”(即设置为零)该层的多个输出特征。假设一个给定的层通常会在训练期间为给定的输入样本返回一个向量 [0.2, 0.5, 1.3, 0.8, 1.1]
,在应用 dropout 之后,这个向量将有几个随机分布的零,例如 [0, 0.5, 1.3, 0, 1.1]
。“失活率”是失活的特征的因子,它通常设置在 0.2 和 0.5 之间。在测试时,没有单元失活,而是将该层的输出值按与失活率相等的因子缩放,以平衡比训练时更多的单元处于活动状态这一情况。
在 tf.keras
中,你可以通过 Dropout
层在网络中引入 dropout,该层将应用于上一层的输出。
让我们在 IMDB 网络中添加两个 Dropout
层,看看它们在减少过拟合方面做得如何:
dpt_model = keras.models.Sequential([
keras.layers.Dense(16, activation=tf.nn.relu, input_shape=(NUM_WORDS,)),
keras.layers.Dropout(0.5),
keras.layers.Dense(16, activation=tf.nn.relu),
keras.layers.Dropout(0.5),
keras.layers.Dense(1, activation=tf.nn.sigmoid)
])
dpt_model.compile(optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy','binary_crossentropy'])
dpt_model_history = dpt_model.fit(train_data, train_labels,
epochs=20,
batch_size=512,
validation_data=(test_data, test_labels),
verbose=2)
Train on 25000 samples, validate on 25000 samples
Epoch 1/20
- 4s - loss: 0.6363 - acc: 0.6294 - binary_crossentropy: 0.6363 - val_loss: 0.5156 - val_acc: 0.8526 - val_binary_crossentropy: 0.5156
Epoch 2/20
- 4s - loss: 0.4776 - acc: 0.7993 - binary_crossentropy: 0.4776 - val_loss: 0.3629 - val_acc: 0.8782 - val_binary_crossentropy: 0.3629
...
Epoch 19/20
- 4s - loss: 0.0752 - acc: 0.9698 - binary_crossentropy: 0.0752 - val_loss: 0.5262 - val_acc: 0.8760 - val_binary_crossentropy: 0.5262
Epoch 20/20
- 4s - loss: 0.0768 - acc: 0.9666 - binary_crossentropy: 0.0768 - val_loss: 0.5360 - val_acc: 0.8754 - val_binary_crossentropy: 0.5360
plot_history([('baseline', baseline_history),
('dropout', dpt_model_history)])
添加 dropout 对基线模型有明显的改进。
回顾一下:防止神经网络过拟合的最常见方法:
- 获取更多训练数据。
- 减少网络容量。
- 添加权重正则化。
- 添加 dropout。
本指南未涉及的两个重要方法是数据扩增和批归一化。