原理解釋|直覺與實現:Batch Normalization

人工智能 數學 操作系統 AI公園 2019-07-04
作者:Harrison Jansma編譯:ronghuaiyang

在本文中,我會回顧一下batch normalization的用處。我也會在Keras中實現一下batch normalization,並在訓練中得到了實際的提升。代碼可以在https://github.com/harrisonjansma/Research-Computer-Vision/tree/master/07-28-18-Implementing-Batch-Norm找到。

Batch Normalization的一個直覺的解釋

訓練中的問題

問題1:當網絡在訓練時,前一層的權值會變換,導致後面的層的輸入也會變化的比較厲害。每一層都必須調整權值來適應每個batch的輸入的分佈。這會使得模型的訓練變慢。如果我們可以讓每一層的輸入的分佈變得相似,那麼整個網絡就會把精力集中在訓練不同的類別上,而不是適應不同的分佈上。

另外一個batch之間不同的分佈的影響是梯度的消失。梯度消失問題是一個大問題,特別是對於sigmoid的激活函數。如果g(x)表示sigmoid激活函數,當|x| 增加,g′(x) 趨向於0。

原理解釋|直覺與實現:Batch Normalization

問題1,當輸入的分佈變化時,神經網絡的輸出也在變化。這就導致了神經網絡的輸出偶爾會進入到sigmoid函數的飽和區域。一旦到了飽和區域,神經元就無法更新權值了,沒有梯度回傳到前面的層去。那麼,我們如何防止神經元的輸出變化到飽和區域呢?

如果我們可以限制神經元的輸出在0的附近,我們可以確保每一層都可以通過反向傳播回傳比較大的梯度。這會使得訓練速度加快,得到更加準確的結果。


原理解釋|直覺與實現:Batch Normalization

Batch Norm解決方案

Batch normalization減輕了輸入的變化的影響。通過對神經元的輸出進行歸一化,激活函數的輸入都是在0附近的,這就保證了沒有梯度的消失,解決了第二個問題。

Batch normalization將每一層的輸出變換成一個單位的高斯分佈。由於這些輸出被輸入到一個激活函數中,激活後的值也是一個正態的分佈。

因為一層的輸出是下一層的輸入,每一層的輸入的分佈對於不同的batch來說就不會有太大的變化。通過減小輸入層的分佈的變化,我們解決了第一個問題。

數學解釋

通過batch normalization,我們尋找一個以0為中心的,單位方差的分佈作為每一層的激活函數的輸入。在訓練的時候,我們用激活的輸入x減去這個batch中的均值μ來得到以0為中心的分佈。

原理解釋|直覺與實現:Batch Normalization

然後,我們用x除以這個batch的方差,這裡需要一個很小的數來防止除0操作, 也就是σ+ϵ。這樣確保了所有的激活函數的輸入分佈具有單位方差。

原理解釋|直覺與實現:Batch Normalization

最後,我們將x通過一個線性變換,通過一個縮放和偏移,得到了 batch normalization的輸出。確保這個歸一化的作用會保持住,儘管網絡在反向傳播的時候會有變化。

原理解釋|直覺與實現:Batch Normalization

當測試模型的時候,我們並不使用batch的均值和方差,因為這可能影響模型。而是計算均值和方差的移動平均來估計訓練集的分佈。這樣的估計是在訓練的過程中對所有的batch的均值和方差進行計算得到的。

Batch Normalization的好處

batch normalization的好處如下:

1. 幫助防止網絡中的梯度消失線性,特別是使用飽和的非線性激活函數的時候(如sigmoid,tanh)

使用batch normalization,我們確保激活函數的輸入不會落入到飽和區域。batch normalization將輸入的分佈變換到單位高斯分佈(0均值,單位方差)。

2. 模型正則化

也許有,Ioffe和Svegeddy聲稱有這個作用,但是並沒有在這個問題上展開說。也許這個效果來自於層的輸入的歸一化?

