無需復雜的數學描述,通過簡單代碼理解卷積模塊
比起晦澀復雜的數學或文本描述,也許代碼能幫助我們更好地理解各種卷積模塊。計算機科學家 Paul-Louis Pröve 用 Keras 對瓶頸模塊、Inception 模塊、殘差模塊等進行了介紹和代碼說明,并在***留下了 AmoebaNet Normal Cell 代碼實現的練習題。
我會盡力定期閱讀與機器學習和人工智能相關的論文。這是緊跟***進展的唯一方法。作為一位計算機科學家,當閱讀科研文本或公式的數學概念時,我常常碰壁。我發現直接用平實的代碼來理解要容易得多。所以在這篇文章中,我希望帶你了解一些精選的用 Keras 實現的***架構中的重要卷積模塊。
如果你在 GitHub 上尋找常用架構的實現,你會找到多得讓人吃驚的代碼。在實踐中,包含足夠多的注釋并用額外的參數來提升模型的能力是很好的做法,但這也會干擾我們對架構本質的理解。為了簡化和縮短代碼片段,我將會使用一些別名函數:
- def conv(x, f, k=3, s=1, p='same', d=1, a='relu'):
- return Conv2D(ffilters=f, kkernel_size=k, sstrides=s,
- ppadding=p, ddilation_rate=d, aactivation=a)(x)
- def dense(x, f, a='relu'):
- return Dense(f, aactivation=a)(x)
- def maxpool(x, k=2, s=2, p='same'):
- return MaxPooling2D(pool_size=k, sstrides=s, ppadding=p)(x)
- def avgpool(x, k=2, s=2, p='same'):
- return AveragePooling2D(pool_size=k, sstrides=s, ppadding=p)(x)
- def gavgpool(x):
- return GlobalAveragePooling2D()(x)
- def sepconv(x, f, k=3, s=1, p='same', d=1, a='relu'):
- return SeparableConv2D(ffilters=f, kkernel_size=k, sstrides=s,
- ppadding=p, ddilation_rate=d, aactivation=a)(x)
我發現,去掉這些模板代碼能有好得多的可讀性。當然,只有你理解我的單字母縮寫時才有效。那就開始吧。
瓶頸模塊
一個卷積層的參數數量取決于卷積核(kernel)的大小、輸入過濾器的數量以及輸出過濾器的數量。你的網絡越寬,則 3×3 卷積的成本就會越高。
- 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)
瓶頸模塊背后的思想是使用成本較低的 1×1 卷積以特定速率 r 來降低通道的數量,從而使后續的 3×3 卷積的參數更少。***,我們再使用另一個 1×1 卷積來拓寬網絡。
Inception 模塊
Inception 模塊引入的思想是:并行地使用不同操作然后融合結果。通過這種方式,網絡可以學習不同類型的過濾器。
- def naive_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 的卷積層。這段代碼是 Inception 模塊的最簡單初級的實現。在實踐中,還會將其與上述的瓶頸思想結合起來,代碼也就會稍微更復雜一些。
Inception 模塊
- def inception_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 誕生前情況則很不一樣。
- def residual_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])
殘差模塊的思想是在卷積模塊的輸出上添加初始激活。通過這種方式,網絡可以通過學習過程決定為輸出使用多少新卷積。注意,Inception 模塊是連接輸出,而殘差模塊是添加它們。
ResNeXt 模塊
從名字上也看得出,ResNeXt 與 ResNet 緊密相關。研究者為卷積模塊引入了基數(cardinality)項,以作為類似于寬度(通道數量)和深度(層數)的又一維度。
基數是指出現在模塊中的并行路徑的數量。這聽起來與 Inception 模塊(有 4 個并行的操作)類似。但是,不同于并行地使用不同類型的操作,當基數為 4 時,并行使用的 4 個操作是相同的。
如果它們做的事情一樣,為什么還要并行呢?這是個好問題。這個概念也被稱為分組卷積(grouped convolution),可追溯到最早的 AlexNet 論文。但是,那時候這種方法主要被用于將訓練過程劃分到多個 GPU 上,而 ResNeXt 則將它們用于提升參數效率。
- def resnext_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 個 1×1 卷積層,其中每個卷積層有 4 個輸出通道(256 / (32*2))。之后,我們會使用 32 個帶有 4 個輸出通道的 3×3 卷積層,后面跟著 32 個帶有 256 個輸出通道的 1×1 層。***一步涉及到疊加這 32 個并行路徑,這能在添加初始輸入構建殘差連接之前提供一個輸出。
左圖:ResNet 模塊;右圖:有大致一樣的參數復雜度的 RexNeXt 模塊
這方面有很多知識需要了解。上圖是其工作過程的圖示,也許你可以復制這段代碼,用 Keras 親自動手構建一個小網絡試試看。這么復雜的描述可以總結成如此簡單的 9 行代碼,是不是很神奇?
隨帶一提,如果基數等于通道的數量,那就會得到所謂的深度可分離卷積(depthwise separable convolution)。自從 Xception 架構出現后,這種方法得到了很多人的使用。
Dense 模塊
密集(dense)模塊是殘差模塊的一個極端版本,其中每個卷積層都會獲得該模塊中所有之前的卷積層的輸出。首先,我們將輸入激活添加到一個列表中,之后進入一個在模塊的深度上迭代的循環。每個卷積輸出也都連接到該列表,這樣后續的迭代會得到越來越多的輸入特征圖。這個方案會繼續,直到達到所需的深度。
- def dense_block(x, f=32, d=5):
- l = x
- for i in range(d):
- x = conv(l, f)
- l = concatenate([l, x])
- return l
盡管要得到表現像 DenseNet 一樣優秀的架構需要耗費幾個月的研究時間,但其實際的基本構建模塊就這么簡單。很神奇吧。
Squeeze-and-Excitation 模塊
SENet 曾短暫地在 ImageNet 上達到過***表現。它基于 ResNeXt,并且重在建模網絡的通道方面的信息。在一個常規的卷積層中,每個通道的點積計算內的疊加操作都有同等的權重。
Squeeze-and-Excitation 模塊
SENet 引入了一種非常簡單的模塊,可以添加到任何已有的架構中。它會創建一個小型神經網絡,該網絡能學習如何根據輸入情況為每個過濾器加權??梢钥吹剑旧聿⒉皇蔷矸e模塊,但可以添加到任何卷積模塊上并有望提升其性能。我想將其添加到混合模塊中。
- def se_block(x, f, rate=16):
- m = gavgpool(x)
- m = dense(m, f // rate)
- m = dense(m, f, a='sigmoid')
- return multiply([x, m])
每個通道都被壓縮成單個值,并被饋送給一個兩層神經網絡。根據通道的分布情況,該網絡會學習基于它們的重要性為這些通道加權。***,這些權重會與卷積激活相乘。
SENet 會有少量額外的計算開銷,但有改善任何卷積模型的潛力。在我看來,這種模塊得到的研究關注還不夠多。
NASNet Normal Cell
難點來了。之前介紹的都是一些簡單但有效的設計,現在我們進入設計神經網絡架構的算法世界。NASNet 的設計方式讓人稱奇,但實際的架構卻又相對復雜。但我們知道,它在 ImageNet 上的表現真的非常好。
NASNet 的提出者通過人工方式定義了一個包含不同類型的卷積和池化層的搜索空間,其中包含不同的可能設置。他們還定義了這些層可以并行或順序排布的方式以及添加或連接的方式。定義完成之后,他們基于一個循環神經網絡構建了一個強化學習(RL)算法,其獎勵是提出了在 CIFAR-10 數據集上表現優良的特定設計。
所得到的架構不僅在 CIFAR-10 上表現優良,而且還在 ImageNet 上取得了當前***。NASNet 由 Normal Cell 和 Reduction Cell 構成,它們在彼此之后重復。
- def normal_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 實現 Normal Cell。其中沒什么新東西,但這種特定的層的組合方式和設定效果就是很好。
倒置殘差模塊
現在你已經了解了瓶頸模塊和可分離卷積。讓我們將它們放到一起吧。如果進行一些測試,你會發現:由于可分離卷積已能降低參數數量,所以壓縮它們可能有損性能,而不會提升性能。
研究者想出了一個做法,做瓶頸殘差模塊相反的事。他們增多了使用低成本 1×1 卷積的通道的數量,因為后續的可分離卷積層能夠極大降低參數數量。它會在關閉這些通道之后再添加到初始激活。
- def inv_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 Normal Cell
AmoebaNet 的 Normal Cell
AmoebaNet 是當前在 ImageNet 上表現***的,甚至在廣義的圖像識別任務上可能也***。類似于 NASNet,它是由一個算法使用前述的同樣的搜索空間設計的。唯一的區別是他們沒使用強化學習算法,而是采用了一種常被稱為「進化(Evolution)」的通用算法。該算法工作方式的細節超出了本文范圍。最終,相比于 NASNet,研究者通過進化算法用更少的計算成本找到了一種更好的方案。它在 ImageNet 上達到了 97.87% 的 Top-5 準確度——單個架構所達到的新高度。
看看其代碼,該模塊沒有添加任何你還沒看過的新東西。你可以試試看根據上面的圖片實現這種新的 Normal Cell,從而測試一下自己究竟掌握了沒有。
總結
希望這篇文章能幫助你理解重要的卷積模塊,并幫助你認識到實現它們并沒有想象中那么困難。有關這些架構的細節請參考它們各自所屬的論文。你會認識到,一旦你理解了一篇論文的核心思想,理解其它部分就會容易得多。請注意,在實際的實現中往往還會添加批歸一化,而且激活函數的應用位置也各有不同。
原文鏈接:
https://towardsdatascience.com/history-of-convolutional-blocks-in-simple-code-96a7ddceac0c
【本文是51CTO專欄機構“機器之心”的原創譯文,微信公眾號“機器之心( id: almosthuman2014)”】