ConvNeXt V2:与屏蔽自动编码器共同设计和缩放ConvNets,论文+代码+实战

简介: ConvNeXt V2:与屏蔽自动编码器共同设计和缩放ConvNets,论文+代码+实战

ConvNeXt V2:与屏蔽自动编码器共同设计和缩放ConvNets,论文+代码+实战


自从Transformer模型在计算机视觉领域封神后,Facebook发表了ConvNeXt V1版本,证明了使用传统的卷积神经网络模型也能表现出优异的成绩,而ConvNeXt V2是对Transformer模型发起的又一新的挑战!


论文地址:


该论文的一句话总结:


本文利用MAE设计了全卷积掩码自编码器:FCMAE和新的全局响应归一化(GRN)层,并提出一个卷积主干新系列:ConvNeXt V2,它显著提高了纯ConvNet在各种视觉基准上的性能,最小的Atto版本仅有3.7M参数,而最大的Huge版本可高达88.9%准确率!


b5d84717256b4573a3d4a1bf15092f5a.png


摘要:在2020年代初,随着改进的架构和更好的表示学习框架的出现,视觉识别领域经历了快速的现代化和性能提升。例如,现代的ConvNets,如ConvNeXt 在各种场景中展现出强大的性能。虽然这些模型最初是为带有ImageNet标签的监督学习而设计的,但它们也可能受益于自监督学习技术,如掩码自编码器(MAE)。


然而,我们发现简单地将这两种方法组合起来会导致表现不佳。在本文中,我们提出了一个全卷积掩码自编码器框架和一个新的全局响应归一化(GRN)层,可以添加到ConvNeXt架构中,以增强通道之间的特征竞争。


这种自监督学习技术和架构改进的共同设计,产生了一个名为ConvNeXt V2的新模型系列,它显著提高了纯ConvNets在各种识别基准测试中的性能,包括ImageNet分类、COCO检测和ADE20K分割。我们还提供了各种大小的预训练ConvNeXt V2模型,从高效的3.7M参数的Atto模型,其在ImageNet上拥有76.7%的top-1准确率,到650M的Huge模型,仅使用公共训练数据就实现了最先进的**88.9%**准确率。


1.全卷积掩码自编码器框架(FCMAE)



**全卷积掩码自编码器框架(FCMAE)**由一个基于稀疏卷积的ConvNeXt编码器和一个轻量级的ConvNeXt块解码器组成。总体而言,自动编码器的架构是不对称的。编码器只处理可见像素,解码器使用编码的像素和掩码标记重建图像。仅在遮罩区域上计算损失。


学习信号是通过以高掩蔽比随机掩蔽原始输入视觉效果并让模型在给定剩余上下文的情况下预测缺失部分来生成的。


掩蔽:使用了一种掩蔽比为0.6的随机掩蔽策略。由于卷积模型具有分层设计,其中在不同阶段对特征进行下采样,因此在最后阶段生成掩码,并递归地上采样,直到达到最佳分辨率。为了在实践中实现这一点,我们从原始输入图像中随机去除了60%的32×32个补丁。我们使用最小的数据扩充,只包括随机调整大小的裁剪。


编码器设计使用ConvNeXt模型作为方法中的编码器,但存在当掩蔽比高时,训练和测试时间不一致的问题,因此在预训练过程中,用子流形稀疏卷积转换编码器中的标准卷积层。



2.全局响应归一化(GRN)层


方法大脑中有许多促进神经元多样性的机制。例如,横向抑制可以帮助增强被激活神经元的反应,增加单个神经元对刺激的对比度和选择性,同时也可以增加神经元群体的反应多样性。在深度学习中,这种形式的横向抑制可以通过响应归一化来实现。在这项工作中,引入了一种新的响应归一化层,称为全局响应归一化(GRN),旨在提高通道的对比度和选择性。提出的GRN单元由三个步骤组成:1)全局特征聚合,2)特征归一化,和3)特征校准。


作者可视化了FCMAE预训练的ConvNeXt-Base模型的激活,并注意到一个有趣的“特征崩溃”现象:有许多停滞或饱和的特征图,并且激活在通道之间变得多余。这种行为主要在ConvNeXt区块中的尺寸扩展MLP层中观察到。

