为什么我们需要BN
回顾梯度下降
我们知道,神经网路的优化方法都是基于梯度下降的思想,简言之,梯度下降优化方法有这三个步骤
①计算当前参数下的损失函数的梯度
②通过链式求导法则更新参数使得损失函数在梯度方向上前进一个步长
③重复①②直到损失函数的值小到我们满意为止
其中,②得到了新的参数,并改变了损失函数的值,我们的目的是,在这一步使得损失函数缩小,这样才有可能在不断地迭代中使得损失函数的值逐渐缩小到我们想要的程度
回顾Normalization
图1图2
如图,这是使用梯度下降时遇到的几种情况,图中圆圈的意思是在这条圆圈上的任意两个点的损失函数的值相同,类似于地理中的等高线,箭头的方向就是在图中某个点的损失函数的梯度方向,箭头的长度就是步长,损失函数按照箭头的路径逐渐走向最低点。
图①指当数据的几个维度量级差距较大时,损失函数在数据上的分布呈现为狭长的椭圆形
图②指当数据的几个维度相差不大时,损失函数在数据上的分布呈现为圆形
对比发现,如果数据分布如图②所示,那么只需要走几步就走到了最低点,而图①不但要走很多很多步才能走到最低点,而且一旦走的步子大一些,如下图,就会越走离最低点越远。
所以,以往在机器学习算法的数据预处理中,必不可少的一步就是归一化,以此来加速模型的收敛,其中一种常用的方式就是Standardization,又称为Z-score normalization,公式如下
μ 是平移参数(shift parameter),σ 是缩放参数(scale parameter)。通过这两个参数进行 shift 和 scale 变换,得到的数据符合均值为 0、方差为 1 的正态分布。
Normalization做到了什么
通过简单回顾梯度下降和Normalization,我们知道,为了使损失函数下降,而且下降的够快,我们期望数据的分布在各个特征上差别不大,而Normalization使得数据在各个维度都是均值为 0、方差为 1 的正态分布,加速了模型的收敛。
梯度爆炸/消失
当我们回到深度学习领域时,我们常常遇到的问题是:梯度消失和梯度爆炸。
我们来回顾一下前向传播的公式
从公式可以看出,在i逐渐增大,也就是网络的逐渐加深的过程中, 不断和权重矩阵 相乘,假设情况乐观,我们的权重矩阵的特征值接近于1,那么相乘之后变化不大,然而实际中,权重矩阵的特征值往往不在1附近,比1小的时候, 越乘越小,比1大的时候, 越乘越大,从而造成了梯度消失/梯度爆炸。
简而言之,每层网络激活函数的输出值随着层数的增多变得分布集中于区间的两端,这种情形就类似前面的图①,会使得神经网络的收敛速度变慢。
既然在逻辑回归算法的梯度下降中 Normalization能调整数据的分布,加快收敛,那神经网络作为逻辑回归模型的衍生,同样基于梯度下降,在每一层的输入做一下Normalization,是否可以起到同样的效果呢?
显然是可以的~
BN是如何发挥作用的
公式原理
设某一隐藏层的输入是 ,类似公式(2),首先需要求出中间值
然后在求出激活值之前进行BN,过程如下
设当前mini-batch有m个样本,对应m个中间值,分别为、、......
①首先求出当前中间值的均值和方差
②然后进行normalization,与公式(1)相似
这里略有不同的是替代是为了避免分母为0
进行到这一步,已经是一个均值为0,方差为1的变量,但是我们不希望所有层的分布总是如此,毕竟我们希望隐藏层能够学习到足够的知识,而数据的分布本身就是需要学习到的,所以我们进一步计算
③适当进行缩放和平移
缩放参数和平移参数,就像权重矩阵一样参与梯度下降的参数更新,随着模型的迭代逐渐改变
④最后使用激活函数
这个过程中,我们既保证了每一层的激活值的数据分布是一个正态分布,体现出来的数据形状类似上图的图②,表现为一个规则的园坑,又能使得每一层不至于在normalization之后损失了太多本该被学习到的知识
总而言之
BN起到了类似normalization的作用,对数据的分布发生了改变,使得损失函数的取值分布接近于圆坑,就像上面的图②,从而可以使用更大的学习率,加快了网络的收敛。
基于这样的好处,我基本上在做任何深度学习的模型的时候都加上BN处理,这样会使我的调参变得很任性,学习率设置的比较大也不怕。
如何实现BN
各大深度学习框架对bn的实现都封装的很简单了
keras
我这里提供了图片二分类的demo,包括了卷积结合bn和全连接层结合bn
def cnnModel(n_W,n_H, Y_data):
# 输入层
input = Input(shape=[n_W,n_H], name='input')
# 卷积层
l_conv = Conv2D(filters=16, kernel_size=(3,3))(input)
# bn
l_bn1=BatchNormalization(axis=-1)(l_conv)
# bn完了调用激活函数,才算完成了卷积操作
l_conv=Activation('relu')(l_bn1)
# pooling
l_pool = MaxPooling2D(pool_size=(2, 2), strides=(2, 2))(l_conv)
# 展开为全连接做准备
l_flat = Flatten()(l_pool)
# 全连接层
l_fc=Dense(512)(l_flat)
l_bn2=BatchNormalization()(l_fc)
l_fc=Activation('relu')(l_bn2)
# dropout
l_fc=Dropout(0.2)(l_fc)
# 输出层 二分类units为1 softmax的话units等于类别数
output = Dense(units=1, activation='sigmoid', name='output')(l_fc)
# 创建model的实例
model = Model(inputs=input, outputs=output, name='CNN-demo')
model.compile(optimizer="adam", loss="binary_crossentropy", metrics=["accuracy"])
return model
tensorflow
我这里提供了dnn二分类的前项传播部分,加上损失函数就可以用了
def forward_propagation(X, training):
"""
前向传播 training是一个bool类型的placeholder,指明是否在训练中
"""
# 先构建 tensor
W1 = tf.get_variable(name="W1", shape=(131, X.shape[0]), dtype=tf.float32,
initializer=tf.contrib.layers.xavier_initializer())
b1 = tf.get_variable(name="b1", shape=(131, 1), dtype=tf.float32, initializer=tf.zeros_initializer())
W2 = tf.get_variable(name="W2", shape=(1, 131), dtype=tf.float32,
initializer=tf.contrib.layers.xavier_initializer())
b2 = tf.get_variable(name="b2", shape=(1, 1), dtype=tf.float32, initializer=tf.zeros_initializer())
# 完成前向传播运算
Z1 = tf.add(tf.matmul(W1, X), b1, name='Z1') # Z1 = np.dot(W1, X) + b1
# 使用batch normalized
bn1 = tf.layers.batch_normalization(Z1, axis=0, training=training, name='bn1')
A1 = tf.nn.relu(bn1, name='A1') # A1 = relu(Z1)
# 这里不用调用激活函数,因为损失函数包含了激活函数
Z2 = tf.add(tf.matmul(W2, A1), b2, name='Z2') # Z2 = np.dot(W2, a1) + b2
return Z2
总之,我们可以在任何一个隐层的中间值和之间加上这些代码
网友评论