DeepLabv2: 論文閱讀與Pytorch實現(xiàn)

作 者: 心有寶寶人自圓

聲 明: 歡迎轉載本文中的圖片或文字,請說明出處

寫在前面

自從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

DILATED CONVOLUTIONS with kernel size 3x3, dilation=2

真實的檢測目標往往是多尺度的,而我們的訓練集一般僅提供了較大尺度的目標。為了解決這一問題,主流的做法是將同一輸入圖片按不同尺度縮小輸入相同參數(shù)的網(wǎng)絡(對于縮小的圖片,同樣的kernel_size能獲得更大的感受野,間接地達到了小尺度目標地效果),再將特征圖或得分圖結合起來得到最終得得分。這樣的操作的確帶來了改進,當使訓練更加地耗時。作者受Spatial Pyramid Pooling地啟發(fā),設計出了對特定特征圖進行多尺度特征上采樣提取的高效計算方法:ASPP。

1.PNG

局部空間不變性是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(lr\ *=\ (1-\frac{iter}{max\_iter})^{power},power=0.9)能夠得到更好的表現(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

[3] doiken23/DeepLab_pytorch

轉載請說明出處。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容