【图像分类】Vision Transformer理论解读+实践测试

简介: Vision Transformer是2021年谷歌在ICLR上提出的算法,算法原理并不复杂,但是其价值是开创性的。它首次将NLP领域火热的Transformer模型架构移植到了CV领域,打破了这两个领域壁垒,并取得不错的成效。论文名称: An Image Is Worth 16x16 Words: Transformers For Image Recognition At Scale。

前言

Vision Transformer是2021年谷歌在ICLR上提出的算法,算法原理并不复杂,但是其价值是开创性的。它首次将NLP领域火热的Transformer模型架构移植到了CV领域,打破了这两个领域壁垒,并取得不错的成效。

论文名称: An Image Is Worth 16x16 Words: Transformers For Image Recognition At Scale
论文链接:https://arxiv.org/abs/2010.11929

模型结构/算法流程

Vision Transformer的模型结构相比于Transformer来说更简单,在Transformer模型中,主要包含Encoder和Decoder结构,而ViT(Vision Transformer)仅借鉴了Encoder结构。
在这里插入图片描述
ViT的处理流程大致可以分为以下几个步骤:

1.图片预处理

预处理这个步骤在论文里并没有详细说明,但是对于ViT这个结构而言,输入的图片尺寸并不是自定义的,ViT-B/16为例,输入的图片尺寸必须为224x224。

因此,首先需要对输入图片进行尺寸变化,具体方式可以是直接缩放(Resize),也可以进行随机裁剪(RandomResizedCrop),在后面复现的代码中,使用的是随机裁剪的方式。

2.图片切割

Transformer需要输入的是一维的Token,对于二维的图像,一种朴素的想法就是把一个个像素点拉平,这样就成了一个一维序列。但是这样造成的一个后果是计算量太庞大,比如一张224x224的图片,变成1维度之后就成了50176,相当于直接输入一篇五万字的文章,模型难以计算。

那么,一个改进的想法就是把一张图片分成nxn个Patch,每一个Patch作为一个Token,这样计算量就大大减小了。

以ViT-B/16为例,将输入图片(224x224)按照16x16大小的Patch进行划分,划分后可以得到共$(224/16)^2=196$个Patch。每个Patch是三通道的小图片,shape为(16, 16, 3),将其展平就变成了一个长度为768的向量。

每一个向量作为一个单独的输入,那样我们总共有196个向量,在代码中,可以变成一个[196,768]的矩阵,进行并行输入。

这一步的操作在论文中是直接采用切割的处理办法,但是在后面的代码实现中,采用了一种更巧妙的解决思路,就是利用一个卷积核大小为16x16,步距为16,卷积核个数为768的卷积层来进行实现。

再来回顾我们的卷积层计算公式:
在这里插入图片描述

输入为[224,244,3],经过卷积层变成[14,14,768],再映射为[196,768]。

这样,就完成了从图片到Token之间的转换。

3.添加[class]token

在上面的结构图中可以看到,输入Encoder的最左侧部分添加了一个0*这个Token,这个就是额外添加的一个[class]token,单独用来处理类别信息,经过Encoder之后,需要单独将这个Token再提取出来,输入到MLP Head之中再输出分类结果。

这也是为什么结构图中MLP Head的位置是和这个[class]token对齐。

4.添加位置编码

在Transformer中,位置编码的作用是为了记忆输入的语序信息。ViT中,同样需要位置编码来记录各图像块之间的位置信息。

这里主要有两种位置编码思路,一种思路是在转换之前(14,14)的图像块矩阵添加二维(2-D)位置编码,另一种思路是在转换后(196+1)这个维度上添加一维(1-D)位置编码。

作者也对其做了实验,实验结果如下表所示:

在这里插入图片描述

可以看到,添加一维位置编码和二维位置编码并没有太大的差异。作者随后也对一维位置编码的结果进行了可视化,结果如下图所示:

在这里插入图片描述

上图中是每一个Patch中各位置的位置编码相似性度量,越接近黄色的位置代表越靠近位置编码的中心位置,可以看到,即使是一维位置编码,同样可以比较好地记录二维信息。

