Semantic Segmentation--Pyramid Scene Parsing Network(PSPNet)論文解讀

PSPNet

Pyramid Scene Parsing Network

收錄:CVPR 2017 (IEEE Conference on Computer Vision and Pattern Recognition)

原文地址: PSPNet

代碼:

效果圖:

在這裏插入圖片描述


Abstract

本文提出的金字塔池化模塊( pyramid pooling module)能夠聚合不同區域的上下文信息,從而提高獲取全局信息的能力。實驗表明這樣的先驗表示(即指代PSP這個結構)是有效的,在多個數據集上展現了優良的效果。


Introduction

場景解析(Scene Parsing)的難度與場景的標籤密切相關。先大多數先進的場景解析框架大多數基於FCN,但FCN存在的幾個問題:

在這裏插入圖片描述

  • Mismatched Relationship:上下文關係匹配對理解複雜場景很重要,例如在上圖第一行,在水面上的大很可能是“boat”,而不是“car”。雖然“boat和“car”很像。FCN缺乏依據上下文推斷的能力。
  • Confusion Categories: 許多標籤之間存在關聯,可以通過標籤之間的關係彌補。上圖第二行,把摩天大廈的一部分識別爲建築物,這應該只是其中一個,而不是二者。這可以通過類別之間的關係彌補。
  • Inconspicuous Classes:模型可能會忽略小的東西,而大的東西可能會超過FCN接收範圍,從而導致不連續的預測。如上圖第三行,枕頭與被子材質一致,被識別成到一起了。爲了提高不顯眼東西的分割效果,應該注重小面積物體。

總結這些情況,許多問題出在FCN不能有效的處理場景之間的關係和全局信息。本論文提出了能夠獲取全局場景的深度網絡PSPNet,能夠融合合適的全局特徵,將局部和全局信息融合到一起。並提出了一個適度監督損失的優化策略,在多個數據集上表現優異。

本文的主要貢獻如下:

  • 提出了一個金字塔場景解析網絡,能夠將難解析的場景信息特徵嵌入基於FCN預測框架中
  • 在基於深度監督損失ResNet上制定有效的優化策略
  • 構建了一個實用的系統,用於場景解析和語義分割,幷包含了實施細節

Related Work

受到深度神經網絡的驅動,場景解析和語義分割獲得了極大的進展。例如FCN、ENet等工作。許多深度卷積神經網絡爲了擴大高層feature的感受野,常用dilated convolution(空洞卷積)、coarse-to-fine structure等方法。本文基於先前的工作,選擇的baseline是帶dilated network的FCN。

大多數語義分割模型的工作基於兩個方面:

  • 一方面:具有多尺度的特徵融合,高層特徵具有強的語義信息,底層特徵包含更多的細節。
  • 另一方面:基於結構預測。例如使用CRF(條件隨機場)做後端細化分割結果。

爲了充分的利用全局特徵層次先驗知識來進行不同場景理解,本文提出的PSP模塊能夠聚合不同區域的上下文從而達到獲取全局上下文的目的。


Architecture

Pyramid Pooling Module

前面也說到了,本文的一大貢獻就是PSP模塊。

在一般CNN中感受野可以粗略的認爲是使用上下文信息的大小,論文指出在許多網絡中沒有充分的獲取全局信息,所以效果不好。要解決這一問題,常用的方法是:

  • 用全局平均池化處理。但這在某些數據集上,可能會失去空間關係並導致模糊。
  • 由金字塔池化產生不同層次的特徵最後被平滑的連接成一個FC層做分類。這樣可以去除CNN固定大小的圖像分類約束,減少不同區域之間的信息損失。

論文提出了一個具有層次全局優先級,包含不同子區域之間的不同尺度的信息,稱之爲pyramid pooling module。

在這裏插入圖片描述
該模塊融合了4種不同金字塔尺度的特徵,第一行紅色是最粗糙的特徵–全局池化生成單個bin輸出,後面三行是不同尺度的池化特徵。爲了保證全局特徵的權重,如果金字塔共有N個級別,則在每個級別後使用1×11×1的卷積將對於級別通道降爲原本的1/N。再通過雙線性插值獲得未池化前的大小,最終concat到一起。

