作 者: 心有寶寶人自圓
聲 明: 歡迎轉載本文中的圖片或文字,請說明出處
寫在前面
自從FCN提出以來,越來越多的語義分割任務開始采用采用全卷積網(wǎng)絡結構,隨著FCN結構使用的增加,研究人員發(fā)先了其結構天生的缺陷極大的限制了分割的準確度:CNNs在high-level (large scale) tasks中取得了十分優(yōu)異的成績,這得益于局部空間不變性(主要是池化層增大了感受野,也丟棄了部分細節(jié)信息)使得網(wǎng)絡能夠學習到層次化的抽象信息,但這卻恰恰不利于low-level (small scale) tasks
DeepLabv2的作者延續(xù)DeepLabv1的atrous算法和denseCRF的后處理邊緣優(yōu)化,并在Spatial Pyramid Pooling結合多尺度特征圖以最大化信息保留和計算節(jié)約的特性啟發(fā)之下,設計了Atrous Spatial Pyramid Pooling(ASPP)來對抗這種細節(jié)丟失的問題,并總結為如下三個主要貢獻:
- 帶洞的卷積,atrous算法
- ASPP能夠捕捉多尺度目標的特征
- Fully connected CRF的后處理過程
1. Introduction
DCNNs在high-level vision tasks(如圖像分類、目標檢測等)取得優(yōu)異得表現(xiàn),這些工作都有共同的主題:end-to-end訓練的方法比人工的特征工程方法更優(yōu)。這得益于CNNs內(nèi)在的局部空間不變性,然而對應low-level vision tasks(語義分割)來說,我們需要準確的位置信息而非空間信息抽象后的層次化信息。
DCNNs應用于low-level vision tasks主要難點是:
- 信號的下采樣使細節(jié)信息丟失
- 網(wǎng)絡對多尺度目標的泛化能力較差
- 空間的局部不變性導致位置信息不再準確
下采樣問題是池化層和stride的聯(lián)合影響造成,是準確性和速度、空間消耗的權衡(即過大的卷積核運算極度耗時、參數(shù)過大)。其目的是為了使較小的卷積核能夠去學習空間中有用的信息(因此需要增大感受野),但這種下采樣必然造成信息的損失。為了在不造成信息損失的情況下增大感受野,作者使用了帶洞的卷積(下均稱atrous方法),由此來顯式地控制感受野的大小,atrous卷積設計的核心理念是臨近的像素攜帶的信息相似。
該圖片來自:vdumoulin/conv_arithmetic

