DeepLab: 通过深度卷积网络和全连接条件随机场实现图像语义分割.
DeepLab v1网络在VGG-16的基础上修改:
- 把全连接层转为卷积层;
- 下采样只保留前三个最大池化,使得输出特征映射为输入的\(\frac{1}{8}\);
- 池化层之后的卷积采用空洞卷积。
创新点:
- 使用空洞卷积 Atrous conv在不增加参数的情况下增加了感受野;
- 使用双线性插值 bi-linear interpolation把特征映射恢复原始分辨率;
- 使用全连接的条件随机场 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的情况下3层3x3卷积的叠加,第三层输出特征图对应的感受野也只有$1+(3-1)\times3=7$。所以,空洞卷积的一个重要作用就是增大感受野。
对于语义分割而言,空洞卷积主要有三个作用:
- 扩大感受野。池化也可以扩大感受野,但空间分辨率降低了,相比之下,空洞卷积可以在扩大感受野的同时不丢失分辨率,且保持像素的相对空间位置不变。简单而言就是空洞卷积可以同时控制感受野和分辨率。
- 获取多尺度上下文信息。当多个带有不同dilation rate的空洞卷积核叠加时,不同的感受野会带来多尺度信息,这对于分割任务是非常重要的。
- 可以降低计算量,不需要引入额外的参数,如上图空洞卷积示意图所示,实际卷积时只有带有红点的元素真正进行计算。
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