摘要: 在本文中,我想帶領大家看一看最近在Keras中實現的體系結構中一系列重要的卷積組塊。
作爲一名計算機科學家,我經常在翻閱科學技術資料或者公式的數學符號時碰壁。我發現通過簡單的代碼來理解要容易得多。因此,在本文中,我想帶領大家看一看最近在Keras中實現的體系結構中一系列重要的卷積組塊。
當你在GitHub網站上尋找常用架構實現時,一定會對它們裏面的代碼量感到驚訝。如果標有足夠多的註釋並使用額外的參數來改善模型,將會是一個非常好的做法,但與此同時這也會分散體系結構的本質。爲了進一步簡化和縮短代碼,我將使用一些別名函數:
defconv(x, f, k=3, s=1, p='same', d=1, a='relu'):
return Conv2D(filters=f, kernel_size=k, strides=s,
padding=p, dilation_rate=d, activation=a)(x)
def dense(x, f, a='relu'):
return Dense(f, activation=a)(x)
defmaxpool(x, k=2, s=2, p='same'):
return MaxPooling2D(pool_size=k, strides=s, padding=p)(x)
defavgpool(x, k=2, s=2, p='same'):
return AveragePooling2D(pool_size=k, strides=s, padding=p)(x)
defgavgpool(x):
return GlobalAveragePooling2D()(x)
defsepconv(x, f, k=3, s=1, p='same', d=1, a='relu'):
return SeparableConv2D(filters=f, kernel_size=k, strides=s,
padding=p, dilation_rate=d, activation=a)(x)
在刪除模板代碼之後的代碼更易讀。當然,這只有在你理解我的首字母縮寫後纔有效。
defconv(x, f, k=3, s=1, p='same', d=1, a='relu'):
return Conv2D(filters=f, kernel_size=k, strides=s,
padding=p, dilation_rate=d, activation=a)(x)
def dense(x, f, a='relu'):
return Dense(f, activation=a)(x)
defmaxpool(x, k=2, s=2, p='same'):
return MaxPooling2D(pool_size=k, strides=s, padding=p)(x)
defavgpool(x, k=2, s=2, p='same'):
return AveragePooling2D(pool_size=k, strides=s, padding=p)(x)
defgavgpool(x):
return GlobalAveragePooling2D()(x)
defsepconv(x, f, k=3, s=1, p='same', d=1, a='relu'):
return SeparableConv2D(filters=f, kernel_size=k, strides=s,
padding=p, dilation_rate=d, activation=a)(x)
瓶頸(Bottleneck)組塊
一個卷積層的參數數量取決於卷積核的大小、輸入過濾器的數量和輸出過濾器的數量。你的網絡越寬,3x3卷積耗費的成本就越大。
def bottleneck(x, f=32, r=4):
x = conv(x, f//r, k=1)
x = conv(x, f//r, k=3)
return conv(x, f, k=1)
瓶頸組塊背後的思想是,使用一個低成本的1x1卷積,按照一定比率r將通道的數量降低,以便隨後的3x3卷積具有更少的參數。最後,我們用另外一個1x1的卷積來拓寬網絡。
Inception模塊
模塊提出了通過並行的方式使用不同的操作並且合併結果的思想。通過這種方式網絡可以學習不同類型的過濾器。
defnaive_inception_module(x, f=32):
a = conv(x, f, k=1)
b = conv(x, f, k=3)
c = conv(x, f, k=5)
d = maxpool(x, k=3, s=1)
return concatenate([a, b, c, d])
在這裏,我們將使用卷積核大小分別爲1、3和5的卷積層與一個MaxPooling層進行合併。這段代碼顯示了Inception模塊的原始實現。實際的實現結合了上述的瓶頸組塊思想,這使它稍微的複雜了一些。
definception_module(x, f=32, r=4):
a = conv(x, f, k=1)
b = conv(x, f//3, k=1)
b = conv(b, f, k=3)
c = conv(x, f//r, k=1)
c = conv(c, f, k=5)
d = maxpool(x, k=3, s=1)
d = conv(d, f, k=1)
return concatenate([a, b, c, d])
剩餘組塊(ResNet)
ResNet是由微軟的研究人員提出的一種體系結構,它允許神經網絡具有任意多的層數,同時還提高了模型的準確度。現在你可能已經習慣使用它了,但在ResNet之前,情況並非如此。
defresidual_block(x, f=32, r=4):
m = conv(x, f//r, k=1)
m = conv(m, f//r, k=3)
m = conv(m, f, k=1)
return add([x, m])
ResNet的思路是將初始的激活添加到卷積組塊的輸出結果中。利用這種方式,網絡可以通過學習過程決定用於輸出的新卷積的數量。值得注意的是,Inception模塊連接這些輸出,而剩餘組塊是用於求和。
ResNeXt組塊
根據它的名稱,你可以猜到ResNeXt與ResNet是密切相關的。作者們將術語“基數(cardinality)”引入到卷積組塊中,作爲另一個維度,如寬度(通道數量)和深度(網絡層數)。
基數是指在組塊中出現的並行路徑的數量。這聽起來類似於以並行的方式出現的4個操作爲特徵的Inception模塊。然而,基數4不是指的是並行使用不同類型的操作,而是簡單地使用相同的操作4次。
它們做的是同樣的事情,那麼爲什麼你還要把它們並行放在一起呢?這個問題問得好。這個概念也被稱爲分組卷積,可以追溯到最早的AlexNet論文。儘管當時它主要用於將訓練過程劃分到多個GPU上,而ResNeXt則使用ResNeXt來提高參數的效率。
defresnext_block(x, f=32, r=2, c=4):
l = []
for i in range(c):
m = conv(x, f//(c*r), k=1)
m = conv(m, f//(c*r), k=3)
m = conv(m, f, k=1)
l.append(m)
m = add(l)
return add([x, m])
這個想法是把所有的輸入通道分成一些組。卷積將只會在其專用的通道組內進行操作,而不會影響其它的。結果發現,每組在提高權重效率的同時,將會學習不同類型的特徵。
想象一個瓶頸組塊,它首先使用一個爲4的壓縮率將256個輸入通道減少到64個,然後將它們再恢復到256個通道作爲輸出。如果想引入爲32的基數和2的壓縮率,那麼我們將使用並行的32個1x1的卷積層,並且每個卷積層的輸出通道是4(256/(32*2))個。隨後,我們將使用32個具有4個輸出通道的3x3的卷積層,然後是32個1x1的卷積層,每個層則有256個輸出通道。最後一步包括添加這32條並行路徑,在爲了創建剩餘連接而添加初始輸入之前,這些路徑會爲我們提供一個輸出。
這有不少的東西需要消化。用上圖可以非常直觀地瞭解都發生了什麼,並且可以通過複製這些代碼在Keras中自己創建一個小型網絡。利用上面9行簡單的代碼可以概括出這些複雜的描述,這難道不是很好嗎?
順便提一下,如果基數與通道的數量相同,我們就會得到一個叫做深度可分卷積(depthwise separable convolution)的東西。自從引入了Xception體系結構以來,這些技術得到了廣泛的應用。
密集(Dense)組塊
密集組塊是剩餘組塊的極端版本,其中每個卷積層獲得組塊中之前所有卷積層的輸出。我們將輸入激活添加到一個列表中,然後輸入一個可以遍歷塊深度的循環。每個卷積輸出還會連接到這個列表,以便後續迭代獲得越來越多的輸入特徵映射。這個方案直到達到了所需要的深度纔會停止。
defdense_block(x, f=32, d=5):
l = x
for i in range(d):
x = conv(l, f)
l = concatenate([l, x])
return l
儘管需要數月的研究才能得到一個像DenseNet這樣出色的體系結構,但是實際的構建組塊其實就這麼簡單。
SENet(Squeeze-and-Excitation)組塊
SENet曾經在短期內代表着ImageNet的較高水平。它是建立在ResNext的基礎之上的,主要針對網絡通道信息的建模。在常規的卷積層中,每個通道對於點積計算中的加法操作具有相同的權重。
SENet引入了一個非常簡單的模塊,可以添加到任何現有的體系結構中。它創建了一個微型神經網絡,學習如何根據輸入對每個過濾器進行加權。正如你看到的那樣,SENet本身不是一個卷積組塊,但是因爲它可以被添加到任何卷積組塊中,並且可能會提高它的性能,因此我想將它添加到混合體中。
defse_block(x, f, rate=16):
m = gavgpool(x)
m = dense(m, f // rate)
m = dense(m, f, a='sigmoid')
return multiply([x, m])
每個通道被壓縮爲一個單值,並被饋送到一個兩層的神經網絡裏。根據通道的分佈情況,這個網絡將根據通道的重要性來學習對其進行加權。最後,再用這個權重跟卷積激活相乘。
SENets只用了很小的計算開銷,同時還可能會改進卷積模型。在我看來,這個組塊並沒有得到應有的重視。
NASNet標準單元
這就是事情變得醜陋的地方。我們正在遠離人們提出的簡捷而有效的設計決策的空間,並進入了一個設計神經網絡體系結構的算法世界。NASNet在設計理念上是令人難以置信的,但實際的體系結構是比較複雜的。我們所瞭解的是,它在ImageNet上表現的很優秀。
通過人工操作,作者們定義了一個不同類型的卷積層和池化層的搜索空間,每個層都具有不同的可能性設置。他們還定義瞭如何以並行的方式、順序地排列這些層,以及這些層是如何被添加的或連接的。一旦定義完成,他們會建立一個基於遞歸神經網絡的強化學習(Reinforcement Learning,RL)算法,如果一個特定的設計方案在CIFAR-10數據集上表現良好,就會得到相應的獎勵。
最終的體系結構不僅在CIFAR-10上表現良好,而且在ImageNet上也獲得了相當不錯的結果。NASNet是由一個標準單元(Normal Cell)和一個依次重複的還原單元(Reduction Cell)組成。
defnormal_cell(x1, x2, f=32):
a1 = sepconv(x1, f, k=3)
a2 = sepconv(x1, f, k=5)
a = add([a1, a2])
b1 = avgpool(x1, k=3, s=1)
b2 = avgpool(x1, k=3, s=1)
b = add([b1, b2])
c2 = avgpool(x2, k=3, s=1)
c = add([x1, c2])
d1 = sepconv(x2, f, k=5)
d2 = sepconv(x1, f, k=3)
d = add([d1, d2])
e2 = sepconv(x2, f, k=3)
e = add([x2, e2])
return concatenate([a, b, c, d, e])
這就是如何在Keras中實現一個標準單元的方法。除了這些層和設置結合的非常有效之外,就沒有什麼新的東西了。
倒置剩餘(Inverted Residual)組塊
到現在爲止,你已經瞭解了瓶頸組塊和可分離卷積。現在就把它們放在一起。如果你做一些測試,就會注意到,因爲可分離卷積已經減少了參數的數量,因此進行壓縮可能會損害性能,而不是提高性能。
作者們提出了與瓶頸組塊和剩餘組塊相反的想法。他們使用低成本的1x1卷積來增加通道的數量,因爲隨後的可分離卷積層已經大大減少了參數的數量。在把通道添加到初始激活之前,降低了通道的數量。
definv_residual_block(x, f=32, r=4):
m = conv(x, f*r, k=1)
m = sepconv(m, f, a='linear')
return add([m, x])
問題的最後一部分是在可分離卷積之後沒有激活函數。相反,它直接被添加到了輸入中。這個組塊被證明當被放到一個體繫結構中的時候是非常有效的。
AmoebaNet標準單元
利用AmoebaNet,我們在ImageNet上達到了當前的最高水平,並且有可能在一般的圖像識別中也是如此。與NASNet類似,AmoebaNet是通過使用與前面相同的搜索空間的算法設計的。唯一的糾結是,他們放棄了強化學習算法,而是採用了通常被稱爲“進化”的遺傳算法。但是,深入瞭解其工作方式的細節超出了本文的範疇。故事的結局是,通過進化,作者們能夠找到一個比NASNet的計算成本更低的更好的解決方案。這在ImageNet-A上獲得了名列前五的97.87%的準確率,也是第一次針對單個體繫結構的。
結論
我希望本文能讓你對這些比較重要的卷積組塊有一個深刻的理解,並且能夠認識到實現起來可能比想象的要容易。要進一步瞭解這些體系結構,請查看相關的論文。你會發現,一旦掌握了一篇論文的核心思想,就會更容易理解其餘的部分了。另外,在實際的實現過程中通常將批量規範化添加到混合層中,並且隨着激活函數應用的的地方會有所變化。
本文作者:【方向】
本文爲雲棲社區原創內容,未經允許不得轉載。