Keras防止过拟合(四) Batch Normalization代码实现

解决过拟合的方法和代码实现,前面已经写过Dropout层L1 L2正则化提前终止训练三种,本篇介绍一下Batch Normalization方法。其最大的好处是加速训练,但对防止过拟合也有一些作用,所以就将其在防过拟合系列中写了吧。

Batch Normalization

原文:
https://arxiv.org/pdf/1502.03167.pdf

有许多优质文章讲解过Batch Normalization,个人推荐这篇【深度学习】深入理解Batch Normalization批标准化。其解释十分细致,容易理解。
本篇中,主要讲解为代码实现所需要理解的概念和代码实现。

1 “Covariate Shift”与“Internal Covariate Shift”

要实现Batch Normalization,首先要明白其出发点。BN用来解决“Internal Covariate Shift”问题,直译为“内部协变量转移”,从名称上完全无法理解。要理解这个概念,首先要理解“Covariate Shift”,直译为“协变量转移”是什么。
这篇文章解释了“Covariate Shift”问题【机器学习】covariate shift现象的解释,同时给出了解决方法。个人在此使用一个例子对“Covariate Shift”进行描述。

机器学习的模型是通过训练集训练后得到的,之后再将其投入到测试集中使用。若要保证模型训练后在测试集中能得到很好的效果,则必须满足IID独立同分布假设(机器学习领域一个很重要的假设),就是假设训练数据和测试数据是满足相同分布的。这是什么意思呢?
举一个简单的例子:我想用个人历史病例来预测个人是否会得心脏病,于是我从60岁以上的老人中随机抽取了50000个样本,其中10000人得了心脏病,以这50000个样本来训练模型。模型训练好后,我想将其投入使用,于是从20到30岁的青年人中随机选择了20000个人测试。想想都知道,模型在测试集上表现一定很差。原因便是,训练数据和测试数据是不满足相同分布。通过这个例子,可以将“Covariate Shift”理解为“数据分布不同”。

“Covariate Shift”是训练集和测试集的数据分布不同,那“Internal Covariate Shift”,多了一个Internal(内部),则为内部数据分布不同。机器学习模型通常有多层,在训练过程中,模型内部的各个隐层输入分布不同,这就是“Internal Covariate Shift”,针对模型内部的各个隐层。而“Covariate Shift”,发生在模型输入层,即输入数据的分布不同(训练集和测试集的输入分布不同)。BN便为这个问题而设计,用来使各个隐层的激活输入分布相同(注意是激活前的输入)。

随着训练的进行,网络中的参数也随着梯度下降在不停更新。一方面,当底层网络中参数发生微弱变化时,由于每一层中的线性变换与非线性激活映射,这些微弱变化随着网络层数的加深而被放大(类似蝴蝶效应);另一方面,参数的变化导致每一层的输入分布会发生改变,进而上层的网络需要不停地去适应这些分布变化,使得我们的模型训练变得困难。上述这一现象叫做Internal Covariate Shift。

“Internal Covariate Shift”带来的问题:

1.上层网络需要不停调整来适应输入数据分布的变化,导致网络学习速度的降低。
2.网络的训练过程容易陷入梯度饱和区,减缓网络收敛速度

引用自Batch Normlization

2 Batch Normalization如何解决“Internal Covariate Shift”

ICS产生的原因:是由于参数更新带来的网络中每一层输入值分布的改变,并且随着网络层数的加深而变得更加严重,因此我们可以通过固定每一层网络输入值的分布来对减缓ICS问题。

固定每一层网络输入值的分布,也可以让输入分布在一定范围从而不落入饱和区。

Batch Normalization本质思想:对于每个隐层神经元,把逐渐向非线性函数映射后向取值区间极限饱和区靠拢的输入分布强制拉回到均值为0方差为1的比较标准的正态分布,使得非线性变换函数的输入值落入对输入比较敏感的区域,以此避免梯度消失问题。因为梯度一直都能保持比较大的状态,所以很明显对神经网络的参数调整效率比较高,就是变动大,就是说向损失函数最优值迈动的步子大,也就是说收敛地快。

引用自Batch Normlization

1.Batch Normalization的核心内容便是要将输入分布强制拉回到均值为0方差为1的比较标准的正态分布(激活前的输入)
除此之外,还要解决一个问题:

