1. 程式人生 > 其它 >【CNN結構設計】無痛的漲點技巧:ACNet

【CNN結構設計】無痛的漲點技巧:ACNet

【CNN結構設計】無痛的漲點技巧:ACNet

公眾號:GiantPandaCV
連結:https://mp.weixin.qq.com/s/ZKjHBpxl4kl83xsGO__tmg
標題:【CNN結構設計】無痛的漲點技巧:ACNet
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

論文連結:https://arxiv.org/pdf/1908.03930.pdf

1. 前言

不知道你是否發現了,CNN的結構創新在這兩年已經變得相對很少了,同時要做出有影響力並且Solid的工作也變得越來越難,最近CNN結構方面的創新主要包含兩個方面:

  • 網路結構搜尋,以Google Brain的EfficientNet為代表作。
  • 獲取更好的特徵表達,主要是將特徵複用,特徵細化做得更加極致,以HRNet,Res2Net等為代表作。

本文要介紹的是ICCV 2019的一個新CNN架構ACNet(全稱為Asymmetric Convolution Net),但是可能很多同學沒有實測或者對裡面要表達的核心思想並沒有捕捉,因此這篇文章的目的是講清楚ACNet的原理並總結它的核心思想,另外借助作者開源的Pytorch程式碼端來加深理解。

2. 介紹

ACNet的切入點為獲取更好的特徵表達,但和其它方法最大的區別在於它沒有帶來額外的超引數,而且在推理階段沒有增加計算量,這是十分具有吸引力的。

在正式介紹ACNet之前,首先來明確一下關於卷積計算的一個等式,這個等式表達的意思就是【對於輸入特徵圖,先進行K(1)和I卷積,K(2)和I卷積後再對結果進行相加,與先進行K(1)和K(2)的逐點相加後再和I進行卷積得到的結果是一致的】

。這也是ACNet在推理階段不增加任何計算量的理論基礎。

\[\boldsymbol{I} * \boldsymbol{K}^{(1)}+\boldsymbol{I} * \boldsymbol{K}^{(2)}=\boldsymbol{I} *\left(\boldsymbol{K}^{(1)} \oplus \boldsymbol{K}^{(2)}\right) \]

3. ACNet原理

下面的Figure1展示了ACNet的思想:

巨集觀上來看「ACNet分為訓練和推理階段,訓練階段重點在於強化特徵提取,實現效果提升。而測試階段重點在於卷積核融合,不增加任何計算量」

  • 「訓練階段」

    :因為卷積是大多數網路的基礎元件,因此ACNet的實驗都是針對3x3卷積進行的。訓練階段就是將現有網路中的每一個3x3卷積換成3x1卷積+1x3卷積+3x3卷積共三個卷積層,最終將這三個卷積層的計算結果進行融合獲得卷積層的輸出。因為這個過程中引入的1x3卷積和3x1卷積是非對稱的,所以將其命名為Asymmetric Convolution。

  • 「推理階段」:如上圖右半部分所示,這部分主要是對三個卷積核進行融合。這部分在實現過程中就是使用融合後的卷積核引數來初始化現有的網路,因此在推理階段,網路結構和原始網路是完全一樣的了,只不過網路引數採用了特徵提取能力更強的引數即融合後的卷積核引數,因此在推理階段不會增加計算量。

總結一下就是ACNet在訓練階段強化了原始網路的特徵提取能力,在推理階段融合卷積核達到不增加計算量的目的。雖然訓練時間增加了一些時間,但卻換來了在推理階段速度無痛的精度提升,怎麼看都是一筆非常划算的交易。下面的Table3展示了在一些經典網路上應用ACNet的結果,對於AlexNet精度提升了比較多,而對ResNet和DenseNet精度則提升不到一個百分點,不過考慮到這個提升是白賺的也還是非常值得肯定的。

總結一下就是ACNet在訓練階段強化了原始網路的特徵提取能力,在推理階段融合卷積核達到不增加計算量的目的。雖然訓練時間增加了一些時間,但卻換來了在推理階段速度無痛的精度提升,怎麼看都是一筆非常划算的交易。下面的Table3展示了在一些經典網路上應用ACNet的結果,對於AlexNet精度提升了比較多,而對ResNet和DenseNet精度則提升不到一個百分點,不過考慮到這個提升是白賺的也還是非常值得肯定的。

4. 為什麼ACNet能漲點?

為什麼ACNet這個看起來十分簡單的操作能為各種網路帶來漲點?論文中提到,ACNet有一個特點是「它提升了模型對影象翻轉和旋轉的魯棒性」,例如訓練好後的1x3卷積和在影象翻轉後仍然能提取正確的特徵(如Figure4左圖所示,2個紅色矩形框就是影象翻轉前後的特徵提取操作,在輸入影象的相同位置處提取出來的特徵還是一樣的)。那麼假設訓練階段只用3x3卷積核,當影象上下翻轉之後,如Figure4右圖所示,提取出來的特徵顯然是不一樣的。

