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