Batch Normalization, 会其意知其形

Batch Normalization

归一化/正则化

数据归一化、正则化是非常重要的步骤,用于重新缩放输入的数值,确保在反向传播期间更好的收敛。一般来说,采用的方法是减去平均值在除以标准差。如果不这样做,某些特征的量纲比较大,在cost函数中将得到更大的加权。数据归一化使得所有特征的权重相等,量纲相同。

对于网络的输入,我们用tanh这个激活函数来举例。如果不做normalization,那么当输入的绝对值比较大的时候,tanh激活函数的输入会比较大,那么tanh就会处于饱和阶段,此时的神经网络对于输入不在敏感,我们的网络基本上已经学不到东西了,激活函数的输出不是+1,就是-1.

-w599

tanh激活函数的导数:
-w456

当输入比较大的时候,tanh处于饱和阶段导数几乎为0,神经网络几乎学习不到东西。

而且,更麻烦的是:这种问题不仅仅出现在输入层,在隐藏层中同样有这个问题

Batch Normalization

总结:BN实际上是用来解决反向传播中的梯度问题的,克服神经网络难以训练的弊病。BN解决了在反向传播过程中的梯度问题(梯度消失和爆炸),同时使得不同scale的w整体更新步调一致。
BN使得,虽然更新了W的值,但是在反向传播的时候,乘以的数不再和w的尺度相关。即尺度较大的w将获得一个较小的梯度,在同等学习速率下其获得的更新更少,这样使得整体w的更新更加稳健起来。

原理

训练深层神经网络是复杂的,因为每一层的输入分布在训练期间随着前一层参数的改变而改变。
BN的思想是:以这样一种方式对每一层的输入进行归一化,即他们具有0的平均输出激活和1的标准差。这是针对每一层的每一个单独的微批次进行的,即单独计算该微批次的平均值和方差,然后归一化。

这类似于网络输入的标准化。有什么好处那?我们知道,对网络输入进行规范化有助于网络学习。但是网络只是一系列层,其中一层的输出会作为下一层的输入。这意味着我们可以把上一层的输入看做后面一个较小网络的的第一层。在应用激活函数之前,先对一个层的输出进行归一化,然后再将其输入到下一层(子网络).

另外一个角度:
大家知道,在机器学习中一个经典假设就是源空间source domain 和 目标空间target domain 的数据分布是一致的。 如果不一致,那么就出现了新的机器学习问题,比如transfer learning。

convariate shift就是源空间和目标空间分布不一致假设下的一个分支问题:它指源空间和目标空间的条件概率是一致的,但是边缘概率不同。也就是:-w325
仔细想想,在神经网络中是不是这样的那?是的!对于神经网络的各层输出,由于他们经过了全连接+激活函数的作用,其分布显然和各层的输入信号分布不同,而且差异会随着网络的增加而增大,但是对应的label是没有变的,这边符合了convariate shift的定义。

google的论文中提到的另一个关键词Internal Covariate Shift就是这个意思:每一层网络的输入都会因为前一层网络参数的变化导致其分布发生改变,这就要求我们必须使用一个很小的学习率和对参数很好的初始化,但是这样么做会让训练过程变得很慢而且复杂。 这种现象就被作者称为Internal Covariate Shift。通过BN可以很好的解决这个问题,并且前提是用mini-batch来训练神经网络。

internal应该是指对于每一个隐藏层的分析。那么通过mini-batch来固定所有层的均值和方差就行了吗?google说这样是可以解决问题的,实际上均值方差相同,分布还真不一定相同。我想,ICS只是这个问题的一种概括的说法吧,属于一种high-level demonstration.

BN添加位置

BN被添加在全连接层和激活函数之间。 输入x和w相乘,经过Batch Normalization,然后再输入到激活函数。

效果

当然,google论文指出BN会带来大约30%的额外计算开销

对比添加batch normalization和不添加的网络,激活函数的输入分布
-w592
可以发现,激活函数的输入分布集中在敏感区域,对应激活函数的导数也比较大,非常有利于网络的学习。

经过激活函数后,激活函数输出分布
-w575

可以发现,经过batch normalization之后,激活函数的输出比较平缓,在饱和阶段和激活阶段都有很多值。对于后面每一层都做这样的操作,那么每一层的输出都会有这样一个分布,更有效的利用tanh的非线性变换,更有利于神经网络的学习。

