AI学习者 · 2022年01月21日

RepVGG溯源 | RepVGG作者前期作品,ACNet零算力白嫖性能(附Pytorch代码详解)

image.png

在一个给定的应用程序设计合适的卷积神经网络(CNN)架构通常涉及很大的工作量,很多研究团体正在研究一个结构中立的CNN结构,它可以很容易地插入到多个成熟的架构,以提高应用程序的性能。

本文提出了非对称卷积块(ACB),这是一种中性的结构,可以作为CNN Backbone的构建块,它使用一维非对称卷积来增强正方形卷积核。对于一个现成的架构,用ACB替换标准的卷积层来构造一个非对称卷积网络(ACNet),它可以被训练以达到更高的精度水平。

经过训练后,等价地将ACNet转换为相同的原始架构,因此不再需要额外的计算。作者观察到ACNet可以明显提高CIFAR和ImageNet上各种模型的性能。通过进一步的实验,将ACB的有效性归因于它增强模型对旋转畸变的鲁棒性和增强标准卷积核的能力。

1 简介

卷积神经网络(CNN)在视觉理解方面取得了巨大的成功,可应用到可穿戴设备、安全系统、手机、汽车等领域的各种应用。由于前端设备的计算资源通常有限,需要实时推理,这些应用程序需要CNN在一定水平的计算预算约束下提供较高的精度。因此,通过简单地使用更多可训练的参数和复杂的连接来增强模型可能是不实用的。因此,作者认为在不增加额外的推理时间、内存占用的情况下,提高CNN的性能是非常有意义的。

另一方面,随着CNN架构设计的进步,现成模型的性能得到了显著的提高。然而,当现有的模型不能满足特定需求时,可能不被允许以大量的人工设计或大量的GPU算力为代价来定制一个新的架构。最近,研究界正在积极的设计一个中立的CNN结构,例如,SE Block和Quasi-Hexagonal Kernels可以直接与各种最新的架构结合,以提高应用程序的性能。

一些最近的调查CNN架构关注:

  1. 层之间应该如何相互联系?
  2. 不同层的输出如何结合?

考虑到这一点,为了寻找一个可以与许多架构相结合的通用的CNN结构,作者试图通过挖掘一个正交的方面来加强标准卷积层:权值和kernel中的空间位置之间的关系
image.png
然后,通过将每个ACB中的非对称kernel添加到标准kernel的相应位置,将ACNet等效地转换为相同的原始架构。由于兼容kernel-size的卷积具有可加性(图2),这一点很明显,但长期被忽视,因此得到的模型可以产生与训练时间ACNet相同的输出。正如实验(第4.1、4.2节)所示,这样做可以明显提高CIFAR和ImageNet上几个Baseline模型的性能。更好的是,ACNet :

  1. 没有引入超参数,这样它就可以与不同的体系结构结合,而无需仔细调整;
  2. 可以简单地用主流CNN框架实现;
  3. 与原始架构相比,不需要额外的推理时间计算负担。

通过进一步的实验,已经部分解释了ACNet的有效性。作者观察到,一个标准卷积核对其学习知识的分布不均匀,因为中心交叉位置(称为kernel的“_skeleton_”)的权重通常更大,与Corner相比,去除它们会导致更高的精度下降。

在每个ACB中,将水平和垂直的kernel添加到skeleton上,从而明确地使skeleton更加强大,并遵循标准方形kernel的本质。有趣的是,正方形、水平和垂直kernel对应位置的权值是随机初始化的,有可能符号相反,因此将它们加起来可能导致更强或更弱的skeleton。然而,通过经验观察到一个一致的现象,即模型总是学会增强每一层的skeleton。这一观察结果可以进一步说明了不同空间位置权重之间关系的研究。

主要贡献:

  1. 建议使用非对称卷积显式增强标准的方形卷积核的不对称卷积可以融合到方形kernel,并无需额外的推理时间计算;
  2. 提出将ACB作为一种中立的CNN构建块。可以通过将成熟架构中的每个平方核卷积层替换为ACB,而不引入任何超参数,这样它的有效性就可以与CNN架构设计相结合;
  3. 在CIFAR-10、CIFAR-100和ImageNet上明显提高了几个通用Baseline模型的准确性;
  4. 证明了skeleton在标准方形卷积核中的重要性,并证明了ACNet在增强此类skeleton方面的有效性;
  5. 结果表明,ACNet可以增强模型对旋转畸变的鲁棒性,这可能会启发对旋转不变性问题的进一步研究。

