语义分割论文-DeepLab系列

DeepLabv1

Semantic image segmentation with deep convolutional nets and fully connected CRFs

原文地址:DeepLabv1

收录:ICLR 2015 (International Conference on Learning Representations)

代码:


Abstract

DeepLab是结合了深度卷积神经网络(DCNNs)和概率图模型(DenseCRFs)的方法。在实验中发现DCNNs做语义分割时精准度不够的问题,根本原因是DCNNs的高级特征的平移不变性(即高层次特征映射)。DeepLab解决这一问题的方法是通过将DCNNs层的响应和完全连接的条件随机场(CRF)结合。同时模型创新性的将hole(即空洞卷积)算法应用到DCNNs模型上,在现代GPU上运行速度达到了8FPS。

Introduction

相比于传统的视觉算法(SIFT或HOG),DCNN以其end-to-end方式获得了很好的效果。这样的成功部分可以归功于DCNN对图像转换的平移不变性(invariance),这根本是源于重复的池化和下采样组合层。平移不变性增强了对数据分层抽象的能力,但同时可能会阻碍低级(low-level)视觉任务,例如姿态估计、语义分割等,在这些任务中我们倾向于精确的定位而不是抽象的空间关系。

DCNN在图像标记任务中存在两个技术障碍:

  • 信号下采样
  • 空间不敏感(invariance)

第一个问题涉及到:在DCNN中重复最大池化和下采样带来的分辨率下降问题,分辨率的下降会丢失细节。DeepLab是采用的atrous(带孔)算法扩展感受野,获取更多的上下文信息。

第二个问题涉及到:分类器获取以对象中心的决策是需要空间变换的不变性,这天然的限制了DCNN的定位精度,DeepLab采用完全连接的条件随机场(DenseCRF)提高模型捕获细节的能力。

论文的主要贡献在于:

  • 速度:带atrous算法的DCNN可以保持8FPS的速度,全连接CRF平均推断需要0.5s
  • 准确:在PASCAL语义分割挑战中获得了第二的成绩
  • 简单:DeepLab是由两个非常成熟的模块(DCNN和CRFs)级联而成

DeepLab系统应用在语义分割任务上,目的是做逐像素分类的,这与使用两阶段的DCNN方法形成鲜明对比(指R-CNN等系列的目标检测工作),R-CNN系列的做法是原先图片上获取候选区域,再送到DCNN中获取分割建议,重新排列取结果。虽然这种方法明确地尝试处理前段分割算法的本质,但在仍没有明确的利用DCNN的预测图。

我们的系统与其他先进模型的主要区别在于DenseCRFs和DCNN的结合。是将每个像素视为CRF节点,利用远程依赖关系,并使用CRF推理直接优化DCNN的损失函数。Koltun(2011)的工作表明完全连接的CRF在语义分割下非常有效。

也有其他组采取非常相似的方向,将DCNN和密集的CRF结合起来,我们已经更新提出了DeepLab系统(指的是DeepLabV2)。


密集分类下的卷积神经网络

这里先描述一下DCNN如何设计,我们调整VGG16模型,转为一个可以有效提取特征的语义分割系统。具体来说,先将VGG16的FC层转为卷积层,模型变为全卷积的方式,在图像的原始分辨率上产生非常稀疏的计算检测分数(步幅32,$步幅=\frac{输入尺寸}{输出特征尺寸}$),为了以更密集(步幅8)的计算得分,我们在最后的两个最大池化层不下采样(padding到原大小),再通过2或4的采样率的空洞卷积对特征图做采样扩大感受野,缩小步幅。

空洞卷积的使用

简单介绍下空洞卷积在卷积神经网络的使用(这在DeepLabv3中有更详细的讨论)。

在1-D的情况下,我们扩大输入核元素之间的步长,如下图Input stride:
mark

如果不是很直观,看下面的在二维图像上应用空洞卷积:
mark

蓝色部分是输入:$7×7$的图像
青色部分是输出:$3×3$的图像
空洞卷积核:$3x3$ 采样率(扩展率)为2 无padding

这种带孔的采样又称atrous算法,可以稀疏的采样底层特征映射,该方法具有通常性,并且可以使用任何采样率计算密集的特征映射。在VGG16中使用不同采样率的空洞卷积,可以让模型再密集的计算时,明确控制网络的感受野。保证DCNN的预测图可靠的预测图像中物体的位置。

训练时将预训练的VGG16的权重做fine-tune,损失函数取是输出的特征图与ground truth下采样8倍做交叉熵和;测试时取输出图双线性上采样8倍得到结果。但DCNN的预测物体的位置是粗略的,没有确切的轮廓。在卷积网络中,因为有多个最大池化层和下采样的重复组合层使得模型的具有平移不变性,我们在其输出的high-level的基础上做定位是比较难的。这需要做分类精度和定位精度之间是有一个自然的折中。

解决这个问题的工作,主要分为两个方向:

  • 第一种是利用卷积网络中多个层次的信息
  • 第二种是采样超像素表示,实质上是将定位任务交给低级的分割方法

DeepLab是结合了DCNNs的识别能力和全连接的CRF的细粒度定位精度,寻求一个结合的方法,结果证明能够产生准确的语义分割结果。

mark

CRF在语义分割上的应用

传统上,CRF已被用于平滑噪声分割图。通常,这些模型包含耦合相邻节点的能量项,有利于相同标签分配空间近端像素。定性的说,这些短程的CRF主要功能是清除在手工特征基础上建立的弱分类器的虚假预测

与这些弱分类器相比,现代的DCNN体系产生质量不同的预测图,通常是比较平滑且均匀的分类结果(即以前是弱分类器预测的结果,不是很靠谱,现在DCNN的预测结果靠谱多了)。在这种情况下,使用短程的CRF可能是不利的,因为我们的目标是恢复详细的局部结构,而不是进一步平滑。而有工作证明可用全连接的CRF来提升分割精度。

CRF在语义分割上的应用:

对于每个像素位置$i$具有隐变量$x_i$(这里隐变量就是像素的真实类别标签,如果预测结果有21类,则$(i∈{1,2,..,21})$),还有对应的观测值$y_i$(即像素点对应的颜色值)。以像素为节点,像素与像素间的关系作为边,构成了一个条件随机场(CRF)。通过观测变量$y_i$来推测像素位置$i$对应的类别标签$x_i$。条件随机场示意图如下
mark

条件随机场符合吉布斯分布($x$是上面的观测值,下面省略全局观测$I$):$$P(x|I)=\frac{1}{Z}\exp(-E(x|I))$$

全连接的CRF模型使用的能量函数$E(x)$为:$$E(x)=\sum_{i}\theta_i(x_i)+\sum_{ij}\theta_{ij}(x_i,x_j)$$这分为一元势函数$\theta_i(x_i)$和二元势函数$\theta_{ij}(x_i,x_j)$两部分.

  • 一元势函数是定义在观测序列位置$i$的状态特征函数,用于刻画观测序列对标记变量的影响。在这里定义为: $$\theta_i(x_i)=-\log P(x_i)$$说白了,就是我们观测到像素点$i$的当前像素为$y_i$,则其对应为标签$x_i$的概率值(例如在城市道路任务中,观测到像素点为黑色,对应车子的可能比天空可能要大)。以前这个一元势函数是通过一些分类器完成的,现在DeepLab中有了DCNN来做像素分割,故这里$P(x_i)$是取DCNN计算关于像素$i$的输出的标签分配概率。
  • 二元势函数是定义在不同观测位置上的转移特征函数,用于刻画变量之间的相关关系以及观测序列对其影响。在这里定义为:$$\theta_{ij}(x_i,x_j)=\mu(x_i,x_j)\sum_{m=1}^K\omega_m k^m(f_i,f_j)$$

    • 其中$ if \ x_i ≠ x_j \ 则\mu(x_i,x_j)=1 ;否则 \mu(x_i,x_j)=0 $。因为是全连接,所有每个像素对都会有值.
      • $k^m(f_i,f_j)$是$(f_i,f_j)$之间的高斯核,$f_i$是像素$i$的特征向量,例如像素点$i$的特征向量$f_i$用$(x,y,r,g,b)$表示。对应的权重为$\omega_m$
      • 在DeepLab中高斯核采用双边位置和颜色组合,式为$$\omega_1\exp-\frac{||p_i-p_j||^2}{2\sigma_{\alpha}^2}-\frac{||I_i-I_j||^2}{2\sigma_{\beta}^2})+\omega_2\exp(-\frac{||p_i-p_j||^2}{2\sigma_{\gamma}^2})$$ 第一核取决于像素位置($p$)和像素颜色强度($I$),第二个核取决于像素位置($p$).

    说白了,二元势函数是描述像素和像素之间的关系,如果比较相似,那可能是一类,否则就裂开,这可以细化边缘。一般的二元势函数只取像素点与周围像素之间的边,这里使用的是全连接,即像素点与其他所有像素之间的关系。

这个公式看起来是很麻烦的,实际上计算时分解近似的平均场然后再计算,感兴趣可参考对应论文.

多尺度预测

论文还探讨了使用多尺度预测提高边界定位效果。具体的,在输入图像和前四个最大池化层的输出上附加了两层的MLP(第一层是128个$3×3$卷积,第二层是128个$1×1$卷积),最终输出的特征映射送到模型的最后一层辅助预测,合起来模型最后的softmax层输入特征多了$5×128=640$个通道,实验表示多尺度有助于提升预测结果,但是效果不如CRF明显。


Experiment

测试细节:

项目 设置
数据集 PASCAL VOC 2012 segmentation benchmark
DCNN模型 权重采用预训练的VGG16
DCNN损失函数 交叉熵
训练器 SGD,batch=20
学习率 初始为0.001,最后的分类层是0.01。每2000次迭代乘0.1
权重 0.9的动量, 0.0005的衰减

DeepLab由DCNN和CRF组成,训练策略是分段训练,即DCNN的输出是CRF的一元势函数,在训练CRF时是固定的。在对DCNN做了fine-tune后,对CRF做交叉验证。这里使用$\omega_2=3$和$\sigma_{\gamma}=3$在小的交叉验证集上寻找最佳的$\omega_1,\sigma_{\alpha},\sigma_{\beta}$,采用从粗到细的寻找策略。

