IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 人工智能 -> FCN论文与实现代码详解 -> 正文阅读

[人工智能]FCN论文与实现代码详解

为了促进自己技术的提升,鄙人打算开一个语义分割系列,讲语义分割中经典的网络论文与具体实现,并应用于一些数据集中。FCN是我第一篇语义分割文章,在此做个记录。

本文假设读者已具备基本的图像分类知识

前言

FCN网络,出自论文Fully Convolutional Networks for Semantic Segmentation,是语义分割的开山之作。2012年,AlexNet凭借在ImageNet比赛中获取冠军,开启了使用深度学习解决计算机视觉问题的时代。

2015年,大家还在研究图像分类,ImageNet 2012年冠军AlexNet,2014年冠军GoogLeNet,2014年亚军VGG,2015年冠军ResNet。而在大家都热衷于研究图像分类这种热潮下,FCN作者能够独创新网络结构用于语义分割,实属不易,而FCN的思想也深深影响了后续语义分割的研究者。

目前,在google scholar上看到,FCN的引用数已达3w+,可见影响之深。

论文简述

论文Introduction开篇就提到卷积神经网络在识别中有着天然的优势,不仅在提升了图像分类的准确度,同时在具有结构化输出的局部任务中攻城略地(如object detection目标检测,keypoint prediction关键点预测...)

接下来最重要的一句话来了!!!

The natural next step in the progression from coarse to fine inference is to make a prediction at every pixel.

这句话什么意思呢?简而言之,“那么很自然地就想到,下一步就是:对每一个像素做预测”,即像素级分类

我们可以这样想,图像分类是整张图的像素对一个标签进行预测,而语义分割这是整张图的像素对整张图的像素类别进行预测。

图像分类:一图->一类;语义分割:一图->一图

那作者是怎么做的呢?他将图像分类中的全连接层(fully connected layer)换成了卷积层,因此网络里都是卷积层,也是网络名字的由来(Fully Convolutional Networks,全卷积网络)

