语义分割专栏(二)复习FCN的编解码结构

前言  在这一期中,我们先简要复习一遍FCN网络,随后进入今天的重点——编码器-解码器架构。

本教程禁止转载。同时,本教程来自知识星球【CV技术指南】更多技术教程,可加入星球学习。

Transformer、目标检测、语义分割交流群

欢迎关注公众号CV技术指南,专注于计算机视觉的技术总结、最新技术跟踪、经典论文解读、CV招聘信息。

CV各大方向专栏与各个部署框架最全教程整理

FCN结构

FCN基于传统的卷积神经网络 (CNN),但做了一些特定的改进,使其可以用于像素级别的语义分割任务。

也许大家还记得,在上一期中我们提到过FCN的网络结构:

网络结构

在蓝色箭头部分,实际上对图像进行了“卷积-BN-非线性”的集合操作,并在池化后让图像尺寸变小;

在红色线部分,执行的是上采样操作(在pytorch官方实现中采用的是双线性插值);

在绿色矩形框部分,通过元素相加的方式对不同位置的特征图进行了融合;

通俗来说,我们先经过一系列的操作,对输入图像进行压缩,再经过另一系列操作对图像进行解压。

于是,压缩图像的那一系列结构我们就称之为编码器,而解压图像的另一系列结构我们称之为解码器

网络结构

通过上图可以看到,最后我们可以得到1/32尺寸的heatmap,1/16尺寸的featuremap和1/8尺寸的featuremap,将1/32尺寸的heatmap进行上采样到原始尺寸,这种模型叫做FCN-32s。这种简单粗暴的方法还原了conv5中的特征,但是其中一些细节是无法恢复的,所以FCN-32s精度很差,不能够很好地还原图像原来的特征。

基于上述原因,所以自然而然的就想到将浅层网络提取的特征和深层特征相融合,这样或许能够更好地恢复其中的细节信息。于是FCN把conv4中的特征对conv7进行2倍上采样之后的特征图进行融合,然后这时候特征图的尺寸为原始图像的1/16,所以再上采样16倍就可以得到原始图像大小的特征图,这种模型叫做FCN-16s。

为了进一步恢复特征细节信息,就重复以上操作。于是乎就把pool3后的特征图对conv7上采样4倍后的特征图和对pool4进行上采样2倍的特征图进行融合,此时的特征图的大小为原始图像的1/8。融合之后再上采样8倍,就可以得到原始图像大小的特征图了,这种模型叫做FCN-8s。

代码实现

  1. backbone部分

