一、前言
昨天读到了一篇有意思的文章,文章提出通过利用卷积调制操作来简化self-attention。还证明了这种简单的方法可以更好地利用卷积层中嵌套的大核(≥7 × 7)。我们都知道ViTs推动了设计识别模型的发展,近几年使用的也相当的多,通常就是CNN网络引入注意力机制,往往可以获得不错的性能,因为相比较与卷积cnn,self-attention能够模拟全局成对依赖关系,这是一种更有效的空间信息编码方式。
自注意机制与所提出的卷积调制运算的比较
作者注意到有研究者通过对标准ResNet进行现代化改进,采用与Transformers相似的设计和训练方法,ConvNets的性能在某些情况下甚至能够超越一些热门的ViTs。由此,引入了一种名为Conv2Former的新型网络结构,旨在更有效地利用空间卷积。与以往工作不同,Conv2Former采用了一种称为卷积调制的操作,通过将大内核卷积的输出与值表示进行Hadamard乘积,模拟了自注意力的输出计算过程。与传统的ViTs使用自注意力不同,Conv2Former完全是一个卷积网络,其计算复杂度随分辨率线性增加,而不是像Transformers那样呈二次增加。
二、Conv2Former的网络架构
在每两个阶段间,引入了一个Patch Embed来降低分辨率,通常是一个2 × 2的卷积,步幅为2。不同的阶段有不同数量的卷积块。
构建了五个Conv2Former变体,分别是Conv2Former- n、Conv2Former- t、Conv2Former- s、Conv2Former- b、Conv2Former- l。参数由下图所示:
残差块、自注意块和所提出的调制块之间的对比图
与自注意相比,(d)方法利用卷积来建立关系,这比自注意更节省内存,特别是在处理高分辨率图像时。与经典的残差块方法(a)相比,(d)还可以通过调制操作来适应输入内容。
在VGG,Resnet等网络提出后,3x3卷积几乎就是构建卷积网络的标准选择,但深度可分离卷积的出现改变了这一格局。ConvNeXt的研究显示,将卷积核从3扩大到7可以提升性能,然而,进一步增大卷积核几乎不再带来性能提升,反而增加了计算负担。
作者认为ConvNeXt未能从大于7×7的更大卷积核中获得更大性能提升的原因在于对空间卷积的使用方式。相比之下,Conv2Former通过观察发现,在卷积核大小从5×5增加到21×21的范围内,性能持续提升。这一现象在不同规模的Conv2Former模型中均得到验证,为更好地利用空间卷积提供了重要的设计指导。
考虑到模型的效率,作者选择将卷积核大小默认设置为11×11。这一研究发现对于ConvNet设计,更大的卷积核可以通过正确的使用方式获得持续的性能提升,为未来的视觉识别模型提供了有益的启示。
在Conv2Former的标准化和激活层方面,标准化层使用Layer Normalization,而不是常用的批标准化,激活层采用GELU。作者发现这种Layer Normalization和GELU的组合能够带来0.1%-0.2%的性能提升。
三、卷积调制操作
卷积核大小影响性能
实验证明,在Conv2Former中,增加深度卷积核大小(从5×5到21×21)可以显著提升模型性能,与传统结论不同,这表明使用卷积特征作为权重能更有效地利用大核,相较于传统方法无法带来明显性能提升。
Hadamard乘积优于求和
对比Hadamard乘积和元素求和两种融合策略,实验证明在Conv2Former中,使用深度卷积特征通过Hadamard乘积调制权重表现更好,尤其对于小型模型。这突显了卷积调制在编码空间信息方面的高效性。
权重策略分析
通过尝试不同的特征图融合方式,包括添加Sigmoid函数、应用L1标准化以及线性归一化等,结果显示Hadamard乘积依然是最优选择。有趣的是,将A的值调整为正值反而导致性能下降,与传统注意机制的做法有所不同,为未来研究提供了新的问题。
四、论文复现
作者也是给出了代码地址:HVision-NKU/Conv2Former
但我看了一下,里面只是给出了一些核心的代码,所以复现还是要靠咱自己。
"""
Copyright (c) 2023, Auorui.
All rights reserved.
reference <https://arxiv.org/pdf/2211.11943.pdf> (Conv2Former: A Simple Transformer-Style ConvNet for Visual Recognition)
Time:2023.12.31, Complete before the end of 2023.
"""
import torch
import torch.nn as nn
from pyzjr.Models import DropPath
C = {'n': [64, 128, 256, 512],
't': [72, 144, 288, 576],
's': [72, 144, 288, 576],
'b': [96, 192, 384, 768],
'l': [128, 256, 512, 1024],
} # reference <https://arxiv.org/pdf/2211.11943.pdf> Table 1
L = {'n': [2, 2, 8, 2],
't': [3, 3, 12, 3],
's': [4, 4, 32, 4],
'b': [4, 4, 34, 4],
'l': [4, 4, 48, 4],
} # reference <https://arxiv.org/pdf/2211.11943.pdf> Table 1
class MLP(nn.Module):
def __init__(self, dim, mlp_ratio=4.):
super().__init__()
self.norm = nn.LayerNorm(dim, eps=1e-6)
self.fc1 = nn.Conv2d(dim, dim * mlp_ratio, 1)
self.pos = nn.Conv2d(dim * mlp_ratio, dim * mlp_ratio, 3, padding=1, groups=dim * mlp_ratio)
self.fc2 = nn.Conv2d(dim * mlp_ratio, dim, 1)
self.act = nn.GELU()
def forward(self, x):
x = self.norm(x.permute(0, 2, 3, 1)).permute(0, 3, 1, 2)
x = self.fc1(x)
x = self.act(x)
x = x + self.act(self.pos(x))
x = self.fc2(x)
return x
class ConvMod(nn.Module):
def __init__(self, dim):
super().__init__()
self.norm = nn.LayerNorm(dim, eps=1e-6)
self.a = nn.Sequential(
nn.Conv2d(dim, dim, 1),
nn.GELU(),
nn.Conv2d(dim, dim, 11, padding=5, groups=dim)
)
self.v = nn.Conv2d(dim, dim, 1)
self.proj = nn.Conv2d(dim, dim, 1)
def forward(self, x):
x = self.norm(x.permute(0, 2, 3, 1)).permute(0, 3, 1, 2)
a = self.a(x)
x = a * self.v(x)
x = self.proj(x)
return x
class Block(nn.Module):
def __init__(self, dim, mlp_ratio=4.0, drop_path=0.):
super().__init__()
self.attn = ConvMod(dim)
self.mlp = MLP(dim, mlp_ratio)
layer_scale_init_value = 1e-6
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)
self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity()
def forward(self, x):
x = x + self.drop_path(self.layer_scale_1.unsqueeze(-1).unsqueeze(-1) * self.attn(x))
x = x + self.drop_path(self.layer_scale_2.unsqueeze(-1).unsqueeze(-1) * self.mlp(x))
return x
class BaseLayer(nn.Module):
def __init__(self, dim, depth, mlp_ratio=4., drop_path=None, downsample=True):
super().__init__()
self.dim = dim
self.drop_path = drop_path
self.blocks = nn.ModuleList([
Block(dim=self.dim,mlp_ratio=mlp_ratio,drop_path=drop_path[i],)
for i in range(depth)
])
# patch merging layer
if downsample:
self.downsample = nn.Sequential(
nn.GroupNorm(num_groups=1, num_channels=dim),
nn.Conv2d(dim, dim * 2, kernel_size=2, stride=2,bias=False)
)
else:
self.downsample = None
def forward(self, x):
for blk in self.blocks:
x = blk(x)
if self.downsample is not None:
x = self.downsample(x)
return x
class Conv2Former(nn.Module):
def __init__(self, num_classes=10, depths=(2,2,8,2), dim=(64,128,256,512), mlp_ratio=2.,drop_rate=0.,
drop_path_rate=0.15, **kwargs):
super().__init__()
norm_layer = nn.LayerNorm
self.num_classes = num_classes
self.num_layers = len(depths)
self.dim = dim
self.mlp_ratio = mlp_ratio
self.pos_drop = nn.Dropout(p=drop_rate)
# stochastic depth decay rule
dpr = [x.item() for x in torch.linspace(0, drop_path_rate, sum(depths))]
# build layers
self.layers = nn.ModuleList()
for i_layer in range(self.num_layers):
layer = BaseLayer(dim[i_layer],
depth=depths[i_layer],
mlp_ratio=self.mlp_ratio,
drop_path=dpr[sum(depths[:i_layer]):sum(depths[:i_layer + 1])],
downsample=(i_layer < self.num_layers - 1),
)
self.layers.append(layer)
self.fc1 = nn.Conv2d(3, dim[0], 1)
self.norm = norm_layer(dim[-1], eps=1e-6,)
self.avgpool = nn.AdaptiveAvgPool2d(1)
self.head = nn.Linear(dim[-1], num_classes) \
if num_classes > 0 else nn.Identity()
self.apply(self._init_weights)
def _init_weights(self, m):
if isinstance(m, nn.Linear):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
if m.bias is not None:
nn.init.constant_(m.bias, 0.)
elif isinstance(m, (nn.Conv1d, nn.Conv2d)):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
if m.bias is not None:
nn.init.constant_(m.bias, 0.)
elif isinstance(m, (nn.LayerNorm, nn.GroupNorm)):
nn.init.constant_(m.bias, 0.)
nn.init.constant_(m.weight, 1.)
def forward_features(self, x):
x = self.fc1(x)
x = self.pos_drop(x)
for layer in self.layers:
x = layer(x)
x = self.norm(x.permute(0, 2, 3, 1)).permute(0, 3, 1, 2)
x = self.avgpool(x)
x = torch.flatten(x, 1)
return x
def forward(self, x):
x = self.forward_features(x)
x = self.head(x)
return x
if __name__ == '__main__':
model = Conv2Former(num_classes=10, depths=L["b"], dim=C["b"], mlp_ratio=2, drop_path_rate=0.1)
input_tensor = torch.randn(1, 3, 224, 224)
output = model(input_tensor)
print("Output shape:", output.shape)
暂时还未进行测试,如果有空再进行实验, 你可以从这里找到我的源码:pyzjr/pyzjr/Models/backbone/Conv2Former_2.py at main · Auorui/pyzjr (github.com)
总结
Conv2Former,这是一种新型的卷积神经网络架构,其核心是卷积调制操作,通过卷积和Hadamard乘积简化了自注意力机制。在ImageNet分类、目标检测和语义分割任务中,Conv2Former相对于先前的CNN和Transformer模型表现更优。作者强调了对大内核卷积的更有效利用,如何将提出的卷积调制块与Transformer结合起来,这是将来的研究方向。