1. 程式人生 > >BN演算法

BN演算法

轉自:https://blog.csdn.net/guoyunfei20/article/details/78404577?locationNum=1&fps=1

Motivation

2015年的論文《Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift》闡述了BN演算法,這個演算法目前已經被大量應用,很多論文都會引用這個演算法,進行網路訓練,可見其強大之處非同一般。

論文作者認為:

網路訓練過程中引數不斷改變導致後續每一層輸入的分佈也發生變化,而學習的過程又要使每一層適應輸入的分佈,因此我們不得不降低學習率、小心地初始化。

論文作者將分佈發生變化稱之為 internal covariate shift。

大家應知道,一般在訓練網路的時會將輸入減去均值,還有些人甚至會對輸入做白化等操作,目的是為了加快訓練。為什麼減均值、白化可以加快訓練呢,這裡做一個簡單地說明:

首先,影象資料是高度相關的,假設其分佈如下圖a所示(簡化為2維)。由於初始化的時候,我們的引數一般都是0均值的,因此開始的擬合y=Wx+b,基本過原點附近,如圖b紅色虛線。因此,網路需要經過多次學習才能逐步達到如紫色實線的擬合,即收斂的比較慢。如果我們對輸入資料先作減均值操作,如圖c,顯然可以加快學習。更進一步的,我們對資料再進行去相關操作,使得資料更加容易區分,這樣又會加快訓練,如圖d。 

白化的方式有好幾種,常用的有PCA白化:即對資料進行PCA操作之後,在進行方差歸一化。這樣資料基本滿足0均值、單位方差、弱相關性。作者首先考慮,對每一層資料都使用白化操作,但分析認為這是不可取的。因為白化需要計算協方差矩陣、求逆等操作,計算量很大,此外,反向傳播時,白化操作不一定可導。於是,作者採用下面的Normalization方法。 

在深度學習中,隨機梯度下降已經成為主要的訓練方法。儘管隨機梯度下降法對於訓練深度網路簡單高效,但需要人為的選擇一些引數,比如學習率、引數初始化、權重衰減係數、dropout比例等。這些引數的選擇對訓練結果至關重要,以至於我們很多時間都浪費在這些的調參上。那麼學完這篇文獻之後,你可以不需要那麼刻意的慢慢調整引數。BN演算法的強大之處表現在: 

1.可以選擇比較大的初始學習率,讓訓練速度飆漲。以前需要慢慢調整學習率,甚至在網路訓練到一半時,還需要想著學習率進一步調小的比例,選擇多少比較合適,現在我們可以採用初始很大的學習率,然後學習率的衰減速度也很大,因為這個演算法收斂很快。當然這個演算法即使你選擇了較小的學習率,也比以前的收斂速度快,因為它具有快速訓練收斂的特性; 

2.你再也不用去理會過擬閤中dropout、L2正則項引數的選擇問題,採用BN演算法後,你可以移除這兩項了引數,或者可以選擇更小的L2正則約束引數了,因為BN具有提高網路泛化能力的特性; 

3.再也不需要使用區域性響應歸一化層了,因為BN本身就是一個歸一化網路層; 

4.可以把訓練資料徹底打亂(防止每批訓練的時候,某一個樣本都經常被挑選到)。 

大家知道,在開始訓練神經網路前,都要對輸入資料做一個歸一化處理,那麼具體為什麼需要歸一化呢?歸一化後有什麼好處呢?原因在於: 

1.神經網路學習過程本質就是為了學習資料分佈,如果訓練資料與測試資料的分佈不同,那麼網路的泛化能力也大大降低;

2.如果每批訓練資料的分佈各不相同(batch 梯度下降),那麼網路就要在每次迭代都去學習適應不同的分佈,這樣將會大大降低網路的訓練速度

對於深度網路的訓練是一個複雜的過程,只要網路的前面幾層發生微小的改變,那麼後面幾層就會被累積放大下去。一旦網路某一層的輸入資料的分佈發生改變,那麼這一層網路就需要去適應學習這個新的資料分佈,所以如果訓練過程中,訓練資料的分佈一直在發生變化,那麼將會影響網路的訓練速度。

大家知道,網路在訓練的過程中,除了輸入層的資料外(因為一般輸入層的資料會人為的對每個樣本進行歸一化),後邊各層的輸入資料的分佈一直在發生變化的。對於中間各層在訓練過程中,資料分佈的改變稱之為internal covariate shift。NP演算法就是為了解決在訓練過程中,中間層資料分佈發生改變的情況下的資料歸一化的。

