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

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

1简介


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

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

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

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

考虑到这一点,为了寻找一个可以与许多架构相结合的通用的CNN结构,作者试图通过挖掘一个正交的方面来加强标准卷积层:权值和kernel中的空间位置之间的关系

image.png

在本文中提出了非对称卷积块(ACB),这是一种创新的结构来取代标准卷积层,如3×3层,在现代CNN中广泛使用的。具体地说,对于替换一个层,构建了一个由组成的ACB,分别包含、和的kernel,将输出相加以丰富特征空间(图1)。由于引入的和层具有非正方形的kernel,将它们称为非对称卷积层。给定一个现成的架构,构造了一个非对称卷积网络(ACNet),将ACB替换每个卷积层,并训练它直到收敛。

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 公式

假设,卷积层的kernel size为H×W,channel数为D,并以C通道特征图作为输入,使用表示三维卷积kernel,表示空间分辨率U×V和C通道的输入特征,和表示空间分辨率U×V和C通道的输出特征。对于这样一层的第个卷积核,对应的输出特征映射通道为

image.png

其中∗是二维卷积算子,是U×V矩阵形式的M的第k个通道,是的第k个输入通道,即H×W的二维kernel。

在现代CNN架构中,批量归一化被广泛用于减少过拟合和加速训练过程。作为一种常见的做法,批归一化层之后通常会进行线性尺度变换,以增强表示能力。与等式相比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

其中I是一个矩阵,和是2个具有兼容大小的2D kernel,是点加。

这里的兼容性意味着可以将较小的kernel“patch”到较大的kernel上。在形式上,在层和层上的这种转换是可行的:

image.png

例如,3×1和1×3 kernel与3×3 kernel是可以兼容。

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

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融合

卷积的同质性允许,BN的线性缩放和偏置可以等价地融合到卷积层中。它可以从等式2中观察到,对于每个分支,如果构造一个新的kernel为γσ,并添加一个偏差项µγσβ,将产生相同的输出,这很容易验证。

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、分支融合

通过将不对称的kernel添加到方形kernel的相应位置上,将3个与BN融合的分支合并为一个标准的卷积层。在实践中,这个转换是通过构建一个原始结构的网络并使用融合的weight进行初始化来实现的,因此可以以与原始架构相同的计算预算产生与ACNet相同的输出。形式上,对于每个滤波器,设是融合的3D kernel,是得到的偏差项,和分别是1×3和3×1层对应滤波器的kernel,有

image.png

然后可以很容易地在任意的滤波器  上得到验证,

image.png

其中,,和分别为原始3×3、1×3和3×1分支的输出。

值得注意的是,虽然一个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


相关文章
|
6月前
|
机器学习/深度学习 算法 PyTorch
RPN(Region Proposal Networks)候选区域网络算法解析(附PyTorch代码)
RPN(Region Proposal Networks)候选区域网络算法解析(附PyTorch代码)
1327 1
|
6月前
|
机器学习/深度学习 关系型数据库 MySQL
大模型中常用的注意力机制GQA详解以及Pytorch代码实现
GQA是一种结合MQA和MHA优点的注意力机制,旨在保持MQA的速度并提供MHA的精度。它将查询头分成组,每组共享键和值。通过Pytorch和einops库,可以简洁实现这一概念。GQA在保持高效性的同时接近MHA的性能,是高负载系统优化的有力工具。相关论文和非官方Pytorch实现可进一步探究。
801 4
|
11天前
|
存储 物联网 PyTorch
基于PyTorch的大语言模型微调指南:Torchtune完整教程与代码示例
**Torchtune**是由PyTorch团队开发的一个专门用于LLM微调的库。它旨在简化LLM的微调流程,提供了一系列高级API和预置的最佳实践
113 59
基于PyTorch的大语言模型微调指南:Torchtune完整教程与代码示例
|
2月前
|
机器学习/深度学习 PyTorch 算法框架/工具
CNN中的注意力机制综合指南:从理论到Pytorch代码实现
注意力机制已成为深度学习模型的关键组件,尤其在卷积神经网络(CNN)中发挥了重要作用。通过使模型关注输入数据中最相关的部分,注意力机制显著提升了CNN在图像分类、目标检测和语义分割等任务中的表现。本文将详细介绍CNN中的注意力机制,包括其基本概念、不同类型(如通道注意力、空间注意力和混合注意力)以及实际实现方法。此外,还将探讨注意力机制在多个计算机视觉任务中的应用效果及其面临的挑战。无论是图像分类还是医学图像分析,注意力机制都能显著提升模型性能,并在不断发展的深度学习领域中扮演重要角色。
82 10
|
19天前
|
机器学习/深度学习 并行计算 PyTorch
提高 PyTorch 性能
提高 PyTorch 是一个非常流行的深度学习框架,它支持动态计算图,非常适合快速原型设计和研究。
36 3
|
5月前
|
机器学习/深度学习 PyTorch 算法框架/工具
【从零开始学习深度学习】26.卷积神经网络之AlexNet模型介绍及其Pytorch实现【含完整代码】
【从零开始学习深度学习】26.卷积神经网络之AlexNet模型介绍及其Pytorch实现【含完整代码】
|
2月前
|
机器学习/深度学习 PyTorch 调度
在Pytorch中为不同层设置不同学习率来提升性能,优化深度学习模型
在深度学习中,学习率作为关键超参数对模型收敛速度和性能至关重要。传统方法采用统一学习率,但研究表明为不同层设置差异化学习率能显著提升性能。本文探讨了这一策略的理论基础及PyTorch实现方法,包括模型定义、参数分组、优化器配置及训练流程。通过示例展示了如何为ResNet18设置不同层的学习率,并介绍了渐进式解冻和层适应学习率等高级技巧,帮助研究者更好地优化模型训练。
128 4
在Pytorch中为不同层设置不同学习率来提升性能,优化深度学习模型
|
5月前
|
机器学习/深度学习 PyTorch 算法框架/工具
【从零开始学习深度学习】28.卷积神经网络之NiN模型介绍及其Pytorch实现【含完整代码】
【从零开始学习深度学习】28.卷积神经网络之NiN模型介绍及其Pytorch实现【含完整代码】
|
27天前
|
机器学习/深度学习 PyTorch 算法框架/工具
聊一聊计算机视觉中常用的注意力机制以及Pytorch代码实现
本文介绍了几种常用的计算机视觉注意力机制及其PyTorch实现,包括SENet、CBAM、BAM、ECA-Net、SA-Net、Polarized Self-Attention、Spatial Group-wise Enhance和Coordinate Attention等,每种方法都附有详细的网络结构说明和实验结果分析。通过这些注意力机制的应用,可以有效提升模型在目标检测任务上的性能。此外,作者还提供了实验数据集的基本情况及baseline模型的选择与实验结果,方便读者理解和复现。
23 0
聊一聊计算机视觉中常用的注意力机制以及Pytorch代码实现
|
3月前
|
机器学习/深度学习 并行计算 PyTorch
GPU 加速与 PyTorch:最大化硬件性能提升训练速度
【8月更文第29天】GPU(图形处理单元)因其并行计算能力而成为深度学习领域的重要组成部分。本文将介绍如何利用PyTorch来高效地利用GPU进行深度学习模型的训练,从而最大化训练速度。我们将讨论如何配置环境、选择合适的硬件、编写高效的代码以及利用高级特性来提高性能。
579 1

热门文章

最新文章