如果都通过BN,那么不就跟把非线性函数替换成线性函数效果相同了?这意味着什么?我们知道,如果是多层的线性函数变换其实这个深层是没有意义的,因为多层线性网络跟一层线性网络是等价的。这意味着网络的表达能力下降了,这也意味着深度的意义就没有了。所以BN为了保证非线性的获得,对变换后的满足均值为0方差为1的x又进行了scale加上shift操作(y=scale*x+shift),每个神经元增加了两个参数scale和shift参数,这两个参数是通过训练学习到的,意思是通过scale和shift把这个值从标准正态分布左移或者右移一点并长胖一点或者变瘦一点,每个实例挪动的程度不一样,这样等价于非线性函数的值从正中心周围的线性区往非线性区动了动。核心思想应该是想找到一个线性和非线性的较好平衡点,既能享受非线性的较强表达能力的好处,又避免太靠非线性区两头使得网络收敛速度太慢。

引用自【深度学习】深入理解Batch Normalization批标准化
2.为解决线性等价问题,要对对变换后的满足均值为0方差为1的x又进行了scale加上shift操作(y=scale*x+shift),每个神经元增加了两个参数scale和shift参数,这两个参数是通过训练学习到的。

3. Keras实现Batch Normalization

论文中的算法流程:

在这里插入图片描述
流程可以总结为4步:计算均值->计算方差->更新x->进行scale和shift操作
其中scale和shift操作的参数y和B是学习出来的,而不是固定值。
这里使用的是一个mini-batch中的所有x用来求解均值和方差。

按照论文思路实现代码:
keras中有定义好的Batch Normalization:

keras.layers.BatchNormalization(axis=-1, momentum=0.99, epsilon=0.001, center=True, scale=True, beta_initializer='zeros', gamma_initializer='ones', moving_mean_initializer='zeros', moving_variance_initializer='ones', beta_regularizer=None, gamma_regularizer=None, beta_constraint=None, gamma_constraint=None)

在这里插入图片描述
其各个参数的描述如上图所示(Keras中文文档)。
其中参数epsilon,center,scale和各个初始化、正则化、约束方法都很好理解,但axis,momentum这两项的用途描述有些模糊。 下面通过源码分析给出其用处。

源码位于keras\layers\normalization.py中,源码核心部分如下:

def call(self, inputs, training=None):
        input_shape = K.int_shape(inputs)
        # Prepare broadcasting shape.
        ndim = len(input_shape)
        reduction_axes = list(range(len(input_shape)))
        del reduction_axes[self.axis]
        broadcast_shape = [1] * len(input_shape)
        broadcast_shape[self.axis] = input_shape[self.axis]

        # Determines whether broadcasting is needed.
        needs_broadcasting = (sorted(reduction_axes) != list(range(ndim))[:-1])

        def normalize_inference():
            if needs_broadcasting:
                # In this case we must explicitly broadcast all parameters.
                broadcast_moving_mean = K.reshape(self.moving_mean,
                                                  broadcast_shape)
                broadcast_moving_variance = K.reshape(self.moving_variance,
                                                      broadcast_shape)
                if self.center:
                    broadcast_beta = K.reshape(self.beta, broadcast_shape)
                else:
                    broadcast_beta = None
                if self.scale:
                    broadcast_gamma = K.reshape(self.gamma,
                                                broadcast_shape)
                else:
                    broadcast_gamma = None
                return K.batch_normalization(
                    inputs,
                    broadcast_moving_mean,
                    broadcast_moving_variance,
                    broadcast_beta,
                    broadcast_gamma,
                    axis=self.axis,
                    epsilon=self.epsilon)
            else:
                return K.batch_normalization(
                    inputs,
                    self.moving_mean,
                    self.moving_variance,
                    self.beta,
                    self.gamma,
                    axis=self.axis,
                    epsilon=self.epsilon)

        # If the learning phase is *static* and set to inference:
        if training in {0, False}:
            return normalize_inference()

        # If the learning is either dynamic, or set to training:
        normed_training, mean, variance = K.normalize_batch_in_training(
            inputs, self.gamma, self.beta, reduction_axes,
            epsilon=self.epsilon)

        if K.backend() != 'cntk':
            sample_size = K.prod([K.shape(inputs)[axis]
                                  for axis in reduction_axes])
            sample_size = K.cast(sample_size, dtype=K.dtype(inputs))
            if K.backend() == 'tensorflow' and sample_size.dtype != 'float32':
                sample_size = K.cast(sample_size, dtype='float32')

            # sample variance - unbiased estimator of population variance
            variance *= sample_size / (sample_size - (1.0 + self.epsilon))

        self.add_update([K.moving_average_update(self.moving_mean,
                                                 mean,
                                                 self.momentum),
                         K.moving_average_update(self.moving_variance,
                                                 variance,
                                                 self.momentum)],
                        inputs)

        # Pick the normalized form corresponding to the training phase.
        return K.in_train_phase(normed_training,
                                normalize_inference,
                                training=training)

