PaddlePaddle 實現 DeepLab V3

HiPete發表於2020-10-25

百度飛槳影像分割日打卡訓練營之 PaddlePaddle 實現 DeepLab V3

課程連結:https://aistudio.baidu.com/aistudio/course/introduce/1767
本文圖片程式碼來自以上課程

A.理論部分

DeepLab V3 相關論文 :Rethinking Atrous Convolution for Semantic Image Segmentation

1. DeepLab V3 網路框架

在這裡插入圖片描述
可以看到 DeepLab V3 使用 Resnet 作為 backbone,提取 feature map 之後以相比 DeepLab V2 改進的 ASPP 結構加 score 分類作為分割頭,還原尺寸並輸出

2. upgraded ASPP

其中改進後的 ASPP 結構如下圖所示:
在這裡插入圖片描述

從 backbone 提出特徵後,進行了四種操作:1x1卷積、dilation 分別為6、12、18的3x3空洞卷積以及 Adaptive pool + interpolation 的操作
當然實際實現過程中空洞卷積的操作可以自己調整,只要能 cover 自己資料集中特徵的 scale 就好
提取出尺寸一樣的 feature map 後把所有 feature map 進行 concat 操作(如圖所示就是堆起來)
之後把堆起來的 feature 進行 1x1卷積降維,維度可以自己設,論文中為256

以上就是 upgraded ASPP 做的全部操作,下面解釋一下空洞卷積和 Adaptive pool 的操作,畢竟朱老師上課專門講了

3. Dilated Convolution

Dilated Convolution 空洞卷積如下圖所示:
在這裡插入圖片描述

目的是擴大卷積操作的感受野,除了把 feature map 池化成多個尺度,另一個思路就是把卷積核的 kernal size 變成多個尺度啦
這裡的做法是在卷積核中填 0 所以叫空洞卷積,這個 dilation 值在正常的卷積操作中是1,而圖中這樣子就是2,所以理解為卷積核中相鄰元素的距離就好

4. Adaptive pool

Adaptive pool 是朱老師將 PSPNet 的時候講的,所以這裡貼 PSPNet 的課件圖:
在這裡插入圖片描述
其實就是把任意尺寸輸入 pool 成任意尺寸輸出,至於是最大值池化還平均值池化在 API 中還是可以選的區別不大
這裡的計算公式計算的是輸出中元素 (i, j) 在原圖 h/w 方向池化池的範圍,floor 指向下取整,H 指的輸入輸出的尺寸,如果計算 w 方向的範圍這裡 i 改成 j
這個操作比較省計算量,這裡加一個肯定收益大於成本

5. Muiti-Grid

Resnet 就不解釋了玩過檢測的都熟,這裡朱老師也推薦換成 MobileNet 好訓練一些。就加一下 DeepLab V3 在 Resnet 中做的 Muiti-Grid 的改進:
在這裡插入圖片描述

把 backbone 中的卷積也改成不同 dilation 指的空洞卷積了,而這個指是作者試驗出來效果比較好的值

B. 程式碼

O嗑誒,下面上程式碼:

1. main

def main():
    place = paddle.fluid.CUDAPlace(0)
    with fluid.dygraph.guard(place):
        x_data = np.random.rand(2, 3, 512, 512).astype(np.float32)
        x = to_variable(x_data)
        model = DeepLab(num_classes=59)
        model.eval()
        pred = model(x)
        print(pred.shape)

主函式,隨機生成一個 (2, 3, 512, 512) 的 tensor,捲入 DeepLab V3 模型中,設為 evalue 模式,輸出預測

2. DeepLab

class DeepLab(Layer):
    def __init__(self, num_classes=59):
        super(DeepLab, self).__init__()
        
        back = ResNet50(pretrained=False, duplicate_blocks=True)

        self.layer0 = fluid.dygraph.Sequential(
                                    back.conv,
                                    back.pool2d_max,
                                    )
        self.layer1 = back.layer1
        self.layer2 = back.layer2
        self.layer3 = back.layer3
        
        # multigrid
        self.layer4 = back.layer4
        self.layer5 = back.layer5
        self.layer6 = back.layer6
        self.layer7 = back.layer7

        feature_dim = 2048
        self.classifier = DeepLabHead(feature_dim, num_classes)

    def forward(self, inputs):
        n, c, h, w = inputs.shape
        x = self.layer0(inputs)
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
        x = self.layer5(x)
        x = self.layer6(x)
        x = self.layer7(x)

        x = self.classifier(x)      # ASPP & classifier

        x = fluid.layers.interpolate(x, (h, w), align_corners=False)    # resize

        return  x