0493c475874a4b20b40f71c385617a48.png


将每个特征通道的激活图可视化为小方块。为了清晰起见,在每个可视化中显示64个通道。可以看到,ConvNeXt V1模型存在特征崩溃问题,其特征是跨通道存在冗余激活(死亡或饱和神经元)。为了解决这个问题,作者引入了一种新的方法来提高训练过程中的特征多样性:全局响应归一化(GRN)层。该技术应用于每个块中的高维特征,从而开发了ConvNeXt V2架构。



3.ConvNeXt V2 Block

2055a4f9311848cdade9eb6fc8085ffe.png

作者根据实验发现,当使用 GRN 时,LayerScale 不是必要的并且可以被删除。利用这种新的块设计,该研究创建了具有不同效率和容量的多种模型,并将其称为 ConvNeXt V2 模型族,模型范围从轻量级(Atto)到计算密集型(Huge)。



4.结果测试




这张图展示了ConvNeXt重新设计的重要性。可以看到配备了FCMAE预训练的ConvNeXt V2巨型模型优于其他架构,在仅使用公共数据的方法中创下了88.9%的最新准确率。



这张图展示了在ImageNet-1K使用IN-21K标签的微调结果。可以看到配备了FCMAE预训练的ConvNeXt V2巨型模型优于其他架构,在仅使用公共数据的方法中创下了88.9%的最新准确率。


这张图展示了对迁移学习的表现进行基准测试,使用Mask RCNN的COCO对象检测和实例分割结果。FLOPS是用图像大小(1280800)来计算的。可以看到在FCMAE上预训练的ConvNeXt V2,在所有模型尺寸上都优于Swin transformer,在巨大的模型体系中实现了最大的差距。

675fdf8e005d46a780cdc5e1a25bae53.png


这张图展示了在ADE20K上的语义分割测试结果,结果显示出与目标检测实验类似的趋势,并且ConvNeXt V2最终模型比V1监督的模型显著改进。



5.代码

网络代码(pytorch实现):

# coding=gbk
import torch
import torch.nn as nn
import torch.nn.functional as F
from timm.models.layers import trunc_normal_, DropPath
class Block(nn.Module):
    """ ConvNeXtV2 Block.
    Args:
        dim (int): Number of input channels.
        drop_path (float): Stochastic depth rate. Default: 0.0
    """
    def __init__(self, dim, drop_path=0.):
        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.grn = GRN(4 * dim)
        self.pwconv2 = nn.Linear(4 * dim, dim)
        self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity()
    def forward(self, x):
        input = x
        x = self.dwconv(x)
        x = x.permute(0, 2, 3, 1)  # (N, C, H, W) -> (N, H, W, C)
        x = self.norm(x)
        x = self.pwconv1(x)
        x = self.act(x)
        x = self.grn(x)
        x = self.pwconv2(x)
        x = x.permute(0, 3, 1, 2)  # (N, H, W, C) -> (N, C, H, W)
        x = input + self.drop_path(x)
        return x
class LayerNorm(nn.Module):
    """ LayerNorm that supports two data formats: channels_last (default) or channels_first.
    The ordering of the dimensions in the inputs. channels_last corresponds to inputs with
    shape (batch_size, height, width, channels) while channels_first corresponds to inputs
    with shape (batch_size, channels, height, width).
    """
    def __init__(self, normalized_shape, eps=1e-6, data_format="channels_last"):
        super().__init__()
        self.weight = nn.Parameter(torch.ones(normalized_shape))
        self.bias = nn.Parameter(torch.zeros(normalized_shape))
        self.eps = eps
        self.data_format = data_format
        if self.data_format not in ["channels_last", "channels_first"]:
            raise NotImplementedError
        self.normalized_shape = (normalized_shape,)
    def forward(self, x):
        if self.data_format == "channels_last":
            return F.layer_norm(x, self.normalized_shape, self.weight, self.bias, self.eps)
        elif self.data_format == "channels_first":
            u = x.mean(1, keepdim=True)
            s = (x - u).pow(2).mean(1, keepdim=True)
            x = (x - u) / torch.sqrt(s + self.eps)
            x = self.weight[:, None, None] * x + self.bias[:, None, None]
            return x
