《ACNet》

读这篇文章的目的就是单纯冲着网络模型的,看在去噪任务上是否可以达到很好的结果。ACNet:通过非对称卷积块增强强大的CNN的内核骨架

一、论文

《ACNet: Strengthening the Kernel Skeletons for Powerful CNN via Asymmetric Convolution Blocks》

摘要:由于在给定应用程序的背景下设计适当的卷积神经网络(CNN)架构通常会涉及大量的人工工作或大量的GPU时间,因此研究团体正在征求架构中立的CNN结构,可以轻松地将其插入多个成熟的架构中以提高性能 在我们的实际应用中。 我们提出非对称卷积块(ACB),这是一种与结构无关的结构,作为CNN构造块,它使用一维非对称卷积来增强平方卷积核。 对于现成的体系结构,我们用ACB取代标准的方形内核卷积层,以构建非对称卷积网络(ACNet),可以对其进行训练以达到更高的准确性。 经过培训,我们等效地将ACNet转换为相同的原始体系结构,因此不再需要额外的计算。 我们已经观察到ACNet可以明显改善CIFAR和ImageNet上各种模型的性能。 通过进一步的实验,我们将ACB的有效性归因于其增强模型对旋转失真的鲁棒性以及增强方形卷积核的中心骨架部分的能力。

二、网络模型

在本文中,我们提出了一种非对称卷积块(ACB),它是一种创新的结构,可以用正方形核(例如3×3层)代替标准的卷积层,该层在现代CNN中被广泛使用。 具体来说,为了替换ad×d层,我们构建了一个ACB,该ACB包括三个分别具有d×d,1×d和d×1内核的并行层,将它们的输出相加以丰富特征空间(图 1)。

图1:ACNet概述。 例如,我们用ACB替换每3×3层,该ACB分别包含3个内核分别为3×3、1×3和3×1内核的三层,并将它们的输出求和。 训练完成后,我们通过将每个ACB中的非对称核添加到骨架上来将模型转换为与原始结构相同的结构,该骨架是方形核的交叉部分,如图所示。 实际上,这种转换是通过使用原始结构构建新模型并使用转换后的ACNet学习参数进行初始化来实现的。

图2:我们使用滑动窗口来直观了解具有不同内核大小的2D卷积的可加性。 在这里,我们有三个卷积层,其内核大小分别为3×3、1×3和3×1,它们采用相同的输入。 例如,我们仅在左上角和右下角描绘滑动窗口。 可以看出,保持可加性的关键是三层可以共享同一滑动窗口。 因此,如果将conv2和conv3的内核添加到conv1的相应位置上,则使用结果内核对原始输入进行操作将产生相同的结果,只有使用乘法的分布特性才能轻松验证(公式5  )。 最佳观看颜色。

图3:BN和分支融合。 令I成为输入特征图M的任意通道,对于每个分支,我们首先将批量归一化的参数等效地融合到卷积核和一个偏差项中,然后将融合核和偏差项相加以获得单层。

三、不对称卷积网络

3.1 公式

对于具有H×W和D滤核尺寸的卷积层,并以C通道特征图作为输入,我们使用表示3D卷积核,输入,这是一个具有U×V和C通道的空间分辨率的特征图,而分别为具有D通道的输出。
   对于该层的第j个过滤器,对应的输出特征图通道为

其中∗是2D卷积算子,是M的第k个通道,形式为U×V矩阵,而是第k个输入通道,即H×W的2D核。

在现代的CNN架构中,批量标准化[16]被广泛采用以减少过度拟合并加速训练过程。 通常,批处理归一化层通常会进行线性缩放变换,以增强表示能力。 与等式相比 1,输出通道然后变为

其中µj和σj是通道方式均值和批量归一化标准偏差的值,γj和βj分别是学习的比例因子和偏差项。

3.2 利用卷积的可加性

我们试图以不对称卷积可以等效地融合到标准方内核层中的方式来使用,这样就不会引入额外的推理时间计算负担。 我们注意到卷积的一个有用特性:如果几个具有兼容大小的2D内核在相同的步幅上以相同的步幅运行以产生具有相同分辨率的输出,并且将它们的输出相加,我们可以将这些内核在相应的位置上相加为 获得将产生相同输出的等效内核。也就是说,即使具有不同的内核大小,可加性也适用于2D卷积,

其中I是矩阵,是两个具有兼容大小的2D内核,而⊕是在相应位置将内核参数逐元素相加。 注意,我可能需要适当修剪或填充。

这里的兼容意味着我们可以将较小的内核“修补”到较大的内核。 形式上,如果,则在p和q层上进行这种转换是可行的。 例如3×1和1×3内核与3×3兼容。

通过研究滑动窗口形式的卷积计算,可以很容易地验证这一点(图2)。对于具有内核的某个滤波器,输出通道上的某个点y为:

其中X是输入M上的对应滑动窗口。显然,当我们总结由两个滤波器产生的两个输出通道时,如果一个通道上的每个点y对应于另一个通道上的相应点共享相同的滑动窗口X,则可加性(等式3)成立。

3.3  ACB可免费改善推理时间