CRF和多尺度的表现

在验证集上的表现:

mark

可以看到带CRF和多尺度的(MSc)的DeepLab模型效果明显上升了。

多尺度的视觉表现:

mark

第一行是普通输出,第二行是带多尺度的输出,可以看出多尺度输出细节部分要好点

离散卷积的表现

在使用离散卷积的过程中,可控制离散卷积的采样率来扩展特征感受野的范围,不同配置的参数如下:

mark

同样的实验结果:
mark

带FOV的即不同离散卷积的配置.可以看到大的离散卷积效果会好一点.

与其他模型相比

mark

与其他先进模型相比,DeepLab捕获到了更细节的边界.

Conclusion

DeepLab创造性的结合了DCNN和CRF产生一种新的语义分割模型,模型有准确的预测结果同时计算效率高。在PASCAL VOC 2012上展现了先进的水平。DeepLab是卷积神经网络和概率图模型的交集,后续可考虑将CNN和CRF结合到一起做end-to-end训练。

后续的DeepLabv2~3是DeepLabv1的升级版,进一步讨论空洞卷积和CRF的使用~




DeepLabv2

DeepLab: Semantic Image Segmentation with Deep Convolutional Nets, Atrous Convolution, and Fully Connected CRFs

原文地址:DeepLabv2

收录:TPAMI2017 (IEEE Transactions on Pattern Analysis and Machine Intelligence, 2017)
代码:

DeepLabv2可以看成是DeepLabv1的强化版,在空洞卷积和全连接的CRF使用上与DeepLabv1类似~


Abstract

本文为使用深度学习的语义分割任务,做出了三个主要贡献:

  • 首先,强调使用空洞卷积,作为密集预测任务的强大工具。空洞卷积能够明确地控制DCNN内计算特征响应的分辨率,即可以有效的扩大感受野,在不增加参数量和计算量的同时获取更多的上下文。

  • 其次,我们提出了空洞空间卷积池化金字塔(atrous spatial pyramid pooling (ASPP)),以多尺度的信息得到更强健的分割结果。ASPP并行的采用多个采样率的空洞卷积层来探测,以多个比例捕捉对象以及图像上下文。

  • 最后,通过组合DCNN和概率图模型,改进分割边界结果。在DCNN中最大池化和下采样组合实现可平移不变性,但这对精度是有影响的。通过将最终的DCNN层响应与全连接的CRF结合来克服这个问题。

论文提出的DeepLabv2在PASCAL VOC2012上表现优异,并在PASCAL-Context, PASCAL-Person-Part, and Cityscapes上都表现不错。

Introduction

DCNN(Deep Convolutional Neural Networks)将CV系统的性能推向了一个新的高度。成功的关键在于DCNN对于局部图像转换的内在不变性。这使得模型可以学习高层次的抽象表示。这种不变性带了高层次抽象表示的同时也可能妨碍诸如语义分割之类的密集预测任务,在空间信息上是不理想的。

将DCNN应用在语义分割任务上,我们着重以下三个问题:

  • 降低特征分辨率
  • 多个尺度上存在对象
  • 由于DCNN的内在不变性,定位精度底

接下来我们讨论这些问题,并着重解决这些问题。

第一个挑战是因为:DCNN连续的最大池化和下采样组合引起的空间分辨率下降,为了解决这个问题,DeepLabv2在最后几个最大池化层中去除下采样,取而代之的是使用空洞卷积,以更高的采样密度计算特征映射

第二个挑战是因为:在多尺度上存在物体。解决这一问题有一个标准方法是将一张图片缩放不同版本,汇总特征或最终预测得到结果,实验表明能提高系统的性能,但这个增加了计算特征响应,需要大量的存储空间。我们受到spatial pyramid pooling(SPP)的启发,提出了一个类似的结构,在给定的输入上以不同采样率的空洞卷积并行采样,相当于以多个比例捕捉图像的上下文,称为ASPP(atrous spatial pyramid pooling)模块

第三个挑战涉及到以下情况:对象分类要求空间变换不变性,而这影响了DCNN的空间定位精度。解决这一问题的一个做法是在计算最终分类结果时,使用跳跃层,将前面的特征融合到一起。DeepLabv2是采样全连接的CRF在增强模型捕捉细节的能力。

下面是一个DeepLab的例子:

mark

总体步骤如下:

  • 输入经过改进的DCNN(带空洞卷积和ASPP模块)得到粗略预测结果,即Aeroplane Coarse Score map
  • 通过双线性插值扩大到原本大小,即Bi-linear Interpolation
  • 再通过全连接的CRF细化预测结果,得到最终输出Final Output

总结一下,DeepLabv2的主要优点在于:

  • 速度: DCNN在现代GPU上以8FPS运行,全连接的CRF在CPU上需要0.5s
  • 准确性:在PASCAL VOC2012,PASCAL-Context, PASCALPerson-Part,Cityscapes都获得的优异的结果
  • 简单性:系统是由两个非常成熟的模块级联而成,DCNNs和CRFs

本文DeepLabv2是在DeepLabv1的基础上做了改进,基础层由VGG16换成了更先进的ResNet,添加了多尺度和ASPP模块技术得到了更好的分割结果。

DCNN应用于语义分割任务上,涉及到分类和定位细化,工作的核心是把两个任务结合起来。

基于DCNN的语义分割系统有三种大类:

  • 第一种:采样基于DCNN的自下而上的图像分割级联。将形状信息合并的分类过程中,这些方法得益于传递的形状边界信息,从而能够很好分割。但这不能从错误中恢复出来。(开始错就会一直错)

  • 第二种:依靠DCNN做密集计算得到预测结果,并将多个独立结果做耦合。其中一种是在多个分辨率下使用DCNN,使用分割树来平滑预测结果。最近有使用skip layer来级联内部的计算特征用于分类。

  • 第三种:使用DCNN直接做密集的像素级别分类。直接使用全卷积方式应用在整个图像,将DCNN后续的FC层转为卷积层,为了处理空间定位问题,使用上采样和连接中间层的特征来细化结果。

我们的工作是建立在这些工作的基础上,自从第一个版本DeepLabv1公布,许多工作采用了其中一个或两个关键要素:在DCNN的结果上使用全连接的CRF细化结果;使用空洞卷积做密集的特征提取。也有许多工作着重这两者间end-to-end的探索,将DCNN和CRF一起做联合学习。

空洞卷积能在保持计算量和参数量的同时扩大感受野,配合使用金字塔池化方案可以聚合多尺度的上下文信息,可通过空洞卷积控制特征分辨率、配合更先进的DCNN模型、多尺度联合技术、并在DCNN之上集成全连接的CRF可以获取更好的分割结果。

DCNN和CRF的组合不是新话题,以前的作品着重于应用局部CRF,这忽略像素间的长期依赖。而DeepLab采用的是全连接的CRF模型,其中高斯核可以捕获长期依赖性,从而得到较好的分割结果。


Method

空洞卷积用于密集特征提取和扩大感受野

DCNN中连续的最大池化和下采样重复组合层大大的降低了最终的feature map的空间分辨率,有一些补救方式是使用deconvolutional layer(转置卷积,用于扩大特征映射分辨率),但这需要额外的空间和计算量。 我们主张使用空洞卷积,可以以任何特征响应分辨率计算任何层的特征映射。

关于空洞卷积使用详解:

首先考虑一维信号,空洞卷积输出为$y[i]$,输入为$x[i]$,长度K的滤波器为$\omega[k]$。定义为:$$y[k]=\sum_{k=1}^Kx[i+r·k] \omega[k] $$ 输入采样的步幅为参数$r$,标准的采样率是$r=1$.如下图(a)所示:

mark

图(b)是采样率$r=2$的采样情况.

在看看在二维信号(图片)上使用空洞卷积的表现,给定一个图像:

mark

  • 上分支:首先下采样将分辨率降低2倍,做卷积。再上采样得到结果。本质上这只是在原图片的1/4内容上做卷积响应。
  • 下分支:如果我们将全分辨率图像做空洞卷积(采样率为2,核大小与上面卷积核相同),直接得到结果。这样可以计算出整张图像的响应,如上图所示,这样做效果更佳。

空洞卷积能够放大滤波器的感受野,速率$r$引入$r-1$个零,有效的将感受野从$k×k$扩展到$k_e=k+(k-1)(r-1)$,而不增加参数和计算量。在DCNN中,常见的做法是混合使用空洞卷积以高的分辨率(理解为采样密度)计算最终的DCNN网络响应。DeepLabv2中使用空洞卷积将特征的密度提升4倍,将输出的特征响应双线性插值上采样8倍恢复到原始的分辨率。

使用ASPP模块表示多尺度图像

许多工作证明使用图像的多尺度信息可以提高DCNN分割不同大小物体的精度,我们尝试了两种方法来处理语义分割中尺度变化。

  • 第一种方法是标准的多尺度处理:将放缩输入为不同版本,分别输入到DCNN中,融合得到分数图得到预测结果。这可以显著的提升预测结果,但是这也耗费了大量的计算力和空间。

  • 第二种方法是受到 SPPNet中SPP模块结构的启发。
    mark
    上图为SPPNet中SPP模块样式。
    DeepLabv2的做法与SPPNet类似,并行的采用多个采样率的空洞卷积提取特征,再将特征融合,类似于空间金字塔结构,形象的称为Atrous Spatial Pyramid Pooling (ASPP)。示意图如下:

mark

在同一Input Feature Map的基础上,并行的使用4个空洞卷积,空洞卷积配置为$r={6,12,18,24}$,核大小为$3×3$。最终将不同卷积层得到的结果做像素加融合到一起.

使用全连接CRF做结构预测用于恢复边界精度

因为最大池化和下采样组合,DCNN的高层特征具有内在不变性(这一点反复说了很多遍了~)。分类性能和定位准确性之间的折中似乎是固有的。如下图,DCNN可以预测对象存在和粗略的位置,但不能精确的划定其边界:

mark

我们将DCNN和全连接的CRF组合到一起,这在前面的DeepLabv1-CRF在语义分割上的应用中详解过了,这部分就跳过了~