金字塔等級的池化核大小是可以設定的,這與送到金字塔的輸入有關。論文中使用的4個等級,核大小分別爲1×12×23×36×61×1,2×2,3×3,6×6

整體架構

在PSP模塊的基礎上,PSPNet的整體架構如下:

在這裏插入圖片描述

  • 基礎層經過預訓練的模型(ResNet101)和空洞卷積策略提取feature map,提取後的feature map是輸入的1/8大小
  • feature map經過Pyramid Pooling Module得到融合的帶有整體信息的feature,在上採樣與池化前的feature map相concat
  • 最後過一個卷積層得到最終輸出

PSPNet本身提供了一個全局上下文的先驗(即指代Pyramid Pooling Module這個結構),後面的實驗會驗證這一結構的有效性。

基於ResNet的深度監督網絡

論文用了一個很“玄學”的方法搞了一個基礎網絡層,如下圖:

在這裏插入圖片描述
在ResNet101的基礎上做了改進,除了使用後面的softmax分類做loss,額外的在第四階段添加了一個輔助的loss,兩個loss一起傳播,使用不同的權重,共同優化參數。後續的實驗證明這樣做有利於快速收斂。


Experiment

論文在ImageNet scene parsing challenge 2016, PASCAL VOC 2012,Cityscapes 三個數據集上做了實驗。

訓練細節:

項目 設置
學習率 採用“poly”策略,即lr=lrbase(1itermaxiter)powerlr=lr_{base}*(1-\frac{iter}{max_{iter}})^{power} 設置lrbase=0.01,power=0.9lr_{base}=0.01,power=0.9,衰減動量設置爲0.9 and 0.0001
迭代次數 ImageNet上設置150K,PASCAL VOC設置30K,Cityscapes設置90K
數據增強 隨機翻轉、尺寸在0.5到2之間縮放、角度在-10到10之間旋轉、隨機的高斯濾波
batchsize batch很重要,設置batch=16(這很吃顯存啊~)
訓練分支網絡 設置輔助loss的權重爲0.4
平臺 Caffe

ImageNet scene parsing challenge 2016

  • 測試不同配置下的ResNet的性能,找到比較好的預訓練模型:

在這裏插入圖片描述

- ResNet50-Baseline: 基於FCN的ResNet50結構,帶空洞卷積的baseline
- ResNet50+B1+MAX:只帶$1×1$的最大池化
- ResNet50+B1+AVE: 只帶$1×1$的平均池化
- ResNet50+B1236+MAX: 帶$1×1,2×2,3×3,6×6$的最大池化
- ResNet50+B1236+AVE: 帶$1×1,2×2,3×3,6×6$的平均池化
- ResNet50+B1236+MAX+DR: 帶$1×1,2×2,3×3,6×6$的最大池化,池化後做通道降維
- **ResNet50+B1236+AVE+DR(best)**: 帶$1×1,2×2,3×3,6×6$的平均池化,池化後做通道降維

可以看到做平均池化的都比最大池化效果要好,最後將多個操作結合得到最終最好的效果。

  • 測試輔助loss的影響:

在這裏插入圖片描述

實驗都是以ResNet50-Baseline爲基準,最後以α=0.4\alpha=0.4爲最佳。

  • 測試預訓練模型的深度:

在這裏插入圖片描述

在這裏插入圖片描述

可以看到在測試的{50,101,152,269}這四個層次的網絡中,網絡越深,效果越好。

  • 多種技巧融合

在這裏插入圖片描述

帶有DA即數據增強,AL即帶輔助loss,PSP帶金字塔池化模塊,MS多尺度。

在IamgeNet上的表現:

在這裏插入圖片描述

PASCAL VOC 2012

在這裏插入圖片描述

可以看到在MS-COCO上預訓練過的效果最好。

Cityscapes

在這裏插入圖片描述

在Cityscapes上表現也是很不錯的。


Conclusion

