DeepLabv1: Semantic Image Segmentation with Deep CNNs and Fully Connected CRFs的論文閱讀與Pytorch實(shí)現(xiàn)

作 者: 心有寶寶人自圓

聲 明: 歡迎轉(zhuǎn)載本文中的圖片或文字,請說明出處

寫在前面

自從FCN提出以來,越來越多的語義分割任務(wù)開始采用采用全卷積網(wǎng)絡(luò)結(jié)構(gòu),隨著FCN結(jié)構(gòu)使用的增加,研究人員發(fā)先了其結(jié)構(gòu)天生的缺陷極大的限制了分割的準(zhǔn)確度:CNNs在high-level (large scale) tasks中取得了十分優(yōu)異的成績,這得益于局部空間不變性(主要是池化層增大了感受野,也丟棄了部分細(xì)節(jié)信息)使得網(wǎng)絡(luò)能夠?qū)W習(xí)到層次化的抽象信息,但這卻恰恰不利于low-level (small scale) tasks

所以Deeplab的作者引入了如下結(jié)構(gòu)來對抗這種細(xì)節(jié)丟失的問題:

  • 帶洞的卷積,atrous算法
  • Fully connected CRF的后處理過程

1. Introduction


CNNs在high-level vision tasks(如圖像分類、目標(biāo)檢測等)取得優(yōu)異得表現(xiàn),這些工作都有共同的主題:end-to-end訓(xùn)練的方法比人工的特征工程方法更優(yōu)。這得益于CNNs內(nèi)在的局部空間不變性,然而對應(yīng)low-level vision tasks(語義分割)來說,我們需要準(zhǔn)確的位置信息而非空間信息抽象后的層次化信息。

CNNs應(yīng)用于low-level vision tasks主要技術(shù)障礙是:

  • 信號的下采樣

  • 空間的局部不變性

    下采樣問題是池化層和striding的聯(lián)合影響產(chǎn)生,其目的是為了使較小的卷積核能夠去學(xué)習(xí)空間中有用的信息(因此需要增大感受野),但這種下采樣必然造成信息的損失。為了在不造成信息損失的情況下增大感受野,作者使用了帶洞的卷積(下均稱atrous方法)

該圖片來自:vdumoulin/conv_arithmetic

DILATED CONVOLUTIONS with kernel size 3x3, dilation=2

局部空間不變性是classifier獲得以對象為中心的決策的要求,主要還是由于池化層得作用只保留了局部空間中最重要的信息,作者使用Fully connected CRF(后稱DenseCRF)進(jìn)行全卷積網(wǎng)絡(luò)訓(xùn)練完成后的后處理,DenseCRF能夠在滿足長程依賴性的同時(shí)捕獲細(xì)節(jié)邊緣信息

2. Related Work


遠(yuǎn)古時(shí)期,分割主要基于信號系統(tǒng),而后是概率模型占據(jù)主流

CNNs發(fā)展起來之后是two-stage的策略(提議區(qū)域+區(qū)域預(yù)測)占據(jù)了主流,但提議區(qū)域會(huì)使得系統(tǒng)面臨前端分割系統(tǒng)的潛在錯(cuò)誤(提議區(qū)域的質(zhì)量直接能影響預(yù)測結(jié)果)

隨FCN提出后one-stage策略(直接基于像素預(yù)測)占據(jù)主流

3.CNN NETWORKS FOR DENSE IMAGE LABELING


Deeplabv1就基本上是按照FCN的結(jié)構(gòu)來設(shè)計(jì)的,只是部分結(jié)構(gòu)進(jìn)行了修改。由于網(wǎng)絡(luò)使用了atrous算法,可以使作為encoder的CNN提取出比FCN更密集的final layer特征:FCN的encoder的final layer下采樣了32倍,而Deeplabv1僅下采樣了8倍

本文和FCN一樣使用了預(yù)訓(xùn)練的VGG-16網(wǎng)絡(luò)

3.1 AGROUS算法

語義分割是一種dense prediction任務(wù),所以能夠使用CNN提取出更密集的feature是提升準(zhǔn)確率的關(guān)鍵,而基于密集feature評分成為Deeplabv1成功的關(guān)鍵。

為了獲得更密集的feature,作者跳過了最后兩個(gè)maxpooling層(maxpool4,5)的下采樣,并在最后三個(gè)卷積層(conv5_1 - 3)和第一個(gè)全連接層(fc6)使用atrous算法