5.Transformer Encoder

ViT虽然采用的是Transformer Encoder的结构,但是和Transformer原始的Encoder还是有所区别,我将两者的结构进行对比,如下图所示,左侧为Transformer原始的Encoder结构。
在这里插入图片描述

可以看到,大致上两者结构是相同的,主要区别在于Norm层的顺序,原始Transformer的Norm层在多头注意力和前馈网络之后,而ViT将其放到前面,这里的原因,论文里没有做解释。

关于Norm层,ViT仍是采用Transformer中用到Layer Normalization,计算公式如下:
在这里插入图片描述

Norm层之后同样是多头注意力层(Multi-Head Attention),和Transformer中的一样。

后面的MLP是个单独的结构,论文里没有绘制其详细结构,看到博主太阳花的小绿豆[1]对其进行了绘制,这里转贴在此。MLP也并不复杂,就是两个线性层+GELU激活函数+Dropout的结构,具体细节可以看后面的代码。

在这里插入图片描述

6.MLP Head

在Transformer Encoder输出结果之后,需要再将第一个添加的Class Token提取出来,然后输入到MLP Head进行分类。在论文中,作者先是在ImageNet21K上进行预训练,MLP Head结构由Linear+tanh激活函数+Linear组成,但是迁移到其它数据集训练时,只需要用一个一个Linear即可。

输出结果之后,再和真实标签做交叉熵损失,这样就可以完成ViT的训练过程。

效果对比

在论文中,作者将ViT和之前图像分类领域比较强的ResNet模型进行了对比测试,结果如下:

在这里插入图片描述

可以看到,右图中,作者使用了谷歌制作的JFT-300M数据集,当数据量小于30M时,ViT的效果表现不如ResNet,但是当数据量逐渐增大时,ViT才会慢慢超越ResNet。由此可见ViT工作的局限性,它必须要在超大数据集上进行预训练,然后再拿到其它数据集上做迁移学习,才会有好的效果。

关于ViT模型的不同版本,论文里也做了说明:
在这里插入图片描述
其中的Layers就是Transformer Encoder中重复堆叠Encoder Block的次数,Hidden Size就是对应通过Embedding层后每个token的dim(向量的长度),MLP size是Transformer Encoder中MLP Block第一个全连接的节点个数(是Hidden Size的四倍),Heads代表Transformer中Multi-Head Attention的heads数。

注意力可视化

ViT这篇论文长达二十多页,里面包含了非常丰富的成果,其中包括注意力可视化。由于作者是首次将Transformer应用到图像领域,里面包含了注意力机制,那么作者就想把注意力得到的结果(也就是Q-K矩阵乘积)换源到图像上,得到结果如下图所示:
在这里插入图片描述
可以看到,模型自动学习到了如果注意画面中的分类主体。

混合模型探索

在论文的最后,作者又探索了一种混合模型(Hybrid),就是将传统CNN和Transformer进行结合。

下表中对比了ViT、ResNet和混合模型在不同图像分类数据集上的测试结果,可以看到当Epochs增大时,ResNet和混合模型的效果均不如ViT模型。
在这里插入图片描述

实践测试

下面就来尝试使用ViT做一个简单的分类任务。官方使用的是Tensorflow2框架,下面我使用的是别人利用Pytorch实现的ViT。
代码来源于https://github.com/WZMIAOMIAO/deep-learning-for-image-processing/tree/master/pytorch_classification/vision_transformer

实验采用的是花蕊数据集,共5个类别,约4000多个样本。

数据集下载:https://pan.baidu.com/s/1vpB3s78bV4Xxowfpe07tdw?pwd=8888

默认使用的是ViT-B/16这个模型,整体结构图[1]如下:
在这里插入图片描述

图片预处理

对数据集和验证集划分之后,需要对数据进行预处理。

这里对训练集的处理方式是随机切成224x224像素的图片,然后进行水平翻转,再进行归一化和标准化处理;对验证集的处理方式是先Resize成256x256的图片,再从中心位置裁剪成224x224,再进行归一化和标准化处理。

