【论文笔记】FasterNet:Run, Don’t Walk: Chasing Higher FLOPS for Faster Neural Networks

前言

在这里插入图片描述
为了设计快速神经网络,许多工作都集中在减少浮点运算(FLOPs)的数量上。然而,作者观察到FLOPs的这种减少不一定会带来延迟的类似程度的减少。这主要源于每秒低浮点运算(FLOPS)效率低下。

为了实现更快的网络,作者重新回顾了FLOPs的运算符,并证明了如此低的FLOPS主要是由于运算符的频繁内存访问,尤其是深度卷积。因此,本文提出了一种新的partial convolution(PConv),通过同时减少冗余计算和内存访问可以更有效地提取空间特征。

基于PConv进一步提出FasterNet,这是一个新的神经网络家族,它在广泛的设备上实现了比其他网络高得多的运行速度,而不影响各种视觉任务的准确性。例如,在ImageNet-1k上小型FasterNet-T0在GPU、CPU和ARM处理器上分别比MobileVitXXS快3.1倍、3.1倍和2.5倍,同时准确度提高2.9%。

大模型FasterNet-L实现了令人印象深刻的83.5%的TOP-1精度,与Swin-B不相上下,同时GPU上的推理吞吐量提高了49%,CPU上的计算时间也节省了42%。

论文链接🔗Run, Don’t Walk: Chasing Higher FLOPS for Faster Neural Networks


1. 介绍

在提升快速性上,研究人员和从业者通常是倾向于设计具有成本效益的快速神经网络,降低计算复杂度,这一点主要通过浮点运算(FLOPs)来衡量。FLOPs是floating point of operations的缩写,是浮点运算次数,可以用来衡量算法/模型复杂度。

MobileNet、ShuffleNet和GhostNet等利用深度卷积(DWConv)和/或组卷积(GConv)来提取空间特征。然而,在减少FLOPs的过程中,算子经常会受到内存访问增加的副作用的影响。

除了上述纯卷积神经网络(CNNs)之外,人们对使视觉Transformer(ViTs)和多层感知器(MLP)架构更小更快也越来越感兴趣。例如,MobileViT和MobileFormer通过将DWConv与改进的注意力机制相结合,降低了计算复杂性。然而,它们仍然受到DWConv的上述问题的困扰,并且还需要修改的注意力机制的专用硬件支持。使用先进但耗时的标准化和激活层也可能限制其在设备上的速度。

所以FLOPs的减少不一定会导致类似水平的延迟 (Latency) 减少。这主要是由于每秒浮点运算数量 (Floating-Point OPerations per Second) 较低。根据公式:
在这里插入图片描述
其中FLOPS是每秒浮点运算的缩写,这个值越高,说明硬件性能越好。
在这里插入图片描述
上图结果表明,许多现有神经网络的FLOPS较低,其FLOPS通常低于流行的ResNet50。由于FLOPS如此之低,这些“快速”的神经网络实际上不够快。它们的FLOPs减少不能转化为延迟的确切减少量。在某些情况下,没有任何改善,甚至会导致更糟的延迟。例如,CycleMLP-B1具有ResNet50的一半FLOPs,但运行速度较慢(即CycleMLPB1与ResNet50:111.9ms与69.4ms)。


为了实现更快的网络,本文重新审视了流行的算子,并证明了较低的 FLOPS 主要是由于算子的频繁内存访问,尤其是 Depth-wise Convolution。

本文旨在通过开发一种简单、快速、有效的运算符来消除这种差异,该运算符可以在减少FLOPs的情况下保持高FLOPS。具体来本文旨在通过开发一种简单、快速、有效的运算符来消除这种差异,该运算符可以在减少FLOPs的情况下保持高FLOPS。


2. 简单介绍以下深度可分离卷积

深度可分离卷积(Depthwise Separable Convolution)是指将卷积操作分解为深度卷积和逐点卷积两步操作的卷积操作。它在卷积神经网络中被广泛应用,可以用来减少模型参数数量、计算量和提高网络效果等作用。

深度可分离卷积的实现方式是,在原来的传统卷积基础上,将卷积分成两步操作:

  1. 深度卷积(Depthwise Convolution):在每个输入通道内分别进行一个卷积操作,不改变通道数
  2. 逐点卷积(Pointwise Convolution):使用1x1卷积核在深度方向上进行卷积,对输出通道进行混合和重新组合。(感觉就是1x1卷积,换了个说法而已)

相比传统卷积,深度可分离卷积具有计算量更小、模型参数更少、性能优越等优点。在移动端等计算资源受限的场景中,深度可分离卷积可以帮助提高神经网络模型的效率和性能。同时,深度可分离卷积还可以应用于目标检测、图像分割和自然语言处理等领域。Mobilenet中使用了大量的深度可分离卷积来代替传统卷积。