2 相关工作

2.1 不对称卷积

非对称卷积通常用于接近现有的平方核卷积层,以进行通信表达和加速。之前的一些工作已经表明,一个标准的d×d卷积层可以分解为一个具有d×1和1×d卷积层,以减少参数和所需的计算。其背后的理论很简单:如果一个二维kernel的秩为1,那么该操作可以等价地转换为一系列的一维卷积。然而,由于深度网络中学习到的kernel具有分布的特征值,其内在秩在实践中高于1,因此直接将转换应用于kernel会导致显著的信息丢失。

Denton等人通过一种基于奇异值分解(svd)的方式找到一个低秩近似,然后对上层进行微调以恢复性能,从而解决了这个问题。Jaderberg等人通过最小化L-2重构误差,成功地学习了水平和垂直kernel。Jin等人应用结构约束使二维kernel可分,获得了与传统CNN相似的2倍加速性能。

另一方面,非对称卷积作为架构设计元素也被广泛应用,以节省参数和计算。例如,在Inception-v3中,7x7卷积被1x7和7x1卷积序列所取代。然而,作者发现这样的替换是不等同的,因为它在低层次上不起作用。

ENet也采用了这种方法来设计一个高效的语义分割网络,其中5x5卷积被分解,允许在合理的计算预算下增加感受野。EDANet使用了类似的方法来分解3x3卷积,结果在性能略有下降的情况下,参数数量和所需的计算量节省了33%。

相比之下,使用一维非对称卷积,不将任何层分解作为架构设计的一部分,而是在训练过程中丰富特征空间,然后将其学习到的知识融合到正方形kernel层中。

2.2  Architecture-neutral CNN structures

这里不打算修改CNN架构,而是使用一些与架构无关的结构来增强现成的模型。因此,本文方法的有效性是对创新架构所取得的进展的补充。具体来说,CNN结构可以被称为体系结构中立(Architecture-Neutral),如果它:

  1. 对具体的体系结构没有任何先验需求
  2. 具有普适性

例如,SE块可以在卷积层之后添加,用学习过的权值重新缩放特征图通道,从而在合理的额外参数和计算负担的代价下获得明显的准确性提高。另一个例子是,可以在模型中插入辅助分类器,以协助监督学习过程,这确实可以在可观察的范围内提高性能,但需要额外的人工工作来调整超参数。

3 不对称卷积网络

3.1 公式

image.png

