论文地址:https://arxiv.org/abs/2103.15808

代码地址:https://github.com/leoxiaobin/CvT

https://github.com/microsoft/CvT/blob/main/lib/models/cls_cvt.py
Transformer大火,最近的论文几乎都是transformer系列了,但是CNN也有其可取之处,未来CNN和transformer结合想必是大势所趋。这篇文章将CNN引入Transformer中,取得了比较好的成绩。

摘要

在本文中,我们提出了一种新的架构,名为卷积视觉转换器(CvT),通过在视觉转换器(ViT)中引入卷积,以产生两种设计的最佳效果,从而提高了性能和效率。这是通过两个主要修改来实现的:包含新的卷积令牌嵌入的Transformer层次结构,以及利用卷积投影的卷积Transformer块。这些变化将卷积神经网络(cnn)的理想特性引入到ViT架构中(即移动、缩放和失真不变性),同时保持了transformer的优点(即动态关注、全局上下文和更好的泛化)。我们通过大量的实验验证了CvT,表明这种方法在ImageNet-1k上比其他Vision transformer和resnet实现了最先进的性能,而且参数更少,FLOPs更低。此外,当在更大的数据集(例如ImageNet-22k)上进行预训练并对下游任务进行微调时,性能也会得到提高。在ImageNet-22k上进行了预训练,我们的CvT-W24在ImageNet-1k的val集上获得了87.7%的最高准确率。最后,我们的结果表明,位置编码,现有的Vision transformer的一个关键组件,可以安全地在我们的模型中删除,从而简化了更高分辨率视觉任务的设计。代码将在https: //github.com/leoxiaobin/CvT发布。
摘要小结:
我们知道卷积具备平移不变性(shift, scale,and distortion invariance)等优点,但是大规模数据情况下,卷积的参数量剧增,但是transformer却可以以较小的参数量取得和CNN相仿的效果。虽然Transformer具备(dynamic attention, global context,and better generalization)等优点,但是对细节和局部特征的提取能力不强。为此将CNN和Transformer结合是一个好方法。CVT比其他transformer和神经网络的参数量更少,且性能更佳。

1.Introduction

提出背景:Transformer在大规模数据上效果好,但是在小数据量上训练时,性能低于普通CNN,原因是ViT缺乏CNN架构的某些属性,而这些属性恰好可以使CNN解决视觉任务。
举例子:如图像具有二维局部结构,空间相邻的像素通常关联性很强。
CNN结构使用局部感受野、共享权值和空间下采样等操作捕获这个局部结构,因此也实现了一定程度的移动、缩放和失真不变性。此外,卷积核的层次结构学习的视觉模式考虑了不同复杂程度的局部空间背景,从简单的低阶边缘和纹理到高阶语义模式。
CVT简单介绍:
CvT设计引入了ViT架构的两个核心部分。首先,我们将变形金刚划分为多个阶段,这些阶段形成了变形金刚的层次结构。每个阶段的开始都包含一个卷积令牌嵌入,该嵌入在一个2d重塑的令牌映射(即,将扁平的令牌序列重塑回空间网格)上执行一个步幅重叠卷积操作,然后是层规范化。这使得模型不仅可以捕获局部信息,还可以逐步减少序列长度,同时在各个阶段增加令牌特征的维度,实现空间下采样,同时增加特征映射的数量,就像在cnn[20]中执行的那样。其次,Transformer模块中每个自注意块之前的线性投影被我们提出的卷积投影所取代,卷积投影在一个2d重塑的令牌映射上采用了s × s深度可分离卷积[5]操作。这使得模型可以进一步捕获局部空间语境,减少注意机制中的语义歧义。它还允许对计算复杂度进行管理,因为卷积的步幅可以用于对键和值矩阵进行子采样,从而在性能下降最小的情况下将效率提高4倍或更多。
总结:
总之,我们提出的卷积视觉变换(CvT)利用了cnn的所有优点:局部感受野、共享权值和空间下采样,同时保留了transformer的所有优点:动态关注、全局上下文融合和更好的泛化。我们的结果表明,当CvT使用ImageNet1k进行预训练时,该方法达到了最先进的性能,同时是轻量级和高效的:与基于cnn的模型(如ResNet)和先前基于变压器的模型(如ViT, DeiT)相比,CvT提高了性能,同时使用更少的FLOPS和参数。此外,CvT在更大规模的预训练中(例如在公共ImageNet-22k数据集上)获得了最先进的性能。最后,我们演示了在这个新设计中,我们可以在不降低模型性能的情况下放弃标记的位置嵌入。这不仅简化了架构设计,而且使它能够轻松适应输入图像的不同分辨率,这对许多视觉任务来说是至关重要的。

