直覺與實(shí)現(xiàn):Batch Normalization
點(diǎn)擊上方“小白學(xué)視覺”,選擇加"星標(biāo)"或“置頂”
重磅干貨,第一時(shí)間送達(dá)
在本文中,我會回顧一下batch normalization的用處。我也會在Keras中實(shí)現(xiàn)一下batch normalization,并在訓(xùn)練中得到了實(shí)際的提升。代碼可以在https://github.com/harrisonjansma/Research-Computer-Vision/tree/master/07-28-18-Implementing-Batch-Norm找到。
Batch Normalization的一個(gè)直覺的解釋
訓(xùn)練中的問題
問題1:當(dāng)網(wǎng)絡(luò)在訓(xùn)練時(shí),前一層的權(quán)值會變換,導(dǎo)致后面的層的輸入也會變化的比較厲害。每一層都必須調(diào)整權(quán)值來適應(yīng)每個(gè)batch的輸入的分布。這會使得模型的訓(xùn)練變慢。如果我們可以讓每一層的輸入的分布變得相似,那么整個(gè)網(wǎng)絡(luò)就會把精力集中在訓(xùn)練不同的類別上,而不是適應(yīng)不同的分布上。
另外一個(gè)batch之間不同的分布的影響是梯度的消失。梯度消失問題是一個(gè)大問題,特別是對于sigmoid的激活函數(shù)。如果g(x)表示sigmoid激活函數(shù),當(dāng)|x| 增加,g′(x) 趨向于0。

問題1,當(dāng)輸入的分布變化時(shí),神經(jīng)網(wǎng)絡(luò)的輸出也在變化。這就導(dǎo)致了神經(jīng)網(wǎng)絡(luò)的輸出偶爾會進(jìn)入到sigmoid函數(shù)的飽和區(qū)域。一旦到了飽和區(qū)域,神經(jīng)元就無法更新權(quán)值了,沒有梯度回傳到前面的層去。那么,我們?nèi)绾畏乐股窠?jīng)元的輸出變化到飽和區(qū)域呢?
如果我們可以限制神經(jīng)元的輸出在0的附近,我們可以確保每一層都可以通過反向傳播回傳比較大的梯度。這會使得訓(xùn)練速度加快,得到更加準(zhǔn)確的結(jié)果。

Batch Norm解決方案
Batch normalization減輕了輸入的變化的影響。通過對神經(jīng)元的輸出進(jìn)行歸一化,激活函數(shù)的輸入都是在0附近的,這就保證了沒有梯度的消失,解決了第二個(gè)問題。
Batch normalization將每一層的輸出變換成一個(gè)單位的高斯分布。由于這些輸出被輸入到一個(gè)激活函數(shù)中,激活后的值也是一個(gè)正態(tài)的分布。
因?yàn)橐粚拥妮敵鍪窍乱粚拥妮斎?,每一層的輸入的分布對于不同的batch來說就不會有太大的變化。通過減小輸入層的分布的變化,我們解決了第一個(gè)問題。
數(shù)學(xué)解釋
通過batch normalization,我們尋找一個(gè)以0為中心的,單位方差的分布作為每一層的激活函數(shù)的輸入。在訓(xùn)練的時(shí)候,我們用激活的輸入x減去這個(gè)batch中的均值μ來得到以0為中心的分布。

然后,我們用x除以這個(gè)batch的方差,這里需要一個(gè)很小的數(shù)來防止除0操作, 也就是σ+?。這樣確保了所有的激活函數(shù)的輸入分布具有單位方差。

最后,我們將x通過一個(gè)線性變換,通過一個(gè)縮放和偏移,得到了 batch normalization的輸出。確保這個(gè)歸一化的作用會保持住,盡管網(wǎng)絡(luò)在反向傳播的時(shí)候會有變化。