論文在結構上提供了一個pyramid pooling module,在不同層次上融合feature,達到語義和細節的融合。 模型的性能表現很大,但感覺主要歸功於一個良好的特徵提取層。在實驗部分講了很多訓練細節,但還是很難復現,這裏值得好好推敲一下。


代碼分析

這裏分析的代碼是Keras版本.

layers_builder.py這個文件,構建了PSPNet。

主要看一下PSP模塊:

在這裏插入圖片描述


def build_pyramid_pooling_module(res, input_shape):
    """Build the Pyramid Pooling Module."""
    # ---PSPNet concat layers with Interpolation
    feature_map_size = tuple(int(ceil(input_dim / 8.0)) for input_dim in input_shape)
    print("PSP module will interpolate to a final feature map size of %s" % (feature_map_size, ))

    # 創建不同尺度的feature
    interp_block1 = interp_block(res, 1, feature_map_size, input_shape)
    interp_block2 = interp_block(res, 2, feature_map_size, input_shape)
    interp_block3 = interp_block(res, 3, feature_map_size, input_shape)
    interp_block6 = interp_block(res, 6, feature_map_size, input_shape)

    # 通道融合,融合所有feature 原本通道爲2048  每層池化佔512個通道  
    # shape=(1,feature_map_size_x,feature_map_size_y,4096)  融合後共4096個
    res = Concatenate()([res,
                         interp_block6,
                         interp_block3,
                         interp_block2,
                         interp_block1])
    return res


def interp_block(prev_layer, level, feature_map_shape, input_shape):
    if input_shape == (473, 473):
        kernel_strides_map = {1: 60,  # (473-60)/60 + 1 = 6 + 1 = 7
                              2: 30,  # (473-30)/30 + 1 = 11 + 1 = 12
                              3: 20,  # (473-20)/20 + 1 = 22 + 1 = 23
                              6: 10}  # (473-10)/10 + 1 = 46 + 1 = 47
    elif input_shape == (713, 713):
        kernel_strides_map = {1: 90,  # (713-90)/90 + 1 = 6 + 1 = 7
                              2: 45,  # (713-45)/45 + 1 = 14 + 1 = 15
                              3: 30,  # (713-30)/30 + 1 = 6 + 1 = 23
                              6: 15}  # (713-15)/15 + 1 = 6 + 1 = 47
    else:
        print("Pooling parameters for input shape ", input_shape, " are not defined.")
        exit(1)

    names = [
        "conv5_3_pool" + str(level) + "_conv",
        "conv5_3_pool" + str(level) + "_conv_bn"
        ]
    kernel = (kernel_strides_map[level], kernel_strides_map[level])
    strides = (kernel_strides_map[level], kernel_strides_map[level])
    prev_layer = AveragePooling2D(kernel, strides=strides)(prev_layer) # 平均池化
    prev_layer = Conv2D(512, (1, 1), strides=(1, 1), name=names[0],
                        use_bias=False)(prev_layer) # 通道降到 原本的1/N = 1/4
    prev_layer = BN(name=names[1])(prev_layer)
    prev_layer = Activation('relu')(prev_layer)
    prev_layer = Lambda(Interp, arguments={'shape': feature_map_shape})(prev_layer) # 放縮到指定大小
    return prev_layer


構建PSPnet網絡:

在這裏插入圖片描述

def build_pspnet(nb_classes, resnet_layers, input_shape, activation='softmax'):
    """Build PSPNet."""
    print("Building a PSPNet based on ResNet %i expecting inputs of shape %s predicting %i classes" % (resnet_layers, input_shape, nb_classes))

    inp = Input((input_shape[0], input_shape[1], 3))
    res = ResNet(inp, layers=resnet_layers) # 基礎的特徵提取層 
    psp = build_pyramid_pooling_module(res, input_shape) # PSP模塊

    x = Conv2D(512, (3, 3), strides=(1, 1), padding="same", name="conv5_4",
               use_bias=False)(psp) # PSP後接卷積輸出結果
    x = BN(name="conv5_4_bn")(x)
    x = Activation('relu')(x)
    x = Dropout(0.1)(x)

    x = Conv2D(nb_classes, (1, 1), strides=(1, 1), name="conv6")(x) # 1x1變換輸出分類結果
    x = Lambda(Interp, arguments={'shape': (input_shape[0], input_shape[1])})(x) # 使用Lambda層放縮到原圖片大小
    x = Activation('softmax')(x) # 經過softmax得到最終輸出

    model = Model(inputs=inp, outputs=x)

    # Solver
    sgd = SGD(lr=learning_rate, momentum=0.9, nesterov=True)
    model.compile(optimizer=sgd,
                  loss='categorical_crossentropy',
                  metrics=['accuracy'])
    return model

