作 者: 心有宝宝人自圆
声 明: 欢迎转载本文中的图片或文字,请说明出处
写在前面
自从FCN提出以来,越来越多的语义分割任务开始采用采用全卷积网络结构,随着FCN结构使用的增加,研究人员发先了其结构天生的缺陷极大的限制了分割的准确度:CNNs在high-level (large scale) tasks中取得了十分优异的成绩,这得益于局部空间不变性(主要是池化层增大了感受野,也丢弃了部分细节信息)使得网络能够学习到层次化的抽象信息,但这却恰恰不利于low-level (small scale) tasks
所以Deeplab的作者引入了如下结构来对抗这种细节丢失的问题:
- 带洞的卷积,atrous算法
- Fully connected CRF的后处理过程
1. Introduction
CNNs在high-level vision tasks(如图像分类、目标检测等)取得优异得表现,这些工作都有共同的主题:end-to-end训练的方法比人工的特征工程方法更优。这得益于CNNs内在的局部空间不变性,然而对应low-level vision tasks(语义分割)来说,我们需要准确的位置信息而非空间信息抽象后的层次化信息。
CNNs应用于low-level vision tasks主要技术障碍是:
信号的下采样
-
空间的局部不变性
下采样问题是池化层和striding的联合影响产生,其目的是为了使较小的卷积核能够去学习空间中有用的信息(因此需要增大感受野),但这种下采样必然造成信息的损失。为了在不造成信息损失的情况下增大感受野,作者使用了带洞的卷积(下均称atrous方法)
该图片来自:vdumoulin/conv_arithmetic
局部空间不变性是classifier获得以对象为中心的决策的要求,主要还是由于池化层得作用只保留了局部空间中最重要的信息,作者使用Fully connected CRF(后称DenseCRF)进行全卷积网络训练完成后的后处理,DenseCRF能够在满足长程依赖性的同时捕获细节边缘信息
2. Related Work
远古时期,分割主要基于信号系统,而后是概率模型占据主流
CNNs发展起来之后是two-stage的策略(提议区域+区域预测)占据了主流,但提议区域会使得系统面临前端分割系统的潜在错误(提议区域的质量直接能影响预测结果)
随FCN提出后one-stage策略(直接基于像素预测)占据主流
3.CNN NETWORKS FOR DENSE IMAGE LABELING
Deeplabv1就基本上是按照FCN的结构来设计的,只是部分结构进行了修改。由于网络使用了atrous算法,可以使作为encoder的CNN提取出比FCN更密集的final layer特征:FCN的encoder的final layer下采样了32倍,而Deeplabv1仅下采样了8倍
本文和FCN一样使用了预训练的VGG-16网络
3.1 AGROUS算法
语义分割是一种dense prediction任务,所以能够使用CNN提取出更密集的feature是提升准确率的关键,而基于密集feature评分成为Deeplabv1成功的关键。
为了获得更密集的feature,作者跳过了最后两个maxpooling层(maxpool4,5)的下采样,并在最后三个卷积层(conv5_1 - 3)和第一个全连接层(fc6)使用atrous算法
带洞的原意是给卷积核中间插入0,而这样的操作等同于给卷积层一个input_stride(普通的卷积默认input_stride = 1),这样就可以使卷积计算后特征图中同样的像素具有更大的感受野(还需要调整padding = input_size *( kernel_size ) // 2才能保证特征图保持输入大小一致)。这样不通过下采样获得的感受野增大不会缺失原有的信息,不像池化层那样引入了近似
最后通过评分层输出的class score maps 只需使用双线性插值上采样8倍即可
损失函数直接使用基于每个像素的Cross Entropy损失并相加,每个像素和每个类别的权重相同(大部分的类别为负类,即背景类,不会对最终分割效果产生影响)
3.2 控制感受野和加速计算
作者在这部分介绍的关于显式控制感受野和加速密集计算的的方法主要涉及全连接层的转换。在我之前介绍FCN的文章中,对于全连接层和卷积层之间的相互转换有过讲解。由于预训练模型都是针对大尺度目标的分类任务的:例如VGG-16的fc6就是有4096个大小为7x7的核,而这么大的核通常又会带来计算问题。
作者使用了对卷积核进行空间采样的方法:simple decimation(在我之前的文章介绍SSD的2.2.2节有讲解),这样就可以显式的降低感受野,并且显著的加快计算速度,节省存储空间
4.恢复细节的边界:全连接随机条件场和多尺度预测
4.1 定位的挑战
如图2所示,CNN计算出的得分图可以可靠地预测图像中物体和大致位置,但不能精确地指出它们的轮廓。CNN在分类准确和定位准确之间存在天然的trade-off:池化层提高了high-level tasks的效果,却带来了信息损失、大尺度的感受野和局部不变性,阻碍了low-level tasks的效果,因此为从得分图推断出原始空间的结果增加了难度。
在Deeplabv1提出前,有两种主流方法去解决定位的挑战:
利用来自CNN多个层的信息(比如FCN的跳跃结构)
采用超像素表示,本质上是将定位任务委托给低级的分割方法
Deeplabv1提出了使用DenseCRF进行后处理来解决定位的挑战
4.2 使用DenseCRF进行准确定位
传统上,CRFs模型是用来平滑分割图上的噪声,尤其是这些模型包含连接邻近节点的能量项(二元项),倾向于对空间近端像素进行相同标签赋值。从定性的说,这些短程CRFs(short-range CRFs)的主要功能是清除基于手工设计的局部特征构建的弱分类器的错误预测。
与弱分类器相比,Deeplab的CNN网络是很强的分类器,图2的结果也表明预测结果很平滑、有很强的同质性。在这个背景下使用短程CRFs是有害的:因为短程CRFs作用是平滑,而不是我们所期望的细化边缘
为了克服短程CRFs的限制,作者使用了全连接CRF
DenseCRF模型采用能量函数
是分配给像素的标签
其中一元项势能表示给像素i处为该标签的概率,该项基于CNN预测分数图
-
为二元项,该项基于图像本身,即考虑像素间的联系
其中,由于是全连接,每两个像素之间都要连接
是由决定的高斯核函数,为:
是像素的位置,是像素的颜色,均是高斯核的超参数
最后谈一谈关于DenseCRF的优化目标,即为调整输入(每个像素的label)使能量函数最小化。但由于是全连接,直接计算的话计算量简直爆炸,所以采用平均场近似的方法进行计算
对于DenseCRF具体内容感兴趣的可以看下有关论文,这里仅做感性的认知,并且仅使用有关pydensecrf库。在后来的语义分割论文中很少看到DenseCRF的影子了,因为对于更强的网络模型,DenseCRF的提升效果不明显,或甚至起到反作用。为什么不用DenseCRF了?
更多关于Dense的细节内容和代码中使用pydensecrf库可参考:
Fully connected CRF(Dense CRF)的python库:我在Linux使用pip install git+https://github.com/lucasb-eyer/pydensecrf.git进行安装完全没有问题,但windows不管怎么弄都安不上😭
CRF的通俗入门:从理论上易懂的介绍了概率图模型CRF
Densecrf与图像分割:内附pydensecrf package的实例,该博客通过对比实验认为当模型够强的情况下,添加crf优化没有太大必要,甚至可能起到反效果
【图像后处理】python实现全连接CRFs后处理:原理+实例,这个代码是目前来看最为清楚的
Github 项目 - Dense CRFs 之 pydensecrf 实现:一些项目中使用Dense CRF的实例
4.2 MULTI-SCALE PREDICTION
作者也像FCN的跳跃结构那样结合了多尺度的得分图,发现对于分割效果并没很多提升,更不如DenseCRF对于分割效果的提升明显,并且带来了额外的计算消耗,因此只使用了最后一层得分图
5. 实验和评价
Dataset:PASCAL VOC 2012 aug 数据集
-
Training:把CNN模型和CRF模型分开训练
先fine-tuneCNN模型,mini-batch = 20,learning rate = 0.001 (最后的分类层为0.01),每迭代2000次lr变为原来的0.1,momentum = 0.9 , weight decay = 0.0005
CNN模型训练fine-tune完成后,开始为DenseCRF调参,使用交叉验证的方法调参(val集里的100张图片):预设,寻找优超参数,平均场迭代次数固定为10
-
不同的网络设计
DeepLab:只使用了CNN
-CRF:使用了DenseCRF进行后处理
-MSc:结合了多尺度的得分图
-7x7:fc6使用的卷积核大小为7x7,默认为4x4
-LargeFOV:arbitrarily control the Field-of- View (FOV) of the models
最后选择的最优组合:DeepLab-CRF-LargeFOV,同时把最后两个classifier层的通道数从4096变为1024
我实现的代码也是按照这个来进行的
6. My code
这里主要列出网络结构和DenseCRF部分,其余部分(如Dataset,数据增广处理,训练、验证)都比较通用可用自己惯用的方法,也可参考我写的FCN主要代码和SSD主要代码
我使用了PASCAL VOC 2012数据集,而没有使用aug版,所以效果比使用aug版的差,mIOU才达到51.92%
- 网络结构:主要由两部分组成,一部分是VGG的Base结构,第二部分是更改VGG的全连接层为LargeFOV全卷积层
import torch
from torch import nn
from torchvision import models
class VggBase(nn.Module):
def __init__(self):
super(VggBase, self).__init__()
self.conv1_1 = nn.Conv2d(3, 64, kernel_size=3, padding=1)
self.conv1_2 = nn.Conv2d(64, 64, kernel_size=3, padding=1)
self.pool1 = nn.MaxPool2d(2, 2)
self.conv2_1 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
self.conv2_2 = nn.Conv2d(128, 128, kernel_size=3, padding=1)
self.pool2 = nn.MaxPool2d(2, 2)
self.conv3_1 = nn.Conv2d(128, 256, kernel_size=3, padding=1)
self.conv3_2 = nn.Conv2d(256, 256, kernel_size=3, padding=1)
self.conv3_3 = nn.Conv2d(256, 256, kernel_size=3, padding=1)
self.pool3 = nn.MaxPool2d(2, 2)
self.conv4_1 = nn.Conv2d(256, 512, kernel_size=3, padding=1)
self.conv4_2 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
self.conv4_3 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
self.relu4 = nn.ReLU(inplace=True)
# 如文中所述:skip subsampling after the last two max-pooling layers in the network
# 2×inthe last three convolutional layers and 4×in the first fully connected layer
self.pool4 = nn.MaxPool2d(3, 1, 1)
self.conv5_1 = nn.Conv2d(512, 512, kernel_size=3, padding=2, dilation=2)
self.conv5_2 = nn.Conv2d(512, 512, kernel_size=3, padding=2, dilation=2)
self.conv5_3 = nn.Conv2d(512, 512, kernel_size=3, padding=2, dilation=2)
self.pool5 = nn.MaxPool2d(3, 1, 1)
self.load_pretrained_layers()
def load_pretrained_layers(self):
pretrained_net = models.vgg16(pretrained=True)
state_dict = self.state_dict()
for c, pc in zip(state_dict.keys(), pretrained_net.features.state_dict().values()):
assert state_dict[c].size() == pc.size()
state_dict[c] = pc
self.load_state_dict(state_dict)
def forward(self, x):
out = torch.relu(self.conv1_1(x))
out = torch.relu(self.conv1_2(out))
out = self.pool1(out)
out = torch.relu(self.conv2_1(out))
out = torch.relu(self.conv2_2(out))
out = self.pool2(out)
out = torch.relu(self.conv3_1(out))
out = torch.relu(self.conv3_2(out))
out = torch.relu(self.conv3_3(out))
out = self.pool3(out)
out = torch.relu(self.conv4_1(out))
out = torch.relu(self.conv4_2(out))
out = torch.relu(self.conv4_3(out))
out = self.pool4(out)
out = torch.relu(self.conv5_1(out))
out = torch.relu(self.conv5_2(out))
out = torch.relu(self.conv5_3(out))
out = self.pool5(out)
return out
class AtrousConv(nn.Module):
def __init__(self, n_classes, out_channels=1024, decimation=3, rate=12):
# 按文中所述:employ kernel size 3×3 and input stride = 12,
# and further change the filter sizes from 4096 to 1024 for the last two layers
super(AtrousConv, self).__init__()
self.decimation = [4096 // out_channels, None, decimation, decimation]
self.atrous = nn.Conv2d(512, out_channels, kernel_size=decimation, padding=rate * (decimation - 1) // 2,
dilation=rate)
self.relu1 = nn.ReLU(inplace=True)
self.fc1 = nn.Conv2d(out_channels, out_channels, kernel_size=1)
self.relu2 = nn.ReLU(inplace=True)
self.fc2 = nn.Conv2d(out_channels, n_classes, kernel_size=1)
self.init_param()
def decimate(self, param: torch.Tensor, decimation):
assert param.dim() == len(decimation)
for d in range(param.dim()):
if decimation[d] is not None:
param = param.index_select(dim=d,
index=torch.arange(start=0, end=param.size(d),
step=decimation[d]).long())
return param
def init_param(self):
pretrained_dict = models.vgg16(pretrained=True).state_dict()
self.atrous.weight.data = self.decimate(pretrained_dict['classifier.0.weight'].view(4096, 512, 7, 7),
self.decimation)
self.atrous.bias.data = self.decimate(pretrained_dict['classifier.0.bias'].view(4096), self.decimation[:1])
self.fc1.weight.data = self.decimate(pretrained_dict['classifier.3.weight'].view(4096, 4096, 1, 1),
[self.decimation[0], self.decimation[0], None, None])
self.fc1.bias.data = self.decimate(pretrained_dict['classifier.3.bias'].view(4096), self.decimation[:1])
nn.init.xavier_normal_(self.fc2.weight)
nn.init.constant_(self.fc2.bias, 0)
def forward(self, x):
for blk in self.children():
x = blk(x)
return x
class DeepLabv1(nn.Module):
def __init__(self, n_classes=21):
super(DeepLabv1, self).__init__()
self.base = VggBase()
self.atrous = AtrousConv(n_classes)
def forward(self, x):
x = self.base(x)
x = self.atrous(x)
# https://zhuanlan.zhihu.com/p/87572724?from_voters_page=true 介绍关于align_corners的内容
# 为了和下采样(transform,PIL)时图像保持一致,使用align_corners=Fasle
# 上面的博文认为align_corners=True对语义分割任务可能更好,有机会可以试一下
# also see:https://discuss.pytorch.org/t/what-we-should-use-align-corners-false/22663/9
x = nn.functional.interpolate(x, scale_factor=8, mode='bilinear', align_corners=False)
return x
- 损失函数:等权重的Cross Entropy Loss
class NLLLoss2d(nn.Module):
def __init__(self):
super(NLLLoss2d, self).__init__()
self.loss = nn.NLLLoss()
def forward(self, pred, true):
pred = nn.functional.log_softmax(pred, dim=1)
return self.loss(pred, true)
- DenseCRF:我是在Linux完成安装pydensecrf的,windows实在装不上...
import numpy as np
import pydensecrf.densecrf as dcrf
import pydensecrf.utils as utils
class DenseCRF(object):
def __init__(self, max_epochs=5, delta_aphla=80, delta_beta=3, w1=10, delta_gamma=3, w2=3):
self.max_epochs = max_epochs
self.delta_gamma = delta_gamma
self.delta_alpha = delta_aphla
self.delta_beta = delta_beta
self.w1 = w1
self.w2 = w2
def __call__(self, image, probmap):
c, h, w = probmap.shape
U = utils.unary_from_softmax(probmap)
U = np.ascontiguousarray(U)
image = np.ascontiguousarray(image)
d = dcrf.DenseCRF2D(w, h, c)
d.setUnaryEnergy(U)
d.addPairwiseGaussian(sxy=self.delta_gamma, compat=self.w2)
d.addPairwiseBilateral(sxy=self.delta_alpha, srgb=self.delta_beta, rgbim=image, compat=self.w1)
Q = d.inference(self.max_epochs)
Q = np.array(Q).reshape((c, h, w))
return Q
- 看一下DenseCRF的使用:
import models # 写Class DenseCRF的文件
import numpy as np
def crf(self, img, prob):
"""
:param img: a PIL image
:param prob: 网络输出score map, in shape of (21 , height, width)
:return: new prob map
"""
crf = models.DenseCRF()
prob = torch.softmax(prob, dim=1)[0].numpy()
res = crf(np.array(image, dtype=np.uint8), prob)
return res.argmax(axis=0)
注意:在进行数据增广时(resize),插值的方法一定要选择NEAREAST而不是默认的Bilinear,否则会对true label image的pixel进行误标,导致问题的出现
一些衡量的Metrics见:wkentaro/pytorch-fcn,它的算法方法非常巧妙
-
分割结果
看来自行车的准确率不高啊😓
Reference
[1] Chen, L. C. , Papandreou, G. , Kokkinos, I. , Murphy, K. , & Yuille, A. L. . (2014). Semantic image segmentation with deep convolutional nets and fully connected crfs. Computer Science.
[2] kazuto1011/ deeplab-pytorch
转载请说明出处。