写点什么

深度学习应用篇 - 计算机视觉 - 图像分类 [3]:ResNeXt、Res2Net、Swin Transformer、Vision Transformer 等模型结构、实现、模型特点详细介绍

  • 2023-06-08
    浙江
  • 本文字数:22408 字

    阅读完需:约 74 分钟

深度学习应用篇-计算机视觉-图像分类[3]:ResNeXt、Res2Net、Swin Transformer、Vision Transformer等模型结构、实现、模型特点详细介绍

深度学习应用篇-计算机视觉-图像分类[3]:ResNeXt、Res2Net、Swin Transformer、Vision Transformer 等模型结构、实现、模型特点详细介绍

1.ResNet

相较于 VGG 的 19 层和 GoogLeNet 的 22 层,ResNet 可以提供 18、34、50、101、152 甚至更多层的网络,同时获得更好的精度。但是为什么要使用更深层次的网络呢?同时,如果只是网络层数的堆叠,那么为什么前人没有获得 ResNet 一样的成功呢?

1.1. 更深层次的网络?

从理论上来讲,加深深度学习网络可以提升性能。深度网络以端到端的多层方式集成了低/中/高层特征和分类器,且特征的层次可通过加深网络层次的方式来丰富。举一个例子,当深度学习网络只有一层时,要学习的特征会非常复杂,但如果有多层,就可以分层进行学习,如 图 1 所示,网络的第一层学习到了边缘和颜色,第二层学习到了纹理,第三层学习到了局部的形状,而第五层已逐渐学习到全局特征。网络的加深,理论上可以提供更好的表达能力,使每一层可以学习到更细化的特征。


1.2. 为什么深度网络不仅仅是层数的堆叠?

1.2.1 梯度消失 or 爆炸

但网络加深真的只有堆叠层数这么简单么?当然不是!首先,最显著的问题就是梯度消失/梯度爆炸。我们都知道神经网络的参数更新依靠梯度反向传播(Back Propagation),那么为什么会出现梯度的消失和爆炸呢?举一个例子解释。如 图 2 所示,假设每层只有一个神经元,且激活函数使用 Sigmoid 函数,则有:



其中, 为 sigmoid 函数。



根据链式求导和反向传播,我们可以得到:


Sigmoid 函数的导数 图 3 所示:



我们可以看到 sigmoid 的导数最大值为 0.25,那么随着网络层数的增加,小于 1 的小数不断相乘导致 逐渐趋近于零,从而产生梯度消失。


那么梯度爆炸又是怎么引起的呢?同样的道理,当权重初始化为一个较大值时,虽然和激活函数的导数相乘会减小这个值,但是随着神经网络的加深,梯度呈指数级增长,就会引发梯度爆炸。但是从 AlexNet 开始,神经网络中就使用 ReLU 函数替换了 Sigmoid,同时 BN(Batch Normalization)层的加入,也基本解决了梯度消失/爆炸问题。

1.2.2 网络退化

现在,梯度消失/爆炸的问题解决了是不是就可以通过堆叠层数来加深网络了呢?Still no!


我们来看看 ResNet 论文中提到的例子(见 图 4),很明显,56 层的深层网络,在训练集和测试集上的表现都远不如 20 层的浅层网络,这种随着网络层数加深,accuracy 逐渐饱和,然后出现急剧下降,具体表现为深层网络的训练效果反而不如浅层网络好的现象,被称为网络退化(degradation)。



为什么会引起网络退化呢?按照理论上的想法,当浅层网络效果不错的时候,网络层数的增加即使不会引起精度上的提升也不该使模型效果变差。但事实上非线性的激活函数的存在,会造成很多不可逆的信息损失,网络加深到一定程度,过多的信息损失就会造成网络的退化。


而 ResNet 就是提出一种方法让网络拥有恒等映射能力,即随着网络层数的增加,深层网络至少不会差于浅层网络。

1..3. 残差块

现在我们明白了,为了加深网络结构,使每一次能够学到更细化的特征从而提高网络精度,需要实现的一点是恒等映射。那么残差网络如何能够做到这一点呢?


恒等映射即为 ,已有的神经网络结构很难做到这一点,但是如果我们将网络设计成 ,即 ,那么只需要使残差函数 ,就构成了恒等映射



残差结构的目的是,随着网络的加深,使 逼近于 0,使得深度网络的精度在最优浅层网络的基础上不会下降。看到这里你或许会有疑问,既然如此为什么不直接选取最优的浅层网络呢?这是因为最优的浅层网络结构并不易找寻,而 ResNet 可以通过增加深度,找到最优的浅层网络并保证深层网络不会因为层数的叠加而发生网络退化。


  • 参考文献


[1] Visualizing and Understanding Convolutional Networks


[2] Deep Residual Learning for Image Recognition

2. ResNeXt(2017)

ResNeXt 是由何凯明团队在 2017 年 CVPR 会议上提出来的新型图像分类网络。ResNeXt 是 ResNet 的升级版,在 ResNet 的基础上,引入了 cardinality 的概念,类似于 ResNet,ResNeXt 也有 ResNeXt-50,ResNeXt-101 的版本。那么相较于 ResNet,ResNeXt 的创新点在哪里?既然是分类网络,那么在 ImageNet 数据集上的指标相较于 ResNet 有何变化?之后的 ResNeXt_WSL 又是什么东西?下面我和大家一起分享一下这些知识。

2.1 ResNeXt 模型结构

在 ResNeXt 的论文中,作者提出了当时普遍存在的一个问题,如果要提高模型的准确率,往往采取加深网络或者加宽网络的方法。虽然这种方法是有效的,但是随之而来的,是网络设计的难度和计算开销的增加。为了一点精度的提升往往需要付出更大的代价。因此,需要一个更好的策略,在不额外增加计算代价的情况下,提升网络的精度。由此,何等人提出了 cardinality 的概念。


下图是 ResNet(左)与 ResNeXt(右)block 的差异。在 ResNet 中,输入的具有 256 个通道的特征经过 1×1 卷积压缩 4 倍到 64 个通道,之后 3×3 的卷积核用于处理特征,经 1×1 卷积扩大通道数与原特征残差连接后输出。ResNeXt 也是相同的处理策略,但在 ResNeXt 中,输入的具有 256 个通道的特征被分为 32 个组,每组被压缩 64 倍到 4 个通道后进行处理。32 个组相加后与原特征残差连接后输出。这里 cardinatity 指的是一个 block 中所具有的相同分支的数目。