在本文中,我们关注3×3卷积,这在现代CNN架构中大量使用。 在给定的体系结构下,我们通过简单地将每个3×3层(以及随后的批处理归一化层,如果有的话)替换为ACB来构建ACNet,该ACB包括三个并行层,内核大小分别为3×3、1×3和3×1  , 分别。

与标准CNN中的常规做法类似,在三层中的每一层之后都进行批归一化,这称为分支,并且将三个分支的输出总和作为ACB的输出。 请注意,我们可以使用与原始模型相同的配置来训练ACNet,而无需调整任何其他超参数。 可以训练ACNet达到更高的准确性。 训练完成后,我们尝试将每个ACB转换为产生相同输出的标准卷积层。 这样,与经过常规训练的对等方相比,我们可以获得不需要任何额外计算的功能更强大的网络。 该转换通过两个步骤完成,即BN融合和分支融合。

BN融合。 卷积的同质性允许随后的批归一化和线性缩放转换等效地融合到卷积层中,并增加了偏差。 从等式可以看出。  2对于每个分支,如果我们构造一个新的内核为并添加一个偏置项,我们将产生相同的输出,可以轻松地对其进行验证。

分支融合。 通过将非对称核添加到方形核的相应位置,我们将三个BN融合分支合并到标准卷积层中。 实际上,这种转换是通过构建原始结构的网络并使用融合的权重进行初始化来实现的,因此,我们可以产生与ACNet相同的输出,并具有与原始体系结构相同的计算预算。 形式上,对于每个滤波器j,令为融合的3D核,bj为获得的偏差项,为在1×3和3处相应滤波器的核 ×1层,我们分别

然后我们可以轻松地验证对于任意滤波器j,O:,:,, j +O¯:,:,j + Oˆ:,:j = CX k = 1 M:,:,k * F:',(  :,kj)+ bj,(8)其中O:,:,j,O¯:,:,j和Oˆ:,:,j是原始3×3、1×3和3×1分支的输出 , 分别。 图3显示了在单个输入通道上获得更多直觉的示例。
   值得注意的是,尽管可以将ACB等价地转换为标准层,但是等效项仅在推理时才成立,因为训练动态是不同的,从而导致了不同的性能。 训练过程的不对等性是由于内核权重的随机初始化以及由它们参与的不同计算流得出的梯度所致。

然后我们可以轻松地验证对于任意滤波器j,

其中是原始3×3、1×3和3×1分支的输出。 图3显示了在单个输入通道上获得更多直觉的示例。

值得注意的是,尽管可以将ACB等价地转换为标准层,但是等效项仅在推理时才成立,因为训练动态是不同的,从而导致了不同的性能。 训练过程的不对等性是由于内核权重的随机初始化以及由它们参与的不同计算流得出的梯度所致。

四、代码 

 https://github.com/DingXiaoH/ACNet

import torch.nn as nn
import torch.nn.init as init
from custom_layers.crop_layer import CropLayer

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,
                 use_affine=True, reduce_gamma=False, use_last_bn=False, gamma_init=None ):
        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, affine=use_affine)

            center_offset_from_origin_border = padding - kernel_size // 2
            ver_pad_or_crop = (padding, center_offset_from_origin_border)
            hor_pad_or_crop = (center_offset_from_origin_border, padding)
            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, affine=use_affine)
            self.hor_bn = nn.BatchNorm2d(num_features=out_channels, affine=use_affine)

            if reduce_gamma:
                assert not use_last_bn
                self.init_gamma(1.0 / 3)

            if use_last_bn:
                assert not reduce_gamma
                self.last_bn = nn.BatchNorm2d(num_features=out_channels, affine=True)

            if gamma_init is not None:
                assert not reduce_gamma
                self.init_gamma(gamma_init)


    def init_gamma(self, gamma_value):
        init.constant_(self.square_bn.weight, gamma_value)
        init.constant_(self.ver_bn.weight, gamma_value)
        init.constant_(self.hor_bn.weight, gamma_value)
        print('init gamma of square, ver and hor as ', gamma_value)

    def single_init(self):
        init.constant_(self.square_bn.weight, 1.0)
        init.constant_(self.ver_bn.weight, 0.0)
        init.constant_(self.hor_bn.weight, 0.0)
        print('init gamma of square as 1, ver and hor as 0')

    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)
            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)
            result = square_outputs + vertical_outputs + horizontal_outputs
            if hasattr(self, 'last_bn'):
                return self.last_bn(result)
            return result
import torch.nn as nn

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):
        if self.rows_to_crop == 0 and self.cols_to_crop == 0:
            return input
        elif self.rows_to_crop > 0 and self.cols_to_crop == 0:
            return input[:, :, self.rows_to_crop:-self.rows_to_crop, :]
        elif self.rows_to_crop == 0 and self.cols_to_crop > 0:
            return input[:, :, :, self.cols_to_crop:-self.cols_to_crop]
        else:
            return input[:, :, self.rows_to_crop:-self.rows_to_crop, self.cols_to_crop:-self.cols_to_crop]

代码和写作风格都是非常值得学习的。

猜你喜欢

转载自blog.csdn.net/LiuJiuXiaoShiTou/article/details/107981939