EfficientFormer 系列算法

1. EfficientFormer V1 模型

论文地址:https://proceedings.neurips.cc/paper_files/paper/2022/file/5452ad8ee6ea6e7dc41db1cbd31ba0b8-Paper-Conference.pdf

EfficientFormer V1基于 ViT 的模型中使用的网络架构和具体的算子,找到端侧低效的原因。然后引入了维度一致的 Transformer Block 作为设计范式。最后,通过网络模型搜索获得不同系列的模型 ------ EfficientFormer。

大多数现有方法通过从服务器 GPU 获得的计算复杂性(MAC)或吞吐量(图像/秒)来优化 Transformer 的推理速度。但是这些指标不能反映实际的设备延迟。为了清楚地了解哪些操作和设计选择会减慢边缘设备上 VIT 的推断,在下图中作者作者对不同模型在端侧运行进行了一些分析,主要是分为 ViT 对图像进行分块的 Patch Embedding、Transformer 中的 Attention 和 MLP,另外还有 LeViT 提出的 Reshape 和一些激活等。提出了下面几个猜想。

观察 1:在移动设备上,具有大核和步长的 patch 嵌入是一个速度瓶颈。

patch 嵌入通常使用一个不重叠的卷积层来实现,该层具有较大的内核大小和步长。一种普遍的看法是,Transformer 网络中 patch 嵌入层的计算成本不显著或可以忽略不计。然而,在上图中比较了具有大核和大步长的 patch 嵌入模型,即 DeiT-S 和 PoolFormer-s24,以及没有它的模型,即 LeViT-256 和 EfficientFormer,结果表明,patch 嵌入反而是移动设备上的速度瓶颈。

大多数编译器都不支持大型内核卷积,并且无法通过 Winograd 等现有算法来加速。或者,非重叠 patch 嵌入可以由一个具有快速下采样的卷积 stem 代替,该卷积 stem 由几个硬件效率高的 3×3 卷积组成。

