目录
一、梯度消失与梯度爆炸
1.1 Glorot 和 He 初始化
1.1.1 tf.keras.initializers.VarianceScaling
1.2 非饱和激活函数
1.2.1 tf.keras.layers.LeakyReLU
1.2.2 tf.keras.layers.PReLU
1.3 批量归一化
1.4 梯度裁剪
1.4.1 tf.keras.optimizers.SGD
二、重用预训练层——解决训练数据不同
2.1 Keras 迁移学习
2.1.1 tf.keras.models.clone_model
2.2 无监督预训练
2.3 辅助任务的预训练
三、优化器优化
3.1 动量优化
3.2 Nesterov 加速梯度
3.3 AdaGrad
3.4 RMSProp
3.5 Adam
3.6 学习率调度
3.6.1 幂调度
3.6.2 指数调度
tf.keras.callbacks.LearningRateScheduler
3.6.3 分段恒定调度
3.6.4 性能调度
tf.keras.callbacks.ReduceLROnPlateau
3.6.5 1 周期调度
四、正则化——避免过拟合
4.1 l1 和 l2 正则化
4.1.1 tf.keras.regularizers.L1
4.1.2 tf.keras.regularizers.L1L2
4.1.3 tf.keras.regularizers.L2
4.2 dropout
4.2.1 tf.keras.layers.Dropout
4.3 最大范数正则化
4.3.1 tf.keras.constraints.MaxNorm
五、小结
在实际模型训练中,可能遇到以下问题:
针对梯度不稳定问题,Glorot 提出希望信号流动过程既不消失也不饱和,使得每层的输出方差等于其输入的方差,除非每层的输入和输出神经元数量相等,否则无法很好的保证。对此提出了 Xavier 初始化或者 Glorot 初始化来初始化每层的连接权重。
Glorot 初始化(激活函数采用逻辑函数)
使用 Glorot 初始化可以大大加快训练速度。用 代替 就成为 LeCun 初始化。各种激活函数的初始化参数见下表:
初始化 | 激活函数 | |
---|---|---|
Glorot | None、tanh、逻辑、softmax | |
He | ReLU 和变体 | |
LeCun | SELU |
默认情况下,Keras 使用具有均匀分布的 Glorot 初始化,但可以在创建层时可以通过设置 kernel_initializer 参数来更改初始化方式。
import tensorflow as tf
tf.keras.layers.Dense(10, activation='relu', kernel_initilizer='he_noraml')
如果想使用均匀分布但是基于 的 He 初始化,则可以通过 Variance Scaling 初始化。
he_avg_init = tf.keras.initializers.VarianceScalling(scale=2, mode='fan_avg', distribution='uniform')
tf.keras.layers.Dense(10, activation='sigmoid', kernel_initilizer=he_avg_init)
tf.keras.initializers.VarianceScaling(
scale=1.0, mode='fan_in', distribution='truncated_normal',
seed=None
)
参数 | 注释 |
---|---|
scale | 缩放因子 |
mode | fan_in、fan_out、fan_avg 之一 |
distribution | 要使用的随机分布。“truncated_normal”、“untruncated_normal”和“uniform”之一。 |
其它 | 见 tf.keras.initializers.VarianceScaling | TensorFlow Core v2.6.0 |
梯度不稳定的一个原因就是由于激活函数选择不当;ReLU 激活函数由于对正值不饱和并且计算速度很快,所以表现要比逻辑函数好很多。但它存在神经元”死亡“问题,特别是在较大的学习率下,神经元权重调整时,输入的加权为负数,此时神经元就会死亡,只会继续输出0,因为 ReLU 激活函数的输入为负时梯度为 0。
通过对 ReLU 函数进行一些改动得到 LeakyRelU 函数,公式见下:
超参数 α 定义函数”泄露“程度,它时 z < 0 时函数的斜率,一般设置为 0.01,这就确保了神经元不会死亡但会陷入长时间的昏迷。同时也有观点认为将 α 设置为 0.2 (大泄露)会有更好的性能。也可以在训练中随机选择 α ,在测试过程将其固定为平均值,会有不错的表现。
也可以将 α 参数化得到 PReLU ,即在反向传播中修改,PReLU 在大型图像数据集的性能较优,但在小规模数据集上存在过拟合的风险。
LeakyReLU 的变体 ELU(指数线性单位)具有更好的性能:更短的训练时时间、更好的测试结果,其定义如下:
ELU 激活函数在 z 取负值时单元的平均输出接近 0 从而缓解梯度消失问题,超参数 α 定义了当 z 为较大负数时 ELU 逼近的值,一般设置为 1 ,并且 z 为负数时梯度不为 0 , 避免了神经元死亡问题;如果 α = 1 ,则函数在所有位置均平滑,有助于加速梯度下降。ELU 的计算速度较慢,但是会在训练过程具有更快的收敛速度。
SELU (可扩展的 ELU)激活函数是 ELU 的变体,如果构建一个仅由密集层堆叠组成的神经网络,并且所有隐藏层均使用 SELU 激活函数,则该网络是自归一化的,即每层的输出倾向于在训练过程中保留平均值 0 和标准差 1 ,从而解决了梯度不稳定的问题;就结果而言 SELU 大大优于其它激活函数。神经网络自归一化的条件如下:
激活函数的选择建议如下:
使用 LeakyReLu 、PReLU激活函数的代码参考如下:
model = tf.keras.models.Sequential([
# 其他层
tf.keras.layers.LeakyReLU(alpha=0.2) # leaky ReLU
tf.keras.layers.PReLU() # PReLU
# 其它层
])
使用 SELU 时,在创建层时设置 activation="selu" 和 kernel_initializer="lecun_normal":
tf.keras.layers.Dense(10, activation="selu", kernel_initializer="lecun_normal")
tf.keras.layers.LeakyReLU(
alpha=0.3, **kwargs
)
参数 | 注释 |
---|---|
alpha | 负斜率系数 |
tf.keras.layers.PReLU(
alpha_initializer='zeros', alpha_regularizer=None,
alpha_constraint=None, shared_axes=None, **kwargs
)
参数 | 注释 |
---|---|
alpha_initializer | 权重的初始化函数 |
alpha_regularizer | 权重的正则化器 |
alpha_constraint | 权重的约束 |
其它 | 见 tf.keras.layers.PReLU | TensorFlow Core v2.6.0 |
利用 Keras 实现批量归一化非常简单,只需要在每个隐藏层的激活函数之前或hi周添加一个 BatchNormalization 层,然后可选地在模型的第一层加一个 BN 层。
model = tf.keras.Sequential([
tf.keras.layers.Flatten(input_shape=[28, 28]),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Dense(300, activation='elu', kernel_initializer='he_normal'),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Dense(100, activation='elu', kernel_initializer='he_normal'),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Dense(10, activation='softmax')])
在层次较少的网络中批量归一化不会有比较明显的结果,但在深层次的网络中影响巨大。
模型摘要如下:
每个 BN 层为每个输入添加了 4 个参数,其中两个参数均值和标准差不受反向传播的影响,是不可训练参数。可以计算出 BN 添加的总共参数个数为:784*4 + 300*4 + 100*4 = 4736,而不可训练的参数为 4736 / 2 = 2368 个。
[(var.name, var.trainable) for var in model.layers[1].variables]
# 输出
[('batch_normalization_1/gamma:0', True),
('batch_normalization_1/beta:0', True),
('batch_normalization_1/moving_mean:0', False),
('batch_normalization_1/moving_variance:0', False)]
上述代码是在激活函数后添加的 BN 操作,若在激活函数前添加 BN ,则隐藏层中删除激活函数并将其作为单独层添加到 BN 之后,并且由于批量归一化的输入包含偏移参数,因此可以删除上一层的偏置项。
model = tf.keras.Sequential([
tf.keras.layers.Flatten(input_shape=[28, 28]),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Dense(300, kernel_initializer='he_normal', use_bias=False),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Activation('elu'),
tf.keras.layers.Dense(100, kernel_initializer='he_normal', use_bias=False),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Activation('elu'),
tf.keras.layers.Dense(10, activation='softmax')
])
梯度裁剪即在反向传播中裁剪梯度,使其不会超过某个阈值。Keras 实现代码如下:
optimizer = tf.keras.optimizers.SGD(clipvalue=1.0)
model.compile(loss='mse', optimizer=optimizer)
该优化器会将梯度向量的每个分量都限制在 -1.0 到 1.0 之间,但可能会改变梯度向量的方向,如会将 [0.9, 100.0] 裁剪成 [0.9, 1.0];若不想改变梯度向量的方向,应使用 clipnorm 参数代替 clipvalue。
tf.keras.optimizers.SGD(
learning_rate=0.01, momentum=0.0, nesterov=False, name='SGD', **kwargs
)
详情见 tf.keras.optimizers.SGD | TensorFlow Core v2.6.0
如果现有任务与曾经训练好的模型的功能相似,则可以通过重用已训练好模型的较低层来提高训练速度,减少训练数据,这就是迁移学习。
往往需要添加预处理步骤将其调整为原始模型所需的大小,当输入具有相似的低级特征时迁移学习最有效。任务越相似,可重用的层数越多。
首先冻结所有可重复使用的层,然后训练模型并查看表现;随后尝试解冻上部隐藏层的一两层并对其训练查看性能是否改善;训练数据越多解冻层数可以越多;解冻重用层也可以降低其学习率。
通过给 Sequential() 函数传入已训练好模型的重用层来创建第二个模型,注意两个模型共享低层,所以需要对已训练好的模型克隆并传递连接权重从而做好备份。
from tensorflow import keras
model_A = keras.models.load_model('../chapter_10/model1.h5')
model_A.summary()
model_B = keras.Sequential(model_A.layers[:-1])
model_B.add(keras.layers.Dense(1, activation='sigmoid'))
# 此时模型 A,B 共享隐藏层,所以训练模型 B 也会改变模型 A
# 对模型 A 克隆
model_clone = keras.models.clone_model(model_A)
model_clone.set_weights(model_A.get_weights())
# 训练的前几个轮次冻结重用层
for layer in model_B.layers[:-1]:
layer.trainable = False
model_B.compile(loss='binary_crossentropy', optimizer='sgd', metrics=['accuracy'])
# 后面开始训练模型
由于新模型 B 的输出层是随机初始化,因而会产生较大的错误,并导致较大的错误梯度,从而破坏重用权重;为此可以在前几个训练模型冻结重用的层。通过将各层的 trainable 属性设置为 False 来冻结该层。然后在训练几个轮次后再解冻重用层。
需要注意迁移学习在小型密集网络中不能很好地工作,迁移学习最适合使用深度卷积神经网络,更倾向于学习较为通用的特征检测器。
tf.keras.models.clone_model(
model, input_tensors=None, clone_function=None
)
参数 | 注释 |
---|---|
model | 模型实例 |
其它 | tf.keras.models.clone_model | TensorFlow Core v2.6.0 |
在训练数据较少,没有相似的训练模型,且收集带有标签的训练数据代价较大时,可以执行无监督预训练(如自动编码器或 GAN),然后可以重用自动编码器的较低层或 GAN 编码器的较低层,并在顶部添加输出层,然后使用有监督学习来微调最终网络。
可以在辅助任务上训练第一个神经网络并轻松获得或生成标记的训练数据,然后对实际任务重用该网络较低层。
第一个神经网络的较低层将学习特征检测器,第二个神经网络将重用这些特征检测。
传统的梯度下降通过直接减去权重的成本函数 的梯度乘以学习率 来更新权重 θ。公式为:
而动量优化则关心先前的梯度:每次迭代中,从动量向量 m 中减去局部梯度,并通过添加该动量向量来更新权重。
动量优化引入了一个超参数 β ,其值为 0 (高摩擦) 到 1(低摩擦)之间,一般取 0.9。但是在实践中会有很好的效果,一般比常规的梯度下降要快。
实现代码如下:
optimizer = keras.optimizers.SGD(learning_rate=0.001, momentum=0.9)
动量优化的一个变体就是 NAG 方法,它不是在局部位置 θ 处更新动量,而是在 θ+βm 处测量成本函数梯度。因为通常动量会指向正确的方向,因此使用在该方向上测得更远的梯度而不是原始位置上的梯度会更准确一些。NAG 通常比常规动量优化更快,使用方式如下:
optimizer = keras.optimizers.SGD(learning_rate=0.001, momentum=0.9, nesterov=True)
下降太快,容易无法收敛到全局最优解,不适合训练深度神经网络
optimizer = keras.optimizers.RMSprop(learning_rate=0.001, rho=0.9)
Adam 代表自适应矩估计,结合了动量优化和 RMSProp 的思想;它是一种自适应学习率算法,因此对超参数学习率只需要较少的调整。
optimizer = keras.optimizers.Adam(learning_rate=0.001, beta_1=0.9, beta_2=0.999)
将学习率设置为迭代次数 t 的函数: 。初始学习率、幂 c (一般是1)和步骤 s 是超参数,学习率在每一步中会下降,在 s 个步骤后下降到一半... 此调度下降速度由快到慢。
在 keras 中实现幂调度只需要设置 SGD 方法的超参数 decay,decay 即 s 的倒数,Keras 默认 c 的值是 1 。
optimizer = keras.optimizers.SGD(learning_rate=0.01, decay=1e-4)
将学习速率设置为 ,学习率每 s 步下降十倍。
from tensorflow import keras
# 指数调度
def exp_decay_fn(epoch):
return 0.01*0.1**(epoch/20)
# 如果不想对初始学习率和 s 进行硬编码,可以创建如下函数:
def exp_decay(lr0, s):
def exp_decay_fn(epoch):
return lr0 * 0.1 ** (epoch / 20)
return exp_decay_fn
# 然后创建一个 LearningRateScheduler 回调函数,并将该回调函数传递给 fit 方法的 callbacks 参数
lr_scheduler = keras.callbacks.LearningRateScheduler(exp_decay)
tf.keras.callbacks.LearningRateScheduler(
schedule, verbose=0
)
参数 | 注释 |
---|---|
schedule | 一个函数,它以轮次索引(整数,从 0 开始索引)和当前学习率(浮点数)作为输入,并返回一个新的学习率作为输出(浮点数) |
其它 | tf.keras.callbacks.LearningRateScheduler | TensorFlow Core v2.6.0 |
保存模型会同时保存优化率和学习器,所以加载训练后的模型也会加载该调度函数,但如果调度函数使用了 epoch 参数,epoch 参数不会被保存;对此需要在 fit 方法设置 initial_epoch 参数确定正确的 epoch 值。
对一些轮次使用恒定的学习率,对另外一些轮次使用较小的学习率等。
# 分段恒定调度
def piecewise_constant_fn(epoch):
if epoch < 5:
return 0.01
elif epoch < 15:
return 0.005
else:
return 0.001
# 后续同指数调度
每 N 步测量一次验证误差,并且当误差停止下降时,将学习率降低 λ 倍。
lr_scheduler = keras.callbacks.ReduceLROnPlateau(factor=0.5, patience=5)
上述代码意思是每当连续 5 个轮次的最好验证损失没有改善时,将学习率乘以 0.5 。
tf.keras.callbacks.ReduceLROnPlateau(
monitor='val_loss', factor=0.1, patience=10, verbose=0,
mode='auto', min_delta=0.0001, cooldown=0, min_lr=0, **kwargs
)
参数 | 注释 |
---|---|
factor | 学习率降低的倍数 |
patience | 没有改善的轮次数 |
其它 | tf.keras.callbacks.ReduceLROnPlateau | TensorFlow Core v2.6.0 |
该调度算法从提高初始学习率开始,中途线性增长到一个值,然后在训练的后半部分再次线性降低到初始学习率,最后的几个轮次再次将学习线性降低几个数量级。
在第十章中采用了提前停止来防止过拟合,在本章的第一小节采用了 BN 来解决不稳定梯度问题,以下将介绍其它正则化技术。
可以使用 l2 正则化来约束神经网络权重,也可以使用 l1 正则化来约束稀疏模型。参考代码如下:
from tensorflow import keras
layer = keras.layers.Dense(100, activation='elu',
kernel_initializer='he_normal',
kernel_regularizer=keras.regularizers.l2(0.01))
l1 正则化则只需要换成 l1() 方法,若想同时使用 l1 和 l2 正则化则可以调用 l1_l2() 方法。
为了避免写过多的重复方法,可以调用 functiools.partial() 函数,该函数可以使为带有一些默认参数值的任何可调用对象创建一个包装函数。
RegularizedDense = partial(keras.layers.Dense, activation='elu',
kernel_initializer='he_normal',
kernel_regularizer=keras.regularizers.l2(0.01))
model = keras.Sequential([
keras.layers.Flatten(input_shape=[28, 28]),
RegularizedDense(300),
RegularizedDense(100),
RegularizedDense(10, activation='softmax', kernel_initializer='glorot_uniform')
])
print(model.summary())
tf.keras.regularizers.L1(
l1=0.01, **kwargs
)
tf.keras.regularizers.L1L2(
l1=0.0, l2=0.0
)
tf.keras.regularizers.L2(
l2=0.01, **kwargs
)
dropout 算法在每个训练步骤中,每个神经元(包括输入神经元但不包括输出神经元)都以概率 p 被暂时删除,即在本个训练步骤中被忽略,但下一步可能被启用。超参数概率 p 被称作 dropout 率,一般在循环神经网络中设置为 20%~30% ,在深度神经网络中被设置为 40%~50%。训练后神经元不再删除。
dropout 训练完的神经元不能与相邻神经元相互适应,也不能过分依赖少数神经元,对输入的微小变化不太敏感,具有更好的鲁棒性,有更好的泛化能力。通常可以只对第一至第三层中的神经元应用 dropoout。
但需要注意,在训练后需要将每个神经元的输入连接权重乘以保留概率 (1-p),或者将每个神经元的输出除以保留概率。
Keras 实现 dropout 的代码如下:
model = keras.Sequential([
keras.layers.Flatten(input_shape=[28, 28]),
keras.layers.Dropout(rate=0.2),
RegularizedDense(300),
keras.layers.Dropout(rate=0.2),
RegularizedDense(100),
keras.layers.Dropout(rate=0.2),
RegularizedDense(10, activation='softmax', kernel_initializer='glorot_uniform')
])
dropout 仅在训练期间激活,因此比较训练损失和验证损失可能会产生误导。需确保没有使用 dropout 来评估训练损失。如果模型过拟合,可以提高 dropout 率;如果欠拟合则可以降低 dropout 率。如果完全 dropout 太强,则可以仅在最后一个隐藏层之后才使用 dropout。如果基于 SELU 激活函数,则应使用 alpha dropout ,这是 dropout 的变体,不会破坏网络的自归一化。
tf.keras.layers.Dropout(
rate, noise_shape=None, seed=None, **kwargs
)
参数见 tf.keras.layers.Dropout | TensorFlow Core v2.6.0
最大范数正则化会限制每个神经元的传入连接权重 ,使得 , 是 l2 范数。最大范数正则化不会把正则化损失项添加到总体损失函数中,而是在每个步骤后计算 ,减少 r 会增加正则化的数量,有助于减少过拟合,它还能缓解梯度不稳定的问题。
Keras 实现最大范数正则化的代码如下:
layer = keras.layers.Dense(100, activation='elu',
kernel_initializer='he_normal',
kernel_constraint=keras.constraints.max_noraml(1.))
max_normal 参数 axis 默认为 0,这意味着最大范数约束将独立应用于每个神经元的权重向量。
tf.keras.constraints.MaxNorm(
max_value=2, axis=0
)
参数见 tf.keras.constraints.MaxNorm | TensorFlow Core v2.6.0
超参数 | 默认值 |
---|---|
内核初始化 | He 初始化 |
激活函数 | ELU |
归一化 | 浅层网络:不需要; 深度网络:BN |
正则化 | 提前停止(可以加 l2 范数) |
优化器 | 动量优化(或者 RMSProp 或 Nadam) |
学习率调度 | 1 周期 |
如果网络是密集层的简单堆叠,则可以自归一化,此时可参考如下配置。
超参数 | 默认值 |
---|---|
内核初始化 | LeCun |
激活函数 | SELU |
归一化 | 不需要 |
正则化 | 如果需要可用 Alpha dropout |
优化器 | 动量优化(或者 RMSProp 或 Nadam) |
学习率调度 | 1 周期 |
以及: