1. 程式人生 > >[深度學習]Semantic Segmentation語義分割之FCN(1)

[深度學習]Semantic Segmentation語義分割之FCN(1)

論文全稱:《Fully Convolutional Networks for Semantic Segmentation》

論文地址:https://arxiv.org/pdf/1411.4038.pdf

論文程式碼:

python caffe版本 https://github.com/shelhamer/fcn.berkeleyvision.org 

python TensorFlow版本 https://github.com/shekkizh/FCN.tensorflow  

https://github.com/MarvinTeichmann/tensorflow-fcn

python keras版本 https://github.com/aurora95/Keras-FCN

python pytorch版本 https://github.com/wkentaro/pytorch-fcn

目錄

目錄

創新之處

Convolutionlization 卷積化

Upsampling 上取樣

 “skip” architecture 跳躍連線

程式碼詳細



這篇論文據說是語義分割的開山之作,是在2014年11月提交到arXiv上的,距離現在已經有四年多時間。這篇論文之前已經有人提出了FCN,但是之前的工作都沒有這篇論文那樣端到端訓練,而且也是第一次在語義分割任務上使用了pre-trained預訓練模型。雖然當年的R-CNN

是基於區域的CNN目標檢測系列的第一篇論文,但是R-CNN把檢測和分割放到一起沒有FCN的效果那麼好,所以2014年之後的很多語義分割的網路都是基於FCN創新。

如果對語義分割不太瞭解可以檢視

[深度學習]從全卷積網路到大型卷積核:深度學習的語義分割全指南

創新之處

  1. 使用可以end-to-end訓練的Fully Convolutional Net和 fine-tuning pre-trained model
  2. 上取樣(Upsampling)生成heatmap
  3.  “skip” architecture結合粗糙的高層語義資訊和細緻的淺層語義資訊

下面詳細展開每一個創新點。

Convolutionlization 卷積化

如上圖是將影象的分類網路中最後的全連線層使用1*1的卷積替換。這就是所謂卷積化。卷積層加全連線層的網路最後輸出的維度 是大小固定的,不適用於影象分割的任務,為了使得最後的輸出根據輸入影象大小變化而變化,所以採用了全卷積的網路,這樣就能接受任意大小和比例的影象輸入。

Upsampling 上取樣

雖然將分類網路重新定義為全卷積的,可以生成任意大小輸入的輸出對映,但是輸出維度通常通過子取樣被減少了。所以需要upsampling使得輸出的heatmap與輸入的影象大小一致才能做到語義分割。為此該論文嘗試使用三種方式,分別是:

1.Shift-and-stitch

2.簡單的雙線性插值

3.通過學習的deconvolution反捲積

第一個方法可以參考關於FCN 論文中的 Shift-and-stitch 的詳盡解釋

第二個方法可以參考雙線性插值演算法的詳細總結

詳細說說反捲積。反捲積也被稱為轉置卷積。反捲積和卷積是一個相反的過程,卷積是多個生成一個,反捲積是一個生成多個。考慮轉置卷積的最簡單方法是首先計算給定輸入形狀的直接卷積的輸出形狀,然後反轉轉置卷積的輸入和輸出形狀。

../_images/no_padding_no_strides.gif

普通卷積過程

../_images/no_padding_no_strides_transposed.gif

反捲積過程

 

 “skip” architecture 跳躍連線

 

為了結合粗糙的高層語義資訊和細緻的淺層語義資訊,提出了DAG。由於更細的尺度預測看到的畫素更少,所以預測需要更少的層,因此從更淺的淨輸出中進行預測是有意義的。將精細層和粗層結合起來,可以使模型在區域性進行預測時尊重全域性結構。他們連線的方式如下圖。FCN採取解決方法是將pool4、pool3、和特徵map融合起來,由於pool3、pool4、特徵map大小尺寸是不一樣的,所以融合應該前上取樣到同一尺寸。這裡的融合是拼接在一起,不是對應元素相加。

FCN8s是上面講的pool4、pool3和特徵map融合,FCN16s是pool4和特徵map融合,FCN32s是隻有特徵map,得出結果都是細節不夠好。

程式碼詳細

程式碼來源:https://github.com/shelhamer/fcn.berkeleyvision.org/blob/master/voc-fcn8s/net.py

是基於python語言的caffe實現的。只討論其中的一個版本FCN-8s,資料集是voc。

首先需要定義一些能夠服用的函式,比如重複多次的卷積加ReLU,maxpool等。

def conv_relu(bottom, nout, ks=3, stride=1, pad=1):
    conv = L.Convolution(bottom, kernel_size=ks, stride=stride,
        num_output=nout, pad=pad,
        param=[dict(lr_mult=1, decay_mult=1), dict(lr_mult=2, decay_mult=0)])
    return conv, L.ReLU(conv, in_place=True)

def max_pool(bottom, ks=2, stride=2):
    return L.Pooling(bottom, pool=P.Pooling.MAX, kernel_size=ks, stride=stride)

然後開始定義基礎網路結構,下面的程式碼與上面的圖片相對應。

    n.conv1_1, n.relu1_1 = conv_relu(n.data, 64, pad=100)
    n.conv1_2, n.relu1_2 = conv_relu(n.relu1_1, 64)
    n.pool1 = max_pool(n.relu1_2)

    n.conv2_1, n.relu2_1 = conv_relu(n.pool1, 128)
    n.conv2_2, n.relu2_2 = conv_relu(n.relu2_1, 128)
    n.pool2 = max_pool(n.relu2_2)

    n.conv3_1, n.relu3_1 = conv_relu(n.pool2, 256)
    n.conv3_2, n.relu3_2 = conv_relu(n.relu3_1, 256)
    n.conv3_3, n.relu3_3 = conv_relu(n.relu3_2, 256)
    n.pool3 = max_pool(n.relu3_3)

    n.conv4_1, n.relu4_1 = conv_relu(n.pool3, 512)
    n.conv4_2, n.relu4_2 = conv_relu(n.relu4_1, 512)
    n.conv4_3, n.relu4_3 = conv_relu(n.relu4_2, 512)
    n.pool4 = max_pool(n.relu4_3)

    n.conv5_1, n.relu5_1 = conv_relu(n.pool4, 512)
    n.conv5_2, n.relu5_2 = conv_relu(n.relu5_1, 512)
    n.conv5_3, n.relu5_3 = conv_relu(n.relu5_2, 512)
    n.pool5 = max_pool(n.relu5_3)

接著是全卷積部分。這一部分有三個分支。這裡解釋一下我之前理解錯誤的一個地方,下圖中有三個密集的輸出,但是真實的網路中只有一個輸出,比如FCN-32s,FCN-16s,FCN-8s。所以這裡的程式碼是FCN-8s就不存在FCN-32s和FCN-16s的密集輸出。

第一個分支:其中n.upscore2對應於

    # fully conv
    n.fc6, n.relu6 = conv_relu(n.pool5, 4096, ks=7, pad=0)
    n.drop6 = L.Dropout(n.relu6, dropout_ratio=0.5, in_place=True)
    n.fc7, n.relu7 = conv_relu(n.drop6, 4096, ks=1, pad=0)
    n.drop7 = L.Dropout(n.relu7, dropout_ratio=0.5, in_place=True)
    n.score_fr = L.Convolution(n.drop7, num_output=21, kernel_size=1, pad=0,
        param=[dict(lr_mult=1, decay_mult=1), dict(lr_mult=2, decay_mult=0)])
    n.upscore2 = L.Deconvolution(n.score_fr,
        convolution_param=dict(num_output=21, kernel_size=4, stride=2,
            bias_term=False),
        param=[dict(lr_mult=0)])

第二個分支:首先得對pool4做一層卷積。這裡特別注意一下crop()函式,論文裡面也提到過,他的主要作用是進行裁切。Eltwise()則是用於將兩個feature以相加的方式組合在一起,因為這裡設定了operation是Sum,其實還可以是點乘等操作。

這裡的Eltwise就對應於:

    n.score_pool4 = L.Convolution(n.pool4, num_output=21, kernel_size=1, pad=0,
        param=[dict(lr_mult=1, decay_mult=1), dict(lr_mult=2, decay_mult=0)])
    n.score_pool4c = crop(n.score_pool4, n.upscore2)
    n.fuse_pool4 = L.Eltwise(n.upscore2, n.score_pool4c,
            operation=P.Eltwise.SUM)
    n.upscore_pool4 = L.Deconvolution(n.fuse_pool4,
        convolution_param=dict(num_output=21, kernel_size=4, stride=2,
            bias_term=False),
        param=[dict(lr_mult=0)])

最後一個分支包括了密集的輸出:跟上一個分支類似,只不過結合的feature變成了pool3。n.upscore8就對應於:

    n.score_pool3 = L.Convolution(n.pool3, num_output=21, kernel_size=1, pad=0,
        param=[dict(lr_mult=1, decay_mult=1), dict(lr_mult=2, decay_mult=0)])
    n.score_pool3c = crop(n.score_pool3, n.upscore_pool4)
    n.fuse_pool3 = L.Eltwise(n.upscore_pool4, n.score_pool3c,
            operation=P.Eltwise.SUM)
    n.upscore8 = L.Deconvolution(n.fuse_pool3,
        convolution_param=dict(num_output=21, kernel_size=16, stride=8,
            bias_term=False),
        param=[dict(lr_mult=0)])
    n.score = crop(n.upscore8, n.data)
    n.loss = L.SoftmaxWithLoss(n.score, n.label,
            loss_param=dict(normalize=False, ignore_label=255))

將n.upscore8和label softmax 得出loss。

最後奉上原始碼

import caffe
from caffe import layers as L, params as P
from caffe.coord_map import crop