python 复制代码
#stem 以一些普通卷积组成
def stem(in_chs, out_chs):
    return nn.Sequential(
        nn.Conv2d(in_chs, out_chs // 2, kernel_size=3, stride=2, padding=1),
        nn.BatchNorm2d(out_chs // 2),
        nn.ReLU(),
        nn.Conv2d(out_chs // 2, out_chs, kernel_size=3, stride=2, padding=1),
        nn.BatchNorm2d(out_chs),
        nn.ReLU(), )

观察 2:一致的特征维度对于选择 token 混合器很重要。MHSA 不一定是速度瓶颈。

最近的工作将基于 ViT 的模型扩展到 MetaFormer,该架构由 MLP 块和未指定的 token 混合器组成。在构建基于 ViT 的模型时,选择 token 混合器是一个重要的设计选择。token 混合器的选择有很多:传统的 MHSA 混合器,它们具有全局感受野;更复杂的移位窗口注意;或者像池化这样的非参数化操作符。

作者将比较范围缩小到两种 token 混合器,池化和 MHSA,其中选择前者是为了简单和高效,而后者是为了更好的性能。大多数公开的移动编译器目前不支持更复杂的 token 混合器,如移位窗口,因此作者将其排除在范围之外。此外,由于本文专注于在没有轻量级卷积帮助的情况下构建体系结构,因此没有使用深度卷积来取代池化。

为了解两个 token 混合器的延迟,作者执行以下两个比较:

首先,通过比较 PoolFormer-s24 和 LeViT-256,作者发现 reshape 操作是 LeViT-256 的一个瓶颈。LeViT-256 的大部分是用 CONV on 4D tensor 实现的,在特征转发到 MHSA 时需要频繁的 reshape 操作,因为 MHSA 必须在 3D tensor 上进行注意(丢弃注意力头的额外尺寸)。reshape 的广泛使用限制了 LeViT 在移动设备上的速度。另一方面,当网络主要由基于 CONV 的实现组成时,池化自然适合 4D 张量,例如,CONV 1×1 作为 MLP 实现,CONV stem 用于下采样。因此,PoolFormer 具有更快的推理速度。 其次,通过比较 DeiT-S 和 LeViT-256,作者发现,如果特征尺寸一致且不需要 reshape,MHSA 不会给手机带来显著的开销。虽然计算量更大,但具有一致 3D 特征的 DeiT-S 可以达到与新 ViT 变体(即 LeViT-256)相当的速度。

观察 3:CONV-BN 比 LN-Linear 更适合延迟,准确性缺陷通常是可以接受的。

选择 MLP 实现是另一个重要的设计选择。通常,会选择两个选项之一:带 3D 线性投影(proj)的 LayerNorm(LN)和带 BatchNorm(BN)的 CONV1×1。CONV-BN 更适合低延迟,因为 BN 可以折叠到之前的卷积用于推理加速,而 LN 仍在推理阶段收集运行统计信息,从而导致延迟。根据本文的实验结果和之前的工作,LN 引入的延迟约占整个网络延迟的 10%-20%。

观察 4:非线性延迟取决于硬件和编译器。

最后,作者研究了非线性,包括 GeLU、ReLU 和 HardSwish。之前的工作表明 GeLU 在硬件上效率不高,会减慢推理速度。然而,作者观察到,GeLU 受到 iPhone 12 的良好支持,几乎不比它的对手 ReLU 慢。

相反,在本文的实验中,HardSwish 的速度惊人地慢,编译器可能无法很好地支持它(LeViT-256 使用 HardSwish 的延迟为 44.5 ms,而使用 GeLU 的延迟为 11.9 ms)。作者的结论是,考虑到手头的特定硬件和编译器,非线性应该根据具体情况来确定。

1.1 EfficientFormer 结构

基于延迟分析,作者提出了 EfficientFormer 的设计,如上图所示。该网络由 patch 嵌入(PatchEmbed)和 meta transformer 块堆栈组成,表示为 MB:

其中 𝑋0 是 Batch 大小为 B、空间大小为 [𝐻,𝑊] 的输入图像,y 是所需输出,m 是块的总数(深度)。MB 由未指定的 token 混合器(TokenMixer)和一个 MLP 块组成,可以表示为:

其中, 是输入到第 𝑖 个 𝑀𝐵 的中间特征。作者进一步将 Stage(或 S)定义为多个 MetaBlocks 的堆栈,这些 MetaBlocks 处理具有相同空间大小的特征,如上图中的 𝑁1× 表示 𝑆1 具有 𝑁1 个 MetaBlocks。该网络包括 4 个阶段。在每个阶段中,都有一个嵌入操作来投影嵌入维度和下采样 token 长度,如上图所示。在上述架构中,EfficientFormer 是一个完全基于 Transformer 的模型,无需集成 MobileNet 结构。接下来,作者深入研究了网络设计的细节。

1.2 Dimension-consistent Design

作者提出了一种维度一致性设计,该设计将网络分割为 4D 分区,其中操作符以卷积网络样式实现(MB4D),以及一个 3D 分区,其中线性投影和注意力在 3D 张量上执行,以在不牺牲效率的情况下享受 MHSA 的全局建模能力(MB3D),如上图所示。具体来说,网络从 4D 分区开始,而 3D 分区应用于最后阶段。注意,上图只是一个实例,4D 和 3D 分区的实际长度稍后通过架构搜索指定。

首先,输入图像由一个具有两个步长为 2,感受野为 3×3 卷积的 Conv stem 处理:

然后,网络从 MB4D 开始,使用一个简单的池化混合器来提取低级特征:

注意,这里作者没有在池化混合器之前使用 LN,因为 4D 分区是基于 CONV-BN 的设计,因此每个池化混合器前面都有一个 BN。

在处理完所有 MB4D 块后,作者执行一次 reshape 以变换特征大小并进入 3D 分区。MB3D 遵循传统 ViT 结构,如上图所示:

python 复制代码
#以 1x1 卷积为主的 MLP
class Mlp(nn.Module):
    def __init__(self, in_features, hidden_features=None,
                 out_features=None, act_layer=nn.GELU, drop=0.):
        super().__init__()
        out_features = out_features or in_features
        hidden_features = hidden_features or in_features
        self.fc1 = nn.Conv2d(in_features, hidden_features, 1)
        self.act = act_layer()
        self.fc2 = nn.Conv2d(hidden_features, out_features, 1)
        self.drop = nn.Dropout(drop)
        self.apply(self._init_weights)
 
        self.norm1 = nn.BatchNorm2d(hidden_features)
        self.norm2 = nn.BatchNorm2d(out_features)
 
    def _init_weights(self, m):
        if isinstance(m, nn.Conv2d):
            trunc_normal_(m.weight, std=.02)
            if m.bias is not None:
                nn.init.constant_(m.bias, 0)
 
    def forward(self, x):
        x = self.fc1(x)
        x = self.norm1(x)
        x = self.act(x)
        x = self.drop(x)
        x = self.fc2(x)
        x = self.norm2(x)
        x = self.drop(x)
        return 

class Meta4D(nn.Module):
 
    def __init__(self, dim, pool_size=3, mlp_ratio=4.,
                 act_layer=nn.GELU,
                 drop=0., drop_path=0.,
                 use_layer_scale=True, layer_scale_init_value=1e-5):
        super().__init__()
 
        self.token_mixer = Pooling(pool_size=pool_size)
        mlp_hidden_dim = int(dim * mlp_ratio)
        #MLP 层
        self.mlp = Mlp(in_features=dim, hidden_features=mlp_hidden_dim,
                       act_layer=act_layer, drop=drop)
        #drop_path 目的是在一个 batch 里面随机去除一部分样本,起正则化作用
        self.drop_path = DropPath(drop_path) if drop_path > 0. \
            else nn.Identity()
        self.use_layer_scale = use_layer_scale #可学习的参数,提供一个特征的缩放
        if use_layer_scale:
            self.layer_scale_1 = nn.Parameter(
                layer_scale_init_value * torch.ones((dim)), requires_grad=True)
            self.layer_scale_2 = nn.Parameter(
                layer_scale_init_value * torch.ones((dim)), requires_grad=True)
 
    def forward(self, x):
        if self.use_layer_scale:
            x = x + self.drop_path(
                self.layer_scale_1.unsqueeze(-1).unsqueeze(-1)
                * self.token_mixer(x))
            x = x + self.drop_path(
                self.layer_scale_2.unsqueeze(-1).unsqueeze(-1)
                * self.mlp(x))
        else:
            x = x + self.drop_path(self.token_mixer(x))
            x = x + self.drop_path(self.mlp(x))
        return x

1.3 Latency Driven Slimming

基于维度一致性设计,作者构建了一个超网,用于搜索上图所示网络架构的有效模型(上图显示了搜索的最终网络的示例)。为了表示这样的超网,作者定义了元路径(MetaPath,MP),它是可能的块的集合:

其中 𝐼 表示 identity path,j 表示第 𝑗 阶段,i 表示第 𝑖 个块。

在超网的 𝑆1 和 𝑆2 中,每个块可以从 MB4D 或 I 中选择,而在 𝑆3 和 𝑆4 中,块可以是 MB3D、MB4D 或 I。出于两个原因,作者仅在最后两个阶段启用 MB3D:首先,由于 MHSA 的计算相对于 token 长度呈二次增长,因此在早期阶段对其进行集成将大大增加计算成本。其次,将全局 MHSA 应用于最后阶段符合这样一种直觉,即网络的早期阶段捕获低级特征,而后期阶段学习长期依赖性。

python 复制代码
#Meta3D 与 Meta4D 在 MLP 有不同,Meta3D 使用如下的 LinearMLP,主要以线性层为主
class LinearMlp(nn.Module):
    """ MLP as used in Vision Transformer, MLP-Mixer and related networks
    """ 
    def __init__(self, in_features, hidden_features=None, out_features=None, act_layer=nn.GELU, drop=0.):
        super().__init__()
        out_features = out_features or in_features
        hidden_features = hidden_features or in_features
 
        self.fc1 = nn.Linear(in_features, hidden_features)
        self.act = act_layer()
        self.drop1 = nn.Dropout(drop)
        self.fc2 = nn.Linear(hidden_features, out_features)
        self.drop2 = nn.Dropout(drop)
 
    def forward(self, x):
        x = self.fc1(x)
        x = self.act(x)
        x = self.drop1(x)
        x = self.fc2(x)
        x = self.drop2(x)
        return x
    
class Meta3D(nn.Module):
 
    def __init__(self, dim, mlp_ratio=4.,
                 act_layer=nn.GELU, norm_layer=nn.LayerNorm,
                 drop=0., drop_path=0.,
                 use_layer_scale=True, layer_scale_init_value=1e-5):
        super().__init__()
        self.norm1 = norm_layer(dim)
        self.token_mixer = Attention(dim)
        self.norm2 = norm_layer(dim)
        mlp_hidden_dim = int(dim * mlp_ratio)
        self.mlp = LinearMlp(in_features=dim, hidden_features=mlp_hidden_dim,
                             act_layer=act_layer, drop=drop)
 
        self.drop_path = DropPath(drop_path) if drop_path > 0. \
            else nn.Identity()
        self.use_layer_scale = use_layer_scale
        if use_layer_scale:
            self.layer_scale_1 = nn.Parameter(
                layer_scale_init_value * torch.ones((dim)), requires_grad=True)
            self.layer_scale_2 = nn.Parameter(
                layer_scale_init_value * torch.ones((dim)), requires_grad=True)
 
    def forward(self, x):
        if self.use_layer_scale:
            x = x + self.drop_path(
                self.layer_scale_1.unsqueeze(0).unsqueeze(0)
                * self.token_mixer(self.norm1(x)))
            x = x + self.drop_path(
                self.layer_scale_2.unsqueeze(0).unsqueeze(0)
                * self.mlp(self.norm2(x)))
        else:
            x = x + self.drop_path(self.token_mixer(self.norm1(x)))
            x = x + self.drop_path(self.mlp(self.norm2(x)))
        return x

以前的硬件感知网络搜索方法通常依赖于在搜索空间中部署每个候选对象的硬件来获得延迟,这非常耗时。在这项工作中,作者提出了一种简单、快速但有效的基于梯度的搜索算法,以获得只需训练一次超网的候选网络。该算法有三个主要步骤。

首先,作者使用 Gumble Softmax 采样对超网进行训练,以获得每个 MP 内块的重要性得分,可以表示为:

其中,α评估 MP 中每个块的重要性,因为它表示选择块的概率;;τ是温度;n 代表 MP 中的块体类型,即对于 𝑆1 和 𝑆2,;对于 S_{3}和 S_{4},。通过训练之后,以获得经过训练的权重和结构参数α。

其次,作者通过收集不同宽度(16 的倍数)的 MB4D 和 MB3D 的设备上延迟来构建延迟查找表。

最后,作者使用查找表在第一步通过延迟评估获得的超网上执行网络瘦身。典型的基于梯度的搜索算法仅选择α最大的块,这不符合本文的范围,因为它无法搜索宽度 𝐶𝑗。事实上,构造一个多宽度的超网需要消耗显存,甚至是不现实的,因为在本文的设计中,每个 MP 都有几个分支。作者不在复杂的搜索空间上直接搜索,而是在单宽度超网上执行逐步瘦身,如下所示。

作者首先将 S_{1,2}和 S_{3,4}的 𝑀𝑃𝑖 重要性得分分别定义为和 𝛼𝑖4𝐷𝛼𝑖𝐼 和 𝛼𝑖3𝐷+𝛼𝑖4𝐷𝛼𝑖𝐼。同样,每个阶段的重要性得分可以通过将该阶段内所有 MP 的得分相加得到。根据重要性得分,作者定义了包含三个选项的动作空间:1)选择 I 作为最小导入 MP,2)删除第一个 MB3D,3)减少最不重要阶段的宽度(乘以 16)。然后,通过查找表计算每个动作的延迟,并评估每个动作的准确度下降。最后,根据每延迟准确度下降 (−𝑚𝑠) 来选择操作。此过程将迭代执行,直到达到目标延迟。