PSPNet的基礎特徵提取層,即構建ResNet(這部分可以自己選定其他模型):

from __future__ import print_function
from math import ceil
from keras.layers import Conv2D, MaxPooling2D, AveragePooling2D
from keras.layers import BatchNormalization, Activation, Input, Dropout, ZeroPadding2D, Lambda
from keras.layers.merge import Concatenate, Add
from keras.models import Model
from keras.optimizers import SGD

learning_rate = 1e-3  # Layer specific learning rate
# Weight decay not implemented


def BN(name=""):
    return BatchNormalization(momentum=0.95, name=name, epsilon=1e-5)


def Interp(x, shape):
    ''' 對圖片做一個放縮,配合Keras的Lambda層使用'''
    from keras.backend import tf as ktf
    new_height, new_width = shape
    resized = ktf.image.resize_images(x, [new_height, new_width],
                                      align_corners=True)
    return resized


def residual_conv(prev, level, pad=1, lvl=1, sub_lvl=1, modify_stride=False):
    lvl = str(lvl)
    sub_lvl = str(sub_lvl)
    names = ["conv"+lvl+"_" + sub_lvl + "_1x1_reduce",
             "conv"+lvl+"_" + sub_lvl + "_1x1_reduce_bn",
             "conv"+lvl+"_" + sub_lvl + "_3x3",
             "conv"+lvl+"_" + sub_lvl + "_3x3_bn",
             "conv"+lvl+"_" + sub_lvl + "_1x1_increase",
             "conv"+lvl+"_" + sub_lvl + "_1x1_increase_bn"]
    if modify_stride is False:
        prev = Conv2D(64 * level, (1, 1), strides=(1, 1), name=names[0],
                      use_bias=False)(prev)
    elif modify_stride is True:
        prev = Conv2D(64 * level, (1, 1), strides=(2, 2), name=names[0],
                      use_bias=False)(prev)

    prev = BN(name=names[1])(prev)
    prev = Activation('relu')(prev)

    prev = ZeroPadding2D(padding=(pad, pad))(prev)
    prev = Conv2D(64 * level, (3, 3), strides=(1, 1), dilation_rate=pad,
                  name=names[2], use_bias=False)(prev)

    prev = BN(name=names[3])(prev)
    prev = Activation('relu')(prev)
    prev = Conv2D(256 * level, (1, 1), strides=(1, 1), name=names[4],
                  use_bias=False)(prev)
    prev = BN(name=names[5])(prev)
    return prev



def short_convolution_branch(prev, level, lvl=1, sub_lvl=1, modify_stride=False):
    lvl = str(lvl)
    sub_lvl = str(sub_lvl)
    names = ["conv" + lvl+"_" + sub_lvl + "_1x1_proj",
             "conv" + lvl+"_" + sub_lvl + "_1x1_proj_bn"]

    if modify_stride is False:
        prev = Conv2D(256 * level, (1, 1), strides=(1, 1), name=names[0],
                      use_bias=False)(prev)
    elif modify_stride is True:
        prev = Conv2D(256 * level, (1, 1), strides=(2, 2), name=names[0],
                      use_bias=False)(prev)

    prev = BN(name=names[1])(prev)
    return prev


def empty_branch(prev):
    return prev