帶洞的原意是給卷積核中間插入0,而這樣的操作等同于給卷積層一個(gè)input_stride(普通的卷積默認(rèn)input_stride = 1),這樣就可以使卷積計(jì)算后特征圖中同樣的像素具有更大的感受野(還需要調(diào)整padding = input_size *( kernel_size ) // 2才能保證特征圖保持輸入大小一致)。這樣不通過下采樣獲得的感受野增大不會(huì)缺失原有的信息,不像池化層那樣引入了近似

最后通過評分層輸出的class score maps 只需使用雙線性插值上采樣8倍即可

損失函數(shù)直接使用基于每個(gè)像素的Cross Entropy損失并相加,每個(gè)像素和每個(gè)類別的權(quán)重相同(大部分的類別為負(fù)類,即背景類,不會(huì)對最終分割效果產(chǎn)生影響)

3.2 控制感受野和加速計(jì)算

作者在這部分介紹的關(guān)于顯式控制感受野和加速密集計(jì)算的的方法主要涉及全連接層的轉(zhuǎn)換。在我之前介紹FCN的文章中,對于全連接層和卷積層之間的相互轉(zhuǎn)換有過講解。由于預(yù)訓(xùn)練模型都是針對大尺度目標(biāo)的分類任務(wù)的:例如VGG-16的fc6就是有4096個(gè)大小為7x7的核,而這么大的核通常又會(huì)帶來計(jì)算問題。

作者使用了對卷積核進(jìn)行空間采樣的方法:simple decimation(在我之前的文章介紹SSD的2.2.2節(jié)有講解),這樣就可以顯式的降低感受野,并且顯著的加快計(jì)算速度,節(jié)省存儲(chǔ)空間

4.恢復(fù)細(xì)節(jié)的邊界:全連接隨機(jī)條件場和多尺度預(yù)測


4.1 定位的挑戰(zhàn)

如圖2所示,CNN計(jì)算出的得分圖可以可靠地預(yù)測圖像中物體和大致位置,但不能精確地指出它們的輪廓。CNN在分類準(zhǔn)確和定位準(zhǔn)確之間存在天然的trade-off:池化層提高了high-level tasks的效果,卻帶來了信息損失、大尺度的感受野和局部不變性,阻礙了low-level tasks的效果,因此為從得分圖推斷出原始空間的結(jié)果增加了難度。

在Deeplabv1提出前,有兩種主流方法去解決定位的挑戰(zhàn):

  • 利用來自CNN多個(gè)層的信息(比如FCN的跳躍結(jié)構(gòu))

  • 采用超像素表示,本質(zhì)上是將定位任務(wù)委托給低級的分割方法

Deeplabv1提出了使用DenseCRF進(jìn)行后處理來解決定位的挑戰(zhàn)

4.2 使用DenseCRF進(jìn)行準(zhǔn)確定位

傳統(tǒng)上,CRFs模型是用來平滑分割圖上的噪聲,尤其是這些模型包含連接鄰近節(jié)點(diǎn)的能量項(xiàng)(二元項(xiàng)),傾向于對空間近端像素進(jìn)行相同標(biāo)簽賦值。從定性的說,這些短程CRFs(short-range CRFs)的主要功能是清除基于手工設(shè)計(jì)的局部特征構(gòu)建的弱分類器的錯(cuò)誤預(yù)測。

與弱分類器相比,Deeplab的CNN網(wǎng)絡(luò)是很強(qiáng)的分類器,圖2的結(jié)果也表明預(yù)測結(jié)果很平滑、有很強(qiáng)的同質(zhì)性。在這個(gè)背景下使用短程CRFs是有害的:因?yàn)槎坛藽RFs作用是平滑,而不是我們所期望的細(xì)化邊緣

為了克服短程CRFs的限制,作者使用了全連接CRF

DenseCRF模型采用能量函數(shù)

  • ?是分配給像素的標(biāo)簽

  • 其中一元項(xiàng)勢能\theta_i(x_i)=-logP(x_i),P(x_i)?表示給像素i處為該標(biāo)簽的概率,該項(xiàng)基于CNN預(yù)測分?jǐn)?shù)圖

  • 為二元項(xiàng)?\theta_{ij}(x_i,x_j)=\mu(x_i,x_j)\sum_{m=1}^Kw_m\cdot k^m(\boldsymbol f_i,\boldsymbol f_j)該項(xiàng)基于圖像本身,即考慮像素間的聯(lián)系

    其中?\begin{cases}\mu(x_i,x_j)=1,x_i \ne x_j\\\mu(x_i,x_j)=0,x_i=x_j\end{cases},由于是全連接,每兩個(gè)像素之間都要連接

    ?w_m是由?決定的高斯核函數(shù),為:

