【论文实操】从ACNet中得到启发:非对称卷积块的使用可以有效提高ACC。即插即用!

887 阅读4分钟

源码:github.com/DingXiaoH/A…
论文:arxiv.org/pdf/1908.03…

前言

  在推理阶段我们往往关心的是训练阶段得到的权重结构是否能够“优秀”,我们在训练阶段往往是不那么在乎训练耗时。如果能有一个trick能够有效的增加ACC,只要在现实可接受 的范围内,那么这个trick可以认定为一个“好trick”。

  在训练阶段使用非对称卷积(以下简称ACBlock)块可以做到(替代非1 x 1 的卷积核的卷积层)提高权重的ACC值,在推理阶段还原被替换的卷积层。 ACBlock的使用可以融合到其他网络中,做到即插即用。

原理简介

ACNET精髓部分

  如下图所示:我们将每3× 3层替换为由3× 3层、1×3和3×1内核组成的ACB,并对其输出进行求和。训练完成后,我们将每个ACB中的不对称核加入到骨架上,即正方形核的交叉部分,如图所示,将模型转换回与原来相同的结构。在实践中,这种转换是通过使用原始结构构建一个新的模型,并使用转换后的ACNet学习参数对其进行初始化来实现的。

img.png

ACNET过程

  我们使用滑动窗口来提供具有不同核大小的2D卷积模块。这里我们有三个卷积层,它们的核大小分别为3 × 3,1 × 3和3 × 1,它们的输入相同。

  例如,我们只描述左上角和右下角的滑动窗口。可以观察到,保持可加性的关键是三个层可以共享同一个滑动窗口。因此,如果我们将conv2和conv3的核加到conv1的相应位置上,使用得到的核对原始输入进行运算将产生相同的结果,仅使用乘法的分配律(Eq. 5)就可以很容易地验证这一点。

            img_1.png

  BN和分支融合。假设我是输入特征图M的任意一个变量,对于每个分支,我们首先等效地将批量归一化的参数融合为卷积核和一个偏置项,然后将融合的核和偏置项相加,得到一层。

             img_2.png

代码实现

import torch
from torch import nn


class CropLayer(nn.Module):

    # (- 1,0)表示该层应该裁剪特征映射的第一行和最后一行。(0, -1)对第一列和最后一列进行裁剪
    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'):
        super(ACBlock, self).__init__()

        # 训练
        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=(kernel_size, 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, kernel_size),
                                  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):
        square_outputs = self.square_conv(input)
        square_outputs = self.square_bn(square_outputs)

        vertical_outputs = self.ver_conv_crop_layer(input)
        vertical_outputs = self.ver_conv(vertical_outputs)
        vertical_outputs = self.ver_bn(vertical_outputs)

        horizontal_outputs = self.hor_conv_crop_layer(input)
        horizontal_outputs = self.hor_conv(horizontal_outputs)
        horizontal_outputs = self.hor_bn(horizontal_outputs)

        return square_outputs + vertical_outputs + horizontal_outputs


if __name__ == "__main__":
    x = torch.ones(1, 3, 224, 224)
    ACN_Conv2d = ACBlock(in_channels=3, out_channels=64, kernel_size=3, padding=1)
    y = ACN_Conv2d(x)
    print(y.shape)

实验验证

  我们使用Alexnet作为模板将ACNET融合进去,进行实验验证。更改的网络如下:

import torch
import torch.nn as nn
from nets import ACBlock

class AlexNetAC(nn.Module):

    def __init__(self, num_classes: int = 1000) -> None:
        super(AlexNetAC, self).__init__()
        self.features = nn.Sequential(
            # nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=2),
            ACBlock(in_channels=3, out_channels=64, kernel_size=3,  padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
            # nn.Conv2d(64, 192, kernel_size=5, padding=2),
            ACBlock(in_channels=64, out_channels=192, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
            # nn.Conv2d(192, 384, kernel_size=3, padding=1),
            ACBlock(in_channels=192, out_channels=384, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            # nn.Conv2d(384, 256, kernel_size=3, padding=1),
            ACBlock(in_channels=384, out_channels=256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            # nn.Conv2d(256, 256, kernel_size=3, padding=1),
            ACBlock(in_channels=256, out_channels=256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
        )
        self.avgpool = nn.AdaptiveAvgPool2d((6, 6))
        self.classifier = nn.Sequential(
            nn.Dropout(),
            nn.Linear(256 * 6 * 6, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(),
            nn.Linear(4096, 4096),
            nn.ReLU(inplace=True),
            nn.Linear(4096, num_classes),
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        x = self.features(x)
        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.classifier(x)
        return x

结语

  在原论文中讲解到了卷积融合的地方不限于相加,大家可以尝试其他的方式进行融合square_outputs 、 vertical_outputs 和 horizontal_outputs。本人能力有限,路过的各位大神若发现纰漏的地方还望指教一二!感谢!希望本文能够帮助到大家。