美文网首页
人人都能懂的机器学习——训练深度神经网络——批标准化

人人都能懂的机器学习——训练深度神经网络——批标准化

作者: 苏小菁在编程 | 来源:发表于2020-06-09 21:41 被阅读0次

    批标准化

    尽管使用He初始化和ELU(或者其他ReLU的变体)可以极大地减少在训练之初梯度不稳的问题,但是无法保证随着训练的进行,梯度不稳的问题不会再次发生。

    Sergey Ioffe和Christian Szegedy在2015年发表了文章,提出了批标准化的技术(BN),该技术解决了梯度消失和爆炸问题。这个技术是在模型每层的激活函数之前或之后增加一个操作,将每一个输入归零并归一化,并且每一层都使用两个参数向量对结果进行缩放和移动。换句话说,这个操作使得模型学习每一层输入的最优范围和均值。在很多情况下,如果你在神经网络的第一层添加了一层批标准化层,那么就不需要再将训练集进行标准化了(也就是用StandardScaler),批标准化会处理好标准化的问题(不过批标准化层一次将一批的输入进行归零和缩放,并且还可以对每个输入特征进行缩放移动)。

    为了将输入数据标准化,算法通过计算当前批次的平均值和标准差,从而评估每个输入的平均值和标准差。整个操作过程见下式:

    \mu_{B}=\frac{1}{m_{B}}\sum\limits_{i=1}^{m_{B}}x_{i} \\ \sigma_{B}^{2}=\frac{1}{m_{B}}\sum\limits_{i=1}^{m_{B}}(x_{i}-\mu_{B})^{2} \\ \vec x_{i}=\frac{x_{i}-\mu_{B}}{\sqrt{\sigma_{B}^{2}+\epsilon}} \\ z_{i}=\gamma \bigotimes \vec x_{i}+\beta=BN_{\gamma,\beta(x_{i})}

    上式中:

    • μ_B 为输入的平均向量, 通过对整个批次B计算得出的(每个输入都包含一个平均值)
    • σ_B 为输入的标准差向量,同样是通过整个批次B计算得出的(每个输入都包含一个标准差)
    • m_B 是该批次中的实例数量
    • x_i 是实例i以0点居中并标准化后的输入向量
    • γ 是神经层的输出缩放参数(对每个输入都有一个缩放参数)
    • 圈乘 表示同位矩阵元素对应相乘(每个输入与它对应的输出缩放参数相乘)
    • β是神经层的输出移动(偏移)参数向量(对应每个输入都有一个偏移参数)。每个输入都通过对应的移动参数进行偏移。
    • epsilon 是一个很小的数,用来避免被0除(一般是10 -5)。它被称为平滑项。
    • z_i 是批标准化的输出,是将输入缩放和移动后的结果

    那么在训练过程中,批标准化操作将其输入标准化,并重新缩放和移动它们。很好!但到了测试过程呢?事情就不是那么简单了。实际上,在测试时我们可能需要对单个实例进行预测,而不是批次预测:在这种情况下,我们将无法计算每个输入的平均值和标准差。此外,即使我们有一批实例,该批次的数量也可能不够,或者实例之间可能不是独立、同分布的(IID),那么计算该批次的统计信息是不可靠的。一种解决方法是等到训练结束,然后通过神经网络运行整个训练集,计算批标准化层每个输入的均值和标准差。在进行预测时,可以使用这些“最终”输入的平均值和标准偏差,而不是每批的输入平均值和标准偏差。然而,大多数成熟深度学习库中的批标准化算法会在训练期间,通过层的输入均值和标准差的移动平均来估计这些”最终“数据。这也是在使用BatchNormalization层时,Keras自动帮你执行的操作。综上所述,在每一个批标准化层中,神经网络会学习四个参数向量:通过正常的反向传播算法,神经网络学习γ(输出的缩放向量)和β(输出偏移向量);使用指数移动平均,估计出μ(最终输入平均向量)和σ(最终输入标准差向量)。请注意,μ和σ是在训练期间计算出的,但在训练神经网络期间 根本不使用它们,只在训练完成之后才使用(用以替换上面公式中的批输入平均值和标准差)。

    Ioffe和Szegedy证明了批标准化显著地改善了所有他们测试的深度神经网络,并对ImageNet分类任务中的表现有了极大的提高(ImageNet是一个巨大的已分类的图片数据库,在评估计算机视觉系统中有着广泛的应用)。梯度消失问题几乎没有了,甚至可以使用一些饱和激活函数比如tanh甚至sigmoid函数。并且神经网络对权重初始化也不再敏感。作者还尝试使用了较大的学习率,极大地加速了训练过程。特别地,他们指出:

    应用于最新的图片分类模型,批标准化少用了14次的训练次数并达到了相同的准确度,并远远超过了原模型的表现。使用批标准化的组合神经网络,我们提高了最佳的ImageNet分类结果:达到4.9% top-5 验证误差(4.8%测试误差),超过了人类分类的准确度。

    最后,批标准化甚至能够发挥正则化器的作用,较少了对其他正则化技术的需求(比如dropout技术,我们在未来的文章中会提到)。

    不过批标准化技术确实增加了模型的复杂度(尽管它不再需要将输入函数标准化了)。另外还有一个运行时间的缺点:在进行预测时,由于在每一层都需要进行额外的计算量,所以预测的速度会变慢。当然,我们是可以在训练完成之后,去掉批标准化层,并且更新前一层的权重和偏差,使得上一层能够直接输出合适的缩放和偏移结果。换句话说,我们可以将批标准化层直接与前一层结合起来。比如说,如果前一层计算了:

    XW+b

    然后批标准化层计算了:

    \gamma \bigotimes (XW+b-\mu)/\sigma+\beta

    上式暂时忽略了分母中的平滑项ipsilong。那么我们可以设:

    W' = \gamma \bigotimes W/\sigma \\ b′ = \gamma \bigotimes (b - \mu)/\sigma + \beta

    那么方程就可以简化为:

    XW'+b'

    那么,我们将前一层的权重和偏差替换成新的权重和偏差,那么我们就可以丢掉批标准化层了(TFLite的优化器会自动完成这个步骤,我们会在未来的文章中展示)。

    你可能会发现,使用批标准化后,训练的速度变慢了,因为每过一遍训练集都需要更大的计算量。但使用批标准化后,模型的收敛速度大大加快,所以模型训练达到一定性能所需的训练周期次数减少了。总的来说,wall time一般是减少的(所谓wall time就是用墙上的钟测量的时间)。

    用Keras实现批标准化

    跟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_3"
    _________________________________________________________________
    Layer (type) Output Shape Param #
    =================================================================
    flatten_3 (Flatten) (None, 784) 0
    _________________________________________________________________
    batch_normalization_v2 (Batc (None, 784) 3136
    _________________________________________________________________
    dense_50 (Dense) (None, 300) 235500
    _________________________________________________________________
    batch_normalization_v2_1 (Ba (None, 300) 1200
    _________________________________________________________________
    dense_51 (Dense) (None, 100) 30100
    _________________________________________________________________
    batch_normalization_v2_2 (Ba (None, 100) 400
    _________________________________________________________________
    dense_52 (Dense) (None, 10) 1010
    =================================================================
    Total params: 271,346
    Trainable params: 268,978
    Non-trainable params: 2,368
    

    可以看到,每个BN层对每个输入神经元加入了四个参数:γ,β,μ和σ(比如第一个BN层加入了3136个参数,即4X784)。后两个参数,即μ和σ,为移动平均值;它们不随着反向传播而变化,所以Keras称之为“Non-trainable”。但是实际上它们是根据训练数据估计出来的平均值,所以其实是可以训练的,但是这里Keras中的不可训练仅仅是指不受反向传播的影响(如果你将BN层参数的总数相加,3136+1200+400,然后除以2,就会得到2368,即模型中的Non-trainable参数的总数)。

    我们再来详细看一下第一层BN层的参数,两个是可训练的,两个是不可训练的:

    >>> [(var.name, var.trainable) for var in model.layers[1].variables]
    [('batch_normalization_v2/gamma:0', True),
    ('batch_normalization_v2/beta:0', True),
    ('batch_normalization_v2/moving_mean:0', False),
    ('batch_normalization_v2/moving_variance:0', False)]
    

    当我们在Keras中创建BN层时,它还会创建两个操作,Keras将在训练期间每次迭代中调用它们。这两个操作会更新移动平均值。既然我们的Keras使用的是TensorFlow后端,那么这两个操作为TensorFlow操作:

    >>> model.layers[1].updates
    [<tf.Operation 'cond_2/Identity' type=Identity>,
    <tf.Operation 'cond_3/Identity' type=Identity>]
    

    批标准化的论文作者主张应在激活函数之前添加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层有一些可调的参数。BN层的默认参数通常没有什么问题,但是有些情况下可能需要调整一下动量(momentum)。这个超参是使BN层更新指数移动平均:当输入一个新的值v,则BN使用下方的公式更新移动平均值:

    \overline{v}=\overline{v}\times{momentum}+v\times{(1-momentum)}

    一个合适的momentum值通常接近于1,比如0.9,0.99,或者0.999(当数据量较大且单批量较小时,可以加更多的9)

    另一个重要的超参数是axis:它会确定标准化哪个轴。这个参数的默认值为-1,也就是默认情况下标准化最后一个轴(使用在其他轴上计算的平均值和标准差)。举个例子,当输入值为2D时(也就是,批数据的形状为[批数量,特征数]),那就意味着每个输入特征将会基于批次中所有的实例计算出来的平均值和标准差进行标准化处理。例如上述代码示例中,第一个BN层将784个输入特性各自独立地标准化(缩放和移动)。如果我们将第一个BN层移动到Flatten层之前,那么该BN层的输入数据为3D结构,数据形状为[批数量,高,宽]。鉴于输入的结构为[28,28],那么BN层就会计算28个平均值和28个标准差(针对数据的最后一个轴,宽,也就是针对图片中的每一列像素单元,基于该批次中的所有实例的该列中的每一行像素值),并对处于该列的所有像素值使用相同的平均值和标准差进行标准化。那么也就会计算出28个缩放参数和28个移动参数。但是如果你想对784个像素独立进行标准化处理,那么就可以将超参数axis设置为:axis=[1,2]。

    之前已经提到过,BN层在训练中和训练完成后所进行的计算是不同的:在训练中,它计算并使用该批次的统计信息;在训练后,它使用“最终”统计信息(比如说移动平均值的最终值)。我们来简单看一下源码示例,来看看Keras是如何做到这一功能的:

    class BatchNormalization(keras.layers.Layer):
        [...]
        def call(self, inputs, training=None):
            [...]
    

    call()方法中有一个training参数,这个参数默认为None,但是fit()方法在训练过程中,会将该training参数设为1。我们在建立自定义层时也可以使用类似的方法,如果我们希望自定义层在训练和测试时实现不同的功能,就需要在call()方法中增加一个training参数,然后用它来决定计算规则。

    批标准化已经成为了深度神经网络中最常用的层之一,常常默认在每个层之后都加一个批标准化层,所以一般会在神经网络架构图中将其忽略。但是Hongyi Zhang等人发表的《固定更新初始化:不带标准化的残差学习》一文可能会改变这一想法:作者表示,通过一种新的固定更新权值初始化技术,可以成功训练一个不带批标准化的非常深的神经网络(10000层!),并在复杂的图像分类任务中表现出最出色的性能。但由于这是前沿研究,我们可能需要更多的研究论证才可以彻底放弃批标准化技术。

    本篇文章向大家介绍了解决梯度问题的一个非常常用的技术——批标准化。之后的文章将再向大家介绍一种解决梯度爆炸问题的方法,梯度裁剪。之后,我们将一起学习预训练模型与迁移学习。

    敬请期待吧!

    相关文章

      网友评论

          本文标题:人人都能懂的机器学习——训练深度神经网络——批标准化

          本文链接:https://www.haomeiwen.com/subject/kfxytktx.html