相关代码:

data_transform = {
  "train": transforms.Compose([transforms.RandomResizedCrop(224),
                               transforms.RandomHorizontalFlip(),
                               transforms.ToTensor(),
                               transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])]),
  "val": transforms.Compose([transforms.Resize(256),
                             transforms.CenterCrop(224),
                             transforms.ToTensor(),
                             transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])])}

图片切割

通过卷积,将图片转成一个个Patch。
相关代码:

class PatchEmbed(nn.Module):
    """
    2D Image to Patch Embedding
    """
    def __init__(self, img_size=224, patch_size=16, in_c=3, embed_dim=768, norm_layer=None):
        super().__init__()
        img_size = (img_size, img_size)
        patch_size = (patch_size, patch_size)
        self.img_size = img_size
        self.patch_size = patch_size
        self.grid_size = (img_size[0] // patch_size[0], img_size[1] // patch_size[1])
        self.num_patches = self.grid_size[0] * self.grid_size[1]

        self.proj = nn.Conv2d(in_c, embed_dim, kernel_size=patch_size, stride=patch_size)
        self.norm = norm_layer(embed_dim) if norm_layer else nn.Identity()

    def forward(self, x):
        B, C, H, W = x.shape
        assert H == self.img_size[0] and W == self.img_size[1], \
            f"Input image size ({H}*{W}) doesn't match model ({self.img_size[0]}*{self.img_size[1]})."

        # flatten: [B, C, H, W] -> [B, C, HW]
        # transpose: [B, C, HW] -> [B, HW, C]
        x = self.proj(x).flatten(2).transpose(1, 2)
        x = self.norm(x)
        return x

多头注意力

多头注意力模块实现时注意是通过一个线性层来同时计算qkv三个矩阵,这样可以提升计算效率。

class Attention(nn.Module):
    def __init__(self,
                 dim,   # 输入token的dim
                 num_heads=8,
                 qkv_bias=False,
                 qk_scale=None,
                 attn_drop_ratio=0.,
                 proj_drop_ratio=0.):
        super(Attention, self).__init__()
        self.num_heads = num_heads
        head_dim = dim // num_heads
        self.scale = qk_scale or head_dim ** -0.5
        self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)
        self.attn_drop = nn.Dropout(attn_drop_ratio)
        self.proj = nn.Linear(dim, dim)
        self.proj_drop = nn.Dropout(proj_drop_ratio)

    def forward(self, x):
        # [batch_size, num_patches + 1, total_embed_dim]
        B, N, C = x.shape

        # qkv(): -> [batch_size, num_patches + 1, 3 * total_embed_dim]
        # reshape: -> [batch_size, num_patches + 1, 3, num_heads, embed_dim_per_head]
        # permute: -> [3, batch_size, num_heads, num_patches + 1, embed_dim_per_head]
        qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4)
        # [batch_size, num_heads, num_patches + 1, embed_dim_per_head]
        q, k, v = qkv[0], qkv[1], qkv[2]  # make torchscript happy (cannot use tensor as tuple)

        # transpose: -> [batch_size, num_heads, embed_dim_per_head, num_patches + 1]
        # @: multiply -> [batch_size, num_heads, num_patches + 1, num_patches + 1]
        attn = (q @ k.transpose(-2, -1)) * self.scale
        attn = attn.softmax(dim=-1)
        attn = self.attn_drop(attn)

        # @: multiply -> [batch_size, num_heads, num_patches + 1, embed_dim_per_head]
        # transpose: -> [batch_size, num_patches + 1, num_heads, embed_dim_per_head]
        # reshape: -> [batch_size, num_patches + 1, total_embed_dim]
        x = (attn @ v).transpose(1, 2).reshape(B, N, C)
        x = self.proj(x)
        x = self.proj_drop(x)
        return x

MLP Block

class Mlp(nn.Module):
    """
    MLP as used in Vision Transformer, MLP-Mixer and related networks
    """
    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