Experiment

DeepLabv2在PASCAL VOC 2012, PASCAL-Context, PASCALPerson- Part, and Cityscapes四个数据集上做了评估。

测试细节:

项目 设置
DCNN模型 权重采用预训练的VGG16,ResNet101
DCNN损失函数 输出的结果与ground truth下采样8倍做像素交叉熵
训练器 SGD,batch=20
学习率 初始为0.001,最后的分类层是0.01。每2000次迭代乘0.1
权重 0.9的动量, 0.0005的衰减

模型对预训练的VGG16和ResNet101模型做fine-tune。训练时DCNN和CRF的是解耦的,即分别训练,训练CRF时DCNN输出作为CRF的一元势函数输入是固定的。

大概训练验证手段是对CRF做交叉验证。使用$\omega_2=3$和$\sigma_{\gamma}=3$在小的交叉验证集上寻找最佳的$\omega_1,\sigma_{\alpha},\sigma_{\beta}$,采用从粗到细的寻找策略。

不同卷积核大小和采样率的组合下的模型:

mark

DeepLab-LargeFOV(kernel size3×3, r = 12)取得了一个很好的平衡。可以看出小卷积核配合高采样率可以保持感受野的前提下显著减少参数量,同时加快计算速度,而且分割效果也很好。使用CRF做后端处理可以保持平均提升了3~5%的性能。

PASCAL VOC 2012

在PASCAL VOC 2012上评估了DeepLab-CRF-LargeFOV模型,这里做了三个主要的改进:

  • 1.训练期间使用不同的学习策略;
  • 2.使用ASPP模块;
  • 3.使用深度网络和多层次处理.

学习策略实验

poly学习策略:学习率计算公式为$lr_{base}*(1-\frac{iter}{max_iter})^{power}$,其中$power=0.9$。

mark

由上图结果,可以看出使用poly策略比固定的step策略效果要好。使用小批量batch_size=10(大了很吃显存,很吃硬件),训练20K得到的效果最佳。

ASPP模块实验

ASPP的结构如下图所示,
mark

并行以不同采样率的空洞卷积捕获不同大小的上下文信息。

下表报告了不同ASPP模块配置的实验结果:

mark

  • baseline的LargeFOV: 具体结构如Fig7的(a),在FC6上具有$r=12$的单分支
  • ASPP-S:具有并行四分支,空洞卷积采用较小的采样率$r={2,4,8,12}$
  • ASPP-L:具有并行四分支,空洞卷积采用较大的采样率$r={6,12,18,24}$

可以看到,使用大采样率的ASPP模块效果要突出。

使用ASPP模块的可视化结果:
mark

高采样率的ASPP更能捕获全局信息,相比之下分割更为合理。

不同深度网络和多尺度处理实验

DeepLabv2主要是在ResNet上做实验,对比了几个方法:

  • 多尺度输入:以比例${ 0.5,0.75,1}$将输入送到DCNN,融合结果
  • 在MS-COCO上预训练模型
  • 训练期间随机缩放(0.5到1.5)输入图片做数据增强

模型处理方法的影响结果:

mark

多尺度带来了2.55%的提升。多种技巧融合得到了77.69%的结果。

使用CRF后端处理的可视化效果:

mark

CRF细化了分割结果,恢复一些错分的像素,同时也明确了一部分分割边界。

DeepLabv2与其他先进模型相比:

mark

效果那自然是很好的~

对比了ResNet101和VGG16做基础层对比:

mark

采用ResNet101做基础层显著的提升了模型性能,基于ResNet101的DeepLab能够比VGG16更好的沿边界分割。

PASCAL-Context

在PASCAL-Context上基于VGG16和ResNet101不同变体的模型与其他先进模型的对比结果:

mark

采样多种技巧将最终结果提升到了45.7%。

可视化结果如下:

mark

可以看到将更好的沿边界分割了。

PASCAL-Person-Part

在PASCAL-Person-Part数据集上更关注于ResNet101模型,与其他模型对比结果:

mark

可视化结果如下:
mark

Cityscapes

因为Cityscapes的数据分辨率较大,故先做下采样2倍,同时使用的各种技巧得到不错的实验结果:

mark

可视化结果如下:
mark

失败案例

论文给出了一些训练失败的案例,如下图:

mark

模型丢失了很多细节,并在CRF后丢失现象更严重了。

Conclusion

DeepLabv2将空洞卷积应用到密集的特征提取,进一步的提出了空洞卷积金字塔池化结构、并将DCNN和CRF融合用于细化分割结果。实验表明,DeepLabv2在多个数据集上表现优异,有着不错的分割性能。


代码分析

代码参考的是github-TensorFlow版本注意这个代码没有实现CRF部分。

这里模型使用的框架和前面的笔记ICNet代码框架相同,可参考初期的NetWork设置详解。

DeepLab_ResNet结构

关于装饰器等定义在NetWork.py中了,这里就不赘述。主要看DeepLab_ResNet模型定义

前面主要是ResNet101的变体结构定义:

ResNet的前体

Reset基础层有两个常见的残差模块的变体:

mark

  • 左边是普通的残差单元: 辅分支通道直接恒等映射,主分支的前两个卷积都降通道数了,第三个卷积扩大回原通道数。这可以保持分割结果的同时大幅度减少计算量。
  • 右边是特殊的残差单元:功能是增通道,即辅和主分支都增加通道;功能是增通道降采样,即辅分支卷积采用2步长,主分支第一个卷积采用2步长;功能包括空洞卷积的,将原$3×3$的普通卷积替换为不同采样率的空洞卷积即可。

