代碼實(shí)踐|通過(guò)簡(jiǎn)單代碼來(lái)回顧卷積塊的歷史
點(diǎn)擊上方“小白學(xué)視覺(jué)”,選擇加"星標(biāo)"或“置頂”
重磅干貨,第一時(shí)間送達(dá)
我試著定期閱讀ML和AI的論文,這是保持不掉隊(duì)的唯一的方法。作為一個(gè)計(jì)算機(jī)科學(xué)家,我常常會(huì)在看科學(xué)性的文字描述或者是數(shù)據(jù)公式的時(shí)候遇到麻煩。我發(fā)現(xiàn)通過(guò)代碼來(lái)理解會(huì)好很多。所以,在這篇文章中,我會(huì)通過(guò)Keras實(shí)現(xiàn)的方式,帶領(lǐng)大家回顧一下最近的一些文章中的重要的卷積塊。
當(dāng)你在GitHub上尋找熱門的結(jié)構(gòu)的實(shí)現(xiàn)的時(shí)候,你可能會(huì)驚訝需要多少代碼。在代碼中包含足夠的注釋以及使用額外的參數(shù)是一個(gè)很好的實(shí)踐,但是同時(shí),也會(huì)使代碼不能聚焦于核心的結(jié)構(gòu)的實(shí)現(xiàn)。為了簡(jiǎn)化代碼,我使用了一些函數(shù)的別名:
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)
我發(fā)現(xiàn)不使用模板代碼,代碼的可讀性增加了不少。當(dāng)然,需要你理解我的單個(gè)單詞的表述才可以。我們開(kāi)始。
卷積層的參數(shù)的數(shù)量取決于kernel的尺寸,輸入的filter的數(shù)量和輸出filter的數(shù)量。你的網(wǎng)絡(luò)越寬,3x3卷積的代價(jià)越大。
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)
bottleneck塊背后的思想是使用計(jì)算量很小的1x1的卷積將通道的數(shù)量減少r倍,接下來(lái)的3x3的卷積的參數(shù)會(huì)大大減小,最后,我們?cè)偈褂昧硪粋€(gè)1x1的卷積將通道數(shù)變回原來(lái)的樣子。
Inception模塊的思想是并行使用不同類型的操作,然后將結(jié)果合并。這樣,網(wǎng)絡(luò)可以學(xué)習(xí)到不同類型的filter。
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的結(jié)果進(jìn)行了合并,后面接一個(gè)MaxPooling層。上面這一小段顯示了一個(gè)inception的樸素的實(shí)現(xiàn)。實(shí)際的實(shí)現(xiàn)和bottlenecks 的思想結(jié)合起來(lái),會(huì)稍微更復(fù)雜一點(diǎn)。

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是微軟的研究人員發(fā)明的一種結(jié)構(gòu),可以讓網(wǎng)絡(luò)變得很深,要多深都可以,同時(shí)仍然可以提高模型的準(zhǔn)確率?,F(xiàn)在你也許已經(jīng)對(duì)很深的網(wǎng)絡(luò)司空見(jiàn)慣了,但是在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])
它的思想是在輸出的卷積塊上加上一個(gè)初始的激活。那樣的話,網(wǎng)絡(luò)可以決定在學(xué)習(xí)的過(guò)程中,輸出使用多少新的卷積。注意,inception模塊在拼接輸出的時(shí)候也拼接了加到上面的殘差塊。
看名字就知道,ResNeXt 和ResNet和接近。作者給卷積塊引入了一個(gè)新的名詞基數(shù),就像是另外的一個(gè)維度,就像寬度(通道數(shù))和深度(層數(shù))一樣。
基數(shù)指的是卷積塊中并行出現(xiàn)的路徑的數(shù)量。聽(tīng)起來(lái)很像inception塊中4個(gè)不同的并行的操作。但是,這里使用的是完全相同的操作,4個(gè)基數(shù)指的是使用4次相同的操作。
但是既然做的是同樣的事情,為什么要并行起來(lái)做呢?問(wèn)得好,這個(gè)概念要追溯到最早的AlexNet中的分組卷積,原先AlexNet是為了將運(yùn)算分開(kāi)利用不同的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])
思想就是對(duì)于所有的輸入通道,將它們分成組。卷積只在組中進(jìn)行,不會(huì)跨組。可以發(fā)現(xiàn),每個(gè)組會(huì)學(xué)到不同的特征,提高了權(quán)值的效率。
想象一下,一個(gè)bottleneck塊首先使用壓縮率為4,將256通道降維到64通道,最后輸出的時(shí)候,再?gòu)?4通道回到256通道。如果我們引入了基數(shù)為32,壓縮率為2,我們并行使用32個(gè)1x1卷積層,每個(gè)組得到4 (256 / (32*2))個(gè)輸出通道。最后一步將32個(gè)并行路徑的結(jié)果加起來(lái),得到一個(gè)輸出,然后再加上初始的輸入,得到殘差連接。

