FCN:Fully Convolutional Networks for Semantic Segmentation的閱讀與pytorch實現

作 者: 心有寶寶人自圓

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

寫在前面

本篇文章介紹的FCN是語義分割(Semantic Segmentation)之中Fully Convolutional Network結構流派的開山鼻祖,以至于之后的語義分割研究基本采取了這種結構。

語義分割的目標是為每圖片中的每一個pixel進行類別的預測(Dense Prediction)

本文的主體內容十分容易理解,但是一些作者介紹的tricks讓人看得云里霧里的(關鍵這些tricks作者最后一般都沒使用??),所以對應FCN的原理理解可以忽略這些tricks(但我還會分享一些理解??)

最后我給出了我的代碼和結果(pytorch),訓練這個真是太難了??以下坑點萬分注意:

a) resize時,插值的方法一定要選擇NEAREAST而不是默認的Bilinear,否則會對true label image的pixel進行誤標

b) 一定要充足的耐心進行訓練,不然你的分割圖像一直是黑的(沒用非極大抑制大概80 epochs,使用后大概40 epochs)

c) 關于不同的loss:loss的設計可能會出現梯度爆炸的現象(若非loss的設計問題),batch size不要設太大;有時候loss的設計實際影響了收斂時間(就是分割圖像一直是黑持續時間)

1. Introduction

語義分割的目標就是要為每個像素做出預測,每個像素要被標記為包含它的目標的類別。而FCN是第一個使用end-to-end,pixel-to-pixel訓練的語義分割方法。FCN能使用任意大小的圖像作為輸入(去除了網絡中的fully connected layers),進行密集預測。學習特征和推斷分別通過feedforward(下采樣)和backpropagation(上采樣)進行,這樣的結構特征使網絡可以進行pixelwise預測。

作者介紹了語義分割一種內在矛盾:全局信息(global/semantic information)和位置信息(location information)。他們分別代表network高層和低層的特征,作者形象的稱號它們為what和where。高層的信息經過了下采樣所以更能代表類別信息,而低層則包含了目標的細節信息,而語義分割則需要全局信息和位置信息的共同編碼,否則在目標邊緣的預測會變得很不準卻。為了解決這一問題作者設計了一種跳躍結構(Skip Architect),進而結合了兩種信息

2. Related Work

在FCN提出之前,語義分割基本是基于pitch-wise訓練的(fine-tuned R-CNN system),以選擇性搜索選取一定大小的proposal region,使用CNN提取proposal region的特征傳入分類器。這樣的操著并非end-to-end,proposal region的大小一般是先驗指定(限制了模型感受野的大小,使其僅對某些scale的特征敏感),隨機選取的proposal region可能高度重疊而造成計算、存儲資源的過多消耗。

3. Fully Convolution Networks

f表示卷積或池化,x為輸入,y為輸出,k是kernel size,s是stride or subsampling factor;下式則表示連續的卷積或池化可以合成等效的一步(當然非線性的激活函數也可以代表,但它對下采樣過程沒有作用)。這個公式也可以說明為什么5x5,stride=1的卷積可以轉化成2個3x3,stride=1的卷積

)

  • 關于損失函數:

    每一圖像的損失是每個空間空間點的損失之和?l(x;\theta)=\sum_{ij}l^{'}(x_{ij};\theta)

    因此每個圖像的隨機梯度下降等于每個空間點的梯度下降之和

3.1 改編分類器以適應密集預測

傳統的分類器(全連接層)使用固定大小的輸入,產生非空間性的輸出,因此全連接層被認為是固定size、丟棄了空間信息;然而全連接也可以視為kernels覆蓋了整個輸入的卷積層(這樣就可以將全連接層與卷積層相互轉換),而卷積層可接受任意大小的輸入,輸出分類maps。使用卷積層代替全連接能帶來更高的計算效率

2.PNG

然而輸出的分類maps(粗糙輸出)的維度由于經過下采樣而比原始輸入的維度更小

3.2 Shift-and-stitch是濾波稀疏

為了將全卷積網絡的粗糙輸出轉化到原始空間的密集預測,作者引入了input shifting(輸入平移)和output interlacing(輸出交織)的trick(然而這并非作者最終選擇使用的上采樣策略??)

給定下采樣因子f(stride),將將原始輸入分別從左上(0,0)開始,分別向右和向下平移[0,f-1]個像素,共得到f^2個輸入分別通過全卷積網絡產生f^2個output,將這些結果交織在一起就能得到原始輸入空間大小的輸出,這樣的預測結果與感受野中心像素有關。可以看出shift-and-stitch與傳統的上采樣方法(如雙線性插值)是不一樣的。然而這種做法并沒有真正利用到低層更細節的信息

之后作者有想出了一個trick:縮小卷積核(等同于對原始圖像進行上采樣),同樣可以達到輸出的維度與輸入的維度相同。然而這種做法導致卷積層的感受野過小、更長的計算時間

3.3 上采樣是反向的卷積

在神經網絡里,一個關于上采樣的自然想法便是反向傳播,所以作者就采用反卷積(deconvolution)的方法進行上采樣。deconvolution中的卷積轉置層的參數是可學習的,然而在作者的倉庫中,設定其為固定值(作者實際使用了雙線性插值的方法)

3.4 Patchwise training是損失的采樣

在隨機優化中,梯度的計算實際是由訓練的分布驅動的。Patchwising training和fully-conv training都可以產生任意的分布(即使它們的效率與重疊部分和小批量的大小有關)。通常來說后者比前者的效率更高(更少的batches)。

對于patchwise training的采樣可以減少類別不平衡和緩解空間的相關性;在fully-conv training中,類別的平衡和緩解空間的相關性可以通過對loss的加權或下采樣loss得到。然而4.3節的結果表明下采樣并沒有對結果產生顯著的影響(類別不平衡為對FCN并不重要),僅加快了收斂的速度

4 分割結構

(遷移學習+微調)

從預訓練網絡的全連接層截斷的網絡,之前的網絡直接使用,全連接層轉換為卷積層(除GoogLeNet)。

把最后的輸出層換為輸出通道為類別數的1x1卷積層

(輸入在以上結構的向前傳播的結果稱為coarse output)

網絡中加入反卷積層進行上采樣(實際是固定的雙線性插值)

作者提出了一種新穎的skip architect(跳躍結構),結合了高層的位置信息和低層的細節信息

3.PNG
  • FCN-32s:將coarse out通過deconv(雙線性插值)直接上采樣32倍

  • FCN-16s:將coarse out通過deconv(雙線性插值)上采樣2倍;使用1x1卷積層處理pool4的輸出使其輸出通道為類別數(額外的預測器);將前兩步結果相加(為方便記作coarse out 2x)后通過deconv(雙線性插值)上采樣16倍

  • FCN-8s:將coarse out 2x通過deconv(雙線性插值)上采樣2倍;使用1x1卷積層處理pool3的輸出使其輸出通道為類別數(額外的預測器);將前兩步結果相加后通過deconv(雙線性插值)上采樣8倍

當繼續采用更低層輸出的跳躍結構后,模型遇到了衰減回饋(diminishing returns),不能對meanIoU等指標產生明顯的改善,因此跳躍結構僅到8s就截止了。

實驗框架

  • 優化器:SGD with momentum=0.9,weight decay=5^{-4} or 2^{-4}(盡管訓練對這些參數不敏感,但對learning rate敏感),10^{-3},10^{-4},5^{-5} for FCN-AlexNet, Vgg-16, GoogLeNet,原分類器中轉化來的卷積層使用Dropout

  • 微調:需花費很長時間,由FCN-32s(微調時作者用了3天......)向16s和8s微調

  • Patch sampling:使用整個圖像進行訓練的效果和sampling patches的效果差不多,且整個圖像進行訓練需要的收斂時間更短,所以直接使用完整圖像進行訓練

  • Class Balancing:正負類的不平衡(背景類為負類)對訓練的效果沒有顯著影響(所以作者直接使用了所有像素計算loss,而沒有進行hard negative mining)

  • Dense Prediction:采用deconv(雙線性插值)進行上采樣,而未使用3節中其他trick

  • 數據增強:隨機鏡像和縮小輸入的scale(增強網絡對小尺度目標的能力)并未產生顯著的效果提升

  • 更多的訓練數據:更好的效果

5. My codes

我使用的是PASCAL VOC2012的數據集,按其劃分好的trainval來進行訓練

為每個分割圖像進行標注:每個pixel表為對應的類別(0-20,0代表背景)

# 每個RGB顏色的值及其標注的類別
VOC_COLORMAP = [[0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0],
                [0, 0, 128], [128, 0, 128], [0, 128, 128], [128, 128, 128],
                [64, 0, 0], [192, 0, 0], [64, 128, 0], [192, 128, 0],
                [64, 0, 128], [192, 0, 128], [64, 128, 128], [192, 128, 128],
                [0, 64, 0], [128, 64, 0], [0, 192, 0], [128, 192, 0],
                [0, 64, 128]]

VOC_CLASSES = ['background', 'aeroplane', 'bicycle', 'bird', 'boat',
               'bottle', 'bus', 'car', 'cat', 'chair', 'cow',
               'diningtable', 'dog', 'horse', 'motorbike', 'person',
               'potted plant', 'sheep', 'sofa', 'train', 'tv/monitor']

# CLASSES_LABEL = {k: v for k, v in zip(VOC_COLORMAP, VOC_CLASSES)}

# 為每個(R, G, B)組合分配類別
colormap2label = torch.zeros((256, 256, 256), dtype=torch.long)
for i, color in enumerate(VOC_COLORMAP):
    colormap2label[color[0], color[1], color[2]] = i


def get_pixel_label(segmentation_image):
    """
    為分割標記圖像的每個像素分配類別標簽
    :param segmentation_image: 標記圖像,a PIL image
    :return: a tensor of (image.height, image.width),為每個像素分配了類別標簽
    """
    cmap = np.array(segmentation_image.convert('RGB'), dtype=np.uint8)

    cmap = colormap2label[
        cmap[:, :, 0].flatten().tolist(), cmap[:, :, 1].flatten().tolist(), cmap[:, :, 2].flatten().tolist()].reshape(
        cmap.shape[0], cmap.shape[1])
    return cmap

網絡的結構

這里只列出了FCN32s和FCN8s,使用的是Vgg-16預訓練模型(注意deconv的權重初始化雙線性插值,不再對其權重進行學習)

import torch
from torch import nn
import torchvision
import numpy as np

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')


def get_bilinear_weights(in_channels, out_channels, kernel_size):
    """
    構造雙線性插值的上采樣的權重
    :param in_channels: 轉置卷積層的輸入通道數
    :param out_channels: 轉置卷積層的輸出通道數
    :param kernel_size: 轉置卷積層的卷積核大小
    :return: 權重, a tensor in shape of (in_channels, out_channels , kernel_size, kernel_size)
    """
    factor = (kernel_size + 1) // 2
    if kernel_size % 2 == 1:
        center = factor - 1  # array從0開始以需要-1
    else:
        center = factor - 0.5  # center = factor + 0.5 - 1
    og = np.ogrid[:kernel_size, :kernel_size]
    filt = (1 - abs(og[0] - center) / factor) * (1 - abs(og[1] - center) / factor)
    weight = np.zeros((in_channels, out_channels, kernel_size, kernel_size), dtype=np.float32)
    weight[range(in_channels), range(out_channels), :, :] = filt  # 只對對角線上核的值進行替換
    return torch.from_numpy(weight)


class FCN32s(nn.Module):
    def __init__(self, n_classes):
        super(FCN32s, self).__init__()

        # 直接使用Vgg-16預訓練網絡,拋棄classifier層,并把fc層轉換為卷積層
        # fc6轉化為conv6,使用的卷積核大小為7x7,該層輸出長度有6個像素的損失,
        # 向上采樣32倍即原始空間192個像素的損失,因而小于192x192的輸入會導致報錯
        # 同時這些像素損失必需通過padding使上采樣的空間大小與原輸入空間一致
        # 其實這個值可以屬于(96,112)都能達到以上效果

        self.conv1_1 = nn.Conv2d(3, 64, kernel_size=3, padding=100)
        self.conv1_2 = nn.Conv2d(64, 64, kernel_size=3, padding=1)
        self.pool1 = nn.MaxPool2d(2, 2)

        self.conv2_1 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
        self.conv2_2 = nn.Conv2d(128, 128, kernel_size=3, padding=1)
        self.pool2 = nn.MaxPool2d(2, 2)

        self.conv3_1 = nn.Conv2d(128, 256, kernel_size=3, padding=1)
        self.conv3_2 = nn.Conv2d(256, 256, kernel_size=3, padding=1)
        self.conv3_3 = nn.Conv2d(256, 256, kernel_size=3, padding=1)
        self.pool3 = nn.MaxPool2d(2, 2)

        self.conv4_1 = nn.Conv2d(256, 512, kernel_size=3, padding=1)
        self.conv4_2 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.conv4_3 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.pool4 = nn.MaxPool2d(2, 2)

        self.conv5_1 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.conv5_2 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.conv5_3 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.pool5 = nn.MaxPool2d(2, 2)

        self.conv6 = nn.Conv2d(512, 4096, kernel_size=7)
        self.dropout6 = nn.Dropout2d()

        self.conv7 = nn.Conv2d(4096, 4096, kernel_size=1)
        self.dropout7 = nn.Dropout2d()

        self.load_pretrained_layers()

        self.score = nn.Conv2d(4096, n_classes, 1)

        # 此處的kernel_size我認為是作者主觀選擇的,默認是下采樣率的2倍
        self.upsample = nn.ConvTranspose2d(n_classes, n_classes, kernel_size=64, stride=32, bias=False)

        self.upsample.weight.data = get_bilinear_weights(n_classes, n_classes, kernel_size=64)
        self.upsample.weight.requires_grad = False

    def forward(self, x):
        # 我們假設輸入圖片的height, width均為能被32整除
        out = torch.relu(self.conv1_1(x))  # (b, 64, h+198, w+198)
        out = torch.relu(self.conv1_2(out))  # (b, 64, h+198, w+198)
        out = self.pool1(out)  # (b, 64, h/2 + 99, w/2 +99)

        out = torch.relu(self.conv2_1(out))  # (b, 128, h/2+99, w+99)
        out = torch.relu(self.conv2_2(out))  # (b, 128, h/2+99, w+99)
        out = self.pool2(out)  # (b, 128, h/4 + 49, w/4 + 49)

        out = torch.relu(self.conv3_1(out))
        out = torch.relu(self.conv3_2(out))
        out = torch.relu(self.conv3_3(out))
        out = self.pool3(out)  # (b, 256, h/8 + 24, w/18 + 24)

        out = torch.relu(self.conv4_1(out))
        out = torch.relu(self.conv4_2(out))
        out = torch.relu(self.conv4_3(out))
        out = self.pool4(out)  # (b, 512, h/16 + 12, w/16 + 12)

        out = torch.relu(self.conv5_1(out))
        out = torch.relu(self.conv5_2(out))
        out = torch.relu(self.conv5_3(out))
        out = self.pool5(out)  # (b, 512, h/32 + 6, w/32 + 6)

        out = torch.relu(self.conv6(out))  # (b, 512, h/32, w/32)
        out = self.dropout6(out)

        out = torch.relu(self.conv7(out))
        out = self.dropout7(out)

        out = self.score(out)

        # 由于轉置卷積的卷積核大小使上采樣32倍后比原始size大了(kernel_size - stride)
        out = self.upsample(out)  # (b, n_classes, h+32, w+32)

        return out[:, :, 16:16 + x.shape[2], 16:16 + x.shape[3]].contiguous()

def load_pretrained_layers(self):
        state_dict = self.state_dict()
        param_names = list(state_dict.keys())

        pretrained_state_dict = torchvision.models.vgg16(pretrained=True).state_dict()
        pretrained_param_names = list(pretrained_state_dict.keys())

        for i, param in enumerate(param_names[:-4]):
            state_dict[param] = pretrained_state_dict[pretrained_param_names[i]]

        state_dict['conv6.weight'] = pretrained_state_dict['classifier.0.weight'].view(4096, 512, 7, 7)
        state_dict['conv6.bias'] = pretrained_state_dict['classifier.0.bias']

        state_dict['conv7.weight'] = pretrained_state_dict['classifier.3.weight'].view(4096, 4096, 1, 1)
        state_dict['conv6.bias'] = pretrained_state_dict['classifier.3.bias']
        self.load_state_dict(state_dict)
    
class FCN8s(nn.Module):
    def __init__(self, n_classes):
        super(FCN8s, self).__init__()

        # 直接使用Vgg-16預訓練網絡,拋棄classifier層,并把fc層轉換為卷積層
        # fc6轉化為conv6,使用的卷積核大小為7x7,該層輸出長度有6個像素的損失,
        # 向上采樣32倍即原始空間192個像素的損失,因而小于192x192的輸入會導致報錯
        # 同時這些像素損失必需通過padding使上采樣的空間大小與原輸入空間一致
        # 其實這個值可以屬于(96,112)都能達到以上效果

        self.conv1_1 = nn.Conv2d(3, 64, kernel_size=3, padding=100)
        self.conv1_2 = nn.Conv2d(64, 64, kernel_size=3, padding=1)
        self.pool1 = nn.MaxPool2d(2, 2)

        self.conv2_1 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
        self.conv2_2 = nn.Conv2d(128, 128, kernel_size=3, padding=1)
        self.pool2 = nn.MaxPool2d(2, 2)

        self.conv3_1 = nn.Conv2d(128, 256, kernel_size=3, padding=1)
        self.conv3_2 = nn.Conv2d(256, 256, kernel_size=3, padding=1)
        self.conv3_3 = nn.Conv2d(256, 256, kernel_size=3, padding=1)
        self.pool3 = nn.MaxPool2d(2, 2)

        self.conv4_1 = nn.Conv2d(256, 512, kernel_size=3, padding=1)
        self.conv4_2 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.conv4_3 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.pool4 = nn.MaxPool2d(2, 2)

        self.conv5_1 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.conv5_2 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.conv5_3 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.pool5 = nn.MaxPool2d(2, 2)

        self.conv6 = nn.Conv2d(512, 4096, kernel_size=7)
        self.dropout6 = nn.Dropout2d()

        self.conv7 = nn.Conv2d(4096, 4096, kernel_size=1)
        self.dropout7 = nn.Dropout2d()

        self.load_pretrained_layers()

        self.score = nn.Conv2d(4096, n_classes, 1)
        self.score_pool4 = nn.Conv2d(512, n_classes, 1)
        self.score_pool3 = nn.Conv2d(256, n_classes, 1)

        # 此處的kernel_size我認為是作者主觀選擇的,默認是下采樣率的2倍
        self.upsample_2x = nn.ConvTranspose2d(n_classes, n_classes, kernel_size=4, stride=2, bias=False)
        self.upsample_8x = nn.ConvTranspose2d(n_classes, n_classes, kernel_size=16, stride=8, bias=False)

        self.upsample_2x.weight.data = get_bilinear_weights(n_classes, n_classes, kernel_size=4)
        self.upsample_2x.weight.requires_grad = False
        self.upsample_8x.weight.data = get_bilinear_weights(n_classes, n_classes, kernel_size=16)
        self.upsample_8x.weight.requires_grad = False

    def forward(self, x):
        # 我們假設輸入圖片的height, width均為能被32整除
        out = torch.relu(self.conv1_1(x))  # (b, 64, h+198, w+198)
        out = torch.relu(self.conv1_2(out))  # (b, 64, h+198, w+198)
        out = self.pool1(out)  # (b, 64, h/2 + 99, w/2 +99)

        out = torch.relu(self.conv2_1(out))  # (b, 128, h/2+99, w+99)
        out = torch.relu(self.conv2_2(out))  # (b, 128, h/2+99, w+99)
        out = self.pool2(out)  # (b, 128, h/4 + 49, w/4 + 49)

        out = torch.relu(self.conv3_1(out))
        out = torch.relu(self.conv3_2(out))
        out = torch.relu(self.conv3_3(out))
        out = self.pool3(out)  # (b, 256, h/8 + 24, w/8 + 24)
        pool3 = out

        out = torch.relu(self.conv4_1(out))
        out = torch.relu(self.conv4_2(out))
        out = torch.relu(self.conv4_3(out))
        out = self.pool4(out)  # (b, 512, h/16 + 12, w/16 + 12)
        pool4 = out

        out = torch.relu(self.conv5_1(out))
        out = torch.relu(self.conv5_2(out))
        out = torch.relu(self.conv5_3(out))
        out = self.pool5(out)  # (b, 512, h/32 + 6, w/32 + 6)

        out = torch.relu(self.conv6(out))  # (b, 512, h/32, w/32)
        out = self.dropout6(out)

        out = torch.relu(self.conv7(out))
        out = self.dropout7(out)

        out = self.score(out)

        # 由于轉置卷積的卷積核大小使上采樣32倍后比原始size大了(kernel_size - stride)
        out = self.upsample_2x(out)  # (b, n_classes, h/16 + 2, w/16 + 2)
        pool4 = self.score_pool4(pool4)  # (b, n_classes, h/16 + 12, w/16 + 12)
        out = out + pool4[:, :, 5:5 + out.size(2), 5:5 + out.size(3)]  # (b, n_classes, h/16 + 2, w/16 + 2)

        out = self.upsample_2x(out)  # (b, n_classes, h/8 + 4 + 2, w/8 + 4 + 2)
        pool3 = self.score_pool3(pool3)  # (b, 256, h/8 + 24, w/8 + 24)
        out = out + pool3[:, :, 9:9 + out.size(2), 9:9 + out.size(3)]  # (b, n_classes, h/8 + 6, w/8 + 6)

        out = self.upsample_8x(out)  # (b, n_classes, h + 48 + 8, w + 48 + 8)

        return out[:, :, 28:28 + x.shape[2], 28:28 + x.shape[3]].contiguous()

    def load_pretrained_layers(self):
        state_dict = self.state_dict()
        param_names = list(state_dict.keys())

        pretrained_state_dict = torchvision.models.vgg16(pretrained=True).state_dict()
        pretrained_param_names = list(pretrained_state_dict.keys())

        for i, param in enumerate(param_names[:-4]):
            state_dict[param] = pretrained_state_dict[pretrained_param_names[i]]

        state_dict['conv6.weight'] = pretrained_state_dict['classifier.0.weight'].view(4096, 512, 7, 7)
        state_dict['conv6.bias'] = pretrained_state_dict['classifier.0.bias']

        state_dict['conv7.weight'] = pretrained_state_dict['classifier.3.weight'].view(4096, 4096, 1, 1)
        state_dict['conv6.bias'] = pretrained_state_dict['classifier.3.bias']
        self.load_state_dict(state_dict)

由于正負類不平衡對于FCN無影響(見第4節),直接使用交叉熵的計算方法來計算pixel loss(注意是2D版)

(其實也可以進行Hard Negative Mining來加快收斂,這里簡單起見使用這種方法)

class LossFunction(nn.Module):
    def __init__(self):
        super(LossFunction, self).__init__()
        self.loss = nn.NLLLoss()

     def forward(self, pred, target):
         pred = nn.functional.log_softmax(pred, dim=1)
         loss = self.loss(pred, target)
         return loss

接下來的Dataset、DataLoader的構建、train和valid的具體函數不再詳細寫了(所有項目都差不多??)

注意

  • 在進行數據增廣時(resize),插值的方法一定要選擇NEAREAST而不是默認的Bilinear,否則會對true label image的pixel進行誤標,導致問題的出現
  • 訓練要有足夠的耐心,作者的32s都訓練了3天
  • 關于batch_size,如果選擇不進行resize,可以將batch_size設為1

一些衡量的Metrics見:wkentaro/pytorch-fcn,它的算法方法非常巧妙

結果:

6.我的問題

從上面的分割結果來看效果還可以...但那些Metrics的值一直上不去...可能是我訓練時間的問題吧(我只訓練了大概一天,可能這是最大的問題了吧,對復雜的圖像的分割能力有待加強??),但mIoU只達到了0.28...而且難以再升上去,這個地方使我很苦惱(可能真得訓練個3天吧??)

這里更新一下:終于找到mIoU上不去的原因了
這個問題所在其實很傻,就是在模型的load_pretrained_layer()中,最后忘記加上了self.load_state_dict()了,等于是預訓練的網絡參數沒有用上,而是重新直接訓練了??
其實就這點問題導致訓練時間拉了極長、輸出為黑的情況出現很長時間。FCN32s的精度太差,收斂的時間還是會稍久一點的,但也不會像重新訓練一樣那么慢
心碎了一地??

我思考了一下問題在哪里,可能是數據集過少的問題,也跟可能是某種類別難以識別(有些類的IoU明顯較差),訓練數據本身不平衡、標注本來就不準確什么的...也可能是FCN模型的真實能力并非想象中那么好...可以試一下讓網絡學習deconv層的參數,亦或直接按照encoder-decoder的做法重新構建一下網絡(雖然更耗時,但肯定能提高細節的預測)

其實大家有功夫可以多訓練一下看看效果,我看那種自動駕駛的訓練集(Cityscapes)的訓練效果會更好一點(數據集里沒有背景類)

Reference

[1] Long, J., Shelhamer, E., & Darrell, T. (2015). Fully convolutional networks for semantic segmentation. In Proceedings of the IEEE conference on computer vision and pattern recognition (pp. 3431-3440)

[2] 《動手學深度學習》

[3] wkentaro/pytorch-fcn

轉載請說明出處。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。