3. 允許更高的學習率

通過防止訓練時候梯度消失的問題,我們可以使用更高的學習率。Batch normalization同樣減少了對於參數尺度的依賴。大的學習了可以增加參數的尺度,從而在反向傳播的時候造成梯度的放大,對於這,我需要更多瞭解一下。

Keras的實現

導入包

import tensorflow as tf
import numpy as np
import os

import keras
from keras.preprocessing.image import ImageDataGenerator, array_to_img,img_to_array, load_img
import matplotlib.pyplot as plt
import matplotlib.image as mpimg

from keras.models import Model, Sequential
from keras.layers import Input

from keras.callbacks import ModelCheckpoint, EarlyStopping
from keras.layers import BatchNormalization
from keras.layers import GlobalAveragePooling2D
from keras.layers import Activation
from keras.layers import Conv2D, MaxPooling2D, Dense
from keras.layers import MaxPooling2D, Dropout, Flatten

import time

數據加載和預處理

在這裡,我們使用了 Cifar 100的數據集,難度合理,不會訓練很長時間。預處理只做了0均值的處理,以及一個圖像的變換的生成器。

 from keras.datasets import cifar100
from keras.utils import np_utils

(x_train, y_train), (x_test, y_test) = cifar100.load_data(label_mode='fine')

#scale and regularize the dataset
x_train = (x_train-np.mean(x_train))
x_test = (x_test - x_test.mean())

x_train = x_train.astype('float32')
x_test = x_test.astype('float32')

#onehot encode the target classes
y_train = np_utils.to_categorical(y_train)
y_test = np_utils.to_categorical(y_test)


train_datagen = ImageDataGenerator(
shear_range=0.2,
zoom_range=0.2,
horizontal_flip=True)

train_datagen.fit(x_train)

train_generator = train_datagen.flow(x_train,
y = y_train,
batch_size=80,)

在Keras中構建模型

我們的網絡結構由 3x3 的卷積層堆疊而成,卷積後面接最大化池化和dropout。每個網絡中有5個卷積block。最後一層是全連接層,有100個節點,使用softmax作為激活函數。

我們構建了4個不同的卷積神經網絡,每個或者使用sigmoid或者使用ReLU激活函數,或者使用了 batch normalization,或者沒有。我們會對比每個網絡的有效的loss。

def conv_block_first(model, bn=True, activation="sigmoid"):
"""
The first convolutional block in each architecture. Only separate so we can specify the input shape.
"""
#First Stacked Convolution
model.add(Conv2D(60,3, padding = "same", input_shape = x_train.shape[1:]))
if bn:
model.add(BatchNormalization())
model.add(Activation(activation))
#Second Stacked Convolution
model.add(Conv2D(60,3, padding = "same"))
if bn:
model.add(BatchNormalization())
model.add(Activation(activation))

model.add(MaxPooling2D())
model.add(Dropout(0.15))
return model
def conv_block(model, bn=True, activation = "sigmoid"):
"""
Generic convolutional block with 2 stacked 3x3 convolutions, max pooling, dropout,
and an optional Batch Normalization.
"""
model.add(Conv2D(60,3, padding = "same"))
if bn:
model.add(BatchNormalization())
model.add(Activation(activation))

model.add(Conv2D(60,3, padding = "same"))
if bn:
model.add(BatchNormalization())
model.add(Activation(activation))

model.add(MaxPooling2D())
model.add(Dropout(0.15))
return model

def conv_block_final(model, bn=True, activation = "sigmoid"):
"""
I bumped up the number of filters in the final block. I made this separate so that I might be able to integrate Global Average Pooling later on.
"""
model.add(Conv2D(100,3, padding = "same"))
if bn:
model.add(BatchNormalization())
model.add(Activation(activation))

model.add(Conv2D(100,3, padding = "same"))
if bn:
model.add(BatchNormalization())
model.add(Activation(activation))

model.add(Flatten())
return model