先看看最后的返回值,使用到K.in_train_phase函数,让训练和测试使用不同的方法。

开始定义的broadcast_shape用来改变mean,var的shape,reduction_axes用在后面的K.normalize_batch_in_training函数中,needs_broadcasting 用来区分mean,var的shape是否需要改变。
为什么有时需要改变mean,var的shape呢? 原因是输入的数据并不是每个轴都要均值,例如3通道的图片集,要对图集大小、图长、图宽做均值,但不对3个通道做均值,而是3个通道各有一个均值,即各个通道互不干涉,所以要相应的改变shape。
axis参数便为此而设计,将一个轴单独取出,使此轴上的各个维度各取均值,互不干涉。 默认值是-1。

K.normalize_batch_in_training函数,用来完成算法流程,其中包含一个_broadcast_normalize_batch_in_training函数,部分代码如下:

def _broadcast_normalize_batch_in_training(x, gamma, beta,
                                           reduction_axes, epsilon=1e-3):

    mean, var = tf.nn.moments(x, reduction_axes,
                              None, None, False)
    normed = tf.nn.batch_normalization(
        x,
        broadcast_mean,
        broadcast_var,
        broadcast_beta,
        broadcast_gamma,
        epsilon)
    return normed, mean, var

reduction_axes用在tf.nn.moments函数中,用来去轴计算mean,var。

tf.nn.batch_normalization函数具体实现计算:

def batch_normalization(x,
                        mean,
                        variance,
                        offset,
                        scale,
                        variance_epsilon,
                        name=None):
  with ops.name_scope(name, "batchnorm", [x, mean, variance, scale, offset]):
    inv = math_ops.rsqrt(variance + variance_epsilon)
    if scale is not None:
      inv *= scale
    # Note: tensorflow/contrib/quantize/python/fold_batch_norms.py depends on
    # the precise order of ops that are generated by the expression below.
    return x * math_ops.cast(inv, x.dtype) + math_ops.cast(
        offset - mean * inv if offset is not None else -mean * inv, x.dtype)

训练的流程便是这样,测试时使用的normalize_inference函数也通过tf.nn.batch_normalization函数求得结果,现在还存在一个问题:
训练时计算mean和var用的是每个mini-batch的x计算出来的,那用于测试集时mean和var是如何得到的。

本来我以为,使用测试集时,也像训练集一样,一个一个mini-batch计算而得。但观察源码后,发现并不是这样的,源码中在测试时使用的方法为normalize_inference,其中并没有计算mean和var的部分,而是直接使用定义好的self.moving_mean,self.moving_variance,那么这两者是怎么来的呢?

首先,两者的初值由定义的初始化方法随机得到。
源码中有这一部分,用来更新self.moving_mean和self.moving_variance。

        self.add_update([K.moving_average_update(self.moving_mean,
                                                 mean,
                                                 self.momentum),
                         K.moving_average_update(self.moving_variance,
                                                 variance,
                                                 self.momentum)],
                        inputs)

使用的方法是权重滑动平均法:
在这里插入图片描述
对于self.moving_mean的更新,可以写为:

self.moving_mean = momentum * self.moving_mean + (1-momentum) * mean

其中,momentum参数对应α,为衰减率,默认值为0.99,mean是每一个mini-batch计算所得的均值。
由此可见,测试时使用的mean和var是又训练时所有mini-batch使用权重滑动平均法求得的。

4.Batch Normalization的好处与不足

好处

①不仅仅极大提升了训练速度,收敛过程大大加快;②还能增加分类效果,一种解释是这是类似于Dropout的一种防止过拟合的正则化表达方式,所以不用Dropout也能达到相当的效果;③另外调参过程也简单多了,对于初始化要求没那么高,而且可以使用大的学习率等。

引用自【深度学习】深入理解Batch Normalization批标准化

这些好处主要源于:BN具有提高网络泛化能力的特性

不足

BN是深度学习调参中非常好用的策略之一(另外一个可能就是Dropout),当你的模型发生梯度消失/爆炸或者损失值震荡比较严重的时候,在BN中加入网络往往能取得非常好的效果。

BN也有一些不是非常适用的场景,在遇见这些场景时要谨慎的使用BN:

受制于硬件限制,每个Batch的尺寸比较小,这时候谨慎使用BN;

在类似于RNN的动态网络中谨慎使用BN;

训练数据集和测试数据集方差较大的时候。

引用自模型优化之Batch Normalization

BN不适用于RNN,为此,可以使用Layer Normalization,下一篇中将讲解Layer Normalization原理和keras实现。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值