語義分割-全卷積網絡 FCN 論文閱讀及代碼實現

今天來看一篇復古的文章,Full Convolutional Networks 即全卷積神經網絡,這是 2015 年的一篇語義分割方向的文章,是一篇比較久遠的開山之作。因為最近在研究語義分割方向,所以還是決定先從這個鼻祖入手,畢竟後面的文章很多都借鑑了這篇文章的思想,掌握好基礎我們才能飛的更高。本篇文章分為兩部分: 論文解讀與代碼實現。

論文解讀

  • 語義分割介紹

語義分割(Semantic Segmentation)的目的是對圖像中每一個像素點進行分類,與普通的分類任務只輸出某個類別不同,語義分割任務輸出是與輸入圖像大小相同的圖像,輸出圖像的每個像素對應了輸入圖像每個像素的類別。

語義分割-全卷積網絡 FCN 論文閱讀及代碼實現

語義分割的預測結果

  • 網絡結構

FCN 的基本結構很簡單,就是全部由卷積層組成的網絡。用於圖像分類的網絡一般結構是"卷積-池化-卷積-池化-全連接",其中卷積和全連接層是有參數的,池化則沒有參數。論文作者認為全連接層讓目標的位置信息消失了,只保留了語義信息,因此將全連接操作更換為卷積操作可以同時保留位置信息及語義信息,達到給每個像素分類的目的。網絡的基本結構如下:

語義分割-全卷積網絡 FCN 論文閱讀及代碼實現

fcn網絡結構

輸入圖像經過卷積和池化之後,得到的 feature map 寬高相對原圖縮小了數倍,例如下圖中,提取特徵之後"特徵長方體"的寬高為原圖像的 1/32,為了得到與原圖大小一致的輸出結果,需要對其進行上採樣(upsampling),下面介紹上採樣的方法之一-反捲積(圖中最終輸出的"厚度"是 21,因為類別數是 21,每一層可以看做是原圖像中的每個像素屬於某類別的概率,coding 的時候需要注意一下)。

語義分割-全卷積網絡 FCN 論文閱讀及代碼實現

網絡結構細節

  • 反捲積

反捲積是上採樣(unsampling) 的一種方式,論文作者在實驗之後發現反捲積相較於其他上採樣方式例如 bilinear upsampling 效率更高,所以採用了這種方式。

關於反捲積的解釋借鑑了這一篇: https://medium.com/activating-robotic-minds/up-sampling-with-transposed-convolution-9ae4f2df52d0, 英文OK的小夥伴推薦看原文,講的很通透。

先看看正向卷積,我們知道卷積操作本質就是矩陣相乘再相加。現在假設我們有一個 4x4 的矩陣,卷積核大小為 3x3,步幅為 1 且不填充,那麼其輸出會是一個 2x2 的矩陣,這個過程實際是一個多對一的映射:

語義分割-全卷積網絡 FCN 論文閱讀及代碼實現

卷積

將整個卷積步驟分成四步來看是這樣的過程:

語義分割-全卷積網絡 FCN 論文閱讀及代碼實現

卷積步驟

從上圖也可以看出,卷積操作其實是保留了位置信息的,例如輸出矩陣中左上角的數字"122"就對應了原矩陣左上方的 9 個元素。

那麼如何將結果的 2x2 的矩陣"擴展"為 4x4 的矩陣呢(一對多的映射)。我們從另一個角度來看卷積,首先我們將卷積核展開為一行,原矩陣展開為一列:

語義分割-全卷積網絡 FCN 論文閱讀及代碼實現

卷積核展開為一行

語義分割-全卷積網絡 FCN 論文閱讀及代碼實現

原矩陣展開為一列

則卷積過程的第一步:

語義分割-全卷積網絡 FCN 論文閱讀及代碼實現

卷積的第一個步驟

