Fast-SCNN的解釋以及使用Tensorflow 2.0的實現(xiàn)
擊上方“智能算法”,選擇加"星標"或“置頂”
重磅干貨,第一時間送達
作者:Kshitiz Rimal
編譯:ronghuaiyang
對圖像分割方法Fast-SCNN的解釋以及實現(xiàn)的代碼分析。

Fast Segmentation Convolutional Neural Network (Fast- scnn)是一種針對高分辨率圖像數(shù)據(jù)的實時語義分割模型,適用于低內(nèi)存嵌入式設(shè)備上的高效計算。原論文的作者是:Rudra PK Poudel, Stephan Liwicki and Roberto Cipolla。本文中使用的代碼并不是作者的正式實現(xiàn),而是我對論文中描述的模型的重構(gòu)的嘗試。
隨著自動駕駛汽車的興起,迫切需要一種能夠?qū)崟r處理輸入的模型。目前已有一些最先進的離線語義分割模型,但這些模型體積大,內(nèi)存大,計算量大,F(xiàn)ast-SCNN可以解決這些問題。
Fast-SCNN的一些關(guān)鍵方面是:
在高分辨率圖像(1024 x 2048px)上的實時分割 得到準確率為68%的平均IOU 在Cityscapes數(shù)據(jù)集上每秒處理123.5幀 不需要大量的預(yù)訓練 結(jié)合高分辨率的空間細節(jié)和低分辨率提取的深度特征
此外,F(xiàn)ast-SCNN使用流行的技術(shù)中最先進的模型來保證上述性能,像用在PSPNet中的金字塔池模塊PPM,使用反向殘余瓶頸層是用于MobileNet V2中用的反向殘差Bottleneck層,以及ContextNet中的特征融合模塊等。同時利用從低分辨率數(shù)據(jù)中提取的深度特征和從高分辨率數(shù)據(jù)中提取的空間細節(jié),確保更好、更快的分割。
現(xiàn)在讓我們開始 Fast-SCNN的探索和實現(xiàn)。Fast-SCNN由4個主要構(gòu)件組成。它們是:
學習下采樣 全局特征提取器 特征融合 分類器

1. 學習下采樣
到目前為止,我們知道深度卷積神經(jīng)網(wǎng)絡(luò)的前幾層提取圖像的邊緣和角點等底層特征。因此,為了充分利用這一特征并使其可用于進一步的層次,需要學習向下采樣。它是一種粗糙的全局特征提取器,可以被網(wǎng)絡(luò)中的其他模塊重用和共享。
學習下采樣模塊使用3層來提取這些全局特征。分別是:Conv2D層,然后是2個深度可分離的卷積層。在實現(xiàn)過程中,在每個Conv2D和深度可分離的Conv層之后,使用一個Batchnorm層和Relu激活,因為通常在這些層之后引入Batchnorm和激活是一種標準實踐。這里,所有3個層都使用2的stride和3x3的內(nèi)核大小。
現(xiàn)在,讓我們首先實現(xiàn)這個模塊。首先,我們安裝Tensorflow 2.0。我們可以簡單地使用谷歌Colab并開始我們的實現(xiàn)。你可以簡單地使用以下命令安裝:
!pip?install?tensorflow-gpu==2.0.0
這里,' -gpu '說明我的谷歌Colab筆記本使用GPU,而在你的情況下,如果你不喜歡使用它,你可以簡單地刪除' -gpu ',然后Tensorflow安裝將利用系統(tǒng)的cpu。
然后導入Tensorflow:
import?tensorflow?as?tf
現(xiàn)在,讓我們首先為我們的模型創(chuàng)建輸入層。在Tensorflow 2.0使用TF.Keras的高級api,我們可以這樣:
input_layer?=?tf.keras.layers.Input(shape=(2048,?1024,?3),?name?=?'input_layer')
這個輸入層是我們要構(gòu)建的模型的入口點。這里我們使用Tf.Keras函數(shù)的api。使用函數(shù)api而不是序列api的原因是,它提供了構(gòu)建這個特定模型所需的靈活性。
接下來,讓我們定義學習下采樣模塊的層。為此,為了使過程簡單和可重用,我創(chuàng)建了一個自定義函數(shù),它將檢查我想要添加的層是一個Conv2D層還是深度可分離層,然后檢查我是否想在層的末尾添加relu。使用這個代碼塊使得卷積的實現(xiàn)在整個實現(xiàn)過程中易于理解和重用。
def?conv_block(inputs,?conv_type,?kernel,?kernel_size,?strides,?padding='same',?relu=True):
??
??if(conv_type?==?'ds'):
????x?=?tf.keras.layers.SeparableConv2D(kernel,?kernel_size,?padding=padding,?strides?=?strides)(inputs)
??else:
????x?=?tf.keras.layers.Conv2D(kernel,?kernel_size,?padding=padding,?strides?=?strides)(inputs)??
??
??x?=?tf.keras.layers.BatchNormalization()(x)
??
??if?(relu):
????x?=?tf.keras.activations.relu(x)
??
??return?x
在TF.Keras中,Convolutional layer定義為tf.keras.layers,深度可分離層為tf.keras.layers.SeparableConv2D。
現(xiàn)在,讓我們通過使用適當?shù)膮?shù)來調(diào)用自定義函數(shù)來為模塊添加層:
lds_layer?=?conv_block(input_layer,?'conv',?32,?(3,?3),?strides?=?(2,?2))
lds_layer?=?conv_block(lds_layer,?'ds',?48,?(3,?3),?strides?=?(2,?2))
lds_layer?=?conv_block(lds_layer,?'ds',?64,?(3,?3),?strides?=?(2,?2))
2. 全局特征提取器
這個模塊的目的是為分割捕獲全局上下文。它直接獲取從學習下采樣模塊的輸出。在這一節(jié)中,我們引入了不同的bottleneck 殘差塊,并引入了一個特殊的模塊,即金字塔池化模塊(PPM)來聚合不同的基于區(qū)域的上下文信息。
讓我們從bottleneck 殘差塊開始。

