【TensorFlow-windows】keras接口——BatchNorm和ResNet
前言
之前學習利用Keras簡單地堆疊卷積網絡去構建分類模型的方法,但是對于很深的網絡結構很難保證梯度在各層能夠正常傳播,經常發生梯度消失、梯度爆炸或者其它奇奇怪怪的問題。為了解決這類問題,大佬們想了各種辦法,比如最原始的L1,L2正則化、權重衰減等,但是在深度學習的各種技巧中,批歸一化(Batch Normalization,BN)和殘差網(Residual Network,ResNet)還是比較有名的,看一波。
國際慣例,參考博客:
BN的原始論文
BN的知乎討論
莫凡大神的BN講解
Batch Normalization原理與實戰
何凱明大佬的caffe-ResNet實現
大佬的keras-ResNet實現
基于keras的Resnet
批歸一化BN
在Keras中文文檔中有總結過其作用:
- 加速收斂
- 控制過擬合,可以少用或不用Dropout和正則
- 降低網絡對初始化權重的敏感程度,因而允許使用較大的學習率
- 可以試用飽和非線性函數(sigmoid等)
以下摘抄一下個人認為論文里面比較重要的語句:
動機
-
DNN訓練的時候,每層輸入的數據分布在不斷變化,因為他們之前層的參數在不斷更新,這就很大程度上降低訓練速度,此時就需要較低的學習率,很小心地初始化模型參數,如果使用飽和非線性函數(saturating nonlinearities,比如tanh和sigmoid)會更難訓練,主要是因為兩端梯度的影響;這個現象稱為內部協方差偏移(internal convariate shift)現象。
-
使用小批量訓練模型的優勢在于,相對于單樣本學習,小批量學習的損失梯度是對整個訓練集的估計,它的質量隨著批大小的上升而提高,此外使用小批量學習的計算比計算m次單個樣本來的更加高效,因為小批量訓練可以利用計算機的并行計算。
-
雖然隨機梯度下降簡單有效,但是需要很小心調整超參,尤其是學習率和模型參數初始化,并且每一層的輸入都受到前面所有層的影響,這個導致訓練比較復雜,網絡參數任何很小的變化都可能在傳播多層以后被放大。但是各層輸入的分布又不得不變化,因為各層需要不斷調整去適應新的分布(每次輸入的樣本分布一般不同)。當學習系統的輸入分布發生變化,就發生了協方差偏移現象(covariate shift)。
-
文章提出一個構想:
假如一個網絡結構是這樣:
l=F2(F1(u,Θ1),Θ2)l=F_2(F_1(u,\Theta_1),\Theta_2) l=F2?(F1?(u,Θ1?),Θ2?)
那么梯度就是
Θ2←Θ2?αm∑i=1m?F2(xi,Θ2)Θ2\Theta_2\leftarrow \Theta_2-\frac{\alpha}{m}\sum_{i=1}^m \frac{\partial F_2(x_i,\Theta_2)}{\Theta_2} Θ2?←Θ2??mα?i=1∑m?Θ2??F2?(xi?,Θ2?)?
( α\alphaα 是學習率,mmm是批大小),這個梯度等價于一個具有輸入為xxx的獨立網絡F2F_2F2?,因此輸入分布可以讓訓練變得更加高效,比如訓練集和測試機的分布相同,這同樣適用于子網絡。因此隨著時間的偏移,保證xxx的分布固定是有好處的,所以Θ2\Theta_2Θ2?沒必要重新調整去彌補xxx分布的變化,其實說白了,大家一起歸一化,固定好分布(均值和方差)。 -
通常情況下的飽和問題和梯度消失問題能夠用ReLU、小心的初始化和較小學習率來解決,當然,我們也可以修正非線性輸入的分布在訓練時更加平穩,此時優化器陷入飽和狀態的幾率會降低,學習速度也會上升。
理論
僅僅是簡單地對每層輸入的歸一化會改變該層所表示的東東,比如對sigmoid的輸入數據歸一化,會將其限制在非線性函數的線性區域(因為sigmoid靠近中心部分接近線性激活),解決它就需要保證插入到網絡的變換能夠代表恒等變換,文章使用縮放因子γ(k)\gamma^{(k)}γ(k)和平移因子β(k)\beta^{(k)}β(k)對歸一化的值進行變換:
y(k)=γ(k)x^(k)+β(k)y^{(k)}=\gamma^{(k)}\hat{x}^{(k)}+\beta^{(k)} y(k)=γ(k)x^(k)+β(k)
實際上,如果γ(k)=Var[x(k)]\gamma^{(k)}=\sqrt{Var[x^{(k)}]}γ(k)=Var[x(k)]?和β(k)=E(x(k))\beta^{(k)}=E(x^{(k)})β(k)=E(x(k)),那么就是反歸一化了,數據直接被恢復成未被歸一化的狀態。其實我當時在這里有一個疑問:批歸一化的目的就是讓神經元的激活值在sigmoid梯度較大的地方,那么為啥還要縮放回去,恢復了原始值,那么梯度不又是兩端梯度么?就跟吹氣球一樣,先把氣球吹得很大,感覺要炸了,就去將它縮小一點,但是又添加了個偏移,把氣球吹回去了。 后來想想,這個問題不難解答,它相當于把較大的東東拆成了較小的東東,然后求導的時候,如果直接對較大的東東求導會發生兩端梯度更新緩慢問題,但是如果由多個小東東組合起來,然后對每個小東東求導,梯度就不會出現在兩端更新,具體看下面的推導,就可以發現每一個參數的梯度不會那么小。
【注】突然就感覺這個思想很像ResNet啊,都是為了解決對原始較大值直接求梯度發生兩端梯度較小問題,只不過BN是將大的數據變成了歸一化數據+縮放+平移,這些值都比較小,求梯度也不會發生兩端梯度的情況;而ResNet是將大的數據變成了數據+殘差項,對這個殘差項求梯度很少情況會發生兩端梯度現象。
前向計算(注意是針對批數據的同一個維度,而非是一個數據的所有維度):
input:B=x1,? ,mparam:γ,βoutput:yi=BNγ,β(xi)μB=1m∑i=1mxiσB2=1m∑i=1m(xi?μB)2x^i=xi?μBσB2+?yi=γx^i+β\begin{aligned} input&: B={x_{1,\cdots,m}}\\ param&:\gamma,\beta \\ output&:y_i=BN_{\gamma,\beta}(x_i) \end{aligned} \\ \begin{aligned} \mu_B&=\frac{1}{m}\sum_{i=1}^m x_i\\ \sigma^2_B&=\frac{1}{m}\sum_{i=1}^m(x_i-\mu_B)^2\\ \hat{x}_i&=\frac{x_i-\mu_B}{\sqrt{\sigma ^2_B+\epsilon}}\\ y_i&=\gamma \hat{x}_i+\beta \end{aligned} inputparamoutput?:B=x1,?,m?:γ,β:yi?=BNγ,β?(xi?)?μB?σB2?x^i?yi??=m1?i=1∑m?xi?=m1?i=1∑m?(xi??μB?)2=σB2?+??xi??μB??=γx^i?+β?
反向傳播:梯度
-
模型參數γ\gammaγ和β\betaβ的梯度
?l?γ=∑i=1m?l?yi?x^i?l?β=∑i=1m?l?yi\begin{aligned} \frac{\partial l}{\partial \gamma}&=\sum_{i=1}^m\frac{\partial l}{\partial y_i}\cdot \hat{x}_i\\ \frac{\partial l}{\partial \beta}&=\sum_{i=1}^m\frac{\partial l}{\partial y_i} \end{aligned} ?γ?l??β?l??=i=1∑m??yi??l??x^i?=i=1∑m??yi??l?? -
鏈式求導時,需要計算
?l?xi=?l?x^i?1σB2+?+?l?σB2?2(xi?μB)m+?l?μB?1m\frac{\partial l}{\partial x_i}=\frac{\partial l}{\partial{\hat{x}_i}}\cdot\frac{1}{\sqrt{\sigma^2_B+\epsilon}}+\frac{\partial l}{\partial \sigma^2_B}\cdot \frac{2(x_i-\mu_B)}{m}+\frac{\partial l}{\partial \mu_B}\cdot\frac{1}{m} ?xi??l?=?x^i??l??σB2?+??1?+?σB2??l??m2(xi??μB?)?+?μB??l??m1?
其中
?l?x^i=?l?yi?γ?l?σB2=∑i=1m?l?x^i?(xi?μB)??12(σB2+?)?32?l?μB=(∑i=1m?l?x^i??1σB2+?)+?l?σB2?∑i=1m?2(xi?μB)m\begin{aligned} \frac{\partial l}{\partial \hat{x}_i}&=\frac{\partial l}{\partial y_i}\cdot \gamma \\ \frac{\partial l}{\partial \sigma^2_B}&=\sum_{i=1}^m\frac{\partial l}{\partial \hat{x}_i}\cdot(x_i-\mu_B)\cdot\frac{-1}{2}(\sigma^2_B+\epsilon)^{-\frac{3}{2}}\\ \frac{\partial l}{\partial \mu_B}&=\left(\sum_{i=1}^m\frac{\partial l}{\partial \hat{x}_i}\cdot\frac{-1}{\sigma^2_B+\epsilon}\right)+\frac{\partial l}{\partial \sigma^2_B}\cdot\frac{\sum_{i=1}^m-2(x_i-\mu_B)}{m} \end{aligned} ?x^i??l??σB2??l??μB??l??=?yi??l??γ=i=1∑m??x^i??l??(xi??μB?)?2?1?(σB2?+?)?23?=(i=1∑m??x^i??l??σB2?+??1?)+?σB2??l??m∑i=1m??2(xi??μB?)??
【注】還有一個問題是BN到底是放在激活之前還是激活之后?知乎上的討論戳這里,原論文的第3.2小節指出實驗時采用的是z=g(BN(Wu))z=g(BN(Wu))z=g(BN(Wu))的方式,即先BN再激活。
Keras中的使用
先看看官方文檔描述:
keras.layers.BatchNormalization(axis=-1, momentum=0.99, epsilon=0.001, center=True, scale=True, beta_initializer='zeros', gamma_initializer='ones', moving_mean_initializer='zeros', moving_variance_initializer='ones', beta_regularizer=None, gamma_regularizer=None, beta_constraint=None, gamma_constraint=None)注意官方文檔的一句話Normalize the activations of the previous layer at each batch,看樣子是在激活后再BN。
ResNet
動機
- 作者首先提出一個疑問:讓模型學的更好是否等價于簡單地堆疊更多的層?其中一個阻礙就是“梯度消失/爆炸”,通常解決方法是規范初始化、中間層歸一化BN。
- 當網絡開始收斂的時候,又出現新問題:當網絡層數加深的時候,準確率提高,但是隨后降低地很快,但這并非是過擬合,增加更多的層導致了更高的訓練誤差。
- 作者又做了個假設與實驗:考慮淺層網絡和它的深層結構,這個深層結構是在淺層網絡的頂部添加恒等映射(identity mapping), 這種構造方法理論上應該能讓深層模型產生的訓練誤差不差于淺層網絡的訓練誤差。但是實驗結果發現這種方案不能產生比理論上得到的構造解更好的結果,也就是說沒達到理論預期。
- 文章的解決方法就是:引入深度殘差框架。思想是不期望每塊堆疊的幾層網絡去學習低層映射,轉而顯式地讓這些層去擬合一個殘差。用公式來解釋就是:假設正常情況下低層的映射是H(x)H(x)H(x), 讓非線堆疊層擬合另一個映射F(x)=H(x)?xF(x)=H(x)-xF(x)=H(x)?x,也就是說原始的映射變成了H(x)=F(x)+xH(x)=F(x)+xH(x)=F(x)+x, 但是這樣的做法建立在一個假設上,即學習殘差映射F(x)F(x)F(x)比直接學習原始的映射H(x)H (x)H(x)容易。極端情況下就是,恒等映射為最優情況,讓殘差趨近于0比讓堆疊的非線性層 逼近0更容易。
不信你試試計算這樣兩個東東:- Relu(w×x)=xRelu(w\times x)=xRelu(w×x)=x與Relu(w×x)+x=xRelu(w\times x)+x=xRelu(w×x)+x=x,哪個容易求解,第二個不用思考就知道www為零矩陣,但是第一個還得想一下,它是斜對角元素為1的單位陣。再者,仿照神經網絡隨便對www進行初始化,然后計算www到零矩陣和單位陣的變換過程,哪個簡單?
- Relu(w×x)=x+?Relu(w\times x)=x+\epsilonRelu(w×x)=x+?與Relu(w×x)+x=x+?Relu(w\times x)+x=x+\epsilonRelu(w×x)+x=x+?,第二個方程對x的波動?\epsilon?更加敏感,比如w×1000=1000.1→1000.2w\times 1000=1000.1\to 1000.2w×1000=1000.1→1000.2與$ (w\times 1000)+1000=1000.1\to 1000.2$,當輸出只變動了0.10.10.1的時候,他們的權重www變化量是多少呢?第一個www變化為w=1000.11000→w′=1000.21000w=\frac{1000.1}{1000}\to w'=\frac{1000.2}{1000}w=10001000.1?→w′=10001000.2?,計算一下變權重變化量相對于原始權重的變化w′?ww=110001\frac{w'-w}{w}=\frac{1}{10001}ww′?w?=100011?,但是第二個www變化w=0.11000→w′=0.21000w=\frac{0.1}{1000}\to w'=\frac{0.2}{1000}w=10000.1?→w′=10000.2?, 相對于開始權重的變化量為w′?ww=1\frac{w'-w}{w}=1ww′?w?=1,很明顯學習殘差時權重增長了一倍,但是直接映射時權重才增長了萬分之一。
- 有人可能跟我一樣較真:為啥權重敏感性越高學習越好?很簡單的想法,比如給人撓癢癢,一個人皮厚(對權重不敏感),能拿雞毛(權重1)和刀子(權重2)給他撓,他都沒感覺,下次他想讓你撓的時候,你拿啥都沒問題(即他感覺這兩權重沒啥區別);但是假設這個人皮薄(對權重敏感),拿刀子給他撓癢癢,他說疼,那下次撓的時候一拿刀子,他就會說錯了錯了,這東西不能撓,也就是讓你學習了正確的知識;所以結果對權重越敏感,越能學到正確的知識。還有一個例子就是:比如同一個人戴眼鏡和不戴眼鏡都在人臉識別訓練集中,這兩張圖片的殘差就類似于這幅眼鏡,如果殘差學習到了這副眼鏡,后來就能更準確地區分帶不帶眼鏡的情況。
理論
可以通過添加連接捷徑表示恒等映射,如下圖所示:
假設這個殘差塊的輸入為xxx,輸出為yyy,那么,通常情況下:
y=F(x,Wi)+xy=F(x,{W_i})+x y=F(x,Wi?)+x
F+x?F+x?F+x?就代表連接捷徑,即對應元素相加;F(x,Wi)F(x,W_i)F(x,Wi?)代表輸入經過FFF幾層非線性層的映射結果,圖中顯示的是兩層,類似于
F(x,Wi)=W2×Relu(W1×x)F(x,W_i)=W_2\times Relu(W_1\times x) F(x,Wi?)=W2?×Relu(W1?×x)
【注】從圖和公式來看,殘差塊不包含塊中最后一層的激活,所以為了使殘差塊有非線性,必須至少兩個層。從加法來看,殘差塊的輸入和輸出應該具有相同維度,不然不能相加。
以上說的是通常情況,還有不通常情況就是假設殘差塊的非線性映射部分輸出的維度比輸入維度小,那么對應元素相加就無法實現,論文就給出了想要維度匹配時的操作:
y=F(x,Wi)+Ws×xy=F(x,{W_i})+W_s\times x y=F(x,Wi?)+Ws?×x
沒錯,就是乘了一個矩陣WsW_sWs?,變換xxx的維度就完事,而且因為是線性操作,影響不大。
Keras中的使用
先看看何凱明大佬在caffe中搭建的ResNet是啥樣的:
兩類殘差塊,一類是在左邊捷徑連接的時候接了個分支,第二類是直接恒等映射過來。
但是大致能知道殘差塊大致包含了兩部分,一部分有較多的層塊,一部分有較少的甚至是一個或者零個層塊,除此之外還有一些細節就是,每個組成殘差塊的每個層塊構造是:卷積->BN->縮放->Relu,為了保證維度相加的可能性,盡量使用卷積核大小為(1,1)步長為1,填充為0,或者卷積核大小為(3,3)步長為1,填充為1,文章的右邊三個卷積塊使用的卷積核大小分別是1,3,11,3,11,3,1,卷積核個數不同,計算卷積后特征圖大小公式是:
n?m+2pS+1\frac{n-m+2p}{S}+1 Sn?m+2p?+1
n是圖像某個維度,m是對應的卷積核維度,p是對應的填充維度,S是步長
接下來在keras中搞事情。
#ResNet-第一類:有側路卷積(大小1);主路卷積用1,3,1的卷積塊(conv-BN-Relu) def conv_block(input_x,kn1,kn2,kn3,side_kn):#主路:第一塊x=Conv2D(filters=kn1,kernel_size=(1,1))(input_x)x=BatchNormalization(axis=-1)(x)x=Activation(relu)(x)#主路:第二塊,注意paddingx=Conv2D(filters=kn2,kernel_size=(3,3),padding='same')(x)x=BatchNormalization(axis=-1)(x)x=Activation(relu)(x)#主路:第三塊x=Conv2D(filters=kn3,kernel_size=(1,1))(x)x=BatchNormalization(axis=-1)(x)x=Activation(relu)(x)#側路y=Conv2D(filters=side_kn,kernel_size=(1,1))(input_x)y=BatchNormalization(axis=-1)(y)y=Activation(relu)(y)#捷徑output=keras.layers.add([x,y])#再激活一次output=Activation(relu)(output)return output #ResNet-第二類:沒有側路卷積;主路卷積用1,3,1的卷積塊(conv-BN-Relu) def identity_block(input_x,kn1,kn2,kn3):#主路:第一塊x=Conv2D(filters=kn1,kernel_size=(1,1))(input_x)x=BatchNormalization(axis=-1)(x)x=Activation(relu)(x)#主路:第二塊,注意paddingx=Conv2D(filters=kn2,kernel_size=(3,3),padding='same')(x)x=BatchNormalization(axis=-1)(x)x=Activation(relu)(x)#主路:第三塊x=Conv2D(filters=kn3,kernel_size=(1,1))(x)x=BatchNormalization(axis=-1)(x)x=Activation(relu)(x)#捷徑output=keras.layers.add([x,input_x])#再激活一次output=Activation(relu)(output)return output構建ResNet50
可以這里和這里的代碼,主要是基于何凱明大佬的ResNet50寫的結構:
-
引入相關包
import tensorflow as tf import tensorflow.keras as keras from tensorflow.keras.layers import Conv2D,AveragePooling2D,MaxPooling2D,BatchNormalization,Activation,Flatten,Dense from tensorflow.keras.activations import relu import numpy as np -
基于以上的BN和兩類ResNet塊結構構建ResNet50
#按照何凱明大佬的ResNet50構建模型 def ResNet50():input_data= keras.layers.Input(shape=(28,28,1))x=Conv2D(filters=64,kernel_size=(7,7),data_format='channels_last')(input_data)#由于不知道怎么padding=3,所以暫時不適用padding和stridex=BatchNormalization(axis=-1)(x)x=Activation(relu)(x)x=MaxPooling2D(pool_size=(3,3),strides=(2,2))(x)x=conv_block(x,64,64,256,256)#Res2a:有側路x=identity_block(x,64,64,256)#Res2b:無側路x=identity_block(x,64,64,256)#Res2c:無側路x=conv_block(x,128,128,512,512)#Res3a:有側路x=identity_block(x,128,128,512)#Res3b:無側路x=identity_block(x,128,128,512)#Res3c:無側路x=identity_block(x,128,128,512)#Res3d:無側路x=conv_block(x,256,256,1024,1024)#Res4a:有側路x=identity_block(x,256,256,1024)#Res4b:無側路x=identity_block(x,256,256,1024)#Res4c:無側路x=identity_block(x,256,256,1024)#Res4d:無側路x=identity_block(x,256,256,1024)#Res4d:無側路x=identity_block(x,256,256,1024)#Res4e:無側路x=identity_block(x,256,256,1024)#Res4f:無側路x=conv_block(x,512,512,2048,2048)#Res5a:有側路x=identity_block(x,512,512,2048)#Res5b:無側路x=identity_block(x,512,512,2048)#Res5c:無側路x=AveragePooling2D(pool_size=(7,7),strides=1)(x)x=Flatten()(x)x=Dense(units=10)(x) #mnist的類別數目x=Activation(keras.activations.softmax)(x)model=keras.models.Model(inputs=input_data, outputs=x)return model -
讀手寫數字
mnist_data=keras.datasets.mnist (train_x,train_y),(test_x,test_y)=mnist_data.load_data() train_y=keras.utils.to_categorical(train_y,10) test_y=keras.utils.to_categorical(test_y,10) train_x=train_x/255.0 test_x=test_x/255.0 train_x=train_x[...,np.newaxis] test_x=test_x[...,np.newaxis] -
構建模型并訓練
model=ResNet50() mnist_data=keras.datasets.mnist (train_x,train_y),(test_x,test_y)=mnist_data.load_data() train_y=keras.utils.to_categorical(train_y,10) test_y=keras.utils.to_categorical(test_y,10) train_x=train_x/255.0 test_x=test_x/255.0 train_x=train_x[...,np.newaxis] test_x=test_x[...,np.newaxis] model.compile(optimizer=tf.keras.optimizers.Adam(),loss=keras.losses.categorical_crossentropy,metrics=['accuracy']) model.fit(train_x,train_y,batch_size=20,epochs=20)太久了,我就不訓練了,而且網絡太大,沒事就OutOfMemory:
Epoch 1/209080/60000 [===>..........................] - ETA: 53:37 - loss: 14.3559 - acc: 0.1073預測的話可以參考之前的博客model.predict之類的
后記
在深度神經網絡中常用的兩個解決梯度消失問題的技巧已經學了,后面再繼續找找案例做,其實為最想要的是嘗試如何把算法移植到手機平臺,最大問題是模型調用和平臺移植,目前可采用的方法有:
- unity在做APP上效果還不錯,能各種移植,且TensorFlow支持C#
- 官方的ensorflow Lite也抽時間看看
- OpenCV有dnn模塊可以調用TensorFlow模型,但是目前還沒學會如何將自己的TensorFlow模型封裝好,到OpenCV調用,只不過官方提供的模型可以調用,自己的模型一直打包出問題。
總結
以上是生活随笔為你收集整理的【TensorFlow-windows】keras接口——BatchNorm和ResNet的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: DNF手游官网地址 DNF手游官网开启内
- 下一篇: java信息管理系统总结_java实现科