當(dāng)測試模型的時(shí)候,我們并不使用batch的均值和方差,因?yàn)檫@可能影響模型。而是計(jì)算均值和方差的移動平均來估計(jì)訓(xùn)練集的分布。這樣的估計(jì)是在訓(xùn)練的過程中對所有的batch的均值和方差進(jìn)行計(jì)算得到的。
Batch Normalization的好處
batch normalization的好處如下:
1. 幫助防止網(wǎng)絡(luò)中的梯度消失線性,特別是使用飽和的非線性激活函數(shù)的時(shí)候(如sigmoid,tanh)
使用batch normalization,我們確保激活函數(shù)的輸入不會落入到飽和區(qū)域。batch normalization將輸入的分布變換到單位高斯分布(0均值,單位方差)。
2. 模型正則化
也許有,Ioffe和Svegeddy聲稱有這個(gè)作用,但是并沒有在這個(gè)問題上展開說。也許這個(gè)效果來自于層的輸入的歸一化?
3. 允許更高的學(xué)習(xí)率
通過防止訓(xùn)練時(shí)候梯度消失的問題,我們可以使用更高的學(xué)習(xí)率。Batch normalization同樣減少了對于參數(shù)尺度的依賴。大的學(xué)習(xí)了可以增加參數(shù)的尺度,從而在反向傳播的時(shí)候造成梯度的放大,對于這,我需要更多了解一下。
Keras的實(shí)現(xiàn)
導(dǎo)入包
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數(shù)據(jù)加載和預(yù)處理
在這里,我們使用了 Cifar 100的數(shù)據(jù)集,難度合理,不會訓(xùn)練很長時(shí)間。預(yù)處理只做了0均值的處理,以及一個(gè)圖像的變換的生成器。
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中構(gòu)建模型
我們的網(wǎng)絡(luò)結(jié)構(gòu)由 3x3 的卷積層堆疊而成,卷積后面接最大化池化和dropout。每個(gè)網(wǎng)絡(luò)中有5個(gè)卷積block。最后一層是全連接層,有100個(gè)節(jié)點(diǎn),使用softmax作為激活函數(shù)。
我們構(gòu)建了4個(gè)不同的卷積神經(jīng)網(wǎng)絡(luò),每個(gè)或者使用sigmoid或者使用ReLU激活函數(shù),或者使用了 batch normalization,或者沒有。我們會對比每個(gè)網(wǎng)絡(luò)的有效的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訓(xùn)練
Sigmoid不使用Batch Normalization
訓(xùn)練卡住了,使用100個(gè)類,模型并不比隨機(jī)猜好(10%的準(zhǔn)確率)。
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])
Sigmoid使用Batch Normalization
和不用 batch normalization不一樣,模型總算是有點(diǎn)起色了,這應(yīng)該是 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])
ReLU不使用Batch Normalization
使用ReLU,不使用 batch normalization,有一點(diǎn)初始的提升,收斂到了一個(gè)局部最小中。
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])
ReLU使用Batch Normalization
和sigmoid一樣, batch normalization在訓(xùn)練中提高了網(wǎng)絡(luò)的能力。
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])
不同結(jié)構(gòu)的對比
我們可以清楚的看到 batch normalization的好處。ReLU 和sigmoid 的模型在沒有batch normalization的時(shí)候,都沒有訓(xùn)練的很好。可能是梯度消失的原因。使用了batch normalization的模型訓(xùn)練的更快,而且效果更好。

結(jié)論
batch normalization減少了訓(xùn)練的時(shí)間,提高了神經(jīng)網(wǎng)絡(luò)的穩(wěn)定性。對于sigmoid和ReLU都有效果。
英文原文鏈接:https://towardsdatascience.com/intuit-and-implement-batch-normalization-c05480333c5b
好消息!
小白學(xué)視覺知識星球
開始面向外開放啦??????
下載1:OpenCV-Contrib擴(kuò)展模塊中文版教程 在「小白學(xué)視覺」公眾號后臺回復(fù):擴(kuò)展模塊中文教程,即可下載全網(wǎng)第一份OpenCV擴(kuò)展模塊教程中文版,涵蓋擴(kuò)展模塊安裝、SFM算法、立體視覺、目標(biāo)跟蹤、生物視覺、超分辨率處理等二十多章內(nèi)容。 下載2:Python視覺實(shí)戰(zhàn)項(xiàng)目52講 在「小白學(xué)視覺」公眾號后臺回復(fù):Python視覺實(shí)戰(zhàn)項(xiàng)目,即可下載包括圖像分割、口罩檢測、車道線檢測、車輛計(jì)數(shù)、添加眼線、車牌識別、字符識別、情緒檢測、文本內(nèi)容提取、面部識別等31個(gè)視覺實(shí)戰(zhàn)項(xiàng)目,助力快速學(xué)校計(jì)算機(jī)視覺。 下載3:OpenCV實(shí)戰(zhàn)項(xiàng)目20講 在「小白學(xué)視覺」公眾號后臺回復(fù):OpenCV實(shí)戰(zhàn)項(xiàng)目20講,即可下載含有20個(gè)基于OpenCV實(shí)現(xiàn)20個(gè)實(shí)戰(zhàn)項(xiàng)目,實(shí)現(xiàn)OpenCV學(xué)習(xí)進(jìn)階。 交流群
歡迎加入公眾號讀者群一起和同行交流,目前有SLAM、三維視覺、傳感器、自動駕駛、計(jì)算攝影、檢測、分割、識別、醫(yī)學(xué)影像、GAN、算法競賽等微信群(以后會逐漸細(xì)分),請掃描下面微信號加群,備注:”昵稱+學(xué)校/公司+研究方向“,例如:”張三 + 上海交大 + 視覺SLAM“。請按照格式備注,否則不予通過。添加成功后會根據(jù)研究方向邀請進(jìn)入相關(guān)微信群。請勿在群內(nèi)發(fā)送廣告,否則會請出群,謝謝理解~