2.related work:

先介绍了一些transformer的一些相关工作,如ViT,将CNN和Transformer结合的工作如BoTNet。(可跳过)
介绍了在transformer中一般怎么用的CNN。在NLP、语音识别中,卷积用来修改transformer快,用CNN替换多头注意力、并行或同步中添加卷积层捕获局部关系。

之前的其他工作提出通过残差连接将注意映射传播到后续层,残差连接首先通过卷积转换。与这些工作不同的是,我们建议将卷积引入到视觉Transformer的两个主要部分:第一,用我们的卷积投影代替现有的基于位置的线性投影来进行注意力操作,第二,使用我们的分层多阶段结构来实现2D重塑令牌映射的不同分辨率,类似于cnn。(这里看起来还是不好懂,没事往下看)

3.Convolutional vision Transformer

对照下代码
在这里插入图片描述
图2:CvT架构的流水线。(a)总体架构,显示了由卷积令牌嵌入层促进的分层多阶段结构。(b) Convolutional Transformer Block的细节,包含卷积投影作为第一层。

卷积视觉转换器(CvT)的整体流程如图2所示。我们在Vision Transformer架构中引入了两个基于卷积的操作,即卷积令牌嵌入(Convolutional Token Embedding)和卷积投影( Convolutional Projection)。如图2 (a)所示,本文借鉴了cnn的多级层次设计,共采用了三个阶段。每个阶段有两个部分。首先,将输入图像(或二维重塑令牌映射)置于卷积令牌嵌入层,卷积令牌嵌入层实现为将重塑令牌的重叠块卷积到二维空间网格作为输入(重叠程度可通过步幅控制)。一个额外的层规范化应用于标记。这允许每个阶段逐步减少令牌的数量(即特征分辨率),同时增加令牌的宽度(即特征维数),从而实现空间下采样和丰富的表达能力,类似于cnn的设计。与之前基于transformer的架构不同[11,30,41,34],我们没有将ad-hod位置嵌入到令牌中。接下来,每个阶段的剩余部分由一组提议的卷积变压器块组成。图2 (b)显示了卷积Transformer块的架构,其中分别应用深度可分离卷积运算[5],即卷积投影,来进行查询、键和值的嵌入,而不是ViT中的标准位置线性投影[11]。此外,分类令牌只在最后一个阶段添加。最后,利用MLP(即全连接)头对最后一级输出的分类标记进行预测。

3.1 Convolutional Token Embedding

首先看上面的图2,可知道一共有3个stage,每个stage都包含了Convolutional Token Embedding和Convolutional Transformer Block。我们拆出一个stage来看。

首先经过Convolutional Token Embedding,这里其实想当于做了一个输入为k×k的卷积,将原始图像分片了,k=patch_size,直接看下代码

class ConvEmbed(nn.Module):
    """ Image to Conv Embedding
    """

    def __init__(self,
                 patch_size=7,
                 in_chans=3,
                 embed_dim=64,
                 stride=4,
                 padding=2,
                 norm_layer=None):
        super().__init__()
        patch_size = to_2tuple(patch_size)
        self.patch_size = patch_size
####这里定义了一个proj卷积结构,将
        self.proj = nn.Conv2d(
            in_chans, embed_dim,
            kernel_size=patch_size,
            stride=stride,
            padding=padding
        )
        self.norm = norm_layer(embed_dim) if norm_layer else None

    def forward(self, x):
        x = self.proj(x)###经过这里后,相当于分片完成,

        B, C, H, W = x.shape#将得到的feature map的向量分解
        x = rearrange(x, 'b c h w -> b (h w) c')
        if self.norm:
            x = self.norm(x)
        x = rearrange(x, 'b (h w) c -> b c h w', h=H, w=W)

        return x

下图是swin-transformer的分片部分