对比来看,没有使用BN的话,激活函数的输出基本上只有两端最极端的值,都处于饱和阶段,要么为+1,要么为-1,这样的神经网络基本上已经学不到东西了。

最后,让我们通过一个三层神经网络来看看batch normalization后,各层激活函数输出的分布来直观感受下BN的效果:

很明显,BN是的每一层的数据输入都是更加有意义的,让每一层的值都在一个范围内更加有效的传递下去。

反 normalize

在BN的论文中,不仅仅有batch normalization还有一个步骤,我们称之为反 normalize,目的是用于抵消BN的操作

为什么要抵消那?BN作者希望,神经网络自己可以通过学习,调节gamma和belt来学习出前面的BN到底有没有起到优化的作用,如果没有起到作用,那么通过gamma和belt来进行一些抵消操作。

从另外一个角度来看:最后的scale and shift操作是为了让因训练而特意引入的BN能够有可能还原最初的输入。即当-w330
从而保证整个network的capacity。

关于capacity的解释:实际上BN是在原模型基础上增加的新操作,这个新操作很大可能会改变某层原来的输入,当然也可能不改变,不改变的时候就是还原原来的输入。如此一来,既可以改变同时也可以不改变,那么模型的容纳能力(capacity)就提升了。

再从这个角度试着理解下:
BN算法强行将数据拉到均值为0,方差为1的比较标准的正态分布上来。但是这样导致了一个问题:只利用了线性区域而导致深层网络无意义,使得模型的表达能力下降。为了保证非线性的获得,所以用scale和shift来伸缩或移动数据

代码实践

Talk is cheap, show me the code!

数据分布:
-w597

中间过程激活每一层激活函数的输入
刚开始时的输入:
-w681

运行一段时间之后各层的输入:
-w648

可以看到如果没有BN,那么很快输入就只有0了。如果使用了BN,那么输入分布在0-1之间,分布比较好。使用的激活函数是ReLU

损失函数:
当使用ReLU激活函数时,没有BN的话,很快网络就学不到东西了,自然也就没了Loss。使用BN后发现loss还是比较低的。
-w593

我们把激活函数换成tanh再试试:
刚开始各个网络层的输入:
-w640

运行一段时间后的输入:
-w634
可以发现,如果不使用BN,那么运行一段时间之后,激活函数的输出就只剩下-1和+1了。这样的网络基本上学不到什么东西,已经死掉了。但是使用BN的,激活函数的输出依旧是分布在-1和+1之间,比较理想。

损失函数:
-w593

import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt


ACTIVATION = tf.nn.tanh
N_LAYERS = 7
N_HIDDEN_UNITS = 30

def fix_seed(seed=1):
    np.random.seed(seed)
    tf.set_random_seed(seed)

def plot_his(inputs, inputs_norm):
    # plot histogram for the inputs of every layer
    for j, all_inputs in enumerate([inputs, inputs_norm]):
        for i, input in enumerate(all_inputs):
            plt.subplot(2, len(all_inputs), j*len(all_inputs)+(i+1))
            plt.cla()
            if i == 0:
                the_range = (-7, 10)
            else:
                the_range = (-1, 1)
            plt.hist(input.ravel(), bins=15, range=the_range, color='#FF5733')
            plt.yticks(())
            if j == 1:
                plt.xticks(the_range)
            else:
                plt.xticks(())
            ax = plt.gca()
            ax.spines['right'].set_color('none')
            ax.spines['top'].set_color('none')
        plt.title("%s normalizing" % ("Without" if j == 0 else "With"))
    plt.draw()
    plt.pause(0.01)