class VGG(nn.Module):
    def __init__(self, pretrained=True):
        super(VGG, self).__init__()

        # conv1 1/2
        self.conv1_1 = nn.Conv2d(3, 64, kernel_size=3, padding=1)
        self.relu1_1 = nn.ReLU(inplace=True)
        self.conv1_2 = nn.Conv2d(64, 64, kernel_size=3, padding=1)
        self.relu1_2 = nn.ReLU(inplace=True)
        self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)

        # conv2 1/4
        self.conv2_1 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
        self.relu2_1 = nn.ReLU(inplace=True)
        self.conv2_2 = nn.Conv2d(128, 128, kernel_size=3, padding=1)
        self.relu2_2 = nn.ReLU(inplace=True)
        self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)

        # conv3 1/8
        self.conv3_1 = nn.Conv2d(128, 256, kernel_size=3, padding=1)
        self.relu3_1 = nn.ReLU(inplace=True)
        self.conv3_2 = nn.Conv2d(256, 256, kernel_size=3, padding=1)
        self.relu3_2 = nn.ReLU(inplace=True)
        self.conv3_3 = nn.Conv2d(256, 256, kernel_size=3, padding=1)
        self.relu3_3 = nn.ReLU(inplace=True)
        self.pool3 = nn.MaxPool2d(kernel_size=2, stride=2)

        # conv4 1/16
        self.conv4_1 = nn.Conv2d(256, 512, kernel_size=3, padding=1)
        self.relu4_1 = nn.ReLU(inplace=True)
        self.conv4_2 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.relu4_2 = nn.ReLU(inplace=True)
        self.conv4_3 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.relu4_3 = nn.ReLU(inplace=True)
        self.pool4 = nn.MaxPool2d(kernel_size=2, stride=2)

        # conv5 1/32
        self.conv5_1 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.relu5_1 = nn.ReLU(inplace=True)
        self.conv5_2 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.relu5_2 = nn.ReLU(inplace=True)
        self.conv5_3 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.relu5_3 = nn.ReLU(inplace=True)
        self.pool5 = nn.MaxPool2d(kernel_size=2, stride=2)
        
        # load pretrained params from torchvision.models.vgg16(pretrained=True)
        if pretrained:
            pretrained_model = vgg16(pretrained=pretrained)
            pretrained_params = pretrained_model.state_dict()
            keys = list(pretrained_params.keys())
            new_dict = {}
            for index, key in enumerate(self.state_dict().keys()):
                new_dict[key] = pretrained_params[keys[index]]
            self.load_state_dict(new_dict)

    def forward(self, x):
        x = self.relu1_1(self.conv1_1(x))
        x = self.relu1_2(self.conv1_2(x))
        x = self.pool1(x)
        pool1 = x

        x = self.relu2_1(self.conv2_1(x))
        x = self.relu2_2(self.conv2_2(x))
        x = self.pool2(x)
        pool2 = x

        x = self.relu3_1(self.conv3_1(x))
        x = self.relu3_2(self.conv3_2(x))
        x = self.relu3_3(self.conv3_3(x))
        x = self.pool3(x)
        pool3 = x

        x = self.relu4_1(self.conv4_1(x))
        x = self.relu4_2(self.conv4_2(x))
        x = self.relu4_3(self.conv4_3(x))
        x = self.pool4(x)
        pool4 = x

        x = self.relu5_1(self.conv5_1(x))
        x = self.relu5_2(self.conv5_2(x))
        x = self.relu5_3(self.conv5_3(x))
        x = self.pool5(x)
        pool5 = x

        return pool1, pool2, pool3, pool4, pool5
  1. FCN-8s部分

class FCNs(nn.Module):
    def __init__(self, num_classes, backbone="vgg"):
        super(FCNs, self).__init__()
        self.num_classes = num_classes
        if backbone == "vgg":
            self.features = VGG()

        # deconv1 1/16
        self.deconv1 = nn.ConvTranspose2d(512, 512, kernel_size=3, stride=2, padding=1, output_padding=1)
        self.bn1 = nn.BatchNorm2d(512)
        self.relu1 = nn.ReLU()

        # deconv1 1/8
        self.deconv2 = nn.ConvTranspose2d(512, 256, kernel_size=3, stride=2, padding=1, output_padding=1)
        self.bn2 = nn.BatchNorm2d(256)
        self.relu2 = nn.ReLU()

        # deconv1 1/4
        self.deconv3 = nn.ConvTranspose2d(256, 128, kernel_size=3, stride=2, padding=1, output_padding=1)
        self.bn3 = nn.BatchNorm2d(128)
        self.relu3 = nn.ReLU()

        # deconv1 1/2
        self.deconv4 = nn.ConvTranspose2d(128, 64, kernel_size=3, stride=2, padding=1, output_padding=1)
        self.bn4 = nn.BatchNorm2d(64)
        self.relu4 = nn.ReLU()

        # deconv1 1/1
        self.deconv5 = nn.ConvTranspose2d(64, 32, kernel_size=3, stride=2, padding=1, output_padding=1)
        self.bn5 = nn.BatchNorm2d(32)
        self.relu5 = nn.ReLU()

        self.classifier = nn.Conv2d(32, num_classes, kernel_size=1)

    def forward(self, x):
        features = self.features(x)

        y = self.bn1(self.relu1(self.deconv1(features[4])) + features[3])

        y = self.bn2(self.relu2(self.deconv2(y)) + features[2])

        y = self.bn3(self.relu3(self.deconv3(y)))

        y = self.bn4(self.relu4(self.deconv4(y)))

        y = self.bn5(self.relu5(self.deconv5(y)))

        y = self.classifier(y)

        return y

