1. 程式人生 > >深度學習基礎系列(七)| Batch Normalization

深度學習基礎系列(七)| Batch Normalization

  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 np
import matplotlib.pyplot as plt
import tensorflow.keras.backend as K
from 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) / 255
print("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)的集合z
z = 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的話,可以使後層網路面對穩定的輸入值,降低梯度發散的可能,從而加快訓練速度;同時也意味著允許使用大點的學習率,加快收斂過程。

  資料如上圖,被壓縮至(-1,1)之間的話,就可以在深層網路使用tanh啟用函式;若進行平移和縮放至(0,1)之間,則又可以使用sigmoid啟用函式。在實際應用中β和γ是可以學習的。

三、Batch Normalizaiotn的實際應用

  理論結合實踐才能確定是否有用,讓我們以keras舉例,看看BN是否能提高效率。

  

  上圖簡要地繪製了BN在神經網路中的位置,在每層網路的啟用函式前。與前述例子不同之處在於,資料不是單個進行標準化,而是以mini batch集合的方式進行標準化。

  我們通過下述程式碼來比較和觀察without_BN模型和with_BN模型的差異:  

import keras
from keras.datasets import cifar10
from keras.preprocessing.image import ImageDataGenerator
from keras.models import Sequential
from keras.layers import Dense, Dropout, Activation, Flatten, BatchNormalization
from keras.layers import Conv2D, MaxPooling2D
from matplotlib import pyplot as plt
import numpy as np

# 為保證公平起見,兩種方式都使用相同的隨機種子
np.random.seed(7)
batch_size = 32
num_classes = 10
epochs = 100
data_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 /= 255
x_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對於優化神經網路,加快訓練速度甚至在提高準確度、降低損失度方面都能發揮積極作用,當然想要取得理想的效果也得需要反覆地嘗試各種組合(比如上述的例子,如果去掉資料增強技術,在我的測試結果,顯示測試集的損失度反而更高,過擬合更嚴重)。