Batch normalization理解

? ? ? ?在學(xué)習(xí)源碼的過程中,發(fā)現(xiàn)在搭建網(wǎng)絡(luò)架構(gòu)的時候,經(jīng)常會用到bn算法(即batch_normalization,批標(biāo)準(zhǔn)化),所以有必要深入去研究其中的奧妙。bn算法的提出在2015年的論文《Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift》

? ? ? ?正如論文開始所說:由于訓(xùn)練過程中各層輸入的分布隨著前幾層參數(shù)的變化而變化,使得訓(xùn)練深度神經(jīng)網(wǎng)絡(luò)變得復(fù)雜。這通過要求較低的學(xué)習(xí)速率仔細(xì)的參數(shù)初始化來減慢訓(xùn)練,并且使得訓(xùn)練具有飽和非線性的模型變得非常困難。我們將這種現(xiàn)象稱為 internal covariate shift。顯而易見,為了解決這個問題,作者提出了bn,所以前提,我們得先理解何為internal covariate shift。

將在訓(xùn)練過程中深度網(wǎng)絡(luò)內(nèi)部節(jié)點(diǎn)分布的變化稱為internal covariate shift。這個internal可以看作隱層
作者以sigmoid為例,z = g(wu+b),其中u為輸入層,w和b為權(quán)重矩陣和偏移向量(即網(wǎng)絡(luò)層里需要學(xué)習(xí)的參數(shù))


隨著|x|的增加,g'(x)會趨向于0,這意味著,在所有維數(shù)上,除了那些絕對值較小的 x=wu+b ,下降到 u 的梯度將消失,模型將緩慢訓(xùn)練。

x 受 W,b 和前面所有層的參數(shù)的影響,在訓(xùn)練過程中這些參數(shù)的變化可能會將 x 的許多維度移入非線性的飽和狀態(tài)并減慢收斂速度。這種影響隨著網(wǎng)絡(luò)深度的增加而放大。

我們都知道在train網(wǎng)絡(luò)之前,會對數(shù)據(jù)進(jìn)行歸一化處理,為的是保持訓(xùn)練和測試數(shù)據(jù)的分布相同,而在神經(jīng)網(wǎng)絡(luò)內(nèi)部,每一層我們都需要有輸出和輸出,除了對原始數(shù)據(jù)的標(biāo)準(zhǔn)化處理,在經(jīng)過網(wǎng)絡(luò)每一層計(jì)算后的數(shù)據(jù),它們的分布是不同的。網(wǎng)絡(luò)的訓(xùn)練,需要去學(xué)習(xí)適應(yīng)不同的數(shù)據(jù)分布,明顯造成的后果就是收斂慢,效果不佳。
另一方面,網(wǎng)絡(luò)前面的參數(shù)變更,會隨著網(wǎng)絡(luò)的深度,其影響不斷累積增大,所以說只要有某一層的數(shù)據(jù)分布發(fā)生變化,后面層的數(shù)據(jù)輸入分布也會不同,結(jié)合前面說的,為了解決中間層數(shù)據(jù)分布改變的情況。

總的來說,bn的操作很簡單,也很容易理解。就是在網(wǎng)絡(luò)的每一層輸入之前,做了一個歸一化處理,就是作用于(wu+b),即bn(wu+b),然后再接激活函數(shù)(非線性映射)。而且,很多論文的代碼里,bn算作了獨(dú)立的一層。

公式如下:


E[x]為均值,sqrt(var)為標(biāo)準(zhǔn)差,然后加上scale和shift兩個可訓(xùn)練的變量
而Batch Normalization可使各隱藏層輸入的均值和方差為任意值。實(shí)際上,從激活函數(shù)的角度來說,如果各隱藏層的輸入均值在靠近0的區(qū)域即處于激活函數(shù)的線性區(qū)域,這樣不利于訓(xùn)練好的非線性神經(jīng)網(wǎng)絡(luò),得到的模型效果也不會太好。這也解釋了為什么需要用 γ 和 β 來做進(jìn)一步處理



y(k)就是經(jīng)過bn處理后的輸出了,論文里提到,當(dāng)

可以恢復(fù)原始的激活,也就是這一層所學(xué)到的原始特征
對于批處理的推理如下,整個思路也很清晰:

到了這里,其實(shí)腦子還是很懵,一知半解,還是屬于抽象的理解,對于其中還是很多疑惑
想要更好的理解,還是得回到數(shù)學(xué)的層面去看

