探索是由一个关键问题引导的:Transformers中的设计决策如何影响ConvNets的性能?
1.训练决策
训练从ResNets最初的90个时期扩展到300个时期。我们使用AdamW优化器[46]、数据增强技术,如Mixup[90]、Cutmix[89]、RandAugment[14]、随机擦除[91],以及正则化方案,包括随机深度[36]和标签平滑[69]。
2.宏观设计
有两个有趣的设计考虑因素:阶段计算比例和“主干”结构。
2.1阶段计算比例
但阶段计算比例略有不同,为1:1:3:1。对于较大的SwinTransformer,比例为1:1:9:1。根据设计,我们将每个阶段的块数从ResNet-50中的(3,4,6,3)调整为(3,3,9,3),这也将FLOP与Swin-T对齐。这将模型的准确率从78.8%提高到79.4%
class ConvNeXt(nn.Module):
def __init__(self, in_chans=3, num_classes=1000,
depths=[3, 3, 9, 3], dims=[96, 192, 384, 768], drop_path_rate=0.,
layer_scale_init_value=1e-6, head_init_scale=1.,
):
研究人员已经彻底研究了计算的分布[53,54],很可能存在更优化的设计。
2.2将主干结构patch化
主干结构:将输入下采样至合适的feature map的过程
在Transformer中,主干结构使用了一种更激进的“patchify”策略,这与大的内核大小相对应(例如内核大小=14或16)和非重叠卷积。
Swin Transformer使用了类似的“patchify”层,但patch大小较小,为4,以适应架构的多阶段设计。我们用一个使用4*4,步幅为4的卷积层。
stem = nn.Sequential(
nn.Conv2d(in_chans, dims[0], kernel_size=4, stride=4),
LayerNorm(dims[0], eps=1e-6, data_format="channels_first")
)
2.3. ResNeXt-ify
ResNeXt的指导原则是“使用更多的组,扩大宽度”。
使用深度卷积,这是分组卷积的一种特殊情况,其中组的数量等于通道的数量。
原因:深度卷积类似于自注意中的加权和运算,它在每个通道的基础上进行运算,即仅在空间维度上混合信息。
self.dwconv = nn.Conv2d(dim, dim, kernel_size=7, padding=3, groups=dim)
# depthwise conv
2.4. Inverted Bottleneck
反向瓶颈:MLP块的隐藏维度比输入维度宽四倍
class Block(nn.Module):
def __init__(self, dim, drop_path=0., layer_scale_init_value=1e-6):
super().__init__()
self.dwconv = nn.Conv2d(dim, dim, kernel_size=7, padding=3, groups=dim) # depthwise conv
self.norm = LayerNorm(dim, eps=1e-6)
self.pwconv1 = nn.Linear(dim, 4 * dim) # pointwise/1x1 convs, implemented with linear layers
self.act = nn.GELU()
self.pwconv2 = nn.Linear(4 * dim, dim)
self.gamma = nn.Parameter(layer_scale_init_value * torch.ones((dim)),
requires_grad=True) if layer_scale_init_value > 0 else None
self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity()
2.5 大卷积核
最显著的方面之一是它们的非局部自我注意,这使每一层都有一个全局的感受野。
增大卷积核:
采用更大的内核大小卷积的好处是显著的。较大内核大小的好处在7*7时达到饱和点
self.dwconv = nn.Conv2d(dim, dim, kernel_size=7, padding=3, groups=dim) # depthwise conv
2.微观设计
重点关注激活函数和规范化层的具体选择。
2.1更换激活函数
高斯误差线性单元,或GELU[32],可以被认为是ReLU的一种更平滑的变体
2.2使用更少的激活函数
Transformer和ResNet块之间的一个小区别是Transformer具有较少的激活功能。
2.3 使用更少的BN层
只保留了一个BN层在1*1卷积之前
2.4 将BN替换成LN
在原始ResNet中直接用LN代替BN将导致次优性能[83]。随着网络架构和训练技术的所有修改,在使用LN训练时没有任何困难;事实上,性能稍好。
2.5分离出来的下采样层
在ResNet中,空间下采样是通过每个阶段开始时的残差块来实现的,使用3*3个conv,步幅2(和1*1在shortcut处具有步幅2的conv)。在Swin Transformers中,在级之间添加了一个单独的下采样层。
我们使用2*2具有用于空间下采样的步长为2的conv层。但是这个改变导致了训练的不收敛,因此又引入了归一化层帮助稳定训练
for i in range(3):
downsample_layer = nn.Sequential(
LayerNorm(dims[i], eps=1e-6, data_format="channels_first"),
nn.Conv2d(dims[i], dims[i+1], kernel_size=2, stride=2),
)
self.downsample_layers.append(downsample_layer)
ConvNeXt全部核心代码
class ConvNeXt(nn.Module):
r""" ConvNeXt
A PyTorch impl of : `A ConvNet for the 2020s` -
https://arxiv.org/pdf/2201.03545.pdf
Args:
in_chans (int): Number of input image channels. Default: 3
num_classes (int): Number of classes for classification head. Default: 1000
depths (tuple(int)): Number of blocks at each stage. Default: [3, 3, 9, 3]
dims (int): Feature dimension at each stage. Default: [96, 192, 384, 768]
drop_path_rate (float): Stochastic depth rate. Default: 0.
layer_scale_init_value (float): Init value for Layer Scale. Default: 1e-6.
head_init_scale (float): Init scaling value for classifier weights and biases. Default: 1.
"""
def __init__(self, in_chans=3, num_classes=1000,
depths=[3, 3, 9, 3], dims=[96, 192, 384, 768], drop_path_rate=0.,
layer_scale_init_value=1e-6, head_init_scale=1.,
):
super().__init__()
self.downsample_layers = nn.ModuleList() # stem and 3 intermediate downsampling conv layers
stem = nn.Sequential(
nn.Conv2d(in_chans, dims[0], kernel_size=4, stride=4),
LayerNorm(dims[0], eps=1e-6, data_format="channels_first")
)
self.downsample_layers.append(stem)
for i in range(3):
downsample_layer = nn.Sequential(
LayerNorm(dims[i], eps=1e-6, data_format="channels_first"),
nn.Conv2d(dims[i], dims[i+1], kernel_size=2, stride=2),
)
self.downsample_layers.append(downsample_layer)
self.stages = nn.ModuleList() # 4 feature resolution stages, each consisting of multiple residual blocks
dp_rates=[x.item() for x in torch.linspace(0, drop_path_rate, sum(depths))]
cur = 0
for i in range(4):
stage = nn.Sequential(
*[Block(dim=dims[i], drop_path=dp_rates[cur + j],
layer_scale_init_value=layer_scale_init_value) for j in range(depths[i])]
)
self.stages.append(stage)
cur += depths[i]
self.norm = nn.LayerNorm(dims[-1], eps=1e-6) # final norm layer
self.head = nn.Linear(dims[-1], num_classes)
self.apply(self._init_weights)
self.head.weight.data.mul_(head_init_scale)
self.head.bias.data.mul_(head_init_scale)
def _init_weights(self, m):
if isinstance(m, (nn.Conv2d, nn.Linear)):
trunc_normal_(m.weight, std=.02)
nn.init.constant_(m.bias, 0)
def forward_features(self, x):
for i in range(4):
x = self.downsample_layers[i](x)
x = self.stages[i](x)
return self.norm(x.mean([-2, -1])) # global average pooling, (N, C, H, W) -> (N, C)
def forward(self, x):
x = self.forward_features(x)
x = self.head(x)
return x