摘要
为了设计快速的神经网络,许多研究都集中在减少浮点运算(FLOPs)的数量。然而,我们观察到这种FLOPs的减少并不一定会导致相同程度的延迟减少。这主要是由于浮点运算每秒效率较低的问题所致。为了实现更快的网络,我们重新审视了流行的操作算子,并证明这种低FLOPS主要是由于操作算子(尤其是深度卷积)频繁进行内存访问所致。
因此我们提出了一种新颖的部分卷积(PConv)方法,通过同时削减冗余计算和内存访问,更有效地提取空间特征。在我们先前的PConv的基础上,我们进一步提出了FasterNet,一种新的神经网络家族,在各种视觉任务中能够以更高的运行速度在广泛的设备上取得相当高的精度,而不会妥协于准确性。
引言
神经网络在计算机视觉诸如图像分类、检测和分割等任务中经历了快速发展。虽然其令人印象深刻的性能推动了许多应用,但目前的趋势是追求快速的神经网络,具有低延迟和高吞吐量,以实现良好的用户体验、即时响应和安全性等方面的需求。
如何提高速度?
研究人员和实践者们不在要求更昂贵的计算设备,而是更倾向于设计成本效益高、具有较低计算复杂性的快速神经网络,主要通过浮点运算次数(FLOPs)来衡量。MobileNets [24, 25, 54]、ShuffleNets [46,84]和GhostNet [17]等网络采用深度卷积(DWConv)[55]和/或组卷积(GConv)[31]来提取空间特征,以降低FLOPs。然而,在减少FLOPs的努力中,这些运算符通常会导致内存访问的副作用。MicroNet [33]进一步对网络进行分解和稀疏化,将其FLOPs减少到了极低的水平。
尽管在FLOPs方面有所改进,但是这种方法在有效的分段计算方面存在问题。此外,上述网络通常伴随着其他数据操作**,如连接、重排和池化**,其运行时间往往对于小模型来说是相当重要的。除了上述纯卷积神经网络之外,还出现了对VITs和MLPs架构进行缩小和加速的兴趣,例如:MobileVits和MobileFormer通过将DWConv与修改后的注意力机制相结合来减少计算复杂度。然而,他们仍然存在DWConv上述问题,并且还需要专门的硬件支持来实现修改后的注意力机制,使用先进但耗时的归一化层和激活层也可能限制他们在设备上的速度。
这些问题一起引发了一下问题:这些“快速”神经网络真的很快嘛?
为了回答这个问题,我们研究了延迟和FLOPs之间的关系。其中FLOPS是每秒浮点运算次数,作为衡量有效计算速度的指标。尽管有很多试图减少FLOPs的尝试,但它们很少考虑同时优化FLOPS以实现真正低延迟的问题。我们在Intel CPU上比较了典型神经网络的FLOPS。图2的结果显示,许多现有的神经网络FLOPS较低,它们的FLOPS通常低于流行的ResNet50。由于FLOPS较低,这些“快速”神经网络实际上并不够快。它们在FLOPS的减少不能完全转化为延迟的减少量。之前的研究[46,48]也注意到了FLOPs和延迟之间的差异,但部分地仍未解决,因为它们采用了低FLOPS的DWConv/GConv和各种数据操作。据认为,目前没有更好的选择。
本文旨在通过开发一种简单而快速有效的运算符,以减少FLOPs并保持高FLOPS,来消除这种差异,具体而言,我们重新审视现有的运算符,特别是DWConv,从计算速度的角度来看,我们发现导致低FLOPS问题的主要原因是频繁的内存访问。然后,我们提出了一种新颖的部分卷积(PConv)作为一种竞争性的替代方案,既减少了计算冗余,又减少了内存访问的次数。
从本质上讲,与常规的Conv相比,PConv的FLOPs较低,但比DWConv/GConv要高,换句话说,PConv更好的利用了设备上的计算能力,经过实验证明,PConv在提取空间特征方面也非常有效。
我们进一步介绍了FasterNet,它主要基于我们的PConv构件而成,作为一种在各种设备上运行非常快速的新型网络家族,特别是我的FasterNet在分类、检测和分割任务中实现了最先进的性能,同时具有更低的延迟和更高的吞吐量。
本文的贡献如下:
- 引入了简单而快速有效的操作符,名为PConv ,他有很大潜力替代现有的首选操作符DWConv
- FasterNet 在各种设备上都能有良好的运行性能和普遍速度
相关工作
我们简要回顾了之前的快速和高效神经网络上的工作,并将这项工作和他们区分开来。
CNN
计算机视觉领域的主流架构,特别是在实际部署中**,快速性和准确性同等重要,**尽管已经进行了许多研究以实现更高的效率,但他们背后的基本原理更多或多或少是进行低秩近似。具体而言,群组卷积[31]和深度可分离卷积[55](包括深度卷积和逐点卷积)可能是最受欢迎的方法。他们已广泛应用于移动/边缘导向网络,如MobileNets [24, 25, 54],ShuffleNets [46, 84],GhostNet [17],EfficientNets [61, 62],TinyNet [18],Xception [8],CondenseNet [27, 78],TVConv [4],MnasNet [60]和FBNet [74]。尽管它们利用滤波器中的冗余来减少参数和FLOP的数量,但在增加网络宽度以弥补准确性下降时,会导致内存访问增加。相比之下,我们考虑特征图中的冗余,并提出了部分卷积来同时减少FLOP和内存访问。
ViT,MLP和变体。
自Dosovitskiy等人(2020)[12]将transformers [69]的应用范围从机器翻译 [69]或预测 [73]扩展到计算机视觉领域以来,对ViT的研究越来越受关注。
许多后续的研究致力于在训练设置和模型设计方面改进 ViT [58, 65, 66]。一个值得注意的趋势是通过减少注意力运算的复杂性 [1,29,45,63,68],将卷积引入 ViTs [6,10,57],或者同时进行这两种操作 [3,34,49,52],追求更好的准确性和延迟之间的折中关系。此外,其他研究 [5,35,64] 提议用简单的基于MLP的操作符替代注意力,但它们往往演变成类似于CNN的形式 [39]。在本文中,我们主要关注卷积操作,特别是 DWConv,原因如下:首先,注意力、卷积之间的优势并不清楚或可争议 [42,71]。其次,基于注意力的机制通常比卷积运算的效率慢,因此对当前的工业界来说不太有利 [26,48]。最后,DWConv 仍然是许多混合模型中的一个流行选择,因此值得进行仔细的研究。
PConv和FasterNet的设计
在本节中,我们首先重新审视DWConv并分析其频繁内存访问的问题。然后,我们引入PConv作为解决这个问题的一种有竞争力的替代算子。之后,我们介绍FasterNet并解释它的详细信息,包括设计考虑。
DWConv是Conv的一个流行变体,已被广泛采用作为许多神经网络的重要组件。对于输入I∈Rc×h×w,DWConv应用c个过滤器W∈Rk×k来计算输出O∈Rc×h×w。如图1(b)所示,每个过滤器在一个输入通道上滑动,并对一个输出通道做出贡献。这种深度计算使得DWConv的FLOPs(浮点运算数)仅为h×w×k2×c,相比于常规的Conv的h×w×k2×c2要低。虽然在减少FLOPs上是有效的,但是,DWConv通常紧跟着一个pointwise convolution(PWConv)时,不能简单地替换成一个常规的Conv,因为这样会导致严重的精度下降。
偏执卷积作为基本操作
特征图在不同通道之间具有很高的相似性,这种冗余性在其他许多研究中也有所涉及[17,82],但其中很少有研究能够以简单而有效的方式充分利用它。具体而言,我们提出了一种称为PConv的简单卷积方法,可以同时降低冗余和内存访问。它仅在一部分输入通道上应用常规卷积进行空间特征提取,并保持其他通道不变,为了实现连贯或规则的内存访问,我们将第一个或最后连续的CP个通道作为整个特征图的代表进行计算。通过使用典型的部分比率r = cp / 4,PConv的浮点运算量仅为常规卷积的16分之一。另外,PConv的内存访问量较小。
PConv followed by PWConv
为了充分高效地利用所有通道信息,我们在PConv之后进一步添加一个点卷积,他们在输入特征图上的有效感受野形状类似于一个T形卷积,相较于均匀处理一个补丁的常规卷积,更加注重中心位置。为了证明这个T型感受野的合理性,我们首先通过计算位置的逐位置Frobenius范数来评估每个位置的重要性。我们假设,如果一个位置的Frobenius范数比其他位置大,它往往更重要。对于一个常规的卷积
我们认为具有最大Frobenius范数的位置是显著位置。然后,我们共同检查预训练的ResNet18中的每个滤波器,找出他们的显著位置,并绘制出显著位置的直方图。中心位置的权重大于其周围的邻居。这与T型计算的特性一致。
FasterNet作为通用骨干网络
考虑到我们创新的PConv和现成的PWConv作为主要构件算子,我们进一步提出了FasterNet,这是一族运行速度快且在许多视觉任务中高效的新型神经网络。
架构概述
我们的目标是尽可能简化架构,没有花里胡哨的特点,以便使其总体上对硬件友好。我们在下图中呈现了总体架构。他有四个阶段,每个阶段都由一层嵌入层(一个带有步长为4的常规Conv4X4)或一个合并层(一个带有步长2的常规Conv2X2)来进行空间下采样和通道数量的扩展,每个阶段都有一组FasterNet块,我们观察到,在最后两个阶段中的块消耗的内存访问较少,并且往往就有更高的FLOPS,这是表1验证过的,因此,我们在最后两个阶段放置了更多的FasterNet块,并相应地分配了更多的计算任务,每个FasterNet块都有一个PConv层,后跟两个PWConv(或Conv1X1)层,他们共同形成倒置残差块,其中间层具有扩展的通道数,并且放置了一个快捷连接以重用输入特征。
除了上述运算符外,归一化和激活层对于性能良好的神经网络也是必不可少的。然而许多之前的研究工作[17,20,54]过度使用这些层,可能限制特征多样性,从而影响性能。这还会降低整体计算速度。相比之下,我们只在每个中间PWConv之后添加这些层,以保留特征多样性,实现更低的延迟,此外,我们使用批量归一化(BN),而不是其他替代方法。BN的好处是他可以与相邻的卷积层合并,使推理速度更快,同时效果与其他方法一样
对于激活层,我们经验性的选择了GELU用于较小的FasterNet变种,选择了Relu用于较大的FasterNet变种,考虑到运行时间和效果,最后的三个层即全局平均池化、一个1X1卷积层和一个全连接层,一起用于特征转换和分类。
附录
论文地址:https://export.arxiv.org/pdf/2303.03667v1.pdf
FasterNet代码:
# --------------------------FasterNet----------------------------
from timm.models.layers import DropPath
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
class BasicStage(nn.Module):
def __init__(self,
dim,
depth=1,
n_div=4,
mlp_ratio=2,
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_FasterNet(nn.Module):
def __init__(self, in_chans, embed_dim, patch_size, patch_stride, norm_layer=nn.BatchNorm2d):
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_FasterNet(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