?p是像素的位置,I是像素的顏色,\sigma_\alpha,\sigma_\beta,\sigma_\gamma均是高斯核的超參數(shù)

最后談一談關(guān)于DenseCRF的優(yōu)化目標(biāo),即為調(diào)整輸入\boldsymbol x(每個(gè)像素的label)使能量函數(shù)E(\boldsymbol x)最小化。但由于是全連接,直接計(jì)算的話計(jì)算量簡直爆炸,所以采用平均場近似的方法進(jìn)行計(jì)算

對于DenseCRF具體內(nèi)容感興趣的可以看下有關(guān)論文,這里僅做感性的認(rèn)知,并且僅使用有關(guān)pydensecrf庫。在后來的語義分割論文中很少看到DenseCRF的影子了,因?yàn)閷τ诟鼜?qiáng)的網(wǎng)絡(luò)模型,DenseCRF的提升效果不明顯,或甚至起到反作用。為什么不用DenseCRF了?

更多關(guān)于Dense的細(xì)節(jié)內(nèi)容和代碼中使用pydensecrf庫可參考:

4.2 MULTI-SCALE PREDICTION

作者也像FCN的跳躍結(jié)構(gòu)那樣結(jié)合了多尺度的得分圖,發(fā)現(xiàn)對于分割效果并沒很多提升,更不如DenseCRF對于分割效果的提升明顯,并且?guī)砹祟~外的計(jì)算消耗,因此只使用了最后一層得分圖

5. 實(shí)驗(yàn)和評價(jià)


  • Dataset:PASCAL VOC 2012 aug 數(shù)據(jù)集

  • Training:把CNN模型和CRF模型分開訓(xùn)練

    先fine-tuneCNN模型,mini-batch = 20,learning rate = 0.001 (最后的分類層為0.01),每迭代2000次lr變?yōu)樵瓉淼?.1,momentum = 0.9 , weight decay = 0.0005

    CNN模型訓(xùn)練fine-tune完成后,開始為DenseCRF調(diào)參,使用交叉驗(yàn)證的方法調(diào)參(val集里的100張圖片):預(yù)設(shè)?,尋找優(yōu)超參數(shù)?,平均場迭代次數(shù)固定為10

  • 不同的網(wǎng)絡(luò)設(shè)計(jì)

  • DeepLab:只使用了CNN

  • -CRF:使用了DenseCRF進(jìn)行后處理

  • -MSc:結(jié)合了多尺度的得分圖

  • -7x7:fc6使用的卷積核大小為7x7,默認(rèn)為4x4

  • -LargeFOV:arbitrarily control the Field-of- View (FOV) of the models

最后選擇的最優(yōu)組合:DeepLab-CRF-LargeFOV,同時(shí)把最后兩個(gè)classifier層的通道數(shù)從4096變?yōu)?024

我實(shí)現(xiàn)的代碼也是按照這個(gè)來進(jìn)行的

6. My code


這里主要列出網(wǎng)絡(luò)結(jié)構(gòu)和DenseCRF部分,其余部分(如Dataset,數(shù)據(jù)增廣處理,訓(xùn)練、驗(yàn)證)都比較通用可用自己慣用的方法,也可參考我寫的FCN主要代碼SSD主要代碼

我使用了PASCAL VOC 2012數(shù)據(jù)集,而沒有使用aug版,所以效果比使用aug版的差,mIOU才達(dá)到51.92%

  • 網(wǎng)絡(luò)結(jié)構(gòu):主要由兩部分組成,一部分是VGG的Base結(jié)構(gòu),第二部分是更改VGG的全連接層為LargeFOV全卷積層