Left: ResNet Block?—?Right: RexNeXt Block of roughly the same parameter complexity
這需要好好消化一下。使用上面的圖看看能不能得到一個(gè)可視化的表示,了解一下發(fā)生了什么,或者拷貝上面的幾行代碼,自己用Keras建一個(gè)小網(wǎng)絡(luò)試試。這么復(fù)雜的描述,只用了9行簡(jiǎn)單的代碼就實(shí)現(xiàn)了,是不是很酷?
順便說(shuō)一下,如果基數(shù)的數(shù)量和通道的數(shù)量相同的話,我們會(huì)得到一個(gè)叫做深度可分離卷積的東西。這個(gè)東西自從Xception 結(jié)構(gòu)之后,就開(kāi)始火了起來(lái)。

dense塊是殘差塊的一種極端的版本,每一個(gè)卷積層都會(huì)得到該模塊中之前的所有卷積層的輸出。第一,我們將輸入的激活加到一個(gè)列表中,然后進(jìn)入一個(gè)循環(huán),遍歷這模塊的深度。每個(gè)卷積的輸出都會(huì)加到這個(gè)列表中,所以下面的循環(huán)會(huì)得到越來(lái)越多的輸入特征圖,直到到達(dá)預(yù)定的深度。
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
研究了幾個(gè)月得到了一個(gè)和DensNet一樣好的結(jié)構(gòu),實(shí)際的構(gòu)建模塊就是這么簡(jiǎn)單,太帥了。
SENet短期內(nèi)曾是ImageNet中最先進(jìn)的。它基于ResNext構(gòu)建,聚焦于對(duì)通道之間的信息進(jìn)行建模。在常規(guī)的卷積中,每個(gè)通道在內(nèi)積操作中對(duì)于加法操作具有相同的權(quán)重。

SENet引入了一個(gè)非常簡(jiǎn)單的模塊,可以在任意的網(wǎng)絡(luò)結(jié)構(gòu)中加入。它構(gòu)建了一個(gè)小的神經(jīng)網(wǎng)絡(luò),可以學(xué)習(xí)到對(duì)于輸入來(lái)說(shuō),每個(gè)filter的權(quán)重是多少。可以看到,這不是一個(gè)卷積模塊,但是可以加入到任意的卷積塊中,而且有可能提高性能。
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])
每個(gè)通道被壓縮成一個(gè)數(shù)值,然后輸入到一個(gè)兩層的的神經(jīng)網(wǎng)絡(luò)中。依賴于通道的分布,這個(gè)網(wǎng)絡(luò)可以學(xué)到基于他們的重要性的權(quán)重。最后,這些權(quán)重和卷積的激活相乘。
SENets引入了一個(gè)很小的計(jì)算量,同時(shí)提升了卷積模型的性能。在我看來(lái),這個(gè)模塊并沒(méi)有得到它應(yīng)有的關(guān)注。
到了這里,開(kāi)始有點(diǎn)難看了。我們要離開(kāi)那個(gè)簡(jiǎn)單有效的設(shè)計(jì)空間了,進(jìn)入一個(gè)設(shè)計(jì)神經(jīng)網(wǎng)絡(luò)的算法的世界。NASNet從如何設(shè)計(jì)的看上去不可思議,但是實(shí)際結(jié)構(gòu)相當(dāng)?shù)膹?fù)雜。反正我就是知道在ImageNet上,表現(xiàn)非常好。