一般來說,如果模型的輸入特征不相關(guān)且滿足標(biāo)準(zhǔn)正態(tài)分布時,模型的表現(xiàn)一般較好。
在訓(xùn)練神經(jīng)網(wǎng)絡(luò)模型時,我們可以事先將特征去相關(guān)并使得它們滿足一個比較好的分布,
這樣,模型的第一層網(wǎng)絡(luò)一般都會有一個比較好的輸入特征,
但是隨著模型的層數(shù)加深,網(wǎng)絡(luò)的非線性變換使得每一層的結(jié)果變得相關(guān)了,且不再滿足分布。
甚至,這些隱藏層的特征分布或許已經(jīng)發(fā)生了偏移。

在經(jīng)過激活層之前,就是x = wu+b,也就是激活函數(shù)的輸入,隨著網(wǎng)絡(luò)加深,x的會逐漸向兩端靠攏(紅色箭頭的地方),那么會造成什么后果,我們可以看下這兩個函數(shù)的導(dǎo)數(shù):
Sigmoid' = sigmoid*(1-sigmoid)
Tanh' = 1-tanh^2


不難看出,在函數(shù)的兩側(cè)梯度變化趨向于0,且變化很慢,這會導(dǎo)致在Back propagation的時候梯度消失,也就是說收斂越來越慢。 在這里感覺也可以理解為:



至于為什么要這么做最后的公式(scale,offset),論文里有提到


簡單地對圖層的每個輸入進(jìn)行規(guī)范化可能會更改圖層可以表示的內(nèi)容。
例如,正則化一個 sigmoid 的輸入將限制他們非線性的線性狀態(tài)
作者也沒具體說明,在我也是不太明白其原理,只能理解為上面的變換會改變原來學(xué)習(xí)到的特征分布,因此加入了可學(xué)習(xí)的γ和β,為什么是可學(xué)習(xí)的,感覺應(yīng)該是讓網(wǎng)絡(luò)自己找到一個在正態(tài)變換后不破壞原特征分布的平衡狀態(tài)。

好吧,很玄,希望有小伙伴能給我解答一下。

測試

? ? ? ?到這里,大致的原理就這樣了,至于后面如何反向傳播,以及推理過程中 均值mean 和 方差var 的設(shè)置,就不再寫下去,網(wǎng)上也很多解讀的資源。
理解了理論之后,結(jié)合實(shí)戰(zhàn)操作才有意思,先附一張圖.


在cnn中,batch_normalization就是取同一個channel上所有批次做處理,粗略畫了這個示意圖
代表batch = 3,channel = 2 , W和H = 2

下面用了numpy,pytorch以及tensorflow的函數(shù)計(jì)算batch_normalization
先看一下pytorch的函數(shù)以及描述

nn.batchnorm2d / tf.layers.batch_normalization


最需要注意的是pytorch和tensorflow 的公式不太一樣,所以結(jié)果會有稍微差異,在測試的時候,我們只需要把其他因素統(tǒng)一就好:


# 導(dǎo)庫
import numpy as np
import tensorflow as tf
import torch.nn as nn
# 創(chuàng)建一個隨機(jī)矩陣
test1 = np.random.rand(4,3,2,2)
test1 = test1.astype(np.float32)
test =test1

先算numpy均值,方差與pytorch(注意設(shè)置momentum = 1,affine=False)

a1 = 0
v = 0
d = []
std = []
for i in range(3):
    a1 = test1[:,i,:,:]
    d.append(a1.sum())
d = np.array(d)
mean = d/16
m = nn.BatchNorm2d(3,affine=False,momentum=1)
input = torch.from_numpy(test1)
output = m(input)
 
# 均值mean
print('torch 尺寸:',input.shape)
print('pytorch 均值:',m.running_mean.data[0],m.running_mean.data[1],m.running_mean.data[2])
print('手算 均值:',mean)
#numpy 計(jì)算: np.mean(np.mean(np.mean(test1,axis=0),axis=1),axis=1)
 
# 方差var = (x-mean) / n
for i in range(3):
    v = test1[:,i,:,:]-mean[i]
    v = ((v**2).sum()/16)**0.5
    std.append(v)
std = np.array(std)
# numpy計(jì)算 :np.std(test1[1,2,3])
print('標(biāo)準(zhǔn)差:',std)
 
#bathnorm
for i in range(3):
    test[:,i,:,:] = (test1[:,i,:,:]-mean[i])/(std[i]+1e-5)
 
print(test[1])
print(output[1])

輸出:

