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

Posted zstar-_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了图像分类Vision Transformer理论解读+实践测试相关的知识,希望对你有一定的参考价值。

前言

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 (224/16)^2=196 (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 selfPytorch CIFAR10图像分类 Vision Transformer(ViT) 篇

Pytorch CIFAR10图像分类 Vision Transformer(ViT) 篇

手把手教你搭建神经网络(使用Vision Transformer进行图像分类)

手把手教你搭建神经网络(使用Vision Transformer进行图像分类)

vision_transformer实战总结:非常简单的VIT入门教程,一定不要错过

vision_transformer实战总结:非常简单的VIT入门教程,一定不要错过