DWConv把Conv的计算量从 h × w × k 2 × c 2 h \times w \times k^{2} \times c^{2} h×w×k2×c2缩小到了 h × w × k 2 × c h \times w \times k^{2} \times c h×w×k2×c

虽然 DWConv 可以有效地降低 FLOPs,但它不能简单地用来取代常规的 Conv,因为会导致严重的精度下降。所以说 DWConv 通常会跟随 Pointwise Convolution (PWConv)。因此在实践中, DWConv的通道数 c (或网络宽度) 增加到c以补偿精度下降。一般在实践中 DWConv 的通道数会变宽,以补偿精度的下降 (比如 MobileNet V2 中变为了6倍宽)。但是这会带来更高的内存访问,降低整体计算速度,尤其是对于 I/O-bound 设备。

注意,这里所说的DWConv,其实就包括了深度卷积和逐点卷积。


3. PConv

3.1 部分卷积PConv的设计

本文给出的解决方案部分卷积 (Partial Convolution, PConv) 的出发点是卷积神经网络特征的冗余性。如下图所示是在预训练的 ResNet50 的中间层中可视化特征图,左上角的图像是输入,特征图在不同通道之间具有很高的相似性
在这里插入图片描述
部分卷积设计的目的是同时减小内存访问和计算冗余,其和常规卷积,DWConv 的区别如下图所示。
在这里插入图片描述
工作原理如下图所示。
在这里插入图片描述

以下会涉及参数量的计算,可以先看下这篇文章:神经网络参数量,计算量FLOPS,内存访问量MAC

PConv 对部分输入通道应用常规的 Conv 来进行空间特征提取,而对其余通道保持不变。对于连续或规则的内存访问,将第一个或最后一个连续的 c p c_{p} cp通道视为整个特征图的代表进行计算。在不丧失一般性的情况下认为输入和输出特征图具有相同数量的通道。因此

  • PConv的FLOPs h × w × k 2 × c p 2 . h \times w \times k^{2} \times c_{p}^{2} . h×w×k2×cp2.
    其中,h和w是特征图的宽和高,k是卷积和的大小, c p c_{p} cp是常规卷积作用的通道数,相当于之前普通卷积的 c i n c_{in} cin变成了 c p c_{p} cp
    在实际实现时一般 r = c p c = 1 4 r=\frac{c_{p}}{c}=\frac{1}{4} r=ccp=41。所以,PConv的FLOPs只有常规卷积的1/16。
  • PConv的内存访问情况: h × w × 2 c p + k 2 × c p 2 ≈ h × w × 2 c p h \times w \times 2 c_{p}+k^{2} \times c_{p}^{2} \approx h \times w \times 2 c_{p} h×w×2cp+k2×cp2h×w×2cp
    其中,h和w是特征图的宽和高,k是卷积和的大小, c p c_{p} cp是常规卷积作用的通道数。PConv的内存访问量仅仅是常规卷积的1/4,所以无需进行内存的访问。

3.2 PConv 之后的 Point-Wise Convolution

为了充分有效地利用来自所有通道的信息,作者进一步在 PConv 之后增加了 Point-Wise Convolution (PWConv),没错就是DWConv之后的那个东西,一般是用来升维的。如下图所示,在输入特征图上的有效感受野一起看起来像一个 T 型的 Conv,和常规 Conv 相比,它更关注中心位置。
在这里插入图片描述

  • T型的Conv的FLOPs h × w × ( k 2 × c p × c + c × ( c − c p ) ) h \times w \times\left(k^{2} \times c_{p} \times c+c \times\left(c-c_{p}\right)\right) h×w×(k2×cp×c+c×(ccp))
  • 解耦成PConv和PWConv的FLOPs h × w × ( k 2 × c p 2 + c × c p ) h \times w \times\left(k^{2} \times c_{p}^{2}+c \times c_{p}\right) h×w×(k2×cp2+c×cp)

很明显,解耦后的计算量更小。

3.3 PConv的代码

class PConv(nn.Module):
    """ Partial convolution (PConv).
    """
    def __init__(self,
                 dim: int,
                 n_div: int,
                 forward: str = "split_cat",
                 kernel_size: int = 3) -> None:
        """ Construct a PConv layer.

        :param dim: Number of input/output channels
        :param n_div: Reciprocal of the partial ratio.
        :param forward: Forward type, can be either 'split_cat' or 'slicing'.
        :param kernel_size: Kernel size.
        """
        super().__init__()
        self.dim_conv = dim // n_div
        self.dim_untouched = dim - self.dim_conv

        self.conv = nn.Conv2d(
            self.dim_conv,
            self.dim_conv,
            kernel_size,
            stride=1,
            padding=(kernel_size - 1) // 2,
            bias=False
        )

        if forward == "slicing":
            self.forward = self.forward_slicing
        elif forward == "split_cat":
            self.forward = self.forward_split_cat
        else:
            raise NotImplementedError

    def forward_slicing(self, x: Tensor) -> Tensor:
        """ Apply forward pass for inference. """
        x[:, :self.dim_conv, :, :] = self.conv(x[:, :self.dim_conv, :, :])

        return x

    def forward_split_cat(self, x: Tensor) -> Tensor:
        """ Apply forward pass for training. """
        x1, x2 = torch.split(x, [self.dim_conv, self.dim_untouched], dim=1)
        x1 = self.conv(x1)
        x = torch.cat((x1, x2), 1)

        return x