class Block(nn.Module):
    def __init__(self,
                 dim,
                 num_heads,
                 mlp_ratio=4.,
                 qkv_bias=False,
                 qk_scale=None,
                 drop_ratio=0.,
                 attn_drop_ratio=0.,
                 drop_path_ratio=0.,
                 act_layer=nn.GELU,
                 norm_layer=nn.LayerNorm):
        super(Block, self).__init__()
        self.norm1 = norm_layer(dim)
        self.attn = Attention(dim, num_heads=num_heads, qkv_bias=qkv_bias, qk_scale=qk_scale,
                              attn_drop_ratio=attn_drop_ratio, proj_drop_ratio=drop_ratio)
        # NOTE: drop path for stochastic depth, we shall see if this is better than dropout here
        self.drop_path = DropPath(drop_path_ratio) if drop_path_ratio > 0. else nn.Identity()
        self.norm2 = norm_layer(dim)
        mlp_hidden_dim = int(dim * mlp_ratio)
        self.mlp = Mlp(in_features=dim, hidden_features=mlp_hidden_dim, act_layer=act_layer, drop=drop_ratio)

    def forward(self, x):
        x = x + self.drop_path(self.attn(self.norm1(x)))
        x = x + self.drop_path(self.mlp(self.norm2(x)))
        return x

VisionTransformer

将上面各部分结合起来,得到VisionTransformer整体结构,可以和上面的结构图对比

class VisionTransformer(nn.Module):
    def __init__(self, img_size=224, patch_size=16, in_c=3, num_classes=1000,
                 embed_dim=768, depth=12, num_heads=12, mlp_ratio=4.0, qkv_bias=True,
                 qk_scale=None, representation_size=None, distilled=False, drop_ratio=0.,
                 attn_drop_ratio=0., drop_path_ratio=0., embed_layer=PatchEmbed, norm_layer=None,
                 act_layer=None):
        """
        Args:
            img_size (int, tuple): input image size
            patch_size (int, tuple): patch size
            in_c (int): number of input channels
            num_classes (int): number of classes for classification head
            embed_dim (int): embedding dimension
            depth (int): depth of transformer
            num_heads (int): number of attention heads
            mlp_ratio (int): ratio of mlp hidden dim to embedding dim
            qkv_bias (bool): enable bias for qkv if True
            qk_scale (float): override default qk scale of head_dim ** -0.5 if set
            representation_size (Optional[int]): enable and set representation layer (pre-logits) to this value if set
            distilled (bool): model includes a distillation token and head as in DeiT models
            drop_ratio (float): dropout rate
            attn_drop_ratio (float): attention dropout rate
            drop_path_ratio (float): stochastic depth rate
            embed_layer (nn.Module): patch embedding layer
            norm_layer: (nn.Module): normalization layer
        """
        super(VisionTransformer, self).__init__()
        self.num_classes = num_classes
        self.num_features = self.embed_dim = embed_dim  # num_features for consistency with other models
        self.num_tokens = 2 if distilled else 1
        norm_layer = norm_layer or partial(nn.LayerNorm, eps=1e-6)
        act_layer = act_layer or nn.GELU

        self.patch_embed = embed_layer(img_size=img_size, patch_size=patch_size, in_c=in_c, embed_dim=embed_dim)
        num_patches = self.patch_embed.num_patches

        self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim))
        self.dist_token = nn.Parameter(torch.zeros(1, 1, embed_dim)) if distilled else None
        self.pos_embed = nn.Parameter(torch.zeros(1, num_patches + self.num_tokens, embed_dim))
        self.pos_drop = nn.Dropout(p=drop_ratio)

        dpr = [x.item() for x in torch.linspace(0, drop_path_ratio, depth)]  # stochastic depth decay rule
        self.blocks = nn.Sequential(*[
            Block(dim=embed_dim, num_heads=num_heads, mlp_ratio=mlp_ratio, qkv_bias=qkv_bias, qk_scale=qk_scale,
                  drop_ratio=drop_ratio, attn_drop_ratio=attn_drop_ratio, drop_path_ratio=dpr[i],
                  norm_layer=norm_layer, act_layer=act_layer)
            for i in range(depth)
        ])
        self.norm = norm_layer(embed_dim)

        # Representation layer
        if representation_size and not distilled:
            self.has_logits = True
            self.num_features = representation_size
            self.pre_logits = nn.Sequential(OrderedDict([
                ("fc", nn.Linear(embed_dim, representation_size)),
                ("act", nn.Tanh())
            ]))
        else:
            self.has_logits = False
            self.pre_logits = nn.Identity()

        # Classifier head(s)
        self.head = nn.Linear(self.num_features, num_classes) if num_classes > 0 else nn.Identity()
        self.head_dist = None
        if distilled:
            self.head_dist = nn.Linear(self.embed_dim, self.num_classes) if num_classes > 0 else nn.Identity()

        # Weight init
        nn.init.trunc_normal_(self.pos_embed, std=0.02)
        if self.dist_token is not None:
            nn.init.trunc_normal_(self.dist_token, std=0.02)

        nn.init.trunc_normal_(self.cls_token, std=0.02)
        self.apply(_init_vit_weights)

    def forward_features(self, x):
        # [B, C, H, W] -> [B, num_patches, embed_dim]
        x = self.patch_embed(x)  # [B, 196, 768]
        # [1, 1, 768] -> [B, 1, 768]
        cls_token = self.cls_token.expand(x.shape[0], -1, -1)
        if self.dist_token is None:
            x = torch.cat((cls_token, x), dim=1)  # [B, 197, 768]
        else:
            x = torch.cat((cls_token, self.dist_token.expand(x.shape[0], -1, -1), x), dim=1)

        x = self.pos_drop(x + self.pos_embed)
        x = self.blocks(x)
        x = self.norm(x)
        if self.dist_token is None:
            return self.pre_logits(x[:, 0])
        else:
            return x[:, 0], x[:, 1]

    def forward(self, x):
        x = self.forward_features(x)
        if self.head_dist is not None:
            x, x_dist = self.head(x[0]), self.head_dist(x[1])
            if self.training and not torch.jit.is_scripting():
                # during inference, return the average of both classifier predictions
                return x, x_dist
            else:
                return (x + x_dist) / 2
        else:
            x = self.head(x)
        return x