EfficientFormer 一共有 4 个阶段。每个阶段都有一个 Embeding(两个 3x3 的 Conv 组成一个 Embeding)来投影 Token 长度(可以理解为 CNN 中的 feature map)。EfficientFormer 是一个完全基于 Transformer 设计的模型,并没有集成 MobileNet 相关内容。最后通过 AUTOML 来搜索 MB_3D 和 MB_4D block 相关参数。最后堆叠 block 形成最终网络。

2. EfficientFormer V2 模型

论文地址:https://openaccess.thecvf.com/content/ICCV2023/papers/Li_Rethinking_Vision_Transformers_for_MobileNet_Size_and_Speed_ICCV_2023_paper.pdf

论文重新审视了 ViT 的设计选择,并提出了一种具有低延迟和高参数效率的改进型超网络。论文进一步引入了一种细粒度联合搜索策略,该策略可以通过同时优化延迟和参数量来找到有效的架构。所提出的模型 EfficientFormerV2 在 ImageNet-1K 上实现了比 MobileNetV2 和 MobileNetV1 高约 4%的 top-1 精度,具有相似的延迟和参数。

EfficientFormerV2 相对于 EfficientFormer 的主要改进如下图所示:

结合局部信息可以提高性能,并使 ViT 在缺少显式位置嵌入的情况下更加鲁棒。PoolFormer 和 EfficientFormer 使用 3×3 平均池化层(作为 local token 混合器。用相同内核大小的 depth-wise 卷积)替换这些层不会引入耗时开销,而使用可忽略的额外参数(0.02M),性能提高了 0.6%。此外。在 ViT 中的前馈网络(FFN)中注入局部信息建模层也有利于以较小的开销提高性能。值得注意的是,通过在 FFN 中放置额外的 depth-wise 3×3 卷积来捕获局部信息,复制了原始局部混合器(池或卷积)的功能。基于这些观察,论文移除了显式残差连接的 local token 混合器,并将 depth-wise 3×3 CONV 移动到 FFN 中,以获得 locality enabled 的统一 FFN(上图(b))。论文将统一的 FFN 应用于网络的所有阶段,如上图(a,b)所示。这种设计修改将网络架构简化为仅两种类型的 block(local FFN 和 global attention),并在相同的耗时(见表 1)下将精度提高到 80.3%,参数开销较小(0.1M)。更重要的是,该修改允许直接使用模块的确切数量搜索网络深度,以提取局部和全局信息,尤其是在网络的后期阶段。