真實的檢測目標往往是多尺度的,而我們的訓練集一般僅提供了較大尺度的目標。為了解決這一問題,主流的做法是將同一輸入圖片按不同尺度縮小輸入相同參數(shù)的網(wǎng)絡(對于縮小的圖片,同樣的kernel_size能獲得更大的感受野,間接地達到了小尺度目標地效果),再將特征圖或得分圖結合起來得到最終得得分。這樣的操作的確帶來了改進,當使訓練更加地耗時。作者受Spatial Pyramid Pooling地啟發(fā),設計出了對特定特征圖進行多尺度特征上采樣提取的高效計算方法:ASPP。
局部空間不變性是classifier獲得以對象為中心的決策的要求,主要還是由于池化層得作用只保留了局部空間中最重要的信息,作者使用Fully connected CRF(后稱DenseCRF)進行全卷積網(wǎng)絡訓練完成后的后處理,DenseCRF能夠在滿足長程依賴性的同時捕獲細節(jié)邊緣信息。
2. METHODS
文章大部分內(nèi)容和DeepLabv1十分相似,最主要的不同就是提出了ASPP。若對Atrous算法和DenseCRF算法有所需求,可以參考DeepLabv1,這里不再贅述
2.1 Multiscale Image Representations
DCNN具有從訓練集中提取復雜的抽象特征的強大能力,而這寫特征存在隱式的尺度,因此顯式地考慮輸入尺度能夠提升網(wǎng)絡模型對于各種尺度特征的泛化能力。為了解決語義分割中的多尺度目標難題,作者測試了兩種處理方法:(1)將輸入圖片進行多次降尺度傳入相同參數(shù)的網(wǎng)絡,并將最終的得分圖經(jīng)過雙線性插值后結合起來;(2)ASPP。
我們先來介紹一下Spatial Pyramid Pooling(SPP)
SPP將最后的MaxPool更改成了多尺度的AdaptiveMaxPool(為了組成固定長度向量傳給fc層),將conv5的特征圖(Vgg-16)分別傳入AdaptiveMaxPool(4), AdaptiveMaxPool(2), AdaptiveMaxPool(1)(僅是描述上圖),得到了不同尺度的特征圖。實驗結果表明能夠很好的增強網(wǎng)絡對于多尺度目標的魯棒性、提升速度、接收任意大小的輸入,此外由于AdaptiveMaxPool(1)的存在使得模型能減少過擬合。SPP雖然獲得了更大的感受野,但丟失了空間信息,與atrous應用的場景類似,作者將池化層更改為了atrous卷積層,在獲得更大感受野的同時保留了原有的空間信息;卷積層同樣擁有fc層的特性,ASPP就能代替SPP+fc。
ASPP
作者使用擁有不同采樣率(感受野)的atrous卷積層,使特征圖并行通過各個atrous卷積層提取出各個尺度的中心像素點特征圖,最后將各個尺度的特征圖結合起來得到最后得得分圖,能增強網(wǎng)絡對多尺度目標的魯棒性,提升網(wǎng)絡的性能。
2.2 訓練方案改進
2.2.1 Learning rate policy
之前作者適用的方法是“step” learning rate policy(達到2000iter的時候使,learning rate衰減為原來的0.1);在文章中作者表明“poly” learning rate policy()能夠得到更好的表現(xiàn)。
2.2.2 ASPP
ASPP是LargeFOV(具體可參考DeepLabv1的網(wǎng)絡設計部分)的泛化,作者提供了ASPP_S( atrous rate = {2, 4, 8, 12})和ASPP_L( atrous rate = {6, 12, 18, 24})兩種感受野,ASPP_L在參數(shù)更少的優(yōu)勢下,能夠得到于ASPP_S相類似的效果
2.2.3 Deeper Networks and Multiscale Processing
作者使用預訓練的ResNet-101代替Vgg-16,并采用2.1所述的將輸入圖片進行多次降尺度傳入相同參數(shù)的網(wǎng)絡的方法來獲得更好的表現(xiàn)。
3. My Code
3.1 ResNet-101 Base
import torch.nn as nn
import torch.nn.functional as F
import torch
import torchvision
torchvision.models.segmentation.deeplabv3_resnet101()
BOTTLENECK_EXPANSION = 4
class ConvBnReLUBlock(nn.Sequential):
def __init__(self, index, in_channels, out_channels, kernel_size, stride, padding, dilation, relu=True):
super(ConvBnReLUBlock, self).__init__()
# 注意conv層中均無bias
self.add_module('conv' + str(index),
nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding, dilation, bias=False))
self.add_module('bn' + str(index), nn.BatchNorm2d(out_channels, eps=1e-5, momentum=0.1))
if relu:
self.add_module("relu" + str(index), nn.ReLU(inplace=True))
class Bottlenenck(nn.Module):
def __init__(self, in_channels, out_channels, stride, dilation, downsample):
super(Bottlenenck, self).__init__()
mid_channels = out_channels // BOTTLENECK_EXPANSION
# 利用stride進行下采樣 -> f(x) - x
self.reduce = ConvBnReLUBlock(1, in_channels, mid_channels, 1, stride, 0, 1, True)
# padding = dilation * (kernel_size - 1)//2 ,保持形狀不變
self.conv3x3 = ConvBnReLUBlock(2, mid_channels, mid_channels, 3, 1, dilation, dilation, True)
# 增加通道數(shù)但形狀不變
self.increase = ConvBnReLUBlock(3, mid_channels, out_channels, 1, 1, 0, 1, False)
# 當in_channel != out_channel時先下采樣 x
self.shortcut = nn.Sequential(ConvBnReLUBlock(4, in_channels, out_channels, 1, stride, 0, 1,
False) if downsample else nn.Identity())
def forward(self, x):
out = self.reduce(x)
out = self.conv3x3(out)
out = self.increase(out)
out += self.shortcut(x)
return F.relu(out)
class ResLayer(nn.Sequential):
def __init__(self, n_layers, in_channels, out_channels, stride, dilation, multi_grads=None):
super(ResLayer, self).__init__()
if multi_grads is None:
multi_grads = [1 for _ in range(n_layers)]
else:
assert n_layers == len(multi_grads)
for i in range(n_layers):
self.add_module('block{}'.format(i + 1),
Bottlenenck(in_channels=in_channels if i == 0 else out_channels,
out_channels=out_channels,
stride=stride if i == 0 else 1, # 僅在第一個block進行對tensor下采樣
dilation=dilation * multi_grads[i],
downsample=True if i == 0 else False)) # 僅在第一個block進行下采樣 x
class Stem(nn.Sequential):
def __init__(self, out_channels):
super(Stem, self).__init__()
self.add_module('stem', ConvBnReLUBlock(1, 3, out_channels, 7, 2, 3, 1))
self.add_module('maxpool', nn.MaxPool2d(3, 2, 1, ceil_mode=False))
class ResNetBase(nn.Sequential):
def __init__(self, n_blocks=None, n_rates=None):
super(ResNetBase, self).__init__()
if n_blocks is None:
n_blocks = [3, 4, 23, 3]
else:
assert len(n_blocks) == 4
if n_rates is None:
n_rates = [1, 1, 2, 4]
else:
assert len(n_rates) == 4
chs = [64 * 2 ** k for k in range(6)]
self.add_module('layer0', Stem(chs[0])) # 下采樣x4
self.add_module('layer1', ResLayer(n_blocks[0], chs[0], chs[2], 1, n_rates[0]))
self.add_module('layer2', ResLayer(n_blocks[1], chs[2], chs[3], 2, n_rates[1])) # 下采樣x2
self.add_module('layer3', ResLayer(n_blocks[2], chs[3], chs[4], 1, n_rates[2])) # resnet-101設置stride=2,下采樣x2
self.add_module('layer4', ResLayer(n_blocks[3], chs[4], chs[5], 1, n_rates[3])) # resnet-101設置stride=2,下采樣x2
try:
self.load_pretrained_layers()
except:
print("Can not load parameters from pretrained ResNet-101.")
self.init_param()
for c in self.modules():
if isinstance(c, nn.BatchNorm2d):
for p in c.parameters():
p.requires_grad = False
def init_param(self):
for n, c in self.named_parameters():
if 'weight' in n and 'bn' not in n:
nn.init.xavier_normal_(c)
if 'bias' in n:
nn.init.constant_(c, 0)
if 'bn' in n:
nn.init.constant_(c, 1)
def load_pretrained_layers(self):
pretrained_net = torchvision.models.resnet101(pretrained=True)
pretrained_state_dict = pretrained_net.state_dict()
state_dict = self.state_dict()
for layer, weight in zip(state_dict.keys(), pretrained_state_dict.values()):
assert state_dict[layer].shape == weight.shape
state_dict[layer] = weight
self.load_state_dict(state_dict)
3.2 ASPP
class ASPP(nn.Module):
def __init__(self, in_channels, out_channels, rates):
super(ASPP, self).__init__()
for i, rate in enumerate(rates):
self.add_module('aspp{}'.format(i),
nn.Conv2d(in_channels, out_channels, 3, 1, rate, rate, bias=True))
self.init_param()
def init_param(self):
for c in self.children():
nn.init.xavier_normal_(c.weight)
nn.init.constant_(c.bias, 0)
def forward(self, x):
return sum([c(x) for c in self.children()])
3.3 Poly Learning Rate Policy
def poly_lr_scheduler(optimizer, n_iter, lr_decay_iter=1, max_iter=100, power=0.9):
"""
與作者給出的按iter衰減不同,我對epoch進行衰減,最大100個epoch,直接傳入optimizer進行l(wèi)r衰減
"""
if n_iter % lr_decay_iter == 0 and n_iter <= max_iter:
for param_gourp in optimizer.param_groups:
param_gourp['lr'] *= (1 - n_iter / max_iter) ** power
print("DECAYING learning rate... The new LR is %f\n" % (optimizer.param_groups[0]['lr']))
3.4Multiscale Processing
class MultiscaleProcess(nn.Module):
def __init__(self, net, scales=None):
super(MultiscaleProcess, self).__init__()
self.net = net
if scales is None:
scales = [0.5, 0.75]
self.scales = scales
def forward(self, x):
out = self.net(x)
_, _, h, w = out.shape
out_mutiscales = []
for s in self.scales:
img = F.interpolate(x, scale_factor=s, mode='bilinear', align_corners=False)
out_mutiscales.append(self.net(img))
out_all = [out] + [F.interpolate(s, size=(h, w), mode='bilinear', align_corners=False) for s in out_mutiscales]
out_max = torch.stack(out_all, dim=0).max(dim=0)[0]
if self.net.training:
return [out] + out_mutiscales + [out_max]
else:
return out_max
3.5 坑點
我主要試驗了如下的幾種組合:
- ResNet-101 + ASPP :僅使用VOC訓練,結果一直都是黑的,因此將進行了特征提取層和打分層分開進行如下的試驗測試,看看ResNet和ASPP模型是不是哪里出問題了
- VGG-16 + ASPP :很容易僅使用VOC訓練出了50+的IoU,大概1個epoch就行
- ResNet-101 + LargeFOV :僅使用VOC較難訓練,20個epoch才能達到接近50的mIoU,但mIoU基本收斂在40+實際分割效果并不是很好,且對于部分類別的效果不佳
試驗后發(fā)現(xiàn)兩部分模型都沒有問題,且較之VGG-16和LargeFOV更難訓練,改用VOC AUG進行訓練
- ResNet-101 + ASPP + Poly Learning Rate Policy :使用VOC AUG進行訓練,十幾個epoch后就能訓練出60+的IoU,各個類別的分割效果都很好,Poly Learning Rate Policy的確能夠提升mIoU
- ResNet-101 + ASPP + Poly Learning Rate Policy + Multiscale Processing:使用VOC AUG進行訓練,能夠更快速的收斂
如上兩個結構的模型結構的模型我都沒有訓練太多epoch(也就20剛出頭,算力有限??),但也確實證明了模型的優(yōu)秀能力。
本次跑模型最大的坑點在于預訓練的BN層:使用預訓練模型的參數(shù)時需要設置固定BN層的參數(shù)不被優(yōu)化,不然會帶來問題。對于BN層,在訓練時,是對mini-batch的訓練數(shù)據(jù)進行歸一化,也即用每一批數(shù)據(jù)的均值和方差,對于我設置的512 x 512的訓練圖像,只能設置batch=4,給模型帶來了嚴重問題。
此外,盡管模型是基于預訓練的ResNet-101網(wǎng)絡進行進行微調(diào),但其參數(shù)遠比VGG-16多太多了,僅使用VOC數(shù)據(jù)集進行訓練很難達到預期效果,所以必須使用VOC AUG數(shù)據(jù)集進行訓練才能避免“全黑”分割結果的出現(xiàn)。
在進行Multiscale Processing時,需要對ground truth label同樣進行下采樣,于是我使用了如下操做:
# 導致訓練失敗的問題代碼
import torchvision.transforms.functional as F
for pred in preds:
_, _, h, w = pred.shape
new_label = []
for l in label_img:
new_l=F.to_tensor(F.resize(F.to_pil_image(l.float().cpu()), (h, w), Image.NEAREST)).long().to(device)
new_label.append(label_img)
label_image = torch.LongTensor(VOC_COLORMAP)[new_label[-1].flatten()].reshape(h, w, 3)
由于使用Nearest插值方法的限制,torch.nn.functional.interploate并不支持Nearest,所以只能ground truth轉換為按圖片并進行插值。
F.to_pil_image()需要傳入進行歸一化處理后的FloatTensor,但我傳入的數(shù)據(jù)是有問題的,因此導致了訓練中的說莫名的錯誤;類似的F.to_tensor()也會將輸入進行歸一化,所以以上的操作達不到我的預期目標。因此我新寫了一個函數(shù)來將ground truth label進行rescale。
# 可以完成訓練
def ground_truth_to_resized_label(label_img, size):
l = label_img.cpu().numpy().astype(np.uint8)
l = Image.fromarray(l).resize(size, resample=Image.NEAREST)
l = torch.LongTensor(np.array(l))
return l
基本上DenseCRF后處理對于DeepLabv2模型的提升已經(jīng)不是十分明顯了,甚至可能其到反效果,現(xiàn)在基本上只有比賽刷分還能用到,如果對于DenseCRF后處理感興趣可參考DeepLabv1中相關的介紹和后處理代碼,這里由于算力的限制,就不后處理啦??
結果
Reference
[1] Chen, L. C. , Papandreou, G. , Kokkinos, I. , Murphy, K. , & Yuille, A. L. . (2017). Deeplab: semantic image segmentation with deep convolutional nets, atrous convolution, and fully connected crfs. IEEE Transactions on Pattern Analysis & Machine Intelligence, 1-1.
[2] kazuto1011/ deeplab-pytorch
轉載請說明出處。