根据我这个系列的前两篇文章,您会发现您的模型已经表现出了一定的泛化能力,并且能够过拟合,接下来应该专注于将泛化能力最大化。
政安晨的个人主页:政安晨
欢迎 👍点赞✍评论⭐收藏
收录专栏: 政安晨的机器学习笔记
希望政安晨的博客能够对您有所裨益,如有不足之处,欢迎在评论区提出指正!
本系列的前两篇文章为:
政安晨:【机器学习基础】(一)—— 泛化:机器学习的目标https://blog.csdn.net/snowdenkeke/article/details/136275013政安晨:【机器学习基础】(二)—— 评估机器学习模型&改进https://blog.csdn.net/snowdenkeke/article/details/136278780
数据集管理
相信您已经知道,深度学习的泛化来源于数据的潜在结构。
如果你的数据允许在样本之间进行平滑插值,你就可以训练出一个具有泛化能力的深度学习模型。如果你的数据过于嘈杂或者本质上是离散的,比如列表排序问题,那么深度学习将无法帮助你解决这类问题。深度学习是曲线拟合,而不是魔法。
因此,你必须确保使用适当的数据集。在收集数据上花费更多的精力和金钱,几乎总是比在开发更好的模型上花费同样的精力和金钱产生更大的投资回报。
确保拥有足够的数据。请记住,你需要对输入−输出空间进行密集采样。利用更多的数据可以得到更好的模型。有时,一开始看起来无法解决的问题,在拥有更大的数据集之后就能得到解决。
尽量减少标签错误。将输入可视化,以检查异常样本并核查标签。
如果有很多特征,而你不确定哪些特征是真正有用的,那么需要进行特征选择。
提高数据泛化潜力的一个特别重要的方法就是特征工程(feature engineering)。对于大多数机器学习问题,特征工程是成功的关键因素。
特征工程
特征工程是指将数据输入模型之前,利用你自己关于数据和机器学习算法(这里指神经网络)的知识对数据进行硬编码的变换(这种变换不是模型学到的),以改善算法的效果。在多数情况下,机器学习模型无法从完全随意的数据中进行学习。呈现给模型的数据应该便于模型进行学习。
咱们来看一个直观的例子:假设你想开发一个模型,输入一张时钟图像,模型就可以输出对应的时间,如下图所示:
如果选择使用图像的原始像素作为输入数据,那么这个机器学习问题解决起来会非常困难。你需要用卷积神经网络来解决,而且还需要耗费大量计算资源来训练这个网络。
但如果你从更高的层次理解了这个问题(你知道人们如何读取时钟显示的时间),就可以为机器学习算法找到更好的输入特征,比如你可以编写5行Python脚本,找到时钟指针对应的黑色像素并输出每个指针顶端的(x, y)坐标,这很简单。这样一个简单的机器学习算法就可以学会这些坐标与时间的对应关系。
你还可以进一步思考:利用坐标变换,将(x, y)坐标转换为相对于图像中心的极坐标。输入变成了每个时钟指针的角度theta。这个特征让问题变得非常简单,无须使用机器学习算法,简单的舍入运算和字典查找就足以给出大致时间。
这就是特征工程的本质:用更简单的方式表述问题,从而使问题更容易解决。特征工程可以让潜在流形变得更平滑、更简单、更有条理。特征工程通常需要深入理解问题。
在深度学习出现之前,特征工程曾经是机器学习工作流程中最重要的部分,因为经典的浅层算法没有足够丰富的假设空间来自主学习有用的表示。
将数据呈现给算法的方式对成功解决问题至关重要。
举例来说,在卷积神经网络成功解决MNIST数字分类问题之前,这个问题的解决方法通常是基于硬编码的特征,比如数字图像中的圆圈个数、图像中的数字高度、像素值的直方图等。
幸运的是,对于现代深度学习,大多数特征工程是不需要做的,因为神经网络能够从原始数据中自动提取有用的特征。这是否意味着,只要使用深度神经网络,就无须担心特征工程呢?并非如此,原因有以下两点:
良好的特征仍然有助于更优雅地解决问题,同时使用更少的资源。例如,使用卷积神经网络解决读取时钟问题是非常可笑的。
良好的特征可以用更少的数据解决问题。深度学习模型自主学习特征的能力依赖于拥有大量的训练数据。如果只有很少的样本,那么特征的信息价值就变得非常重要。
提前终止
在深度学习中,我们总是使用过度参数化的模型:模型自由度远远超过拟合数据潜在流形所需的最小自由度。这种过度参数化并不是问题,因为永远不会完全拟合一个深度学习模型。这样的拟合根本没有泛化能力。你总是在达到最小训练损失之前很久就会中断训练。
在训练过程中找到最佳泛化的拟合,即欠拟合曲线和过拟合曲线之间的确切界线,是提高泛化能力的最有效的方法之一。
在我以前文章中的例子中,我们首先让模型训练时间比需要的时间更长,以确定最佳验证指标对应的轮数,然后重新训练一个新模型,正好训练这个轮数。这是很标准的做法,但需要做一些冗余工作,有时代价很高。当然,你也可以在每轮结束时保存模型,一旦找到了最佳轮数,就重新使用最近一次保存的模型。在Keras中,我们通常使用EarlyStopping回调函数来实现这一点,它会在验证指标停止改善时立即中断训练,同时记录最佳模型状态。
模型正则化
正则化方法是一组最佳实践,可以主动降低模型完美拟合训练数据的能力,其目的是提高模型的验证性能。它之所以被称为模型的“正则化”,是因为它通常使模型变得更简单、更“规则”,曲线更平滑、更“通用”。因此,模型对训练集的针对性更弱,能够更好地近似数据的潜在流形,从而具有更强的泛化能力。
请记住,模型正则化过程应该始终由一个准确的评估方法来引导。只有能够衡量泛化,你才能实现泛化。我们来具体了解几种最常用的正则化方法,并将其实际应用于改进以前我以前文章中的影评分类模型。
缩减模型容量
你已经知道,一个太小的模型不会过拟合。为模型降低过拟合最简单的方法,就是缩减模型容量,即减少模型中可学习参数的个数(这由层数和每层单元个数决定)。
如果模型的记忆资源有限,它就不能简单地记住训练数据;
为了让损失最小化,它必须学会对目标有预测能力的压缩表示,这也正是我们感兴趣的数据表示。同时请记住,你的模型应该具有足够多的参数,以防欠拟合,即模型应避免记忆资源不足。在容量过大和容量不足之间,要找到一个平衡点。
不幸的是,没有一个魔法公式能够确定最佳层数或每层的最佳大小。
你必须评估一系列不同的模型架构(当然是在验证集上评估,而不是测试集),以便为数据找到最佳的模型规模。要确定合适的模型规模,一般的工作流程是开始时选择相对较少的层和参数,然后逐渐增加层的大小或添加新层,直到这种增加对验证损失的影响变得很小。
咱们在我以前文章中的影评分类模型上试一下(是以下这篇文章):
政安晨:【示例演绎机器学习】(二)—— 神经网络的二分类问题示例 (影评分类)https://blog.csdn.net/snowdenkeke/article/details/136204994
准备
import tensorflow
from tensorflow import keras
from tensorflow.keras import layers
import numpy as np
(初始模型)
from tensorflow.keras.datasets import imdb
(train_data, train_labels), _ = imdb.load_data(num_words=10000)
def vectorize_sequences(sequences, dimension=10000):
results = np.zeros((len(sequences), dimension))
for i, sequence in enumerate(sequences):
results[i, sequence] = 1.
return results
train_data = vectorize_sequences(train_data)
model = keras.Sequential([
layers.Dense(16, activation="relu"),
layers.Dense(16, activation="relu"),
layers.Dense(1, activation="sigmoid")
])
model.compile(optimizer="rmsprop",
loss="binary_crossentropy",
metrics=["accuracy"])
history_original = model.fit(train_data, train_labels,
epochs=20, batch_size=512, validation_split=0.4)
上述这个模型的训练过程如下:
现在我们尝试用较小的模型来代替它,代码如下:
(容量更小的模型)
model = keras.Sequential([
layers.Dense(4, activation="relu"),
layers.Dense(4, activation="relu"),
layers.Dense(1, activation="sigmoid")
])
model.compile(optimizer="rmsprop",
loss="binary_crossentropy",
metrics=["accuracy"])
history_smaller_model = model.fit(
train_data, train_labels,
epochs=20, batch_size=512, validation_split=0.4)
训练过程如下:
咱们现在对比一下初始模型与较小模型的验证损失:
作者政安晨为小伙伴们把画图的代码写好:
(大家可以修改下面的代码显示不同参数的值的图)
import matplotlib.pyplot as plt
history_dict = history_original.history
history_dict2 = history_smaller_model.history
#loss_values = history_dict["loss"]
val_loss_values = history_dict["val_loss"]
val_loss_values2 = history_dict2["val_loss"]
epochs = range(1, len(val_loss_values) + 1)
# "b"表示“蓝色实线”
plt.plot(epochs, val_loss_values, "b", label="Original Validation loss")
# "bo"表示“蓝色圆点”
plt.plot(epochs, val_loss_values2, "bo", label="Small Validation loss")
plt.title("Training and validation loss")
plt.xlabel("Epochs")
plt.ylabel("Loss")
plt.legend()
plt.show()
演绎如下:
(上图为对于影评分类问题,初始模型与较小模型的对比)
如上图所示,较小模型开始过拟合的时间要晚于初始模型(前者10轮后开始过拟合,而后者5轮后就开始过拟合),而且开始过拟合之后,它的性能下降速度也更慢。
如下代码所示,我们现在添加一个容量更大的模型——其容量远大于问题所需。虽然过度参数化的模型很常见,但肯定会有这样一种情况:模型的记忆容量过大。如果模型立刻开始过拟合,而且它的验证损失曲线看起来很不稳定、方差很大,你就知道模型容量过大了(不过验证指标不稳定的原因也可能是验证过程不可靠,比如验证集太小)。
(容量更大的模型)
model = keras.Sequential([
layers.Dense(512, activation="relu"),
layers.Dense(512, activation="relu"),
layers.Dense(1, activation="sigmoid")
])
model.compile(optimizer="rmsprop",
loss="binary_crossentropy",
metrics=["accuracy"])
history_larger_model = model.fit(
train_data, train_labels,
epochs=20, batch_size=512, validation_split=0.4)
演绎如下:
演绎较大模型与初始模型的性能对比:
画图代码如下:
import matplotlib.pyplot as plt
history_dict = history_original.history
history_dict2 = history_larger_model.history
#loss_values = history_dict["loss"]
val_loss_values = history_dict["val_loss"]
val_loss_values2 = history_dict2["val_loss"]
epochs = range(1, len(val_loss_values) + 1)
# "b"表示“蓝色实线”
plt.plot(epochs, val_loss_values, "b", label="Original Validation loss")
# "bo"表示“蓝色圆点”
plt.plot(epochs, val_loss_values2, "bo", label="Larger Validation loss")
plt.title("Training and validation loss")
plt.xlabel("Epochs")
plt.ylabel("Loss")
plt.legend()
plt.show()
对比图:
仅仅过了一轮,较大模型几乎立即开始过拟合,而且过拟合程度要严重得多。
它的验证损失波动更大。此外,它的训练损失很快就接近于零。模型的容量越大,它拟合训练数据的速度就越快(得到很小的训练损失),但也更容易过拟合(导致训练损失和验证损失有很大差异)。
添加权重正则化
小伙伴们可能知道奥卡姆剃刀原理:如果一件事有两种解释,那么最可能正确的解释就是更简单的那种,即假设更少的那种。这个原理也适用于神经网络学到的模型:给定训练数据和网络架构,多组权重值(多个模型)都可以解释这些数据。简单模型比复杂模型更不容易过拟合。
这里的简单模型是指参数值分布的熵更小的模型(或参数更少的模型,比如上一节中的例子)。因此,降低过拟合的一种常见方法就是强制让模型权重只能取较小的值,从而限制模型的复杂度,这使得权重值的分布更加规则。这种方法叫作权重正则化(weight regularization),其实现方法是向模型损失函数中添加与较大权重值相关的成本(cost)。这种成本有两种形式。
L1正则化:添加的成本与权重系数的绝对值(权重的L1范数)成正比。
L2正则化:添加的成本与权重系数的平方(权重的L2范数)成正比。神经网络的L2正则化也叫作权重衰减(weight decay)。不要被不同的名称迷惑,权重衰减与L2正则化在数学上是完全相同的。
在Keras中,添加权重正则化的方法是向层中传入权重正则化项实例(weight regularizer instance)作为关键字参数。下面我们向最初的影评分类模型中添加L2权重正则化,代码如下所示:
(向模型中添加L2权重正则化)
from tensorflow.keras import regularizers
model = keras.Sequential([
layers.Dense(16,
kernel_regularizer=regularizers.l2(0.002),
activation="relu"),
layers.Dense(16,
kernel_regularizer=regularizers.l2(0.002),
activation="relu"),
layers.Dense(1, activation="sigmoid")
])
model.compile(optimizer="rmsprop",
loss="binary_crossentropy",
metrics=["accuracy"])
history_l2_reg = model.fit(
train_data, train_labels,
epochs=20, batch_size=512, validation_split=0.4)
在上述代码中,l2(0.002)的含义是该层权重矩阵的每个系数都会使模型总损失值增加0.002 * weight_coefficient_value ** 22。
训练过程:
注意,因为只在训练时添加这个惩罚项,所以该模型的训练损失会比测试损失大很多。
咱们画图对比一下:
上图展示了L2正则化惩罚项的影响。如你所见,虽然两个模型的参数个数相同,但具有L2正则化的模型比初始模型更不容易过拟合。
你还可以用Keras的权重正则化项来代替L2正则化项,代码如下所示:
(Keras中不同的权重正则化项)
from tensorflow.keras import regularizers
# L1正则化
regularizers.l1(0.001)
# 同时做L1正则化和L2正则化
regularizers.l1_l2(l1=0.001, l2=0.001)
请注意,权重正则化更常用于较小的深度学习模型。大型深度学习模型往往是过度参数化的,限制权重值大小对模型容量和泛化能力没有太大影响。
在这种情况下,应首选另一种正则化方法:dropout。
dropout是神经网络最常用且最有效的正则化方法之一,它由多伦多大学的Geoffrey Hinton和他的学生开发。
对某一层使用dropout,就是在训练过程中随机舍弃该层的一些输出特征(将其设为0)。
比方说,某一层在训练过程中对给定输入样本的返回值应该是向量[0.2, 0.5, 1.3, 0.8, 1.1]。使用dropout之后,这个向量会有随机几个元素变为0,比如变为[0, 0.5, 1.3, 0, 1.1]。dropout比率(dropout rate)是指被设为0的特征所占的比例,它通常介于0.2~0.5。测试时没有单元被舍弃,相应地,该层的输出值需要按dropout比率缩小,因为这时比训练时有更多的单元被激活,需要加以平衡。
考虑一个包含某层输出的NumPy矩阵layer_output,其形状为(batch_size,features)。训练时,我们随机将矩阵中的一些值设为0。
# 训练时,将50%的输出单元设为0
layer_output *= np.random.randint(0, high=2, size=layer_output.shape)
测试时,我们将输出按dropout比率缩小。这里我们乘以0.5(因为训练时舍弃了一半的单元)。
# 测试时
layer_output *= 0.5
注意,为实现这一过程,还可以在训练的同时完成两个运算,而测试时保持输出不变。这也是实践中常用的实现方法,如下图所示。
# 训练时
layer_output *= np.random.randint(0, high=2, size=layer_output.shape)
# 注意,这里是按比例放大,而不是按比例缩小
layer_output /= 0.5
(训练时对激活矩阵使用dropout,并在训练时按比例放大。测试时激活矩阵保持不变)
这一方法可能看起来有些奇怪和随意。为什么它能够降低过拟合?
Hinton说他的灵感之一来自于银行的防欺诈机制。用他自己的话来说:“我去银行,柜员不停地换人,我问其中一人这是为什么。他说他不知道,但他们经常换来换去。我想这一定是因为银行职员要想成功欺诈银行,他们之间要合作才行。这让我意识到,随机删除每个样本的一部分神经元,可以阻止‘阴谋’,从而降低过拟合。”dropout的核心思想是在层的输出值中引入噪声,打破不重要的偶然模式(也就是Hinton所说的“阴谋”)。如果没有噪声,那么神经网络将记住这些偶然模式。
在Keras中,你可以通过Dropout层向模型中引入dropout。dropout将被应用于前一层的输出。下面我们向IMDB模型中添加两个Dropout层,看看它降低过拟合的效果如何,代码如下所示:
(向IMDB模型中添加dropout)
model = keras.Sequential([
layers.Dense(16, activation="relu"),
layers.Dropout(0.5),
layers.Dense(16, activation="relu"),
layers.Dropout(0.5),
layers.Dense(1, activation="sigmoid")
])
model.compile(optimizer="rmsprop",
loss="binary_crossentropy",
metrics=["accuracy"])
history_dropout = model.fit(
train_data, train_labels,
epochs=20, batch_size=512, validation_split=0.4)
训练过程如下:
咱们画图演绎(小伙伴们应该可以自己编辑代码完成下图了,呵呵):
总之,要想将神经网络的泛化能力最大化,并防止过拟合,最常用的方法如下所述:
1)获取更多或更好的训练数据。
2)找到更好的特征。
3)缩减模型容量。
4)添加权重正则化(用于较小的模型)。
5)添加dropout。
小伙伴们可以自己演绎哈。