因此,引入1x3這樣的水平卷積核可以提升模型對影象上下翻轉的魯棒性,豎直方向的3x1卷積核同理。

5. Pytorch程式碼實現

我們來看一下作者的ACNet基礎結構Pytorch實現,即將原始的3x3卷積變成3x3+1x3+3x1:

 #去掉因為3x3卷積的padding多出來的行或者列
class CropLayer(nn.Module):

    #   E.g., (-1, 0) means this layer should crop the first and last rows of the feature map. And (0, -1) crops the first and last columns
    def __init__(self, crop_set):
        super(CropLayer, self).__init__()
        self.rows_to_crop = - crop_set[0]
        self.cols_to_crop = - crop_set[1]
        assert self.rows_to_crop >= 0
        assert self.cols_to_crop >= 0

    def forward(self, input):
        return input[:, :, self.rows_to_crop:-self.rows_to_crop, self.cols_to_crop:-self.cols_to_crop]

# 論文提出的3x3+1x3+3x1
class ACBlock(nn.Module):

    def __init__(self, in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, padding_mode='zeros', deploy=False):
        super(ACBlock, self).__init__()
        self.deploy = deploy
        if deploy:
            self.fused_conv = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=(kernel_size,kernel_size), stride=stride,
                                      padding=padding, dilation=dilation, groups=groups, bias=True, padding_mode=padding_mode)
        else:
            self.square_conv = nn.Conv2d(in_channels=in_channels, out_channels=out_channels,
                                         kernel_size=(kernel_size, kernel_size), stride=stride,
                                         padding=padding, dilation=dilation, groups=groups, bias=False,
                                         padding_mode=padding_mode)
            self.square_bn = nn.BatchNorm2d(num_features=out_channels)

            center_offset_from_origin_border = padding - kernel_size // 2
            ver_pad_or_crop = (center_offset_from_origin_border + 1, center_offset_from_origin_border)
            hor_pad_or_crop = (center_offset_from_origin_border, center_offset_from_origin_border + 1)
            if center_offset_from_origin_border >= 0:
                self.ver_conv_crop_layer = nn.Identity()
                ver_conv_padding = ver_pad_or_crop
                self.hor_conv_crop_layer = nn.Identity()
                hor_conv_padding = hor_pad_or_crop
            else:
                self.ver_conv_crop_layer = CropLayer(crop_set=ver_pad_or_crop)
                ver_conv_padding = (0, 0)
                self.hor_conv_crop_layer = CropLayer(crop_set=hor_pad_or_crop)
                hor_conv_padding = (0, 0)
            self.ver_conv = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=(3, 1),
                                      stride=stride,
                                      padding=ver_conv_padding, dilation=dilation, groups=groups, bias=False,
                                      padding_mode=padding_mode)

            self.hor_conv = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=(1, 3),
                                      stride=stride,
                                      padding=hor_conv_padding, dilation=dilation, groups=groups, bias=False,
                                      padding_mode=padding_mode)
            self.ver_bn = nn.BatchNorm2d(num_features=out_channels)
            self.hor_bn = nn.BatchNorm2d(num_features=out_channels)


    # forward函式
    def forward(self, input):
        if self.deploy:
            return self.fused_conv(input)
        else:
            square_outputs = self.square_conv(input)
            square_outputs = self.square_bn(square_outputs)
            # print(square_outputs.size())
            # return square_outputs
            vertical_outputs = self.ver_conv_crop_layer(input)
            vertical_outputs = self.ver_conv(vertical_outputs)
            vertical_outputs = self.ver_bn(vertical_outputs)
            # print(vertical_outputs.size())
            horizontal_outputs = self.hor_conv_crop_layer(input)
            horizontal_outputs = self.hor_conv(horizontal_outputs)
            horizontal_outputs = self.hor_bn(horizontal_outputs)
            # print(horizontal_outputs.size())
            return square_outputs + vertical_outputs + horizontal_outputs

然後在推理階段進行卷積核融合的程式碼實現地址為:acnet_fusion.py

6. 思考

從實驗結果中可以看到,在推理階段即使融合操作放在BN層之前,相比原始網路仍有一定提升(AlexNet的56.18% vs 55.92%,ResNet-18的70.82% vs 70.36%),作者沒有講解這部分的原理,我比較同意魏凱峰大佬的觀點,如下:

這部分的原因個人理解是來自梯度差異化,原來只有一個3x3卷積層,梯度可以看出一份,而添加了1x3和3x1卷積層後,部分位置的梯度變為2份和3份,也是更加細化了。而且理論上可以融合無數個卷積層不斷逼近現有網路的效果極限,融合方式不限於相加(訓練和推理階段一致即可),融合的卷積層也不限於或尺寸。