import torch.nn as nn
import torch.nn.init as init
import torch

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, 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)


            if padding - kernel_size // 2 >= 0:
                #   Common use case. E.g., k=3, p=1 or k=5, p=2
                self.crop = 0
                #   Compared to the KxK layer, the padding of the 1xK layer and Kx1 layer should be adjust to align the sliding windows (Fig 2 in the paper)
                hor_padding = [padding - kernel_size // 2, padding]
                ver_padding = [padding, padding - kernel_size // 2]
            else:
                #   A negative "padding" (padding - kernel_size//2 < 0, which is not a common use case) is cropping.
                #   Since nn.Conv2d does not support negative padding, we implement it manually
                self.crop = kernel_size // 2 - padding
                hor_padding = [0, padding]
                ver_padding = [padding, 0]

            self.ver_conv = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=(kernel_size, 1),
                                      stride=stride,
                                      padding=ver_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_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:
                self.init_gamma(1.0 / 3)

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

 # BN融合
    def _fuse_bn_tensor(self, conv, bn):
        std = (bn.running_var + bn.eps).sqrt()
        t = (bn.weight / std).reshape(-1, 1, 1, 1)
        return conv.weight * t, bn.bias - bn.running_mean * bn.weight / std
 # 分支融合
    def _add_to_square_kernel(self, square_kernel, asym_kernel):
        asym_h = asym_kernel.size(2)
        asym_w = asym_kernel.size(3)
        square_h = square_kernel.size(2)
        square_w = square_kernel.size(3)
        square_kernel[:, :, square_h // 2 - asym_h // 2: square_h // 2 - asym_h // 2 + asym_h,
                square_w // 2 - asym_w // 2: square_w // 2 - asym_w // 2 + asym_w] += asym_kernel

    def get_equivalent_kernel_bias(self):
        hor_k, hor_b = self._fuse_bn_tensor(self.hor_conv, self.hor_bn)
        ver_k, ver_b = self._fuse_bn_tensor(self.ver_conv, self.ver_bn)
        square_k, square_b = self._fuse_bn_tensor(self.square_conv, self.square_bn)
        self._add_to_square_kernel(square_k, hor_k)
        self._add_to_square_kernel(square_k, ver_k)
        return square_k, hor_b + ver_b + square_b
 # 部署使用
    def switch_to_deploy(self):
        deploy_k, deploy_b = self.get_equivalent_kernel_bias()
        self.deploy = True
        self.fused_conv = nn.Conv2d(in_channels=self.square_conv.in_channels, out_channels=self.square_conv.out_channels,
                                    kernel_size=self.square_conv.kernel_size, stride=self.square_conv.stride,
                                    padding=self.square_conv.padding, dilation=self.square_conv.dilation, groups=self.square_conv.groups, bias=True,
                                    padding_mode=self.square_conv.padding_mode)
        self.__delattr__('square_conv')
        self.__delattr__('square_bn')
        self.__delattr__('hor_conv')
        self.__delattr__('hor_bn')
        self.__delattr__('ver_conv')
        self.__delattr__('ver_bn')
        self.fused_conv.weight.data = deploy_k
        self.fused_conv.bias.data = deploy_b


    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)
            if self.crop > 0:
                ver_input = input[:, :, :, self.crop:-self.crop]
                hor_input = input[:, :, self.crop:-self.crop, :]
            else:
                ver_input = input
                hor_input = input
            vertical_outputs = self.ver_conv(ver_input)
            vertical_outputs = self.ver_bn(vertical_outputs)
            horizontal_outputs = self.hor_conv(hor_input)
            horizontal_outputs = self.hor_bn(horizontal_outputs)
            result = square_outputs + vertical_outputs + horizontal_outputs
            return result

# 直接运行测试
if __name__ == '__main__':
    N = 1
    C = 2
    H = 62
    W = 62
    O = 8
    groups = 4

    x = torch.randn(N, C, H, W)
    print('input shape is ', x.size())

    test_kernel_padding = [(3,1), (3,0), (5,1), (5,2), (5,3), (5,4), (5,6)]

    for k, p in test_kernel_padding:
        acb = ACBlock(C, O, kernel_size=k, padding=p, stride=1, deploy=False)
        acb.eval()
        for module in acb.modules():
            if isinstance(module, nn.BatchNorm2d):
                nn.init.uniform_(module.running_mean, 0, 0.1)
                nn.init.uniform_(module.running_var, 0, 0.2)
                nn.init.uniform_(module.weight, 0, 0.3)
                nn.init.uniform_(module.bias, 0, 0.4)
        out = acb(x)
        acb.switch_to_deploy()
        deployout = acb(x)
        print('difference between the outputs of the training-time and converted ACB is')
        print(((deployout - out) ** 2).sum())

3.2 利用卷积的可加性

作者试图采用非对称卷积,使它们可以等价地融合到标准的方形核层中,这样就不会引入额外的推理时间以及计算负担。作者注意到卷积的一个有用的属性:如果几个大小兼容的二维kernel以相同的stride对相同的输入进行操作,产生相同分辨率的输出,并将它们的输出相加,则可以将这些kernel在相应的位置相加,得到一个等价的kernel,该kernel将产生相同的输出。也就是说,可加性可能适用于二维卷积,即使是不同的kernel size,
image.png
其中,X为输入M上对应的滑动窗口。

显然,当将2个滤波器产生的2个输出通道相加时,如果一个通道上的每个点 在另一个通道上的对应点共享相同的滑动窗口X,则可加性成立。

3.3 ACB推理的提升

在本文中重点关注3×3卷积,因为它在CNN架构中被大量使用。给定一个架构,用ACB替换每个3×3层(以及BN层)来构建ACB,ACB包含3个并行层,kernel size分别为3×3、1×3和3×1。与标准CNN的常见做法类似,三层之后都进行批量归一化,称为一个分支,将三个分支的输出汇总为ACB的输出。请注意,这里可以使用与原始模型相同的配置来训练ACNet,而不需要调整任何额外的超参数。

如4.1节和4.2节所示,ACNet可以通过训练达到更高的准确性水平。当训练完成后,试图将每个ACB转换为一个标准的卷积层,产生相同的输出。通过这样做可以获得一个更强大的网络,同时不需要额外的计算。这种转换通过2个步骤实现,即BN融合分支融合
image.png

1、BN融合

image.png

def _fuse_bn_tensor(self, conv, bn):
 std = (bn.running_var + bn.eps).sqrt()
    t = (bn.weight / std).reshape(-1, 1, 1, 1)
    return conv.weight * t, bn.bias - bn.running_mean * bn.weight / std

2、分支融合

image.png
值得注意的是,虽然一个ACB可以等价地转换为一个标准层,但这种等价性只在推理时成立,因为训练动态不同,从而导致不同的性能。训练过程的不等价性是由于kernel权值的随机初始化,以及它们所参与的不同计算流所导出的梯度。

def _add_to_square_kernel(self, square_kernel, asym_kernel):
    asym_h = asym_kernel.size(2)
    asym_w = asym_kernel.size(3)
    square_h = square_kernel.size(2)
    square_w = square_kernel.size(3)
    square_kernel[:, :, square_h // 2 - asym_h // 2: square_h // 2 - asym_h // 2 + asym_h, square_w // 2 - asym_w // 2: square_w // 2 - asym_w // 2 + asym_w] += asym_kernel

4 实验更精彩

4.1 消融实验

虽然已经通过前面的实验证明了ACNet的有效性,但仍然希望找到一些解释。在本小节中试图通过一系列消融实验来研究ACNet。具体来说,主要关注以下三个设计决策:

  1. 水平kernel的使用
  2. 垂直kernel的使用
  3. 每个分支中的BN层的使用

为了具有可比性,使用相同的训练配置,在ImageNet上用不同的消融训练几个AlexNet和ResNet-18模型。值得注意的是,如果去除分支中的BN层,将对整个ACB 的输出进行批归一化,即批归一化层的位置从前求和变为后求和。
image.png
从表4中可以看出,去掉这三种设计中的任何一种都会降低模型的质量。然而,虽然水平卷积和垂直卷积都能提高性能,但由于在实际应用中对水平方向和垂直方向的处理不平等,可能会存在一定的差异。
image.png

例如,通常会对训练数据进行随机左右翻转,而不进行上下翻转。因此,如果一个上下颠倒的图像输入模型,最初3 x3层应该产生意义的结果,这是自然的,但水平kernel将产生相同的输出在原始图像上的轴对称位置(图4)。也就是说,ACB仍然可以提取一部分正确的特征。考虑到这一点,假设ACB可以增强模型对旋转畸变的鲁棒性,使模型能够更好地泛化未知数据

然后,用整个验证集包括逆时针90度旋转、180度旋转和上下翻转在内的旋转扭曲图像对之前训练的模型进行测试。当然,每个模型的精度都显著降低,但水平kernel模型在180度旋转和上下翻转的图像上的精度明显更高。

例如,在原始输入上,只配备水平kernel的ResNet-18的精度略低于只配备垂直kernel的ResNet-18,但在180个旋转输入上的精度要高0.75%。与基础模型相比,该模型在原始/180翻转图像上的准确率分别提高了0.34% / 1.27%。可以预见,在180度旋转和上下翻转输入条件下,模型具有相似的性能,因为180度旋转加上左右翻转等于上下翻转,并且由于数据增强方法,模型对左右翻转具有鲁棒性

综上所述,已经证明ACB的有效性,特别是其中的水平kernel,可以通过一个可观察的边际来增强模型对旋转畸变的鲁棒性。虽然这可能不是ACNet有效的主要原因,但作者认为它有希望对旋转不变性问题的进一步研究起到启发作用。

4.2 ACB提升方形kernel的skeleton

直观地说,在方形kernel上添加水平和垂直kernel可以被视为显式增强skeleton部分的一种手段,作者试图通过研究skeleton和corner权重之间的差异来解释ACNet的有效性。
image.png
受CNN剪枝方法的启发,从去除不同空间位置的一些权重开始,并在CIFAR-10上使用ResNet-56观察性能的下降。具体地说,在kernel中随机设置一些单独的权值为零,并测试模型。如图5a所示,对于标记为corner的曲线,从每个3×3 kernel的4个corner中随机选择权值,并将其设为零,以获得每个卷积层给定的全局稀疏比。注意,作为4/9=44.4%,稀疏比的44%意味着去除4个corner的大部分权重。对于skeleton,只从每个kernel的skeleton中随机选择权重。对于全局的,kernel中的每个个体权重都有相等的机会被选择。用不同的随机种子重复5次,并绘制了平均±std曲线。

可以观察到,由于随机效应,所有曲线随着稀疏比的增加而呈下降趋势,但由于随机效应,而不是单调的。很明显,从corner去除权重对模型的损害较小,但修剪skeleton的危害更大。这一现象表明,skeleton的权重对模型的表征能力更为重要。

将继续验证这一观察结果是否适用于ACNet。通过BN和分支融合对ACNet对等物进行转换,然后对其进行同样的实验。如图5b所示,可以观察到一个更显著的差距,例如,修剪几乎所有的corner的权重只会使模型的精度降低到60%以上。另一方面,修剪skeleton造成的损伤更大,当修剪skeleton所获得的全局稀疏度比仅达到13%,即去除13%×9/5=23.4%的skeleton权重时,模型被破坏。
image.png
image.png
在图6a和图6b中给出了正常训练的ResNet-56和融合的ACNet,其中一定网格的数值和颜色表示3×3层相应位置的平均相对重要性,即值越大,背景色越深表示参数的平均重要性较高。

从图6a可以看出,正常训练的ResNet-56以不平衡的方式分布参数的大小,即中心点的大小最大,4角处的点最小。从图6b可以看出,ACNet加剧了这种不平衡,4个角的A值下降到0.400以下,skeleton点的A值在0.666以上。特别是,中心点的a值为1.000,这意味着这个位置在每一个3×3层中始终具有主导重要性。值得注意的是,正方形、水平和垂直kernel对应位置的权值有可能出现符号的相反增长,因此加起来可能会产生更大或更小的幅度。但都可以观察到一个一致的现象,即模型总是学会增强每一层的skeleton。

如果将非对称核添加到其他位置而不是中心skeleton,模型将如何?

具体来说,使用与之前相同的训练配置训练ResNet-56的ACNet,但将水平卷积向输入的底部移动一个像素,并将垂直卷积向右移动。因此,在分支融合过程中,将与BN融合的不对称kernel添加到正方形kernel的右下角边界(图6c),以得到一个等价的网络。可以观察到,这种ACBs也可以增强边界,但不像常规ACBs对skeleton的强度高。该模型的准确率为94.67%,比常规的ACNet模型低0.42%。此外,还在融合模型上进行了类似的剪枝实验(图5c)。正如观察到的,修剪角落仍然提供最好的准确性,修剪增强的右下角边界并没有比左上角的2×2方块更好的结果,也就是说,虽然边界的大小增加了,其他部分仍然是必不可少的。

综上所述:

  1. 在标准方形kernel中,skeleton本质上比Corner更重要
  2. ACB能显著增强skeleton,进而提升性能
  3. 与普通ACB相比,在边界上添加水平和垂直kernel会降低模型的性能

因此,将ACNet的有效性部分归因于其进一步增强skeleton的能力。ACNet直观地遵循方形卷积核的性质。

4.3 分类实验

1、CIFAR10

image.png

2、CIFAR100

image.png

3、ImageNet

image.png
如表3所示,AlexNet、ResNet-18和DenseNet-121的Top-1准确率分别提高了1.52%、0.78%和1.18%。

在实践中,针对同一精度目标,可以利用ACNet增强一个更高效的模型,以更少的推理时间、能量消耗和存储空间实现目标。另一方面,在计算预算或模型大小的相同限制下,可以使用ACNet来明显地提高精度,这样从终端用户的角度来看,所获得的性能可以被视为免费的好处。

5 参考
[1].ACNet: Strengthening the Kernel Skeletons for Powerful CNN via Asymmetric Convolution Blocks

原文:集智书童
作者:ChaucerG

推荐阅读

更多嵌入式AI技术相关内容请关注嵌入式AI专栏。
推荐阅读
关注数
18838
内容数
1371
嵌入式端AI,包括AI算法在推理框架Tengine,MNN,NCNN,PaddlePaddle及相关芯片上的实现。欢迎加入微信交流群,微信号:aijishu20(备注:嵌入式)
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息