DeepLabv1: Semantic Image Segmentation with Deep CNNs and Fully Connected CRFs的论文阅读与Pytorch实现

作 者: 心有宝宝人自圆

声 明: 欢迎转载本文中的图片或文字,请说明出处

写在前面

自从FCN提出以来,越来越多的语义分割任务开始采用采用全卷积网络结构,随着FCN结构使用的增加,研究人员发先了其结构天生的缺陷极大的限制了分割的准确度:CNNs在high-level (large scale) tasks中取得了十分优异的成绩,这得益于局部空间不变性(主要是池化层增大了感受野,也丢弃了部分细节信息)使得网络能够学习到层次化的抽象信息,但这却恰恰不利于low-level (small scale) tasks

所以Deeplab的作者引入了如下结构来对抗这种细节丢失的问题:

  • 带洞的卷积,atrous算法
  • Fully connected CRF的后处理过程

1. Introduction


CNNs在high-level vision tasks(如图像分类、目标检测等)取得优异得表现,这些工作都有共同的主题:end-to-end训练的方法比人工的特征工程方法更优。这得益于CNNs内在的局部空间不变性,然而对应low-level vision tasks(语义分割)来说,我们需要准确的位置信息而非空间信息抽象后的层次化信息。

CNNs应用于low-level vision tasks主要技术障碍是:

  • 信号的下采样

  • 空间的局部不变性

    下采样问题是池化层和striding的联合影响产生,其目的是为了使较小的卷积核能够去学习空间中有用的信息(因此需要增大感受野),但这种下采样必然造成信息的损失。为了在不造成信息损失的情况下增大感受野,作者使用了带洞的卷积(下均称atrous方法)

该图片来自:vdumoulin/conv_arithmetic

DILATED CONVOLUTIONS with kernel size 3x3, dilation=2

局部空间不变性是classifier获得以对象为中心的决策的要求,主要还是由于池化层得作用只保留了局部空间中最重要的信息,作者使用Fully connected CRF(后称DenseCRF)进行全卷积网络训练完成后的后处理,DenseCRF能够在满足长程依赖性的同时捕获细节边缘信息

2. Related Work


远古时期,分割主要基于信号系统,而后是概率模型占据主流

CNNs发展起来之后是two-stage的策略(提议区域+区域预测)占据了主流,但提议区域会使得系统面临前端分割系统的潜在错误(提议区域的质量直接能影响预测结果)

随FCN提出后one-stage策略(直接基于像素预测)占据主流

3.CNN NETWORKS FOR DENSE IMAGE LABELING


Deeplabv1就基本上是按照FCN的结构来设计的,只是部分结构进行了修改。由于网络使用了atrous算法,可以使作为encoder的CNN提取出比FCN更密集的final layer特征:FCN的encoder的final layer下采样了32倍,而Deeplabv1仅下采样了8倍

本文和FCN一样使用了预训练的VGG-16网络

3.1 AGROUS算法

语义分割是一种dense prediction任务,所以能够使用CNN提取出更密集的feature是提升准确率的关键,而基于密集feature评分成为Deeplabv1成功的关键。

为了获得更密集的feature,作者跳过了最后两个maxpooling层(maxpool4,5)的下采样,并在最后三个卷积层(conv5_1 - 3)和第一个全连接层(fc6)使用atrous算法