以上是本文對bottleneck殘差塊的描述。與上面類似,現(xiàn)在讓我們使用tf.keras高級api來實現(xiàn)。
我們首先根據(jù)上表的描述自定義一些函數(shù)。我們從殘差塊開始,它將調(diào)用我們的自定義conv_block函數(shù)來添加Conv2D,然后添加DepthWise Conv2D層,然后point-wise卷積層,如上表所述。然后將point-wise卷積的最終輸出與原始輸入相加,使其成為殘差。
def?_res_bottleneck(inputs,?filters,?kernel,?t,?s,?r=False):
????
????tchannel?=?tf.keras.backend.int_shape(inputs)[-1]?*?t
????x?=?conv_block(inputs,?'conv',?tchannel,?(1,?1),?strides=(1,?1))
????x?=?tf.keras.layers.DepthwiseConv2D(kernel,?strides=(s,?s),?depth_multiplier=1,?padding='same')(x)
????x?=?tf.keras.layers.BatchNormalization()(x)
????x?=?tf.keras.activations.relu(x)
????x?=?conv_block(x,?'conv',?filters,?(1,?1),?strides=(1,?1),?padding='same',?relu=False)
????if?r:
????????x?=?tf.keras.layers.add([x,?inputs])
????return?x
這個bottleneck殘差塊在架構(gòu)中被多次添加,添加的次數(shù)由表中的' n '參數(shù)表示。因此,根據(jù)本文描述的架構(gòu),為了添加n次,我們引入了另一個自定義函數(shù)來完成這個任務(wù)。
def?bottleneck_block(inputs,?filters,?kernel,?t,?strides,?n):
??x?=?_res_bottleneck(inputs,?filters,?kernel,?t,?strides)
??
??for?i?in?range(1,?n):
????x?=?_res_bottleneck(x,?filters,?kernel,?t,?1,?True)
??return?x
gfe_layer?=?bottleneck_block(lds_layer,?64,?(3,?3),?t=6,?strides=2,?n=3)
gfe_layer?=?bottleneck_block(gfe_layer,?96,?(3,?3),?t=6,?strides=2,?n=3)
gfe_layer?=?bottleneck_block(gfe_layer,?128,?(3,?3),?t=6,?strides=1,?n=3)
在這里,你會注意到這些bottleneck塊的第一個輸入來自學習下采樣模塊的輸出。這個全局特征提取器部分的最后一塊是金字塔池化模塊,簡稱PPM。