而我们知道,经过一系列卷积层和池化层后,感受野不断地增大,同时图片的尺寸不断地减小,而我们最后要求输出的结果要跟原图大小相同,我们怎么还原回原图的大小呢?(论文中说到Adding differentiable interpolation layers

这个从小图还原回大图的步骤叫作上采样,而作者在论文里提及的上采样有:

  1. 3.2节Shift-and-stitch is filter dilation的shift-and-stich方法(未采用)
  2. upsampling,采用例如双线性插值bilinear插值方法(未采用)
  3. deconvolution networks(反卷积层,实际是个卷积层,正确的名字叫转置卷积)(采用)

最终采用转置卷积的原因是什么呢?

  1. 转置卷积层的参数可学习
  2. 用转置卷积的最终效果表现好

最终,作者通过使用将全连接层转换成卷积层转置卷积作为上采样方法的FCN网络,在PASCAL VOC数据集和NYUDv2数据集上达到了state of the art(达到最高水准的)

网络结构及实现

上面说过,FCN将分类网络中的全连接层换成了卷积层,而前面这部分可复用于语义分割的网络层(卷积,池化),叫作backbone

通过backbone来提取特征,配以不同的网络层,完成不同的任务,是计算机视觉(cv)领域的常规操作。

作者经过实验,测试出VGG16作为backbone的性能最好,便在该backbone基础上进行其他实验。

FCN网络,作者提出了三个版本,分别叫做FCN-32s,FCN-16s,FCN-8s(见下图)

其中,pool5前的网络是VGG16网络。(上图中的poolx为卷积后输出的特征图,image为输入图像)

VGG网络的特征为它每个卷积层的kernel_size=3,stride=1,padding=1(该配置下,经过一个卷积层,图片大小不会更改),每经过一个convx就会有一个kernel_size=2,stride=2的max pooling进行下采样将图片缩小为原来的1/2大小,这是理解FCN网络前必须掌握的知识。

因此pool1为原图1/2的大小,pool2为1/4,pool3为1/8,pool4为1/16,pool5为1/32。而conv6-7则是将VGG16中用于分类的两个全连接层换成了1x1的卷积层(1x1卷积用于改变通道数)

FCN-32s网络结构则是在conv6-7后添加一个转置卷积,而这个转置卷积的作用是将特征图(1/32大小)直接放大32倍,得到跟输入图像大小相等的特征图,从而实现语义分割。

看完FCN-32s的思路后,你或许会产生“就这?我上我也行”的想法,没错,确实就这。

一步放大32倍的效果会不会不太好?作者因此有了FCN-16s和FCN-8s的思路。

FCN-16s:在conv6-7后添加一个将特征图放大2倍的转置卷积层,因此在经过该转置卷积层后,特征图大小为1/16(对应上图中的2x conv7),随后将pool4特征图(1/16)与2x conv7合并,完成语义信息的融合,此时大小为1/16(不妨叫它fuse_pool4),最后添加一个将特征图放大16倍的转置卷积层,将特征图还原为输入图大小。

FCN-8s:与16s操作类似,得到fuse_pool4(1/16)后,让其经过一个放大2倍的转置卷积层,变为1/8大小的特征图,此时将其与pool3合并,完成语义信息融合,此时特征图大小为1/8,最后让其经过一个放大8倍的转置卷积层,还原回原图大小。

(上述pool与放大后的特征图信息融合时实际上还需要对齐通道数,但为了讲述方便,省略了)

这就是FCN三个网络结构的原理了。

转置卷积

上述一直在提及转置卷积,读者不必过于深究其原理,只要知道它的作用是上采样(将特征图尺寸放大)即可

如果想其了解原理,请看该链接

抽丝剥茧,带你理解转置卷积(反卷积)_史丹利复合田的博客-CSDN博客_转置卷积和反卷积(详细)

转置卷积(Transposed Convolution)_太阳花的小绿豆的博客-CSDN博客_转置卷积(有gif动图演示)

经过转置卷积后的尺寸大小计算公式

其中stride[0]表示高度方向的stride,padding[0]表示高度方向的padding,kernel_size[0]表示高度方向的kernel_size,索引[1]都表示宽度方向上的。

代码实现

为方便读者理解论文网络实现细节,鄙人搭建一个VGG16作为backbone的FCN网络。

此处使用paddle深度学习框架进行网络模型的搭建,以下为网络结构代码

class FCN8s(nn.Layer):
    def __init__(self, num_classes=21):
        super(FCN8s, self).__init__()
        # num_classes要包含背景,如果是PASCAL VOC则是20+1
        self.layer1 = self.make_block(num=2, in_channels=3, out_channels=64)
        self.layer2 = self.make_block(num=2, in_channels=64, out_channels=128)
        self.layer3 = self.make_block(num=3, in_channels=128, out_channels=256)
        self.layer4 = self.make_block(num=3, in_channels=256, out_channels=512)
        self.layer5 = self.make_block(num=3, in_channels=512, out_channels=512)
        # 下面的两个卷积层代替了原来VGG网络的全连接层(原本为4096,此处可根据gpu性能,设置为其他数,此处设为2048)
        mid_channels = 2048
        self.conv6 = nn.Conv2D(in_channels=512, out_channels=mid_channels, kernel_size=7, padding=3)
        self.conv7 = nn.Conv2D(in_channels=mid_channels, out_channels=mid_channels, kernel_size=1)
        
        # 3个1*1的卷积,用于改变pool的通道数,为了后续融合语义信息
        self.score32 = nn.Conv2D(in_channels=mid_channels, out_channels=num_classes, kernel_size=1)
        self.score16 = nn.Conv2D(in_channels=512, out_channels=num_classes, kernel_size=1)
        self.score8 = nn.Conv2D(in_channels=256, out_channels=num_classes, kernel_size=1)
        
        # 3个转置卷积,用于扩大特征图
        # 若参数kernel_size:stride:padding=4:2:1,此时stride为扩大倍数
        weight_8x = paddle.ParamAttr(
            initializer=paddle.nn.initializer.Assign(bilinear_kernel(num_classes, num_classes, 16))
        )
        self.up_sample8x = nn.Conv2DTranspose(
            in_channels=num_classes,
            out_channels=num_classes,
            kernel_size=16, stride=8, padding=4,
            weight_attr=weight_8x
        )
        
        weight_16x = paddle.ParamAttr(
            initializer=paddle.nn.initializer.Assign(bilinear_kernel(num_classes, num_classes, 4))
        )
        self.up_sample16x = nn.Conv2DTranspose(
            in_channels=num_classes,
            out_channels=num_classes,
            kernel_size=4, stride=2, padding=1,
            weight_attr=weight_16x
        )
        
        weight_32x = paddle.ParamAttr(
            initializer=paddle.nn.initializer.Assign(bilinear_kernel(num_classes, num_classes, 4))
        )
        self.up_sample32x = nn.Conv2DTranspose(
            in_channels=num_classes,
            out_channels=num_classes,
            kernel_size=4, stride=2, padding=1,
            weight_attr=weight_32x
        )  
        
    def make_block(self, num: int, in_channels: int, out_channels: int, padding=1):
        """根据传入的in,out和需要构建的块数搭建网络块"""
        blocks = []
        blocks.append(nn.Conv2D(in_channels=in_channels, out_channels=out_channels, kernel_size=3, padding=padding))
        blocks.append(nn.ReLU())
        for i in range(num-1):
            blocks.append(nn.Conv2D(in_channels=out_channels, out_channels=out_channels, kernel_size=3, padding=1))
            blocks.append(nn.ReLU())
        blocks.append(nn.MaxPool2D(kernel_size=2, stride=2, ceil_mode=True))
        
        return nn.Sequential(*blocks)
    
    def forward(self, inputs):
        # inputs [3, 1, 1],以原始输入图像尺寸为1
        # features
        out = self.layer1(inputs)  # [64, 1/2, 1/2]
        out = self.layer2(out)  # [128, 1/4, 1/4]
        pool3 = self.layer3(out)  # [256, 1/8, 1/8]
        pool4 = self.layer4(pool3)  # [512, 1/16, 1/16]
        pool5 = self.layer5(pool4)  # [512, 1/32, 1/32]
        x = self.conv6(pool5)  # [mid_channels, 1/32, 1/32]
        x = self.conv7(x)  # [mid_channels, 1/32, 1/32]
        score32 = self.score32(x)  # [num_classes, 1/32, 1/32]
        
        up_pool16 = self.up_sample32x(score32)  # [num_classes, 1/16, 1/16]
        score16 = self.score16(pool4)  # [num_classes, 1/16, 1/16]
        fuse_16 = paddle.add(up_pool16, score16)
        
        up_pool8 = self.up_sample16x(fuse_16)  # [num_classes, 1/8, 1/8]
        score8 = self.score8(pool3)  # [num_classes, 1/8, 1/8]
        fuse_8 = paddle.add(up_pool8, score8)
        heatmap = self.up_sample8x(fuse_8)
        
        return heatmap

其中需要注意的几个点

  1. 使用make_block函数生成VGG网络的每个块(上图中的convx)
  2. 需要为转置卷积层初始化权重参数,该参数通过bilinear_kernel函数生成。

该函数的实现如下:

def bilinear_kernel(in_channels, out_channels, kernel_size):
    factor = (kernel_size + 1) // 2
    if kernel_size % 2 == 1:
        center = factor - 1
    else:
        center = factor - 0.5
    og = np.ogrid[:kernel_size, :kernel_size]
    filt = (1 - abs(og[0] - center) / factor) * \
           (1 - abs(og[1] - center) / factor)
    weight = np.zeros((in_channels, out_channels, kernel_size, kernel_size),
                      dtype='float32')
    weight[range(in_channels), range(out_channels), :, :] = filt
    return paddle.to_tensor(weight, dtype="float32")

具体原理我并不是很清楚,但这是论文作者github源码提供的权重初始化方法。

  1. 论文作者在实现网络时,在第一个vgg block的第一个卷积层处设置了padding=100的参数(可查看作者使用caffe的源码),并因为该padding=100,后续在pool4和pool3融合过程中采用了crop将特征图大小对齐。原因是什么呢?当初作者说是为了接受任意尺寸图片的输入,但实际上没有必要,增大特征图带来的是对GPU显存的极大占用。接受任意尺寸图片(预测过程中的,训练时需要求相同尺寸图片输入)可在forward过程中进行上采样(如bilinear interpolation)进行图片尺寸的对齐。

训练时遇到的坑

  1. 需要为转置卷积层初始化权重参数,我曾经试过没初始化参数,网络会卡在32miou左右,最后越训练越低,且非常难训练)。paddle中为网络层载入初始化参数需要设置weight_attr属性。因为载入的参数是一个固定值Tensor,因此要用paddle.nn.initializer.Assign接口封装起来,再传入weight_attr属性
  2. 转置卷积的kernel_size,stride,padding参数选择问题,根据公式