def conv_relu(bottom, nout, ks=3, stride=1, pad=1):
    conv = L.Convolution(bottom, kernel_size=ks, stride=stride,
        num_output=nout, pad=pad,
        param=[dict(lr_mult=1, decay_mult=1), dict(lr_mult=2, decay_mult=0)])
    return conv, L.ReLU(conv, in_place=True)

def max_pool(bottom, ks=2, stride=2):
    return L.Pooling(bottom, pool=P.Pooling.MAX, kernel_size=ks, stride=stride)

def fcn(split):
    n = caffe.NetSpec()
    pydata_params = dict(split=split, mean=(104.00699, 116.66877, 122.67892),
            seed=1337)
    if split == 'train':
        pydata_params['sbdd_dir'] = '../data/sbdd/dataset'
        pylayer = 'SBDDSegDataLayer'
    else:
        pydata_params['voc_dir'] = '../data/pascal/VOC2011'
        pylayer = 'VOCSegDataLayer'
    n.data, n.label = L.Python(module='voc_layers', layer=pylayer,
            ntop=2, param_str=str(pydata_params))

    # the base net
    n.conv1_1, n.relu1_1 = conv_relu(n.data, 64, pad=100)
    n.conv1_2, n.relu1_2 = conv_relu(n.relu1_1, 64)
    n.pool1 = max_pool(n.relu1_2)

    n.conv2_1, n.relu2_1 = conv_relu(n.pool1, 128)
    n.conv2_2, n.relu2_2 = conv_relu(n.relu2_1, 128)
    n.pool2 = max_pool(n.relu2_2)

    n.conv3_1, n.relu3_1 = conv_relu(n.pool2, 256)
    n.conv3_2, n.relu3_2 = conv_relu(n.relu3_1, 256)
    n.conv3_3, n.relu3_3 = conv_relu(n.relu3_2, 256)
    n.pool3 = max_pool(n.relu3_3)

    n.conv4_1, n.relu4_1 = conv_relu(n.pool3, 512)
    n.conv4_2, n.relu4_2 = conv_relu(n.relu4_1, 512)
    n.conv4_3, n.relu4_3 = conv_relu(n.relu4_2, 512)
    n.pool4 = max_pool(n.relu4_3)

    n.conv5_1, n.relu5_1 = conv_relu(n.pool4, 512)
    n.conv5_2, n.relu5_2 = conv_relu(n.relu5_1, 512)
    n.conv5_3, n.relu5_3 = conv_relu(n.relu5_2, 512)
    n.pool5 = max_pool(n.relu5_3)

    # fully conv
    n.fc6, n.relu6 = conv_relu(n.pool5, 4096, ks=7, pad=0)
    n.drop6 = L.Dropout(n.relu6, dropout_ratio=0.5, in_place=True)
    n.fc7, n.relu7 = conv_relu(n.drop6, 4096, ks=1, pad=0)
    n.drop7 = L.Dropout(n.relu7, dropout_ratio=0.5, in_place=True)
    n.score_fr = L.Convolution(n.drop7, num_output=21, kernel_size=1, pad=0,
        param=[dict(lr_mult=1, decay_mult=1), dict(lr_mult=2, decay_mult=0)])
    n.upscore2 = L.Deconvolution(n.score_fr,
        convolution_param=dict(num_output=21, kernel_size=4, stride=2,
            bias_term=False),
        param=[dict(lr_mult=0)])

    n.score_pool4 = L.Convolution(n.pool4, num_output=21, kernel_size=1, pad=0,
        param=[dict(lr_mult=1, decay_mult=1), dict(lr_mult=2, decay_mult=0)])
    n.score_pool4c = crop(n.score_pool4, n.upscore2)
    n.fuse_pool4 = L.Eltwise(n.upscore2, n.score_pool4c,
            operation=P.Eltwise.SUM)
    n.upscore_pool4 = L.Deconvolution(n.fuse_pool4,
        convolution_param=dict(num_output=21, kernel_size=4, stride=2,
            bias_term=False),
        param=[dict(lr_mult=0)])

    n.score_pool3 = L.Convolution(n.pool3, num_output=21, kernel_size=1, pad=0,
        param=[dict(lr_mult=1, decay_mult=1), dict(lr_mult=2, decay_mult=0)])
    n.score_pool3c = crop(n.score_pool3, n.upscore_pool4)
    n.fuse_pool3 = L.Eltwise(n.upscore_pool4, n.score_pool3c,
            operation=P.Eltwise.SUM)
    n.upscore8 = L.Deconvolution(n.fuse_pool3,
        convolution_param=dict(num_output=21, kernel_size=16, stride=8,
            bias_term=False),
        param=[dict(lr_mult=0)])

    n.score = crop(n.upscore8, n.data)
    n.loss = L.SoftmaxWithLoss(n.score, n.label,
            loss_param=dict(normalize=False, ignore_label=255))

    return n.to_proto()

def make_net():
    with open('train.prototxt', 'w') as f:
        f.write(str(fcn('train')))

    with open('val.prototxt', 'w') as f:
        f.write(str(fcn('seg11valid')))

if __name__ == '__main__':
    make_net()