注意下forward_slicing中的卷积操作,我一开始还在想要怎么做到部分卷积,没想到就是切片,还是蛮有意思的。

另外,通过代码forward_split_cat可以看到,训练阶段的PConv只是进行了部分卷积,没有进行PW卷积。

4. 基于PConv的视觉骨干模型 FasterNet

4.1 实现细节

鉴于新型PConv和现成的PWConv作为主要的算子,进一步提出FasterNet,这是一个新的神经网络家族,运行速度非常快,对许多视觉任务非常有效。作者的目标是使体系结构尽可能简单,使其总体上对硬件友好。
在这里插入图片描述
如上图所示,他有4个Stages,Stage1之前通过了Embedding层(一个常规的4x4卷积,步长为4,相当于将宽高缩小为了1/4),其他的Stage之前使用Merging层(常规个2x2卷积,步长为2,也是起下采样作用),每个 Stage 都由一些 FasterNet 块堆叠而成。

这里注意下几个细节。

  • 作者观察到,最后两个Stage中的块消耗更少的内存访问,并且具有更高的FLOPS,因此,作者在最后2个 Stage放置了更多的 FasterNet 块,并相应地将更多的计算分配到最后2个 Stage。
  • 每个 FasterNet 块都是由1个 PConv 和后续的两个 1×1 的 Point-wise Convolution 构成的。这三者构成了一个倒残差的架构,中间层的通道数量更多,并放置了一个 Shortcut 连接以重用输入特征。

可以发现,FasterNet Block是不会改变图像大小的,只有Merging才会进行下采样。

4.2 Normalization 层和激活函数的使用

Normalization 层和激活函数神经网络而言是必不可少的,但是如果在整个网络中过度使用这些层,就有可能会限制特征的多样性,从而损害网络的性能,还可能会降低整体计算速度。所以作者只把它们放在每个中间层的 PWConv 之后,以保持特征多样性并获得更低的 Latency。

FasterNet 使用 BN 来代替 LN 等。BN 的好处是它可以通过结构重参数化的方式合并到相邻的 Conv 层中,以便更快地推断。

对于激活函数,考虑到运行时间和有效性,作者经验性地选择 GELU 作为较小的 FasterNet 变体,ReLU 作为较大的 FasterNet 变体。

为了在不同的计算预算下服务于广泛的应用程序,作者提供了 FasterNetT0/1/2,FasterNet-S,FasterNet-M,和 FasterNet-L。

4.3 代码实现

# PConv
class Partial_conv3(nn.Module):
    def __init__(self, dim, n_div, forward):
        super().__init__()
        self.dim_conv3 = dim // n_div
        self.dim_untouched = dim - self.dim_conv3
        self.partial_conv3 = nn.Conv2d(self.dim_conv3, self.dim_conv3, 3, 1, 1, bias=False)
 
        if forward == 'slicing':
            self.forward = self.forward_slicing
        elif forward == 'split_cat':
            self.forward = self.forward_split_cat
        else:
            raise NotImplementedError
 
    def forward_slicing(self, x):
        # only for inference
        x = x.clone()  # !!! Keep the original input intact for the residual connection later
        x[:, :self.dim_conv3, :, :] = self.partial_conv3(x[:, :self.dim_conv3, :, :])
 
        return x
 
    def forward_split_cat(self, x):
        # for training/inference
        x1, x2 = torch.split(x, [self.dim_conv3, self.dim_untouched], dim=1)
        x1 = self.partial_conv3(x1)
        x = torch.cat((x1, x2), 1)
        return x
 
 