torch 尺寸: torch.Size([4, 3, 2, 2])
pytorch 均值: tensor(0.4218) tensor(0.5399) tensor(0.3418)
手算 均值: [0.42182693 0.5398935  0.34179664]
標(biāo)準(zhǔn)差: [0.26444062 0.2462885  0.22490181]
numpy結(jié)果: 
 [[[-1.0535254  -0.4905532 ]
  [-1.3194345  -0.22114275]]

 [[ 0.5717635   1.1570975 ]
  [-1.1665905  -1.5158345 ]]

 [[ 1.6828372   0.8369611 ]
  [ 0.32095668 -0.89949685]]]
pytorch結(jié)果:
 tensor([[[-1.0535, -0.4905],
         [-1.3194, -0.2211]],

        [[ 0.5717,  1.1570],
         [-1.1665, -1.5158]],

        [[ 1.6827,  0.8369],
         [ 0.3209, -0.8994]]])

可以看出bn的計(jì)算是一樣的,接下來看一下tf版本的,在這里我加上了tf.nn.batch_normalitization
需要將test維度轉(zhuǎn)成(N, H, W, C)

x = test1
b = test
x = np.transpose(x,(0,2,3,1))
b = np.transpose(b,(0,2,3,1))
axis = list(range(len(x)-1))
x = tf.convert_to_tensor(x)
wb_mean, wb_var = tf.nn.moments(x,axis)
scale = tf.Variable(tf.ones([3]))
offset = tf.Variable(tf.zeros([3]))
variance_epsilon = 1e-5
Wx_plus_b = tf.nn.batch_normalization(x, wb_mean, wb_var, offset, scale, variance_epsilon)

Wx_plus_b1 = (x - wb_mean) / tf.sqrt(wb_var + variance_epsilon)
Wx_plus_b1 = Wx_plus_b1 * scale + offset

Wx_plus_b2 = tf.layers.batch_normalization(x,momentum=1,scale=False,epsilon= 1e-5)

? 最后對比各種計(jì)算結(jié)果

with tf.Session() as sess:
    tf.global_variables_initializer().run()
    print('nn.bn: \n',sess.run(Wx_plus_b[1]))
    print('手算bn:\n',sess.run(Wx_plus_b1[1]))
    print('layers.bn: \n',sess.run(Wx_plus_b2[1]))
    print('numpy bn: \n',b[1])
    print('output: \n',output[1])

輸出:

nn.bn: 
 [[[-1.0535603   0.57178396  1.6829035 ]
  [-0.49056947  1.1571388   0.83699405]]

 [[-1.319478   -1.1666319   0.32096925]
  [-0.22115016 -1.5158883  -0.8995324 ]]]
手算bn:
 [[[-1.0535601   0.57178396  1.6829034 ]
  [-0.49056944  1.1571388   0.836994  ]]

 [[-1.319478   -1.1666319   0.32096922]
  [-0.22115014 -1.5158883  -0.8995324 ]]]
layers.bn: 
 [[[-1.0535202   0.57176065  1.6828288 ]
  [-0.49055076  1.1570916   0.8369569 ]]

 [[-1.319428   -1.1665846   0.32095507]
  [-0.22114165 -1.5158268  -0.8994923 ]]]
numpy bn: 
 [[[-1.0535254   0.5717635   1.6828372 ]
  [-0.4905532   1.1570975   0.8369611 ]]

 [[-1.3194345  -1.1665905   0.32095668]
  [-0.22114275 -1.5158345  -0.89949685]]]
output: 
 tensor([[[-1.0535, -0.4905],
         [-1.3194, -0.2211]],

        [[ 0.5717,  1.1570],
         [-1.1665, -1.5158]],

        [[ 1.6827,  0.8369],
         [ 0.3209, -0.8994]]])

結(jié)語

對于batchnorm,還是有很多地方不懂或者不理解的。
文章寫得比較亂,也并不嚴(yán)謹(jǐn),有需要改正的也請指出

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 所用工具 筆:hb鉛筆,櫻花針管筆 pn頭,溫莎水彩筆2.3號,高光筆 顏料:歌文12色 紙:獲多福32K
    馬小七0813閱讀 1,080評論 12 38
  • 前段時間在看這本書, 看了一大半了, 很多東西講的還是很有道理的, 這一系列文章作為筆記, 供自己今后翻閱. 1....
    Dev_hell03W閱讀 412評論 0 0
  • (一)通過URL傳遞函數(shù)名稱和參數(shù) 這個方案是歷史最悠久,至少目前也是使用最普遍的方案。這個方案的優(yōu)點(diǎn)是技術(shù)最簡單...
    松哥888閱讀 1,705評論 0 2
  • 《八月情思》 流年此月桂花開,葉綠花黃百里香。 楚楚嬌娥情切切,嫣語若蘭醉牛郎。 今朝月桂花復(fù)至,...
    潛龍隨筆閱讀 194評論 0 0