放大2倍和8倍有着两种不同的方式(但放大倍数都取决于stride),读者可以代入公式进行验证计算。

  1. kernel_size=4x, stride=2x, padding=x
  2. kernel_size=2x, stride=2x

为什么最终选取了(a)方式呢?因为设置为(a)方式时网络能够收敛,且性能较好。选取(b)方式时,网络像没有初始化权重参数一样,miou卡在32miou左右,最后越训练越低

同时,又因为VGG16网络参数量大,从零训练非常难,非常考验调参功力。所以可以采用迁移学习的方式,基于预训练模型训练。

基于预训练模型训练,此处我实验了基于VGG16预训练模型为backbone的分割性能和基于ResNet34预训练模型为backbone的分割性能。

最终选定ResNet34作为backbone,参数量小,易调参,性能表现优。

optimizer采用SGD,learning_rate设置为0.03,weight_decay设置为1e-2,mean iou可达53%,鄙人最高训练到56%miou,=_=但仍未达到fcn论文中提到的65.5% miou,有较大差距。(论文作者甚至还是基于VGG16调的65.5,泪目了,调参真是门技术活

最终模型的预测效果如图所示:

下一篇文章将会带着你了解PASCAL VOC2012数据集,手把手教你读取数据集,利用paddle深度学习框架进行FCN网络的训练验证!

文章链接:读取PSACAL VOC,训练FCN全流程_Horace_01的博客-CSDN博客

  人工智能 最新文章
2022吴恩达机器学习课程——第二课(神经网
第十五章 规则学习
FixMatch: Simplifying Semi-Supervised Le
数据挖掘Java——Kmeans算法的实现
大脑皮层的分割方法
【翻译】GPT-3是如何工作的
论文笔记:TEACHTEXT: CrossModal Generaliz
python从零学(六)
详解Python 3.x 导入(import)
【答读者问27】backtrader不支持最新版本的
上一篇文章      下一篇文章      查看所有文章
加:2022-08-06 10:44:55  更:2022-08-06 10:45:00 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/29 8:35:01-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码
数据统计