图像分割论文 | DeepLabV1V2V3V3+四版本 | ICLR2015 CVPR2017

1,699 阅读4分钟
  • v1论文名称:“SEMANTIC IMAGE SEGMENTATION WITH DEEP CONVOLUTIONAL NETS AND FULLY CONNECTED CRFS”
  • v1论文链接:arxiv.org/pdf/1412.70…
  • v2论文名称:“DeepLab: Semantic Image Segmentation with Deep Convolutional Nets, Atrous Convolution, and Fully Connected CRFs”
  • v2论文链接:arxiv.org/abs/1606.00…
  • v3论文名称:“Rethinking Atrous Convolution for Semantic Image Segmentation”
  • v3论文链接:arxiv.org/abs/1706.05…
  • v3+论文名称:“Encoder-Decoder with Atrous Separable Convolution for Semantic Image Segmentation”
  • v3+论文链接:arxiv.org/abs/1802.02…

0 综述

DeepLab总共分成了4个版本,分别是:v1,v2,v3和v3+。有点YOLO的感觉了。DeepLab每一代都有一些可圈可点的地方。

Unet对于处理医学图像有天然的优势,所以我阅读DeepLab的论文,也并不是以复现其代码为目的,而是学习其核心技巧,然后融入到Unet当中。

1 v1

第一代的版本,关键的改动是把VGG改成了DRN的结构,关于DRN的介绍可以看: juejin.cn/post/691748…

我找到了一个pytorch的deeplabv1中修改之后的VGG的代码,大家可以欣赏一下:

import torch
import torch.nn as nn
from torchvision import models

class VGG16_LargeFOV(nn.Module):
    def __init__(self, num_classes=21, input_size=321, split='train', init_weights=True):
        super(VGG16_LargeFOV, self).__init__()
        self.input_size = input_size
        self.split = split
        self.features = nn.Sequential(
            ### conv1_1 conv1_2 maxpooling
            nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1),
            nn.ReLU(True),
            nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=1),
            nn.ReLU(True),
            nn.MaxPool2d(kernel_size=3, stride=2, padding=1),

            ### conv2_1 conv2_2 maxpooling
            nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1),
            nn.ReLU(True),
            nn.Conv2d(128, 128, kernel_size=3, stride=1, padding=1),
            nn.ReLU(True),
            nn.MaxPool2d(kernel_size=3, stride=2, padding=1),

            ### conv3_1 conv3_2 conv3_3 maxpooling
            nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1),
            nn.ReLU(True),
            nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
            nn.ReLU(True),
            nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
            nn.ReLU(True),
            nn.MaxPool2d(kernel_size=3, stride=2, padding=1),


            ### conv4_1 conv4_2 conv4_3 maxpooling(stride=1)
            nn.Conv2d(256, 512, kernel_size=3, stride=1, padding=1),
            nn.ReLU(True),
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
            nn.ReLU(True),
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
            nn.ReLU(True),
            nn.MaxPool2d(kernel_size=3, stride=1, padding=1),

            ### conv5_1 conv5_2 conv5_3 (dilated convolution dilation=2, padding=2)
            ### maxpooling(stride=1)
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=2, dilation=2),
            nn.ReLU(True),
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=2, dilation=2),
            nn.ReLU(True),
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=2, dilation=2),
            nn.ReLU(True),
            nn.MaxPool2d(kernel_size=3, stride=1, padding=1),
            ### average pooling
            nn.AvgPool2d(kernel_size=3, stride=1, padding=1),

            ### fc6 relu6 drop6
            nn.Conv2d(512, 1024, kernel_size=3, stride=1, padding=12, dilation=12),
            nn.ReLU(True),
            nn.Dropout2d(0.5),

            ### fc7 relu7 drop7 (kernel_size=1, padding=0)
            nn.Conv2d(1024, 1024, kernel_size=1, stride=1, padding=0),
            nn.ReLU(True),
            nn.Dropout2d(0.5),

            ### fc8
            nn.Conv2d(1024, num_classes, kernel_size=1, stride=1, padding=0)
        )
        
        if init_weights:
            self._initialize_weights()
    
    def forward(self, x):
        output = self.features(x)
        if self.split == 'test':
            output = nn.functional.interpolate(output, size=(self.input_size, self.input_size), mode='bilinear', align_corners=True)
        return output
    
    def _initialize_weights(self):
        for m in self.named_modules():
            if isinstance(m[1], nn.Conv2d):
                if m[0] == 'features.38':
                    nn.init.normal_(m[1].weight.data, mean=0, std=0.01)
                    nn.init.constant_(m[1].bias.data, 0.0)
            

if __name__ == "__main__":
    model = VGG16_LargeFOV()
    x = torch.ones([2, 3, 321, 321])
    y = model(x)
    print(y.shape)

没什么难得,大家安装pytorch的话可以直接跑一下,我第一次看到过dropout2D层哈哈

上图中DCNN output是指VGG16跑出来的结果,之后还需要经过CRF来做一个处理。这个CRF是条件随机场,说实话这个东西我也不太明白,不过好在在DeepLabv3之后就不再使用这个东西了。从图中可以看到:

  • CRF是起到一个边缘化处理的效果,对边缘处理后效果很好;
  • CRF有迭代的次数,所以在论文中也提出CRFasRNN的观点。场的概念挺复杂的,幸好v3之后不用了,不然又是一个工程路上的拦路虎。

2 v2

这个版本在第一个版本的基础上,受SPP(spatial pyramid Pooling)的影响,提出了ASPP空洞空间金字塔池化。其次,把v1的backbone从VGG16改成了resnet。

在GoogleNet中,提出了Inception模块,而后又有Xception,这些Inception模块就是不同大小的卷积核的卷积层并行,然后最后拼接的一个结构。

而这里的ASPP结构和Inception结构类似:

是不同膨胀系数的卷积层构成的,然后拼接方式是求和,这里展示出ASPP结构的pytorch代码:

import torch
import torch.nn as nn
import torch.nn.functional as F


class _ASPP(nn.Module):
    """
    Atrous spatial pyramid pooling (ASPP)
    """

    def __init__(self, in_ch, out_ch, rates):
        super(_ASPP, self).__init__()
        for i, rate in enumerate(rates):
            self.add_module(
                "c{}".format(i),
                nn.Conv2d(in_ch, out_ch, 3, 1, padding=rate, dilation=rate, bias=True),
            )

        for m in self.children():
            nn.init.normal_(m.weight, mean=0, std=0.01)
            nn.init.constant_(m.bias, 0)

    def forward(self, x):
        return sum([stage(x) for stage in self.children()])

此外,v2使用的是Poly学习旅衰减策略:

lriter=lr0(1itermaxiter)powerlr_{iter} = lr_0*(1-\frac{iter}{max_{iter}})^{power}

在论文中说,当power=0.9的时候,效果最好。

3 v3

  1. 使用了Multi-grid,通俗的说就是使用更多膨胀系数更大的膨胀卷积,v2膨胀系数止步到4,现在到16:

  2. 加入BN层到ASPP结构;

4 v3+

  1. 改进就是在decode部分:

类似与Unet的decoder部分,这部分的创新力度有限。

  1. 借鉴了MobileNet的可分离卷积,改成了可分离碰撞卷积:

  2. 借鉴了Xception模块:

5 总结

整体来看,我认为DeepLab的最大的创新在于结合了DRN来做语义分割的encoder,然后提出了ASPP的模块。

此外,如果觉得我的文章内容还不够详细,可以阅读文章开头给出的论文原文,关于PyTorch实现DeepLab不同版本,我找到了这样一个帖子参考:zhuanlan.zhihu.com/p/68531147