2.1 搜索空间优化

通过统一的 FFN 和删除残差连接的 token mixer,V2 检查来自 EfficientFormer 的搜索空间是否仍然足够,特别是在深度方面。论文改变了网络深度(每个阶段中的 block 数)和宽度(通道数),并发现更深和更窄的网络会带来更好的精度(0.2%的改进)、更少的参数(0.3M 的减少)和更少的耗时(0.1ms 的加速),如上表所示。因此,论文将此网络设置为新的基线(精度 80.5%),以验证后续的设计修改,并为架构搜索提供更深入的超网络。

此外,具有进一步缩小的空间分辨率(1/64)的 5 阶段模型已广泛用于有效的 ViT 工作。为了证明是否应该从一个 5 阶段超网络中搜索,论文在当前的基线网络中添加了一个额外的阶段,并验证了性能增益和开销。值得注意的是,尽管考虑到小的特征分辨率,计算开销不是一个问题,但附加阶段是参数密集型的。因此需要缩小网络维度(深度或宽度),以将参数和延迟与基线模型对齐,以便进行公平比较。如上表所示,尽管节省了 MACs(0.12G),但 5 阶段模型的最佳性能在更多参数(0.39M)和延迟开销(0.2ms)的情况下意外降至 80.31%。这符合直觉,即五阶段计算效率高,但参数密集。鉴于 5 阶段网络无法在现有的规模和速度范围内引入更多潜力,论文坚持 4 阶段设计。这一分析也解释了为什么某些 ViT 在 MACs 精度方面提供了出色的 Pareto curve,但在大小上往往非常冗余。作为最重要的一点,优化单一度量很容易陷入困境。