import torch
  from torch import nn
  from torchvision import models
  
  class VggBase(nn.Module):
      def __init__(self):
          super(VggBase, self).__init__()
          self.conv1_1 = nn.Conv2d(3, 64, kernel_size=3, padding=1)
          self.conv1_2 = nn.Conv2d(64, 64, kernel_size=3, padding=1)
          self.pool1 = nn.MaxPool2d(2, 2)
  
          self.conv2_1 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
          self.conv2_2 = nn.Conv2d(128, 128, kernel_size=3, padding=1)
          self.pool2 = nn.MaxPool2d(2, 2)
  
          self.conv3_1 = nn.Conv2d(128, 256, kernel_size=3, padding=1)
          self.conv3_2 = nn.Conv2d(256, 256, kernel_size=3, padding=1)
          self.conv3_3 = nn.Conv2d(256, 256, kernel_size=3, padding=1)
          self.pool3 = nn.MaxPool2d(2, 2)
  
          self.conv4_1 = nn.Conv2d(256, 512, kernel_size=3, padding=1)
          self.conv4_2 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
          self.conv4_3 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
          self.relu4 = nn.ReLU(inplace=True)
          # 如文中所述:skip subsampling after the last two max-pooling layers in the network
          # 2×inthe last three convolutional layers and 4×in the first fully connected layer
          self.pool4 = nn.MaxPool2d(3, 1, 1)
  
          self.conv5_1 = nn.Conv2d(512, 512, kernel_size=3, padding=2, dilation=2)
          self.conv5_2 = nn.Conv2d(512, 512, kernel_size=3, padding=2, dilation=2)
          self.conv5_3 = nn.Conv2d(512, 512, kernel_size=3, padding=2, dilation=2)
          self.pool5 = nn.MaxPool2d(3, 1, 1)
  
          self.load_pretrained_layers()
  
      def load_pretrained_layers(self):
          pretrained_net = models.vgg16(pretrained=True)
          state_dict = self.state_dict()
          for c, pc in zip(state_dict.keys(), pretrained_net.features.state_dict().values()):
              assert state_dict[c].size() == pc.size()
              state_dict[c] = pc
          self.load_state_dict(state_dict)
  
      def forward(self, x):
          out = torch.relu(self.conv1_1(x))
          out = torch.relu(self.conv1_2(out))
          out = self.pool1(out)
  
          out = torch.relu(self.conv2_1(out))
          out = torch.relu(self.conv2_2(out))
          out = self.pool2(out)
  
          out = torch.relu(self.conv3_1(out))
          out = torch.relu(self.conv3_2(out))
          out = torch.relu(self.conv3_3(out))
          out = self.pool3(out)
  
          out = torch.relu(self.conv4_1(out))
          out = torch.relu(self.conv4_2(out))
          out = torch.relu(self.conv4_3(out))
          out = self.pool4(out)
  
          out = torch.relu(self.conv5_1(out))
          out = torch.relu(self.conv5_2(out))
          out = torch.relu(self.conv5_3(out))
          out = self.pool5(out)
  
          return out
  
  
  class AtrousConv(nn.Module):
      def __init__(self, n_classes, out_channels=1024, decimation=3, rate=12):
          # 按文中所述:employ kernel size 3×3 and input stride = 12,
          # and further change the filter sizes from 4096 to 1024 for the last two layers
          super(AtrousConv, self).__init__()
          self.decimation = [4096 // out_channels, None, decimation, decimation]
          self.atrous = nn.Conv2d(512, out_channels, kernel_size=decimation, padding=rate * (decimation - 1) // 2,
                                  dilation=rate)
          self.relu1 = nn.ReLU(inplace=True)
          self.fc1 = nn.Conv2d(out_channels, out_channels, kernel_size=1)
          self.relu2 = nn.ReLU(inplace=True)
          self.fc2 = nn.Conv2d(out_channels, n_classes, kernel_size=1)
  
          self.init_param()
  
      def decimate(self, param: torch.Tensor, decimation):
          assert param.dim() == len(decimation)
          for d in range(param.dim()):
              if decimation[d] is not None:
                  param = param.index_select(dim=d,
                                             index=torch.arange(start=0, end=param.size(d),
                                                                step=decimation[d]).long())
          return param
  
      def init_param(self):
          pretrained_dict = models.vgg16(pretrained=True).state_dict()
          self.atrous.weight.data = self.decimate(pretrained_dict['classifier.0.weight'].view(4096, 512, 7, 7),
                                                  self.decimation)
          self.atrous.bias.data = self.decimate(pretrained_dict['classifier.0.bias'].view(4096), self.decimation[:1])
  
          self.fc1.weight.data = self.decimate(pretrained_dict['classifier.3.weight'].view(4096, 4096, 1, 1),
                                               [self.decimation[0], self.decimation[0], None, None])
          self.fc1.bias.data = self.decimate(pretrained_dict['classifier.3.bias'].view(4096), self.decimation[:1])
  
          nn.init.xavier_normal_(self.fc2.weight)
          nn.init.constant_(self.fc2.bias, 0)
  
      def forward(self, x):
          for blk in self.children():
              x = blk(x)
          return x
  
  
  class DeepLabv1(nn.Module):
      def __init__(self, n_classes=21):
          super(DeepLabv1, self).__init__()
          self.base = VggBase()
          self.atrous = AtrousConv(n_classes)
  
      def forward(self, x):
          x = self.base(x)
          x = self.atrous(x)
          # https://zhuanlan.zhihu.com/p/87572724?from_voters_page=true 介紹關(guān)于align_corners的內(nèi)容
          # 為了和下采樣(transform,PIL)時(shí)圖像保持一致,使用align_corners=Fasle
          # 上面的博文認(rèn)為align_corners=True對語義分割任務(wù)可能更好,有機(jī)會(huì)可以試一下
          # also see:https://discuss.pytorch.org/t/what-we-should-use-align-corners-false/22663/9
          x = nn.functional.interpolate(x, scale_factor=8, mode='bilinear', align_corners=False)
          return x
  • 損失函數(shù):等權(quán)重的Cross Entropy Loss
    class NLLLoss2d(nn.Module):
        def __init__(self):
            super(NLLLoss2d, self).__init__()
            self.loss = nn.NLLLoss()
    
        def forward(self, pred, true):
            pred = nn.functional.log_softmax(pred, dim=1)
            return self.loss(pred, true)
  • DenseCRF:我是在Linux完成安裝pydensecrf的,windows實(shí)在裝不上...
    import numpy as np
    import pydensecrf.densecrf as dcrf
    import pydensecrf.utils as utils
    
    class DenseCRF(object):
        def __init__(self, max_epochs=5, delta_aphla=80, delta_beta=3, w1=10, delta_gamma=3, w2=3):
            self.max_epochs = max_epochs
            self.delta_gamma = delta_gamma
            self.delta_alpha = delta_aphla
            self.delta_beta = delta_beta
            self.w1 = w1
            self.w2 = w2
    
        def __call__(self, image, probmap):
            c, h, w = probmap.shape
    
            U = utils.unary_from_softmax(probmap)
            U = np.ascontiguousarray(U)
    
            image = np.ascontiguousarray(image)
    
            d = dcrf.DenseCRF2D(w, h, c)
            d.setUnaryEnergy(U)
    
            d.addPairwiseGaussian(sxy=self.delta_gamma, compat=self.w2)
            d.addPairwiseBilateral(sxy=self.delta_alpha, srgb=self.delta_beta, rgbim=image, compat=self.w1)
    
            Q = d.inference(self.max_epochs)
            Q = np.array(Q).reshape((c, h, w))
    
            return Q
  • 看一下DenseCRF的使用:
    import models # 寫Class DenseCRF的文件
    import numpy as np
    
    
    def crf(self, img, prob):
        """
            :param img: a PIL image
            :param prob: 網(wǎng)絡(luò)輸出score map, in shape of (21 , height, width)
            :return: new prob map
            """  
        crf = models.DenseCRF()
        prob = torch.softmax(prob, dim=1)[0].numpy()
        res = crf(np.array(image, dtype=np.uint8), prob)                        
    
        return res.argmax(axis=0)

注意:在進(jìn)行數(shù)據(jù)增廣時(shí)(resize),插值的方法一定要選擇NEAREAST而不是默認(rèn)的Bilinear,否則會(huì)對true label image的pixel進(jìn)行誤標(biāo),導(dǎo)致問題的出現(xiàn)

一些衡量的Metrics見:wkentaro/pytorch-fcn,它的算法方法非常巧妙

  • 分割結(jié)果





    看來自行車的準(zhǔn)確率不高啊??

Reference


[1] Chen, L. C. , Papandreou, G. , Kokkinos, I. , Murphy, K. , & Yuille, A. L. . (2014). Semantic image segmentation with deep convolutional nets and fully connected crfs. Computer Science.

[2] kazuto1011/ deeplab-pytorch

[3] doiken23/DeepLab_pytorch

[4] 【圖像后處理】python實(shí)現(xiàn)全連接CRFs后處理

轉(zhuǎn)載請說明出處。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容