梯度消失或梯度爆炸。这在训练过程中通过DNN反向传播,梯度变得越来越小或越来越大。这两个问题都使得较低的层很难训练。
对于如此大的网络,我们可能没有足够的训练数据,或者做标签的成本太高。
训练可能非常慢。
具有数百万个参数的模型会有严重过拟合的风险。
在本章,我们将研究这些问题。我们从探索梯度消失和梯度爆炸问题开始,接下来,我们将研究迁移学习和无监督预训练,即使在标签数据很少的情况下,也可以帮助我们解决复杂的问题,然后讨论极大加速训练大型模型的各种优化器,最后介绍一下正则化技术。
1. 梯度消失和 梯度爆炸问题
在反向传播过程中,传播误差对于网络中每个参数的梯度,更新每个参数。随着算法向下传播到较低层,梯度通常会越来越小。结果梯度下降更新使较低层的连接权重保持不变,训练并不能收敛到一个良好的解,这我们称为梯度消失。
某些情况下,可能会出现相反的情况:梯度可能越来越大,各层需要更新很大的权重直到算法发散,这是梯度爆炸。
1.1 权重初始化策略
为缓解不稳定梯度的方法,有人认为每层输出的方差等于其输入的方差,并且需要再反方向时流过某层之前和之后的梯度具有相同的方差。有人找到了一种初始化策略,称为Xavier初始化或者Glorot初始化。
默认情况下,Keras使用具有均匀分布的Glorot初始化。通过设置kernel_initializer="he_uniform" 或 kernel_initializer="he_normal"
from tensorflow import kerasras
keras.layers.Dense(10, activation="relu", kernel_initializer="he_normal")
<keras.layers.core.dense.Dense at 0x7f7e6d3ff940>
1.2 非饱和激活函数
有人认为,梯度不稳定的问题,部分由于激活函数选择不当导致。事实证明,Relu激活函数,由于它对正值不饱和,在深度神经网络中表现要好得多。但Relu并不完美,在训练过程中,某些神经元实际上“死亡”了,这意味着它们停止输出除0以外的任何值。当神经元的权重进行调整时,其输入的加权和所有实例均为负数,神经元会死亡,只会继续输出零。因此使用Relu函数的变体——ELU,还有像SELU。通常SELU>ELU>leaky ReLU > RELU > tanh > logistic.
keras.layers.Dense(10, activation="selu", kernel_initializer="lecun_normal")
<keras.layers.core.dense.Dense at 0x7f7e6d4b2e50>
1.3 批量归一化
尽管he初始化及ELU激活函数,一起使用可以显著减少在训练开始时的梯度消失/梯度爆炸问题,但这并不能不保证它们在训练期间不会再出现。有人提出批量归一化(BN)的技术来解决这些问题。该技术在模型中的每个隐藏层的激活函数之前或之后添加一个操作。该操作对每个输入零中心并归一化,然后每层使用两个新的参数向量缩放和偏移其结果。换句话说:该操作可以使模型学习各层输入的最佳缩放和均值。在许多情况下,如果将BN层添加为神经网络的第一层,则无须归一化训练集;BN层会为你完成此操作(因为它一次只能查看一个批次,它还可以重新缩放和偏移每个输入特征)。
为了使输入零中心并归一化,该算法需要估计每个输入的均值和标准差。通过评估当前小批次上的输入的均值和标准差来做到这一点。
因此在训练期间,BN会归一化其输入,然后重新缩放并偏移它们。那么在测试期间呢?这不那么简单,确实,我们可能需要对单个实例而并不是成批次的实例做预测。在这种情况下,我们无法计算每个输入的均值和标准差。而且,即使我们确实有一批次实例,它也可能太小,或者这些实例可能不是独立的和相同分布的,因此在这批实例上计算统计信息将不可靠。
在每个批归一化层中,会学习四个参数向量:缩放向量γ、偏移向量β和使用指数移动平均值估计的均值向量μ和标准差σ。请注意:μ和σ是在训练期间估算的,但仅在训练后使用。
提高了性能
梯度消失问题大大减少,加快了学习过程
增加模型复杂性
有运行时间的损失
用Keras实现批量归一化,实现批量归一化既简单又直观,只需在每个隐藏层的激活函数之前或之后添加一个BatchNormalization层。
model = keras.models.Sequential([
keras.layers.Flatten(input_shape=[28, 28]),
keras.layers.BatchNormalization(),
keras.layers.Dense(300, activation="elu", kernel_initializer="he_normal"),
keras.layers.BatchNormalization(),
keras.layers.Dense(100, activation="elu", kernel_initializer="he_normal"),
keras.layers.BatchNormalization(),
keras.layers.Dense(10, activation="softmax")
model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
flatten (Flatten) (None, 784) 0
batch_normalization (BatchN (None, 784) 3136
ormalization)
dense_3 (Dense) (None, 300) 235500
batch_normalization_1 (Batc (None, 300) 1200
hNormalization)
dense_4 (Dense) (None, 100) 30100
batch_normalization_2 (Batc (None, 100) 400
hNormalization)
dense_5 (Dense) (None, 10) 1010
=================================================================
Total params: 271,346
Trainable params: 268,978
Non-trainable params: 2,368
_________________________________________________________________
2022-02-26 12:20:11.093249: I tensorflow/core/platform/cpu_feature_guard.cc:151] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations: AVX2 FMA
To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.
[(var.name, var.trainable) for var in model.layers[1].variables]
[('batch_normalization/gamma:0', True),
('batch_normalization/beta:0', True),
('batch_normalization/moving_mean:0', False),
('batch_normalization/moving_variance:0', False)]
如果你想在激活函数之前添加BN层,需要再隐藏层中删除激活函数,并将其作为单独的层添加在BN层之后。此外由于归一化层的每个输入都包含一个偏移参数,因此可以从上一层中删除偏置项。
model = keras.models.Sequential([
keras.layers.Flatten(input_shape=[28, 28]),
keras.layers.BatchNormalization(),
keras.layers.Dense(300, kernel_initializer="he_normal", use_bias=False),
keras.layers.BatchNormalization(),
keras.layers.Activation("elu"),
keras.layers.Dense(100, kernel_initializer="he_normal", use_bias=False),
keras.layers.BatchNormalization(),
keras.layers.Activation("elu"),
keras.layers.Dense(10, activation="softmax")
1.4 梯度裁剪
缓解梯度爆炸问题是另一种流行技术是在反向传播期间裁剪梯度,使它们永远不会超过某个阈值,这称为梯度裁剪,常用语循环神经网络。因为在RNN难以使用批量归一化。
在Keras中,实现梯度裁剪仅仅是一个在创建优化器时设置clipvalue或cliopnorm参数问题
optimizer = keras.optimizers.SGD(clipvalue=1.0)
model.compile(loss="mse", optimizer=optimizer)
该优化器会将梯度向量的每个分量都裁剪为-1和1之间的值,这意味着所有损失的偏导数将限制在-1到1之间。注意:它可能会改改变梯度向量的方向,例如:原始梯度向量为[0.9, 100],则其大部分指向第二个轴的方向,但按值裁剪后,将得到[0.9, 1.0].该点大致指向两个轴之间的对角线。实际上,要确保“梯度裁剪”不更改梯度向量的方向,我们可以通过设置clipnorm而不是clipvalue按照范数来裁剪。如果l2范数大于我们选择的阈值,就会裁剪整个梯度,例如:设置clipnorm=1.0,则向量[0.9, 100]将被裁剪为[0.008999, 0.9999],都可以尝试使用(按值裁剪和按范数裁剪),看看哪个选择在验证集上表现更好。
2. 重用预训练层
从头开始训练非常大的DNN通常并不是一个好主意:相反,试图找到一个现有的与你要解决任务相似的神经网络,然后重用该网络的较低层,此技术称为迁移学习。它不仅会大大加快训练速度,而且会大大减少训练数据。
2.1 用Keras进行迁移学习
模型A: 除凉鞋和衬衫之外的8个类别的fashion MNIST数据集,构建了Keras模型。
模型B: 有凉鞋和衬衫的图像,想训练二分类器,但数据集非常小,使用与模型A相同的架构,性能也不错,但希望能更好。
我们意识到模型B的任务与模型A的任务非常相似,可以通过迁移学习去提升。
model_A = keras.models.load_model("my_model_A.h5")
model_B_on_A = keras.models.Sequential(model_A.layers[:-1])
model_B_on_A.add(keras.layers.Dense(1, activation="sigmoid"))
请注意,model_A和model_B_on_A现在共享一些层,当训练model_B_on_A时,也会影响model_A.如果想避免这种情况,需要在重用model_A的层之前对其进行克隆。使用clone_model()来克隆模型A的架构,然后复制其权重,因为clone_model()不会克隆权重。
model_A_clone = keras.models.clone_model(model_A)
model_A_clone.set_weights(model_A.get_weights())
现在可以为任务B训练model_B_on_A,但是由于新的输出层时随机初始化的,它会产生较大的错误,因此将存在较大的错误梯度。这可能会破坏重用的权重。为了避免这种情况,一种方法是在前几个轮次时冻结重用的层,给新层一些时间来学习合理的权重,为此,将可训练的属性设置为False并编译模型。
for layer in model_B_on_A.layers[:-1]:
layer.trainable = False
model_B_on_A.compile(loss="binary_crossentropy", optimizer="sgd", metrics=["accuracy"])
训练模型几个轮次,然后解冻重用的层,并继续进行训练来微调任务B,解冻重用层之后,降低学习率通常是个好主意,可以再次避免损坏重用的权重。
history = model_B_on_A.fit(X_train_B, y_train_B, epochs=4, validation_data=(X_valid_B, y_valid_B))
for layer in model_B_on_A.layers[:-1]:
layer.trainable=True
optimizer = keras.optimizers.SGD(learning_rate=1e-4)
model_B_on_A.compile(loss="binary_crossentropy", optimizer=optimizer, metrics=["accuracy"])
history = model_B_on_A.fit(X_train_B, y_train_B, epochs=16, validation_data=(X_valid_B, y_valid_B))
model_B_on_A.evaluate(X_test_B, y_test_B)
2.2 无监督预训练
假设要处理一个没有太多标签训练数据的复杂任务,且我们找不到类似任务上的训练模型。该怎么办?
首先,收集更多带有标签的训练数据,如果做不到,仍然可以执行无监督预训练。确实,收集未标记的训练实例很便宜,但标注它们却很昂贵。如果收集大量未标记的训练数据,则可以尝试使用它们来训练无监督模型,例如自动编码器或GAN。然后,可以重用自动编码器的较低层或GAN判别器的较低层,在顶部为你的任务添加输出层,并使用有监督学习(带有标记的训练实例)来微调最终的网络。
在无监督训练中,使用一个无监督学习技术对无标记数据进行训练,然后使用一个有监督学习技术对有标记数据进行最后任务的微调,无监督部分可以一次训练一层,也可以直接训练整个模型。
3. 更快的优化器
到目前为止我们知道四种加快训练速度的方法:连接权重初始化、好的激活函数、批量归一化、重用预训练网络。另外,与常规的梯度下降优化器相比,使用更快的优化器也可以带来巨大的速度提升。如:动量优化、Nesterov加速梯度、AdaGrad、RMSProp,最后事Adam和Nadam优化。
3.1 优化器
optimizer = keras.optimizers.SGD(learning_rate=0.001, momentum=0.9)
optimizer = keras.optimizers.SGD(learning_rate=0.001, momentum=0.9, nesterov=True)
optimizer = keras.optimizers.RMSprop(learning_rate=0.001, rho=0.9)
optimizer = keras.optimizers.Adam(learning_rate=0.001, beta_1=0.9, beta_2=0.999)
3.2 学习率调度
找到一个好的学习率非常重要,设置太高,训练可能会发散,设置太低,收敛到最优解,会花费很长时间。
最常用的学习率调度
分段恒定调度
optimizer = keras.optimizers.SGD(learning_rate=0.01, decay=1e-4)
def exponential_decay_fn(epoch):
return 0.01*0.1**(epoch/20)
lr_scheduler = keras.callbacks.LearningRateScheduler(exponential_decay_fn)
history = model.fit(X_train_scaled, y_train, [...], callbacks=[lr_scheduler])
分段恒定调度,与指数调度的方法类似,先定义调度函数,创建LearningRateScheduler函数,然后将其传给fit()方法
def piecewise_constant_fn(epoch):
if epoch < 5:
return 0.01
elif epoch < 15:
return 0.005
else:
return 0.0001
对于性能调度,使用ReduceLROnPlateau回调函数,如果将以下回调函数传递给fit()方法,则每当连续5个轮次的最好验证损失都没有改善时,它将使学习率乘以0.5.
lr_scheduler = keras.callbacks.ReduceLROnPlateau(factor=0.5, patience=5)
tf.keras提供了另一种学习率调度的方法:使用keras.optimizers.schedule中可以使用的调度之一来定义学习率,然后将该学习率传递给任意优化器。
s = 20 * len(X_train) // 32
learning_rate = keras.optimizers.schedules.ExponentialDecay(0.01, s, 0.1)
optimizer = keras.optimizers.SGD(learning_rate)
4. 通过正则化避免过拟合
流行的正则化技术:l1和l2正则化,dropout和最大范数正则化
4.1 l1和l2正则化
使用l2正则化来约束神经网络的连接权重,如果想要稀疏模型(许多权重等于0)则可以使用l1正则化。
layer = keras.layers.Dense(100, activation="elu",
kernel_initializer="he_normal",
kernel_regularizer=keras.regularizers.l2(0.01))
l2函数返回一个正则化函数,在训练过程中的每个步骤都将调用该正则化函数来计算正则化损失,然后将其添加到最终损失中。
通常你希望将相同的正则化函数应用于网络中的所有层,并在所有隐藏层中使用相同的激活函数和相同的初始化策略,因此你可能发现自己重复了相同的参数。这使代码很难看,且容易出错。为了避免这种情况,可以尝试使用循环来重构代码。另一种选择是使用Python中的functools.partial()函数,该函数可以使你为带有一些默认参数值的任何可调度对象创建一个小的包装函数。
from functools import partial
RegularizedDense = partial(keras.layers.Dense,
activation="elu",
kernel_initializer="he_normal",
kernel_regularizer=keras.regularizers.l2(0.01)
model = keras.models.Sequential([
keras.layers.Flatten(input_shape=[28, 28]),
RegularizedDense(300),
RegularizedDense(100),
RegularizedDense(10, activation="softmax",
kernel_initializer="glorot_uniform")
4.2 dropout正则化
这是一个非常简单的算法:每个训练步骤中,每个神经元(包括输入神经元,但始终不包括输出神经元)都有暂时“删除”的概率p.这意味着在这个训练步骤中它被完全忽略,但在下一步中可能处于活动状态。超参数p称为dropout率。在循环神经网络中接近20-30%,卷积神经网络40-50%。
训练后,我们需要将每个输入连接权重乘以保留概率(1-p)。
model = keras.models.Sequential([
keras.layers.Flatten(input_shape=[28, 28]),
keras.layers.Dropout(rate=0.2),
keras.layers.Dense(300, activation="elu", kernel_initializer="he_normal"),
keras.layers.Dropout(rate=0.2),
keras.layers.Dense(100, activation="elu", kernel_initializer="he_normal"),
keras.layers.Dropout(rate=0.2),
keras.layers.Dense(100, activation="softmax")
由于dropout仅在训练期间激活,因此比较训练损失和验证损失可能会产生误导。具体而言,模型可能会过拟合训练集,但仍具有相似的训练损失和验证损失。因此,请确保没有使用dropout来评估训练损失(训练之后)。
dropout确实会明显减慢收敛速度,但正确的微调,会导致更好的模型。
如果要基于SELU激活函数,对自归一化网络进行正则化,则应使用alpha dropout:这是dropout的一种变体,它保留了其输入的均值和标准差。
4.3 最大范数正则化
在Keras中实现最大范数正则化,将每个隐藏层的kernel_constraint参数设置为具有适当最大值的max_norm()约束。
keras.layers.Dense(100, activation="elu",kernel_initializer="he_normal",
kernel_constraint=keras.constraints.max_norm(1.)
<keras.layers.core.dense.Dense at 0x7f7e52cb61c0>
每次训练迭代后,模型的fit()方法会调用max_norm()返回的对象,将层的权重传递给该对象,并获得返回的缩放权重,然后替换该层的权重。