FCN的编码器解码器结构

在 FCN 中,编码器是由多个卷积层构成的,用来提取输入图像的特征信息。由于卷积层会逐渐减小图像的大小,因此经过多次卷积之后,得到的特征图大小会变小,也就是下采样。这个过程可以看做是对图像信息的压缩和抽象,类似于将图像转换为一些更高层次的特征表示。

接下来,解码器负责将编码器输出的低分辨率特征图还原为原始图像大小的高分辨率特征图。解码器通过一系列反卷积(也称为上采样)层进行图像的放大,逐步恢复出原始图像中的更细节的信息。这个过程可以看做是将抽象的特征还原为原始图像的过程。

现在,你已经大概知道什么是__编码器解码器__结构了。

编码器-解码器

它是计算机视觉领域中的一种基本网络结构,这种先压缩,再解压的思想,在后续的很多结构中都会出现,在语义分割领域,这就是祖师爷般的存在~ 再提一句,一些很优秀的网络(如 U-Net、SegNet 等),都是在在编码器-解码器结构的基础上进行了更细致的设计和改进得到的

优缺点

其实说到上面,核心内容已经讲完啦~ 这个结构的基本思想就是先压缩,再解压,其他的内容就在其他网络中慢慢领悟吧~ 下面来看一下非常非常简单的优缺点分析:

优点:

能够提取高层次的特征信息:编码器通过卷积层等操作,能够有效地提取图像的高层次特征信息,这些信息对于图像分类、目标检测、语义分割等任务非常重要。
能够还原图像细节信息:解码器通过上采样、反卷积等操作,能够将编码器输出的低分辨率特征图还原为原始图像大小的高分辨率特征图,从而恢复出原始图像中的细节信息。
结构简单:编码器-解码器结构是一种相对简单的网络结构,易于理解和实现,并且具有较好的可解释性。

缺点:

由于多次卷积和池化操作,编码器-解码器结构会使得图像信息逐渐丢失,导致一些细节信息无法恢复,例如边缘和纹理等。
在解码器中使用反卷积和上采样等操作,容易引起信息的混叠和失真,导致图像质量下降。
编码器-解码器结构的训练需要大量的数据和计算资源,模型参数较多,训练过程较为困难。

编码器-解码器总结

编码器-解码器(encoder-decoder)是语义分割领域中最重要的一种结构,它的核心思想就是先压缩,再解压,后续提到的绝大部分网络采用的都是这种原始的结构。在此基础之上,我们可以改变上采样方式,在论文中常常叫做微调编码器;也可以改变上采样方式,在论文中常常叫做微调解码器。通过这两种方式,我们就能创造出很多很多的种样式的神经网络~

欢迎关注公众号CV技术指南,专注于计算机视觉的技术总结、最新技术跟踪、经典论文解读、CV招聘信息。

【技术文档】《从零搭建pytorch模型教程》122页PDF下载

QQ交流群:470899183。群内有大佬负责解答大家的日常学习、科研、代码问题。

模型部署交流群:732145323。用于计算机视觉方面的模型部署、高性能计算、优化加速、技术学习等方面的交流。

其它文章

上线一天,4k star | Facebook:Segment Anything

3090单卡5小时,每个人都能训练专属ChatGPT,港科大开源LMFlow

Efficient-HRNet | EfficientNet思想+HRNet技术会不会更强更快呢?

实践教程|GPU 利用率低常见原因分析及优化

ICLR 2023 | SoftMatch: 实现半监督学习中伪标签的质量和数量的trade-off

目标检测创新:一种基于区域的半监督方法,部分标签即可(附原论文下载)

CNN的反击!InceptionNeXt: 当 Inception 遇上 ConvNeXt

神经网络的可解释性分析:14种归因算法

无痛涨点:目标检测优化的实用Trick

详解PyTorch编译并调用自定义CUDA算子的三种方式

深度学习训练模型时,GPU显存不够怎么办?

CV各大方向专栏与各个部署框架最全教程整理

计算机视觉入门1v3辅导班

计算机视觉各个方向交流群

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值