ResNet部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
from kaffe.tensorflow import Network
import tensorflow as tf
class DeepLabResNetModel(Network):
def setup(self, is_training, num_classes):
'''Network definition.
Args:
is_training: whether to update the running mean and variance of the batch normalisation layer.
If the batch size is small, it is better to keep the running mean and variance of
the-pretrained model frozen.
num_classes: number of classes to predict (including background).
'''
(self.feed('data')
.conv(7, 7, 64, 2, 2, biased=False, relu=False, name='conv1')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn_conv1')
.max_pool(3, 3, 2, 2, name='pool1')
.conv(1, 1, 256, 1, 1, biased=False, relu=False, name='res2a_branch1')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn2a_branch1'))
(self.feed('pool1')
.conv(1, 1, 64, 1, 1, biased=False, relu=False, name='res2a_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn2a_branch2a')
.conv(3, 3, 64, 1, 1, biased=False, relu=False, name='res2a_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn2a_branch2b')
.conv(1, 1, 256, 1, 1, biased=False, relu=False, name='res2a_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn2a_branch2c'))
(self.feed('bn2a_branch1',
'bn2a_branch2c')
.add(name='res2a')
.relu(name='res2a_relu')
.conv(1, 1, 64, 1, 1, biased=False, relu=False, name='res2b_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn2b_branch2a')
.conv(3, 3, 64, 1, 1, biased=False, relu=False, name='res2b_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn2b_branch2b')
.conv(1, 1, 256, 1, 1, biased=False, relu=False, name='res2b_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn2b_branch2c'))
(self.feed('res2a_relu',
'bn2b_branch2c')
.add(name='res2b')
.relu(name='res2b_relu')
.conv(1, 1, 64, 1, 1, biased=False, relu=False, name='res2c_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn2c_branch2a')
.conv(3, 3, 64, 1, 1, biased=False, relu=False, name='res2c_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn2c_branch2b')
.conv(1, 1, 256, 1, 1, biased=False, relu=False, name='res2c_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn2c_branch2c'))
(self.feed('res2b_relu',
'bn2c_branch2c')
.add(name='res2c')
.relu(name='res2c_relu')
.conv(1, 1, 512, 2, 2, biased=False, relu=False, name='res3a_branch1')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn3a_branch1'))
(self.feed('res2c_relu')
.conv(1, 1, 128, 2, 2, biased=False, relu=False, name='res3a_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn3a_branch2a')
.conv(3, 3, 128, 1, 1, biased=False, relu=False, name='res3a_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn3a_branch2b')
.conv(1, 1, 512, 1, 1, biased=False, relu=False, name='res3a_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn3a_branch2c'))
(self.feed('bn3a_branch1',
'bn3a_branch2c')
.add(name='res3a')
.relu(name='res3a_relu')
.conv(1, 1, 128, 1, 1, biased=False, relu=False, name='res3b1_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn3b1_branch2a')
.conv(3, 3, 128, 1, 1, biased=False, relu=False, name='res3b1_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn3b1_branch2b')
.conv(1, 1, 512, 1, 1, biased=False, relu=False, name='res3b1_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn3b1_branch2c'))
(self.feed('res3a_relu',
'bn3b1_branch2c')
.add(name='res3b1')
.relu(name='res3b1_relu')
.conv(1, 1, 128, 1, 1, biased=False, relu=False, name='res3b2_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn3b2_branch2a')
.conv(3, 3, 128, 1, 1, biased=False, relu=False, name='res3b2_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn3b2_branch2b')
.conv(1, 1, 512, 1, 1, biased=False, relu=False, name='res3b2_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn3b2_branch2c'))
(self.feed('res3b1_relu',
'bn3b2_branch2c')
.add(name='res3b2')
.relu(name='res3b2_relu')
.conv(1, 1, 128, 1, 1, biased=False, relu=False, name='res3b3_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn3b3_branch2a')
.conv(3, 3, 128, 1, 1, biased=False, relu=False, name='res3b3_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn3b3_branch2b')
.conv(1, 1, 512, 1, 1, biased=False, relu=False, name='res3b3_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn3b3_branch2c'))
(self.feed('res3b2_relu',
'bn3b3_branch2c')
.add(name='res3b3')
.relu(name='res3b3_relu')
.conv(1, 1, 1024, 1, 1, biased=False, relu=False, name='res4a_branch1')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn4a_branch1'))

上面一段代码示意图如下:

mark

总结一下,假设原输入为$(1024,1024,3)$

  • 先卷积+池化操作,提取特征映射pool1为$(256,256,64)$
  • 经过一个增加通道的残差模块,得到res2a_relu为$(256,256,256)$,后面再跟两个普通的残差模块,得到res2c_relu
  • 再接一个升通道降采样的残差模块,得到res3a_relu为$(128,128,512)$,后面接三个普通的残差模块,得到res3b3_relu

这是ResNet变体初期的实现,到res3b3_relu输出的特征映射步幅已经为$\frac{1024}{128}=8$了,后面要配合带空洞卷积的残差模块了~

ResNet的变体部分

DeepLabv2中使用的ResNet变体的前面部分与原ResNet模型基本一致,下面部分就是主要的改进部分了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
'''与上面代码有重复 '''
(self.feed('res3b2_relu',
'bn3b3_branch2c')
.add(name='res3b3')
.relu(name='res3b3_relu')
.conv(1, 1, 1024, 1, 1, biased=False, relu=False, name='res4a_branch1')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn4a_branch1'))
(self.feed('res3b3_relu')
.conv(1, 1, 256, 1, 1, biased=False, relu=False, name='res4a_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4a_branch2a')
.atrous_conv(3, 3, 256, 2, padding='SAME', biased=False, relu=False, name='res4a_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4a_branch2b')
.conv(1, 1, 1024, 1, 1, biased=False, relu=False, name='res4a_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn4a_branch2c'))
(self.feed('bn4a_branch1',
'bn4a_branch2c')
.add(name='res4a')
.relu(name='res4a_relu')
.conv(1, 1, 256, 1, 1, biased=False, relu=False, name='res4b1_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b1_branch2a')
.atrous_conv(3, 3, 256, 2, padding='SAME', biased=False, relu=False, name='res4b1_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b1_branch2b')
.conv(1, 1, 1024, 1, 1, biased=False, relu=False, name='res4b1_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn4b1_branch2c'))
(self.feed('res4a_relu',
'bn4b1_branch2c')
.add(name='res4b1')
.relu(name='res4b1_relu')
.conv(1, 1, 256, 1, 1, biased=False, relu=False, name='res4b2_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b2_branch2a')
.atrous_conv(3, 3, 256, 2, padding='SAME', biased=False, relu=False, name='res4b2_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b2_branch2b')
.conv(1, 1, 1024, 1, 1, biased=False, relu=False, name='res4b2_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn4b2_branch2c'))
(self.feed('res4b1_relu',
'bn4b2_branch2c')
.add(name='res4b2')
.relu(name='res4b2_relu')
.conv(1, 1, 256, 1, 1, biased=False, relu=False, name='res4b3_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b3_branch2a')
.atrous_conv(3, 3, 256, 2, padding='SAME', biased=False, relu=False, name='res4b3_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b3_branch2b')
.conv(1, 1, 1024, 1, 1, biased=False, relu=False, name='res4b3_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn4b3_branch2c'))
(self.feed('res4b2_relu',
'bn4b3_branch2c')
.add(name='res4b3')
.relu(name='res4b3_relu')
.conv(1, 1, 256, 1, 1, biased=False, relu=False, name='res4b4_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b4_branch2a')
.atrous_conv(3, 3, 256, 2, padding='SAME', biased=False, relu=False, name='res4b4_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b4_branch2b')
.conv(1, 1, 1024, 1, 1, biased=False, relu=False, name='res4b4_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn4b4_branch2c'))
(self.feed('res4b3_relu',
'bn4b4_branch2c')
.add(name='res4b4')
.relu(name='res4b4_relu')
.conv(1, 1, 256, 1, 1, biased=False, relu=False, name='res4b5_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b5_branch2a')
.atrous_conv(3, 3, 256, 2, padding='SAME', biased=False, relu=False, name='res4b5_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b5_branch2b')
.conv(1, 1, 1024, 1, 1, biased=False, relu=False, name='res4b5_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn4b5_branch2c'))
(self.feed('res4b4_relu',
'bn4b5_branch2c')
.add(name='res4b5')
.relu(name='res4b5_relu')
.conv(1, 1, 256, 1, 1, biased=False, relu=False, name='res4b6_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b6_branch2a')
.atrous_conv(3, 3, 256, 2, padding='SAME', biased=False, relu=False, name='res4b6_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b6_branch2b')
.conv(1, 1, 1024, 1, 1, biased=False, relu=False, name='res4b6_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn4b6_branch2c'))
(self.feed('res4b5_relu',
'bn4b6_branch2c')
.add(name='res4b6')
.relu(name='res4b6_relu')
.conv(1, 1, 256, 1, 1, biased=False, relu=False, name='res4b7_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b7_branch2a')
.atrous_conv(3, 3, 256, 2, padding='SAME', biased=False, relu=False, name='res4b7_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b7_branch2b')
.conv(1, 1, 1024, 1, 1, biased=False, relu=False, name='res4b7_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn4b7_branch2c'))
(self.feed('res4b6_relu',
'bn4b7_branch2c')
.add(name='res4b7')
.relu(name='res4b7_relu')
.conv(1, 1, 256, 1, 1, biased=False, relu=False, name='res4b8_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b8_branch2a')
.atrous_conv(3, 3, 256, 2, padding='SAME', biased=False, relu=False, name='res4b8_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b8_branch2b')
.conv(1, 1, 1024, 1, 1, biased=False, relu=False, name='res4b8_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn4b8_branch2c'))
(self.feed('res4b7_relu',
'bn4b8_branch2c')
.add(name='res4b8')
.relu(name='res4b8_relu')
.conv(1, 1, 256, 1, 1, biased=False, relu=False, name='res4b9_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b9_branch2a')
.atrous_conv(3, 3, 256, 2, padding='SAME', biased=False, relu=False, name='res4b9_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b9_branch2b')
.conv(1, 1, 1024, 1, 1, biased=False, relu=False, name='res4b9_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn4b9_branch2c'))
(self.feed('res4b8_relu',
'bn4b9_branch2c')
.add(name='res4b9')
.relu(name='res4b9_relu')
.conv(1, 1, 256, 1, 1, biased=False, relu=False, name='res4b10_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b10_branch2a')
.atrous_conv(3, 3, 256, 2, padding='SAME', biased=False, relu=False, name='res4b10_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b10_branch2b')
.conv(1, 1, 1024, 1, 1, biased=False, relu=False, name='res4b10_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn4b10_branch2c'))
(self.feed('res4b9_relu',
'bn4b10_branch2c')
.add(name='res4b10')
.relu(name='res4b10_relu')
.conv(1, 1, 256, 1, 1, biased=False, relu=False, name='res4b11_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b11_branch2a')
.atrous_conv(3, 3, 256, 2, padding='SAME', biased=False, relu=False, name='res4b11_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b11_branch2b')
.conv(1, 1, 1024, 1, 1, biased=False, relu=False, name='res4b11_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn4b11_branch2c'))
(self.feed('res4b10_relu',
'bn4b11_branch2c')
.add(name='res4b11')
.relu(name='res4b11_relu')
.conv(1, 1, 256, 1, 1, biased=False, relu=False, name='res4b12_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b12_branch2a')
.atrous_conv(3, 3, 256, 2, padding='SAME', biased=False, relu=False, name='res4b12_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b12_branch2b')
.conv(1, 1, 1024, 1, 1, biased=False, relu=False, name='res4b12_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn4b12_branch2c'))
(self.feed('res4b11_relu',
'bn4b12_branch2c')
.add(name='res4b12')
.relu(name='res4b12_relu')
.conv(1, 1, 256, 1, 1, biased=False, relu=False, name='res4b13_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b13_branch2a')
.atrous_conv(3, 3, 256, 2, padding='SAME', biased=False, relu=False, name='res4b13_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b13_branch2b')
.conv(1, 1, 1024, 1, 1, biased=False, relu=False, name='res4b13_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn4b13_branch2c'))
(self.feed('res4b12_relu',
'bn4b13_branch2c')
.add(name='res4b13')
.relu(name='res4b13_relu')
.conv(1, 1, 256, 1, 1, biased=False, relu=False, name='res4b14_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b14_branch2a')
.atrous_conv(3, 3, 256, 2, padding='SAME', biased=False, relu=False, name='res4b14_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b14_branch2b')
.conv(1, 1, 1024, 1, 1, biased=False, relu=False, name='res4b14_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn4b14_branch2c'))
(self.feed('res4b13_relu',
'bn4b14_branch2c')
.add(name='res4b14')
.relu(name='res4b14_relu')
.conv(1, 1, 256, 1, 1, biased=False, relu=False, name='res4b15_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b15_branch2a')
.atrous_conv(3, 3, 256, 2, padding='SAME', biased=False, relu=False, name='res4b15_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b15_branch2b')
.conv(1, 1, 1024, 1, 1, biased=False, relu=False, name='res4b15_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn4b15_branch2c'))
(self.feed('res4b14_relu',
'bn4b15_branch2c')
.add(name='res4b15')
.relu(name='res4b15_relu')
.conv(1, 1, 256, 1, 1, biased=False, relu=False, name='res4b16_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b16_branch2a')
.atrous_conv(3, 3, 256, 2, padding='SAME', biased=False, relu=False, name='res4b16_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b16_branch2b')
.conv(1, 1, 1024, 1, 1, biased=False, relu=False, name='res4b16_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn4b16_branch2c'))
(self.feed('res4b15_relu',
'bn4b16_branch2c')
.add(name='res4b16')
.relu(name='res4b16_relu')
.conv(1, 1, 256, 1, 1, biased=False, relu=False, name='res4b17_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b17_branch2a')
.atrous_conv(3, 3, 256, 2, padding='SAME', biased=False, relu=False, name='res4b17_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b17_branch2b')
.conv(1, 1, 1024, 1, 1, biased=False, relu=False, name='res4b17_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn4b17_branch2c'))
(self.feed('res4b16_relu',
'bn4b17_branch2c')
.add(name='res4b17')
.relu(name='res4b17_relu')
.conv(1, 1, 256, 1, 1, biased=False, relu=False, name='res4b18_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b18_branch2a')
.atrous_conv(3, 3, 256, 2, padding='SAME', biased=False, relu=False, name='res4b18_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b18_branch2b')
.conv(1, 1, 1024, 1, 1, biased=False, relu=False, name='res4b18_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn4b18_branch2c'))
(self.feed('res4b17_relu',
'bn4b18_branch2c')
.add(name='res4b18')
.relu(name='res4b18_relu')
.conv(1, 1, 256, 1, 1, biased=False, relu=False, name='res4b19_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b19_branch2a')
.atrous_conv(3, 3, 256, 2, padding='SAME', biased=False, relu=False, name='res4b19_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b19_branch2b')
.conv(1, 1, 1024, 1, 1, biased=False, relu=False, name='res4b19_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn4b19_branch2c'))
(self.feed('res4b18_relu',
'bn4b19_branch2c')
.add(name='res4b19')
.relu(name='res4b19_relu')
.conv(1, 1, 256, 1, 1, biased=False, relu=False, name='res4b20_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b20_branch2a')
.atrous_conv(3, 3, 256, 2, padding='SAME', biased=False, relu=False, name='res4b20_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b20_branch2b')
.conv(1, 1, 1024, 1, 1, biased=False, relu=False, name='res4b20_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn4b20_branch2c'))
(self.feed('res4b19_relu',
'bn4b20_branch2c')
.add(name='res4b20')
.relu(name='res4b20_relu')
.conv(1, 1, 256, 1, 1, biased=False, relu=False, name='res4b21_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b21_branch2a')
.atrous_conv(3, 3, 256, 2, padding='SAME', biased=False, relu=False, name='res4b21_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b21_branch2b')
.conv(1, 1, 1024, 1, 1, biased=False, relu=False, name='res4b21_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn4b21_branch2c'))
(self.feed('res4b20_relu',
'bn4b21_branch2c')
.add(name='res4b21')
.relu(name='res4b21_relu')
.conv(1, 1, 256, 1, 1, biased=False, relu=False, name='res4b22_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b22_branch2a')
.atrous_conv(3, 3, 256, 2, padding='SAME', biased=False, relu=False, name='res4b22_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn4b22_branch2b')
.conv(1, 1, 1024, 1, 1, biased=False, relu=False, name='res4b22_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn4b22_branch2c'))
(self.feed('res4b21_relu',
'bn4b22_branch2c')
.add(name='res4b22')
.relu(name='res4b22_relu')
.conv(1, 1, 2048, 1, 1, biased=False, relu=False, name='res5a_branch1')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn5a_branch1'))
(self.feed('res4b22_relu')
.conv(1, 1, 512, 1, 1, biased=False, relu=False, name='res5a_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn5a_branch2a')
.atrous_conv(3, 3, 512, 4, padding='SAME', biased=False, relu=False, name='res5a_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn5a_branch2b')
.conv(1, 1, 2048, 1, 1, biased=False, relu=False, name='res5a_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn5a_branch2c'))
(self.feed('bn5a_branch1',
'bn5a_branch2c')
.add(name='res5a')
.relu(name='res5a_relu')
.conv(1, 1, 512, 1, 1, biased=False, relu=False, name='res5b_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn5b_branch2a')
.atrous_conv(3, 3, 512, 4, padding='SAME', biased=False, relu=False, name='res5b_branch2b')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn5b_branch2b')
.conv(1, 1, 2048, 1, 1, biased=False, relu=False, name='res5b_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn5b_branch2c'))
(self.feed('res5a_relu',
'bn5b_branch2c')
.add(name='res5b')
.relu(name='res5b_relu')
.conv(1, 1, 512, 1, 1, biased=False, relu=False, name='res5c_branch2a')
.batch_normalization(is_training=is_training, activation_fn=tf.nn.relu, name='bn5c_branch2a')
.atrous_conv(3, 3, 512, 4, padding='SAME', biased=False, relu=False, name='res5c_branch2b')
.batch_normalization(activation_fn=tf.nn.relu, name='bn5c_branch2b', is_training=is_training)
.conv(1, 1, 2048, 1, 1, biased=False, relu=False, name='res5c_branch2c')
.batch_normalization(is_training=is_training, activation_fn=None, name='bn5c_branch2c'))
(self.feed('res5b_relu',
'bn5c_branch2c')
.add(name='res5c')
.relu(name='res5c_relu')
.atrous_conv(3, 3, num_classes, 6, padding='SAME', relu=False, name='fc1_voc12_c0'))

上面一段代码示意图如下:

mark

总结一下,输入res3b3_relu为$(128,128,512)$

  • 经过升通道带空洞卷积(r=2)的残差模块,得到res4a_relu为$(128,128,1024)$
  • 再经过22个带空洞卷积(r=2)的残差模块,得到res4b_relu为$(128,128,1024)$
  • 再接一个升通道带空洞卷积(r=4)的残差模块,得到res5a_relu为$(128,128,2048)$
  • 后面接两个带空洞卷积(r=4)的的残差模块,得到res5c_relu为$(128,128,2048)$

这是ResNet变体后部分的实现,到res5c_relu输出的特征映射步幅虽然是$\frac{1024}{128}=8$了,但是因为空洞卷积的使用,感受野扩大了很多,到这里,ResNet的部分算是结束了,下面就是ASPP模块了~

ASPP模块

DeepLabv2的ASPP和SPP模块很相似,主要就是在同一输入特征上应用不同采样率的空洞卷积,将结果融合到一起即可~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(self.feed('res5b_relu',
'bn5c_branch2c')
.add(name='res5c')
.relu(name='res5c_relu')
.atrous_conv(3, 3, num_classes, 6, padding='SAME', relu=False, name='fc1_voc12_c0'))
(self.feed('res5c_relu')
.atrous_conv(3, 3, num_classes, 12, padding='SAME', relu=False, name='fc1_voc12_c1'))
(self.feed('res5c_relu')
.atrous_conv(3, 3, num_classes, 18, padding='SAME', relu=False, name='fc1_voc12_c2'))
(self.feed('res5c_relu')
.atrous_conv(3, 3, num_classes, 24, padding='SAME', relu=False, name='fc1_voc12_c3'))
(self.feed('fc1_voc12_c0',
'fc1_voc12_c1',
'fc1_voc12_c2',
'fc1_voc12_c3')
.add(name='fc1_voc12'))

上面一段代码示意图如下:

mark

总结一下,输入res5c_relu为$(128,128,2048)$

  • 并行经过空洞卷积层,卷积核为$(3,3,num_class)$,取$num_class=21$
  • 4个并行空洞卷积采样率为$r=6,12,18,24$
  • 得到的输出作像素加,得到最终输出fc1_voc12为$(128,128,21)$

这是ASPP模块的实现.

总得来说DeepLabv2中关于DCNN模型的实验还是很容易理解的(起码比前面的ICNet看起来简单多了),关于CRF部分,现在TensorFlow1.4中有contrib的CRF,有兴趣的可以实验一下~




DeepLabv3

Rethinking Atrous Convolution for Semantic Image Segmentation

原文地址:DeepLabv3

代码:


Abstract

DeepLabv3进一步探讨空洞卷积,这是一个在语义分割任务中:可以调整滤波器视野、控制卷积神经网络计算的特征响应分辨率的强大工具。为了解决多尺度下的目标分割问题,我们设计了空洞卷积级联或不同采样率空洞卷积并行架构。此外,我们强调了ASPP(Atrous Spatial Pyramid Pooling)模块,该模块可以在获取多个尺度上卷积特征,进一步提升性能。同时,我们分享了实施细节和训练方法,此次提出的DeepLabv3相比先前的版本有显著的效果提升,在PASCAL VOC 2012上获得了先进的性能。

Introduction

对于语义分割任务,在应用深度卷积神经网络中的有两个挑战:

  • 第一个挑战:连续池化和下采样,让高层特征具有局部图像变换的内在不变性,这允许DCNN学习越来越抽象的特征表示。但同时引起的特征分辨率下降,会妨碍密集的定位预测任务,因为这需要详细的空间信息。DeepLabv3系列解决这一问题的办法是使用空洞卷积(前两个版本会使用CRF细化分割结果),这允许我们可以保持参数量和计算量的同时提升计算特征响应的分辨率,从而获得更多的上下文。
  • 第二个挑战:多尺度目标的存在。现有多种处理多尺度目标的方法,我们主要考虑4种,如下图:
    mark

    • a. Image Pyramid: 将输入图片放缩成不同比例,分别应用在DCNN上,将预测结果融合得到最终输出
    • b. Encoder-Decoder: 利用Encoder阶段的多尺度特征,运用到Decoder阶段上恢复空间分辨率(代表工作有FCN、SegNet、PSPNet等工作)
    • c. Deeper w. Atrous Convolution: 在原始模型的顶端增加额外的模块,例如DenseCRF,捕捉像素间长距离信息
    • d. Spatial Pyramid Pooling: 空间金字塔池化具有不同采样率和多种视野的卷积核,能够以多尺度捕捉对象

DeepLabv3的主要贡献在于:

  • 本文重新讨论了空洞卷积的使用,这让我们在级联模块和空间金字塔池化的框架下,能够获取更大的感受野从而获取多尺度信息。

  • 改进了ASPP模块:由不同采样率的空洞卷积和BN层组成,我们尝试以级联或并行的方式布局模块。

  • 讨论了一个重要问题:使用大采样率的$3×3$的空洞卷积,因为图像边界响应无法捕捉远距离信息,会退化为1×1的卷积, 我们建议将图像级特征融合到ASPP模块中。

  • 阐述了训练细节并分享了训练经验,论文提出的”DeepLabv3”改进了以前的工作,获得了很好的结果

现有多个工作表明全局特征或上下文之间的互相作用有助于做语义分割,我们讨论四种不同类型利用上下文信息做语义分割的全卷积网络。

  • 图像金字塔(Image pyramid): 通常使用共享权重的模型,适用于多尺度的输入。小尺度的输入响应控制语义,大尺寸的输入响应控制细节。通过拉布拉斯金字塔对输入变换成多尺度,传入DCNN,融合输出。这类的缺点是:因为GPU存储器的限制,对于更大/更深的模型不方便扩展。通常应用于推断阶段

  • 编码器-解码器(Encoder-decoder): 编码器的高层次的特征容易捕获更长的距离信息,在解码器阶段使用编码器阶段的信息帮助恢复目标的细节和空间维度。例如SegNet利用下采样的池化索引作为上采样的指导;U-Net增加了编码器部分的特征跳跃连接到解码器;RefineNet等证明了Encoder-Decoder结构的有效性。

  • 上下文模块(Context module):包含了额外的模块用于级联编码长距离的上下文。一种有效的方法是DenseCRF并入DCNN中,共同训练DCNN和CRF。

  • 空间金字塔池化(Spatial pyramid pooling):采用空间金字塔池化可以捕捉多个层次的上下文。在ParseNet中从不同图像等级的特征中获取上下文信息;DeepLabv2提出ASPP,以不同采样率的并行空洞卷积捕捉多尺度信息。最近PSPNet在不同网格尺度上执行空间池化,并在多个数据集上获得优异的表现。还有其他基于LSTM方法聚合全局信息。

我们的工作主要探讨空洞卷积作为上下文模块和一个空间金字塔池化的工具,这适用于任何网络。具体来说,我们取ResNet最后一个block,复制多个级联起来,送入到ASPP模块后。我们通过实验发现使用BN层有利于训练过程,为了进一步捕获全局上下文,我们建议在ASPP上融入图像级特征.


Method

空洞卷积应用于密集的特征提取

这在DeepLabv1和DeepLabv2都已经讲过,这里不详解了~

深层次的空洞卷积

我们首先探讨将空洞卷积应用在级联模块。具体来说,我们取ResNet中最后一个block,在下图中为block4,并在其后面增加级联模块。

mark

  • 上图(a)所示,整体图片的信息总结到后面非常小的特征映射上,但实验证明这是不利于语义分割的。如下图:
    mark
    使用步幅越长的特征映射,得到的结果反倒会差,结果最好的out_stride = 8 需要占用较多的存储空间。因为连续的下采样会降低特征映射的分辨率,细节信息被抽取,这对语义分割是有害的。

  • 上图(b)所示,可使用不同采样率的空洞卷积保持输出步幅的为out_stride = 16.这样不增加参数量和计算量同时有效的缩小了步幅。

### Atrous Spatial Pyramid Pooling

对于在DeepLabv2中提出的ASPP模块,其在特征顶部映射图并行使用了四种不同采样率的空洞卷积。这表明以不同尺度采样是有效的,我们在DeepLabv3中向ASPP中添加了BN层。不同采样率的空洞卷积可以有效的捕获多尺度信息,但是,我们发现随着采样率的增加,滤波器的有效权重(权重有效的应用在特征区域,而不是填充0)逐渐变小。如下图所示:

mark

当我们不同采样率的$3×3$卷积核应用在$65×65$的特征映射上,当采样率接近特征映射大小时,$3×3$的滤波器不是捕捉全图像的上下文,而是退化为简单的$1×1$滤波器,只有滤波器中心点的权重起了作用。

为了克服这个问题,我们考虑使用图片级特征。具体来说,我们在模型最后的特征映射上应用全局平均,将结果经过$1×1$的卷积,再双线性上采样得到所需的空间维度。最终,我们改进的ASPP包括:

  • 一个$1×1$卷积和三个$3×3$的采样率为$rates = {6,12,18}$的空洞卷积,滤波器数量为256,包含BN层。针对output_stride=16的情况。如下图(a)部分Atrous Spatial Pyramid Pooling
  • 图像级特征,即将特征做全局平均池化,经过卷积,再融合。如下图(b)部分Image Pooling.

改进后的ASPP模块如下图所示:
mark

注意当output_stride=8时,加倍了采样率。所有的特征通过$1×1$级联到一起,生成最终的分数.

Experiment

采用的是预训练的ResNet为基础层,并配合使用了空洞卷积控制输出步幅。因为输出步幅output_stride(定义为输入图像的分辨率与最终输出分辨率的比值)。当我们输出步幅为8时,原ResNet的最后两个block包含的空洞卷积的采样率为$r=2$和$r=4$。

模型的训练设置:

部分 设置
数据集 PASCAL VOC 2012
工具 TensorFlow
裁剪尺寸 采样513大小的裁剪尺寸
学习率策略 采用poly策略, 在初始学习率基础上,乘以$(1-\frac{iter}{max_iter})^{power}$,其中$power=0.9$
BN层策略 当output_stride=16时,我们采用batchsize=16,同时BN层的参数做参数衰减0.9997。
在增强的数据集上,以初始学习率0.007训练30K后,冻结BN层参数。
采用output_stride=8时,再使用初始学习率0.001训练30K。
训练output_stride=16比output_stride=8要快很多,因为中间的特征映射在空间上小的四倍。但因为output_stride=16在特征映射上粗糙是牺牲了精度。
上采样策略 在先前的工作上,
我们是将最终的输出与GroundTruth下采样8倍做比较
现在我们发现保持GroundTruth更重要,故我们是将最终的输出上采样8倍与完整的GroundTruth比较。

Going Deeper with Atrous Convolution实验

我们首先试试级联使用多个带空洞卷积的block模块。

  • ResNet50:如下图,我们探究输出步幅的影响,当输出步幅为256时,由于严重的信号抽取,性能大大的下降了。
    mark
    而当我们使用不同采样率的空洞卷积,结果大大的上升了,这表现在语义分割中使用空洞卷积的必要性。

  • ResNet50 vs. ResNet101: 用更深的模型,并改变级联模块的数量。如下图,当block增加性能也随之增加。
    mark

  • Multi-grid: 我们使用的变体残差模块,采用Multi-gird策略,即主分支的三个卷积都使用空洞卷积,采样率设置Multi-gird策略。按照如下图:
    mark

    • 应用不同策略通常比单倍数$(r_1,r_2,r_3)=(1,1,1)$效果要好
    • 简单的提升倍数是无效的$(r_1,r_2,r_3)=(2,2,2)$
    • 最好的随着网络的深入提升性能.即block7下$(r_1,r_2,r_3)=(1,2,1)$
  • Inference strategy on val set
    推断期间使用output_stride = 8,有着更丰富的细节内容:
    mark

Atrous Spatial Pyramid Pooling实验

  • ASPP模块相比以前增加了BN层,对比multi-grid策略和图片层级特征提升实验结果:

mark

  • Inference strategy on val set
    推断期间使用output_stride = 8,有着更丰富的细节内容,采用多尺度输入和翻转,性能进一步提升了:
    mark

在PASCAL VOC 2012上表现:

mark

Cityscapes表现

多种技巧配置结果:

mark

与其他模型相比:
mark

其他参数的影响

  • 上采样策略和裁剪大小和BN层的影响:
    mark
  • 不同batchsize的影响:
    mark
  • 不同评估步幅的影响:
    mark

Conclusion

DeepLabv3重点探讨了空洞卷积的使用,同时改进了ASPP模块,便于更好的捕捉多尺度上下文。


代码分析

因为没找到官方的代码,在github上找了一个DeepLabV3-TensorFlow版本.

训练脚本分析

先找到train_voc12.py训练文件。

找到关键的main方法:

创建训练模型 & 计算loss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
def main():
"""创建模型 and 准备训练."""
h = args.input_size
w = args.input_size
input_size = (h, w)
# 设置随机种子
tf.set_random_seed(args.random_seed)
# 创建线程队列,准备数据
coord = tf.train.Coordinator()
# 读取数据
image_batch, label_batch = read_data(is_training=True)
# 创建训练模型
net, end_points = deeplabv3(image_batch,
num_classes=args.num_classes,
depth=args.num_layers,
is_training=True,
)
# 对于小的batchsize,保持BN layers的统计参数更佳(即冻结预训练模型的BN参数)
# If is_training=True, 统计参数在训练期间会被更新
# 注意的是:即使is_training=False ,BN参数gamma (scale) and beta (offset) 也会更新
# 取出模型预测值
raw_output = end_points['resnet{}/logits'.format(args.num_layers)]
# Which variables to load. Running means and variances are not trainable,
# thus all_variables() should be restored.
restore_var = [v for v in tf.global_variables() if 'fc' not in v.name
or not args.not_restore_last]
if args.freeze_bn:
all_trainable = [v for v in tf.trainable_variables() if 'beta' not in
v.name and 'gamma' not in v.name]
else:
all_trainable = [v for v in tf.trainable_variables()]
conv_trainable = [v for v in all_trainable if 'fc' not in v.name]
# 上采样logits输出,取代ground truth下采样
raw_output_up = tf.image.resize_bilinear(raw_output, [h, w]) # 双线性插值放大到原大小
# Predictions: 忽略标签中大于或等于n_classes的值
label_proc = tf.squeeze(label_batch) # 删除数据标签tensor的shape中维度值为1
mask = label_proc <= args.num_classes # 忽略标签中大于或等于n_classes的值
seg_logits = tf.boolean_mask(raw_output_up, mask) #取出预测值中感兴趣的mask
seg_gt = tf.boolean_mask(label_proc, mask) # 取出数据标签中标注的mask(感兴趣的mask)
seg_gt = tf.cast(seg_gt, tf.int32) # 转换一下数据类型
# 逐像素做softmax loss.
loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=seg_logits,
labels=seg_gt)
seg_loss = tf.reduce_mean(loss)
seg_loss_sum = tf.summary.scalar('loss/seg', seg_loss) # TensorBoard记录
# 增加正则化损失
reg_losses = tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES)
reg_loss = tf.add_n(reg_losses)
reg_loss_sum = tf.summary.scalar('loss/reg', reg_loss)
tot_loss = seg_loss + reg_loss
tot_loss_sum = tf.summary.scalar('loss/tot', tot_loss)
seg_pred = tf.argmax(seg_logits, axis=1)
# 计算MIOU
train_mean_iou, train_update_mean_iou = streaming_mean_iou(seg_pred,
seg_gt, args.num_classes, name="train_iou")
train_iou_sum = tf.summary.scalar('accuracy/train_mean_iou',
train_mean_iou)

关于streaming_mean_iou方法代码见metric_ops.py,该方法用于计算每步的平均交叉点(mIOU),即先计算每个类别的IOU,再平均到各个类上。

IOU的计算定义如下:$$IOU = \frac{true_positive}{true_positive+false_positive+false_negative}$$该方法返回一个update_op操作用于估计数据流上的度量,更新变量并返回mean_iou.

上面代码初始化了DeepLabv3模型,并取出模型输出,计算了loss,并计算了mIOU.

训练参数设置

这里学习率没有使用poly策略,该github说学习率设置0.00001效果更好点~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 初始化训练参数
train_initializer = tf.variables_initializer(var_list=tf.get_collection(
tf.GraphKeys.LOCAL_VARIABLES, scope="train_iou"))
# 定义 loss and 优化参数.
# 这里学习率没采用poly策略
base_lr = tf.constant(args.learning_rate)
step_ph = tf.placeholder(dtype=tf.float32, shape=())
# learning_rate = tf.scalar_mul(base_lr,
# tf.pow((1 - step_ph / args.num_steps), args.power))
learning_rate = base_lr
lr_sum = tf.summary.scalar('params/learning_rate', learning_rate)
train_sum_op = tf.summary.merge([seg_loss_sum, reg_loss_sum,
tot_loss_sum, train_iou_sum, lr_sum])

创建交叉验证模型,并设置输出值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 交叉验证模型
image_batch_val, label_batch_val = read_data(is_training=False)
_, end_points_val = deeplabv3(image_batch_val,
num_classes=args.num_classes,
depth=args.num_layers,
reuse=True,
is_training=False,
)
raw_output_val = end_points_val['resnet{}/logits'.format(args.num_layers)] # 交叉验证输出
nh, nw = tf.shape(image_batch_val)[1], tf.shape(image_batch_val)[2]
seg_logits_val = tf.image.resize_bilinear(raw_output_val, [nh, nw])
seg_pred_val = tf.argmax(seg_logits_val, axis=3)
seg_pred_val = tf.expand_dims(seg_pred_val, 3)
seg_pred_val = tf.reshape(seg_pred_val, [-1,])
seg_gt_val = tf.cast(label_batch_val, tf.int32)
seg_gt_val = tf.reshape(seg_gt_val, [-1,])
mask_val = seg_gt_val <= args.num_classes - 1
seg_pred_val = tf.boolean_mask(seg_pred_val, mask_val)
seg_gt_val = tf.boolean_mask(seg_gt_val, mask_val)
val_mean_iou, val_update_mean_iou = streaming_mean_iou(seg_pred_val,
seg_gt_val, num_classes=args.num_classes, name="val_iou")
val_iou_sum = tf.summary.scalar('accuracy/val_mean_iou', val_mean_iou)

训练模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
val_initializer = tf.variables_initializer(var_list=tf.get_collection(
tf.GraphKeys.LOCAL_VARIABLES, scope="val_iou"))
test_sum_op = tf.summary.merge([val_iou_sum])
global_step = tf.train.get_or_create_global_step()
opt = tf.train.MomentumOptimizer(learning_rate, args.momentum)
grads_conv = tf.gradients(tot_loss, conv_trainable)
# train_op = opt.apply_gradients(zip(grads_conv, conv_trainable))
train_op = slim.learning.create_train_op(
tot_loss, opt,
global_step=global_step,
variables_to_train=conv_trainable,
summarize_gradients=True)
# Set up tf session and initialize variables.
config = tf.ConfigProto()
config.gpu_options.allow_growth = True
sess = tf.Session(config=config)
sess.run(tf.global_variables_initializer())
sess.run(tf.local_variables_initializer())
# Saver for storing checkpoints of the model.
saver = tf.train.Saver(var_list=tf.global_variables(), max_to_keep=20)
# 如果有checkpoint则加载
if args.ckpt > 0 or args.restore_from is not None:
loader = tf.train.Saver(var_list=restore_var)
load(loader, sess, args.snapshot_dir)
# 开始线程队列
threads = tf.train.start_queue_runners(coord=coord, sess=sess)
# tf.get_default_graph().finalize()
summary_writer = tf.summary.FileWriter(args.snapshot_dir,
sess.graph)
# 迭代训练
for step in range(args.ckpt, args.num_steps):
start_time = time.time()
feed_dict = { step_ph : step }
tot_loss_float, seg_loss_float, reg_loss_float, _, lr_float, _,train_summary = sess.run([tot_loss, seg_loss, reg_loss, train_op,
learning_rate, train_update_mean_iou, train_sum_op],
feed_dict=feed_dict)
train_mean_iou_float = sess.run(train_mean_iou)
duration = time.time() - start_time
sys.stdout.write('step {:d}, tot_loss = {:.6f}, seg_loss = {:.6f}, ' \
'reg_loss = {:.6f}, mean_iou = {:.6f}, lr: {:.6f}({:.3f}' \
'sec/step)\n'.format(step, tot_loss_float, seg_loss_float,
reg_loss_float, train_mean_iou_float, lr_float, duration)
)
sys.stdout.flush()
if step % args.save_pred_every == 0 and step > args.ckpt:
summary_writer.add_summary(train_summary, step)
sess.run(val_initializer)
for val_step in range(NUM_VAL-1):
_, test_summary = sess.run([val_update_mean_iou, test_sum_op],
feed_dict=feed_dict)
summary_writer.add_summary(test_summary, step)
val_mean_iou_float= sess.run(val_mean_iou)
save(saver, sess, args.snapshot_dir, step)
sys.stdout.write('step {:d}, train_mean_iou: {:.6f}, ' \
'val_mean_iou: {:.6f}\n'.format(step, train_mean_iou_float,
val_mean_iou_float))
sys.stdout.flush()
sess.run(train_initializer)
if coord.should_stop():
coord.request_stop()
coord.join(threads)

模型分析

上面看完了训练脚本,下面看看DeepLabv3的模型定义脚本libs.nets.deeplabv3.py.

deeplabv3中ResNet变体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
def deeplabv3(inputs,
num_classes,
depth=50,
aspp=True,
reuse=None,
is_training=True):
"""DeepLabV3
Args:
inputs: A tensor of size [batch, height, width, channels].
depth: ResNet的深度 一般为101或51.
aspp: 是否使用ASPP module, if True, 使用4 blocks with multi_grid=(1,2,4), if False, 使用7 blocks with multi_grid=(1,2,1).
reuse: 模型参数重用(验证会重用训练的模型参数)
Returns:
net: A rank-4 tensor of size [batch, height_out, width_out, channels_out].
end_points: 模型的组合
"""
if aspp:
multi_grid = (1,2,4)
else:
multi_grid = (1,2,1)
scope ='resnet{}'.format(depth)
with tf.variable_scope(scope, [inputs], reuse=reuse) as sc:
end_points_collection = sc.name + '_end_points'
with slim.arg_scope(resnet_arg_scope(weight_decay=args.weight_decay,
batch_norm_decay=args.bn_weight_decay)):
with slim.arg_scope([slim.conv2d, bottleneck, bottleneck_hdc],
outputs_collections=end_points_collection):
with slim.arg_scope([slim.batch_norm], is_training=is_training):
net = inputs
net = resnet_utils.conv2d_same(net, 64, 7, stride=2, scope='conv1')
net = slim.max_pool2d(net, [3, 3], stride=2, scope='pool1')
with tf.variable_scope('block1', [net]) as sc:
base_depth = 64
for i in range(2):
with tf.variable_scope('unit_%d' % (i + 1), values=[net]):
net = bottleneck(net, depth=base_depth * 4,
depth_bottleneck=base_depth, stride=1)
with tf.variable_scope('unit_3', values=[net]):
net = bottleneck(net, depth=base_depth * 4,
depth_bottleneck=base_depth, stride=2)
net = slim.utils.collect_named_outputs(end_points_collection,
sc.name, net)
with tf.variable_scope('block2', [net]) as sc:
base_depth = 128
for i in range(3):
with tf.variable_scope('unit_%d' % (i + 1), values=[net]):
net = bottleneck(net, depth=base_depth * 4,
depth_bottleneck=base_depth, stride=1)
with tf.variable_scope('unit_4', values=[net]):
net = bottleneck(net, depth=base_depth * 4,
depth_bottleneck=base_depth, stride=2)
net = slim.utils.collect_named_outputs(end_points_collection,
sc.name, net)
with tf.variable_scope('block3', [net]) as sc:
base_depth = 256
num_units = 6
if depth == 101:
num_units = 23
elif depth == 152:
num_units = 36
for i in range(num_units):
with tf.variable_scope('unit_%d' % (i + 1), values=[net]):
net = bottleneck(net, depth=base_depth * 4,
depth_bottleneck=base_depth, stride=1)
net = slim.utils.collect_named_outputs(end_points_collection,
sc.name, net)
with tf.variable_scope('block4', [net]) as sc:
base_depth = 512
for i in range(3):
with tf.variable_scope('unit_%d' % (i + 1), values=[net]):
net = bottleneck_hdc(net, depth=base_depth * 4,
depth_bottleneck=base_depth, stride=1, rate=2,
multi_grid=multi_grid)
net = slim.utils.collect_named_outputs(end_points_collection,
sc.name, net)

这部分实现的变体的ResNet结构,包括带mutli-grid的残差模块由libs.nets.deeplabv3.py中的bottleneck_hdc方法提供。

带mutli-grid策略的bottleneck_hdc残差结构代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@slim.add_arg_scope
def bottleneck_hdc(inputs,
depth,
depth_bottleneck,
stride,
rate=1,
multi_grid=(1,2,4),
outputs_collections=None,
scope=None,
use_bounded_activations=False):
"""Hybrid Dilated Convolution Bottleneck.
Multi_Grid = (1,2,4)
See Understanding Convolution for Semantic Segmentation.
When putting together two consecutive ResNet blocks that use this unit, one
should use stride = 2 in the last unit of the first block.
Args:
inputs: A tensor of size [batch, height, width, channels].
depth: The depth of the ResNet unit output.
depth_bottleneck: The depth of the bottleneck layers.
stride: The ResNet unit's stride. Determines the amount of downsampling of
the units output compared to its input.
rate: An integer, rate for atrous convolution.
multi_grid: multi_grid sturcture.
outputs_collections: Collection to add the ResNet unit output.
scope: Optional variable_scope.
use_bounded_activations: Whether or not to use bounded activations. Bounded
activations better lend themselves to quantized inference.
Returns:
The ResNet unit's output.
"""
with tf.variable_scope(scope, 'bottleneck_v1', [inputs]) as sc:
depth_in = slim.utils.last_dimension(inputs.get_shape(), min_rank=4)
# 是否降采样
if depth == depth_in:
shortcut = resnet_utils.subsample(inputs, stride, 'shortcut')
else:
shortcut = slim.conv2d(
inputs,
depth, [1, 1],
stride=stride,
activation_fn=tf.nn.relu6 if use_bounded_activations else None,
scope='shortcut')
# 残差结构的主分支
residual = slim.conv2d(inputs, depth_bottleneck, [1, 1], stride=1,
rate=rate*multi_grid[0], scope='conv1')
residual = resnet_utils.conv2d_same(residual, depth_bottleneck, 3, stride,
rate=rate*multi_grid[1], scope='conv2')
residual = slim.conv2d(residual, depth, [1, 1], stride=1,
rate=rate*multi_grid[2], activation_fn=None, scope='conv3')
# 是否后接激活函数
if use_bounded_activations:
# Use clip_by_value to simulate bandpass activation.
residual = tf.clip_by_value(residual, -6.0, 6.0)
output = tf.nn.relu6(shortcut + residual)
else:
output = tf.nn.relu(shortcut + residual)
return slim.utils.collect_named_outputs(outputs_collections,
sc.name,
output)

下面是关于aspp模块和后期的空洞卷积策略使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
if aspp:
with tf.variable_scope('aspp', [net]) as sc:
aspp_list = []
branch_1 = slim.conv2d(net, 256, [1,1], stride=1,
scope='1x1conv')
branch_1 = slim.utils.collect_named_outputs(
end_points_collection, sc.name, branch_1)
aspp_list.append(branch_1)
for i in range(3):
branch_2 = slim.conv2d(net, 256, [3,3], stride=1, rate=6*(i+1), scope='rate{}'.format(6*(i+1)))
branch_2 = slim.utils.collect_named_outputs(end_points_collection, sc.name, branch_2)
aspp_list.append(branch_2)
aspp = tf.add_n(aspp_list)
aspp = slim.utils.collect_named_outputs(end_points_collection, sc.name, aspp)
# 增加图像级特征,即全局平均池化
with tf.variable_scope('img_pool', [net]) as sc:
"""Image Pooling
See ParseNet: Looking Wider to See Better
"""
pooled = tf.reduce_mean(net, [1, 2], name='avg_pool',
keep_dims=True)
pooled = slim.utils.collect_named_outputs(end_points_collection,
sc.name, pooled)
pooled = slim.conv2d(pooled, 256, [1,1], stride=1, scope='1x1conv')
pooled = slim.utils.collect_named_outputs(end_points_collection,
sc.name, pooled)
pooled = tf.image.resize_bilinear(pooled, tf.shape(net)[1:3])
pooled = slim.utils.collect_named_outputs(end_points_collection,
sc.name, pooled)
# 将图像级特征融合到aspp中
with tf.variable_scope('fusion', [aspp, pooled]) as sc:
net = tf.concat([aspp, pooled], 3)
net = slim.utils.collect_named_outputs(end_points_collection,
sc.name, net)
net = slim.conv2d(net, 256, [1,1], stride=1, scope='1x1conv')
net = slim.utils.collect_named_outputs(end_points_collection,
sc.name, net)
# 如果不使用aspp, 则使用带mutli-grid的残差结构
else:
with tf.variable_scope('block5', [net]) as sc:
base_depth = 512
for i in range(3):
with tf.variable_scope('unit_%d' % (i + 1), values=[net]):
net = bottleneck_hdc(net, depth=base_depth * 4,
depth_bottleneck=base_depth, stride=1, rate=4)
net = slim.utils.collect_named_outputs(end_points_collection,
sc.name, net)
with tf.variable_scope('block6', [net]) as sc:
base_depth = 512
for i in range(3):
with tf.variable_scope('unit_%d' % (i + 1), values=[net]):
net = bottleneck_hdc(net, depth=base_depth * 4,
depth_bottleneck=base_depth, stride=1, rate=8)
net = slim.utils.collect_named_outputs(end_points_collection,
sc.name, net)
with tf.variable_scope('block7', [net]) as sc:
base_depth = 512
for i in range(3):
with tf.variable_scope('unit_%d' % (i + 1), values=[net]):
net = bottleneck_hdc(net, depth=base_depth * 4,
depth_bottleneck=base_depth, stride=1, rate=16)
net = slim.utils.collect_named_outputs(end_points_collection,
sc.name, net)
# 输出
with tf.variable_scope('logits',[net]) as sc:
net = slim.conv2d(net, num_classes, [1,1], stride=1,
activation_fn=None, normalizer_fn=None)
net = slim.utils.collect_named_outputs(end_points_collection,
sc.name, net)
end_points = slim.utils.convert_collection_to_dict(
end_points_collection)
return net, end_points
if __name__ == "__main__":
x = tf.placeholder(tf.float32, [None, 512, 512, 3])
net, end_points = deeplabv3(x, 21)
for i in end_points:
print(i, end_points[i])

代码本身还是很容易理解的~


到这里整个DeepLabv3就算结束了~




DeepLab系列总结

截图内容源于官方的PPT

关于DeepLabv1,DeepLabv2,DeepLabv3汇总:

DeepLab系列针对的Task

DeepLab是针对语义分割(Semantic Segmentation)任务提出的深度学习系统:

mark

语义分割的要求:

  • 语义分割是对图像做密集的分割任务,分割每个像素到指定类别上
  • 将图像分割成几个有意义的目标
  • 给对象分配指定类型标签

语义分割的用途:

  • 自动驾驶
  • 医疗辅助

mark


DeepLabv1 & DeepLabv2

  • 使用DCNN做密集的分类任务,产生的预测图有目标大概的位置,但比较粗糙
  • 使用条件随机场(CRF)细化分割结果

mark

核心解决思路

对于标准的DCNN有哪些问题? 针对这些问题,DeepLab的解决办法
1.分辨率: 输出特征分辨率较小
2.池化: 对于输入变换具有内在不变性
1.使用空洞卷积
2. 使用CRF

mark

DCNN中使用空洞卷积

mark

  • 移除原网络最后两个池化层
  • 使用$rate=2$的空洞卷积采样

如上图右下所示,标准的卷积只能获取原图1/4的内容,而新的带孔卷积可以在全图上获取信息。

DeepLabv1到DeepLabv2有一个变化:

mark

由左边到右边,主要是在DCNN中应用了空洞卷积密集的提取特征,左边的输出步幅是16,需要上采样16倍得到预测结果,可以看到结果是比较模糊的;而右边是在DCNN中使用空洞卷积,保持步幅为8,只需要上采样8倍,结果清晰了很多。

CRF部分

DCNN存在分类和定位之间的折中问题,预测到目标的大概位置但比较模糊。

mark

CRF尝试找到图像像素之间的关系: 相近且相似的像素大概率为同一标签;CRF考虑像素的概率分配标签;迭代细化结果。

模型结构介绍

DeepLabv1结构介绍

mark
DeepLabv1是在VGG16的基础上做了修改:

  • VGG16的全连接层转为卷积
  • 最后的两个池化层去掉了下采样
  • 后续卷积层的卷积核改为了空洞卷积
  • 在ImageNet上预训练的VGG16权重上做finetune

可视化结果如下:
mark

DeepLabv2结构介绍

DeepLabv2在DeepLabv1上做了改进:

mark

  • 用多尺度获得更好的分割效果(使用ASPP)
  • 基础层由VGG16转为ResNet
  • 使用不同的学习策略(poly)
ASPP模块
为什么要提出ASPP? 解决思路 实施办法
语义分割挑战:在多尺度上存储目标 在给定的特征层上使用不同采样率的卷积有效的重采样 使用不同采样率的空洞卷积并行采样

mark

ASPP中在给定的Input Feature Map上以$r=(6,12,18,24)$的$3×3$空洞卷积并行采样。

mark

ASPP各个空洞卷积分支采样后结果最后融合到一起(通道相同,做像素加),得到最终预测结果.

DeepLabv2可视化结果:

mark

DeepLabv1 & DeepLabv2优势

mark

  • 速度上: 使用空洞卷积的Dense DCNN达到8fps,全连接的CRF需要0.5s
  • 精准度:在几个先进的数据集上达到了先进的结果
  • 建议性:系统由两个成熟的模块组成,DCNNs和CRFs

DeepLabv3

相比DeepLabv1 & DeepLabv2的改变

mark

  • 提出了更通用的框架,适用于任何网络
  • 复制了ResNet最后的block,并级联起来
  • 在ASPP中使用BN层
  • 没有使用CRF

模型结构介绍

mark

  • 复制ResNet最后一个block多个副本,级联到一起
    • 在本文中,block5-7是block4的副本
  • 每个block中包含三个卷积(使用Mutli-gird策略)
  • 最后一个block的最后一个卷积步长为2(???)
  • 为了维持原图尺寸,使用不同的采样率(每层采样率乘2)空洞卷积代替原卷积

ASPP模块

mark

相比于DeepLabv2的ASPP模块,有以下变化和问题:

  • ASPP中应用了BN层
  • 随着采样率的增加,滤波器中有效的权重减少了(有效权重减少,难以捕获原距离信息,这要求合理控制采样率的设置)
  • 使用模型最后的特征映射的全局平均池化(为了克服远距离下有效权重减少的问题)

mark

新的ASPP模块包括:

  • 一个$1×1$卷积和3个$3×3$的空洞卷积(采样率为(6,12,18)),每个卷积核都有256个且都有BN层
  • 包含图像级特征(即全局平均池化)

所有分支得到的结果通过$1×1$卷积级联到一起得到最终结果。

DeepLabv3的实验结果

在PASCAL VOC 2012测试集上,相比于DeepLabv2的77.69%,DeepLabv3有2%的提升:

mark

最好的结果包含:

  • ASPP
  • 输出步幅为8
  • 翻转和随机缩放的数据增强

可视化结果:

mark

Thanks for your support!