python 复制代码
# 深度卷积前馈网络(局部模块)
class FFN(nn.Module):
    def __init__(self, dim, pool_size=3, mlp_ratio=4.,
                 act_layer=nn.GELU,
                 drop=0., drop_path=0.,
                 use_layer_scale=True, layer_scale_init_value=1e-5):
        super().__init__()
 
        mlp_hidden_dim = int(dim * mlp_ratio)       # 隐藏层维度
        self.mlp = Mlp(in_features=dim, hidden_features=mlp_hidden_dim,
                       act_layer=act_layer, drop=drop, mid_conv=True)       # 多层感知机
 
        self.drop_path = DropPath(drop_path) if drop_path > 0. \
            else nn.Identity()
        self.use_layer_scale = use_layer_scale
        if use_layer_scale:
            self.layer_scale_2 = nn.Parameter(
                layer_scale_init_value * torch.ones(dim).unsqueeze(-1).unsqueeze(-1), requires_grad=True)   # 缩放因子
 
    def forward(self, x):
        if self.use_layer_scale:
            x = x + self.drop_path(self.layer_scale_2 * self.mlp(x))    # 残差连接
        else:
            x = x + self.drop_path(self.mlp(x))
        return x

2.2 多头注意力改进

论文研究了在不增加模型大小和耗时的额外开销的情况下提高注意力模块性能的技术。如图(c)所示,论文研究了 MHSA 的两种方法。首先通过添加 depth-wise 3×3 CONV 将局部信息注入到 Value 矩阵(V)中,也采用了这种方法。其次通过在 head 维度上添加全连接层来实现注意力头之间的通信,如图 2(c)所示。通过这些修改,进一步将性能提高到 80.8%,与基线模型相比,具有相似的参数和延迟。

python 复制代码
class Attention4D(torch.nn.Module):
    def __init__(self, dim=384, key_dim=32, num_heads=8,
                 attn_ratio=4,
                 resolution=7,
                 act_layer=nn.ReLU,
                 stride=None):
        super().__init__()
        self.num_heads = num_heads
        self.scale = key_dim ** -0.5
        self.key_dim = key_dim
        self.nh_kd = nh_kd = key_dim * num_heads
 
        if stride is not None:
            self.resolution = math.ceil(resolution / stride)
            self.stride_conv = nn.Sequential(nn.Conv2d(dim, dim, kernel_size=3, stride=stride, padding=1, groups=dim),
                                             nn.BatchNorm2d(dim), )
            self.upsample = nn.Upsample(scale_factor=stride, mode='bilinear')
        else:
            self.resolution = resolution
            self.stride_conv = None
            self.upsample = None
 
        self.N = self.resolution ** 2
        self.N2 = self.N
        self.d = int(attn_ratio * key_dim)
        self.dh = int(attn_ratio * key_dim) * num_heads
        self.attn_ratio = attn_ratio
        h = self.dh + nh_kd * 2
        self.q = nn.Sequential(nn.Conv2d(dim, self.num_heads * self.key_dim, 1),
                               nn.BatchNorm2d(self.num_heads * self.key_dim), )
        self.k = nn.Sequential(nn.Conv2d(dim, self.num_heads * self.key_dim, 1),
                               nn.BatchNorm2d(self.num_heads * self.key_dim), )
        self.v = nn.Sequential(nn.Conv2d(dim, self.num_heads * self.d, 1),
                               nn.BatchNorm2d(self.num_heads * self.d),
                               )
        self.v_local = nn.Sequential(nn.Conv2d(self.num_heads * self.d, self.num_heads * self.d,
                                               kernel_size=3, stride=1, padding=1, groups=self.num_heads * self.d),
                                     nn.BatchNorm2d(self.num_heads * self.d), )
        self.talking_head1 = nn.Conv2d(self.num_heads, self.num_heads, kernel_size=1, stride=1, padding=0)
        self.talking_head2 = nn.Conv2d(self.num_heads, self.num_heads, kernel_size=1, stride=1, padding=0)
 
        self.proj = nn.Sequential(act_layer(),
                                  nn.Conv2d(self.dh, dim, 1),
                                  nn.BatchNorm2d(dim), )
 
        points = list(itertools.product(range(self.resolution), range(self.resolution)))
        N = len(points)
        attention_offsets = {}
        idxs = []
        for p1 in points:
            for p2 in points:
                offset = (abs(p1[0] - p2[0]), abs(p1[1] - p2[1]))
                if offset not in attention_offsets:
                    attention_offsets[offset] = len(attention_offsets)
                idxs.append(attention_offsets[offset])
        self.attention_biases = torch.nn.Parameter(
            torch.zeros(num_heads, len(attention_offsets)))
        self.register_buffer('attention_bias_idxs',
                             torch.LongTensor(idxs).view(N, N))
 
    @torch.no_grad()
    def train(self, mode=True):
        super().train(mode)
        if mode and hasattr(self, 'ab'):
            del self.ab
        else:
            self.ab = self.attention_biases[:, self.attention_bias_idxs]
 
    def forward(self, x):  # x (B,N,C)
        B, C, H, W = x.shape
        if self.stride_conv is not None:
            x = self.stride_conv(x)
 
        q = self.q(x).flatten(2).reshape(B, self.num_heads, -1, self.N).permute(0, 1, 3, 2)     # 降维为 2 维,转置
        k = self.k(x).flatten(2).reshape(B, self.num_heads, -1, self.N).permute(0, 1, 2, 3)
        v = self.v(x)
        v_local = self.v_local(v)       # 通过一个 3x3 的卷积将局部信息注入 V 中
        v = v.flatten(2).reshape(B, self.num_heads, -1, self.N).permute(0, 1, 3, 2)     # 降维为 2 维,转置
 
        attn = (
                (q @ k) * self.scale    # 矩阵乘法,计算相似度
                +
                (self.attention_biases[:, self.attention_bias_idxs]     # 融合一个位置编码
                 if self.training else self.ab)
        )
        # attn = (q @ k) * self.scale
        attn = self.talking_head1(attn)     # 1x1 卷积->全连接,实现注意力头部之间的通信
        attn = attn.softmax(dim=-1)
        attn = self.talking_head2(attn)     # 同上
 
        x = (attn @ v)      # 注意力融合
 
        out = x.transpose(2, 3).reshape(B, self.dh, self.resolution, self.resolution) + v_local     # 最后再与 v_local 融合
        if self.upsample is not None:
            out = self.upsample(out)
 
        out = self.proj(out)    # 输出再进行激活 + 卷积 + 正则
        return out
