深度神经网络简介
深度神经网络,Deep Neural Networks,DNN
潜在的问题:
- 梯度消失问题或梯度爆炸问题,gradients growing ever smaller or larger
- 没有足够的训练数据,或者做标签的成本太高
- 训练速度太慢
- 具有数百万个参数的模型会有严重过拟合训练集的风险,尤其是在没有足够的训练实例或噪声太大的情况下
在本章中,我们将研究所有这些问题,并介绍解决这些问题的技术。我们从探索梯度消失和梯度爆炸问题及其一些受欢迎的解决方法开始。接下来,我们将研究迁移学习和无监督预训练,即使在标签数据很少的情况下,它们也可以帮助你解决复杂的任务。然后我们将讨论可以极大加速训练大型模型的各种优化器。最后我们将介绍一些流行的针对大型神经网络的正则化技术。
梯度消失与梯度爆炸问题
Unfortunately, gradients often get smaller and smaller as the algorithm progresses down to the lower layers. As a result, the gradient descent update leaves the lower layers’ connection weights virtually unchanged, and training never converges to a good solution. This is called the vanishing gradients problem. In some cases, the opposite can happen: the gradients can grow bigger and bigger until layers get insanely large weight updates and the algorithm diverges. This is the exploding gradients problem, which surfaces most often in recurrent neural networks. More generally, deep neural networks suffer from unstable gradients; different layers may learn at widely different speeds.
这些问题很久以前就凭经验观察到了,这也是深度神经网络在2000年代初期被大量抛弃的原因之一。目前尚不清楚是什么原因导致在训练DNN时使梯度如此不稳定,但是Xavier Glorot和Yoshua Bengio在2010年的一篇论文中阐明了一些观点。
可能是激活函数和初始化权重技术有问题。sigmod 函数均值为0.5,当输入变大(负数或正数)时,该函数会以0或1饱和,并且导数非常接近0,因此反向传播开始时它几乎没有梯度可以通过网络传播回去。当反向传播通过顶层向下传播时,存在的小梯度不断被稀释,因此对底层来说,实际上什么也没有留下。
Glorot和Bengio在它们的论文中提出了一种能显著缓解不稳定梯度问题的方法。他们指出,我们需要信号在两个方向上正确流动:进行预测时,信号为正向;在反向传播梯度时,信号为反向。我们既不希望信号消失,也不希望它爆炸并饱和。为了使信号正确流动,作者认为,我们需要每层输出的方差等于其输入的方差,并且我们需要在反方向时流过某层之前和之后的梯度具有相同的方差。
输入神经元: $ fan_{in} $ ,输出神经元 $ fan_{out} $ ,令 $ fan_{avg} = fan_{in} + fan_{out} $
这是使用sigmoid激活函数时的Glorot初始化。
如果将 $ fan_{in} $ 替换为 $ fan_{out} $ ,则可以得到 Yann LeCun 在90年代提出的LeCun initialization。当 $ fan_{in} = fan_{out} $ 时,LeCun初始化等效于Glorot初始化。It took over a decade for researchers to realize how important this trick is. Using Glorot initialization can speed up training considerably, and it is one of the practices that led to the success of deep learning.
还有一些论文为不同的激活函数提供了类似的初始化策略。
Initialization | Activation functions | σ² (Normal) |
---|---|---|
Glorot | None, tanh, sigmoid, softmax | 1 / fanavg |
He | ReLU, Leaky ReLU, ELU, GELU, Swish, Mish | 2 / fanin |
LeCun | SELU | 1 / fanin |
默认情况下,Keras使用具有均匀分布的Glorot初始化,可以设置 kernel_initializer="he_uniform"
或 kernel_initializer="he_normal"
来将其修改为He初始化,
import tensorflow as tf
dense = tf.keras.layers.Dense(50, activation="relu", kernel_initializer="he_normal")
也可以使用上面的表格里的其他方法,也可以使用 Variance Scaling 初始化
For example, if you want He initialization with a uniform distribution and based on $ fan_{avg} $ (rather than $ fan_{in} $ ), you can use the following code:
he_avg_init = tf.keras.initializers.VarianceScaling(scale=2., mode="fan_avg", distribution="uniform")
dense = tf.keras.layers.Dense(50, activation="sigmoid", kernel_initializer=he_avg_init)
更好的激活函数
看看下面这段话,有点搞笑:
One of the insights in the 2010 paper by Glorot and Bengio was that the problems with unstable gradients were in part due to a poor choice of activation function. Until then most people had assumed that if Mother Nature had chosen to use roughly sigmoid activation functions in biological neurons, they must be an excellent choice. But it turns out that other activation functions behave much better in deep neural networks—in particular, the ReLU activation function, mostly because it does not saturate(包和) for positive values, and also because it is very fast to compute.
Unfortunately, the ReLU activation function is not perfect. It suffers from a problem known as the dying ReLUs: during training, some neurons effectively “die”, meaning they stop outputting anything other than 0. In some cases, you may find that half of your network’s neurons are dead, especially if you used a large learning rate. A neuron dies when its weights get tweaked in such a way that the input of the ReLU function (i.e., the weighted sum of the neuron’s inputs plus its bias term) is negative for all instances in the training set. When this happens, it just keeps outputting zeros, and gradient descent does not affect it anymore because the gradient of the ReLU function is zero when its input is negative. 注:除非它是第一个隐藏层的一部分,否则死亡的神经元有时可能会复活:梯度下降可能确实会调整下面各层中的神经元,使得死亡神经元输入的加权和再次为正。
Leaky ReLU
ReLU的变体,例如leaky ReLU
The leaky ReLU activation function is defined as LeakyReLUα(z) = max(αz, z)
超参数 $ \alpha $ 定义了函数“泄漏”的程度: z < 0 时函数的斜率,通常设置为0.01,这个小斜率保证了leaky ReLU 永远不会死亡,它们可能会陷入长时间的昏迷,但是有机会最后醒来。
2015年 Bing Xu et al. 的一篇论文比较了ReLU激活函数的几种变体,其中有结论为:泄漏的变体(the leaky variants)要好于严格的ReLU激活函数,设置 $ \alpha = 0.2 $ 似乎比 $ \alpha = 0.01 $ 会产生更好的性能。论文还研究了随机泄漏ReLU, randomized leaky ReLU (RReLU) ,训练过程中在给定范围内随机选择 $ \alpha $ ,测试过程中将其固定为平均值,它的表现也相当不错,似乎可以充当正则化函数,最后论文评估了参数化泄漏ReLU函数, the parametric leaky ReLU (PReLU) ,其中的 α 可以在训练期间学习,可以通过反向传播进行更改, PReLU was reported to strongly outperform ReLU on large image datasets, but on smaller datasets it runs the risk of overfitting the training set.
Keras includes the classes LeakyReLU
and PReLU
in the tf.keras.layers
package. Just like for other ReLU variants, you should use He initialization with these. For example:
leaky_relu = tf.keras.layers.LeakyReLU(alpha=0.2) # defaults to alpha=0.3
dense = tf.keras.layers.Dense(50, activation=leaky_relu, kernel_initializer="he_normal")
If you prefer, you can also use LeakyReLU as a separate layer in your model; it makes no difference for training and predictions:
model = tf.keras.models.Sequential([
[...] # more layers
tf.keras.layers.Dense(50, kernel_initializer="he_normal"), # no activation
tf.keras.layers.LeakyReLU(alpha=0.2), # activation as a separate layer
[...] # more layers
])
For PReLU, replace LeakyReLU with PReLU. There is currently no official implementation of RReLU in Keras, but you can fairly easily implement your own .
ReLU, leaky ReLU, and PReLU all suffer from the fact that they are not smooth functions: their derivatives abruptly change (at z = 0). 这种不连续性会使梯度下降在最优值附近反弹,并减缓收敛速度。
ELU and SELU
In 2015, a paper by Djork-Arné Clevert et al. proposed a new activation function, called the exponential linear unit (ELU,指数线性单元), that outperformed all the ReLU variants in the authors’ experiments: training time was reduced, and the neural network performed better on the test set. Equation 11-2 shows this activation function’s definition.
z小于0时函数取负值,这使得该单元的平均输出接近于0,有助于缓解梯度消失,超参数 $ \alpha $ 定义一个值通常取1,也可以调整,这个值是当z为较大负数时ELU函数逼近的值。z小于0时具有非0梯度,能避免神经元死亡。如果 $ \alpha $ 取1,那么该函数在所有位置都平滑,有助于加速梯度下降。
缺点是计算较慢(指数函数),训练时收敛速度快,因此训练速度能弥补,但是测试时ELU网络将比ReLU网络慢。
Using ELU with Keras is as easy as setting activation="elu"
, and like with other ReLU variants, you should use He initialization.
2017年, Günter Klambauer et al.提出the scaled ELU (SELU) activation,可扩展的指数性单元激活函数,。作者表明,如果你构建一个仅由密集层堆叠组成的神经网络,并且如果所有隐藏层都使用SELU激活函数,则该网络是自归一化的:每层的输出倾向于在训练过程中保留平均值0和标准差1,从而解决了梯度消失/梯度爆炸的问题。结果,SELU激活函数通常大大优于这些神经网络(尤其是深层神经网络)的其他激活函数。但是,有一些产生自归一化的条件:
- 输入特征必须是标准化的
- 每个隐藏层的权重必须使用LeCun正态初始化,kernel_initializer="lecun_normal"
- 网络的架构必须是顺序的。循环神经网络RNN或者 Wide & Deep 网络,都不能保证自归一化
- 不能使用正则化技术,You cannot use regularization techniques like ℓ1 or ℓ2 regularization, max-norm, batch-norm, or regular dropout (these are discussed later in this chapter).
这些条件都很致命。
GELU, Swish, and Mish
GELU was introduced in a 2016 paper by Dan Hendrycks and Kevin Gimpel. Once again, you can think of it as a smooth variant of the ReLU activation function.
where $ \Phi $ is the standard Gaussian cumulative distribution function (CDF,标准高斯累积分布函数): $ \Phi(z) $ corresponds to the probability that a value sampled randomly from a normal distribution of mean 0 and variance 1 is lower than z.
As you can see in Figure 11-4, GELU resembles(像) ReLU: it approaches 0 when its input z is very negative, and it approaches z when z is very positive. However, whereas all the activation functions we’ve discussed so far were both convex(凸) and monotonic(单调), the GELU activation function is neither: from left to right, it starts by going straight, then it wiggles down, reaches a low point around –0.17 (near z ≈ –0.75), and finally bounces up and ends up going straight toward the top right. 这种相当复杂的形状,以及它在每一点都有曲率的事实,可以解释为什么它能如此出色地工作,尤其是对于复杂的任务:梯度下降可能会发现更容易适应复杂的模式。在实践中,它通常优于迄今为止讨论的所有其他激活函数。 However, it is a bit more computationally intensive, and the performance boost it provides is not always sufficient to justify the extra cost. That said, it is possible to show that it is approximately equal to zσ(1.702 z), where σ is the sigmoid function: using this approximation also works very well, and it has the advantage of being much faster to compute.
The GELU paper also introduced the sigmoid linear unit (SiLU) activation function, which is equal to zσ(z), but it was outperformed by GELU in the authors’ tests. Interestingly, a 2017 paper by Prajit Ramachandran et al.10 rediscovered the SiLU function by automatically searching for good activation functions. The authors named it Swish, and the name caught on. In their paper, Swish outperformed every other function, including GELU.Ramachandran et al. later generalized Swish by adding an extra hyperparameter β to scale the sigmoid function’s input. The generalized Swish function is Swishβ(z) = zσ(βz), so GELU is approximately equal to the generalized Swish function using β = 1.702. You can tune β like any other hyperparameter.Alternatively, it’s also possible to make β trainable and let gradient descent optimize it: much like PReLU, this can make your model more powerful, but it also runs the risk of overfitting the data.
Another quite similar activation function is Mish, which was introduced in a 2019 paper by Diganta Misra.11 It is defined as mish(z) = ztanh(softplus(z)), where softplus(z) = log(1 + exp(z)). 与GELU and Swish一样,光滑,非凸,非单调。
So, which activation function should you use for the hidden layers of your deep neural networks? ReLU remains a good default for simple tasks: it’s often just as good as the more sophisticated(复杂的) activation functions, plus it’s very fast to compute, and many libraries and hardware accelerators provide ReLU-specific optimizations. However, Swish is probably a better default for more complex tasks, and you can even try parametrized Swish with a learnable β parameter for the most complex tasks. Mish may give you slightly better results, but it requires a bit more compute. If you care a lot about runtime latency, then you may prefer leaky ReLU, or parametrized leaky ReLU for more complex tasks. For deep MLPs, give SELU a try, but make sure to respect the constraints listed earlier. If you have spare time and computing power, you can use cross-validation to evaluate other activation functions as well.
activation="gelu"
activation="swish"
Keras supports GELU and Swish out of the box,但是没有支持Mish or the generalized Swish activation function,可以自行实现。
Batch Normalization(批归一化)
该技术包括在模型中的每个隐藏层的激活函数之前或之后添加一个操作。该操作对每个输入零中心并归一化,然后每层使用两个新的参数向量缩放和偏移其结果:一个用于缩放,另一个用于偏移。换句话说,该操作可以使模型学习各层输入的最佳缩放和均值。在许多情况下,如果你将BN层添加为神经网络的第一层,则无须归一化训练集(例如,使用StandardScaler或Normalization);BN层会为你完成此操作(因为它一次只能查看一个批次,它还可以重新缩放和偏移每个输入特征)。
为了使输入零中心并归一化,该算法需要估计每个输入的均值和标准差。通过评估当前小批次上的输入的均值和标准差(因此称为“批量归一化”)来做到这一点。
- $\vec \mu_{B} $ 是输入均值的向量,在整个小批量B上评估(每个输入包含一个均值)
- $\vec \sigma_{B} $ 是输入标准差的向量,也在整个小批量中进行评估(每个输入包含一个标准差)
- $ m_{B} $ 是小批量中的实例数量。
- $\hat x^{(i)} $ 是实例i的零中心和归一化输入的向量。
- $ \gamma $ 是该层的输出缩放参数向量(每个输入包含一个缩放参数)
- ⊗ 表示逐元素乘法(每个输入乘以其相应的输出缩放参数)。
- $ \beta $ 是层的输出移动(偏移)参数向量(每个输入包含一个偏移参数)。每个输入都通过其相应的移动参数进行偏移。
- $ \epsilon $ 平滑项,一个很小的数字,避免被0除,通常为10e-5
- $\vec z^{(i)} $是BN操作的输出。它是输入的缩放和偏移版本。
因此在训练期间,BN会归一化其输入,然后重新缩放并偏移它们。好!那在测试期间呢?这不那么简单。确实,我们可能需要对单个实例而不是成批次的实例做出预测:在这种情况下,我们无法计算每个输入的均值和标准差。而且,即使我们确实有一批次实例,它也可能太小,或者这些实例可能不是独立的和相同分布的,因此在这批实例上计算统计信息将是不可靠的。一种解决方法是等到训练结束,然后通过神经网络运行整个训练集,计算BN层每个输入的均值和标准差。然后,在进行预测时,可以使用这些“最终”的输入均值和标准差,而不是一个批次的输入均值和标准差。然而,大多数批量归一化的实现都是通过使用该层输入的均值和标准差的移动平均值来估计训练期间的最终统计信息。这是Keras在使用BatchNormalization层时自动执行的操作。综上所述,在每个批归一化层中学习了四个参数向量:通过常规反向传播学习γ(输出缩放向量)和β(输出偏移向量),和使用指数移动平均值估计的μ(最终的输入均值向量)和σ(最终输入标准差向量)。请注意,μ和σ是在训练期间估算的,但仅在训练后使用。
Ioffe和Szegedy证明,批量归一化极大地改善了他们试验过的所有深度神经网络,从而极大地提高了ImageNet分类任务的性能。
批量归一化应用于最先进的图像分类模型,以少14倍的训练步骤即可达到相同的精度,在很大程度上击败了原始模型……使用批量归一化网络的集成,我们在ImageNet分类中改进了已发布的最好结果:前5位的验证错误达到了4.9%(和4.8%的测试错误),超过了人工评分者的准确性。
增加了模型的复杂性,运行时间增长。幸运的是,经常可以在训练后将BN层与上一层融合,从而避免了运行时的损失,TFLite的优化器会自动执行该操作。
你可能会发现训练相当慢,因为当你使用批量归一化时每个轮次要花费更多时间。通常情况下,这被BN的收敛速度要快得多的事实而抵消,因此达到相同性能所需的轮次更少。总时间会减少。
Implementing batch normalization with Keras
As with most things with Keras, implementing batch normalization is straightforward and intuitive. Just add a BatchNormalization layer before or after each hidden layer’s activation function. You may also add a BN layer as the first layer in your model, but a plain Normalization layer generally performs just as well in this location (its only drawback is that you must first call its adapt() method). For example, this model applies BN after every hidden layer and as the first layer in the model (after flattening the input images):
model = tf.keras.Sequential([
tf.keras.layers.Flatten(input_shape=[28, 28]),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Dense(300, activation="relu", kernel_initializer="he_normal"),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Dense(100, activation="relu", kernel_initializer="he_normal"),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Dense(10, activation="softmax")
])
在这个只有两个隐藏层的小示例中,批量归一化不可能产生非常积极的影响。但是对于更深层的网络,它可以带来巨大的改变。
model.summary()
每个BN层的每个输入添加了四个参数。Non-trainable params
的含义是不受反向传播的影响。
查看第一层参数
>>> [(var.name, var.trainable) for var in model.layers[1].variables]
BN论文的作者主张在激活函数之前(而不是之后)添加BN层(就像我们刚才所做的那样)。关于此问题,存在一些争论,哪个更好取决于你的任务——你也可以对此进行试验,看看哪个选择最适合你的数据集。要在激活函数之前添加BN层,必须从隐藏层中删除激活函数,并将其作为单独的层添加到BN层之后。此外,由于批量归一化层的每个输入都包含一个偏移参数,因此你可以从上一层中删除偏置项(创建时只需传递 use_bias=False
即可):
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")
])
BatchNormalization类具有许多可以调整的超参数。默认值通常可以,但是你偶尔可能需要调整 omentum
。BatchNormalization层在更新指数移动平均值时使用此超参数。
一个良好的动量值通常接近1;例如0.9、0.99或0.999(对于较大的数据集和较小的批处理,你需要更多的9)
另一个重要的超参数是axis:它确定哪个轴应该被归一化。默认为-1,这意味着默认情况下它将对最后一个轴进行归一化(使用跨其他轴计算得到的均值和标准差)。当输入批次为2D(即批次形状为[批次大小,特征])时,这意味着将基于在批次中所有实例上计算得到的均值和标准差对每个输入特征进行归一化。例如,先前代码示例中的第一个BN层将独立地归一化(重新缩放和偏移)784个输入特征中的每一个。如果将第一个BN层移动到Flatten层之前,则输入批次将为3D,形状为[批次大小,高度,宽度]。因此,BN层将计算28个均值和28个标准差(每列像素1个,在批次中的所有实例以及在列中所有行之间计算),它将使用相同的均值和标准差对给定列中的所有像素进行归一化。也是只有28个缩放参数和28个偏移参数。相反,你如果仍然要独立的处理784个像素中的每一个,则应设置axis=[1,2]。
BatchNormalization已成为深度神经网络(dnn)中最常用的层之一,特别是深度卷积神经网络deep convolutional neural networks,以至于在图表中通常将其省略,因为假定在每层之后都添加了BN。
梯度裁剪 Gradient Clipping
在反向传播期间裁剪梯度,使它们永远不会超过某个阈值。这称为梯度裁剪,常用于循环神经网络。
In Keras, implementing gradient clipping is just a matter of setting the clipvalue or clipnorm argument when creating an optimizer, like this:
optimizer = tf.keras.optimizers.SGD(clipvalue=1.0)
model.compile([...], optimizer=optimizer)
该优化器会将梯度向量的每个分量都裁剪为-1.0和1.0之间的值。这意味着所有损失的偏导数(相对于每个可训练的参数)将限制在-1.0和1.0之间。
阈值是你可以调整的超参数。注意,它可能会改变梯度向量的方向。例如,如果原始梯度向量为[0.9,100.0],则其大部分指向第二个轴的方向。但是按值裁剪后,将得到[0.9,1.0],该点大致指向两个轴之间的对角线。实际上,这种方法行之有效。
如果要确保“梯度
裁剪”不更改梯度向量的方向,你应该通过设置clipnorm而不是clipvalue按照范数来裁剪。如果2范数大于你选择的阈值,则会裁剪整个梯度。例如,如果你设置 clipnorm=1.0
,则向量[0.9,100.0]将被裁剪为[0.00899964,0.9999595],保留其方向,但几乎消除了第一个分量。如果你观察到了在训练过程中梯度爆炸(可以使用TensorBoard跟踪梯度的大小),可能要尝试使用两种方法(按值裁剪和按范数裁剪),看看哪个选择在验证集上表现更好。
迁移学习
书上这一节名称是 Reusing Pretrained Layers ,翻译过来是 重用预训练层 ,实际上就是迁移学习,transfer learning 。
它不仅会大大加快训练速度,而且会大大减少训练数据。
如果新任务的输入图片的大小与原始任务中使用的图片不同,通常必须添加预处理步骤将其调整为原始模型所需的大小。一般而言,当输入具有类似的低级特征时,迁移学习最有效。
通常应该替换掉原始模型的输出层,因为它对于新任务很有可能根本没有用,甚至对于新任务而言,可能没有正确数量的输出。类似地,原始模型的上部分隐藏层不太可能像下部分那样有用,因为对新任务最有用的高级特征可能与对原始任务最有用的特征有很大的不同。你需要找到正确的层数来重用。
任务越相似,可重用的层越多(从较低的层开始)。对于非常相似的任务,请尝试保留所有的隐藏层和只是替换掉输出层。
首先尝试冻结所有可重复使用的层(使其权重不可训练,这样梯度下降就不会对其进行修改),训练模型并查看其表现。然后尝试解冻上部隐藏层中的一两层,使反向传播可以对其进行调整,再查看性能是否有所提高。你拥有的训练数据越多,可以解冻的层就越多。当解冻重用层时,降低学习率也很有用,可以避免破坏其已经调整好的权重。
如果你仍然无法获得良好的性能,并且你的训练数据很少,试着去掉顶部的隐藏层,然后再次冻结所有其余的隐藏层。你可以进行迭代,直到找到合适的可以重复使用的层数。如果你有大量的训练数据,则可以尝试替换顶部的隐藏层而不是去掉它们,你甚至可以添加更多的隐藏层。
总结:就是玄学……一点也没有数学上的优雅。
用Keras进行迁移学习
举例如下:
首先,你需要加载模型A并基于该模型的层创建一个新模型。让我们重用除输出层之外的所有层:
[...] # Assuming model A was already trained and saved to "my_model_A"
model_A = tf.keras.models.load_model("my_model_A")
model_B_on_A = tf.keras.Sequential(model_A.layers[:-1])
model_B_on_A.add(tf.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 = tf.keras.models.clone_model(model_A)
model_A_clone.set_weights(model_A.get_weights())
tf.keras.models.clone_model()
only clones the architecture, not the weights. If you don’t copy them manually usingset_weights()
, they will be initialized randomly when the cloned model is first used.
现在你可以为任务B训练model_B_on_A,但是由于新的输出层是随机初始化的,它会产生较大的错误(至少在前几个轮次内),因此将存在较大的错误梯度,这可能会破坏重用的权重。为了避免这种情况,一种方法是在前几个轮次时冻结重用的层,给新层一些时间来学习合理的权重。为此,请将每一层的可训练属性设置为False并编译模型:
for layer in model_B_on_A.layers[:-1]:
layer.trainable = False
optimizer = tf.keras.optimizers.SGD(learning_rate=0.001)
model_B_on_A.compile(loss="binary_crossentropy", optimizer=optimizer, 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 = tf.keras.optimizers.SGD(learning_rate=0.001)
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))
那么,最终结果是什么?好了,该模型的测试精度为99.25%,这意味着迁移学习将错误率从2.8%降低到了几乎0.7%!那是四倍的差距!
你被说服了吗?你不应该:我作弊了!我尝试了许多配置,直到发现一个有明显改进的配置。如果你试着更改类别或随机种子,会发现改进通常会下降,甚至消失或反转。我所做的就是“折磨数据直到信服为止”。当论文看起来过于优秀时,你应该要怀疑:也许这个浮华的新技术实际上并没有多大帮助(事实上,它甚至可能降低性能),但作者尝试了许多变体,仅报告了最好的结果(这可能是由于运气所致),而没有提及他们在途中遇到了多少次失败。在大多数情况下,这根本不是恶意的,但这是造成如此多的科学结果永远无法复现的部分原因。
我为什么要作弊?事实证明,迁移学习在小型密集型网络中不能很好地工作,大概是因为小型网络学习的模式很少,密集网络学习的是非常特定的模式,这在其他任务中不是很有用。迁移学习最适合使用深度卷积神经网络,该神经网络倾向于学习更为通用的特征检测器(尤其是在较低层)。我们将在第14章中使用刚刚讨论的技术重新审视迁移学习(我保证,这一次不会作弊!)。
无监督预训练 Unsupervised Pretraining
收集未标记的训练实例通常很便宜,但标记它们却很昂贵。如果你可以收集大量未标记的训练数据,则可以尝试使用它们来训练无监督模型,例如自动编码器或GAN(generative adversarial network 生成式对抗神经网络)。然后,你可以重用自动编码器的较低层或GAN判别器的较低层,在顶部为你的任务添加输出层,并使用有监督学习(即带有标记的训练实例)来微调最终的网络。
Geoffrey Hinton和他的团队在2006年使用的正是这种技术,它引起了神经网络的复兴以及深度学习的成功。Until 2010, unsupervised pretraining—typically with restricted Boltzmann machines (RBMs; see the notebook at https://homl.info/extra-anns)—was the norm for deep nets, and only after the vanishing gradients problem was alleviated did it become much more common to train DNNs purely using supervised learning. Unsupervised pretraining (today typically using autoencoders or GANs rather than RBMs) is still a good option when you have a complex task to solve, no similar model you can reuse, and little labeled training data but plenty of unlabeled training data.
请注意,在深度学习的早期很难训练深度模型,因此人们会使用一种称为贪婪逐层预训练(greedy layer-wise pretraining)的技术(如图11-5所示)。它们首先训练一个单层的无监督模型,通常是RBM,然后冻结该层并在其之上添加另一个层,然后再次训练模型(实际上只是训练新层),然后冻结新层并在其上添加另一层,再次训练模型,以此类推。Nowadays, things are much simpler: people generally train the full unsupervised model in one shot and use autoencoders or GANs rather than RBMs.
In unsupervised training, a model is trained on all data, including the unlabeled data, using an unsupervised learning technique, then it is fine-tuned for the final task on just the labeled data using a supervised learning technique; the unsupervised part may train one layer at a time as shown here, or it may train the full model directly
辅助任务的预训练
如果你没有太多标记的训练数据,最后一个选择是在辅助任务上训练第一个神经网络,你可以轻松地为其获得或生成标记的训练数据,然后对实际任务重用该网络的较低层。第一个神经网络的较低层将学习特征检测器,第二个神经网络可能会重用这些特征检测器。
例如,如果你要构建一个识别人脸的系统,每个人可能只有几张照片,显然不足以训练一个好的分类器。收集每个人的数百张图片不切实际。但是,你可以在网络上收集很多随机人物的图片,然后训练第一个神经网络来检测两个不同的图片是否是同一个人。这样的网络将会学习到很好的人脸特征检测器,因此重用其较低层可以使你用很少的训练数据来训练一个好的人脸分类器。
对于自然语言处理(NLP, natural language processing )应用,你可以下载数百万个文本文档的语料库并从中自动生成带标签的数据。例如,你可以随机屏蔽一些单词并训练模型来预测缺失的单词(例如,它应该能预测句子“What___you saying”中的缺失单词可能是“are”或者“were”)。如果你可以训练模型在此任务上达到良好的性能,那么它已经对语言有相当多的了解,你当然可以在实际任务中重用它并在带标签的数据上进行微调。
自监督学习,Self-supervised learning,是指你从数据本身自动生成标签,然后使用有监督学习技术在所得到的“标签”数据集上训练模型。由于此方法不需要任何人工标记,因此最好将其分类为无监督学习的一种形式。
更快的优化器
Training a very large deep neural network can be painfully slow. So far we have seen four ways to speed up training (and reach a better solution): applying a good initialization strategy for the connection weights, using a good activation function, using batch normalization, and reusing parts of a pretrained network (possibly built for an auxiliary task or using unsupervised learning). Another huge speed boost comes from using a faster optimizer than the regular gradient descent optimizer. In this section we will present the most popular optimization algorithms: momentum, Nesterov accelerated gradient, AdaGrad, RMSProp, and finally Adam and its variants.
动量优化
梯度下降通过直接减去关于权重的成本函数 $ J(\pmb{\theta}) $ 的梯度乘以学习率 $ \eta $ 来更新权重,它不关心较早的梯度是什么。如果局部梯度很小,则它会走得非常缓慢。
动量优化非常关系先前的梯度是什么,每次迭代时,它都会从动量向量 m 中减去局部梯度,并通过添加该动量向量来更新权重,梯度是用于加速度而不是速度。 为了模拟某种摩擦机制并防止动量变得过大,该算法引入了一个新的超参数 $ \beta $ 称为动量,必须将其设置为0(高摩擦)和1(无摩擦)之间。典型的动量值为0.9。
可以证明,如果梯度保持恒定,那么最终速度(即权重更新的最大大小)等于该梯度乘以学习率 $ \eta $ 再乘以 $ \frac{1}{1-\beta} $ ,例如如果 $ \beta=0.9 $ 那么最终速度等于梯度乘以学习率的10倍,因此动量优化最终比梯度下降快10倍!这使动量优化比梯度下降要更快得地从平台逃脱。我们此前看到,当输入的比例非常不同时,成本函数将看起来像一个拉长的碗。梯度下降相当快地沿着陡峭的斜坡下降,但是沿着山谷下降需要很长时间。相反,动量优化将沿着山谷滚动得越来越快,直到达到谷底(最优解)。在不使用批量归一化的深层神经网络中,较高的层通常会得到比例不同的输入,因此使用动量优化会有所帮助。它还可以帮助绕过局部优化问题。
由于这种动量势头,优化器可能会稍微过调,然后又回来,再次过调,在稳定于最小点之前会多次振荡。这是在系统中有一些摩擦力的原因之一:它消除了这些振荡,从而加快了收敛速度。
Implementing momentum optimization in Keras is a no-brainer: just use the SGD optimizer and set its momentum hyperparameter, then lie back and profit!
optimizer = tf.keras.optimizers.SGD(learning_rate=0.001, momentum=0.9)
动量优化的一个缺点是它增加了另一个超参数来调整。但是,动量值为0.9通常在实践中效果很好,几乎总是比常规的“梯度下降”更快。
Nesterov Accelerated Gradient
Yurii Nesterov于1983年提出的动量优化的一个小变体几乎总是比原始动量优化要快。Nesterov加速梯度(Nesterov AcceleratedGradient,NAG)方法也称为Nesterov动量优化,它不是在局部位置 θ ,而是在 θ+βm 处沿动量方向稍微提前处测量成本函数的梯度:
如图,NAG通常更快。
NAG通常比常规动量优化更快。要使用它,只需在创建SGD优化器时设置 nesterov=True
即可:
optimizer = tf.keras.optimizers.SGD(learning_rate=0.001, momentum=0.9, nesterov=True)
AdaGrad
梯度下降从快速沿最陡的坡度下降开始,该坡度没有直接指向全局最优解,然后非常缓慢地下降到谷底。如果算法可以更早地纠正其方向,使它更多地指向全局最优解,那将是很好的。AdaGrad算法通过沿最陡峭的维度按比例缩小梯度向量来实现此校正。
请记住 $ \otimes $ 符号表示逐元素相乘, $ \oslash $ 表示逐元素相除, $ \varepsilon $ 是避免除以0的平滑项,通常取10e-10。第一步将梯度的平方累加。第二步类似梯度下降,但是梯度向量按比例因子 $ \sqrt{\pmb{s} + \varepsilon} $ 缩小了。
简而言之,该算法会降低学习率,但是对于陡峭的维度,它的执行速度要比对缓慢下降的维度的执行速度要快。这称为自适应学习率, adaptive learning rate 。它有助于将结果更新更直接地指向全局最优解。另一个好处是它几乎不需要调整学习率超参数 $ \eta $ 。
图:AdaGrad与梯度下降法:前者可以较早地修正其方向,使之指向最优解。
对于简单的二次问题,AdaGrad经常表现良好,但是在训练神经网络时,它往往停止得太早。学习率被按比例缩小,以至于算法在最终达到全局最优解之前完全停止了。因此,即使Keras有Adagrad优化器,你也不应使用它来训练深度神经网络(不过,它对于诸如线性回归之类的简单任务可能是有效的)。尽管如此,了解AdaGrad仍有助于掌握其他自适应学习率优化器。
RMSProp
RMSProp算法由Geoffrey Hinton和Tijmen Tieleman于2012年创建,并由Geoffrey Hinton 在他的Coursera 神经网络课程中( 幻灯片:https://homl.info/57;视频:https://homl.info/58)介绍。有趣的是,由于作者没有写论文来描述该算法,因此研究人员经常在它们论文中引用为“slide 29 in lecture 6”。
只是累加最近迭代中的梯度(而不是自训练开始以来的所有梯度)来解决 AdaGrad 下降太快的问题。它通过在第一步中使用指数衰减来实现。
衰减率 $ \beta $ 通常设置为0.9。是的,它又是一个新的超参数,但是此默认值通常效果很好,因此你可能根本不需要调整它。
As you might expect, Keras has an RMSprop optimizer:
optimizer = tf.keras.optimizers.RMSprop(learning_rate=0.001, rho=0.9)
注意rho参数对应公式中的 $ \beta $ ,除了非常简单的问题外,该优化器几乎总是比AdaGrad表现更好。实际上,直到Adam优化出现之前,它一直是许多研究人员首选的优化算法。
Adam及其变体
代表自适应矩估计,结合了动量优化和RMSProp的思想:就像动量优化一样,它跟踪过去梯度的指数衰减平均值。就像RMSProp一样,它跟踪过去平方梯度的指数衰减平均值。
动量衰减超参数 $ \beta_1 $ 通常被初始化为0.9,缩放衰减超参数 $ \beta_2 $ 通常被初始化为0.999
optimizer = tf.keras.optimizers.Adam(learning_rate=0.001, beta_1=0.9, beta_2=0.999)
由于Adam是一种自适应学习率算法(如AdaGrad和RMSProp),因此对学习率仅需要较小的调整,通常设为0.001,这使得Adam甚至比梯度下降更易于使用。
如果你开始对所有这些不同的技术感到不知所措,并且想知道如何为自己的任务选择合适的一个,请不要担心,本章末提供了一些实用的准则。
Finally, three variants of Adam are worth mentioning: AdaMax, Nadam, and AdamW.
关于AdaMax
In practice, this can make AdaMax more stable than Adam, but it really depends on the dataset, and in general Adam performs better. So, this is just one more optimizer you can try if you experience problems with Adam on some task.
Nadam优化是Adam优化加上Nesterov技巧,因此其收敛速度通常比Adam稍快。In his report introducing this technique, the researcher Timothy Dozat compares many different optimizers on various tasks and finds that Nadam generally outperforms Adam but is sometimes outperformed by RMSProp.
AdamW是Adam的一个变体,它集成了一种名为权重衰减,weight decay ,的正则化技术。权重衰减通过将模型的权重乘以衰减因子(如0.99)来减少每次训练迭代时模型权重的大小。这可能会让你想起ℓ2正则化,其目的也是保持权重较小,事实上,可以从数学上证明ℓ2正则化等价于使用SGD时的权重衰减。然而当使用Adam或其变体时,ℓ2正则化和权重衰减不等价:在实践中,将Adam与ℓ2正则化导致的模型通常不能像SGD产生的模型那样泛化。AdamW通过将Adam与体重衰减正确结合来解决这个问题。
自适应优化方法(包括RMSProp、Adam、AdaMax、Nadam和AdamW优化)通常很好,可以快速收敛到一个好的解决方案。然而,Ashia C.Wilson等人的2017年的一篇论文表明,它们可以导致在某些数据集上泛化较差的解决方案。因此,当您对模型的性能感到失望时,请尝试使用NAG:您的数据集可能只是对自适应梯度过敏。还要看看最新的研究,因为进展很快。
tf.keras.optimizers.Adam
tf.keras.optimizers.Nadam
tf.keras.optimizers.Adamax
tf.keras.optimizers.experimental.AdamW.
# For AdamW, you probably want to tune the weight_decay hyperparameter.
训练稀疏模型:使用加强的l1正规化, strong ℓ1 regularization ,如果这样还不够,那么请查看TensorFlow Model Optimization Toolkit(TF-MOT),它提供了一个剪枝API,能够根据连接的大小在训练期间迭代地删除连接。
下表compares all the optimizers we’ve discussed so far ( is bad, is average, and ** is good).
Class | Convergence speed | Convergence quality |
---|---|---|
SGD | * | *** |
SGD(momentum=...) | ** | *** |
SGD(momentum=..., nesterov=True) | ** | *** |
Adagrad | *** | * (stops too early) |
RMSprop | *** | or * |
Adam | *** | or * |
AdaMax | *** | or * |
Nadam | *** | or * |
AdamW | *** | or * |
Learning Rate Scheduling
译为:学习率调度,大意是在训练期间适当地改变学习率的值。
Power scheduling,幂调度:将学习率设置为迭代次数t的函数, $ \eta(t) =\frac{\eta_0}{(1+ \frac{t}{s})^c } $ ,幂c(通常设置为1),学习率每步都会下降。
Exponential scheduling,指数调度,学习率每s步将逐渐下降10倍。 $ \eta(t) = \eta_0 0.1^{ \frac{t}{s} } $ ,幂调度越来越缓慢地降低学习率,而指数调度则使学习率每s步降低10倍。
Piecewise constant scheduling,分段恒定调度,Use a constant learning rate for a number of epochs
Performance scheduling,性能调度:每N步测量一次验证误差(就像提前停止一样),并且当误差停止下降时,将学习率降低λ倍。
1cycle scheduling,1周期调度,先提高学习率,线性增长,然后再线性降低,在最后几个 epoch 时将学习率线性降低几个数量级……
在Keras中实现幂调度是最简单的选择:只需在创建优化器时设置超参数decay即可:
optimizer = tf.keras.optimizers.SGD(learning_rate=0.01, decay=1e-4)
decay是s(学习率除以多个数字单位所需要的步数)的倒数,Keras假定c等于1。
指数调度和分段调度也非常简单。你首先需要定义一个函数,该函数采用当前轮次并返回学习率。例如,让我们实现指数调度:
def exponential_decay_fn(epoch):
return 0.01 * 0.1 ** (epoch / 20)
如果不想对η0和s进行硬编码,则可以创建一个返回配置函数的函数:
def exponential_decay(lr0, s):
def exponential_decay_fn(epoch):
return lr0 * 0.1 ** (epoch / s)
return exponential_decay_fn
exponential_decay_fn = exponential_decay(lr0=0.01, s=20)
接下来,创建一个LearningRateScheduler回调函数,为其提供调度函数,然后将此回调函数传递给fit()方法:
lr_scheduler = tf.keras.callbacks.LearningRateScheduler(exponential_decay_fn)
history = model.fit(X_train, y_train, [...], callbacks=[lr_scheduler])
LearningRateScheduler将在每个轮次开始时更新优化器的learning_rate属性。通常每个轮次更新一次学习率就足够了,但是如果你希望更频繁地更新学习率(例如在每个步骤中),则可以编写自己的回调函数。如果每个轮次有很多个步骤,则每一步都更新学习率是有意义的。另外,你可以使用keras.optimizers.schedules方法,这将在稍后进行介绍。
After training, history.history["lr"]
gives you access to the list of learning rates used during training.
调度函数可以选择将当前学习率作为第二个参数。例如,以下调度函数将以前的学习率乘以 $ 0.1^{1/20} $ 这将导致相同的指数衰减(但现在
衰减从轮次0开始而不是1开始)
def exponential_decay_fn(epoch, lr):
return lr * 0.1 ** (1 / 20)
此实现依赖于优化器的初始学习率(与先前的实现相反),因此请确保对其进行适当的设置。
保存模型时,优化器及其学习率也会随之保存。这意味着有了这个新的调度函数,你只需加载经过训练的模型,从中断的地方继续进行训练。但是,如果你的调度函数使用epoch参数,事情就变得不那么简单了:epoch不会被保存,并且每次你调用fit()方法时都会将其重置为0。如果你要继续训练一个中断的模型,则可能会导致一个很高的学习率,这可能会损害模型的权重。一种解决方法是手动设置fit()方法的initial_epoch参数,使epoch从正确的值开始。
对于分段恒定调度,可以使用以下调度函数,然后创建带有此函数的LearningRateScheduler回调函数,并将其传递给fit()方法,就像我们对指数调度所做的那样:
def piecewise_constant_fn(epoch):
if epoch < 5:
return 0.01
elif epoch < 15:
return 0.005
else:
return 0.001
对于性能调度,请使用ReduceLROnPlateau回调函数。例如,如果将以下回调函数传递给fit()方法,则每当连续5个轮次的最好验证损失都没有改善时,它将使学习率乘以0.5(有其他选项可用,请查看文档以获取更多详细信息):
lr_scheduler = tf.keras.callbacks.ReduceLROnPlateau(factor=0.5, patience=5)
history = model.fit(X_train, y_train, [...], callbacks=[lr_scheduler])
最后,tf.keras提供了另一种实现学习率调度的方法:使用keras.optimizers.schedule中可以使用的调度之一来定义学习率,然后将该学习率传递给任意优化器。这种方法在每个步骤更新学习率而不是每个轮次。例如,以下是实现与我们先前定义的exponential_decay_fn()函数相同的指数调度的方法:
import math
batch_size = 32
n_epochs = 25
n_steps = n_epochs * math.ceil(len(X_train) / batch_size)
scheduled_learning_rate = tf.keras.optimizers.schedules.ExponentialDecay(
initial_learning_rate=0.01, decay_steps=n_steps, decay_rate=0.1)
optimizer = tf.keras.optimizers.SGD(learning_rate=scheduled_learning_rate)
这很简单,而且当你保存模型时,学习率及其调度(包括其状态)也将被保存。但是,这种方法不是Keras API的一部分,它只适用于tf.keras。
对于1周期方法,实现起来没有特别的困难:只需创建一个每次迭代都能修改学习率的自定义回调函数即可(你可以通过更改self.model.optimizer.lr来更新优化器的学习率)
总而言之,指数衰减、性能调度和1周期都可以大大加快收敛速度,因此请尝试一下!
正则化
l1与l2正则化
l1与l2正则化的理论部分参阅前面的博客
layer = tf.keras.layers.Dense(100, activation="relu", kernel_initializer="he_normal",
kernel_regularizer=tf.keras.regularizers.l2(0.01))
l2()
函数返回一个正则化函数,在训练过程中的每个步骤都将调用该正则化函数来计算正则化损失。然后将其添加到最终损失中。如果要l1正则化,可以使用 keras.regularizers.l()
,也可以同时使用l1和l2正则化: keras.regularizers.l1_l2()
由于你通常希望将相同的正则化函数应用于网络中的所有层,并在所有隐藏层中使用相同的激活函数和相同的初始化策略,因此你可能会发现自己重复了相同的参数。这使代码很难看且容易出错。为了避免这种情况,你可以尝试使用循环来重构代码。另一种选择是使用Python的functools.partial()函数,该函数可以使你为带有一些默认参数值的任何可调用对象创建一个小的包装函数:
from functools import partial
RegularizedDense = partial(tf.keras.layers.Dense,
activation="relu",
kernel_initializer="he_normal",
kernel_regularizer=tf.keras.regularizers.l2(0.01))
model = tf.keras.Sequential([
tf.keras.layers.Flatten(input_shape=[28, 28]),
RegularizedDense(100),
RegularizedDense(100),
RegularizedDense(10, activation="softmax")
])
dropout
对于深度神经网络,dropout是最受欢迎的正则化技术之一。它是由Geoffrey Hinton在2012年的论文中提出的,并且在Nitish Srivastava等人2014年的论文中得到了进一步的详述。已被证明是非常成功的:只需要增加dropout,即使最先进的神经网络也能获得1~2%的准确率提升。这听起来可能不算很多,但是当模型已经具有95%的准确率时,将准确率提高2%意味着将错误率降低了近40%(从5%的错误率降低到大约3%)。
这是一个非常简单的算法:在每个训练步骤中,每个神经元(包括输入神经元,但始终不包括输出神经元)都有暂时“删除”的概率p,这意味着在这个训练步骤中它被完全忽略,但在下一步中可能处于活动状态。超参数p称为dropout率,通常设置为10%到50%:在循环神经网络中接近20~30%,在卷积神经网络中接近40~50%。训练后,神经元不再被删除。
It’s surprising at first that this destructive technique works at all. Would a company perform better if its employees were told to toss a coin every morning to decide whether or not to go to work? Well, who knows; perhaps it would! The company would be forced to adapt its organization; it could not rely on any single person to work the coffee machine or perform any other critical tasks, so this expertise would have to be spread across several people. Employees would have to learn to cooperate with many of their coworkers, not just a handful of them. The company would become much more resilient. If one person quit, it wouldn’t make much of a difference. It’s unclear whether this idea would actually work for companies, but it certainly does for neural networks. Neurons trained with dropout cannot co-adapt with their neighboring neurons; they have to be as useful as possible on their own. They also cannot rely excessively on just a few input neurons; they must pay attention to each of their input neurons. They end up being less sensitive to slight changes in the inputs. In the end, you get a more robust network that generalizes better.
在实践中,你通常只可以对第一至第三层(不包括输出层)中的神经元应用dropout
有一个小而重要的技术细节。假设p=75%:在训练的每一步中,平均只有25%的神经元是活跃的。这意味着在训练后,一个神经元将连接到训练期间四倍多的输入神经元。为了弥补这一事实,我们需要在训练期间将每个神经元的输入连接权重乘以四。如果我们不这样做,神经网络将不会表现良好,因为它将在训练期间和训练后看到不同的数据。更一般地说,我们需要在训练过程中用保持概率(1–p)除以连接权重。
# 训练期间,它会随机丢弃一些输入(将它们设置为0),然后将其余输入除以保留概率
# 使用0.2的dropout率在每个Dense层之前应用dropout正则化
model = tf.keras.Sequential([
tf.keras.layers.Flatten(input_shape=[28, 28]),
tf.keras.layers.Dropout(rate=0.2),
tf.keras.layers.Dense(100, activation="relu",
kernel_initializer="he_normal"),
tf.keras.layers.Dropout(rate=0.2),
tf.keras.layers.Dense(100, activation="relu",
kernel_initializer="he_normal"),
tf.keras.layers.Dropout(rate=0.2),
tf.keras.layers.Dense(10, activation="softmax")
])
[...] # compile and train the model
由于dropout仅在训练期间激活,因此比较训练损失和验证损失可能会产生误导。具体而言,模型可能会过拟合训练集,但仍具有相似的训练损失和验证损失。因此,请确保没有使用dropout来评估训练损失(例如,训练之后)。如果你发现模型过拟合,则可以提高dropout率。相反,如果模型欠拟合训练集,则应尝试降低dropout率。它还可以帮助增加较大层的dropout率,并减小较小层的dropout率。此外,许多最新的架构仅在最后一个隐藏层之后才使用dropout,因此如果完全dropout太强,你可以尝试一下此方法。
dropout确实会明显减慢收敛速度,但是如果正确微调,通常会导致更好的模型。因此,花额外的时间和精力是值得的。
tips:如果要基于上面提到的SELU激活函数对自归一化网络进行正则化,则应使用alpha dropout:这是dropout的一种变体,它保留了其输入的均值和标准差(在与SELU相同的论文中介绍,因为常规的dropout会破坏自归一化)。
Monte Carlo (MC) Dropout
2016年,Yarin Gal和Zoubin Ghahramani的论文:建立了dropout网络与近似贝叶斯推理之间的联系,提供了坚实的数学依据;介绍了一种MC dropout 方法,可以提高任何训练后的dropout模型的性能,而无须重新训练甚至根本不用修改它,它可以更好地测量模型的不确定性,并且实现起来非常简单。
下面的代码是MC Dropout的完整实现,可提升我们先前训练的dropout模型,而无须重新训练它:
import numpy as np
y_probas = np.stack([model(X_test, training=True) for sample in range(100)])
y_proba = y_probas.mean(axis=0)
# 我们仅对测试集进行100个预测,设置training=True来确保Dropout层处于激活状态,然后把预测堆叠起来
# 由于dropout处于激活状态,因此所有预测都将有所不同。
分析:
我们仅对测试集进行100个预测,设置training=True来确保Dropout层处于激活状态,然后把预测堆叠起来。由于dropout处于激活状态,因此所有预测都将有所不同。回想一下,predict()返回一个矩阵,其中每个实例一行,每个类一列。因为测试集中有10 000个实例和10个类,所以这是一个形状为[10000,10]的矩阵。我们堆叠了100个这样的矩阵,因此y_probas是一个形状为[100,10000,10]的数组。一旦我们对第一个维度进行平均(axis=0),我们得到y_proba,它是形状[10000,10]的数组,就像我们通过单个预测得到的一样。就是这样!对具有dropout功能的多个预测进行平均,这使蒙特卡罗估计通常比关闭dropout的单个预测的结果更可靠。例如,让我们看一下模型对Fashion MNIST测试集中第一个实例的预测,关闭dropout:
>>> model.predict(X_test[:1]).round(3)
array([[0. , 0. , 0. , 0. , 0. , 0.024, 0. , 0.132, 0. ,
0.844]], dtype=float32)
The model is fairly confident (84.4%) that this image belongs to class 9 (ankle boot). Compare this with the MC dropout prediction:
>>> y_proba[0].round(3)
array([0. , 0. , 0. , 0. , 0. , 0.067, 0. , 0.209, 0.001,
0.723], dtype=float32)
The model still seems to prefer class 9, but its confidence dropped down to 72.3%, and the estimated probabilities for classes 5 (sandal) and 7 (sneaker) have increased, which makes sense given they’re also footwear.
MC dropout tends to improve the reliability of the model’s probability estimates. This means that it’s less likely to be confident but wrong, which can be dangerous: just imagine a self-driving car confidently ignoring a stop sign. It’s also useful to know exactly which other classes are most likely. Additionally, you can take a look at the standard deviation of the probability estimates:
>>> y_std = y_probas.std(axis=0)
>>> y_std[0].round(3)
array([0. , 0. , 0. , 0.001, 0. , 0.096, 0. , 0.162, 0.001,
0.183], dtype=float32)
Apparently there’s quite a lot of variance in the probability estimates for class 9: the standard deviation is 0.183, which should be compared to the estimated probability of 0.723: if you were building a risk-sensitive system (e.g., a medical or financial system), you would probably treat such an uncertain prediction with extreme caution. You would definitely not treat it like an 84.4% confident prediction. The model’s accuracy also got a (very) small boost from 87.0% to 87.2%:
>>> y_pred = y_proba.argmax(axis=1)
>>> accuracy = (y_pred == y_test).sum() / len(y_test)
>>> accuracy
0.8717
您使用的蒙特卡罗样本数(本例中为100)是一个可以调整的超参数。它越高,预测和不确定性估计就越准确。然而,如果加倍,推理时间也会加倍。此外,在一定数量的样本以上,您会注意到几乎没有改进。您的工作是根据您的应用程序,在延迟和准确性之间找到合适的折衷方案。
果你的模型包含在训练过程中以特殊方式运行的其他层(例如BatchNormalization层),则你不应像我们刚才那样强制训练模式。相反,你应该使用以下MCDropout类来替换Dropout层:
class MCDropout(tf.keras.layers.Dropout):
def call(self, inputs, training=False):
return super().call(inputs, training=True)
在这里,我们只是继承了Dropout层,并覆盖call()方法来强制设置training参数为True)。同样,你可以通过继承AlphaDropout来定义MCAlpha Dropout类。如果你要从头开始创建模型,则只需使用MCDropout而不是Dropout。但是,如果你有一个已经使用Dropout训练过的模型,则需要创建一个与现有模型相同的新模型,不同之处在于它用MCDropout替换了Dropout层,然后将现有模型的权重复制到你的新模型中。
简而言之,MC Dropout是一种出色的技术,可以提升dropout模型并提供更好的不确定性估计。当然,由于这只是训练期间的常规dropout,所以它也像正则化函数。
Max-Norm Regularization
最大范数正则化,Max-Norm Regularization。对于每个神经元,它会限制传入连接的权重w,令w的l2范数小于等于r,r是最大范数超参数。最大范数正则化不会把正则化损失项添加到总体损失函数中。通常在每个训练步骤后通过计算w的l2范数。减小r会增加正则化的数量,并有助于减少过拟合。最大范数正则化还可以帮助缓解不稳定的梯度问题(如果你未使用“批量归一化”)。
要在Keras中实现最大范数正则化,请将每个隐藏层的 kernel_constraint
参数设置为具有适当最大值的 max_norm()
约束:
dense = tf.keras.layers.Dense(
100, activation="relu", kernel_initializer="he_normal", kernel_constraint=tf.keras.constraints.max_norm(1.))
每次训练迭代后,模型的fit()方法会调用由max_norm()返回的对象,将层的权重传递给该对象,并获得返回的缩放权重,然后替换该层的权重。如果需要,你可以定义自己的自定义约束函数,并将其用作kernel_constraint。你还可以通过设置bias_constraint参数来约束偏置项。max_norm()函数的参数axis默认为0。Dense层通常具有形状为[输入数量,神经元数量]的权重,因此使用axis=0意味着最大范数约束将独立应用于每个神经元的权重向量。如果你要将最大范数与卷积层一起使用,请确保正确设置max_norm()约束的axis参数(通常axis=[0,1,2])。
总结和实用指南
Summary and Practical Guidelines
In this chapter we have covered a wide range of techniques, and you may be wondering which ones you should use. This depends on the task, and there is no clear consensus yet, but I have found the configuration in Table 11-3 to work fine in most cases, without requiring much hyperparameter tuning. That said, 请不要将这些默认值视为硬性规定!
Hyperparameter | Default value |
---|---|
Kernel initializer | He initialization |
Activation function | ReLU if shallow; Swish if deep |
Normalization | None if shallow; batch norm if deep |
Regularization | Early stopping; weight decay if needed |
Optimizer | Nesterov accelerated gradients or AdamW |
Learning rate schedule | Performance scheduling or 1cycle |
如果网络是密集层的简单堆叠,则它可以自归一化,你应该使用下表配置
Hyperparameter | Default value |
---|---|
Kernel initializer | LeCun initialization |
Activation function | SELU |
Normalization | None (self-normalization) |
Regularization | Alpha dropout if needed |
Optimizer | Nesterov accelerated gradients |
Learning rate schedule | Performance scheduling or 1cycle |
不要忘了归一化输入特征!你如果可以找到解决类似问题的神经网络,那应该尝试重用部分神经网络;如果有大量未标记的数据,则应使用无监督预训练;如果有相似任务的大量标记的数据,则应该在辅助任务上使用预训练。
一些例外:
- 如果你需要稀疏模型,则可以使用 ℓ1 正则化(可以选择在训练后将很小的权重归零)。如果你需要更稀疏的模型,则可以使用TensorFlow模型优化工具包。这会破坏自归一化,因此在这种情况下,你应使用默认配置。
- 如果你需要低延迟的模型(执行闪电般快速预测的模型),则可能需要使用更少的层,将批量归一化层融合到先前的层中,并使用更快的激活函数,例如leaky ReLU或仅仅使用ReLU。拥有稀疏模型也将有所帮助。最后,你可能想把浮点精度从32位降低到16位甚至8位。再一次检查TFMOT(TensorFlow Model Optimization Toolkit,TensorFlow模型优化工具包)。
- 如果你要构建风险敏感的应用,或者推理延迟在你的应用中不是很重要,则可以使用MC Dropout来提高性能并获得更可靠的概率估计以及不确定性估计。
有了这些准则,你现在就可以训练非常深的网络了!我希望你现在相信使用Keras可以走很长一段路了。但是,有时候你可能需要更多的控制。例如,编写自定义损失函数或调整训练算法。对于这种情况,你需要使用TensorFlow的较低级API。
下一篇博客见。