关于人工智能:深度学习应用篇计算机视觉图像分类3ResNeXtRes2NetVision-Transformer等模型结构

5次阅读

共计 28869 个字符,预计需要花费 73 分钟才能阅读完成。

深度学习利用篇 - 计算机视觉 - 图像分类[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 函数,则有:

$$
z_{i+1} = w_ia_i+b_i\\
a_{i+1} = \sigma(z_{i+1})
$$

其中,$\sigma(\cdot)$ 为 sigmoid 函数。

依据链式求导和反向流传,咱们能够失去:

$$
\frac{\partial y}{\partial a_1} = \frac{\partial y}{\partial a_4}\frac{\partial a_4}{\partial z_4}\frac{\partial z_4}{\partial a_3}\frac{\partial a_3}{\partial z_3}\frac{\partial z_3}{\partial a_2}\frac{\partial a_2}{\partial z_2}\frac{\partial z_2}{\partial a_1} \\
= \frac{\partial y}{\partial a_4}\sigma^{‘}(z_4)w_3\sigma^{‘}(z_3)w_2\sigma^{‘}(z_2)w_1
$$

Sigmoid 函数的导数 $\sigma^{‘}(x)$ 如 图 3 所示:

咱们能够看到 sigmoid 的导数最大值为 0.25,那么随着网络层数的减少,小于 1 的小数一直相乘导致 $\frac{\partial y}{\partial a_1}$ 逐步趋近于零,从而产生梯度隐没。

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

1.2.2 网络进化

当初,梯度隐没 / 爆炸的问题解决了是不是就能够通过重叠层数来加深网络了呢?Still no!

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

为什么会引起网络进化呢?依照实践上的想法,当浅层网络成果不错的时候,网络层数的减少即便不会引起精度上的晋升也不该使模型成果变差。但事实上非线性的激活函数的存在,会造成很多不可逆的信息损失,网络加深到肯定水平,过多的信息损失就会造成网络的进化。

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

1..3. 残差块

当初咱们明确了,为了加深网络结构,使每一次可能学到更细化的特色从而进步网络精度,须要实现的一点是 恒等映射。那么残差网络如何可能做到这一点呢?

恒等映射即为 $H(x) = x$,已有的神经网络构造很难做到这一点,然而如果咱们将网络设计成 $H(x) = F(x) + x$,即 $F(x) = H(x) – x$,那么只须要使残差函数 $F(x) = 0$,就形成了恒等映射 $H(x) = F(x)$。

残差构造的目标是,随着网络的加深,使 $F(x)$ 迫近于 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 的训练成果。

    1. 参考文献
      ResNet

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 有了很大的晋升。

  • 参考文献
    Res2Net

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,将输出依照 2 ×2 的相邻 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 畛域的通用骨干网络。

  • 参考文献
    Swin Transformer

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 构造中,输出是一个二维的矩阵,矩阵的形态能够示意为 $(N,D)$,其中 $N$ 是 sequence 的长度,而 $D$ 是 sequence 中每个向量的维度。因而,在 ViT 算法中,首先须要设法将 $H \times W \times C$ 的三维图像转化为 $(N,D)$ 的二维输出。

ViT 中的具体实现形式为:将 $H \times W \times C$ 的图像,变为一个 $N \times (P^2 * C)$ 的序列。这个序列能够看作是一系列展平的图像块,也就是将图像切分成小块后,再将其展平。该序列中一共蕴含了 $N=HW/P^2$ 个图像块,每个图像块的维度则是 $(P^2*C)$。其中 $P$ 是图像块的大小,$C$ 是通道数量。通过如上变换,就能够将 $N$ 视为 sequence 的长度了。

然而,此时每个图像块的维度是 $(P^2*C)$,而咱们理论须要的向量维度是 $D$,因而咱们还须要对图像块进行 Embedding。这里 Embedding 的形式非常简单,只须要对每个 $(P^2*C)$ 的图像块做一个线性变换,将维度压缩为 $D$ 即可。

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

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

# 图像分块、Embedding
class 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 多头注意力

将图像转化为 $N \times (P^2 * C)$ 的序列后,就能够将其输出到 Transformer 构造中进行特征提取了,如 图 3 所示。

Transformer 构造中最重要的构造就是 Multi-head Attention,即多头注意力构造。具备 2 个 head 的 Multi-head Attention 构造如 图 4 所示。输出 $a^i$ 通过转移矩阵,并切分生成 $q^{(i,1)}$、$q^{(i,2)}$、$k^{(i,1)}$、$k^{(i,2)}$、$v^{(i,1)}$、$v^{(i,2)}$,而后 $q^{(i,1)}$ 与 $k^{(i,1)}$ 做 attention,失去权重向量 $\alpha$,将 $\alpha$ 与 $v^{(i,1)}$ 进行加权求和,失去最终的 $b^{(i,1)}(i=1,2,…,N)$,同理能够失去 $b^{(i,2)}(i=1,2,…,N)$。接着将它们拼接起来,通过一个线性层进行解决,失去最终的后果。

其中,应用 $q^{(i,j)}$、$k^{(i,j)}$ 与 $v^{(i,j)}$ 计算 $b^{(i,j)}(i=1,2,…,N)$ 的办法是缩放点积注意力 (Scaled Dot-Product Attention)。构造如 图 5 所示。首先应用每个 $q^{(i,j)}$ 去与 $k^{(i,j)}$ 做 attention,这里说的 attention 就是匹配这两个向量有多靠近,具体的形式就是计算向量的加权内积,失去 $\alpha_{(i,j)}$。这里的加权内积计算形式如下所示:

$$ \alpha_{(1,i)} = q^1 * k^i / \sqrt{d} $$

其中,$d$ 是 $q$ 和 $k$ 的维度,因为 $q*k$ 的数值会随着维度的增大而增大,因而除以 $\sqrt{d}$ 的值也就相当于归一化的成果。

接下来,把计算失去的 $\alpha_{(i,j)}$ 取 softmax 操作,再将其与 $v^{(i,j)}$ 相乘。

具体代码实现如下所示。

#Multi-head Attention
class 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

假如咱们将原始图像切分成 $3 \times 3$ 共 9 个小图像块,最终的输出序列长度却是 10,也就是说咱们这里人为的减少了一个向量进行输出,咱们通常将人为减少的这个向量称为 Class Token。那么这个 Class Token 有什么作用呢?

咱们能够设想,如果没有这个向量,也就是将 $N=9$ 个向量输出 Transformer 构造中进行编码,咱们最终会失去 9 个编码向量,可对于图像分类工作而言,咱们应该抉择哪个输入向量进行后续分类呢?因而,ViT 算法提出了一个可学习的嵌入向量 Class Token,将它与 9 个向量一起输出到 Transformer 构造中,输入 10 个编码向量,而后用这个 Class Token 进行分类预测即可。

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

  • Positional Encoding

依照 Transformer 构造中的地位编码习惯,这个工作也应用了地位编码。不同的是,ViT 中的地位编码没有采纳原版 Transformer 中的 $sincos$ 编码,而是间接设置为可学习的 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 16×16 Words:Transformers for Image Recognition at Scale

正文完
 0