我使用vit_base_patch16_224_in21k这个模型,训练了10个epoch,验证集上准确率达到了98.5%。
整体模型还是比较大的,预训练权重大小为393MB,但是训练速度还是挺快的,因为在代码中有个冻结权重的操作,主干部分全部冻结,仅训练分类头。

代码备份

代码备份在此:https://pan.baidu.com/s/10vBdFFtGNPSuSvxXTJij4A?pwd=8888

References

[1]https://blog.csdn.net/qq_37541097/article/details/118242600
[2]https://www.bilibili.com/video/BV15P4y137jb

目录
相关文章
|
1天前
|
监控 Java 测试技术
如何构建高效的自动化测试框架:策略与实践
【7月更文挑战第6天】构建高效的自动化测试框架是一个持续的过程,需要不断迭代和优化。通过遵循设计原则、选择合适的关键技术、并遵循科学的实施步骤,我们可以构建出稳定、可靠、易于维护的自动化测试框架,为软件质量的提升和交付周期的缩短提供有力支持。
|
3天前
|
敏捷开发 测试技术 持续交付
自动化测试在敏捷开发中的实践
【7月更文挑战第5天】自动化测试在敏捷开发中扮演着至关重要的角色。通过制定合适的测试策略、选择合适的测试工具、编写和维护测试脚本以及集成到持续集成流程中,可以显著提高测试效率、加快反馈周期、提高测试覆盖率和降低测试成本。未来,随着技术的不断发展和敏捷开发的深入应用,自动化测试将在软件开发中发挥更加重要的作用。
|
5天前
|
敏捷开发 监控 测试技术
软件测试中的敏捷实践:如何有效整合测试与开发
【7月更文挑战第3天】在软件开发的快速迭代周期中,敏捷测试成为确保产品质量的关键。本文将探讨如何在敏捷开发环境中整合测试与开发工作,包括持续集成、测试自动化和跨功能团队协作的策略,旨在提升软件交付的速度和质量。
|
3天前
|
敏捷开发 jenkins 测试技术
软件测试中的敏捷实践:持续集成与自动化测试的融合之道
在软件开发领域,敏捷实践已成为提升项目响应速度和质量保证的重要手段。本文深入探讨了持续集成(CI)与自动化测试如何相辅相成,共同构建起一套高效、可靠的软件测试体系。通过实例分析,揭示了将敏捷原则应用于软件测试过程中的关键策略,并提出了实现这一目标的可行路径。文章旨在为软件测试专业人员提供一套结合敏捷理念的实用方法论,以应对快速迭代的软件项目需求。
|
3天前
|
测试技术 程序员 持续交付
软件测试中的敏捷实践:提升效率与质量的双重策略
在软件开发领域,敏捷实践已被证明是提高项目适应性、效率和产品质量的关键因素。本文深入探讨了敏捷方法在软件测试中的应用,并分析了其对测试流程的积极影响。通过引用最新的行业研究数据和案例分析,文章揭示了敏捷测试如何促进团队协作、缩短反馈循环、优化资源分配,并最终提升软件产品的整体质量。结合逻辑严密的分析框架和科学的数据支持,本研究旨在为软件测试专业人士提供一套实证基础的实践指南。
|
5天前
|
敏捷开发 算法 搜索推荐
软件测试的演变:从传统方法到敏捷实践
本文深入探讨了软件测试领域的发展轨迹,从早期以代码为中心的测试方法,到今日强调快速迭代和持续集成的敏捷测试实践。文章通过分析历史数据、行业报告以及权威研究,揭示了测试自动化、跨功能团队合作以及质量保证在现代软件开发中的重要性。进一步地,本文还讨论了如何将科学严谨性融入测试过程,包括采用基于证据的测试策略、利用统计方法评估软件质量,并提出了逻辑严密的测试案例设计原则。
|
5天前
|
机器学习/深度学习 人工智能 算法
自动化测试的演进与实践
随着软件行业的飞速发展,传统的手工测试方式已无法满足日益增长的软件质量保证需求。自动化测试作为提高软件测试效率和质量的关键工具,其发展和应用受到业界广泛关注。本文旨在探讨自动化测试技术的发展历程、面临的挑战及未来的发展方向。通过分析自动化测试的优势与局限,结合最新的行业数据和研究结果,揭示自动化测试在现代软件开发中的核心地位及其实践价值。
9 0
|
5天前
|
敏捷开发 jenkins 测试技术
软件测试中的敏捷实践:持续集成与自动化测试的融合
在软件开发领域,敏捷方法论的兴起带来了开发流程的重大变革。特别是持续集成(CI)和自动化测试的结合使用,为提升软件质量和开发效率提供了强有力的支持。本文将探讨持续集成环境下自动化测试的实施策略、优势以及面临的挑战,并借助实证数据和案例分析,揭示这一组合如何促进敏捷团队高效地应对快速变化的需求与代码库。
13 0
|
6天前
|
测试技术 UED
软件测试的科学与艺术:从数据导向到逻辑严密的实践
本文旨在探讨软件测试领域中数据导向和逻辑严密性的重要性,并分析如何通过科学严谨的方法提升测试效率和质量。文章首先概述了软件测试的基本概念和挑战,随后深入讨论了数据在测试设计和结果分析中的关键作用,以及如何利用逻辑推理来构建有效的测试案例和识别潜在缺陷。最后,本文提出了一系列实践建议,旨在帮助测试人员更好地整合数据驱动和逻辑推理方法,以实现软件测试的最优化。
9 0
|
7天前
|
Java 测试技术
Java中的测试驱动开发(TDD)实践
Java中的测试驱动开发(TDD)实践