def fn_block(model):
"""
I'm not going for a very deep fully connected block, mainly so I can save on memory.
"""
model.add(Dense(100, activation = "softmax"))
return model

def build_model(blocks=3, bn=True, activation = "sigmoid"):
"""
Builds a sequential network based on the specified parameters.

blocks: number of convolutional blocks in the network, must be greater than 2.
bn: whether to include batch normalization or not.
activation: activation function to use throughout the network.
"""
model = Sequential()

model = conv_block_first(model, bn=bn, activation=activation)

for block in range(1,blocks-1):
model = conv_block(model, bn=bn, activation = activation)

model = conv_block_final(model, bn=bn, activation=activation)
model = fn_block(model)

return model

def compile_model(model, optimizer = "rmsprop", loss ="categorical_crossentropy", metrics = ["accuracy"]):
"""
Compiles a neural network.

model: the network to be compiled.
optimizer: the optimizer to use.
loss: the loss to use.
metrics: a list of keras metrics.
"""
model.compile(optimizer = optimizer,
loss = loss,
metrics = metrics)
return model
#COMPILING THE 4 MODELS
sigmoid_without_bn = build_model(blocks = 5, bn=False, activation ="sigmoid")
sigmoid_without_bn = compile_model(sigmoid_without_bn)

sigmoid_with_bn = build_model(blocks = 5, bn=True, activation = "sigmoid")
sigmoid_with_bn = compile_model(sigmoid_with_bn)

relu_without_bn = build_model(blocks = 5, bn=False, activation = "relu")
relu_without_bn = compile_model(relu_without_bn)

relu_with_bn = build_model(blocks = 5, bn=True, activation = "relu")
relu_with_bn = compile_model(relu_with_bn)

Model訓練

Sigmoid不使用Batch Normalization

訓練卡住了,使用100個類,模型並不比隨機猜好(10%的準確率)。

history1 = sigmoid_without_bn.fit_generator(
train_generator,
steps_per_epoch=2000,
epochs=20,
verbose=0,
validation_data=(x_test, y_test),
callbacks = [model_checkpoint])
原理解釋|直覺與實現:Batch Normalization

Sigmoid使用Batch Normalization

和不用 batch normalization不一樣,模型總算是有點起色了,這應該是 batch normalization的減輕了梯度消失的作用。

history2 = sigmoid_with_bn.fit_generator(
train_generator,
steps_per_epoch=2000,
verbose=0,
epochs=20,
validation_data=(x_test, y_test),
callbacks = [model_checkpoint])


原理解釋|直覺與實現:Batch Normalization

ReLU不使用Batch Normalization

使用ReLU,不使用 batch normalization,有一點初始的提升,收斂到了一個局部最小中。

history3 = relu_without_bn.fit_generator(
train_generator,
steps_per_epoch=2000,
epochs=20,
verbose=0,
validation_data=(x_test, y_test),
callbacks = [model_checkpoint])


原理解釋|直覺與實現:Batch Normalization

ReLU使用Batch Normalization

和sigmoid一樣, batch normalization在訓練中提高了網絡的能力。

history4 = relu_with_bn.fit_generator(
train_generator,
steps_per_epoch=2000,
verbose=0,
epochs=20,
validation_data=(x_test, y_test),
callbacks = [model_checkpoint])


原理解釋|直覺與實現:Batch Normalization

不同結構的對比

我們可以清楚的看到 batch normalization的好處。ReLU 和sigmoid 的模型在沒有batch normalization的時候,都沒有訓練的很好。可能是梯度消失的原因。使用了batch normalization的模型訓練的更快,而且效果更好。

原理解釋|直覺與實現:Batch Normalization

結論

batch normalization減少了訓練的時間,提高了神經網絡的穩定性。對於sigmoid和ReLU都有效果。

原文鏈接:https://towardsdatascience.com/intuit-and-implement-batch-normalization-c05480333c5b

更多文章,請關注微信公眾號:AI公園

相關推薦

推薦中...