def built_net(xs, ys, norm):
    def add_layer(inputs, in_size, out_size, activation_function=None, norm=False):
        Weights = tf.Variable(tf.random_normal([in_size, out_size], mean=0.0, stddev=1.0))
        biases = tf.Variable(tf.zeros([1, out_size]) + 0.1)

        # Full Connect
        Wx_plus_b = tf.matmul(inputs, Weights) + biases

        # Batch Normalization
        if norm:
            fc_mean, fc_var = tf.nn.moments(Wx_plus_b, axes=[0])
            scale = tf.Variable(tf.ones([out_size]))
            shift = tf.Variable(tf.zeros([out_size]))
            epsilon = 0.001

            # apply moving average for mean and var when train on batch
            ema = tf.train.ExponentialMovingAverage(decay=0.5)
            def mean_var_with_update():
                ema_apply_op = ema.apply([fc_mean, fc_var])
                with tf.control_dependencies([ema_apply_op]):
                    return tf.identity(fc_mean), tf.identity(fc_var)
            mean, var = mean_var_with_update()

            # Wx_plus_b = (Wx_plus_b - fc_mean) / tf.sqrt(fc_var + 0.001)
            # Wx_plus_b = Wx_plus_b * scale + shift
            Wx_plus_b = tf.nn.batch_normalization(Wx_plus_b, mean, var, shift, scale, epsilon)

        # Activation
        if activation_function == None:
            outputs = Wx_plus_b
        else:
            outputs = activation_function(Wx_plus_b)

        return outputs

    if norm:
        # BN for first layer
        fc_mean, fc_var = tf.nn.moments(xs, axes=[0])
        scale = tf.Variable(tf.ones([1]))
        shift = tf.Variable(tf.zeros([1]))
        epsilon = 0.001

        ema = tf.train.ExponentialMovingAverage(decay=0.5)
        def mean_var_with_update():
            ema_apply_op = ema.apply([fc_mean, fc_var])
            with tf.control_dependencies([ema_apply_op]):
                return tf.identity(fc_mean), tf.identity(fc_var)

        mean, var = mean_var_with_update()

        xs = tf.nn.batch_normalization(xs, mean, var, shift, scale, epsilon)

    # record inputs for every layer
    layers_inputs = [xs]

    # build hidden layer
    for layer_idx in range(N_LAYERS):
        layer_input = layers_inputs[layer_idx]
        in_size = layer_input.get_shape()[1].value

        output = add_layer(
            inputs              = layer_input,
            in_size             = in_size,
            out_size            = N_HIDDEN_UNITS,
            activation_function = ACTIVATION,
            norm                = norm
        )

        layers_inputs.append(output)

    # build output layer
    prediction = add_layer(layers_inputs[-1], N_HIDDEN_UNITS, 1, activation_function=None)

    cost = tf.reduce_mean(tf.reduce_sum(tf.square(ys - prediction), reduction_indices=[1]))
    train_op = tf.train.GradientDescentOptimizer(learning_rate=0.001).minimize(loss=cost)

    return train_op, cost, layers_inputs

# main progress
if __name__ == '__main__':

    fix_seed(2018)

    # make up data
    x_data = np.linspace(start=-7, stop=10, num=2500)[:, np.newaxis]
    np.random.shuffle(x_data)
    noise = np.random.normal(loc=0, scale=8, size=x_data.shape)
    y_data = np.square(x_data) - 5 + noise

    # plot input data
    plt.scatter(x=x_data, y=y_data)
    plt.show()

    # prepare tf
    xs = tf.placeholder(tf.float32, [None, 1])
    ys = tf.placeholder(tf.float32, [None, 1])
    train_op,      cost,      layers_inputs      = built_net(xs, ys, norm=False)
    train_op_norm, cost_norm, layers_inputs_norm = built_net(xs, ys, norm=True)

    # init tf
    sess = tf.Session()
    init = tf.global_variables_initializer()
    sess.run(init)

    # record cost
    cost_his = []
    cost_his_norm = []

    plt.ion() # 打开交互模式
    plt.figure(figsize=(7,3))
    for i in range(250): #[0,249]
        print(i)
        if i % 50 == 0: #plot histgram
            all_inputs, all_inputs_norm = sess.run([layers_inputs, layers_inputs_norm], feed_dict={xs: x_data, ys:y_data})
            plot_his(all_inputs, all_inputs_norm)

        # train on batch
        sess.run([train_op, train_op_norm], feed_dict={xs:x_data, ys:y_data})

        # record cost
        cost_his.append(sess.run(cost, feed_dict={xs:x_data, ys:y_data}))
        cost_his_norm.append(sess.run(cost_norm, feed_dict={xs:x_data, ys:y_data}))

    plt.ioff()
    plt.figure()
    plt.plot(np.arange(len(cost_his)), np.array(cost_his), label='no BN')
    plt.plot(np.arange(len(cost_his)), np.array(cost_his_norm), label='BN')
    plt.legend()
    plt.show()

获取更多机器学习干货、荐货,欢迎关注机器学习荐货情报局 ,加入荐货大家庭。局长的位置还空着呦~
公众号

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值