作者手動(dòng)定義了一個(gè)搜索空間,使用不同可能的設(shè)置搜索不同類型的卷積核池化層,還定義了這些層是如何設(shè)計(jì)成并行的,如何相加的,如何拼接的。一旦定義好了,就開(kāi)始進(jìn)行強(qiáng)化學(xué)習(xí),基于循環(huán)神經(jīng)網(wǎng)絡(luò),如果設(shè)計(jì)出的網(wǎng)絡(luò)在CIFAR-10上表現(xiàn)的很好的話,就得到獎(jiǎng)勵(lì)。
最后得到的結(jié)構(gòu)不僅僅是在CIFAR-10上表現(xiàn)的好,在ImageNet上也取得了業(yè)界領(lǐng)先。NASNet由基礎(chǔ)的Normal Cell和Reduction Cell相互重復(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來(lái)實(shí)現(xiàn)Normal Cell。除了這些層的組合之外,并沒(méi)有什么新的東西,效果非常好。
Inverted Residual 塊
到目前為止,你聽(tīng)說(shuō)過(guò)了 bottleneck block 和 可分離卷積,現(xiàn)在讓我們把這兩個(gè)東西放到一起,如果你跑一些測(cè)試,你會(huì)注意到可分離卷積已經(jīng)減少了參數(shù)的數(shù)量,再用 bottleneck block壓縮的話,可能會(huì)傷害到性能。

作者實(shí)際上做了件和bottleneck residual block相反的事情,使用1x1的卷積核來(lái)增加通道的數(shù)量,因?yàn)榻酉聛?lái)的可分離卷積已經(jīng)很大程度上減小了參數(shù)的數(shù)量,然后在和初始激活相加之前把通道數(shù)降下來(lái)。
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]
最后一個(gè)困惑是,可分離卷積后面并沒(méi)有接一個(gè)激活函數(shù),而是直接和輸入相加。這個(gè)block加到結(jié)構(gòu)里之后,非常的有效。

使用AmoebaNet ,我們達(dá)到了當(dāng)前在ImageNet上的業(yè)界最佳,也可能是圖像識(shí)別領(lǐng)域的業(yè)界最佳。和NASNet相似,這是由一個(gè)算法設(shè)計(jì)的,使用了相同的搜索空間。只是將強(qiáng)化學(xué)習(xí)算法換成了常常用來(lái)進(jìn)化的遺傳算法。這篇文章中,我們不進(jìn)行詳細(xì)的介紹。結(jié)果就是,通過(guò)進(jìn)化,作者可以找到一個(gè)比NASNet更好的方法,同時(shí)計(jì)算量也更小。在ImageNet上Top-5的準(zhǔn)確率達(dá)到了 97.87%,這是單個(gè)模型第一次有這樣的結(jié)果。
看看代碼,這個(gè)block中并沒(méi)有加入什么你沒(méi)見(jiàn)過(guò)的新東西,為什么不基于上面的圖,自己試試實(shí)現(xiàn)一下新的Normal Cell,看看自己是不是能跟得上?
我希望這個(gè)文章可以給你一個(gè)關(guān)于重要的卷積block的扎實(shí)的理解,實(shí)現(xiàn)這些block也許你想的要容易的多。去看看對(duì)應(yīng)的論文,可以得到一個(gè)更加詳細(xì)的理解。你會(huì)注意到,一旦你抓住了論文的核心思想,其余的理解起來(lái)就容易了。另外還要注意的是,在實(shí)際的實(shí)現(xiàn)中,常常會(huì)加入Batch Normalization,使用的激活函數(shù)也會(huì)有差別。
交流群
歡迎加入公眾號(hào)讀者群一起和同行交流,目前有SLAM、三維視覺(jué)、傳感器、自動(dòng)駕駛、計(jì)算攝影、檢測(cè)、分割、識(shí)別、醫(yī)學(xué)影像、GAN、算法競(jìng)賽等微信群(以后會(huì)逐漸細(xì)分),請(qǐng)掃描下面微信號(hào)加群,備注:”昵稱+學(xué)校/公司+研究方向“,例如:”張三 + 上海交大 + 視覺(jué)SLAM“。請(qǐng)按照格式備注,否則不予通過(guò)。添加成功后會(huì)根據(jù)研究方向邀請(qǐng)進(jìn)入相關(guān)微信群。請(qǐng)勿在群內(nèi)發(fā)送廣告,否則會(huì)請(qǐng)出群,謝謝理解~

