本文主要从理论与实战的方面对深度学习中的Batch Normalization进行介绍,并
以MNIST数据集作为实战讲解,通过比较加入Batch Normalization前后网络的性能来让大家对Batch Normalization的作用与效果有更加直观的感受。
Batch Normalization,简称BN,
原始论文《Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift》
1.概述
标准化(normalization)是一大类方法,用于让输入到机器学习模型中的样本彼此之间更加相似,这有助于模型的学习与对新数据的泛化。
最常见的数据标准化形式:将数据减去其平均值使其中心为0,然后将数据除以其标准差使其标准差为1。实际上,这种做法假设数据服从正态分布(也叫高斯分布),并确保让该分布的中心为0,同时缩放到方差为1。
normalized_data = (data - np.mean(data, axis=...)) / np.std(data, axis=...)
最常见的数据标准化都是在将数据输入模型之前对数据做标准化,但在网络的每一次变换之后输出的数据发生了变化,都应该考虑数据标准化。
2.批标准化
批标准化(batch normalization)是Ioffe 和Szegedy 在2015 年提出的一种层的类型,即使在训练过程中均值和方差随时间发生变化,它也可以适应性地将各层输出的数据标准化。
批标准化的工作原理是,训练过程中在内部保存已读取每批数据均值和方差的指数移动平均值。批标准化的主要效果是,它有助于梯度传播(这一点和残差连接很像),因此允许更深的网络。对于有些特别深的网络,只有包含多个BatchNormalization 层时才能进行训练。例如,BatchNormalization 广泛用于Keras 内置的许多高级卷积神经网络
架构,比如ResNet50、Inception V3 和Xception。
**Internal Covariate Shift
BN论文作者认为:网络训练过程中参数不断改变导致后续每一层输入的分布也发生变化,而学习的过程又要使每一层适应输入的分布,因此我们不得不降低学习率、小心地初始化。作者将分布发生变化称之为internal covariate shift。
在深度学习中,由于问题的复杂性,我们往往会使用较深层数的网络进行训练,随着训练的进行,网络中的参数也随着梯度下降在不停更新。
一方面,当底层网络中参数发生微弱变化时,由于每一层中的线性变换与非线性激活映射,这些微弱变化随着网络层数的加深而被放大(类似蝴蝶效应);
另一方面,参数的变化导致每一层的输入分布会发生改变,进而上层的网络需要不停地去适应这些分布变化,使得我们的模型训练变得困难。上述这一现象叫做Internal Covariate Shift。
由于上层网络需要不停调整来适应输入数据分布的变化,导致网络学习速度的降低。要缓解ICS的问题,就要明白它产生的原因,产生的原因是由于参数更新带来的网络中每一层输入值分布的改变,并且随着网络层数的加深而变得更加严重,因此我们可以通过固定每一层网络输入值的分布来对减缓ICS问题。
3.算法定义及简介
在深度学习中,由于采用全部一次性加载数据训练方式对内存要求较大,且每一轮训练时间过长,一般都会采用对数据做划分,用 mini-batch对网络进行训练。因此,Batch Normalization也就在mini-batch的基础上进行计算。
传统的神经网络,只是在将样本 x 输入输入层之前对x进行标准化处理(减均值,除标准差),以降低样本间的差异性。BN是在此基础上,不仅仅只对输入层的输入数据x进行标准化,还对每个隐藏层的输入进行标准化,如下图:
我们关注当前层的第 j 个维度,也就是第 j 个神经元结点,对当前维度进行规范化:
通过上面的变换,我们用更加简化的方式来对数据进行规范化,使得第 L 层的输入每个特征的分布均值为0,方差为1。
以上操作我们虽然缓解了ICS问题,让每一层网络的输入数据分布都变得稳定,但却导致了数据表达能力的缺失。
一方面通过变换操作改变了原有数据的信息表达(representation ability of the network),使得底层网络学习到的参数信息丢失。另一方面,通过让每一层的输入分布均值为0,方差为1,会使得输入在经过sigmoid或tanh激活函数时,容易陷入非线性激活函数的线性区域。
因此,BN又引入了两个可学习的参数 γ 与 β ,这两个参数的引入是为了恢复数据本身的表达能力,对规范化后的数据进行线性变换:
以上的两个步骤就是整个Batch Normalization在模型训练中的算法的过程,保证了输入数据的表达能力。
4.Batch Normalization的优点
(1)BN使得网络中每层输入数据的分布相对稳定,加速模型学习速度;
BN通过规范化与线性变换使得每一层网络的输入数据的均值与方差都在一定范围内,使得后一层网络不必不断去适应底层网络中输入的变化,从而实现了网络中层与层之间的解耦,允许每一层进行独立学习,有利于提高整个神经网络的学习速度。
(2) BN允许网络使用饱和性激活函数(例如sigmoid,tanh等),缓解梯度消失问题;
(3)BN具有一定的正则化效果,可以丢弃Dropout
在Batch Normalization中,由于我们使用mini-batch的均值与方差作为对整体训练样本均值与方差的估计,尽管每一个batch中的数据都是从总体样本中抽样得到,但不同mini-batch的均值与方差会有所不同,这就为网络的学习过程中增加了随机噪音,与Dropout通过关闭神经元给网络训练带来噪音类似,在一定程度上对模型起到了正则化的效果。
原作者通过也证明了网络加入BN后,可以丢弃Dropout,模型也同样具有很好的泛化效果。
5.实战
实战部分使用MNIST数据集,并使用TensorFlow中的Batch Normalization结构来进行BN的实现,具体代码如下:
import numpy as np
import pandas as pd
import tensorflow as tf
import tqdm
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings("ignore")
%matplotlib inline
#tensorflow.examples.tutorials is now deprecated and
# it is recommended to use tensorflow.keras.datasets
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True) #此方法将被启用
# mnist = tf.keras.datasets.mnist #推荐使用此方法
# (X_train, y_train), (X_test, y_test) = mnist.load_data()
构建模型
定义一下神经网络的类,这个类里面主要包括了以下方法:
build_network:前向计算
fully_connected:全连接计算
train:训练模型
test:测试模型
class NeuralNetWork():
def __init__(self, initial_weights, activation_fn, use_batch_norm):
"""
初始化网络对象
:param initial_weights: 权重初始化值,是一个list,list中每一个元素是一个权重矩阵
:param activation_fn: 隐层激活函数
:param user_batch_norm: 是否使用batch normalization
"""
self.use_batch_norm = use_batch_norm
self.name = "With Batch Norm" if use_batch_norm else "Without Batch Norm"
self.is_training = tf.placeholder(tf.bool, name='is_training')
# 存储训练准确率
self.training_accuracies = []
self.build_network(initial_weights, activation_fn)
def build_network(self, initial_weights, activation_fn):
"""
构建网络图
:param initial_weights: 权重初始化,是一个list
:param activation_fn: 隐层激活函数
"""
self.input_layer = tf.placeholder(tf.float32, [None, initial_weights[0].shape[0]])
layer_in = self.input_layer
# 前向计算(不计算最后输出层)
for layer_weights in initial_weights[:-1]:
layer_in = self.fully_connected(layer_in, layer_weights, activation_fn)
# 输出层
self.output_layer = self.fully_connected(layer_in, initial_weights[-1])
def fully_connected(self, layer_in, layer_weights, activation_fn=None):
"""
抽象出的全连接层计算
"""
# 如果使用BN与激活函数
if self.use_batch_norm and activation_fn:
weights = tf.Variable(layer_weights)
linear_output = tf.matmul(layer_in, weights)
# 调用BN接口
batch_normalized_output = tf.layers.batch_normalization(linear_output, training=self.is_training)
return activation_fn(batch_normalized_output)
# 如果不使用BN或激活函数(即普通隐层)
else:
weights = tf.Variable(layer_weights)
bias = tf.Variable(tf.zeros([layer_weights.shape[-1]]))
linear_output = tf.add(tf.matmul(layer_in, weights), bias)
return activation_fn(linear_output) if activation_fn else linear_output
def train(self, sess, learning_rate, training_batches, batches_per_validate_data, save_model=None):
"""
训练模型
:param sess: TensorFlow Session
:param learning_rate: 学习率
:param training_batches: 用于训练的batch数
:param batches_per_validate_data: 训练多少个batch对validation数据进行一次验证
:param save_model: 存储模型
"""
# 定义输出label
labels = tf.placeholder(tf.float32, [None, 10])
# 定义损失函数
cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=labels,
logits=self.output_layer))
# 准确率
correct_prediction = tf.equal(tf.argmax(self.output_layer, 1), tf.argmax(labels, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
#
if self.use_batch_norm:
with tf.control_dependencies(tf.get_collection(tf.GraphKeys.UPDATE_OPS)):
train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(cross_entropy)
else:
train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(cross_entropy)
# 显示进度条
for i in tqdm.tqdm(range(training_batches)):
batch_x, batch_y = mnist.train.next_batch(60)
sess.run(train_step, feed_dict={self.input_layer: batch_x,
labels: batch_y,
self.is_training: True})
if i % batches_per_validate_data == 0:
val_accuracy = sess.run(accuracy, feed_dict={self.input_layer: mnist.validation.images,
labels: mnist.validation.labels,
self.is_training: False})
self.training_accuracies.append(val_accuracy)
print("{}: The final accuracy on validation data is {}".format(self.name, val_accuracy))
# 存储模型
if save_model:
tf.train.Saver().save(sess, save_model)
def test(self, sess, test_training_accuracy=False, restore=None):
# 定义label
labels = tf.placeholder(tf.float32, [None, 10])
# 准确率
correct_prediction = tf.equal(tf.argmax(self.output_layer, 1), tf.argmax(labels, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
# 是否加载模型
if restore:
tf.train.Saver().restore(sess, restore)
test_accuracy = sess.run(accuracy, feed_dict={self.input_layer: mnist.test.images,
labels: mnist.test.labels,
self.is_training: False})
print("{}: The final accuracy on test data is {}".format(self.name, test_accuracy))
增加辅助函数train_and_test以及plot绘图函数就可以开始对BN进行测试:
def train_and_test(use_larger_weights, learning_rate, activation_fn, training_batches=50000, batches_per_validate_data=500):
"""
使用相同的权重初始化生成两个网络对象,其中一个使用BN,另一个不使用BN
:param use_larger_weights: 是否使用更大的权重
:param learning_rate: 学习率
:param activation_fn: 激活函数
:param training_batches: 训练阶段使用的batch数(默认为50000)
:param batches_per_validate_data: 训练多少个batch后在validation数据上进行测试
"""
if use_larger_weights:
# 较大初始化权重,标准差为10
# 构造一个4层神经网络,输入层结点数784,三个隐层均为128维,输出层10个结点
weights = [np.random.normal(size=(784,128), scale=10.0).astype(np.float32),
np.random.normal(size=(128,128), scale=10.0).astype(np.float32),
np.random.normal(size=(128,128), scale=10.0).astype(np.float32),
np.random.normal(size=(128,10), scale=10.0).astype(np.float32)
]
else:
# 较小初始化权重,标准差为0.05
weights = [np.random.normal(size=(784,128), scale=0.05).astype(np.float32),
np.random.normal(size=(128,128), scale=0.05).astype(np.float32),
np.random.normal(size=(128,128), scale=0.05).astype(np.float32),
np.random.normal(size=(128,10), scale=0.05).astype(np.float32)
]
tf.reset_default_graph()
nn = NeuralNetWork(weights, activation_fn, use_batch_norm=False) # Without BN
bn = NeuralNetWork(weights, activation_fn, use_batch_norm=True) # With BN
with tf.Session() as sess:
tf.global_variables_initializer().run()
print("【Training Result:】\n")
nn.train(sess, learning_rate, training_batches, batches_per_validate_data)
bn.train(sess, learning_rate, training_batches, batches_per_validate_data)
print("\n【Testing Result:】\n")
nn.test(sess)
bn.test(sess)
plot_training_accuracies(nn, bn, batches_per_validate_data=batches_per_validate_data)
全部代码地址:
https://colab.research.google.com/drive/1UoWhUMrlqSz91Y4BIdU45wuaW7GXUBgr#scrollTo=YKuk7rsYozgy
主要控制一下三个变量:
- 权重矩阵(较小初始化权重,标准差为0.05;较大初始化权重,标准差为10)
- 学习率(较小学习率:0.01;较大学习率:2)
- 隐层激活函数(relu,sigmoid)
使用不同的权重以及学习率,测试结果如下:
5 参考资料
【1】Batch Normalization原理与实战
https://zhuanlan.zhihu.com/p/34879333
【2】深度学习中的Batch Normalization
https://blog.youkuaiyun.com/whitesilence/article/details/75667002
【3】《Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift》