不要著急,可以細細體會一下,卷積操作確實如此。可以看出,4x16 的卷積核矩陣與 16x1 的輸入矩陣相乘,得到了 4x1 的輸出矩陣,達到了多對一映射的效果,那麼將卷積核矩陣轉置為 16x4 乘上輸出矩陣 4x1 就可以達到一對多映射的效果,因此反捲積也叫做轉置卷積。具體過程如下:

語義分割-全卷積網絡 FCN 論文閱讀及代碼實現

一對多

語義分割-全卷積網絡 FCN 論文閱讀及代碼實現

轉置卷積

總結來說,全卷積網絡的基本結構就是"卷積-反捲積-分類器",通過卷積提取位置信息及語義信息,通過反捲積上採樣至原圖像大小,再加上深層特徵與淺層特徵的融合,達到了語義分割的目的。

代碼實現

自己實現了簡單版本的 FCN,代碼地址 github:

https://github.com/FroyoZzz/CV-Papers-Codes

歡迎提問以及 star。

VGG16 網絡代碼,forward 返回了卷積過程中各層的輸出,以便後面與反捲積的特徵做融合:

class VGG(nn.Module):
 def __init__(self, pretrained=True):
 super(VGG, self).__init__()
 # conv1 1/2
 self.conv1_1 = nn.Conv2d(3, 64, kernel_size=3, padding=1)
 self.relu1_1 = nn.ReLU(inplace=True)
 self.conv1_2 = nn.Conv2d(64, 64, kernel_size=3, padding=1)
 self.relu1_2 = nn.ReLU(inplace=True)
 self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
 # conv2 1/4
 self.conv2_1 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
 self.relu2_1 = nn.ReLU(inplace=True)
 self.conv2_2 = nn.Conv2d(128, 128, kernel_size=3, padding=1)
 self.relu2_2 = nn.ReLU(inplace=True)
 self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)
 # conv3 1/8
 self.conv3_1 = nn.Conv2d(128, 256, kernel_size=3, padding=1)
 self.relu3_1 = nn.ReLU(inplace=True)
 self.conv3_2 = nn.Conv2d(256, 256, kernel_size=3, padding=1)
 self.relu3_2 = nn.ReLU(inplace=True)
 self.conv3_3 = nn.Conv2d(256, 256, kernel_size=3, padding=1)
 self.relu3_3 = nn.ReLU(inplace=True)
 self.pool3 = nn.MaxPool2d(kernel_size=2, stride=2)
 # conv4 1/16
 self.conv4_1 = nn.Conv2d(256, 512, kernel_size=3, padding=1)
 self.relu4_1 = nn.ReLU(inplace=True)
 self.conv4_2 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
 self.relu4_2 = nn.ReLU(inplace=True)
 self.conv4_3 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
 self.relu4_3 = nn.ReLU(inplace=True)
 self.pool4 = nn.MaxPool2d(kernel_size=2, stride=2)
 # conv5 1/32
 self.conv5_1 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
 self.relu5_1 = nn.ReLU(inplace=True)
 self.conv5_2 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
 self.relu5_2 = nn.ReLU(inplace=True)
 self.conv5_3 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
 self.relu5_3 = nn.ReLU(inplace=True)
 self.pool5 = nn.MaxPool2d(kernel_size=2, stride=2)
 
 # load pretrained params from torchvision.models.vgg16(pretrained=True)
 if pretrained:
 pretrained_model = vgg16(pretrained=pretrained)
 pretrained_params = pretrained_model.state_dict()
 keys = list(pretrained_params.keys())
 new_dict = {}
 for index, key in enumerate(self.state_dict().keys()):
 new_dict[key] = pretrained_params[keys[index]]
 self.load_state_dict(new_dict)
 def forward(self, x):
 x = self.relu1_1(self.conv1_1(x))
 x = self.relu1_2(self.conv1_2(x))
 x = self.pool1(x)
 pool1 = x
 x = self.relu2_1(self.conv2_1(x))
 x = self.relu2_2(self.conv2_2(x))
 x = self.pool2(x)
 pool2 = x
 x = self.relu3_1(self.conv3_1(x))
 x = self.relu3_2(self.conv3_2(x))
 x = self.relu3_3(self.conv3_3(x))
 x = self.pool3(x)
 pool3 = x
 x = self.relu4_1(self.conv4_1(x))
 x = self.relu4_2(self.conv4_2(x))
 x = self.relu4_3(self.conv4_3(x))
 x = self.pool4(x)
 pool4 = x
 x = self.relu5_1(self.conv5_1(x))
 x = self.relu5_2(self.conv5_2(x))
 x = self.relu5_3(self.conv5_3(x))
 x = self.pool5(x)
 pool5 = x
 return pool1, pool2, pool3, pool4, pool5