下图是 InceptionNet 的两种 inception module 结构,左边是 inception module 的 naive 版本,右边是使用了降维方法的 inception module。相较于右边,左边很明显的缺点就是参数大,计算量巨大。使用不同大小的卷积核目的是为了提取不同尺度的特征信息,对于图像而言,多尺度的信息有助于网络更好地对图像信息进行选择,并且使得网络对于不同尺寸的图像输入有更好的适应能力,但多尺度带来的问题就是计算量的增加。因此在右边的模型中,InceptionNet 很好地解决了这个问题,首先是 1×1 的卷积用于特征降维,减小特征的通道数后再采取多尺度的结构提取特征信息,在降低参数量的同时捕获到多尺度的特征信息。


ResNeXt 正是借鉴了这种“分割-变换-聚合”的策略,但用相同的拓扑结构组建 ResNeXt 模块。每个结构都是相同的卷积核,保持了结构的简洁,使得模型在编程上更方便更容易,而 InceptionNet 则需要更为复杂的设计。


2.2 ResNeXt 模型实现

ResNeXt 与 ResNet 的模型结构一致,主要差别在于 block 的搭建,因此这里用 paddle 框架来实现 block 的代码


class ConvBNLayer(nn.Layer):    def __init__(self, num_channels, num_filters, filter_size, stride=1,                 groups=1, act=None, name=None, data_format="NCHW"                ):        super(ConvBNLayer, self).__init__()        self._conv = Conv2D(            in_channels=num_channels, out_channels=num_filters,            kernel_size=filter_size, stride=stride,            padding=(filter_size - 1) // 2, groups=groups,            weight_attr=ParamAttr(name=name + "_weights"), bias_attr=False,            data_format=data_format        )        if name == "conv1":            bn_name = "bn_" + name        else:            bn_name = "bn" + name[3:]        self._batch_norm = BatchNorm(            num_filters, act=act, param_attr=ParamAttr(name=bn_name + '_scale'),            bias_attr=ParamAttr(bn_name + '_offset'), moving_mean_name=bn_name + '_mean',            moving_variance_name=bn_name + '_variance', data_layout=data_format        )
def forward(self, inputs): y = self._conv(inputs) y = self._batch_norm(y) return y

class BottleneckBlock(nn.Layer): def __init__(self, num_channels, num_filters, stride, cardinality, shortcut=True, name=None, data_format="NCHW" ): super(BottleneckBlock, self).__init__() self.conv0 = ConvBNLayer(num_channels=num_channels, num_filters=num_filters, filter_size=1, act='relu', name=name + "_branch2a", data_format=data_format ) self.conv1 = ConvBNLayer( num_channels=num_filters, num_filters=num_filters, filter_size=3, groups=cardinality, stride=stride, act='relu', name=name + "_branch2b", data_format=data_format )
self.conv2 = ConvBNLayer( num_channels=num_filters, num_filters=num_filters * 2 if cardinality == 32 else num_filters, filter_size=1, act=None, name=name + "_branch2c", data_format=data_format )
if not shortcut: self.short = ConvBNLayer( num_channels=num_channels, num_filters=num_filters * 2 if cardinality == 32 else num_filters, filter_size=1, stride=stride, name=name + "_branch1", data_format=data_format )
self.shortcut = shortcut
def forward(self, inputs): y = self.conv0(inputs) conv1 = self.conv1(y) conv2 = self.conv2(conv1)
if self.shortcut: short = inputs else: short = self.short(inputs)
y = paddle.add(x=short, y=conv2) y = F.relu(y) return y
复制代码

2.3 ResNeXt 模型特点

  1. ResNeXt 通过控制 cardinality 的数量,使得 ResNeXt 的参数量和 GFLOPs 与 ResNet 几乎相同。

  2. 通过 cardinality 的分支结构,为网络提供更多的非线性,从而获得更精确的分类效果。

2.4 ResNeXt 模型指标


上图是 ResNet 与 ResNeXt 的参数对比,可以看出,ResNeXt 与 ResNet 几乎是一模一样的参数量和计算量,然而两者在 ImageNet 上的表现却不一样。



从图中可以看出,ResNeXt 除了可以增加 block 中 3×3 卷积核的通道数,还可以增加 cardinality 的分支数来提升模型的精度。ResNeXt-50 和 ResNeXt-101 都大大降低了对应 ResNet 的错误率。图中,ResNeXt-101 从 32×4d 变为 64×4d,虽然增加了两倍的计算量,但也能有效地降低分类错误率。


在 2019 年何凯明团队开源了 ResNeXt_WSL,ResNeXt_WSL 是何凯明团队使用弱监督学习训练的 ResNeXt,ResNeXt_WSL 中的 WSL 就表示 Weakly Supervised Learning(弱监督学习)。


ResNeXt101_32×48d_WSL 有 8 亿+的参数,是通过弱监督学习预训练的方法在 Instagram 数据集上训练,然后用 ImageNet 数据集做微调,Instagram 有 9.4 亿张图片,没有经过特别的标注,只带着用户自己加的话题标签。ResNeXt_WSL 与 ResNeXt 是一样的结构,只是训练方式有所改变。下图是 ResNeXt_WSL 的训练效果。




ResNeXt


GoogLeNet

3.Res2Net(2020)

2020 年,南开大学程明明组提出了一种面向目标检测任务的新模块 Res2Net。并且其论文已被 TPAMI2020 录用。Res2Net 和 ResNeXt 一样,是 ResNet 的变体形式,只不过 Res2Net 不止提高了分类任务的准确率,还提高了检测任务的精度。Res2Net 的新模块可以和现有其他优秀模块轻松整合,在不增加计算负载量的情况下,在 ImageNet、CIFAR-100 等数据集上的测试性能超过了 ResNet。因为模型的残差块里又有残差连接,所以取名为 Res2Net。

3.1 Res2Net 模型结构


模型结构看起来很简单,将输入的特征 x,split 为 k 个特征,第 i+1(i = 0, 1, 2,...,k-1) 个特征经过 3×3 卷积后以残差连接的方式融合到第 i+2 个特征中。这就是 Res2Net 的主要结构。那么这样做的目的是为什么呢?能够有什么好处呢?答案就是多尺度卷积。多尺度特征在检测任务中一直是很重要的,自从空洞卷积提出以来,基于空洞卷积搭建的多尺度金字塔模型在检测任务上取得里程碑式的效果。不同感受野下获取的物体的信息是不同的,小的感受野可能会看到更多的物体细节,对于检测小目标也有很大的好处,而大的感受野可以感受物体的整体结构,方便网络定位物体的位置,细节与位置的结合可以更好地得到具有清晰边界的物体信息,因此,结合了多尺度金字塔的模型往往能获得很好地效果。在 Res2Net 中,特征 k2 经过 3×3 卷积后被送入 x3 所在的处理流中,k2 再次被 3×3 的卷积优化信息,两个 3×3 的卷积相当于一个 5×5 的卷积。那么,k3 就想当然与融合了 3×3 的感受野和 5×5 的感受野处理后的特征。以此类推,7×7 的感受野被应用在 k4 中。就这样,Res2Net 提取多尺度特征用于检测任务,以提高模型的准确率。在这篇论文中,s 是比例尺寸的控制参数,也就是可以将输入通道数平均等分为多个特征通道。s 越大表明多尺度能力越强,此外一些额外的计算开销也可以忽略。

3.2 Res2Net 模型实现

Res2Net 与 ResNet 的模型结构一致,主要差别在于 block 的搭建,因此这里用 paddle 框架来实现 block 的代码


class ConvBNLayer(nn.Layer):    def __init__(            self,            num_channels,            num_filters,            filter_size,            stride=1,            groups=1,            is_vd_mode=False,            act=None,            name=None, ):        super(ConvBNLayer, self).__init__()
self.is_vd_mode = is_vd_mode self._pool2d_avg = AvgPool2D( kernel_size=2, stride=2, padding=0, ceil_mode=True) self._conv = Conv2D( in_channels=num_channels, out_channels=num_filters, kernel_size=filter_size, stride=stride, padding=(filter_size - 1) // 2, groups=groups, weight_attr=ParamAttr(name=name + "_weights"), bias_attr=False) if name == "conv1": bn_name = "bn_" + name else: bn_name = "bn" + name[3:] self._batch_norm = BatchNorm( num_filters, act=act, param_attr=ParamAttr(name=bn_name + '_scale'), bias_attr=ParamAttr(bn_name + '_offset'), moving_mean_name=bn_name + '_mean', moving_variance_name=bn_name + '_variance')
def forward(self, inputs): if self.is_vd_mode: inputs = self._pool2d_avg(inputs) y = self._conv(inputs) y = self._batch_norm(y) return y

class BottleneckBlock(nn.Layer): def __init__(self, num_channels1, num_channels2, num_filters, stride, scales, shortcut=True, if_first=False, name=None): super(BottleneckBlock, self).__init__() self.stride = stride self.scales = scales self.conv0 = ConvBNLayer( num_channels=num_channels1, num_filters=num_filters, filter_size=1, act='relu', name=name + "_branch2a") self.conv1_list = [] for s in range(scales - 1): conv1 = self.add_sublayer( name + '_branch2b_' + str(s + 1), ConvBNLayer( num_channels=num_filters // scales, num_filters=num_filters // scales, filter_size=3, stride=stride, act='relu', name=name + '_branch2b_' + str(s + 1))) self.conv1_list.append(conv1) self.pool2d_avg = AvgPool2D(kernel_size=3, stride=stride, padding=1)
self.conv2 = ConvBNLayer( num_channels=num_filters, num_filters=num_channels2, filter_size=1, act=None, name=name + "_branch2c")
if not shortcut: self.short = ConvBNLayer( num_channels=num_channels1, num_filters=num_channels2, filter_size=1, stride=1, is_vd_mode=False if if_first else True, name=name + "_branch1")
self.shortcut = shortcut
def forward(self, inputs): y = self.conv0(inputs) xs = paddle.split(y, self.scales, 1) ys = [] for s, conv1 in enumerate(self.conv1_list): if s == 0 or self.stride == 2: ys.append(conv1(xs[s])) else: ys.append(conv1(xs[s] + ys[-1])) if self.stride == 1: ys.append(xs[-1]) else: ys.append(self.pool2d_avg(xs[-1])) conv1 = paddle.concat(ys, axis=1) conv2 = self.conv2(conv1)
if self.shortcut: short = inputs else: short = self.short(inputs) y = paddle.add(x=short, y=conv2) y = F.relu(y) return y
复制代码

3.3 模型特点

  1. 可与其他结构整合,如 SENEt, ResNeXt, DLA 等,从而增加准确率。

  2. 计算负载不增加,特征提取能力更强大。

3.4 模型指标

ImageNet 分类效果如下图



Res2Net-50 就是对标 ResNet50 的版本。


Res2Net-50-299 指的是将输入图片裁剪到 299×299 进行预测的 Res2Net-50,因为一般都是裁剪或者 resize 到 224×224。


Res2NeXt-50 为融合了 ResNeXt 的 Res2Net-50。


Res2Net-DLA-60 指的是融合了 DLA-60 的 Res2Net-50。


Res2NeXt-DLA-60 为融合了 ResNeXt 和 DLA-60 的 Res2Net-50。


SE-Res2Net-50 为融合了 SENet 的 Res2Net-50。


blRes2Net-50 为融合了 Big-Little Net 的 Res2Net-50。


Res2Net-v1b-50 为采取和 ResNet-vd-50 一样的处理方法的 Res2Net-50。


Res2Net-200-SSLD 为 Paddle 使用简单的半监督标签知识蒸馏(SSLD,Simple Semi-supervised Label Distillation)的方法来提升模型效果得到的。


可见,Res2Net 都取得了十分不错的成绩。


COCO 数据集效果如下图



Res2Net-50 的各种配置都比 ResNet-50 高。


显著目标检测数据集指标效果如下图



ECSSD、PASCAL-S、DUT-OMRON、HKU-IS 都是显著目标检测任务中现在最为常用的测试集,显著目标检测任务的目的就是分割出图片中的显著物体,并用白色像素点表示,其他背景用黑色像素点表示。从图中可以看出来,使用 Res2Net 作为骨干网络,效果比 ResNet 有了很大的提升。


4.Swin Trasnformer(2021)

Swin Transformer 是由微软亚洲研究院在今年公布的一篇利用 transformer 架构处理计算机视觉任务的论文。Swin Transformer 在图像分类,图像分割,目标检测等各个领域已经屠榜,在论文中,作者分析表明,Transformer 从 NLP 迁移到 CV 上没有大放异彩主要有两点原因:1. 两个领域涉及的 scale 不同,NLP 的 token 是标准固定的大小,而 CV 的特征尺度变化范围非常大。2. CV 比起 NLP 需要更大的分辨率,而且 CV 中使用 Transformer 的计算复杂度是图像尺度的平方,这会导致计算量过于庞大。为了解决这两个问题,Swin Transformer 相比之前的 ViT 做了两个改进:1.引入 CNN 中常用的层次化构建方式构建层次化 Transformer 2.引入 locality 思想,对无重合的 window 区域内进行 self-attention 计算。另外,Swin Transformer 可以作为图像分类、目标检测和语义分割等任务的通用骨干网络,可以说,Swin Transformer 可能是 CNN 的完美替代方案。

4.1 Swin Trasnformer 模型结构

下图为 Swin Transformer 与 ViT 在处理图片方式上的对比,可以看出,Swin Transformer 有着 ResNet 一样的残差结构和 CNN 具有的多尺度图片结构。



整体概括:


下图为 Swin Transformer 的网络结构,输入的图像先经过一层卷积进行 patch 映射,将图像先分割成 4 × 4 的小块,图片是 224×224 输入,那么就是 56 个 path 块,如果是 384×384 的尺寸,则是 96 个 path 块。这里以 224 × 224 的输入为例,输入图像经过这一步操作,每个 patch 的特征维度为 4x4x3=48 的特征图。因此,输入的图像变成了 H/4×W/4×48 的特征图。然后,特征图开始输入到 stage1,stage1 中 linear embedding 将 path 特征维度变成 C,因此变成了 H/4×W/4×C。然后送入 Swin Transformer Block,在进入 stage2 前,接下来先通过 Patch Merging 操作,Patch Merging 和 CNN 中 stride=2 的 1×1 卷积十分相似,Patch Merging 在每个 Stage 开始前做降采样,用于缩小分辨率,调整通道数,当 H/4×W/4×C 的特征图输送到 Patch Merging,将输入按照 2x2 的相邻 patches 合并,这样子 patch 块的数量就变成了 H/8 x W/8,特征维度就变成了 4C,之后经过一个 MLP,将特征维度降为 2C。因此变为 H/8×W/8×2C。接下来的 stage 就是重复上面的过程。



每步细说:


Linear embedding


下面用 Paddle 代码逐步讲解 Swin Transformer 的架构。 以下代码为 Linear embedding 的操作,整个操作可以看作一个 patch 大小的卷积核和 patch 大小的步长的卷积对输入的 B,C,H,W 的图片进行卷积,得到的自然就是大小为 B,C,H/patch,W/patch 的特征图,如果放在第一个 Linear embedding 中,得到的特征图就为 B,96,56,56 的大小。Paddle 核心代码如下。


class PatchEmbed(nn.Layer):    """ Image to Patch Embedding    Args:        img_size (int): Image size.  Default: 224.        patch_size (int): Patch token size. Default: 4.        in_chans (int): Number of input image channels. Default: 3.        embed_dim (int): Number of linear projection output channels. Default: 96.        norm_layer (nn.Layer, optional): Normalization layer. Default: None    """
def __init__(self, img_size=224, patch_size=4, in_chans=3, embed_dim=96, norm_layer=None): super().__init__() img_size = to_2tuple(img_size) patch_size = to_2tuple(patch_size) patches_resolution = [ img_size[0] // patch_size[0], img_size[1] // patch_size[1] ] self.img_size = img_size self.patch_size = patch_size self.patches_resolution = patches_resolution self.num_patches = patches_resolution[0] * patches_resolution[1] #patch个数
self.in_chans = in_chans self.embed_dim = embed_dim
self.proj = nn.Conv2D( in_chans, embed_dim, kernel_size=patch_size, stride=patch_size) #将stride和kernel_size设置为patch_size大小 if norm_layer is not None: self.norm = norm_layer(embed_dim) else: self.norm = None
def forward(self, x): B, C, H, W = x.shape x = self.proj(x) # B, 96, H/4, W4
x = x.flatten(2).transpose([0, 2, 1]) # B Ph*Pw 96 if self.norm is not None: x = self.norm(x) return x
复制代码


Patch Merging


以下为 PatchMerging 的操作。该操作以 2 为步长,对输入的图片进行采样,总共得到 4 张下采样的特征图,H 和 W 降低 2 倍,因此,通道级拼接后得到的是 B,4C,H/2,W/2 的特征图。然而这样的拼接不能够提取有用的特征信息,于是,一个线性层将 4C 的通道筛选为 2C, 特征图变为了 B,2C, H/2, W/2。细细体会可以发现,该操作像极了卷积常用的 Pooling 操作和步长为 2 的卷积操作。Poling 用于下采样,步长为 2 的卷积同样可以下采样,另外还起到了特征筛选的效果。总结一下,经过这个操作原本 B,C,H,W 的特征图就变为了 B,2C,H/2,W/2 的特征图,完成了下采样操作。


class PatchMerging(nn.Layer):    r""" Patch Merging Layer.    Args:        input_resolution (tuple[int]): Resolution of input feature.        dim (int): Number of input channels.        norm_layer (nn.Layer, optional): Normalization layer.  Default: nn.LayerNorm    """
def __init__(self, input_resolution, dim, norm_layer=nn.LayerNorm): super().__init__() self.input_resolution = input_resolution self.dim = dim self.reduction = nn.Linear(4 * dim, 2 * dim, bias_attr=False) self.norm = norm_layer(4 * dim) def forward(self, x): """ x: B, H*W, C """ H, W = self.input_resolution B, L, C = x.shape assert L == H * W, "input feature has wrong size" assert H % 2 == 0 and W % 2 == 0, "x size ({}*{}) are not even.".format( H, W)
x = x.reshape([B, H, W, C]) # 每次降采样是两倍,因此在行方向和列方向上,间隔2选取元素。 x0 = x[:, 0::2, 0::2, :] # B H/2 W/2 C x1 = x[:, 1::2, 0::2, :] # B H/2 W/2 C x2 = x[:, 0::2, 1::2, :] # B H/2 W/2 C x3 = x[:, 1::2, 1::2, :] # B H/2 W/2 C # 拼接在一起作为一整个张量,展开。通道维度会变成原先的4倍(因为H,W各缩小2倍) x = paddle.concat([x0, x1, x2, x3], -1) # B H/2 W/2 4*C x = x.reshape([B, H * W // 4, 4 * C]) # B H/2*W/2 4*C
x = self.norm(x) # 通过一个全连接层再调整通道维度为原来的两倍 x = self.reduction(x)
return x
复制代码


Swin Transformer Block:


下面的操作是根据 window_size 划分特征图的操作和还原的操作,原理很简单就是并排划分即可。


def window_partition(x, window_size):    """    Args:        x: (B, H, W, C)        window_size (int): window size
Returns: windows: (num_windows*B, window_size, window_size, C) """ B, H, W, C = x.shape x = x.reshape([B, H // window_size, window_size, W // window_size, window_size, C]) windows = x.transpose([0, 1, 3, 2, 4, 5]).reshape([-1, window_size, window_size, C]) return windows

def window_reverse(windows, window_size, H, W): """ Args: windows: (num_windows*B, window_size, window_size, C) window_size (int): Window size H (int): Height of image W (int): Width of image
Returns: x: (B, H, W, C) """ B = int(windows.shape[0] / (H * W / window_size / window_size)) x = windows.reshape([B, H // window_size, W // window_size, window_size, window_size, -1]) x = x.transpose([0, 1, 3, 2, 4, 5]).reshape([B, H, W, -1]) return x
复制代码


Swin Transformer 中重要的当然是 Swin Transformer Block 了,下面解释一下 Swin Transformer Block 的原理。先看一下 MLP 和 LN,MLP 和 LN 为多层感知机和相对于 BatchNorm 的 LayerNorm。原理较为简单,因此直接看 paddle 代码即可。


class Mlp(nn.Layer):    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.fc2 = nn.Linear(hidden_features, out_features)        self.drop = nn.Dropout(drop)
def forward(self, x): x = self.fc1(x) x = self.act(x) x = self.drop(x) x = self.fc2(x) x = self.drop(x) return x
复制代码


下图就是 Shifted Window based MSA 是 Swin Transformer 的核心部分。Shifted Window based MSA 包括了两部分,一个是 W-MSA(窗口多头注意力),另一个就是 SW-MSA(移位窗口多头自注意力)。这两个是一同出现的。



一开始,Swin Transformer 将一张图片分割为 4 份,也叫 4 个 Window,然后独立地计算每一部分的 MSA。由于每一个 Window 都是独立的,缺少了信息之间的交流,因此作者又提出了 SW-MSA 的算法,即采用规则的移动窗口的方法。通过不同窗口的交互,来达到特征的信息交流。注意,这一部分是本论文的精华,想要了解的同学必须要看懂源代码


class WindowAttention(nn.Layer):    """ Window based multi-head self attention (W-MSA) module with relative position bias.    It supports both of shifted and non-shifted window.
Args: dim (int): Number of input channels. window_size (tuple[int]): The height and width of the window. num_heads (int): Number of attention heads. qkv_bias (bool, optional): If True, add a learnable bias to query, key, value. Default: True qk_scale (float | None, optional): Override default qk scale of head_dim ** -0.5 if set attn_drop (float, optional): Dropout ratio of attention weight. Default: 0.0 proj_drop (float, optional): Dropout ratio of output. Default: 0.0 """
def __init__(self, dim, window_size, num_heads, qkv_bias=True, qk_scale=None, attn_drop=0., proj_drop=0.):
super().__init__() self.dim = dim self.window_size = window_size # Wh, Ww self.num_heads = num_heads head_dim = dim // num_heads self.scale = qk_scale or head_dim ** -0.5
# define a parameter table of relative position bias relative_position_bias_table = self.create_parameter( shape=((2 * window_size[0] - 1) * (2 * window_size[1] - 1), num_heads), default_initializer=nn.initializer.Constant(value=0)) # 2*Wh-1 * 2*Ww-1, nH self.add_parameter("relative_position_bias_table", relative_position_bias_table)
# get pair-wise relative position index for each token inside the window coords_h = paddle.arange(self.window_size[0]) coords_w = paddle.arange(self.window_size[1]) coords = paddle.stack(paddle.meshgrid([coords_h, coords_w])) # 2, Wh, Ww coords_flatten = paddle.flatten(coords, 1) # 2, Wh*Ww relative_coords = coords_flatten.unsqueeze(-1) - coords_flatten.unsqueeze(1) # 2, Wh*Ww, Wh*Ww relative_coords = relative_coords.transpose([1, 2, 0]) # Wh*Ww, Wh*Ww, 2 relative_coords[:, :, 0] += self.window_size[0] - 1 # shift to start from 0 relative_coords[:, :, 1] += self.window_size[1] - 1 relative_coords[:, :, 0] *= 2 * self.window_size[1] - 1 self.relative_position_index = relative_coords.sum(-1) # Wh*Ww, Wh*Ww self.register_buffer("relative_position_index", self.relative_position_index)
self.qkv = nn.Linear(dim, dim * 3, bias_attr=qkv_bias) self.attn_drop = nn.Dropout(attn_drop) self.proj = nn.Linear(dim, dim) self.proj_drop = nn.Dropout(proj_drop)
self.softmax = nn.Softmax(axis=-1)
def forward(self, x, mask=None): """ Args: x: input features with shape of (num_windows*B, N, C) mask: (0/-inf) mask with shape of (num_windows, Wh*Ww, Wh*Ww) or None """ B_, N, C = x.shape qkv = self.qkv(x).reshape([B_, N, 3, self.num_heads, C // self.num_heads]).transpose([2, 0, 3, 1, 4]) q, k, v = qkv[0], qkv[1], qkv[2] # make torchscript happy (cannot use tensor as tuple)
q = q * self.scale attn = q @ swapdim(k ,-2, -1)
relative_position_bias = paddle.index_select(self.relative_position_bias_table, self.relative_position_index.reshape((-1,)),axis=0).reshape((self.window_size[0] * self.window_size[1],self.window_size[0] * self.window_size[1], -1))
relative_position_bias = relative_position_bias.transpose([2, 0, 1]) # nH, Wh*Ww, Wh*Ww attn = attn + relative_position_bias.unsqueeze(0)
if mask is not None: nW = mask.shape[0] attn = attn.reshape([B_ // nW, nW, self.num_heads, N, N]) + mask.unsqueeze(1).unsqueeze(0) attn = attn.reshape([-1, self.num_heads, N, N]) attn = self.softmax(attn) else: attn = self.softmax(attn)
attn = self.attn_drop(attn)
x = swapdim((attn @ v),1, 2).reshape([B_, N, C]) x = self.proj(x) x = self.proj_drop(x) return x
复制代码



4.2 Swin Trasnformer 模型实现

Swin Transformer 涉及模型代码较多,所以建议完整的看 Swin Transformer 的代码,因此推荐一下桨的Swin Transformer实现。

4.3 Swin Trasnformer 模型特点

  1. 首次在 cv 领域的 transformer 模型中采用了分层结构。分层结构因为其不同大小的尺度,使不同层特征有了更加不同的意义,较浅层的特征具有大尺度和细节信息,较深层的特征具有小尺度和物体的整体轮廓信息,在图像分类领域,深层特征具有更加有用的作用,只需要根据这个信息判定物体的类别即可,但是在像素级的分割和检测任务中,则需要更为精细的细节信息,因此,分层结构的模型往往更适用于分割和检测这样的像素级要求的任务中。Swin Transformer 模仿 ResNet 采取了分层的结构,使其成为了 cv 领域的通用框架。

  2. 引入 locality 思想,对无重合的 window 区域内进行 self-attention 计算。不仅减少了计算量,而且多了不同窗口之间的交互。

4.4 Swin Trasnformer 模型效果


第一列为对比的方法,第二列为图片尺寸的大小(尺寸越大浮点运算量越大),第三列为参数量,第四列为浮点运算量,第五列为模型吞吐量。可以看出,Swin-T 在 top1 准确率上超过了大部分模型 EffNet-B3 确实是个优秀的网络,在参数量和 FLOPs 都比 Swin-T 少的情况下,略优于 Swin-T,然而,基于 ImageNet1K 数据集,Swin-B 在这些模型上取得了最优的效果。另外,Swin-L 在 ImageNet-22K 上的 top1 准确率达到了 87.3%的高度,这是以往的模型都没有达到的。并且 Swin Transformer 的其他配置也取得了优秀的成绩。图中不同配置的 Swin Transformer 解释如下。



C 就是上面提到的类似于通道数的值,layer numbers 就是 Swin Transformer Block 的数量了。这两个都是值越大,效果越好。和 ResNet 十分相似。


下图为 COCO 数据集上目标检测与实例分割的表现。都是相同网络在不同骨干网络下的对比。可以看出在不同 AP 下,Swin Transformer 都有大约 5%的提升,这已经是很优秀的水平了。怪不得能成为 ICCV2021 最佳 paer。



下图为语义分割数据集 ADE20K 上的表现。相较于同为 transformer 的 DeiT-S, Swin Transformer-S 有了 5%的性能提升。相较于 ResNeSt-200,Swin Transformer-L 也有 5%的提升。另外可以看到,在 UNet 的框架下,Swin Transformer 的各个版本都有十分优秀的成绩,这充分说明了 Swin Transformer 是 CV 领域的通用骨干网络。



5.ViT( Vision Transformer-2020)

在计算机视觉领域中,多数算法都是保持 CNN 整体结构不变,在 CNN 中增加 attention 模块或者使用 attention 模块替换 CNN 中的某些部分。有研究者提出,没有必要总是依赖于 CNN。因此,作者提出 ViT[1]算法,仅仅使用 Transformer 结构也能够在图像分类任务中表现很好。


受到 NLP 领域中 Transformer 成功应用的启发,ViT 算法中尝试将标准的 Transformer 结构直接应用于图像,并对整个图像分类流程进行最少的修改。具体来讲,ViT 算法中,会将整幅图像拆分成小图像块,然后把这些小图像块的线性嵌入序列作为 Transformer 的输入送入网络,然后使用监督学习的方式进行图像分类的训练。


该算法在中等规模(例如 ImageNet)以及大规模(例如 ImageNet-21K、JFT-300M)数据集上进行了实验验证,发现:


  • Transformer 相较于 CNN 结构,缺少一定的平移不变性和局部感知性,因此在数据量不充分时,很难达到同等的效果。具体表现为使用中等规模的 ImageNet 训练的 Transformer 会比 ResNet 在精度上低几个百分点。

  • 当有大量的训练样本时,结果则会发生改变。使用大规模数据集进行预训练后,再使用迁移学习的方式应用到其他数据集上,可以达到或超越当前的 SOTA 水平。

5.1 ViT 模型结构与实现

ViT 算法的整体结构如 图 1 所示。


5.1.1. ViT 图像分块嵌入

考虑到在 Transformer 结构中,输入是一个二维的矩阵,矩阵的形状可以表示为 ,其中 是 sequence 的长度,而 是 sequence 中每个向量的维度。因此,在 ViT 算法中,首先需要设法将 的三维图像转化为 的二维输入。


ViT 中的具体实现方式为:将 的图像,变为一个 的序列。这个序列可以看作是一系列展平的图像块,也就是将图像切分成小块后,再将其展平。该序列中一共包含了 个图像块,每个图像块的维度则是 。其中 是图像块的大小, 是通道数量。经过如上变换,就可以将 视为 sequence 的长度了。


但是,此时每个图像块的维度是 $(P^2C)D(P^2C)D$ 即可。


上述对图像进行分块以及 Embedding 的具体方式如 图 2 所示。



具体代码实现如下所示。本文中将每个大小为 的图像块经过大小为 的卷积核来代替原文中将大小为 的图像块展平后接全连接运算的操作。


#图像分块、Embeddingclass PatchEmbed(nn.Layer):    def __init__(self, img_size=224, patch_size=16, in_chans=3, embed_dim=768):        super().__init__()        # 原始大小为int,转为tuple,即:img_size原始输入224,变换后为[224,224]        img_size = to_2tuple(img_size)        patch_size = to_2tuple(patch_size)        # 图像块的个数        num_patches = (img_size[1] // patch_size[1]) * \            (img_size[0] // patch_size[0])        self.img_size = img_size        self.patch_size = patch_size        self.num_patches = num_patches        # kernel_size=块大小,即每个块输出一个值,类似每个块展平后使用相同的全连接层进行处理        # 输入维度为3,输出维度为块向量长度        # 与原文中:分块、展平、全连接降维保持一致        # 输出为[B, C, H, W]        self.proj = nn.Conv2D(            in_chans, embed_dim, kernel_size=patch_size, stride=patch_size)
def forward(self, x): B, C, H, W = x.shape assert H == self.img_size[0] and W == self.img_size[1], \ "Input image size ({H}*{W}) doesn't match model ({self.img_size[0]}*{self.img_size[1]})." # [B, C, H, W] -> [B, C, H*W] ->[B, H*W, C] x = self.proj(x).flatten(2).transpose((0, 2, 1)) return x
复制代码

5.1.2. ViT 多头注意力

将图像转化为 的序列后,就可以将其输入到 Transformer 结构中进行特征提取了,如 图 3 所示。



Transformer 结构中最重要的结构就是 Multi-head Attention,即多头注意力结构。具有 2 个 head 的 Multi-head Attention 结构如 图 4 所示。输入 经过转移矩阵,并切分生成 ,然后 做 attention,得到权重向量 ,将 进行加权求和,得到最终的 ,同理可以得到 。接着将它们拼接起来,通过一个线性层进行处理,得到最终的结果。



其中,使用 计算 的方法是缩放点积注意力 (Scaled Dot-Product Attention)。 结构如 图 5 所示。首先使用每个 去与 做 attention,这里说的 attention 就是匹配这两个向量有多接近,具体的方式就是计算向量的加权内积,得到 。这里的加权内积计算方式如下所示:



其中, 的维度,因为 的数值会随着维度的增大而增大,因此除以 的值也就相当于归一化的效果。


接下来,把计算得到的 取 softmax 操作,再将其与 相乘。



具体代码实现如下所示。


#Multi-head Attentionclass Attention(nn.Layer):    def __init__(self,                 dim,                 num_heads=8,                 qkv_bias=False,                 qk_scale=None,                 attn_drop=0.,                 proj_drop=0.):        super().__init__()        self.num_heads = num_heads        head_dim = dim // num_heads        self.scale = qk_scale or head_dim**-0.5        # 计算 q,k,v 的转移矩阵        self.qkv = nn.Linear(dim, dim * 3, bias_attr=qkv_bias)        self.attn_drop = nn.Dropout(attn_drop)        # 最终的线性层        self.proj = nn.Linear(dim, dim)        self.proj_drop = nn.Dropout(proj_drop)
def forward(self, x): N, C = x.shape[1:] # 线性变换 qkv = self.qkv(x).reshape((-1, N, 3, self.num_heads, C // self.num_heads)).transpose((2, 0, 3, 1, 4)) # 分割 query key value q, k, v = qkv[0], qkv[1], qkv[2] # Scaled Dot-Product Attention # Matmul + Scale attn = (q.matmul(k.transpose((0, 1, 3, 2)))) * self.scale # SoftMax attn = nn.functional.softmax(attn, axis=-1) attn = self.attn_drop(attn) # Matmul x = (attn.matmul(v)).transpose((0, 2, 1, 3)).reshape((-1, N, C)) # 线性变换 x = self.proj(x) x = self.proj_drop(x) return x
复制代码

5.1.3. 多层感知机(MLP)

Transformer 结构中还有一个重要的结构就是 MLP,即多层感知机,如 图 6 所示。



多层感知机由输入层、输出层和至少一层的隐藏层构成。网络中各个隐藏层中神经元可接收相邻前序隐藏层中所有神经元传递而来的信息,经过加工处理后将信息输出给相邻后续隐藏层中所有神经元。在多层感知机中,相邻层所包含的神经元之间通常使用“全连接”方式进行连接。多层感知机可以模拟复杂非线性函数功能,所模拟函数的复杂性取决于网络隐藏层数目和各层中神经元数目。多层感知机的结构如 图 7 所示。



具体代码实现如下所示。


class Mlp(nn.Layer):    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.fc2 = nn.Linear(hidden_features, out_features)        self.drop = nn.Dropout(drop)
def forward(self, x): # 输入层:线性变换 x = self.fc1(x) # 应用激活函数 x = self.act(x) # Dropout x = self.drop(x) # 输出层:线性变换 x = self.fc2(x) # Dropout x = self.drop(x) return x
复制代码

5.1.4. DropPath

除了以上重要模块意外,代码实现过程中还使用了 DropPath(Stochastic Depth)来代替传统的 Dropout 结构,DropPath 可以理解为一种特殊的 Dropout。其作用是在训练过程中随机丢弃子图层(randomly drop a subset of layers),而在预测时正常使用完整的 Graph。


具体实现如下:


def drop_path(x, drop_prob=0., training=False):    if drop_prob == 0. or not training:        return x    keep_prob = paddle.to_tensor(1 - drop_prob)    shape = (paddle.shape(x)[0], ) + (1, ) * (x.ndim - 1)    random_tensor = keep_prob + paddle.rand(shape, dtype=x.dtype)    random_tensor = paddle.floor(random_tensor)    output = x.divide(keep_prob) * random_tensor    return output
class DropPath(nn.Layer): def __init__(self, drop_prob=None): super(DropPath, self).__init__() self.drop_prob = drop_prob
def forward(self, x): return drop_path(x, self.drop_prob, self.training)
复制代码

5.1.5 基础模块

基于上面实现的 Attention、MLP、DropPath 模块就可以组合出 Vision Transformer 模型的一个基础模块,如 图 8 所示。



基础模块的具体实现如下:


class Block(nn.Layer):    def __init__(self,                 dim,                 num_heads,                 mlp_ratio=4.,                 qkv_bias=False,                 qk_scale=None,                 drop=0.,                 attn_drop=0.,                 drop_path=0.,                 act_layer=nn.GELU,                 norm_layer='nn.LayerNorm',                 epsilon=1e-5):        super().__init__()        self.norm1 = eval(norm_layer)(dim, epsilon=epsilon)        # Multi-head Self-attention        self.attn = Attention(            dim,            num_heads=num_heads,            qkv_bias=qkv_bias,            qk_scale=qk_scale,            attn_drop=attn_drop,            proj_drop=drop)        # DropPath        self.drop_path = DropPath(drop_path) if drop_path > 0. else Identity()        self.norm2 = eval(norm_layer)(dim, epsilon=epsilon)        mlp_hidden_dim = int(dim * mlp_ratio)        self.mlp = Mlp(in_features=dim,                       hidden_features=mlp_hidden_dim,                       act_layer=act_layer,                       drop=drop)
def forward(self, x): # Multi-head Self-attention, Add, LayerNorm x = x + self.drop_path(self.attn(self.norm1(x))) # Feed Forward, Add, LayerNorm x = x + self.drop_path(self.mlp(self.norm2(x))) return x
复制代码

5.1.6. 定义 ViT 网络

基础模块构建好后,就可以构建完整的 ViT 网络了。在构建完整网络结构之前,还需要给大家介绍几个模块:


  • Class Token


假设我们将原始图像切分成 共 9 个小图像块,最终的输入序列长度却是 10,也就是说我们这里人为的增加了一个向量进行输入,我们通常将人为增加的这个向量称为 Class Token。那么这个 Class Token 有什么作用呢?


我们可以想象,如果没有这个向量,也就是将 个向量输入 Transformer 结构中进行编码,我们最终会得到 9 个编码向量,可对于图像分类任务而言,我们应该选择哪个输出向量进行后续分类呢?因此,ViT 算法提出了一个可学习的嵌入向量 Class Token,将它与 9 个向量一起输入到 Transformer 结构中,输出 10 个编码向量,然后用这个 Class Token 进行分类预测即可。


其实这里也可以理解为:ViT 其实只用到了 Transformer 中的 Encoder,而并没有用到 Decoder,而 Class Token 的作用就是寻找其他 9 个输入向量对应的类别。


  • Positional Encoding


按照 Transformer 结构中的位置编码习惯,这个工作也使用了位置编码。不同的是,ViT 中的位置编码没有采用原版 Transformer 中的 编码,而是直接设置为可学习的 Positional Encoding。对训练好的 Positional Encoding 进行可视化,如 图 9 所示。我们可以看到,位置越接近,往往具有更相似的位置编码。此外,出现了行列结构,同一行/列中的 patch 具有相似的位置编码。



  • MLP Head


得到输出后,ViT 中使用了 MLP Head 对输出进行分类处理,这里的 MLP Head 由 LayerNorm 和两层全连接层组成,并且采用了 GELU 激活函数。


具体代码如下所示。


首先构建基础模块部分,包括:参数初始化配置、独立的不进行任何操作的网络层。


#参数初始化配置trunc_normal_ = nn.initializer.TruncatedNormal(std=.02)zeros_ = nn.initializer.Constant(value=0.)ones_ = nn.initializer.Constant(value=1.)
#将输入 x 由 int 类型转为 tuple 类型def to_2tuple(x): return tuple([x] * 2)
#定义一个什么操作都不进行的网络层class Identity(nn.Layer): def __init__(self): super(Identity, self).__init__()
def forward(self, input): return input
复制代码


完整代码如下所示。


class VisionTransformer(nn.Layer):    def __init__(self,                 img_size=224,                 patch_size=16,                 in_chans=3,                 class_dim=1000,                 embed_dim=768,                 depth=12,                 num_heads=12,                 mlp_ratio=4,                 qkv_bias=False,                 qk_scale=None,                 drop_rate=0.,                 attn_drop_rate=0.,                 drop_path_rate=0.,                 norm_layer='nn.LayerNorm',                 epsilon=1e-5,                 **args):        super().__init__()        self.class_dim = class_dim
self.num_features = self.embed_dim = embed_dim # 图片分块和降维,块大小为patch_size,最终块向量维度为768 self.patch_embed = PatchEmbed( img_size=img_size, patch_size=patch_size, in_chans=in_chans, embed_dim=embed_dim) # 分块数量 num_patches = self.patch_embed.num_patches # 可学习的位置编码 self.pos_embed = self.create_parameter( shape=(1, num_patches + 1, embed_dim), default_initializer=zeros_) self.add_parameter("pos_embed", self.pos_embed) # 人为追加class token,并使用该向量进行分类预测 self.cls_token = self.create_parameter( shape=(1, 1, embed_dim), default_initializer=zeros_) self.add_parameter("cls_token", self.cls_token) self.pos_drop = nn.Dropout(p=drop_rate)
dpr = np.linspace(0, drop_path_rate, depth) # transformer self.blocks = nn.LayerList([ Block( dim=embed_dim, num_heads=num_heads, mlp_ratio=mlp_ratio, qkv_bias=qkv_bias, qk_scale=qk_scale, drop=drop_rate, attn_drop=attn_drop_rate, drop_path=dpr[i], norm_layer=norm_layer, epsilon=epsilon) for i in range(depth) ])
self.norm = eval(norm_layer)(embed_dim, epsilon=epsilon)
# Classifier head self.head = nn.Linear(embed_dim, class_dim) if class_dim > 0 else Identity()
trunc_normal_(self.pos_embed) trunc_normal_(self.cls_token) self.apply(self._init_weights) # 参数初始化 def _init_weights(self, m): if isinstance(m, nn.Linear): trunc_normal_(m.weight) if isinstance(m, nn.Linear) and m.bias is not None: zeros_(m.bias) elif isinstance(m, nn.LayerNorm): zeros_(m.bias) ones_(m.weight) def forward_features(self, x): B = paddle.shape(x)[0] # 将图片分块,并调整每个块向量的维度 x = self.patch_embed(x) # 将class token与前面的分块进行拼接 cls_tokens = self.cls_token.expand((B, -1, -1)) x = paddle.concat((cls_tokens, x), axis=1) # 将编码向量中加入位置编码 x = x + self.pos_embed x = self.pos_drop(x) # 堆叠 transformer 结构 for blk in self.blocks: x = blk(x) # LayerNorm x = self.norm(x) # 提取分类 tokens 的输出 return x[:, 0]
def forward(self, x): # 获取图像特征 x = self.forward_features(x) # 图像分类 x = self.head(x) return x
复制代码

5.2 ViT 模型指标

ViT 模型在常用数据集上进行迁移学习,最终指标如 图 10 所示。可以看到,在 ImageNet 上,ViT 达到的最高指标为 88.55%;在 ImageNet ReaL 上,ViT 达到的最高指标为 90.72%;在 CIFAR100 上,ViT 达到的最高指标为 94.55%;在 VTAB(19 tasks)上,ViT 达到的最高指标为 88.55%。


5.3 ViT 模型特点

  • 作为 CV 领域最经典的 Transformer 算法之一,不同于传统的 CNN 算法,ViT 尝试将标准的 Transformer 结构直接应用于图像,并对整个图像分类流程进行最少的修改。

  • 为了满足 Transformer 输入结构的要求,将整幅图像拆分成小图像块,然后把这些小图像块的线性嵌入序列输入到网络。同时,使用了 Class Token 的方式进行分类预测。


更多文章请关注公重号:汀丶人工智能



  • 参考文献


[1] An Image is Worth 16x16 Words:Transformers for Image Recognition at Scale


发布于: 刚刚阅读数: 3
用户头像

本博客将不定期更新关于NLP等领域相关知识 2022-01-06 加入

本博客将不定期更新关于机器学习、强化学习、数据挖掘以及NLP等领域相关知识,以及分享自己学习到的知识技能,感谢大家关注!

评论

发布
暂无评论
深度学习应用篇-计算机视觉-图像分类[3]:ResNeXt、Res2Net、Swin Transformer、Vision Transformer等模型结构、实现、模型特点详细介绍_人工智能_汀丶人工智能_InfoQ写作社区