FCN:从图片分类到像素分类

FCN:从图片分类到像素分类

  1. FCN 模型结构介绍
  2. 转置卷积
  3. 衡量标准
  4. FCN 性能
  5. 论文中其它有意思结论



一、FCN 模型结构介绍

FCN 是卷积神经网络用于图像语义分割的开山之作,其效果目前被很多更新的设计超越,但其思想依然影响深远。


首先,什么是图像的语义分割?

图像分类任务大家应该很了解,即在图像层面上进行分类,而语义分割则是在像素层面上进行分类,即对于图像的每一个像素,分辨出它属于哪一种物体。

图像分类
语义分割
原论文地址:
Long_Fully_Convolutional_Networks_2015_CVPR_paper.pdf

FCN 即 Fully Convolutional Networks,全卷积网络。其模型结构非常简单,使用譬如 VGG 对图像特征进行提取,移除最后的全连接层,使用上采样的转置卷积层(Transpose Convolution)将多次下采样的特征图恢复到和原图一样的大小,然后对每个像素生成一个分类的标签:

具体代码实现上,使用 Pytorch 实现 FCN32s如下,32s 表示转置卷积层前的特征提取网络的整体步长为32,即使用 stride = 2 的下采样 5 次:

class FCN32s(nn.Module):

    def __init__(self, pretrained_net, n_class):
        super().__init__()
        self.n_class = n_class
        # pretrained_net 即特征提取网络,如 移除最后的全连接层的 VGG
        self.pretrained_net = pretrained_net
        self.relu    = nn.ReLU(inplace=True)
        self.deconv1 = nn.ConvTranspose2d(512, 512, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn1     = nn.BatchNorm2d(512)
        self.deconv2 = nn.ConvTranspose2d(512, 256, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn2     = nn.BatchNorm2d(256)
        self.deconv3 = nn.ConvTranspose2d(256, 128, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn3     = nn.BatchNorm2d(128)
        self.deconv4 = nn.ConvTranspose2d(128, 64, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn4     = nn.BatchNorm2d(64)
        self.deconv5 = nn.ConvTranspose2d(64, 32, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn5     = nn.BatchNorm2d(32)
        self.classifier = nn.Conv2d(32, n_class, kernel_size=1)

    def forward(self, x):
        output = self.pretrained_net(x)
        x5 = output['x5']  # size=(N, 512, x.H/32, x.W/32)

        score = self.bn1(self.relu(self.deconv1(x5)))     # size=(N, 512, x.H/16, x.W/16)
        score = self.bn2(self.relu(self.deconv2(score)))  # size=(N, 256, x.H/8, x.W/8)
        score = self.bn3(self.relu(self.deconv3(score)))  # size=(N, 128, x.H/4, x.W/4)
        score = self.bn4(self.relu(self.deconv4(score)))  # size=(N, 64, x.H/2, x.W/2)
        score = self.bn5(self.relu(self.deconv5(score)))  # size=(N, 32, x.H, x.W)
        score = self.classifier(score)                    # size=(N, n_class, x.H/1, x.W/1)

        return score  # size=(N, n_class, x.H/1, x.W/1)

由于 32 的步长有些大,生成的分割有些不够精确,作者还设计了两个更精细的网络:FCN16s 和 FCN8s。

FCN 结构

FCN16s 将 pool5 层输出(此时整体步长为 32)的特征图 2 倍上采样一次后,将其与 pool4 层的输出相加,后进行4次上采样得到原图大小的特征图;FCN16s 将 pool5 层输出(此时整体步长为 32)的特征图上采样一次后,将其与 pool4 层的输出相加,相加后的结果再 2 倍上采样一次后与 pool3 的输出结果相加,接着进行3次上采样得到原图大小的特征图。

具体的 Pytorch 实现如下:

class FCN16s(nn.Module):

    def __init__(self, pretrained_net, n_class):
        super().__init__()
        self.n_class = n_class
        # pretrained_net 即特征提取网络,如 移除最后的全连接层的 VGG
        self.pretrained_net = pretrained_net
        self.relu    = nn.ReLU(inplace=True)
        self.deconv1 = nn.ConvTranspose2d(512, 512, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn1     = nn.BatchNorm2d(512)
        self.deconv2 = nn.ConvTranspose2d(512, 256, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn2     = nn.BatchNorm2d(256)
        self.deconv3 = nn.ConvTranspose2d(256, 128, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn3     = nn.BatchNorm2d(128)
        self.deconv4 = nn.ConvTranspose2d(128, 64, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn4     = nn.BatchNorm2d(64)
        self.deconv5 = nn.ConvTranspose2d(64, 32, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn5     = nn.BatchNorm2d(32)
        self.classifier = nn.Conv2d(32, n_class, kernel_size=1)

    def forward(self, x):
        output = self.pretrained_net(x)
        x5 = output['x5']  # size=(N, 512, x.H/32, x.W/32)
        x4 = output['x4']  # size=(N, 512, x.H/16, x.W/16)

        score = self.relu(self.deconv1(x5))               # size=(N, 512, x.H/16, x.W/16)
        score = self.bn1(score + x4)                      # element-wise add, size=(N, 512, x.H/16, x.W/16)
        score = self.bn2(self.relu(self.deconv2(score)))  # size=(N, 256, x.H/8, x.W/8)
        score = self.bn3(self.relu(self.deconv3(score)))  # size=(N, 128, x.H/4, x.W/4)
        score = self.bn4(self.relu(self.deconv4(score)))  # size=(N, 64, x.H/2, x.W/2)
        score = self.bn5(self.relu(self.deconv5(score)))  # size=(N, 32, x.H, x.W)
        score = self.classifier(score)                    # size=(N, n_class, x.H/1, x.W/1)

        return score  # size=(N, n_class, x.H/1, x.W/1)


class FCN8s(nn.Module):

    def __init__(self, pretrained_net, n_class):
        super().__init__()
        self.n_class = n_class
        # pretrained_net 即特征提取网络,如 移除最后的全连接层的 VGG
        self.pretrained_net = pretrained_net
        self.relu    = nn.ReLU(inplace=True)
        self.deconv1 = nn.ConvTranspose2d(512, 512, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn1     = nn.BatchNorm2d(512)
        self.deconv2 = nn.ConvTranspose2d(512, 256, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn2     = nn.BatchNorm2d(256)
        self.deconv3 = nn.ConvTranspose2d(256, 128, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn3     = nn.BatchNorm2d(128)
        self.deconv4 = nn.ConvTranspose2d(128, 64, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn4     = nn.BatchNorm2d(64)
        self.deconv5 = nn.ConvTranspose2d(64, 32, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn5     = nn.BatchNorm2d(32)
        self.classifier = nn.Conv2d(32, n_class, kernel_size=1)

    def forward(self, x):
        output = self.pretrained_net(x)
        x5 = output['x5']  # size=(N, 512, x.H/32, x.W/32)
        x4 = output['x4']  # size=(N, 512, x.H/16, x.W/16)
        x3 = output['x3']  # size=(N, 256, x.H/8,  x.W/8)

        score = self.relu(self.deconv1(x5))               # size=(N, 512, x.H/16, x.W/16)
        score = self.bn1(score + x4)                      # element-wise add, size=(N, 512, x.H/16, x.W/16)
        score = self.relu(self.deconv2(score))            # size=(N, 256, x.H/8, x.W/8)
        score = self.bn2(score + x3)                      # element-wise add, size=(N, 256, x.H/8, x.W/8)
        score = self.bn3(self.relu(self.deconv3(score)))  # size=(N, 128, x.H/4, x.W/4)
        score = self.bn4(self.relu(self.deconv4(score)))  # size=(N, 64, x.H/2, x.W/2)
        score = self.bn5(self.relu(self.deconv5(score)))  # size=(N, 32, x.H, x.W)
        score = self.classifier(score)                    # size=(N, n_class, x.H/1, x.W/1)

        return score  # size=(N, n_class, x.H/1, x.W/1)
参考链接:github.com/yunlongdong/

各 FCN 效果如下:

至此,FCN 模型大体就介绍完了,接下来讲讲一些细节。


二、转置卷积

要了解转置卷积,可以看看这篇博客:

blog.csdn.net/LoseInVai
blog.csdn.net/FortiLZ/a

在这里我们主要搞明白转置卷积的使用。我们知道,在卷积层中,输入输出的形状关系为:

\\ o = [ (i + 2p - k)/s ] +1

其中:

  • O : 为 output size
  • i: 为 input size
  • p: 为 padding size
  • k: 为kernel size
  • s: 为 stride size
  • [] 为下取整运算

在 s=1 时我们让:

i = o' \\ o = i'

可以得到:

\\ o' = i' - 2p + k - 1

当 S>1 时,则有

\\ i' = [ (o' + 2p - k)/s ] +1

得到:

o'(0) = ( i' - 1) \times s + k - 2p\\ o'(1) = o'(0) + 1\\ o'(2) = o'(0) + 2\\ ...\\ o'(s-1) = o'(0) + s-1\\

即:

o'(n) =o'(0) + n = ( i' - 1) \times s + k - 2p + n,\\ n =\{0, 1, 2...s-1\}

其中的 n 为 ouput padding。

torch.nn.ConvTranspose2d(in_channels, out_channels, kernel_size, stride=1, padding=0, output_padding=0, groups=1, bias=True, dilation=1)
Pytorch 中 ConvTranspose2d的使用:
[PyTorch]PyTorch中反卷积的用法 - 向前奔跑的少年 - 博客园
blog.csdn.net/qq_259648


三、语义分割的衡量标准

  • piexl accuracy(PA,像素精度):标记正确/总像素数目
  • mean piexl accurancy(MPA,平均像素精度):每个类内像素被正确分类的比例,之后求所有类的平均。
  • mean intersection over union(MIoU,均交并比):在每个类上计算IoU,之后平均。

\\ MIoU = \frac{1}{k+1} \sum_{i=0}^{k}{\frac{p_{ii}}{\sum_{1}^{k}{p_{ij,j \ne i}}}}

其中 p_{ij} 表示对于某像素,模型预测其为分类 j,但真实分类为 j,同理 p_{ii} 则表示模型分类正确。为什么要 k+1?,因为还包含背景类,官方计算 mean IoU 时,背景类的 IoU也是要计算在其中的。比如在 VOC 2012 的排行榜上:

PASCAL VOC Challenge performance evaluation server

可以看到 FCN-8s 的成绩为 62.2,将20类的 IoU 求平均可得:

(76.8+34.2+68.9+49.4+60.3+75.3+74.7+77.6+21.4+62.5+46.8+71.8+63.9+76.5+73.9+45.2+72.4+37.4+70.9+55.1)/20 = 60.75

你会发现计算出来的结果低于 62.2,根据小编的复现经验,FCN 的背景类 IoU 可以很轻松地达到 90+,取背景类为 92 则得到:

(76.8+34.2+68.9+49.4+60.3+75.3+74.7+77.6+21.4+62.5+46.8+71.8+63.9+76.5+73.9+45.2+72.4+37.4+70.9+55.1+92)/21=62.238

so,无论是排行榜还是论文,背景类的 IoU 是要算在 mean IoU 中滴,Pixel Accuracy 就更是如此啦。

  • Frequency Weighted Intersection over Union(FWIoU,频权交并比):为MIoU的一种提升,这种方法根据每个类出现的频率为其设置权重,求加权平均。


四、FCN 的性能


五、论文中其它有意思的结论

1、Shift-and-stitch is filter rarefaction

偏移缝合是一种滤波稀疏化策略。

什么是 Shift-and-stitch ?

简单来说就是一种上采样方式,比如说使用 maxpooling 上采样 2 倍,那就把图片偏移 (1,0),(0,1),(1,1) 加上原图,计算 4 次,把结果交叉编织在一起,就的得到了最终上采样的结果:

参考:blog.csdn.net/qinghuaci






2、Upsampling is backwards strided convolution

上采样即反向传播时的下采样卷积。

可参考:blog.csdn.net/LoseInVai


3、Patchwise training is loss sampling

切块训练实际上是一种 loss 值的采样策略。


PS:
广告时间啦~
理工生如何提高人文素养软实力?快关注微信公众号:


欢迎扫码关注~

编辑于 2019-07-25

文章被以下专栏收录