FCN 網絡,結構也很簡單,包含了反捲積以及與淺層信息的融合:

class FCNs(nn.Module):
 def __init__(self, num_classes, backbone="vgg"):
 super(FCNs, self).__init__()
 self.num_classes = num_classes
 if backbone == "vgg":
 self.features = VGG()
 # deconv1 1/16
 self.deconv1 = nn.ConvTranspose2d(512, 512, kernel_size=3, stride=2, padding=1, output_padding=1)
 self.bn1 = nn.BatchNorm2d(512)
 self.relu1 = nn.ReLU()
 # deconv1 1/8
 self.deconv2 = nn.ConvTranspose2d(512, 256, kernel_size=3, stride=2, padding=1, output_padding=1)
 self.bn2 = nn.BatchNorm2d(256)
 self.relu2 = nn.ReLU()
 # deconv1 1/4
 self.deconv3 = nn.ConvTranspose2d(256, 128, kernel_size=3, stride=2, padding=1, output_padding=1)
 self.bn3 = nn.BatchNorm2d(128)
 self.relu3 = nn.ReLU()
 # deconv1 1/2
 self.deconv4 = nn.ConvTranspose2d(128, 64, kernel_size=3, stride=2, padding=1, output_padding=1)
 self.bn4 = nn.BatchNorm2d(64)
 self.relu4 = nn.ReLU()
 # deconv1 1/1
 self.deconv5 = nn.ConvTranspose2d(64, 32, kernel_size=3, stride=2, padding=1, output_padding=1)
 self.bn5 = nn.BatchNorm2d(32)
 self.relu5 = nn.ReLU()
 self.classifier = nn.Conv2d(32, num_classes, kernel_size=1)
 def forward(self, x):
 features = self.features(x)
 y = self.bn1(self.relu1(self.deconv1(features[4])) + features[3])
 y = self.bn2(self.relu2(self.deconv2(y)) + features[2])
 y = self.bn3(self.relu3(self.deconv3(y)) + features[1])
 y = self.bn4(self.relu4(self.deconv4(y)) + features[0])
 y = self.bn5(self.relu5(self.deconv5(y)))
 y = self.classifier(y)
 return y

訓練,每個 epoch 會保存一個模型,默認保存在./models下:

python train.py

我的訓練結果,上面一行是預測值,下面一行是目標值:

5 個 epoch 時,效果還不是很好:

語義分割-全卷積網絡 FCN 論文閱讀及代碼實現

5 個 epoch

10 個 epoch 時:

語義分割-全卷積網絡 FCN 論文閱讀及代碼實現

10 個 epoch

20 個 epoch 時,明顯進步:

語義分割-全卷積網絡 FCN 論文閱讀及代碼實現

20 個 epoch

訓練好的模型由於比較大我就沒有傳到 git 上,數據集比較小自己訓練的話大概半個小時就可以訓練結束,前提是有顯卡,如果需要訓練好的模型的話可以留言我發給你。

PS:歡迎關注我的個人微信公眾號 [MachineLearning學習之路],和我一起學習 python,深度學習,計算機視覺 !


分享到:


相關文章: