Large Kernel Matters —— GCN
論文全稱:Large Kernel Matters——Improve Semantic Segmentation by Global Convolutional Network
作者欄裏面有熟悉的Face++的Jian Sun~
原文地址:Large Kernel Matters —— GCN
實現代碼:
Abstract
在現有的模型架構設計中有這樣一個趨勢:
堆疊小卷積核比大卷積核更有效。(主要說的是VGG的
考慮到這一問題,提出了Global Convolutional Network(GCN),同時給出了一個基於殘差(residual,即Resnet的主要貢獻)的boundary refinement(BR)模塊用於細化物體邊界,論文在PASCAL VOC 2012(82.2%)和Cityscapes(76.9% )上達到了state-of-the-art.
Introduction
論文上來先分析Semantic Segmentation。
我們要做的任務分成兩部分:classification
&localization
.
論文指出這兩部分在要義上是相互矛盾的(these two tasks are naturally contradictory
),從而論文提出了一個改進架構Global Convolutional Network(GCN)
。
部分 | 任務 | 要義 | GCN的設計 |
---|---|---|---|
classification | 每個像素有對應的語義信息,即分類要正確 | 要求模型對各種變換有不變形(例如翻轉和旋轉) | 網絡應該採用較大的核,使得feature map與像素分類層之間有密切的連接,從而增強處理不同變換的能力 |
localization | 每個像素分類標籤與對應的種類對齊 | 要求對變換敏感,可以精確分割像素 | 模型應該是完全卷積(沒有FC或全局池化),保持定位信息 |
大致的示意圖如下:
A. Classification : 多次提取高層次的feature map,然後做分類.
例如:AlexNet,VGGNet,GoogleNet,ResNet等,整體呈現的是“圓錐形網絡”(Cone-shaped),這樣的feature map的空間上是粗糙的(高語義的),分類任務可通過後續接FC層或全局池化層,這樣有模型能接收不同類型的輸入,保持較強的魯棒性。B. Segmentation : 輸入和輸出大小相同(feature map尺度沒變),全卷積層最後每個通道對應一個分類結果.
這需要有較大的feature map保持空間信息。所有大多數模型例如:FCN,DeepLab,Deconv-Net採用桶狀(Barrel-shaped)結構,使用Deconvolution, Unpooling 和Dilated-Convolution等技術反向生成高分辨率feature map.- C. GCN : 全卷積設計,最後分類使用了多通道的信息(和ShuffleNet有那麼點神似).
考慮到B中分類器和feature map不是全局連接,難以處理輸入上的變換。例如下圖,分類器與輸入對象的紅點對齊,如果接收野不夠大(valid receptive filed,VRF),則很難分別出來.如果使用更大的feature map,則效果更差.
而GCN就是在使用全卷積的結構上儘可能的使用大的卷積核,從而達到分類和分割平衡。
總結一下Paper的Contributions:
- 針對“classification” and “localization” 提出了GCN架構
- 引入邊界細化模塊(Boundary Refinement block)進一步提高物體邊界的劃分性能
- 在PASCAL VOC 2012(82.2%) and Cityscapes(76.9%)取得state-of-the-art.
Related Work
以FCN爲基礎,從三個方面嘗試提高semantic segmentation 性能:
- Context Embedding : 上下文語義嵌入。這裏列舉了一些工作:ParseNet使用全局池化分支增加額外上下文信息;Dilated-Net使用空洞卷積增加上下文信息;Deeplab-V2使用
Atrous Spatial Pyramid Pooling
模塊增加上下文信息. - Resolution Enlarging : FCN是使用deconvolution來提高層次的feature map的分辨率;在Deconv-Net和SegNet上採用unpooling來學習上採樣;Deeplab和Dilated-Net提出了一個特殊的dilated convolution來擴大feature map的大小,從而達到更大的分辨圖.
- Boundary Alignment : 邊界對齊的任務是優化分割對象的邊界預測。許多方法是使用CRF(條件隨機場)。後續的Deeplab提出的denseCRF等等一大堆的CRF改進版。
這裏點出本文的看法:Semantic Segmentation是在大的feature map上的分類任務.(semantic segmentation is a classification task on large feature map)
Architecture
在GCN架構中,將核大小增加到feature map的空間大小,從而可以獲取全局信息(Global Convolution)。這裏沒有直接使用大卷積核,而是採用GoogleNet的思想(可以參考我以前寫的GoogleNet筆記Inception-V2),將大卷積核拆分爲卷積組合,即GCN模塊(下圖F2.B),這裏在卷積層後不使用非線性,保持計算複雜度爲
模型的整體架構如下(下圖F2.A):
模型使用ResNet作爲特徵提取層(後面實驗有介紹),使用FCN的結構作爲分割架構。不同尺度的特徵是從不同大小的feature map上提取的,對於不同層級的feature map使用GCN提取全局信息,同時高層次的feature map通過上採樣來補充語義信息,最終融合得到預測圖。這裏提出了一個殘差結構(residual structure)的BR模塊(上圖F2.C)學習邊界信息。
Experiment
論文在PASCAL VOC 2012和Cityscapes上做了評估。
項目 | 屬性 |
---|---|
特徵提取層 | 預訓練的Resnet152權重 |
優化器 | 標準的SGD |
batch size | 1 |
權重衰減 | momentum 0.99 and weight decay 0.0005 |
數據增強 | 去均值,水平翻轉 |
度量標準 | 標準平均IoU值 |
實現工具 | Caffe |
實驗方案
將輸入放縮到
- B:單純的
1×1 卷積,提供一個baseline - A:GCN結構,使用參數
k 控制核大小,論文使用了不同k (3到15,使用奇數保持對齊),論證了論文思想。在k=15 是接收野大概爲16×16 ,達到了“全局卷積”,在實驗結果上來看,性能隨着內核大小k 增加而增加,從而論證了GCN的設計思想。
- C:考慮到模型性能提升可能是因爲參數增加,這裏設計一個
k×k 的卷積核做對比,從結果來看,GCN結構比大卷積核效果好,可以看到隨着k增加,大卷積核效果反倒下降了,這其中的原因可能是因爲過擬合,在訓練過程中,因爲參數多,網絡難以收斂,實際原因還有待研究
D:對於大卷積核,可以使用小卷積核堆疊達到類似的接收野,爲了等效比較,在堆疊小卷積時沒用使用非線性(在VGG那些網絡中使用非線性了,但是計算量也上去了),結果是GCN好一點。
同時還比較了堆疊小卷積核數量的影響,隨着卷積核數目減少性能也減少了,結果還是GCN好一點:
其他實驗:爲了進一步分析GCN是如何影響分割結果的,即論證GCN是通過引入密集連接來提高分類結果,通過BR來細化邊緣分割,論文將評分分爲兩部分:
- boundary region:像素點接近分割邊緣的部分,取distance<7
- internal region: 除了boundary region剩餘的像素
實驗結果如下(
k=15 ):
可以看到GCN有效的提高了Internal的評分,BR提高了Boundary的評分.
預訓練模型
考慮到大卷積核的優點,很自然的把GCN的思想應用到ResNet152上,論文提出了一個ResNet-GCN結構,如下圖:
爲了儘可能同條件比較,將Resnet-GCN精心選擇
實驗結果如下:
可以看到,在ImageNet上Resnet-GCN的結果稍差,但是在semantic segmentation上表現好了很多,可以有信心的得出GCN有助於提高分割性能。
PASCAL VOC 2012
在PASCAL VOC 2012數據集上,
先使用MS COCO數據集預訓練,訓練分爲三個階段:
- Stage-1: 將COCO,BSD,標準的PASCAL VOC 2012混合成一個109,892數據集訓練
- Stage-2: 使用BSD,PASCAL VOC 2012預訓練調整模型
- Stage-3: 只使用PASCAL VOC 2012
輸入圖片在Stage-1會填充到
GCN+BR效果較好,同時後添加CRF會進一步提升.
提交到PASCAL VOC 2012獲得了state-of-the-art:
Cityscapes
Cityscapes的圖片大小固定爲
- Stage-1 : 混合標註粗糙和精細的圖片,一共22973張,一起訓練
- Stage-2 : 只對訓練集上圖片做微調
結果如下:
在測試階段,將圖片切分爲4個
Conclusion
給出分析結果:大核有助於提高分類結果,全卷積可以保持分割信息,使用BR可以細化分割結果。後面的實驗給出了驗證,實驗效果還可以。
但是把,提交的結果中分成了多個訓練階段,其中各種的訓練技巧不好復現啊。
模型代碼分析
github上覆現的模型代碼: Pytorch
GCN模塊的實現:
# many are borrowed from https://github.com/ycszen/pytorch-ss/blob/master/gcn.py
class _GlobalConvModule(nn.Module):
def __init__(self, in_dim, out_dim, kernel_size):
super(_GlobalConvModule, self).__init__()
pad0 = (kernel_size[0] - 1) / 2
pad1 = (kernel_size[1] - 1) / 2
# kernel size had better be odd number so as to avoid alignment error
super(_GlobalConvModule, self).__init__()
self.conv_l1 = nn.Conv2d(in_dim, out_dim, kernel_size=(kernel_size[0], 1),
padding=(pad0, 0)) # 左kx1卷積
self.conv_l2 = nn.Conv2d(out_dim, out_dim, kernel_size=(1, kernel_size[1]),
padding=(0, pad1)) # 左1xk卷積
self.conv_r1 = nn.Conv2d(in_dim, out_dim, kernel_size=(1, kernel_size[1]),
padding=(0, pad1)) # 右1xk卷積
self.conv_r2 = nn.Conv2d(out_dim, out_dim, kernel_size=(kernel_size[0], 1),
padding=(pad0, 0)) # 右kx1卷積
def forward(self, x):
x_l = self.conv_l1(x)
x_l = self.conv_l2(x_l)
x_r = self.conv_r1(x)
x_r = self.conv_r2(x_r)
x = x_l + x_r # sum操作
return x
BF模塊實現:
class _BoundaryRefineModule(nn.Module):
def __init__(self, dim):
super(_BoundaryRefineModule, self).__init__()
self.relu = nn.ReLU(inplace=True)
self.conv1 = nn.Conv2d(dim, dim, kernel_size=3, padding=1) # 分支3x3卷積
self.conv2 = nn.Conv2d(dim, dim, kernel_size=3, padding=1) # 分支3x3卷積
def forward(self, x):
residual = self.conv1(x)
residual = self.relu(residual) # Conv + ReLU
residual = self.conv2(residual) # Conv
out = x + residual # sum操作
return out
基於Resnet152的整體架構
class GCN(nn.Module):
def __init__(self, num_classes, input_size, pretrained=True):
super(GCN, self).__init__()
self.input_size = input_size
resnet = models.resnet152()
if pretrained:
resnet.load_state_dict(torch.load(res152_path))
self.layer0 = nn.Sequential(resnet.conv1, resnet.bn1, resnet.relu)
self.layer1 = nn.Sequential(resnet.maxpool, resnet.layer1)
self.layer2 = resnet.layer2
self.layer3 = resnet.layer3
self.layer4 = resnet.layer4
# 所有GCN模塊
self.gcm1 = _GlobalConvModule(2048, num_classes, (7, 7))
self.gcm2 = _GlobalConvModule(1024, num_classes, (7, 7))
self.gcm3 = _GlobalConvModule(512, num_classes, (7, 7))
self.gcm4 = _GlobalConvModule(256, num_classes, (7, 7))
# 所有BR模塊
self.brm1 = _BoundaryRefineModule(num_classes)
self.brm2 = _BoundaryRefineModule(num_classes)
self.brm3 = _BoundaryRefineModule(num_classes)
self.brm4 = _BoundaryRefineModule(num_classes)
self.brm5 = _BoundaryRefineModule(num_classes)
self.brm6 = _BoundaryRefineModule(num_classes)
self.brm7 = _BoundaryRefineModule(num_classes)
self.brm8 = _BoundaryRefineModule(num_classes)
self.brm9 = _BoundaryRefineModule(num_classes)
initialize_weights(self.gcm1, self.gcm2, self.gcm3, self.gcm4, self.brm1, self.brm2, self.brm3,
self.brm4, self.brm5, self.brm6, self.brm7, self.brm8, self.brm9)
def forward(self, x):
# if x: 512
fm0 = self.layer0(x) # 256
fm1 = self.layer1(fm0) # 128
fm2 = self.layer2(fm1) # 64
fm3 = self.layer3(fm2) # 32
fm4 = self.layer4(fm3) # 16
gcfm1 = self.brm1(self.gcm1(fm4)) # 16
gcfm2 = self.brm2(self.gcm2(fm3)) # 32
gcfm3 = self.brm3(self.gcm3(fm2)) # 64
gcfm4 = self.brm4(self.gcm4(fm1)) # 128
# 上採樣融合輸出
fs1 = self.brm5(F.upsample_bilinear(gcfm1, fm3.size()[2:]) + gcfm2) # 32
fs2 = self.brm6(F.upsample_bilinear(fs1, fm2.size()[2:]) + gcfm3) # 64
fs3 = self.brm7(F.upsample_bilinear(fs2, fm1.size()[2:]) + gcfm4) # 128
fs4 = self.brm8(F.upsample_bilinear(fs3, fm0.size()[2:])) # 256
out = self.brm9(F.upsample_bilinear(fs4, self.input_size)) # 512
return out