DeepLab: 通过深度卷积网络和全连接条件随机场实现图像语义分割.

DeepLab v1网络在VGG-16的基础上修改:

创新点:

  1. 使用空洞卷积 Atrous conv在不增加参数的情况下增加了感受野;
  2. 使用双线性插值 bi-linear interpolation把特征映射恢复原始分辨率;
  3. 使用全连接的条件随机场 CRF精细化分割结果。

1. 空洞卷积

空洞卷积(Dilated/Atrous Convolution)也叫扩张卷积或者膨胀卷积,是指在卷积核中插入空洞,起到扩大感受野的作用。空洞卷积的直接做法是在常规卷积核中填充0,用来扩大感受野,且进行计算时,空洞卷积中实际只有非零的元素起了作用。假设以一个变量a来衡量空洞卷积的扩张系数,则加入空洞之后的实际卷积核尺寸与原始卷积核尺寸之间的关系:

\[K = k+(k-1)(a-1)\]

其中$k$为原始卷积核大小,$a$为卷积扩张率(dilation rate),$K$为经过扩展后实际卷积核大小。除此之外,空洞卷积的卷积方式跟常规卷积一样。当$a=1$时,空洞卷积就退化为常规卷积。$a=1,2,4$时,空洞卷积示意图如下:

当$a=1$,原始卷积核尺寸为$3\times3$,就是常规卷积。$a=2$时,加入空洞之后的卷积核$=3+(3-1)\times(2-1)=5$,对应的感受野可计算为$2^{(a+2)}-1=7$。$a=3$时,卷积核可以变化到$3+(3-1)(4-1)=9$,感受野则增长到$2^{(a+2)}-1=15$。对比不加空洞卷积的情况,在stride为1的情况下33x3卷积的叠加,第三层输出特征图对应的感受野也只有$1+(3-1)\times3=7$。所以,空洞卷积的一个重要作用就是增大感受野。

对于语义分割而言,空洞卷积主要有三个作用:

  1. 扩大感受野。池化也可以扩大感受野,但空间分辨率降低了,相比之下,空洞卷积可以在扩大感受野的同时不丢失分辨率,且保持像素的相对空间位置不变。简单而言就是空洞卷积可以同时控制感受野和分辨率。
  2. 获取多尺度上下文信息。当多个带有不同dilation rate的空洞卷积核叠加时,不同的感受野会带来多尺度信息,这对于分割任务是非常重要的。
  3. 可以降低计算量,不需要引入额外的参数,如上图空洞卷积示意图所示,实际卷积时只有带有红点的元素真正进行计算。

2. 后处理技术

早期语义分割模型效果较为粗糙,在没有更好的特征提取模型的情况下,研究者们便在神经网络模型的粗糙结果进行后处理(Post-Processing),主要方法就是一些常用的概率图模型,比如说条件随机场(Conditional Random Field,CRF)和马尔可夫随机场(Markov Random Field,MRF)。

CRF是一种经典的概率图模型,简单而言就是给定一组输入序列的条件下,求另一组输出序列的条件概率分布模型,CRF在自然语言处理领域有着广泛应用。CRF在语义分割后处理中用法的基本思路如下:对于网络的粗粒度分割结果而言,每个像素点$i$具有对应的类别标签$x_i$和观测值$y_i$,以每个像素为节点,以像素与像素之间的关系作为边即可构建一个CRF模型。在这个CRF模型中,我们通过观测变量$y_i$来预测像素$i$对应的标签值$x_i$。

但从Deeplab v3开始,主流的语义分割网络就不再热衷于后处理技术了。一个典型的观点认为神经网络分割效果不好才会用后处理技术,这说明在分割网络本身上还有很大的提升空间。一是CRF本身不太容易训练,二来语义分割任务的端到端趋势。后来语义分割领域的SOTA网络也确实证明了这一点。

3. DeepLab的实现

DeepLabV1的模型结构十分简单,基于VGG16做了一些改动。将VGG16的最后三层全连接层替换为卷积层。其中,第十一、十二、十三层为dilation=2的空洞卷积,第十四层为dilation=4的空洞卷积。

from torchvision import models

class DeepLabV1(nn.Module):
    def __init__(self, num_classes):
        super(DeepLabV1, self).__init__()
        #前13层是VGG16的前13层,分为5个stage
        self.num_classes = num_classes
        vgg16 = models.vgg16(pretrained=True)
        self.backbone = vgg16.features
        
        self.stage_1 = nn.Sequential(
            #空洞卷积
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=4, dilation=4),
            nn.BatchNorm2d(512),
            nn.ReLU(),
            
            nn.Conv2d(512, 512, kernel_size=1, stride=1, padding=0),
            nn.BatchNorm2d(512),
            nn.ReLU(),
            
            nn.Conv2d(512, 512, kernel_size=1, stride=1, padding=0),
            nn.BatchNorm2d(512),
            nn.ReLU(),
        )
        self.final = nn.Sequential(
            nn.Conv2d(512, self.num_classes, kernel_size=3, padding=1)
        )
        
    def forward(self, x):
        #调用VGG16的前13层 VGG13
        x = self.backbone(x)[-1]
        x = self.stage_1(x)
        x = nn.functional.interpolate(input=x,scale_factor=8,mode='bilinear')
        x = self.final(x)
        return x