class GRN(nn.Module):
    """ GRN (Global Response Normalization) layer
    """
    def __init__(self, dim):
        super().__init__()
        self.gamma = nn.Parameter(torch.zeros(1, 1, 1, dim))
        self.beta = nn.Parameter(torch.zeros(1, 1, 1, dim))
    def forward(self, x):
        Gx = torch.norm(x, p=2, dim=(1, 2), keepdim=True)
        Nx = Gx / (Gx.mean(dim=-1, keepdim=True) + 1e-6)
        return self.gamma * (x * Nx) + self.beta + x
class ConvNeXtV2(nn.Module):
    """ ConvNeXt V2
    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.
        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., head_init_scale=1.
                 ):
        super().__init__()
        self.depths = depths
        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]) 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)
        print(x.size())
        x = self.head(x)
        return x
def convnextv2_atto(num_classes=100, **kwargs):
    model = ConvNeXtV2(depths=[2, 2, 6, 2], dims=[40, 80, 160, 320], num_classes=num_classes, **kwargs)
    return model
def convnextv2_femto(num_classes=100, **kwargs):
    model = ConvNeXtV2(depths=[2, 2, 6, 2], dims=[48, 96, 192, 384], num_classes=num_classes, **kwargs)
    return model
def convnext_pico(num_classes=100, **kwargs):
    model = ConvNeXtV2(depths=[2, 2, 6, 2], dims=[64, 128, 256, 512], num_classes=num_classes, **kwargs)
    return model
def convnextv2_nano(num_classes=100, **kwargs):
    model = ConvNeXtV2(depths=[2, 2, 8, 2], dims=[80, 160, 320, 640], num_classes=num_classes, **kwargs)
    return model
def convnextv2_tiny(num_classes=100, **kwargs):
    model = ConvNeXtV2(depths=[3, 3, 9, 3], dims=[96, 192, 384, 768], num_classes=num_classes, **kwargs)
    return model
def convnextv2_base(num_classes=100, **kwargs):
    model = ConvNeXtV2(depths=[3, 3, 27, 3], dims=[128, 256, 512, 1024], num_classes=num_classes, **kwargs)
    return model
def convnextv2_large(num_classes=100, **kwargs):
    model = ConvNeXtV2(depths=[3, 3, 27, 3], dims=[192, 384, 768, 1536], num_classes=num_classes, **kwargs)
    return model
def convnextv2_huge(num_classes=100, **kwargs):
    model = ConvNeXtV2(depths=[3, 3, 27, 3], dims=[352, 704, 1408, 2816], num_classes=num_classes, **kwargs)
    return model
if __name__ == "__main__":
    m = convnextv2_atto(num_classes=3)
    params = sum(p.numel() for p in m.parameters())
    print(params)
    input = torch.randn(1, 3, 256, 256)
    out = m(input)
    print(out.shape)


训练代码:

import sys
import torch
from torch import nn
from net import convnextv2_atto
import numpy as np
from torch.optim import lr_scheduler
import os
from tqdm import tqdm
from torchvision import transforms
from torchvision.datasets import ImageFolder
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
def main():
    # 解决中文显示问题
    # plt.rcParams['font.sans-serif'] = ['SimHei']
    # plt.rcParams['axes.unicode_minus'] = False
    ROOT_TRAIN = r'D:/other/ClassicalModel/data/'
    ROOT_TEST = r'D:/other/ClassicalModel/data/'
    # 创建定义文件夹以及文件
    filename = 'record.txt'
    save_path = 'runs'
    path_num = 1
    while os.path.exists(save_path + f'{path_num}'):
        path_num += 1
    os.mkdir(save_path + f'{path_num}')
    os.mkdir(save_path + f'{path_num}/save_model')
    f = open(save_path + f'{path_num}/' + filename, 'w')
    save_path = save_path + f'{path_num}/save_model'
    # 将图像的像素值归一化到【-1, 1】之间
    normalize = transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
    train_transform = transforms.Compose([
        transforms.Resize((224, 224)),#重调尺寸
        transforms.RandomVerticalFlip(),#随即裁剪
        transforms.ToTensor(),
        normalize])
    val_transform = transforms.Compose([
        transforms.Resize((224, 224)),
        transforms.ToTensor(),
        normalize])
    train_dataset = ImageFolder(ROOT_TRAIN, transform=train_transform)
    val_dataset = ImageFolder(ROOT_TEST, transform=val_transform)
    val_num = len(val_dataset)
    train_dataloader = DataLoader(train_dataset, batch_size=4, shuffle=True)
    val_dataloader = DataLoader(val_dataset, batch_size=4, shuffle=True)
    # 使用GPU加速
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    model = convnextv2_atto(num_classes=3).to(device)
    # 定义一个损失函数,交叉熵损失函数
    loss_function = nn.CrossEntropyLoss()
    # 定义一个优化器
    optimizer = torch.optim.SGD(model.parameters(), lr=0.005, momentum=0.9)
    epochs = 30
    best_acc = 0.0
    train_steps = len(train_dataloader)
    for epoch in range(epochs):
        # train
        model.train()
        running_loss = 0.0
        train_bar = tqdm(train_dataloader, file=sys.stdout)
        for step, data in enumerate(train_bar):
            images, labels = data
            optimizer.zero_grad()
            logits= model(images.to(device))
            loss = loss_function(logits, labels.to(device))
            loss.backward()
            optimizer.step()
            # print statistics
            running_loss += loss.item()
            train_bar.desc = "train epoch[{}/{}] loss:{:.3f}".format(epoch + 1,
                                                                     epochs,
                                                                     loss)
        # validate
        model.eval()
        acc = 0.0  # accumulate accurate number / epoch
        with torch.no_grad():
            val_bar = tqdm(val_dataloader, file=sys.stdout)
            for val_data in val_bar:
                val_images, val_labels = val_data
                outputs = model(val_images.to(device))  # eval model only have last output layer
                predict_y = torch.max(outputs, dim=1)[1]
                acc += torch.eq(predict_y, val_labels.to(device)).sum().item()
        val_accurate = acc / val_num
        print('[epoch %d] train_loss: %.3f  val_accuracy: %.3f' %
              (epoch + 1, running_loss / train_steps, val_accurate))
        f.write('[epoch %d] train_loss: %.3f  val_accuracy: %.3f\n' %
              (epoch + 1, running_loss / train_steps, val_accurate))
        if val_accurate > best_acc:
            best_acc = val_accurate
            torch.save(model.state_dict(), save_path+'/best_model.pth')
            # 保存最后一轮的权重文件
        if epoch == epochs - 1:
            torch.save(model.state_dict(), save_path + '/last_model.pth')
    f.close()
    print('Done!')
if __name__ == '__main__':
    main()


训练结果:

我们以3分类的马铃薯为例,convnextv2_atto仅有3.38M的参数,而准确率达到96.11%,相比上代95.23%确实有显著的提升。



结论:ConvNeXt V2显著提高了纯ConvNet在各种视觉基准上的性能,在一定程度上超越了transformer!

相关文章
|
7月前
|
机器学习/深度学习 计算机视觉
【YOLOv8改进】CoordAttention: 用于移动端的高效坐标注意力机制 (论文笔记+引入代码)
该专栏聚焦YOLO目标检测的创新改进与实战,介绍了一种新的移动网络注意力机制——坐标注意力。它将位置信息融入通道注意力,通过1D特征编码处理,捕获长距离依赖并保持位置精度。生成的注意力图能增强目标表示,适用于MobileNetV2、MobileNeXt和EfficientNet等网络,提高性能,且几乎不增加计算成本。在ImageNet分类和下游任务(目标检测、语义分割)中表现出色。YOLOv8中引入了CoordAtt模块,实现位置敏感的注意力。更多详情及配置见相关链接。
|
7月前
|
机器学习/深度学习 网络架构 计算机视觉
YOLOv5改进 | 检测头篇 | 利用DBB重参数化模块魔改检测头实现暴力涨点 (附代码 + 详细修改教程)
YOLOv5改进 | 检测头篇 | 利用DBB重参数化模块魔改检测头实现暴力涨点 (附代码 + 详细修改教程)
357 3
|
7月前
|
机器学习/深度学习 测试技术 Ruby
YOLOv8改进 | 主干篇 | 反向残差块网络EMO一种轻量级的CNN架构(附完整代码 + 修改教程)
YOLOv8改进 | 主干篇 | 反向残差块网络EMO一种轻量级的CNN架构(附完整代码 + 修改教程)
221 0
|
5月前
|
机器学习/深度学习 存储 测试技术
【YOLOv10改进-注意力机制】iRMB: 倒置残差移动块 (论文笔记+引入代码)
YOLOv10专栏介绍了融合CNN与Transformer的iRMB模块,用于轻量级模型设计。iRMB在保持高效的同时结合了局部和全局信息处理,减少了资源消耗,提升了移动端性能。在ImageNet等基准上超越SOTA,且在目标检测等任务中表现优秀。代码示例展示了iRMB的实现细节,包括自注意力机制和卷积操作的整合。更多配置信息见相关链接。
|
7月前
|
机器学习/深度学习 测试技术 Ruby
YOLOv5改进 | 主干篇 | 反向残差块网络EMO一种轻量级的CNN架构(附完整代码 + 修改教程)
YOLOv5改进 | 主干篇 | 反向残差块网络EMO一种轻量级的CNN架构(附完整代码 + 修改教程)
260 2
|
5月前
|
机器学习/深度学习 人工智能 计算机视觉
【YOLOv8改进 - 注意力机制】HCF-Net 之 MDCR:多稀释通道细化器模块 ,以不同的稀释率捕捉各种感受野大小的空间特征 | 小目标
HCF-Net是针对红外小目标检测的深度学习模型,采用U-Net改进架构,包含PPA、DASI和MDCR模块。PPA利用多分支特征提取增强小目标表示,DASI实现自适应通道融合,MDCR通过多扩张率深度可分离卷积细化空间特征。实验显示,HCF-Net在SIRST数据集上表现出色,超越其他方法。代码和论文可在给出的链接获取。
|
6月前
|
机器学习/深度学习 算法 计算机视觉
YOLOv8改进 | 注意力机制 | 用于增强小目标感受野的RFEM
💡💡💡本专栏所有程序均经过测试,可成功执行💡💡
|
6月前
|
机器学习/深度学习 计算机视觉
YOLOv8改进 | 注意力机制 | 添加适用于遥感图像的LSKblock注意力——【二次创新+完整代码】
遥感目标检测的研究主要集中在改进方向边界框的表示上,而忽略了遥感场景中独特的先验知识。 这类先验知识对于准确检测微小目标至关重要,因为这些目标往往需要更大的上下文信息才能被正确识别。提出的 LSKNet 可以动态调整其大的空间感受野,以更好地模拟不同目标的距离上下文,从而提高遥感目标检测的精度。 LSKNet 是第一个在遥感目标检测中探索大选择性核机制的方法。
|
6月前
|
机器学习/深度学习 自然语言处理 并行计算
YOLOv8改进 | 注意力机制 | 在主干网络中添加MHSA模块【原理+附完整代码】
Transformer中的多头自注意力机制(Multi-Head Self-Attention, MHSA)被用来增强模型捕捉序列数据中复杂关系的能力。该机制通过并行计算多个注意力头,使模型能关注不同位置和子空间的特征,提高了表示多样性。在YOLOv8的改进中,可以将MHSA代码添加到`/ultralytics/ultralytics/nn/modules/conv.py`,以增强网络的表示能力。完整实现和教程可在提供的链接中找到。
|
7月前
|
机器学习/深度学习 编解码 计算机视觉
【YOLOv8改进】 SPD-Conv空间深度转换卷积,处理低分辨率图像和小对象问题 (论文笔记+引入代码)
YOLO目标检测专栏探讨了CNN在低分辨率和小目标检测中的局限性,提出SPD-Conv新架构,替代步长卷积和池化层,通过空间到深度层和非步长卷积保持细粒度信息。创新点包括消除信息损失、通用设计和性能提升。YOLOv5和ResNet应用SPD-Conv后,在困难任务上表现优越。详情见YOLO有效改进系列及项目实战目录。