一、BN概述

就像啟用函式層、卷積層、全連線層、池化層一樣,BN也屬於網路的一層。前面提到網路除了輸入層外,其它各層因為前層網路在訓練的時候更新了引數,而引起後層輸入資料分佈的變化。這個時候我們可能就會想,如果在每一層輸入時,再加個預處理操作,把它歸一化至:均值0、方差為1,然後再輸入後層計算,這樣便解決了前面所提到的Internal Covariate Shift的問題了。事實上,論文中演算法本質原理就是這樣:在網路的每一層輸入的時候,又插入了一個歸一化層,也就是先做一個歸一化處理,然後再進入網路的下一層。不過文獻提到的歸一化層,並不像我們想象的那麼簡單,它是一個可學習、有引數的網路層。

二、預處理

說到神經網路輸入資料預處理,最好的演算法莫過於白化預處理。然而白化計算量太大了,很不划算,還有就是白化不是處處可微的,所以在深度學習中,其實很少用到白化。經過白化預處理後,資料滿足條件: 

1.特徵之間的相關性降低,這個就相當於PCA(主成分分析);

2.資料均值、標準差歸一化,也就是使得每一維特徵均值為0,標準差為1。

如果資料特徵維數比較大,要進行PCA,也就是實現白化的第1個要求,是需要計算特徵向量,計算量非常大,於是為了簡化計算,作者忽略了第1個要求,僅僅使用了下面的公式進行預處理,也就是近似白化預處理。

由於訓練過程採用了batch隨機梯度下降,因此指的是一批訓練資料時,各神經元輸入值的平均值;指的是一批訓練資料時各神經元輸入值的標準差!

三、變換重構

看到上邊的公式,大家可能會感覺很簡單。就是對中間層的輸入資料歸一化嘛!其實沒那麼簡單,比如網路中間層的資料的真實分佈是一個Sigmoid分佈,按上式強制歸一化處理後,勢必會破壞此分佈!為了解決這個問題,論文提出了:變換重構的概念!引入了可學習的引數γ、β,這才是BN演算法關鍵之處:

每一個神經元都會有一對這樣的引數γ、β。這樣其實當:

時,是可以恢復出原始的某一層所學到的特徵的。引入可學習重構引數γ、β,讓網路可以學習恢復出原始網路所要學習的特徵分佈。

四、BN實現

BN層的前向傳導過程公式為:

五、思考1

網路一旦訓練完成;所以的引數都是固定不變的了;因此一個網路在預測時,測試樣本經過中間各層時,用到的是訓練好的μ、β來進行歸一化處理!所以,預測階段BN的公式為:

上式的簡單理解為:對於均值,直接計算batch的平均值;然後對標準差採用batch的無偏估計。最後: 

六、思考2

文獻主要是把BN變換,置於網路啟用函式層的前面。在沒有采用BN的時候,啟用函式層是這樣的: 

z = g(Wx + b)

增加BN後,是這樣的:

z = g( BN(Wx + b) )

其實,因為偏置引數b經過BN層後是沒有用的,最後也會被均值歸一化。另一方面,BN層後面還有個β引數作為偏置項,所以b這個引數就可以不用了。因此最後把BN層+啟用函式層就變成了:

z = g( BN(Wx) ) 

七、BN在CNN中的使用

通過上面的學習,我們知道BN層是對於每個神經元做歸一化處理,甚至只需要對某一個神經元進行歸一化,而不是對一整層網路的神經元進行歸一化。

既然BN是對單個神經元的運算,那麼在CNN中卷積層上要怎麼辦?假如某一層卷積層有6個特徵圖,每個特徵圖的大小是100*100,這樣就相當於這一層網路有6*100*100個神經元,如果採用BN,就會有6*100*100個引數γ、β,這樣豈不是太恐怖了。因此卷積層上的BN使用,其實也是使用了類似權值共享的策略,把一整張特徵圖當做一個神經元進行處理。

說白了,這就是相當於求取所有樣本所對應的一個特徵圖的所有神經元的平均值、方差,然後對這個特徵圖神經元做歸一化。下面是TensorFlow在mnist資料集分類的舉例:

