tensorflow学习笔记——ResNet
自2012年AlexNet提出以來(lái),圖像分類、目標(biāo)檢測(cè)等一系列領(lǐng)域都被卷積神經(jīng)網(wǎng)絡(luò)CNN統(tǒng)治著。接下來(lái)的時(shí)間里,人們不斷設(shè)計(jì)新的深度學(xué)習(xí)網(wǎng)絡(luò)模型來(lái)獲得更好的訓(xùn)練效果。一般而言,許多網(wǎng)絡(luò)結(jié)構(gòu)的改進(jìn)(例如從VGG到ResNet可以給很多不同的計(jì)算機(jī)視覺(jué)領(lǐng)域帶來(lái)進(jìn)一步性能的提高。
ResNet(Residual Neural Network)由微軟研究員的 Kaiming He 等四位華人提出,通過(guò)使用 Residual Uint 成功訓(xùn)練152層深的神經(jīng)網(wǎng)絡(luò),在 ILSVRC 2015比賽中獲得了冠軍,取得了 3.57%的top-5 的錯(cuò)誤率,同時(shí)參數(shù)量卻比 VGGNet低,效果非常突出,因?yàn)樗?ldquo;簡(jiǎn)單與實(shí)用”并存,之后很多方法都建立在ResNet50或者ResNet101的基礎(chǔ)上完成的,檢測(cè),分割,識(shí)別等領(lǐng)域都紛紛使用ResNet,Alpha zero 也使用了ResNet,所以可見(jiàn)ResNet確實(shí)很好用。ResNet的結(jié)構(gòu)可以極快的加速超深神經(jīng)網(wǎng)絡(luò)的訓(xùn)練,模型的準(zhǔn)確率也有非常大的提升。之前我們學(xué)習(xí)了Inception V3,而Inception V4則是將 Inception Module和ResNet相結(jié)合。可以看到ResNet是一個(gè)推廣性非常好的網(wǎng)絡(luò)結(jié)構(gòu),甚至可以直接應(yīng)用到 Inception Net中。
1,Highway Network簡(jiǎn)介
在ResNet之前,瑞士教授 Schmidhuber 提出了 Highway Network,原理與ResNet很相似。這位Schmidhuber 教授同時(shí)也是 LSTM網(wǎng)絡(luò)的發(fā)明者,而且是早在1997年發(fā)明的,可謂是神經(jīng)網(wǎng)絡(luò)領(lǐng)域元老級(jí)的學(xué)者。通常認(rèn)為神經(jīng)網(wǎng)絡(luò)的深度對(duì)其性能非常重要,但是網(wǎng)絡(luò)越深其訓(xùn)練難度越大,Highway Network的目標(biāo)就是解決極深的神經(jīng)網(wǎng)絡(luò)難以訓(xùn)練的問(wèn)題。Highway Network相當(dāng)于修改了每一層的激活函數(shù),此前的激活函數(shù)只是對(duì)輸入做一個(gè)非線性變換 y = H(x, WH) ,Highway Network 則允許保留一定比例的原始輸入 x,即 y =H(x, WH) .T(x, WT) + x . C(x, WC) ,其中 T是變換系數(shù),C為保留系數(shù)。論文中令 C= 1 - T。這樣前面一層的信息,有一定比例可以不經(jīng)過(guò)矩陣乘法和非線性變換,直接傳輸?shù)较乱粚樱路鹨粭l信息高速公路,因而得名 Highway Network。Highway Network主要通過(guò) gating units 學(xué)習(xí)如何控制網(wǎng)絡(luò)中的信息流,即學(xué)習(xí)原理信息應(yīng)保留的比例。這個(gè)可學(xué)習(xí)的 gating機(jī)制,正是借鑒自Schmidhuber 教授早年的 LSTM 訓(xùn)練神經(jīng)網(wǎng)絡(luò)中的gating。幾百乃至上千層深的 Highway Network可以直接使用梯度下降算法訓(xùn)練,并可以配合多種非線性激活函數(shù),學(xué)習(xí)極深的神經(jīng)網(wǎng)絡(luò)現(xiàn)在變得可行了。事實(shí)上,Highway Network 的設(shè)計(jì)在理論上允許其訓(xùn)練任意深的網(wǎng)絡(luò),其優(yōu)化方法基本上與網(wǎng)絡(luò)的深度獨(dú)立,而傳統(tǒng)的神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)則對(duì)深度非常敏感,訓(xùn)練復(fù)雜度隨著深度增加而急劇增加。
2,模型加深存在的問(wèn)題
ResNet 和 HighWay Network非常類似,也就是允許原始輸入信息直接傳輸?shù)胶竺娴膶又小esNet最初的靈感來(lái)自這個(gè)問(wèn)題:在不斷加神經(jīng)網(wǎng)絡(luò)的深度時(shí),會(huì)出現(xiàn)一個(gè) Degradation 的問(wèn)題,即準(zhǔn)確率會(huì)先上升然后達(dá)到飽和,再持續(xù)增加深度則會(huì)導(dǎo)致準(zhǔn)確率下降。這并不是一個(gè)過(guò)擬合的問(wèn)題,因?yàn)椴还庠跍y(cè)試機(jī)上誤差增大,訓(xùn)練集本身誤差也會(huì)增大。假設(shè)有一個(gè)比較淺的網(wǎng)絡(luò)達(dá)到了飽和的準(zhǔn)確率,那么后面再加上幾個(gè) y=x 的全等映射層,起碼誤差不會(huì)增加,即更深的網(wǎng)絡(luò)不應(yīng)該帶來(lái)訓(xùn)練集上誤差上升。而這里提到的使用全等映射直接將前一層輸出傳到后面的思想,就是 ResNet的靈感來(lái)源。假定某段神經(jīng)網(wǎng)絡(luò)的輸入是 x,期望輸出是 H(x),如果我們直接把輸入 x 傳到輸出作為初始結(jié)果,那么此時(shí)我們需要學(xué)習(xí)的目標(biāo)就是 F(x) = H(x) - x。如下圖所示,這就是一個(gè)ResNet的殘差學(xué)習(xí)單元(Residual Unit),ResNet相當(dāng)于將學(xué)習(xí)目標(biāo)改變了,不再是學(xué)習(xí)一個(gè)完整的輸出 H(x),只是輸出和輸入的差別 H(x) - x,即殘差。
如下圖所示,CIFIR10 數(shù)據(jù)的一個(gè)實(shí)驗(yàn),左側(cè)為訓(xùn)練誤差,右側(cè)是測(cè)試誤差,不光在測(cè)試集上誤差比較大,訓(xùn)練集本身的誤差也非常大。
隨著網(wǎng)絡(luò)越深,精準(zhǔn)度的變化如下圖:
通過(guò)實(shí)驗(yàn)可以發(fā)現(xiàn):隨著網(wǎng)絡(luò)層級(jí)的不斷增加,模型精度不斷得到提升,而當(dāng)網(wǎng)絡(luò)層級(jí)增加到一定的數(shù)目以后,訓(xùn)練精度和測(cè)試精度迅速下降,這說(shuō)明當(dāng)網(wǎng)絡(luò)變得很深以后,深度網(wǎng)絡(luò)變得更加難以訓(xùn)練了。
3,為什么深度模型難以訓(xùn)練
為什么隨著網(wǎng)絡(luò)層級(jí)越深,模型效果卻變差了呢?
3.1 鏈?zhǔn)椒▌t與梯度彌散
下圖是一個(gè)簡(jiǎn)單的神經(jīng)網(wǎng)絡(luò)圖,由輸入層,隱含層,輸出層構(gòu)成:
回想一下神經(jīng)網(wǎng)絡(luò)反向傳播的原理,先通過(guò)正向傳播計(jì)算出結(jié)果 output,然后通過(guò)與樣本比較得出誤差值 Etotal:
根據(jù)誤差結(jié)果,利用著名的“鏈?zhǔn)椒▌t”求偏導(dǎo),使結(jié)果誤差反向傳播從而得出權(quán)重w調(diào)整的梯度。下圖是輸出結(jié)果到隱含層的反向傳播過(guò)程(隱含層到輸入層的反向傳播過(guò)程也是類似):
通過(guò)不斷迭代,對(duì)參數(shù)矩陣進(jìn)行不斷調(diào)整后,使得輸出結(jié)果的誤差值更小,使輸出結(jié)果與事實(shí)更加接近。
從上面的過(guò)程來(lái)看,神經(jīng)網(wǎng)絡(luò)在反向傳播過(guò)程中要不斷地傳播梯度,而當(dāng)網(wǎng)絡(luò)層數(shù)加深時(shí),梯度在傳播過(guò)程中會(huì)逐漸消失(假如采用Sigmoid函數(shù),對(duì)于幅度為1的信號(hào),每向后傳遞一層,梯度就衰減為原來(lái)的 0.25,層數(shù)越多,衰減越厲害),導(dǎo)致無(wú)法對(duì)前面網(wǎng)絡(luò)層的權(quán)重進(jìn)行有效的調(diào)整。
3.2 1.01365 = 37.783 與 0.99 365 = 0.0255
4,ResNet的特點(diǎn)
假設(shè):假如有一個(gè)比較淺網(wǎng)絡(luò)(Shallow Net)的準(zhǔn)確率達(dá)到了飽和,那么后面再加上幾個(gè) y = x 的恒等映射(Identity Mappings),按理說(shuō),即使準(zhǔn)確率不能再提速了,起碼誤差不會(huì)增加(也即更深的網(wǎng)絡(luò)不應(yīng)該帶來(lái)訓(xùn)練集上誤差的上升),但是實(shí)驗(yàn)證明準(zhǔn)確率下降了,這說(shuō)明網(wǎng)絡(luò)越深,訓(xùn)練難度越大。而這里提到的使用恒等映射直接將前一層輸出傳到后面的思想,便是著名深度殘差網(wǎng)絡(luò)ResNet的靈感來(lái)源。
ResNet引入了殘差網(wǎng)絡(luò)結(jié)構(gòu)(residual Network),通過(guò)這種殘差網(wǎng)絡(luò)結(jié)構(gòu),可以把網(wǎng)絡(luò)層弄得很深(據(jù)說(shuō)目前可以達(dá)到1000多層),并且最終的分類效果也非常好,殘差網(wǎng)絡(luò)的基本結(jié)構(gòu)如下圖所示,很明顯,該圖示帶有跳躍結(jié)構(gòu)的:
F(x) 是一個(gè)殘差映射 w, r, t 恒等,如果說(shuō)恒等是理想,很容易將權(quán)重值設(shè)定為0,如果理想化映射更接近于恒等映射,便更容易發(fā)現(xiàn)微小波動(dòng)。
殘差網(wǎng)絡(luò)借鑒了高速網(wǎng)絡(luò)(Highway Network)的跨層鏈接思想,但對(duì)其進(jìn)行修改(殘差項(xiàng)原本是帶權(quán)值的,但是ResNet用恒等映射代替之)
假定某段神經(jīng)網(wǎng)絡(luò)的輸入是x,期望輸出是H(x),即H(x)是期望的復(fù)雜潛在映射,如果是要學(xué)習(xí)這樣的模型,則訓(xùn)練難度會(huì)比較大;
回想前面的假設(shè),如果已經(jīng)學(xué)習(xí)到較飽和的準(zhǔn)確率(或者當(dāng)發(fā)現(xiàn)下層的誤差變大時(shí)),那么接下來(lái)的學(xué)習(xí)目標(biāo)就轉(zhuǎn)變?yōu)楹愕扔成涞膶W(xué)習(xí),也就是使輸入x近似于輸出H(x),以保持在后面的層次中不會(huì)造成精度下降。
在上圖的殘差網(wǎng)絡(luò)結(jié)構(gòu)圖中,通過(guò)“shortcut connections(捷徑連接)”的方式,直接把輸入x傳到輸出作為初始結(jié)果,輸出結(jié)果為H(x)=F(x)+x,當(dāng)F(x)=0時(shí),那么H(x)=x,也就是上面所提到的恒等映射。于是,ResNet相當(dāng)于將學(xué)習(xí)目標(biāo)改變了,不再是學(xué)習(xí)一個(gè)完整的輸出,而是目標(biāo)值H(X)和x的差值,也就是所謂的殘差F(x) = H(x)-x,因此,后面的訓(xùn)練目標(biāo)就是要將殘差結(jié)果逼近于0,使到隨著網(wǎng)絡(luò)加深,準(zhǔn)確率不下降。
這種殘差跳躍式的結(jié)構(gòu),打破了傳統(tǒng)的神經(jīng)網(wǎng)絡(luò)n-1層的輸出只能給n層作為輸入的慣例,使某一層的輸出可以直接跨過(guò)幾層作為后面某一層的輸入,其意義在于為疊加多層網(wǎng)絡(luò)而使得整個(gè)學(xué)習(xí)模型的錯(cuò)誤率不降反升的難題提供了新的方向。
至此,神經(jīng)網(wǎng)絡(luò)的層數(shù)可以超越之前的約束,達(dá)到幾十層、上百層甚至千層,為高級(jí)語(yǔ)義特征提取和分類提供了可行性。
下面感受一下34層的深度殘差網(wǎng)絡(luò)的結(jié)構(gòu)圖:
從圖中可以看出,怎么有一些“shortcut connections(捷徑連接)”是實(shí)現(xiàn),有一些是虛線,有什么區(qū)別呢?
因?yàn)榻?jīng)過(guò)“shortcut-connections(捷徑連接)”后,H(x) = F(x) + x,如果 F(x) 和 x 通道相同,則可直接相加,那么通道不同怎么相加呢。上圖的實(shí)線,虛線就是為了區(qū)分這兩種情況的:
實(shí)線的Connection部分,表示通道相同,如上圖的第一個(gè)粉色矩形和第三個(gè)粉色矩形,都是 3*3*64 的特征圖,由于通道相同,所以采用計(jì)算方式為H(x) = F(x) + x;
虛線的 Connection 部分,表示通道不同,如上圖的第一個(gè)綠色矩形和第三個(gè)粉色矩形,分別為 3*3*64 和 3*3*128 的特征圖,通道不同,采用的計(jì)算方式為H(x) = F(x) + Wx,其中 W 為卷積操作,用來(lái)調(diào)整x維度的。
下圖是兩層及三層的ResNet殘差學(xué)習(xí)模塊:
兩種結(jié)構(gòu)分別針對(duì) ResNet34(左圖)和 ResNet50/101/152(右圖),其目的主要就是為了降低參數(shù)的數(shù)目,左圖是兩個(gè) 3*3*256 的卷積,參數(shù)數(shù)目:3*3*256*256*2 = 1179648,右圖是第一個(gè)1*1的卷積把256維通道降到64維,然后在最后通過(guò)1*1卷積恢復(fù),整體上用的參數(shù)數(shù)目為:1*1*256*64 + 3*3*64*64 + 1*1*64*256 = 69632,右圖的參數(shù)數(shù)量比左圖減少 16.94倍,因此,右圖的主要目的就是為了減少參數(shù)量,從而減少計(jì)算量。
對(duì)于常規(guī)的ResNet,可以用于34層或者更少的網(wǎng)絡(luò)中(左圖);對(duì)于更深的網(wǎng)絡(luò)(如101層),則使用右圖,其目的是減少計(jì)算和參數(shù)量。
經(jīng)檢驗(yàn),深度殘差網(wǎng)絡(luò)的確解決了退化問(wèn)題,如下圖所示,上圖為平原網(wǎng)絡(luò)(plain network)網(wǎng)絡(luò)層次越深(34層)比網(wǎng)絡(luò)層次淺的(18層)的誤差率更高;右圖為殘差網(wǎng)絡(luò)ResNet的網(wǎng)絡(luò)層次越深(34層)比網(wǎng)絡(luò)層次淺(18層)的誤差率更低。
5,VGGNet-19 VS ResNet-34(ResNet的創(chuàng)新點(diǎn))
在提出殘差學(xué)習(xí)的思想,傳統(tǒng)的卷積網(wǎng)絡(luò)或者全連接網(wǎng)絡(luò)在信息傳遞的時(shí)候或多或少會(huì)存在信息丟失,損耗等問(wèn)題,同時(shí)還有導(dǎo)致梯度小時(shí)或梯度爆炸,導(dǎo)致很深的網(wǎng)絡(luò)無(wú)法訓(xùn)練。ResNet在一定程度上解決了這個(gè)問(wèn)題,通過(guò)直接將輸入信息繞道傳到輸出,保護(hù)信息的完整性,整個(gè)網(wǎng)絡(luò)只需要學(xué)習(xí)輸入,輸出差別的那一部分,簡(jiǎn)化學(xué)習(xí)目標(biāo)和難度。
下圖所示為 VGGNet-19,以及一個(gè)34層深的普通卷積網(wǎng)絡(luò),和34層深的ResNet網(wǎng)絡(luò)的對(duì)比圖。可以看到普通直連的卷積神經(jīng)網(wǎng)絡(luò)和ResNet的最大區(qū)別在于,ResNet有很多旁路的支線將輸入直接連到后面的層,使得后面的層可以直接學(xué)習(xí)殘差,這種結(jié)構(gòu)也被稱為 shortcut或 skip connections。
傳統(tǒng)的卷積層或全連接層在信息傳遞時(shí),或多或少的會(huì)存在信息丟失,損耗等問(wèn)題。ResNet 在某種程度上解決了這個(gè)問(wèn)題,通過(guò)直接將輸入信息繞道傳到輸出,保護(hù)信息的完整性,整個(gè)網(wǎng)絡(luò)則需要學(xué)習(xí)輸入,輸出差別的那一部分,簡(jiǎn)化學(xué)習(xí)目標(biāo)和難度。
在ResNet的論文中,處理下圖中的兩層殘差學(xué)習(xí)單元,還有三層的殘差學(xué)習(xí)單元。兩層的殘差學(xué)習(xí)單元中包含兩個(gè)相同輸出通道數(shù)(因?yàn)闅埐畹扔谀繕?biāo)輸出減去輸入,即 H(x) - x,因此輸入,輸出維度需保持一致)的 3*3 卷積;而3層的殘差網(wǎng)絡(luò)則使用了 Network In Network 和 Inception Net中的 1*1 卷積,并且是在中間 3*3 的卷積前后都使用了 1*1 卷積,有先降維再升維的操作。另外,如果有輸入,輸出維度不同的情況,我們可以對(duì) x 做一個(gè)線性映射變換維度,再連接到后面的層。
下圖為 VGG-19 ,直連的 34層網(wǎng)絡(luò),和ResNet的34層網(wǎng)絡(luò)的結(jié)構(gòu)對(duì)比:
6,ResNet不同層數(shù)的網(wǎng)絡(luò)配置
下圖是ResNet 不同層數(shù)時(shí)的網(wǎng)絡(luò)配置(這里我們特別提出ResNet50和ResNet101,主要是因?yàn)樗麄兊某鲧R率很高,所以需要做特別的說(shuō)明):
上表中,我們一共提出了五種深度的ResNet,分別是18, 34, 50, 101和152,首先看圖2最左側(cè),我們發(fā)現(xiàn)所有的網(wǎng)絡(luò)都分為五部分,分別是 conv1, conv2_x, conv3_x, conv4_x , conv5_x,之后的其他論文也會(huì)專門用這個(gè)稱呼指代 ResNet 50 或者 101 的每部分。
拿 101-layer 那列,我們先看看 101-layer 是不是真的是 101 層網(wǎng)絡(luò),首先有個(gè) 輸入 7*7*64的卷積,然后經(jīng)過(guò) 3 + 4 + 23+ 3 = 33 個(gè) building block ,每個(gè) block 為3層,所以有 33*3 = 99 層,最后有個(gè) fc 層(用于分類),所有有 1+99+1=101層,確實(shí)有101層網(wǎng)絡(luò);
注意1:101 層網(wǎng)絡(luò)僅僅指卷積或者全連接層,而激活層或者 Pooling 層并沒(méi)有計(jì)算在內(nèi);
注意2:這里我們關(guān)注50-layer 和 101-layer 這兩列,可以發(fā)現(xiàn),他們唯一的不同在于 conv4_x, ResNet50有6個(gè)block,而 ResNet101有 23 個(gè) block,插了17個(gè)block,也就是 17*3=51層。
在使用了ResNet的結(jié)構(gòu)后,可以發(fā)現(xiàn)層數(shù)不斷加深導(dǎo)致的訓(xùn)練集上誤差增大的現(xiàn)象被消除了,ResNet 網(wǎng)絡(luò)的訓(xùn)練誤差會(huì)隨著層數(shù)增大而逐漸減小,并且在測(cè)試機(jī)上的表現(xiàn)也會(huì)變好。在ResNet推出后不久,Google就借鑒了ResNet的精髓,提出了 Inception V4和 Inception-ResNet-V2,并通過(guò)融合這兩個(gè)模型,在 ILSVRC數(shù)據(jù)集上取得了驚人的 3.08%的錯(cuò)誤率。可見(jiàn),ResNet及其思想對(duì)卷積神經(jīng)網(wǎng)絡(luò)研究的貢獻(xiàn)確實(shí)非常顯著,具有很強(qiáng)的推廣性。在ResNet的作者的第二篇相關(guān)論文 Identity Mappings in Deep Rsidual Networks中,ResNet V2被提出。ResNet V2和 ResNet V1 的主要區(qū)別在于,作者通過(guò)研究 ResNet 殘差學(xué)習(xí)單元的傳播公式,發(fā)現(xiàn)前饋和反饋信息可以直接傳輸,因此 skip connection 的非線性激活函數(shù)(如ReLU)替換為 Identity Mappings(y = x)。同時(shí),ResNet V2在每一層中都使用了Batch Normalization。這樣處理之后,新的殘差學(xué)習(xí)單元將比以前更容易訓(xùn)練且泛化性更強(qiáng)。
根據(jù) Schmidhuber 教授的觀點(diǎn),ResNet 類似于一個(gè)沒(méi)有Gates 的LSTM 網(wǎng)絡(luò),即將輸入 x 傳遞到后面層的過(guò)程是一直發(fā)生的,而不是學(xué)習(xí)出來(lái)的。同時(shí),最近也有兩篇論文表示,ResNet 基本等價(jià)于 RNN且ResNet的效果類似于在多層網(wǎng)絡(luò)間的集成方法(ensemble)。ResNet在加深網(wǎng)絡(luò)層數(shù)上做出來(lái)重大貢獻(xiàn),而另一篇論文 The Power of Depth for Feedforward Neural Networks 則從理論上證明了加深網(wǎng)絡(luò)比加寬網(wǎng)絡(luò)更有效,算是給ResNet 提供了聲援,也是給深度學(xué)習(xí)為什么要深才有效提供合理的解釋。
7,TensorFlow 實(shí)現(xiàn)ResNet V2網(wǎng)絡(luò)
在ResNet的作者的第二篇相關(guān)論文《Identity Mappings in Deep Residual Networks》中,提出了ResNet V2。ResNet V2 和 ResNet V1 的主要區(qū)別在于,作者通過(guò)研究 ResNet 殘差學(xué)習(xí)單元的傳播公式,發(fā)現(xiàn)前饋和反饋信號(hào)可以直接傳輸,因此“shortcut connection”(捷徑連接)的非線性激活函數(shù)(如ReLU)替換為 Identity Mappings。同時(shí),ResNet V2 在每一層中都使用了 Batch Normalization。這樣處理后,新的殘差學(xué)習(xí)單元比以前更容易訓(xùn)練且泛化性更強(qiáng)。
下面我們使用TensorFlow實(shí)現(xiàn)一個(gè)ResNet V2 網(wǎng)絡(luò)。我們依然使用方便的 contrib.slim 庫(kù)來(lái)輔助創(chuàng)建 ResNet,其余載入的庫(kù)還有原生的 collections。本文代碼主要來(lái)自于TensorFlow的開(kāi)源實(shí)現(xiàn)。
我們使用 collections.namedtuple 設(shè)計(jì)ResNet 基本Block 模塊組的 named tuple,并用它創(chuàng)建 Block 的類,但只包含數(shù)據(jù)結(jié)構(gòu),不包含具體方法。我們要定義一個(gè)典型的 Block,需要輸入三個(gè)參數(shù),分別是 scope,unit_fn 和 args。以Block('block1', bottleneck, [(256, 64, 1]) x 2 + [(256, 64, 2 )]) 這一行代碼為例,它可以定義一個(gè)典型的Block,其中 block1 就是我們這個(gè)Block 的名稱(或 scope);bottleneck 是ResNet V2中的殘差學(xué)習(xí)單元;而最后一個(gè)參數(shù)[(256, 64, 1]) x 2 + [(256, 64, 2 )] 則是這個(gè)Block 的 args,args 是一個(gè)列表,其中每個(gè)元素都對(duì)應(yīng)一個(gè) bottleneck殘差學(xué)習(xí)單元,前面兩個(gè)元素都是 (256,64,1),最后一個(gè)是(256,64,2)。每一個(gè)元素都是一個(gè)三元 tuple,即 (depth,depth_bottleneck, stride)。比如(256, 64, 3)代表構(gòu)建的 bottleneck 殘差學(xué)習(xí)單元(每個(gè)殘差學(xué)習(xí)單元包含三個(gè)卷積層)中,第三層輸出通道數(shù) depth 為 256,前兩層輸出通道數(shù) depth_bottleneck 為64,且中間那層的步長(zhǎng) stride 為3。這個(gè)殘差學(xué)習(xí)單元結(jié)構(gòu)即為 [(1x1/s1, 64), (3x3/s2, 64), (1x1/s1, 256)]。而在這個(gè)Block中,一共有3個(gè)bottleneck殘差學(xué)習(xí)單元,除了最后一個(gè)的步長(zhǎng)由3變?yōu)?,其余都一致。
#_*_coding:utf-8_*_
import collections
import tensorflow as tf
slim = tf.contrib.slim
class Block(collections.namedtuple('Block', ['scope', 'uint_fn', 'args'])):
'A named tuple describing a ResNet block'
下面定義一個(gè)降采樣 subsample的方法,參數(shù)包括 inputs(輸入),factor(采樣因子)和scope。這個(gè)函數(shù)也非常簡(jiǎn)單,如果factor為1,則不做修改直接返回 inputs;如果不為1,則使用 slim.max_pool2d 最大池化來(lái)實(shí)現(xiàn),通過(guò)1x1的池化尺寸,stride作步長(zhǎng),即可實(shí)現(xiàn)降采樣。
def subsample(inputs, factor, scope=None):
if factor == 1:
return inputs
else:
return slim.max_pool2d(inputs, [1, 1], stride=factor, scope=scope)
再定義一個(gè) conv2d_same函數(shù)創(chuàng)建卷積層。先判斷 stride 是否為1,如果為1,則直接使用 slim.conv2d 并令 padding 模式為SAME。如果 stride 不為1,則顯式地 pad zero,要pad zero 的總數(shù)為 Kernel_size -1 ,pad_beg 為 pad/2,pad_end 為余下的部分。接下來(lái)使用 tf.pad 對(duì)輸入變量進(jìn)行補(bǔ)零操作。最后,因?yàn)橐呀?jīng)進(jìn)行了 zero padding ,所以只需要使用一個(gè) padding 模式為VALID 的 slim.conv2d 創(chuàng)建這個(gè)卷積層。
def conv2d_same(inputs, num_outputs, kernel_size, stride, scope=None):
if stride == 1:
return slim.conv2d(inputs, num_outputs, kernel_size, stride=1,
padding='SAME', scope=scope)
else:
pad_total = kernel_size - 1
pad_beg = pad_total // 2
pad_end = pad_total - pad_beg
inputs = tf.pad(inputs, [[0, 0], [pad_beg, pad_end],
[pad_beg, pad_end], [0, 0]])
return slim.conv2d(inputs, num_outputs, kernel_size, stride=stride,
padding='VALID', scope=scope)
接下來(lái)定義堆疊Blocks的函數(shù),參數(shù)中的 net 即為輸入,blocks是之前定義的Block 的class 的列表,而 outputs_collections 則是用來(lái)收集各個(gè) end_points 的 collections。下面使用兩層循環(huán),逐個(gè)Block,逐個(gè)Residual Uint 地堆疊,先使用兩個(gè) tf.variable_scope 將殘差學(xué)習(xí)單元命名為 block1 / uint_1 的形式。在第二層循環(huán)中,我們拿到每個(gè)Block中每個(gè)Residual Unit的args,并展開(kāi)為 depth,depth_bottleneck 和 stide,其含義在前面定義Blocks類時(shí)已經(jīng)學(xué)習(xí)過(guò)。然后使用 unit_fn 函數(shù)(即殘差學(xué)習(xí)單元的生成函數(shù))順序地創(chuàng)建并連接所有的殘差學(xué)習(xí)單元。最后,我們使用 slim.utils.collect_named_outpouts 函數(shù)將輸出 net 添加到 collection 中 。最后,當(dāng)所有 Block 中的所有Residual Unit 都堆疊完之后,我們?cè)俜祷刈詈蟮?net 作為 stack_blocks_dense 函數(shù)的結(jié)果。
@slim.add_arg_scope
def stack_blocks_dense(net, blocks, outputs_collections=None):
for block in blocks:
with tf.variable_scope(block.scope, 'block', [net]) as sc:
for i, unit in enumerate(block.args):
with tf.variable_scope('unit_%d' % (i+1), values=[net]):
unit_depth, unit_depth_bottleneck, unit_stride = unit
net = block.unit_fn(net,
depth=unit_depth,
unit_depth_bottleneck=unit_depth_bottleneck,
steide=unit_stride)
net = slim.utils.collect_named_outputs(outputs_collections, sc.name, net)
return net
這里創(chuàng)建 ResNet通用的 arg_scope,關(guān)于 arg_scope,我們已經(jīng)知道其功能——用來(lái)定義某些函數(shù)的參數(shù)默認(rèn)值。這里定義訓(xùn)練標(biāo)記 is_training 默認(rèn)為TRUE,權(quán)重衰減速率 weight_decay 默認(rèn)為 0.0001,BN的衰減速率默認(rèn)為 0.997,BN的 epsilon默認(rèn)為 1e-5,BN的 scale默認(rèn)為 TRUE,和Inception V3定義 arg_scope一樣,先設(shè)置好BN的各項(xiàng)參數(shù),然后通過(guò)slim.arg_scope將 slim.conv2d的幾個(gè)默認(rèn)參數(shù)設(shè)置好:權(quán)重正則器設(shè)置為 L2正則,權(quán)重初始化器設(shè)為 slim.variance_scaling_initializer(),激活函數(shù)設(shè)為 ReLU,標(biāo)準(zhǔn)化器設(shè)為 BN。并將最大池化 的padding模式默認(rèn)設(shè)為 SAME(注意,ResNet原論文中使用的 VALID模式,設(shè)為SAME可讓特征對(duì)其更簡(jiǎn)單,大家可以嘗試改為 VALID)。最后將幾層嵌套的 arg_scope 作為結(jié)果返回。
def resnet_arg_scope(is_training=True,
weight_decay=0.0001,
batch_norm_decay=0.997,
batch_norm_epsilon=1e-5,
batch_norm_scale=True):
batch_norm_params = {
'is_training': is_training,
'decay': batch_norm_decay,
'epsilon': batch_norm_epsilon,
'scale': batch_norm_scale,
'updates_collections': tf.GraphKeys.UPDATE_OPS,
}
with slim.arg_scope(
[slim.conv2d],
weights_regularizer=slim.l2_regularizer(weight_decay),
weights_initializer=slim.variance_scaling_initializer(),
activation_fn=tf.nn.relu,
normalizer_fn=slim.batch_norm,
normalizer_params=batch_norm_params
):
with slim.arg_scope([slim.batch_norm], **batch_norm_params):
with slim.arg_scope([slim.max_pool2d], padding='SAME') as arg_sc:
return arg_sc
接下來(lái)定義核心的 bottleneck 殘差學(xué)習(xí)單元,它是ResNet V2 的論文中提到的 Full Preactivation Residual Unit 的一個(gè)變種。它和ResNet V1 中的殘差學(xué)習(xí)單元的主要區(qū)別有兩點(diǎn),一是在每層前都用了Batch Bormalization,而是對(duì)輸入進(jìn)行 practivation,而不是在卷積進(jìn)行激活函數(shù)處理。我們來(lái)看一下bottleneck 函數(shù)的參數(shù),inputs是輸入,depth,depth_bottleneck和stride這三個(gè)參數(shù)前面的 Batch Normalization,并使用 ReLU函數(shù)進(jìn)行預(yù)激活Preactivate。然后定義 shortcut(即直連的 x):如果殘差單元的輸入通道數(shù) depth_in和輸出通道數(shù) depth一致,那么使用 subsample按步長(zhǎng)為 stride 對(duì) Inputs 進(jìn)行空間上的降采樣(確保空間尺寸和殘差一致,因?yàn)闅埐钪虚g那層的卷積步長(zhǎng)為 stride);如果輸入,輸出通道數(shù)不一樣,我們用步長(zhǎng)為 stride 的1*1 卷積改變其通道數(shù),使得與輸出通道數(shù)一致。然后定義 Residual(殘差),residual這里有3層,先是一個(gè)1*1尺寸,步長(zhǎng)為1,輸出通道數(shù)為depth_bottleneck的卷積,然后是一個(gè)3*3尺寸,步長(zhǎng)為 stride,輸出通道數(shù)為 depth_bottleneck的卷積,最后是一個(gè)1*1的卷積,步長(zhǎng)為1,輸出通道數(shù)為depth的卷積,得到最終的 residual,這里注意最后一層沒(méi)有正則項(xiàng)也沒(méi)有激活函數(shù)。然后將residual 和 shortcut 相加,得到最后結(jié)果 output,再使用 slim.utils.collect_named_outpouts 將結(jié)果添加進(jìn) collection并返回 output 作為函數(shù)結(jié)果。
@slim.add_arg_scope
def bottleneck(inputs, depth, depth_bottleneck, stride,
outputs_collections=None, scope=None):
with tf.variable_scope(scope, 'bottleneck_v2', [inputs]) as sc:
depth_in = slim.utils.last_dimension(inputs.get_shape(), min_rank=4)
preact = slim.batch_norm(inputs, activation_fn=tf.nn.relu, scope='preact')
if depth == depth_in:
shortcut = subsample(inputs, stride, 'shortcut')
else:
shortcut = slim.conv2d(preact, depth, [1, 1], stride=stride,
normalizer_fn=None, activation_fn=None,
scope='shortcut')
residual = slim.conv2d(preact, depth_bottleneck, [1, 1], stride=1,
scope='conv1')
residual = conv2d_same(residual, depth_bottleneck, 3, stride,
scope='conv2')
residual = slim.conv2d(residual, depth, [1, 1], stride=1,
normalozer_fn=None, activation_fn=None,
scope='conv3')
output = shortcut + residual
return slim.utils.collect_named_outputs(outputs_collections, sc.name, output)
下面定義生成ResNet V2 的主函數(shù),我們只需要預(yù)先定義好網(wǎng)絡(luò)的殘差學(xué)習(xí)模塊組blocks,它就可以生成對(duì)應(yīng)的完整的ResNet。先看看這個(gè)函數(shù)的參數(shù),Inputs 即輸入,blocks為定義好的Block類的列表,num_classes是最后輸出的類數(shù)。global_pool 標(biāo)志是否加上最后的一層全局平均池化,include_root_block 標(biāo)志是否加上ResNet網(wǎng)絡(luò)最前面通常使用的7*7卷積和最大池化,reuse標(biāo)志是否重用,scope是整個(gè)網(wǎng)絡(luò)的名稱。在函數(shù)體內(nèi),我們先定義好variable_scope及 end_points_collection,再通過(guò) slim.arg_scope 將(slim.con2d,bottleneck, stack_block_dense)這三個(gè)函數(shù)的參數(shù) outputs_collections默認(rèn)設(shè)為 end_points_collection。然后根據(jù) include_root_block標(biāo)記,創(chuàng)建ResNet最前面的 64輸出通道的步長(zhǎng)為2的7*7卷積,然后再接一個(gè)步長(zhǎng)為2的3*3的最大池化。經(jīng)歷兩個(gè)步長(zhǎng)為2的層,圖片尺寸已經(jīng)被縮小為1/4。然后,使用前面定義好的 stack_blocks_dense 將殘差學(xué)習(xí)模塊組生成好,再根據(jù)標(biāo)記添加全局池化層,這里用 tf.reduce_mean 實(shí)現(xiàn)全局平均池化,效率比直接用 avg_pool高。下面根據(jù)是否有分類數(shù),添加一個(gè)輸出通道數(shù)為 Num_classes的1*1卷積(該卷積層無(wú)激活函數(shù)和正則項(xiàng)),再添加一個(gè) Softmax層輸出網(wǎng)絡(luò)結(jié)果。同時(shí)使用 slim.utils.convert_collection_to_dict 將 collection 轉(zhuǎn)化為Python的 dict,最后返回 net 和 end_points。
def resnet_v2(inputs,
blocks,
num_classes=None,
global_pool=True,
include_root_block=True,
reuse=None,
scope=None):
with tf.variable_scope(scope, 'resnet_v2', [inputs], reuse=reuse) as sc:
end_points_collection = sc.original_name_scope + '_end_points'
with slim.arg_scope([slim.conv2d, bottleneck,
stack_blocks_dense],
outputs_collections=end_points_collection):
net = inputs
if include_root_block:
with slim.arg_scope([slim.conv2d],
activation_fn=None, normalizer_fn=None):
net = conv2d_same(net, 64, 7, stride=2, scope='conv1')
net = slim.max_pool2d(net, [3, 3], stride=2, scope='pool1')
net = stack_blocks_dense(net, blocks)
net = slim.batch_norm(net, activation_fn=tf.nn.relu, scope='postnorm')
if global_pool:
net = tf.reduce_mean(net, [1, 2], name='pool5', keep_dims=True)
if num_classes is not None:
net = slim.conv2d(net, num_classes, [1, 1], activation_fn=None,
normalizer_fn=None, scope='logits')
end_points = slim.utils.convert_collection_to_dict(
end_points_collection
)
if num_classes is not None:
end_points['predictions'] = slim.softmax(net, scope='predictions')
return net, end_points
至此,我們就將 ResNet 的生成函數(shù)定義好了。下面根據(jù)ResNet不同層數(shù)時(shí)的網(wǎng)絡(luò)配置圖中推薦的幾個(gè)不同深度的ResNet網(wǎng)絡(luò)配置,來(lái)設(shè)計(jì)層數(shù)分別為 50, 101, 152 和 200 的ResNet。我們先來(lái)看 50層的ResNet,其嚴(yán)格遵守了圖中的設(shè)置,4個(gè)殘差學(xué)習(xí)Blocks 的 units數(shù)量分別為3, 4, 6和3,總層數(shù)即為 (3+4+6+3)x3+2=50。需要注意的時(shí),殘差學(xué)習(xí)模塊之前的卷積,池化已經(jīng)將尺寸縮小為4倍,我們前3個(gè)Blocks又都包含步長(zhǎng)為2的層,因此總尺寸縮小了 4*8=32倍,輸入圖片尺寸最后變?yōu)?224/32=7 。和 Inception V3很像,ResNet 不斷使用步長(zhǎng)為2的層來(lái)縮減尺寸,但同時(shí)輸出通道數(shù)也在持續(xù)增加,最后達(dá)到了 2048。
def resnet_v2_50(inputs,
num_classes=None,
global_pool=True,
reuse=None,
scope='resnet_v2_50'):
blocks = [
Block('block1', bottleneck, [(256, 64, 1)] * 2 + [(256, 64, 2)]),
Block('block2', bottleneck, [(512, 128, 1)] * 3 + [(512, 128, 2)]),
Block('block3', bottleneck, [(1024, 256, 1)] * 5 + [(1024, 256, 2)]),
Block('block4', bottleneck, [(2048, 512, 1)] * 3)
]
return resnet_v2(inputs, blocks, num_classes, global_pool,
include_root_block=True, reuse=reuse, scope=scope)
101 層的ResNet 和50層相比,主要變化就是把4個(gè)Blocks的units 數(shù)量從3, 4, 6,3提升到了3, 4, 23, 3 。即將第三個(gè)殘差學(xué)習(xí)Block 的units 數(shù)增加到接近4倍。
def resnet_v2_101(inputs,
num_classes=None,
global_pool=True,
reuse=None,
scope='resnet_v2_101'):
blocks = [
Block('block1', bottleneck, [(256, 64, 1)] * 2 + [(256, 64, 2)]),
Block('block2', bottleneck, [(512, 128, 1)] * 3 + [(512, 128, 2)]),
Block('block3', bottleneck, [(1024, 256, 1)] * 22 + [(1024, 256, 2)]),
Block('block4', bottleneck, [(2048, 512, 1)] * 3)
]
return resnet_v2(inputs, blocks, num_classes, global_pool,
include_root_block=True, reuse=reuse, scope=scope)
然后152層的ResNet,則是將第二個(gè)Block 的units數(shù)提高到8,將第三個(gè) Block的 units 數(shù)提高到36。Units數(shù)量提升的主要場(chǎng)所依然是第三個(gè)Block。
def resnet_v2_152(inputs,
num_classes=None,
global_pool=True,
reuse=None,
scope='resnet_v2_152'):
blocks = [
Block('block1', bottleneck, [(256, 64, 1)] * 2 + [(256, 64, 2)]),
Block('block2', bottleneck, [(512, 128, 1)] * 7 + [(512, 128, 2)]),
Block('block3', bottleneck, [(1024, 256, 1)] * 35 + [(1024, 256, 2)]),
Block('block4', bottleneck, [(2048, 512, 1)] * 3)
]
return resnet_v2(inputs, blocks, num_classes, global_pool,
include_root_block=True, reuse=reuse, scope=scope)
最后,200層的Resnet 相比152層的ResNet ,沒(méi)有繼續(xù)提升第三個(gè)Block的units數(shù),而是將第二個(gè)Block的 units 數(shù)一下子提升到了23。
def resnet_v2_200(inputs,
num_classes=None,
global_pool=True,
reuse=None,
scope='resnet_v2_200'):
blocks = [
Block('block1', bottleneck, [(256, 64, 1)] * 2 + [(256, 64, 2)]),
Block('block2', bottleneck, [(512, 128, 1)] * 23 + [(512, 128, 2)]),
Block('block3', bottleneck, [(1024, 256, 1)] * 35 + [(1024, 256, 2)]),
Block('block4', bottleneck, [(2048, 512, 1)] * 3)
]
return resnet_v2(inputs, blocks, num_classes, global_pool,
include_root_block=True, reuse=reuse, scope=scope)
最后我們使用一直以來(lái)的測(cè)評(píng)函數(shù) timne_tensorflow_run,來(lái)測(cè)試 152層深的 ResNet(即獲得 ILSVRC 2015 冠軍的版本)的forward 性能。圖片尺寸回歸到AlexNet ,VGGNet的 224*224,batch_size 為32。我們將 is_training 這個(gè) FLAG置為FALSE。然后使用 resnet_v2_152 創(chuàng)建網(wǎng)絡(luò),再由 time_tensorflow_run 函數(shù)測(cè)評(píng)其 forward 性能。這里不再對(duì)訓(xùn)練時(shí)的性能進(jìn)行測(cè)試了,大家可以自行測(cè)試求解ResNet全部參數(shù)的梯度所需要的時(shí)間。
def time_tensorflow_run(session, target, info_string):
num_steps_burn_in = 10
total_duration = 0.0
total_duration_squared = 0.0
for i in range(num_batches + num_steps_burn_in):
start_time = time.time()
_ = session.run(target)
duration = time.time() - start_time
if i >= num_steps_burn_in:
if not i % 10:
print('%s: step %d, duration=%.3f'%(datetime.now(),
i - num_steps_burn_in, duration))
total_duration += duration
total_duration_squared += duration * duration
mn = total_duration / num_batches
vr = total_duration_squared / num_batches - mn * mn
sd = math.sqrt(vr)
print('%s: %s across %d steps, %.3f +/- %.3f sec / batch'% (datetime.now(),
info_string, num_batches, mn, sd))
if __name__ == '__main__':
batch_size = 32
height, width = 224, 224
inputs = tf.random_uniform((batch_size, height, width, 3))
with slim.arg_scope(resnet_arg_scope(is_training=False)):
net, endpoints = resnet_v2_152(inputs, 1000)
init = tf.global_variables_initializer()
sess = tf.Session()
sess.run(init)
num_batches = 100
time_tensorflow_run(sess, net, 'Forward')
這里可以看到,雖然這個(gè)ResNet有152層深,但其forward計(jì)算耗時(shí)并沒(méi)有特別夸張,相比 VGGNet 和 Inception_v3,大概只增加了 50%,每batch為 0.122 秒。這說(shuō)明 ResNet也是一個(gè)實(shí)用的卷積神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu),不僅支持超深網(wǎng)絡(luò)的訓(xùn)練,同時(shí)在實(shí)際工業(yè)應(yīng)用時(shí)也有不差的forward 性能。
2019-09-17 13:40:28.111221: step 0, duration=0.124 2019-09-17 13:40:29.336873: step 10, duration=0.122 2019-09-17 13:40:30.555401: step 20, duration=0.122 2019-09-17 13:40:31.774261: step 30, duration=0.122 2019-09-17 13:40:32.993206: step 40, duration=0.122 2019-09-17 13:40:34.210301: step 50, duration=0.122 2019-09-17 13:40:35.426938: step 60, duration=0.122 2019-09-17 13:40:36.644774: step 70, duration=0.122 2019-09-17 13:40:37.861877: step 80, duration=0.122 2019-09-17 13:40:39.078488: step 90, duration=0.122 2019-09-17 13:40:40.173907: Forward across 100 steps, 0.012 +/- 0.037 sec / batch
本文我們完整的學(xué)習(xí)了ResNet的基本原理及Tensorflow實(shí)現(xiàn),也設(shè)計(jì)了一系列不同深度的 ResNet。如果大家感興趣可以自行探索不同深度,乃至不同殘差單元結(jié)構(gòu)的ResNet的分類性能。例如,ResNet 原論文中主要增加的時(shí)第二個(gè)和第三個(gè)Block的 units數(shù),大家可以嘗試增加其余兩個(gè)Block的 units數(shù),或者修改bottleneck單元中的 depth,depth_bottleneck等參數(shù),可對(duì)其參數(shù)設(shè)置的意義加深理解。ResNet 可以算是深度學(xué)習(xí)中的一個(gè)里程碑式的圖片,真正意義上支持極深神經(jīng)網(wǎng)絡(luò)的訓(xùn)練。其網(wǎng)絡(luò)結(jié)構(gòu)值得反復(fù)思索,如Google等已將其融合到自家的 Inception Net中,并取得了非常好的效果。相信ResNet的成功也會(huì)啟發(fā)其他在深度學(xué)習(xí)領(lǐng)域研究的靈感。
完整代碼如下:
import collections
import tensorflow as tf
slim = tf.contrib.slim
class Block(collections.namedtuple('Block', ['scope', 'unit_fn', 'args'])):
"""A named tuple describing a ResNet block.
Its parts are:
scope: The scope of the `Block`.
unit_fn: The ResNet unit function which takes as input a `Tensor` and
returns another `Tensor` with the output of the ResNet unit.
args: A list of length equal to the number of units in the `Block`. The list
contains one (depth, depth_bottleneck, stride) tuple for each unit in the
block to serve as argument to unit_fn.
"""
def subsample(inputs, factor, scope=None):
"""Subsamples the input along the spatial dimensions.
Args:
inputs: A `Tensor` of size [batch, height_in, width_in, channels].
factor: The subsampling factor.
scope: Optional variable_scope.
Returns:
output: A `Tensor` of size [batch, height_out, width_out, channels] with the
input, either intact (if factor == 1) or subsampled (if factor > 1).
"""
if factor == 1:
return inputs
else:
return slim.max_pool2d(inputs, [1, 1], stride=factor, scope=scope)
def conv2d_same(inputs, num_outputs, kernel_size, stride, scope=None):
"""Strided 2-D convolution with 'SAME' padding.
When stride > 1, then we do explicit zero-padding, followed by conv2d with
'VALID' padding.
Note that
net = conv2d_same(inputs, num_outputs, 3, stride=stride)
is equivalent to
net = slim.conv2d(inputs, num_outputs, 3, stride=1, padding='SAME')
net = subsample(net, factor=stride)
whereas
net = slim.conv2d(inputs, num_outputs, 3, stride=stride, padding='SAME')
is different when the input's height or width is even, which is why we add the
current function. For more details, see ResnetUtilsTest.testConv2DSameEven().
Args:
inputs: A 4-D tensor of size [batch, height_in, width_in, channels].
num_outputs: An integer, the number of output filters.
kernel_size: An int with the kernel_size of the filters.
stride: An integer, the output stride.
rate: An integer, rate for atrous convolution.
scope: Scope.
Returns:
output: A 4-D tensor of size [batch, height_out, width_out, channels] with
the convolution output.
"""
if stride == 1:
return slim.conv2d(inputs, num_outputs, kernel_size, stride=1,
padding='SAME', scope=scope)
else:
# kernel_size_effective = kernel_size + (kernel_size - 1) * (rate - 1)
pad_total = kernel_size - 1
pad_beg = pad_total // 2
pad_end = pad_total - pad_beg
inputs = tf.pad(inputs,
[[0, 0], [pad_beg, pad_end], [pad_beg, pad_end], [0, 0]])
return slim.conv2d(inputs, num_outputs, kernel_size, stride=stride,
padding='VALID', scope=scope)
@slim.add_arg_scope
def stack_blocks_dense(net, blocks,
outputs_collections=None):
"""Stacks ResNet `Blocks` and controls output feature density.
First, this function creates scopes for the ResNet in the form of
'block_name/unit_1', 'block_name/unit_2', etc.
Args:
net: A `Tensor` of size [batch, height, width, channels].
blocks: A list of length equal to the number of ResNet `Blocks`. Each
element is a ResNet `Block` object describing the units in the `Block`.
outputs_collections: Collection to add the ResNet block outputs.
Returns:
net: Output tensor
"""
for block in blocks:
with tf.variable_scope(block.scope, 'block', [net]) as sc:
for i, unit in enumerate(block.args):
with tf.variable_scope('unit_%d' % (i + 1), values=[net]):
unit_depth, unit_depth_bottleneck, unit_stride = unit
net = block.unit_fn(net,
depth=unit_depth,
depth_bottleneck=unit_depth_bottleneck,
stride=unit_stride)
net = slim.utils.collect_named_outputs(outputs_collections, sc.name, net)
return net
def resnet_arg_scope(is_training=True,
weight_decay=0.0001,
batch_norm_decay=0.997,
batch_norm_epsilon=1e-5,
batch_norm_scale=True):
"""Defines the default ResNet arg scope.
TODO(gpapan): The batch-normalization related default values above are
appropriate for use in conjunction with the reference ResNet models
released at https://github.com/KaimingHe/deep-residual-networks. When
training ResNets from scratch, they might need to be tuned.
Args:
is_training: Whether or not we are training the parameters in the batch
normalization layers of the model.
weight_decay: The weight decay to use for regularizing the model.
batch_norm_decay: The moving average decay when estimating layer activation
statistics in batch normalization.
batch_norm_epsilon: Small constant to prevent division by zero when
normalizing activations by their variance in batch normalization.
batch_norm_scale: If True, uses an explicit `gamma` multiplier to scale the
activations in the batch normalization layer.
Returns:
An `arg_scope` to use for the resnet models.
"""
batch_norm_params = {
'is_training': is_training,
'decay': batch_norm_decay,
'epsilon': batch_norm_epsilon,
'scale': batch_norm_scale,
'updates_collections': tf.GraphKeys.UPDATE_OPS,
}
with slim.arg_scope(
[slim.conv2d],
weights_regularizer=slim.l2_regularizer(weight_decay),
weights_initializer=slim.variance_scaling_initializer(),
activation_fn=tf.nn.relu,
normalizer_fn=slim.batch_norm,
normalizer_params=batch_norm_params):
with slim.arg_scope([slim.batch_norm], **batch_norm_params):
# The following implies padding='SAME' for pool1, which makes feature
# alignment easier for dense prediction tasks. This is also used in
# https://github.com/facebook/fb.resnet.torch. However the accompanying
# code of 'Deep Residual Learning for Image Recognition' uses
# padding='VALID' for pool1. You can switch to that choice by setting
# slim.arg_scope([slim.max_pool2d], padding='VALID').
with slim.arg_scope([slim.max_pool2d], padding='SAME') as arg_sc:
return arg_sc
@slim.add_arg_scope
def bottleneck(inputs, depth, depth_bottleneck, stride,
outputs_collections=None, scope=None):
"""Bottleneck residual unit variant with BN before convolutions.
This is the full preactivation residual unit variant proposed in [2]. See
Fig. 1(b) of [2] for its definition. Note that we use here the bottleneck
variant which has an extra bottleneck layer.
When putting together two consecutive ResNet blocks that use this unit, one
should use stride = 2 in the last unit of the first block.
Args:
inputs: A tensor of size [batch, height, width, channels].
depth: The depth of the ResNet unit output.
depth_bottleneck: The depth of the bottleneck layers.
stride: The ResNet unit's stride. Determines the amount of downsampling of
the units output compared to its input.
rate: An integer, rate for atrous convolution.
outputs_collections: Collection to add the ResNet unit output.
scope: Optional variable_scope.
Returns:
The ResNet unit's output.
"""
with tf.variable_scope(scope, 'bottleneck_v2', [inputs]) as sc:
depth_in = slim.utils.last_dimension(inputs.get_shape(), min_rank=4)
preact = slim.batch_norm(inputs, activation_fn=tf.nn.relu, scope='preact')
if depth == depth_in:
shortcut = subsample(inputs, stride, 'shortcut')
else:
shortcut = slim.conv2d(preact, depth, [1, 1], stride=stride,
normalizer_fn=None, activation_fn=None,
scope='shortcut')
residual = slim.conv2d(preact, depth_bottleneck, [1, 1], stride=1,
scope='conv1')
residual = conv2d_same(residual, depth_bottleneck, 3, stride,
scope='conv2')
residual = slim.conv2d(residual, depth, [1, 1], stride=1,
normalizer_fn=None, activation_fn=None,
scope='conv3')
output = shortcut + residual
return slim.utils.collect_named_outputs(outputs_collections,
sc.name,
output)
def resnet_v2(inputs,
blocks,
num_classes=None,
global_pool=True,
include_root_block=True,
reuse=None,
scope=None):
"""Generator for v2 (preactivation) ResNet models.
This function generates a family of ResNet v2 models. See the resnet_v2_*()
methods for specific model instantiations, obtained by selecting different
block instantiations that produce ResNets of various depths.
Args:
inputs: A tensor of size [batch, height_in, width_in, channels].
blocks: A list of length equal to the number of ResNet blocks. Each element
is a resnet_utils.Block object describing the units in the block.
num_classes: Number of predicted classes for classification tasks. If None
we return the features before the logit layer.
include_root_block: If True, include the initial convolution followed by
max-pooling, if False excludes it. If excluded, `inputs` should be the
results of an activation-less convolution.
reuse: whether or not the network and its variables should be reused. To be
able to reuse 'scope' must be given.
scope: Optional variable_scope.
Returns:
net: A rank-4 tensor of size [batch, height_out, width_out, channels_out].
If global_pool is False, then height_out and width_out are reduced by a
factor of output_stride compared to the respective height_in and width_in,
else both height_out and width_out equal one. If num_classes is None, then
net is the output of the last ResNet block, potentially after global
average pooling. If num_classes is not None, net contains the pre-softmax
activations.
end_points: A dictionary from components of the network to the corresponding
activation.
Raises:
ValueError: If the target output_stride is not valid.
"""
with tf.variable_scope(scope, 'resnet_v2', [inputs], reuse=reuse) as sc:
end_points_collection = sc.original_name_scope + '_end_points'
with slim.arg_scope([slim.conv2d, bottleneck,
stack_blocks_dense],
outputs_collections=end_points_collection):
net = inputs
if include_root_block:
# We do not include batch normalization or activation functions in conv1
# because the first ResNet unit will perform these. Cf. Appendix of [2].
with slim.arg_scope([slim.conv2d],
activation_fn=None, normalizer_fn=None):
net = conv2d_same(net, 64, 7, stride=2, scope='conv1')
net = slim.max_pool2d(net, [3, 3], stride=2, scope='pool1')
net = stack_blocks_dense(net, blocks)
# This is needed because the pre-activation variant does not have batch
# normalization or activation functions in the residual unit output. See
# Appendix of [2].
net = slim.batch_norm(net, activation_fn=tf.nn.relu, scope='postnorm')
if global_pool:
# Global average pooling.
net = tf.reduce_mean(net, [1, 2], name='pool5', keep_dims=True)
if num_classes is not None:
net = slim.conv2d(net, num_classes, [1, 1], activation_fn=None,
normalizer_fn=None, scope='logits')
# Convert end_points_collection into a dictionary of end_points.
end_points = slim.utils.convert_collection_to_dict(end_points_collection)
if num_classes is not None:
end_points['predictions'] = slim.softmax(net, scope='predictions')
return net, end_points
def resnet_v2_50(inputs,
num_classes=None,
global_pool=True,
reuse=None,
scope='resnet_v2_50'):
"""ResNet-50 model of [1]. See resnet_v2() for arg and return description."""
blocks = [
Block('block1', bottleneck, [(256, 64, 1)] * 2 + [(256, 64, 2)]),
Block(
'block2', bottleneck, [(512, 128, 1)] * 3 + [(512, 128, 2)]),
Block(
'block3', bottleneck, [(1024, 256, 1)] * 5 + [(1024, 256, 2)]),
Block(
'block4', bottleneck, [(2048, 512, 1)] * 3)]
return resnet_v2(inputs, blocks, num_classes, global_pool,
include_root_block=True, reuse=reuse, scope=scope)
def resnet_v2_101(inputs,
num_classes=None,
global_pool=True,
reuse=None,
scope='resnet_v2_101'):
"""ResNet-101 model of [1]. See resnet_v2() for arg and return description."""
blocks = [
Block(
'block1', bottleneck, [(256, 64, 1)] * 2 + [(256, 64, 2)]),
Block(
'block2', bottleneck, [(512, 128, 1)] * 3 + [(512, 128, 2)]),
Block(
'block3', bottleneck, [(1024, 256, 1)] * 22 + [(1024, 256, 2)]),
Block(
'block4', bottleneck, [(2048, 512, 1)] * 3)]
return resnet_v2(inputs, blocks, num_classes, global_pool,
include_root_block=True, reuse=reuse, scope=scope)
def resnet_v2_152(inputs,
num_classes=None,
global_pool=True,
reuse=None,
scope='resnet_v2_152'):
"""ResNet-152 model of [1]. See resnet_v2() for arg and return description."""
blocks = [
Block(
'block1', bottleneck, [(256, 64, 1)] * 2 + [(256, 64, 2)]),
Block(
'block2', bottleneck, [(512, 128, 1)] * 7 + [(512, 128, 2)]),
Block(
'block3', bottleneck, [(1024, 256, 1)] * 35 + [(1024, 256, 2)]),
Block(
'block4', bottleneck, [(2048, 512, 1)] * 3)]
return resnet_v2(inputs, blocks, num_classes, global_pool,
include_root_block=True, reuse=reuse, scope=scope)
def resnet_v2_200(inputs,
num_classes=None,
global_pool=True,
reuse=None,
scope='resnet_v2_200'):
"""ResNet-200 model of [2]. See resnet_v2() for arg and return description."""
blocks = [
Block(
'block1', bottleneck, [(256, 64, 1)] * 2 + [(256, 64, 2)]),
Block(
'block2', bottleneck, [(512, 128, 1)] * 23 + [(512, 128, 2)]),
Block(
'block3', bottleneck, [(1024, 256, 1)] * 35 + [(1024, 256, 2)]),
Block(
'block4', bottleneck, [(2048, 512, 1)] * 3)]
return resnet_v2(inputs, blocks, num_classes, global_pool,
include_root_block=True, reuse=reuse, scope=scope)
from datetime import datetime
import math
import time
def time_tensorflow_run(session, target, info_string):
num_steps_burn_in = 10
total_duration = 0.0
total_duration_squared = 0.0
for i in range(num_batches + num_steps_burn_in):
start_time = time.time()
_ = session.run(target)
duration = time.time() - start_time
if i >= num_steps_burn_in:
if not i % 10:
print('%s: step %d, duration = %.3f' %
(datetime.now(), i - num_steps_burn_in, duration))
total_duration += duration
total_duration_squared += duration * duration
mn = total_duration / num_batches
vr = total_duration_squared / num_batches - mn * mn
sd = math.sqrt(vr)
print('%s: %s across %d steps, %.3f +/- %.3f sec / batch' %
(datetime.now(), info_string, num_batches, mn, sd))
if __name__ == '__main__':
batch_size = 32
height, width = 224, 224
inputs = tf.random_uniform((batch_size, height, width, 3))
with slim.arg_scope(resnet_arg_scope(is_training=False)):
net, end_points = resnet_v2_152(inputs, 1000)
init = tf.global_variables_initializer()
sess = tf.Session()
sess.run(init)
num_batches = 100
time_tensorflow_run(sess, net, "Forward")
本文是學(xué)習(xí)ResNet網(wǎng)絡(luò)的筆記,參考了《tensorflow實(shí)戰(zhàn)》這本書中關(guān)于ResNet的章節(jié),寫的非常好,所以在此做了筆記,侵刪。
而且本文在學(xué)習(xí)中,摘抄了下面博客的ResNet筆記,也寫的通俗易通:
https://my.oschina.net/u/876354/blog/1634322
https://www.zybuluo.com/rianusr/note/1419006
https://my.oschina.net/u/876354/blog/1622896
參考文獻(xiàn):https://blog.csdn.net/u013181595/article/details/80990930
https://blog.csdn.net/lanran2/article/details/79057994
ResNet的論文文獻(xiàn): https://arxiv.org/abs/1512.03385
強(qiáng)烈建議學(xué)習(xí)何凱文關(guān)于深度殘差網(wǎng)絡(luò)的兩篇經(jīng)典論文,深度殘差網(wǎng)絡(luò)的主要思想,便是來(lái)自下面兩篇論文:
《Deep Residual Learning for Image Recognition》(基于深度殘差學(xué)習(xí)的圖像識(shí)別)
《Identity Mappings in Deep Residual Networks》(深度殘差網(wǎng)絡(luò)中的特征映射)
在學(xué)習(xí)后,確實(shí)對(duì)ResNet 理解了不少,在此很感謝。
總結(jié)
以上是生活随笔為你收集整理的tensorflow学习笔记——ResNet的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 【JVM】Java 中的经典垃圾回收器
- 下一篇: 不触发事件,vue子组件传值给父组件