Batch Normalization(批量标准化,简称BN)是近些年来深度学习优化中一个重要的手段。BN能带来如下优点:
- 加速训练过程;
- 可以使用较大的学习率;
- 允许在深层网络中使用sigmoid这种易导致梯度消失的激活函数;
- 具有轻微地正则化效果,以此可以降低dropout的使用。
但为什么BN能够如此有效?让我们来一探究竟。
一、Covariate Shift
Convariate shift是BN论文作者提出来的概念,其意是指具有不同分布的输入值对深度网络学习的影响。举个例子,假设我们有一个玫瑰花的深度学习网络,这是一个二分类的网络,1表示识别为玫瑰,0则表示非玫瑰花。我们先看看训练数据集的一部分:
直观来说,玫瑰花的特征表现很明显,都是红色玫瑰花。 再看看训练数据集的另一部分:
很明显,这部分数据的玫瑰花各种颜色都有,其特征分布与上述数据集是不一样的。通过下图我们可以再比较下:
图中右侧部分绿色圆圈指的是玫瑰,红色打叉指的是非玫瑰,蓝色线为深度学习最后训练出来的边界。这张图可以更加直观地比较出两个数据集的特征分布是不一样的,这种不一样也就是所谓的covariate shift,而这种分布不一致将减缓训练速度。
为什么这么说呢?输入值的分布不同,也可以理解为输入特征值的scale差异较大,与权重进行矩阵相乘后,会产生一些偏离较大地差异值;而深度学习网络需要通过训练不断更新完善,那么差异值产生的些许变化都会深深影响后层,偏离越大表现越为明显;因此,对于反向传播来说,这些现象都会导致梯度发散,从而需要更多的训练步骤来抵消scale不同带来的影响,也需要更多地步骤才能最终收敛。
而BN的作用就是将这些输入值进行标准化,降低scale的差异至同一个范围内。这样做的好处在于一方面提高梯度的收敛程度,加快训练速度;另一方面使得每一层可以尽量面对同一特征分布的输入值,减少了变化带来的不确定性,也降低了对后层网路的影响,各层网路变得相对独立。
也许上述的解释可能有些晦涩,让我们通过代码和图像来直观理解。
二、Batch Normalization
BN的计算公式如下图所示:
简单地说,通过计算均值和方差后,mini-batch的数据进行标准化,再加上β和γ可以使数据进行移动和缩放。
我们以某个CIFAR-10的数据为例,通过BN的转换来查看数据分布的前后变化,以下为示例代码:
import numpy as npimport matplotlib.pyplot as pltimport tensorflow.keras.backend as Kfrom tensorflow import keras(train_images, train_labels), (test_images, test_labels) = keras.datasets.cifar10.load_data()# 输入图片尺寸为(32, 32, 3),经flatten后大小为(3072, 1)x = train_images[0].reshape(-1) / 255print("x:", x.shape)# 假设我们的隐藏层第一层的输出为(1024, 1),则反推权重大小为(1024, 3072)w = K.eval(K.random_normal_variable(shape=(1024, 3072), mean=0, scale=1))print("w:", w.shape)# 进行矩阵乘法得到大小为(1024, 1)的集合zz = np.dot(w, x)print("z:", z.shape)a = K.constant(z)# 求均值mean = K.mean(a)print("mean:", K.eval(mean))var = K.var(a)# 求方差print("var:", K.eval(var))# 对z进行batch normalization,gamma为0表示不进行移动,beta为0.25表示将normal后的值压缩至1/4大小a = K.eval(K.batch_normalization(a, mean, var, 0, 0.25))# flatten normal值a = a.reshape(-1)print("batch_normal_a:", a.shape)#以图的方式直观展示without_BN和with_BN的区别p1 = plt.subplot(211)p1.hist(z, 50, density=1, facecolor='g', alpha=0.75)p1.set_title("data distribution without BN")p1.set_xlabel('data range')p1.set_ylabel('probability')p1.grid(True)#p1.axis([-4, 4, 0, 1])p2 = plt.subplot(212)p2.hist(a, 50, density=1, facecolor='g', alpha=0.75)p2.set_title("data distribution with BN")p2.set_xlabel('data range')p2.set_ylabel('probability')p2.grid(True)#p2.axis([-4, 4, 0, 1])plt.subplots_adjust(hspace=1)plt.show()
其图像为:
从图像分析可知,数据经过标准化后,其形状保持大致不变,但尺寸被我们压缩至(-1, 1)之间,而原尺寸在(-80,80)之间。
通过平移和缩放,BN可以使数据被限定在我们想要的范围内,所以每层的输出数据都进行BN的话,可以使后层网络面对稳定的输入值,降低梯度发散的可能,从而加快训练速度;同时也意味着允许使用大点的学习率,加快收敛过程。
被缩放的数据让使用sigmoid或tanh激活函数在深层网络变成可能,并且在实际应用中β和γ是可以学习的,下图是一个直观的解释图。
三、Batch Normalizaiotn的实际应用
理论结合实践才能确定是否有用,让我们以keras举例,看看BN是否能提高效率。
上图简要地绘制了BN在神经网络中的位置,在每层网络的激活函数前。与前述例子不同之处在于,数据不是单个进行标准化,而是以mini batch集合的方式进行标准化。
我们通过下述代码来比较和观察without_BN模型和with_BN模型的差异:
import kerasfrom keras.datasets import cifar10from keras.preprocessing.image import ImageDataGeneratorfrom keras.models import Sequentialfrom keras.layers import Dense, Dropout, Activation, Flatten, BatchNormalizationfrom keras.layers import Conv2D, MaxPooling2Dfrom matplotlib import pyplot as pltimport numpy as np# 为保证公平起见,两种方式都使用相同的随机种子np.random.seed(7)batch_size = 32num_classes = 10epochs = 100data_augmentation = True# The data, split between train and test sets:(x_train, y_train), (x_test, y_test) = cifar10.load_data()# Convert class vectors to binary class matrices.y_train = keras.utils.to_categorical(y_train, num_classes)y_test = keras.utils.to_categorical(y_test, num_classes)x_train = x_train.astype('float32')x_test = x_test.astype('float32')x_train /= 255x_test /= 255# without_BN模型的训练model_without_bn = Sequential()model_without_bn.add(Conv2D(32, (3, 3), padding='same', input_shape=x_train.shape[1:]))model_without_bn.add(Activation('relu'))model_without_bn.add(Conv2D(32, (3, 3)))model_without_bn.add(Activation('relu'))model_without_bn.add(MaxPooling2D(pool_size=(2, 2)))model_without_bn.add(Dropout(0.25))model_without_bn.add(Conv2D(64, (3, 3), padding='same'))model_without_bn.add(Activation('relu'))model_without_bn.add(Conv2D(64, (3, 3)))model_without_bn.add(Activation('relu'))model_without_bn.add(MaxPooling2D(pool_size=(2, 2)))model_without_bn.add(Dropout(0.25))model_without_bn.add(Flatten())model_without_bn.add(Dense(512))model_without_bn.add(Activation('relu'))model_without_bn.add(Dropout(0.5))model_without_bn.add(Dense(num_classes))model_without_bn.add(Activation('softmax'))opt = keras.optimizers.rmsprop(lr=0.0001, decay=1e-6)model_without_bn.compile(loss='categorical_crossentropy', optimizer=opt, metrics=['accuracy'])if not data_augmentation: history_without_bn = model_without_bn.fit(x_train, y_train, batch_size=batch_size, epochs=epochs, validation_data=(x_test, y_test), shuffle=True)else: # 使用数据增强获取更多的训练数据 datagen = ImageDataGenerator(width_shift_range=0.1, height_shift_range=0.1, horizontal_flip=True) datagen.fit(x_train) history_without_bn = model_without_bn.fit_generator(datagen.flow(x_train, y_train, batch_size=batch_size), epochs=epochs, validation_data=(x_test, y_test), workers=4)# with_BN模型的训练model_with_bn = Sequential()model_with_bn.add(Conv2D(32, (3, 3), padding='same', input_shape=x_train.shape[1:]))model_with_bn.add(BatchNormalization())model_with_bn.add(Activation('relu'))model_with_bn.add(Conv2D(32, (3, 3)))model_with_bn.add(BatchNormalization())model_with_bn.add(Activation('relu'))model_with_bn.add(MaxPooling2D(pool_size=(2, 2)))model_with_bn.add(Conv2D(64, (3, 3), padding='same'))model_with_bn.add(BatchNormalization())model_with_bn.add(Activation('relu'))model_with_bn.add(Conv2D(64, (3, 3)))model_with_bn.add(BatchNormalization())model_with_bn.add(Activation('relu'))model_with_bn.add(MaxPooling2D(pool_size=(2, 2)))model_with_bn.add(Flatten())model_with_bn.add(Dense(512))model_with_bn.add(BatchNormalization())model_with_bn.add(Activation('relu'))model_with_bn.add(Dense(num_classes))model_with_bn.add(BatchNormalization())model_with_bn.add(Activation('softmax'))opt = keras.optimizers.rmsprop(lr=0.001, decay=1e-6)model_with_bn.compile(loss='categorical_crossentropy', optimizer=opt, metrics=['accuracy'])if not data_augmentation: history_with_bn = model_without_bn.fit(x_train, y_train, batch_size=batch_size, epochs=epochs, validation_data=(x_test, y_test), shuffle=True)else: # 使用数据增强获取更多的训练数据 datagen = ImageDataGenerator(width_shift_range=0.1, height_shift_range=0.1, horizontal_flip=True) datagen.fit(x_train) history_with_bn = model_with_bn.fit_generator(datagen.flow(x_train, y_train, batch_size=batch_size), epochs=epochs, validation_data=(x_test, y_test), workers=4)# 比较两种模型的精确度plt.plot(history_without_bn.history['val_acc'])plt.plot(history_with_bn.history['val_acc'])plt.title('Model accuracy')plt.ylabel('Validation Accuracy')plt.xlabel('Epoch')plt.legend(['No Batch Normalization', 'With Batch Normalization'], loc='lower right')plt.grid(True)plt.show()# 比较两种模型的损失率plt.plot(history_without_bn.history['loss'])plt.plot(history_without_bn.history['val_loss'])plt.plot(history_with_bn.history['loss'])plt.plot(history_with_bn.history['val_loss'])plt.title('Model loss')plt.ylabel('Loss')plt.xlabel('Epoch')plt.legend(['Training Loss without BN', 'Validation Loss without BN', 'Training Loss with BN', 'Validation Loss with BN'], loc='upper right')plt.show()
两种模型的代码差异主要为两点:
- with_BN模型放弃了dropout函数,因为BN本身带有轻微地正则效果
- with_BN的学习率较without_BN模型放大了10倍
本模型中,我们使用了数据增强技术,我们来看看最终的比较图像:
上图显示,测试数据集的精确度明显with_BN模型(87%)要高于without_BN模型(77%)。从训练速度来说,with_BN模型大概在第22代时已经很接近于最终收敛,而without_BN模型大概在第40代时接近于最终收敛,说明with_BN模型也会比较快。
再比较看看损失度,明显可以看出无论是训练集还是测试集,with_BN模型要低于without_BN模型。
四、结论
BN对于优化神经网络,加快训练速度甚至在提高准确度、降低损失度方面都能发挥积极作用,当然想要取得理想的效果也得需要反复地尝试各种组合(比如上述的例子,如果去掉数据增强技术,在我的测试结果,显示测试集的损失度反而更高,过拟合更严重)。