PPM使用上個卷積層出來的特征圖,然后應(yīng)用多個子區(qū)域平均池化和以及上采樣函數(shù)來得到不同的子區(qū)域的特征表示,然后連接在一起,這樣就帶有了本地和全局上下文的信息,可以讓圖像的分割過程更準確。
使用TF.Keras來實現(xiàn),我們使用了另外一個自定義函數(shù):
def?pyramid_pooling_block(input_tensor,?bin_sizes):
??concat_list?=?[input_tensor]
??w?=?64
??h?=?32
??for?bin_size?in?bin_sizes:
????x?=?tf.keras.layers.AveragePooling2D(pool_size=(w//bin_size,?h//bin_size),?strides=(w//bin_size,?h//bin_size))(input_tensor)
????x?=?tf.keras.layers.Conv2D(128,?3,?2,?padding='same')(x)
????x?=?tf.keras.layers.Lambda(lambda?x:?tf.image.resize(x,?(w,h)))(x)
????concat_list.append(x)
??return?tf.keras.layers.concatenate(concat_list)
我們添加這個PPM模塊,它將從最后一個bottleneck塊獲取輸入。
gfe_layer?=?pyramid_pooling_block(gfe_layer,?[2,4,6,8])
這里的第二個參數(shù)是要提供給PPM模塊的bin的數(shù)量,這里使用的bin的數(shù)量是按照論文中描述的一樣。這些bin用于在不同的子區(qū)域進行AveragePooling ,如上面的自定義函數(shù)所述。
3. 特征融合

在這個模塊中,兩個輸入相加以更好地表示分割。第一個是從學習下采樣模塊中提取的高級特征,這個學習下采樣模塊先進行point-wise卷積,再加入到第二個輸入中。這里在point-wise卷積的最后沒有進行激活。
ff_layer1?=?conv_block(lds_layer,?'conv',?128,?(1,1),?padding='same',?strides=?(1,1),?relu=False)
第二個輸入是全局特征提取器的輸出。但在加入第二個輸入之前,它們首先進行上采樣(4,4),然后進行DepthWise卷積,最后是另一個point-wise卷積。在point-wise卷積輸出中不添加激活,激活是在這兩個輸入相加后引入的。

這是使用TF.Keras實現(xiàn)的低分辨率操作:
ff_layer2?=?tf.keras.layers.UpSampling2D((4,?4))(gfe_layer)
ff_layer2?=?tf.keras.layers.DepthwiseConv2D(128,?strides=(1,?1),?depth_multiplier=1,?padding='same')(ff_layer2)
ff_layer2?=?tf.keras.layers.BatchNormalization()(ff_layer2)
ff_layer2?=?tf.keras.activations.relu(ff_layer2)
ff_layer2?=?tf.keras.layers.Conv2D(128,?1,?1,?padding='same',?activation=None)(ff_layer2)
現(xiàn)在,讓我們將這兩個輸入添加到特征融合模塊中。
ff_final?=?tf.keras.layers.add([ff_layer1,?ff_layer2])
ff_final?=?tf.keras.layers.BatchNormalization()(ff_final)
ff_final?=?tf.keras.activations.relu(ff_final)
4. 分類器
在分類器部分,引入了2個深度可分離的卷積層和1個Point-wise的卷積層。在每個層之后,還進行了BatchNorm層和ReLU激活。
這里需要注意的是,在原論文中,沒有提到在point-wise卷積層之后添加上采樣和Dropout層,但在本文的后面部分描述了這些層是在 point-wise卷積層之后添加的。因此,在實現(xiàn)過程中,我也按照論文的要求引入了這兩層。
在根據(jù)最終輸出的需要進行上采樣之后,SoftMax將作為最后一層的激活。
classifier?=?tf.keras.layers.SeparableConv2D(128,?(3,?3),?padding='same',?strides?=?(1,?1),?name?=?'DSConv1_classifier')(ff_final)
classifier?=?tf.keras.layers.BatchNormalization()(classifier)
classifier?=?tf.keras.activations.relu(classifier)
classifier?=?tf.keras.layers.SeparableConv2D(128,?(3,?3),?padding='same',?strides?=?(1,?1),?name?=?'DSConv2_classifier')(classifier)
classifier?=?tf.keras.layers.BatchNormalization()(classifier)
classifier?=?tf.keras.activations.relu(classifier)
classifier?=?conv_block(classifier,?'conv',?19,?(1,?1),?strides=(1,?1),?padding='same',?relu=True)
classifier?=?tf.keras.layers.Dropout(0.3)(classifier)
classifier?=?tf.keras.layers.UpSampling2D((8,?8))(classifier)
classifier?=?tf.keras.activations.softmax(classifier)
編譯模型
現(xiàn)在我們已經(jīng)添加了所有的層,讓我們創(chuàng)建最終的模型并編譯它。為了創(chuàng)建模型,如上所述,我們使用了來自TF.Keras的函數(shù)api。這里,模型的輸入是學習下采樣模塊中描述的初始輸入層,輸出是最終分類器的輸出。
fast_scnn?=?tf.keras.Model(inputs?=?input_layer?,?outputs?=?classifier,?name?=?'Fast_SCNN')
現(xiàn)在,讓我們用優(yōu)化器和損失函數(shù)來編譯它。在原論文中,作者在訓練過程中使用了動量值為0.9,批大小為12的SGD優(yōu)化器。他們還在學習率策略中使用了多項式學習率,base值為0.045,power為0.9。為了簡單起見,我在這里沒有使用任何學習率策略,但如果需要,你可以自己添加。此外,在編譯模型時從ADAM optimizer開始總是一個好主意,但是在這個CityScapes dataset的特殊情況下,作者只使用了SGD。但在一般情況下,最好從ADAM optimizer開始,然后根據(jù)需要轉(zhuǎn)向其他不同的優(yōu)化器。對于損失函數(shù),作者使用了交叉熵損失,在實現(xiàn)過程中也使用了交叉熵損失。
optimizer?=?tf.keras.optimizers.SGD(momentum=0.9,?lr=0.045)
fast_scnn.compile(loss='categorical_crossentropy',?optimizer=optimizer,?metrics=['accuracy'])
在本文中,作者使用CityScapes數(shù)據(jù)集中的19個類別進行訓練和評價。通過這個實現(xiàn),你可以根據(jù)特定項目所需的任意數(shù)量的輸出進行調(diào)整。
下面是一些Fast-SCNN的驗證結(jié)果,與輸入圖像和ground truth進行了比較。


英文原文:https://medium.com/deep-learning-journals/fast-scnn-explained-and-implemented-using-tensorflow-2-0-6bd17c17a49e