class MLPBlock(nn.Module):
    def __init__(self,
                 dim,
                 n_div,
                 mlp_ratio,
                 drop_path,
                 layer_scale_init_value,
                 act_layer,
                 norm_layer,
                 pconv_fw_type
                 ):
 
        super().__init__()
        self.dim = dim
        self.mlp_ratio = mlp_ratio
        self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity()
        self.n_div = n_div
 
        mlp_hidden_dim = int(dim * mlp_ratio)
 
        mlp_layer = [
            nn.Conv2d(dim, mlp_hidden_dim, 1, bias=False),
            norm_layer(mlp_hidden_dim),
            act_layer(),
            nn.Conv2d(mlp_hidden_dim, dim, 1, bias=False)
        ]
 
        self.mlp = nn.Sequential(*mlp_layer)
 
        self.spatial_mixing = Partial_conv3(
            dim,
            n_div,
            pconv_fw_type
        )
 
        if layer_scale_init_value > 0:
            self.layer_scale = nn.Parameter(layer_scale_init_value * torch.ones((dim)), requires_grad=True)
            self.forward = self.forward_layer_scale
        else:
            self.forward = self.forward
 
    def forward(self, x):
        shortcut = x
        x = self.spatial_mixing(x)
        x = shortcut + self.drop_path(self.mlp(x))
        return x
 
    def forward_layer_scale(self, x):
        shortcut = x
        x = self.spatial_mixing(x)
        x = shortcut + self.drop_path(
            self.layer_scale.unsqueeze(-1).unsqueeze(-1) * self.mlp(x))
        return x
 
  # FasterNet基础模块
class BasicStage(nn.Module):
    def __init__(self,
                 dim,
                 depth=1,
                 n_div=4,
                 mlp_ratio=2,
                 # drop_path=[0.0],
                 layer_scale_init_value=0,
                 norm_layer=nn.BatchNorm2d,
                 act_layer=nn.ReLU,
                 pconv_fw_type='split_cat'
                 ):
        super().__init__()
        dpr = [x.item()
               for x in torch.linspace(0, 0.0, sum([1, 2, 8, 2]))]
        blocks_list = [
            MLPBlock(
                dim=dim,
                n_div=n_div,
                mlp_ratio=mlp_ratio,
                drop_path=dpr[i],
                layer_scale_init_value=layer_scale_init_value,
                norm_layer=norm_layer,
                act_layer=act_layer,
                pconv_fw_type=pconv_fw_type
            )
            for i in range(depth)
        ]
 
        self.blocks = nn.Sequential(*blocks_list)
 
    def forward(self, x):
        x = self.blocks(x)
        return x
 
 # 最开始的下采样
class PatchEmbed(nn.Module):
 
    def __init__(self, in_chans, embed_dim, patch_size, patch_stride, norm_layer):
        super().__init__()
        self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_size, stride=patch_stride, bias=False)
        if norm_layer is not None:
            self.norm = norm_layer(embed_dim)
        else:
            self.norm = nn.Identity()
 
    def forward(self, x):
        x = self.norm(self.proj(x))
        return x
 
    def fuseforward(self, x):
        x = self.proj(x)
        return x
 
 # 后面的下采样
class PatchMerging(nn.Module):
 
    def __init__(self, dim, out_dim, k, patch_stride2, norm_layer=nn.BatchNorm2d):
        super().__init__()
        self.reduction = nn.Conv2d(dim, out_dim, kernel_size=k, stride=patch_stride2, bias=False)
        if norm_layer is not None:
            self.norm = norm_layer(out_dim)
        else:
            self.norm = nn.Identity()
 
    def forward(self, x):
        x = self.norm(self.reduction(x))
        return x
 
    def fuseforward(self, x):
        x = self.reduction(x)
        return x

5. PConv具有更高的FLOPS

作者在3种不同设备上测量 Latency (Batch Size=1) 和 Throughput (Batch Size=32),GPU (2080Ti), CPU (Intel i9-9900X, using a single thread), 和 ARM。

本小节作者展示了 PConv 速度更快,更好地利用了设备上的计算能力。比如将10层 PConv 堆起来,然后测量GPU、CPU 和 ARM 处理器上的 FLOPs 和延迟/吞吐量所示。
在这里插入图片描述
PConv 的 FLOPs 只有普通 Conv 的 1/16,在 GPU、CPU 和 ARM 上分别比 DWConv 高14倍、6.5倍和22.7倍。常规的 Conv 拥有最高的 FLOPS,因为它已经不断优化了多年。GConv 和 DWConv 尽管显著降低了 FLOPs,但 FLOPS 却急剧下降。此外,它们倾向于增加通道数量以弥补性能下降,但是这会增加它们的延迟。

6. 总结

为了实现更快的网络,本文重新审视了流行的算子,并证明了较低的 FLOPS 主要是由于算子的频繁内存访问,尤其是 Depth-wise Convolution。本文提出了一种新的部分卷积 (Partial Convolution, PConv),通过同时减少冗余计算和内存访问,可以更有效地提取空间特征。在 PConv 的基础上,作者进一步提出了 FasterNet,一种新的神经网络家族,在各种设备上的运行速度远高于其他网络。

Reference

通用 Vision Backbone 超详细解读 (二十二):FasterNet:追求更快的神经网络
CVPR2023|不好意思我要加速度了!FasterNet:更高FLOPS是更快更强的底气
yolov5增加fasternet结构

  • 6
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值