def residual_short(prev_layer, level, pad=1, lvl=1, sub_lvl=1, modify_stride=False):
    prev_layer = Activation('relu')(prev_layer)
    block_1 = residual_conv(prev_layer, level,
                            pad=pad, lvl=lvl, sub_lvl=sub_lvl,
                            modify_stride=modify_stride)

    block_2 = short_convolution_branch(prev_layer, level,
                                       lvl=lvl, sub_lvl=sub_lvl,
                                       modify_stride=modify_stride)
    added = Add()([block_1, block_2])
    return added


def residual_empty(prev_layer, level, pad=1, lvl=1, sub_lvl=1):
    prev_layer = Activation('relu')(prev_layer)

    block_1 = residual_conv(prev_layer, level, pad=pad,
                            lvl=lvl, sub_lvl=sub_lvl)
    block_2 = empty_branch(prev_layer)
    added = Add()([block_1, block_2])
    return added


def ResNet(inp, layers):
    '''構建ResNet'''
    # Names for the first couple layers of model
    names = ["conv1_1_3x3_s2",
             "conv1_1_3x3_s2_bn",
             "conv1_2_3x3",
             "conv1_2_3x3_bn",
             "conv1_3_3x3",
             "conv1_3_3x3_bn"]

    # Short branch(only start of network)

    cnv1 = Conv2D(64, (3, 3), strides=(2, 2), padding='same', name=names[0],
                  use_bias=False)(inp)  # "conv1_1_3x3_s2"
    bn1 = BN(name=names[1])(cnv1)  # "conv1_1_3x3_s2/bn"
    relu1 = Activation('relu')(bn1)  # "conv1_1_3x3_s2/relu"

    cnv1 = Conv2D(64, (3, 3), strides=(1, 1), padding='same', name=names[2],
                  use_bias=False)(relu1)  # "conv1_2_3x3"
    bn1 = BN(name=names[3])(cnv1)  # "conv1_2_3x3/bn"
    relu1 = Activation('relu')(bn1)  # "conv1_2_3x3/relu"

    cnv1 = Conv2D(128, (3, 3), strides=(1, 1), padding='same', name=names[4],
                  use_bias=False)(relu1)  # "conv1_3_3x3"
    bn1 = BN(name=names[5])(cnv1)  # "conv1_3_3x3/bn"
    relu1 = Activation('relu')(bn1)  # "conv1_3_3x3/relu"

    res = MaxPooling2D(pool_size=(3, 3), padding='same',
                       strides=(2, 2))(relu1)  # "pool1_3x3_s2"

    # ---Residual layers(body of network)

    """
    Modify_stride --Used only once in first 3_1 convolutions block.
    changes stride of first convolution from 1 -> 2
    """

    # 2_1- 2_3
    res = residual_short(res, 1, pad=1, lvl=2, sub_lvl=1)
    for i in range(2):
        res = residual_empty(res, 1, pad=1, lvl=2, sub_lvl=i+2)

    # 3_1 - 3_3
    res = residual_short(res, 2, pad=1, lvl=3, sub_lvl=1, modify_stride=True)
    for i in range(3):
        res = residual_empty(res, 2, pad=1, lvl=3, sub_lvl=i+2)
    if layers is 50:
        # 4_1 - 4_6
        res = residual_short(res, 4, pad=2, lvl=4, sub_lvl=1)
        for i in range(5):
            res = residual_empty(res, 4, pad=2, lvl=4, sub_lvl=i+2)
    elif layers is 101:
        # 4_1 - 4_23
        res = residual_short(res, 4, pad=2, lvl=4, sub_lvl=1)
        for i in range(22):
            res = residual_empty(res, 4, pad=2, lvl=4, sub_lvl=i+2)
    else:
        print("This ResNet is not implemented")

    # 5_1 - 5_3
    res = residual_short(res, 8, pad=4, lvl=5, sub_lvl=1)
    for i in range(2):
        res = residual_empty(res, 8, pad=4, lvl=5, sub_lvl=i+2)

    res = Activation('relu')(res)
    return res
發佈了77 篇原創文章 · 獲贊 572 · 訪問量 67萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章