python 复制代码
#AttnFFN(Local Global 模块)
class AttnFFN(nn.Module):
    def __init__(self, dim, mlp_ratio=4.,
                 act_layer=nn.ReLU, norm_layer=nn.LayerNorm,
                 drop=0., drop_path=0.,
                 use_layer_scale=True, layer_scale_init_value=1e-5,
                 resolution=7, stride=None):
 
        super().__init__()
 
        self.token_mixer = Attention4D(dim, resolution=resolution, act_layer=act_layer, stride=stride)      # MHSA 多头自注意力
        mlp_hidden_dim = int(dim * mlp_ratio)       # 隐藏层维度
        self.mlp = Mlp(in_features=dim, hidden_features=mlp_hidden_dim,
                       act_layer=act_layer, drop=drop, mid_conv=True)       # 深度卷积
 
        self.drop_path = DropPath(drop_path) if drop_path > 0. \
            else nn.Identity()      # drop_path 概率
        self.use_layer_scale = use_layer_scale
        if use_layer_scale:     # 缩放因子
            self.layer_scale_1 = nn.Parameter(
                layer_scale_init_value * torch.ones(dim).unsqueeze(-1).unsqueeze(-1), requires_grad=True)
            self.layer_scale_2 = nn.Parameter(
                layer_scale_init_value * torch.ones(dim).unsqueeze(-1).unsqueeze(-1), requires_grad=True)
 
    def forward(self, x):
        if self.use_layer_scale:
            x = x + self.drop_path(self.layer_scale_1 * self.token_mixer(x))    # 多头自注意力 + 残差连接
            x = x + self.drop_path(self.layer_scale_2 * self.mlp(x))    # mlp 深度卷积 + 残差连接
 
        else:
            x = x + self.drop_path(self.token_mixer(x))
            x = x + self.drop_path(self.mlp(x))
        return x

2.3 更高分辨率注意力

注意机制有利于性能。然而,将其应用于高分辨率特征会损害部署效率,因为它具有与空间分辨率相对应的二次时间复杂度。论文研究了将 MHSA 有效应用于更高分辨率(早期阶段)的策略。回想一下,在当前基线网络中,MHSA 仅在输入图像空间分辨率为 1/32 的最后阶段使用。论文将额外的 MHSA 应用于具有 1/16 特征大小的倒数第二个阶段,并观察到准确度提高了 0.9%。另一方面,推理速度减慢了几乎 2.7 倍。因此,有必要适当降低注意力模块的复杂性。