# coding=UTF-8
# neural network structure for this sample:
#
# · · · · · · · · · ·   (input data, 1-deep)                     X [batch, 28, 28,  1]
# @ @ @ @ @ @ @ @ @ @   conv. layer +BN  5x5x1=> 24 stride 1    W1 [    5,  5,  1, 24]  B1 [24]
#                                                               Y1 [batch, 28, 28,  6]
#   @ @ @ @ @ @ @ @     conv. layer +BN  5x5x6=> 48 stride 2    W2 [    5,  5,  6, 48]  B2 [48]
#                                                               Y2 [batch, 14, 14, 12]
#     @ @ @ @ @ @       conv. layer +BN 4x4x12=> 64 stride 2    W3 [    4,  4, 12, 64]  B3 [64]
#                                                               Y3 [batch,  7,  7, 24] 
#                                                => reshaped to YY [batch, 7*7*24]
#      \x/x\x\x/        fully connected layer (relu+dropout+BN) W4 [7*7*24, 200]        B4 [200]
#       · · · ·                                                 Y4 [batch,  200]
#       \x/x\x/         fully connected layer (softmax)         W5 [200,     10]        B5 [10]
#        · · ·                                                   Y [batch,   10]
 
import tensorflow as tf
import math
from tensorflow.examples.tutorials.mnist import input_data as mnist_data
tf.set_random_seed(0.0)
 
# Download images and labels into mnist.test (10K images+labels) 
# and mnist.train (60K images+labels)
mnist = mnist_data.read_data_sets("data", one_hot=True, reshape=False, validation_size=0)
 
# Ylogits      - input data that need tobe batch normalised. For convolutional
#                layer, it's a 4-D tensor. For fully connected layer, it's a 2-D tensor
# is_test      - flag, is_test = False for train
#                      is_test = True  for test
# offset       - beta
#                gamma(scaling) is not useful for relu 
def batchnormForRelu(Ylogits, is_test, Iteration, offset, convolutional=False):
    # adding the iteration prevents from averaging across non-existing iterations
    exp_moving_avg = tf.train.ExponentialMovingAverage(0.999, Iteration) 
    bnepsilon = 1e-5
    if convolutional:
        mean, variance = tf.nn.moments(Ylogits, [0, 1, 2])
    else:
        mean, variance = tf.nn.moments(Ylogits, [0])
    update_moving_averages = exp_moving_avg.apply([mean, variance])
    m = tf.cond(is_test, lambda: exp_moving_avg.average(mean), lambda: mean)
    v = tf.cond(is_test, lambda: exp_moving_avg.average(variance), lambda: variance)
    Ybn = tf.nn.batch_normalization(Ylogits, m, v, offset, None, bnepsilon)
    return Ybn, update_moving_averages
 
def compatible_convolutional_noise_shape(Y):
    noiseshape = tf.shape(Y)
    noiseshape = noiseshape * tf.constant([1,0,0,1]) + tf.constant([0,1,1,0])
    return noiseshape
 
# input X: 28x28 grayscale images
X  = tf.placeholder(tf.float32, [None, 28, 28, 1])
# correct answers will go here
Y_ = tf.placeholder(tf.float32, [None, 10])
# variable learning rate
lr = tf.placeholder(tf.float32)
# test flag for batch norm
tst = tf.placeholder(tf.bool)
Iter = tf.placeholder(tf.int32)
# dropout probability
pkeep = tf.placeholder(tf.float32)
pkeep_conv = tf.placeholder(tf.float32)
 
# three convolutional layers with their channel counts, and a
# fully connected layer (tha last layer has 10 softmax neurons)
K = 24  # 1st convolutional layer output depth
L = 48  # 2nd convolutional layer output depth
M = 64  # 3rd convolutional layer
N = 200 # 4th fully connected layer
 
W1 = tf.Variable(tf.truncated_normal([6, 6, 1, K], stddev=0.1))  # 6x6 patch, 1 input channel, K output channels
B1 = tf.Variable(tf.constant(0.1, tf.float32, [K]))
W2 = tf.Variable(tf.truncated_normal([5, 5, K, L], stddev=0.1))
B2 = tf.Variable(tf.constant(0.1, tf.float32, [L]))
W3 = tf.Variable(tf.truncated_normal([4, 4, L, M], stddev=0.1))
B3 = tf.Variable(tf.constant(0.1, tf.float32, [M]))
 
W4 = tf.Variable(tf.truncated_normal([7 * 7 * M, N], stddev=0.1))
B4 = tf.Variable(tf.constant(0.1, tf.float32, [N]))
W5 = tf.Variable(tf.truncated_normal([N, 10], stddev=0.1))
B5 = tf.Variable(tf.constant(0.1, tf.float32, [10]))
 