class PatchEmbed(nn.Module):
    r""" 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.Module, 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]]####长有多少个patch、宽有多少patch
        self.img_size = img_size
        self.patch_size = patch_size
        self.patches_resolution = patches_resolution
        self.num_patches = patches_resolution[0] * patches_resolution[1]

        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)###相当于一个4×4的卷积,输入为3,输出为96,
        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
        # FIXME look at relaxing size constraints
##########重点在这里有所不同
        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]})."
        x = self.proj(x).flatten(2).transpose(1, 2)  # B Ph*Pw C
        if self.norm is not None:
            x = self.norm(x)
        return x
以下是CVT部分:
		B, C, H, W = x.shape#将得到的feature map的向量分解
		x = rearrange(x, 'b c h w -> b (h w) c')
		if self.norm:
		     x = self.norm(x)
		x = rearrange(x, 'b (h w) c -> b c h w', h=H, w=W)
 		return x

我们对比看一下,可以知道CVT主要是将其h、w两个维度和c变换然后经过norm,再换为原来的维度位置,而swin transformer是直接将H*W和c变换。
将Convolutional Token Embedding的翻译再看一下:
将输入图像(或二维重塑令牌映射)置于卷积令牌嵌入层,卷积令牌嵌入层实现为将重塑令牌的重叠块卷积到二维空间网格作为输入(重叠程度可通过步幅控制)。一个额外的层规范化应用于标记。这允许每个阶段逐步减少令牌的数量(即特征分辨率),同时增加令牌的宽度(即特征维数),从而实现空间下采样和丰富的表达能力,类似于cnn的设计。与之前基于transformer的架构不同[11,30,41,34],我们没有将ad-hod位置嵌入到令牌中。

同时注意一点:CvT中是没有位置编码的,因为采用的是卷积,而不是linear projection,卷积可以记住位置信息,因此就去掉了原始transformer中的位置编码操作。(Windows attention中)看下面的ViT中是带有位置编码信息的。

在这里插入图片描述
这个模块的输入是reshap成2D结构的token,主要做的是对这个2D的特征图进行一次卷积操作。卷积的目的在于保证每个阶段都能减小特征图的尺寸,增加特征图通道数,相应的将其reshape成token后,token的数量也会减少,但是token的维度会增加。这使得token能够在越来越大的空间足迹上表示越来越复杂的视觉模式,类似于cnn的特征层。

Convolutional Projection for Attention

提出的卷积投影层的目标是实现局部空间环境的额外建模,并通过允许K和V矩阵的欠采样来提供效率效益。基本上,提出的带有卷积投影的Transformer块是原始Transformer块的泛化。虽然之前的工作[13,39]试图在Transformer块中添加额外的卷积模块来进行语音识别和自然语言处理,但它们导致了更复杂的设计和额外的计算成本。相反,我们提出用深度可分离卷积取代原来的多头自注意(MHSA)的位置线性投影,形成卷积投影层。
解析一下
实际本文的这块工作是做了一个Transformer的Block堆叠的工作,而每个block模块则是由self-attention+MLP模块构成。重点这个模块将self-attention中的q,k,v矩阵做了改动。同时将深度可分离替换了MHSA的位置线性投影形成卷积投影层。(这里就是之前提到的位置编码)

3.2.1 Implementation Details

图3 (a)显示了ViT[11]中使用的原始位置线性投影,图3 (b)显示了我们提出的s × s卷积投影。如图3 (b)所示,令牌首先被重塑为一个2D令牌映射。接下来,使用深度可分离的卷积层实现卷积投影(Convolutional Projection),核大小为s。最后,将投影的符号展平为1D以便后续处理。这可以表示为:
在这里插入图片描述
其中,Conv2d是深度可分离卷积操作。在这里插入图片描述
然后将得到的token其送入MLP模块,这样一个block模块就构成了。而本模块则是由多个block构成。
下面这个图就是block结构
在这里插入图片描述
整理过程就是这个样子,这个block 模块主要是由self-attention+MLP组成。
先经过norm1(norm_layer=nn.LayerNorm,)然后是Attention,然后是DropPath,然后将这个经过droupPath的结果与初始x相加得到新的x。最后将经过norm2和MLP操作的值放入droupPath中将这个结果与上面得到的新的x相加。先看一下block的代码部分。(注意对照上面那个图)。

class Block(nn.Module):

    def __init__(self,
                 dim_in,
                 dim_out,
                 num_heads,
                 mlp_ratio=4.,
                 qkv_bias=False,
                 drop=0.,
                 attn_drop=0.,
                 drop_path=0.,
                 act_layer=nn.GELU,
                 norm_layer=nn.LayerNorm,
                 **kwargs):
        super().__init__()

        self.with_cls_token = kwargs['with_cls_token']

        self.norm1 = norm_layer(dim_in)
        self.attn = Attention(
            dim_in, dim_out, num_heads, qkv_bias, attn_drop, drop,
            **kwargs
        )

        self.drop_path = DropPath(drop_path) \
            if drop_path > 0. else nn.Identity()
        self.norm2 = norm_layer(dim_out)

        dim_mlp_hidden = int(dim_out * mlp_ratio)
        self.mlp = Mlp(
            in_features=dim_out,
            hidden_features=dim_mlp_hidden,
            act_layer=act_layer,
            drop=drop
        )

    def forward(self, x, h, w):
        res = x

        x = self.norm1(x)
        attn = self.attn(x, h, w)
        x = res + self.drop_path(attn)
        x = x + self.drop_path(self.mlp(self.norm2(x)))

        return x

这里的重点就是attention怎么操作的。
attention
首先看下Attention部分代码

class Attention(nn.Module):
    def __init__(self,
                 dim_in,
                 dim_out,
                 num_heads,
                 qkv_bias=False,
                 attn_drop=0.,
                 proj_drop=0.,
                 method='dw_bn',
                 kernel_size=3,
                 stride_kv=1,
                 stride_q=1,
                 padding_kv=1,
                 padding_q=1,
                 with_cls_token=True,
                 **kwargs
                 ):
        super().__init__()
        self.stride_kv = stride_kv
        self.stride_q = stride_q
        self.dim = dim_out
        self.num_heads = num_heads
        # head_dim = self.qkv_dim // num_heads
        self.scale = dim_out ** -0.5
        self.with_cls_token = with_cls_token

        self.conv_proj_q = self._build_projection(
            dim_in, dim_out, kernel_size, padding_q,
            stride_q, 'linear' if method == 'avg' else method
        )
        self.conv_proj_k = self._build_projection(
            dim_in, dim_out, kernel_size, padding_kv,
            stride_kv, method
        )
        self.conv_proj_v = self._build_projection(
            dim_in, dim_out, kernel_size, padding_kv,
            stride_kv, method
        )

        self.proj_q = nn.Linear(dim_in, dim_out, bias=qkv_bias)
        self.proj_k = nn.Linear(dim_in, dim_out, bias=qkv_bias)
        self.proj_v = nn.Linear(dim_in, dim_out, bias=qkv_bias)

        self.attn_drop = nn.Dropout(attn_drop)
        self.proj = nn.Linear(dim_out, dim_out)
        self.proj_drop = nn.Dropout(proj_drop)

    def _build_projection(self,
                          dim_in,
                          dim_out,
                          kernel_size,
                          padding,
                          stride,
                          method):
        if method == 'dw_bn':
     。。。。。。。。。。
        return proj

    def forward_conv(self, x, h, w):
      。。。。。。。
        return q, k, v

    def forward(self, x, h, w):
        if (
            self.conv_proj_q is not None
            or self.conv_proj_k is not None
            or self.conv_proj_v is not None
        ):
            q, k, v = self.forward_conv(x, h, w)

        q = rearrange(self.proj_q(q), 'b t (h d) -> b h t d', h=self.num_heads)
        k = rearrange(self.proj_k(k), 'b t (h d) -> b h t d', h=self.num_heads)
        v = rearrange(self.proj_v(v), 'b t (h d) -> b h t d', h=self.num_heads)

        attn_score = torch.einsum('bhlk,bhtk->bhlt', [q, k]) * self.scale
        attn = F.softmax(attn_score, dim=-1)
        attn = self.attn_drop(attn)

        x = torch.einsum('bhlt,bhtv->bhlv', [attn, v])
        x = rearrange(x, 'b h t d -> b t (h d)')

        x = self.proj(x)
        x = self.proj_drop(x)

        return x

上面的代码中有两个重要的部分一个是_build_projection操作,一个是forward_conv部分。这里实际划分qkv的是_build_projection,如果没有划分我们就启用forward_conv。看下这个
在这里插入图片描述

_build projection
首先我们先看一下原始的ViT的Linear projectio结构(a)是采用线性映射的方法。CVT(b)中是用了一个深度可分离卷积(看图中橙色的那个卷积块)通过卷积块,后来通过Convolutional Projection(_build projection)输出一个token块proj,然后将token展平为q、k、v。
在这里插入图片描述

这里有个关键部分是_build_projection操作,就是深度可分离部分,但是作者写的代码是可以选择的。这一块就是图3(b)中的部分

   def _build_projection(self,
                          dim_in,
                          dim_out,
                          kernel_size,
                          padding,
                          stride,
                          method):
        if method == 'dw_bn':
            proj = nn.Sequential(OrderedDict([
                ('conv', nn.Conv2d(
                    dim_in,
                    dim_in,
                    kernel_size=kernel_size,
                    padding=padding,
                    stride=stride,
                    bias=False,
                    groups=dim_in
                )),
                ('bn', nn.BatchNorm2d(dim_in)),
                ('rearrage', Rearrange('b c h w -> b (h w) c')),
            ]))
        elif method == 'avg':
            proj = nn.Sequential(OrderedDict([
                ('avg', nn.AvgPool2d(
                    kernel_size=kernel_size,
                    padding=padding,
                    stride=stride,
                    ceil_mode=True
                )),
                ('rearrage', Rearrange('b c h w -> b (h w) c')),
            ]))
        elif method == 'linear':
            proj = None
        else:
            raise ValueError('Unknown method ({})'.format(method))

        return proj

这里是forward_conv部分,可以看到这里传进来的是x、h、w。这一步结束后我们将得到三个展平的q、k、v

 def forward_conv(self, x, h, w):
        if self.with_cls_token:
            cls_token, x = torch.split(x, [1, h*w], 1)

        x = rearrange(x, 'b (h w) c -> b c h w', h=h, w=w)

        if self.conv_proj_q is not None:
            q = self.conv_proj_q(x)
        else:
            q = rearrange(x, 'b c h w -> b (h w) c')

        if self.conv_proj_k is not None:
            k = self.conv_proj_k(x)
        else:
            k = rearrange(x, 'b c h w -> b (h w) c')

        if self.conv_proj_v is not None:
            v = self.conv_proj_v(x)
        else:
            v = rearrange(x, 'b c h w -> b (h w) c')

        if self.with_cls_token:
            q = torch.cat((cls_token, q), dim=1)
            k = torch.cat((cls_token, k), dim=1)
            v = torch.cat((cls_token, v), dim=1)

        return q, k, v

下面是Attention的forward部分。
首先定义了conv_proj_q、k、v,其实这里就是划分了q/k/v了。得到qkv后就进行attention的其他部分操作了。
在这里插入图片描述

def forward(self, x, h, w):
#这里如果conv_proj_q、k、v不存在
#我们就启用自己定义的k、q、v(forward_conv)
        if (
            self.conv_proj_q is not None
            or self.conv_proj_k is not None
            or self.conv_proj_v is not None
        ):
            q, k, v = self.forward_conv(x, h, w)
####这里看下面的图就知道proj_q是什么了
        q = rearrange(self.proj_q(q), 'b t (h d) -> b h t d', h=self.num_heads)
        k = rearrange(self.proj_k(k), 'b t (h d) -> b h t d', h=self.num_heads)
        v = rearrange(self.proj_v(v), 'b t (h d) -> b h t d', h=self.num_heads)
####这里的enisum是个求和公式
        attn_score = torch.einsum('bhlk,bhtk->bhlt', [q, k]) * self.scale
        attn = F.softmax(attn_score, dim=-1)
        attn = self.attn_drop(attn)

        x = torch.einsum('bhlt,bhtv->bhlv', [attn, v])
        x = rearrange(x, 'b h t d -> b t (h d)')

        x = self.proj(x)
        x = self.proj_drop(x)

        return x

在这里插入图片描述
attention部分执行完后,就完成block的其他操作了,前面我们也讲过了。
剩余部分是实验部分了,可以参考原文或者下面的链接

参考(https://blog.csdn.net/qq_37937847/article/details/117564682

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