尽管一些工作提出了基于窗口的注意力,或下采样 Key 和 Value 来缓解这个问题,但论文发现它们不是最适合移动部署的选项。由于复杂的窗口划分和重新排序,基于窗口的注意力很难在移动设备上加速。对于[40]中的下采样 Key(K)和 Value(V),需要全分辨率查询(Q)来保持注意力矩阵乘法后的输出分辨率(Out):

根据测试该模型的耗时仅下降到 2.8ms,仍然比基线模型慢 2 倍。因此,为了在网络的早期阶段执行 MHSA,论文将所有 Query、Key 和 Value 降采样到固定的空间分辨率(1/32),并将注意力的输出插值回原始分辨率,以馈送到下一层,如图所示((d)和(e))。我们称这种方法为"Stride Attention"。如表所示,这种简单的近似值将延迟从 3.5ms 显著降低到 1.5ms,并保持了具有竞争力的准确性(81.5%对 81.7%)。

python 复制代码
class Attention4DDownsample(torch.nn.Module):
    def __init__(self, dim=384, key_dim=16, num_heads=8,
                 attn_ratio=4,
                 resolution=7,
                 out_dim=None,
                 act_layer=None,
                 ):
        super().__init__()
        self.num_heads = num_heads
        self.scale = key_dim ** -0.5
        self.key_dim = key_dim
        self.nh_kd = nh_kd = key_dim * num_heads
 
        self.resolution = resolution
 
        self.d = int(attn_ratio * key_dim)
        self.dh = int(attn_ratio * key_dim) * num_heads
        self.attn_ratio = attn_ratio
        h = self.dh + nh_kd * 2
 
        if out_dim is not None:
            self.out_dim = out_dim
        else:
            self.out_dim = dim
 
        self.resolution2 = math.ceil(self.resolution / 2)
        self.q = LGQuery(dim, self.num_heads * self.key_dim, self.resolution, self.resolution2)         # 双注意力下采样
 
        self.N = self.resolution ** 2
        self.N2 = self.resolution2 ** 2
 
        self.k = nn.Sequential(nn.Conv2d(dim, self.num_heads * self.key_dim, 1),
                               nn.BatchNorm2d(self.num_heads * self.key_dim), )
        self.v = nn.Sequential(nn.Conv2d(dim, self.num_heads * self.d, 1),
                               nn.BatchNorm2d(self.num_heads * self.d),
                               )
        self.v_local = nn.Sequential(nn.Conv2d(self.num_heads * self.d, self.num_heads * self.d,
                                               kernel_size=3, stride=2, padding=1, groups=self.num_heads * self.d),
                                     nn.BatchNorm2d(self.num_heads * self.d), )
 
        self.proj = nn.Sequential(
            act_layer(),
            nn.Conv2d(self.dh, self.out_dim, 1),
            nn.BatchNorm2d(self.out_dim), )
 
        points = list(itertools.product(range(self.resolution), range(self.resolution)))
        points_ = list(itertools.product(
            range(self.resolution2), range(self.resolution2)))
        N = len(points)
        N_ = len(points_)
        attention_offsets = {}
        idxs = []
        for p1 in points_:
            for p2 in points:
                size = 1
                offset = (
                    abs(p1[0] * math.ceil(self.resolution / self.resolution2) - p2[0] + (size - 1) / 2),
                    abs(p1[1] * math.ceil(self.resolution / self.resolution2) - p2[1] + (size - 1) / 2))
                if offset not in attention_offsets:
                    attention_offsets[offset] = len(attention_offsets)
                idxs.append(attention_offsets[offset])
 
        self.register_buffer('attention_biases', torch.zeros(num_heads, 196))
        self.register_buffer('attention_bias_idxs',
                             torch.ones(49, 196).long())
 
        self.attention_biases_seg = torch.nn.Parameter(
            torch.zeros(num_heads, len(attention_offsets)))
        self.register_buffer('attention_bias_idxs_seg',
                             torch.LongTensor(idxs).view(N_, N))
 
    @torch.no_grad()
    def train(self, mode=True):
        super().train(mode)
        if mode and hasattr(self, 'ab'):
            del self.ab
        else:
            self.ab = self.attention_biases_seg[:, self.attention_bias_idxs_seg]
 
    def forward(self, x):  # x (B,N,C)
        B, C, H, W = x.shape
 
        q = self.q(x).flatten(2).reshape(B, self.num_heads, -1, H * W // 4).permute(0, 1, 3, 2)
        k = self.k(x).flatten(2).reshape(B, self.num_heads, -1, H * W).permute(0, 1, 2, 3)
        v = self.v(x)
        v_local = self.v_local(v)
        v = v.flatten(2).reshape(B, self.num_heads, -1, H * W).permute(0, 1, 3, 2)
 
        attn = (q @ k) * self.scale                     # (H * W // 4, H * W)
        bias = self.attention_biases_seg[:, self.attention_bias_idxs_seg] if self.training else self.ab
        bias = torch.nn.functional.interpolate(bias.unsqueeze(0), size=(attn.size(-2), attn.size(-1)), mode='bicubic')
        attn = attn + bias
 
        attn = attn.softmax(dim=-1)
 
        x = (attn @ v).transpose(2, 3)                  # (H * W // 4, H * W)
        out = x.reshape(B, self.dh, H // 2, W // 2) + v_local
 
        out = self.proj(out)
        return out

2.4 注意力降采样

大多数视觉主干利用跨步卷积或池化层来执行静态和局部下采样,并形成分层结构。最近的一些研究开始探索注意力下采样。例如,LeViT 和 UniNet 建议通过注意力机制将特征分辨率减半,以实现全局感受野的上下文感知下采样。具体而言,Query 中的 token 数量减少一半,以便对注意力模块的输出进行下采样:

然而,决定如何减少 Query 中的 token 数量是非常重要的。Graham 等人根据经验使用池化对 Query 进行下采样,而 Liu 等人建议搜索局部或全局方法。为了在移动设备上实现可接受的推理速度,将注意力下采样应用于具有高分辨率的早期阶段是不利的,这限制了以更高分辨率搜索不同下采样方法的现有工作的价值。

相反,论文提出了一种同时使用局部性和全局依赖性的组合策略,如图(f)所示。为了获得下采样的 Query,论文使用池化层作为静态局部下采样,使用 3×3 DWCONV 作为可学习的局部下采样。此外,注意力下采样模块残差连接到 regular strided CONV,以形成 local-global 方式,类似于下采样 bottlenecks 或 inverted bottlenecks。如表所示,通过略微增加参数和耗时开销,论文进一步将注意力下采样的准确率提高到 81.8%。

python 复制代码
class LGQuery(torch.nn.Module):
    def __init__(self, in_dim, out_dim, resolution1, resolution2):
        super().__init__()
        self.resolution1 = resolution1
        self.resolution2 = resolution2
        self.pool = nn.AvgPool2d(1, 2, 0)
        self.local = nn.Sequential(nn.Conv2d(in_dim, in_dim, kernel_size=3, stride=2, padding=1, groups=in_dim),
                                   )
        self.proj = nn.Sequential(nn.Conv2d(in_dim, out_dim, 1),
                                  nn.BatchNorm2d(out_dim), )
 
    def forward(self, x):
        B, C, H, W = x.shape
        local_q = self.local(x)
        pool_q = self.pool(x)
        q = local_q + pool_q            # 双路径下采
        q = self.proj(q)
        return q

对于模型大小,EfficientFormerV 2-S0 比 EdgeViT-XXS 超出了 1.3%的 top-1 精度,甚至少了 0.6M 参数,比 MobileNetV 2 ×1.0 优于 3.5%的 top-1,参数数量相似。对于大模型,EfficientFormerV 2-L 模型实现了与最近的 EfficientFormerL 7 相同的精度,同时小 3.1 倍。在速度方面,在延迟相当或更低的情况下,EfficientFormerV2-S2 的性能分别优于 UniNet-B1,EdgeViT-S 和 EfficientFormerL 1,分别为 0.8%,0.6%和 2.4%。EiffcientFormer V2-S1 的效率分别比 MobileViT-XS、EdgeViT-XXS 和 EdgeViTXS 高出 4.2%、4.6%和 1.5%,其中 MES 要高得多。

3. 小结

  • EfficientFormerV1 证明了视觉的 Transformer 可以在移动设备上以 MobileNet 速度运行。经全面的延迟分析,在一系列基于 VIT 的架构中识别低效的运算符,指导新的设计范式。

  • EfficientFormerV2 进一步提出了在大小和速度上的细粒度联合搜索,并获得了轻量级和推理速度超快的模型。

相关推荐
Altair澳汰尔9 分钟前
数据分析和AI丨知识图谱,AI革命中数据集成和模型构建的关键推动者
人工智能·算法·机器学习·数据分析·知识图谱
A懿轩A33 分钟前
C/C++ 数据结构与算法【栈和队列】 栈+队列详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·栈和队列
Python机器学习AI37 分钟前
分类模型的预测概率解读:3D概率分布可视化的直观呈现
算法·机器学习·分类
吕小明么1 小时前
OpenAI o3 “震撼” 发布后回归技术本身的审视与进一步思考
人工智能·深度学习·算法·aigc·agi
1 9 J2 小时前
数据结构 C/C++(实验五:图)
c语言·数据结构·c++·学习·算法
程序员shen1616112 小时前
抖音短视频saas矩阵源码系统开发所需掌握的技术
java·前端·数据库·python·算法
汝即来归2 小时前
选择排序和冒泡排序;MySQL架构
数据结构·算法·排序算法
咒法师无翅鱼3 小时前
【定理证明工具调研】Coq, Isabelle and Lean.
算法
风清云淡_A3 小时前
【java基础系列】实现数字的首位交换算法
java·算法
涵涵子RUSH3 小时前
合并K个升序链表(最优解)
算法·leetcode