# The model
# batch norm scaling is not useful with relus
# batch norm offsets are used instead of biases
Y1l = tf.nn.conv2d(X, W1, strides=[1, 1, 1, 1], padding='SAME')
Y1bn, update_ema1 = batchnormForRelu(Y1l, tst, Iter, B1, convolutional=True)
Y1r = tf.nn.relu(Y1bn)
Y1 = tf.nn.dropout(Y1r, pkeep_conv, compatible_convolutional_noise_shape(Y1r))
stride = 2  # output is 14x14
Y2l = tf.nn.conv2d(Y1, W2, strides=[1, stride, stride, 1], padding='SAME')
Y2bn, update_ema2 = batchnormForRelu(Y2l, tst, Iter, B2, convolutional=True)
Y2r = tf.nn.relu(Y2bn)
Y2 = tf.nn.dropout(Y2r, pkeep_conv, compatible_convolutional_noise_shape(Y2r))
stride = 2  # output is 7x7
Y3l = tf.nn.conv2d(Y2, W3, strides=[1, stride, stride, 1], padding='SAME')
Y3bn, update_ema3 = batchnormForRelu(Y3l, tst, Iter, B3, convolutional=True)
Y3r = tf.nn.relu(Y3bn)
Y3 = tf.nn.dropout(Y3r, pkeep_conv, compatible_convolutional_noise_shape(Y3r))
# reshape
YY = tf.reshape(Y3, shape=[-1, 7 * 7 * M])
Y4l = tf.matmul(YY, W4)
Y4bn, update_ema4 = batchnormForRelu(Y4l, tst, Iter, B4)
Y4r = tf.nn.relu(Y4bn)
Y4 = tf.nn.dropout(Y4r, pkeep)
Ylogits = tf.matmul(Y4, W5) + B5
Y = tf.nn.softmax(Ylogits)
 
update_ema = tf.group(update_ema1, update_ema2, update_ema3, update_ema4)
 
# cross-entropy loss function (= -sum(Y_i * log(Yi)) ), normalised for batches of 100  images
# TensorFlow provides the softmax_cross_entropy_with_logits function to avoid numerical stability
# problems with log(0) which is NaN
cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits=Ylogits, labels=Y_)
cross_entropy = tf.reduce_mean(cross_entropy)*100
 
# accuracy of the trained model, between 0 (worst) and 1 (best)
correct_prediction = tf.equal(tf.argmax(Y, 1), tf.argmax(Y_, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
 
# training step, the learning rate is a placeholder
train_step = tf.train.AdamOptimizer(lr).minimize(cross_entropy)
 
# init
init = tf.global_variables_initializer()
sess = tf.Session()
sess.run(init)
 
def training_step(i, update_test_data, update_train_data):
 
    # training on batches of 100 images with 100 labels
    batch_X, batch_Y = mnist.train.next_batch(100)
 
    # learning rate decay
    max_learning_rate = 0.02
    min_learning_rate = 0.0001
    decay_speed = 1600
    learning_rate = min_learning_rate + (max_learning_rate - min_learning_rate) * math.exp(-i/decay_speed)
 
    # compute training values for visualisation
    if update_train_data:
        a, c = sess.run([accuracy, cross_entropy], \
        {X: batch_X, Y_: batch_Y, tst: False, pkeep: 1.0, pkeep_conv: 1.0})
        print(str(i) + ": accuracy:" + str(a) + " loss: " + str(c) + " (lr:" + str(learning_rate) + ")")
    # compute test values for visualisation
    if update_test_data:
        a, c = sess.run([accuracy, cross_entropy], \
        {X: mnist.test.images, Y_: mnist.test.labels, tst: True, pkeep: 1.0, pkeep_conv: 1.0})
        print(str(i) + ": ********* epoch " + str(i*100//mnist.train.images.shape[0]+1) + " ********* test accuracy:" + str(a) + " test loss: " + str(c))
    # the backpropagation training step
    sess.run(train_step, {X: batch_X, Y_: batch_Y, lr: learning_rate, tst: False, pkeep: 0.75, pkeep_conv: 1.0})
    sess.run(update_ema, {X: batch_X, Y_: batch_Y, tst: False, Iter: i, pkeep: 1.0, pkeep_conv: 1.0})
 
if __name__ == "__main__":
    for i in range(0, 1000):
       training_step(i, True, True)