無需複雜的數學描述,通過簡單程式碼理解卷積模組

吳攀發表於2018-12-10

比起晦澀複雜的數學或文字描述,也許程式碼能幫助我們更好地理解各種卷積模組。電腦科學家 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(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)
def maxpool(x, k=2, s=2, p='same'):
 return MaxPooling2D(pool_size=k, strides=s, padding=p)(x)
def avgpool(x, k=2, s=2, p='same'):
 return AveragePooling2D(pool_size=k, strides=s, padding=p)(x)
def gavgpool(x):
 return GlobalAveragePooling2D()(x)
def sepconv(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)

我發現,去掉這些模板程式碼能有好得多的可讀性。當然,只有你理解我的單字母縮寫時才有效。那就開始吧。

瓶頸模組

一個卷積層的引數數量取決於卷積核(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 引入了一種非常簡單的模組,可以新增到任何已有的架構中。它會建立一個小型神經網路,該網路能學習如何根據輸入情況為每個過濾器加權。可以看到,它本身並不是卷積模組,但可以新增到任何卷積模組上並有望提升其效能。我想將其新增到混合模組中。

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

相關文章