模型就是這樣,ResNet50 是事先寫好匯入的,包括空洞卷積操作,重點不是它就不上了
其中 classifier 裡是包括了 upgraded ASPP 操作和分類操作
interpolate 就是 interpolation,還原成原尺寸

3. DeepLabHead

class DeepLabHead(fluid.dygraph.Sequential):
    def __init__(self, num_channels, num_classes):
        super(DeepLabHead, self).__init__(
                ASPPModule(num_channels, 256, [12, 24, 36]),
                Conv2D(256, 256, 3, padding=1),
                BatchNorm(256, act='relu'),
                Conv2D(256, num_classes, 1)
                )

分割頭 DeepLabHead 類直接繼承了 paddle 動態圖中的 Sequential 類,使用中感覺這個類對於網路結構模組化還是很方便的
就, upgraded ASPP 之後卷一下再分類

4. ASPPModule

來直接看 upgraded ASPP :

class ASPPModule(Layer):
    def __init__(self, num_channels, num_filters, rates):
        super(ASPPModule, self).__init__()

        self.features = []
        self.features.append(
                fluid.dygraph.Sequential(
                    Conv2D(num_channels, num_filters, 1),
                    BatchNorm(num_filters, act='relu')
                )
        )
        self.features.append(ASPPPooling(num_channels, num_filters))

        for r in rates:
            self.features.append(
                ASPPConv(num_channels, num_filters, r)
                )
        self.project = fluid.dygraph.Sequential(
                                    Conv2D(num_filters*(2 + len(rates) ), num_filters, 1),
                                    BatchNorm(num_filters, act='relu'),
        )

    def forward(self, inputs):
        res = []
        for op in self.features:
            res.append(op(inputs))
            
        x = fluid.layers.concat(res,axis=1)
        x = self.project(x)

        return x

因為這裡操作分5路,所以定義了一個 list 把操作定義好裝進去,結果放在 res 這個 list 中再一起 concat,project 就是上面提到的降維為 256 的操作
feature 中先後裝了1x1卷積、 Adaptive pool 和 dilation 分別為12、24、36的3x3空洞卷積這一組操作

Adaptive pool 操作:

class ASPPPooling(Layer):
    def __init__(self, num_channels, num_filters):
        super(ASPPPooling, self).__init__()
        self.features = fluid.dygraph.Sequential(
                        Conv2D(num_channels, num_filters, 1),
                        BatchNorm(num_filters, act='relu')
                        )

    def forward(self, inputs):
        n, c, h, w = inputs.shape

        x = fluid.layers.adaptive_pool2d(inputs,1)
        x = self.features(x)

        x = fluid.layers.interpolate(x, (h, w), align_corners=False)
        return x

是的又是調 API

空洞卷積:

class ASPPConv(fluid.dygraph.Sequential):
    def __init__(self, num_channels, num_filters, dilation):
        super(ASPPConv, self).__init__(
            Conv2D(num_channels, num_filters, filter_size=3, padding=dilation, dilation=dilation),
            BatchNorm(num_filters, act='relu')
        )

padding 和 dilation 保持一致是因為這樣可以保持輸出尺寸一致

O嗑誒,維度我就不推了反正這個維度跑通了

C. 總結

總結一下這個影像分割7日打卡

一開始只是因為想在畢設里加點影像分割相關成分好水過就參加了
之前在學校上過類似的課也接觸過Paddle,但這次小白表示被朱老師現場 coding 復現模型+ debug 震撼到了,也感覺這樣的效果是最好的,至少對我這種剛踩了點 DL 門框,想進一步對分割領域有個總體認識不知道如何下手的小白來說效果超出預期
為朱老師打call

很感謝百度的無私佈道,期待接下來發布的 PaddleSeg ,以後再有類似活動也會積極關注的

相關文章