带洞的原意是给卷积核中间插入0,而这样的操作等同于给卷积层一个input_stride(普通的卷积默认input_stride = 1),这样就可以使卷积计算后特征图中同样的像素具有更大的感受野(还需要调整padding = input_size *( kernel_size ) // 2才能保证特征图保持输入大小一致)。这样不通过下采样获得的感受野增大不会缺失原有的信息,不像池化层那样引入了近似

最后通过评分层输出的class score maps 只需使用双线性插值上采样8倍即可

损失函数直接使用基于每个像素的Cross Entropy损失并相加,每个像素和每个类别的权重相同(大部分的类别为负类,即背景类,不会对最终分割效果产生影响)

3.2 控制感受野和加速计算

作者在这部分介绍的关于显式控制感受野和加速密集计算的的方法主要涉及全连接层的转换。在我之前介绍FCN的文章中,对于全连接层和卷积层之间的相互转换有过讲解。由于预训练模型都是针对大尺度目标的分类任务的:例如VGG-16的fc6就是有4096个大小为7x7的核,而这么大的核通常又会带来计算问题。

作者使用了对卷积核进行空间采样的方法:simple decimation(在我之前的文章介绍SSD的2.2.2节有讲解),这样就可以显式的降低感受野,并且显著的加快计算速度,节省存储空间

4.恢复细节的边界:全连接随机条件场和多尺度预测


4.1 定位的挑战

如图2所示,CNN计算出的得分图可以可靠地预测图像中物体和大致位置,但不能精确地指出它们的轮廓。CNN在分类准确和定位准确之间存在天然的trade-off:池化层提高了high-level tasks的效果,却带来了信息损失、大尺度的感受野和局部不变性,阻碍了low-level tasks的效果,因此为从得分图推断出原始空间的结果增加了难度。

在Deeplabv1提出前,有两种主流方法去解决定位的挑战:

  • 利用来自CNN多个层的信息(比如FCN的跳跃结构)

  • 采用超像素表示,本质上是将定位任务委托给低级的分割方法

Deeplabv1提出了使用DenseCRF进行后处理来解决定位的挑战

4.2 使用DenseCRF进行准确定位

传统上,CRFs模型是用来平滑分割图上的噪声,尤其是这些模型包含连接邻近节点的能量项(二元项),倾向于对空间近端像素进行相同标签赋值。从定性的说,这些短程CRFs(short-range CRFs)的主要功能是清除基于手工设计的局部特征构建的弱分类器的错误预测。

与弱分类器相比,Deeplab的CNN网络是很强的分类器,图2的结果也表明预测结果很平滑、有很强的同质性。在这个背景下使用短程CRFs是有害的:因为短程CRFs作用是平滑,而不是我们所期望的细化边缘

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

DenseCRF模型采用能量函数

  • ​是分配给像素的标签

  • 其中一元项势能\theta_i(x_i)=-logP(x_i),P(x_i)​表示给像素i处为该标签的概率,该项基于CNN预测分数图

  • 为二元项​\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)该项基于图像本身,即考虑像素间的联系

    其中​\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},由于是全连接,每两个像素之间都要连接

    w_m是由​决定的高斯核函数,为:

p是像素的位置,I是像素的颜色,\sigma_\alpha,\sigma_\beta,\sigma_\gamma均是高斯核的超参数

最后谈一谈关于DenseCRF的优化目标,即为调整输入\boldsymbol x(每个像素的label)使能量函数E(\boldsymbol x)最小化。但由于是全连接,直接计算的话计算量简直爆炸,所以采用平均场近似的方法进行计算

对于DenseCRF具体内容感兴趣的可以看下有关论文,这里仅做感性的认知,并且仅使用有关pydensecrf库。在后来的语义分割论文中很少看到DenseCRF的影子了,因为对于更强的网络模型,DenseCRF的提升效果不明显,或甚至起到反作用。为什么不用DenseCRF了?

更多关于Dense的细节内容和代码中使用pydensecrf库可参考:

4.2 MULTI-SCALE PREDICTION

作者也像FCN的跳跃结构那样结合了多尺度的得分图,发现对于分割效果并没很多提升,更不如DenseCRF对于分割效果的提升明显,并且带来了额外的计算消耗,因此只使用了最后一层得分图

5. 实验和评价


  • Dataset:PASCAL VOC 2012 aug 数据集

  • Training:把CNN模型和CRF模型分开训练

    先fine-tuneCNN模型,mini-batch = 20,learning rate = 0.001 (最后的分类层为0.01),每迭代2000次lr变为原来的0.1,momentum = 0.9 , weight decay = 0.0005

    CNN模型训练fine-tune完成后,开始为DenseCRF调参,使用交叉验证的方法调参(val集里的100张图片):预设​,寻找优超参数​,平均场迭代次数固定为10

  • 不同的网络设计

  • DeepLab:只使用了CNN

  • -CRF:使用了DenseCRF进行后处理

  • -MSc:结合了多尺度的得分图

  • -7x7:fc6使用的卷积核大小为7x7,默认为4x4

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

最后选择的最优组合:DeepLab-CRF-LargeFOV,同时把最后两个classifier层的通道数从4096变为1024

我实现的代码也是按照这个来进行的

6. My code


这里主要列出网络结构和DenseCRF部分,其余部分(如Dataset,数据增广处理,训练、验证)都比较通用可用自己惯用的方法,也可参考我写的FCN主要代码SSD主要代码

我使用了PASCAL VOC 2012数据集,而没有使用aug版,所以效果比使用aug版的差,mIOU才达到51.92%

  • 网络结构:主要由两部分组成,一部分是VGG的Base结构,第二部分是更改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 介绍关于align_corners的内容
          # 为了和下采样(transform,PIL)时图像保持一致,使用align_corners=Fasle
          # 上面的博文认为align_corners=True对语义分割任务可能更好,有机会可以试一下
          # 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
  • 损失函数:等权重的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实在装不上...
    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: 网络输出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)

注意:在进行数据增广时(resize),插值的方法一定要选择NEAREAST而不是默认的Bilinear,否则会对true label image的pixel进行误标,导致问题的出现

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

  • 分割结果





    看来自行车的准确率不高啊😓

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实现全连接CRFs后处理

转载请说明出处。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351