第七章:ViT & Swin Transformer

VIT

vit的流程图如下所示
transformer输入的二维的[batch,seq,emb],也就是批次,语句长度,每个字被映射成的向量。
而要想在cv领域也使用transformer,整体的思路是
1 将输入图像分为不同大小的patches
假设输入的图像大小为224*224*3,那么如果我们想要划分为的patches是16*16的形状,那么显然一共会有14*14=196个patches,然后我们还想要每个patches映射为786维的token(注意这里的token长度是自己定义的)
如何划分为patches呢?只需要使用16x16且步长为16的卷积核就好了,这样就实现了每次卷积一个patch,隐含了拆分patch的操作
如何映射到786维呢?只需要将out_channel设置为786即可

2 将patches输入到Linear Projection of Flattened Pathes中,这实际上就是一个embedding层
通过步骤一,原大小为[224,224,3]的输入经过卷积核就得到了[14,14,786]的输出,再将高宽展平就得到了需要的[196,786]

self.proj = nn.Conv2d(3, 768, kernel_size=16, stride=16)

使用

x = self.proj(x).flatten(2).transpose(1, 2)

即可展平为196,并将channel移动到最后,变为[batch,196,786]
3 将得到的向量添加一个class_token到最前面,当作这个图片的类别
这里是需要初始化一个一维向量,和模型一起训练,到最后的classification的时候就只需要把这一维给拿出来就好了
这样形状就从[196,786]变为[197,786]

cls_token = self.cls_token.expand(x.shape[0], -1, -1)
x = torch.cat((cls_token, x), dim=1)  # [B, 197, 768]

4 给这些向量添加位置信息

self.pos_embed = nn.Parameter(torch.zeros(1, num_patches + self.num_tokens, embed_dim))
x = self.pos_drop(x + self.pos_embed)

这里只是相加,没有维度方向的拼接

5 将添加了class_token和位置信息的token输入到Transformer Encoder中

在这里插入图片描述

Transformer Encoder的模块流程图如下所示

在这里插入图片描述
Transformer Encoder是由多个Encoder Block堆叠而成的
添加位置信息的x经过堆叠而成的blocks

x = self.blocks(x)

而blocks需要重复调用Block

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)
        ])

而在Block中需要调用Atten和MLP Block,也就是上图中Encoder Block的下半部分和右边的MLP Block

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

Atten就是transformer的多头注意力,首先需要构建生成qkv的模块

self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)

这里生成dim * 3的维度,是因为在这里qkv矩阵是一起生成的,实际上也可以构建三个nn.Linear(dim, dim , bias=qkv_bias),也是一样的

B, N, C = x.shape  //B=batch,N=197,C=786
# qkv(): -> [batch_size, 197, 3 * 786]
        # reshape: -> [batch_size, 197, 3, num_heads, 786//num_heads]
        # permute: -> [3, batch_size, num_heads, 197, 786//num_heads]
        qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4)

在这里,如果有num_heads个头,则需要将token分成num_heads份,最后再拼接起来

q, k, v = qkv[0], qkv[1], qkv[2]

这里因为上面把3换到了最前面,就可以分成qkv了,这里的3是刚刚linear里的3

		attn = (q @ k.transpose(-2, -1)) * self.scale
  	    # [batch_size, num_heads, 197, 197]
        attn = attn.softmax(dim=-1)
        attn = self.attn_drop(attn)

        # @: multiply -> [batch_size, num_heads, 197, 786//num_heads]
        # transpose: -> [batch_size, 197, num_heads, 786//num_heads]
        # reshape: -> [batch_size, 197, 786]
        x = (attn @ v).transpose(1, 2).reshape(B, N, C)

这里就是注意力的操作了
后面再经过一个融合层更好的融合一下,再经过MLP Block层就完成了一个Block

return self.pre_logits(x[:, 0])

最后将x的class token返回并通过一个class层即可

Swin-Transformer

Swin-Transformer是针对目标检测的模型,相比起VIT有几点不同
在这里插入图片描述

1 构建的feature map具有层次感,对于目标检测和分割任务都具有更大的优势
2 用windows将feature map分隔开,在windows窗口内部进行多头注意力的计算,大大降低了计算量
在这里插入图片描述
整体的流程图如上所示
其中Patch Partition的流程图如下所示
在这里插入图片描述
使用4x4的卷积核分割patch,然后在channel方向进行展平,这样图像的大小就变为了[h/4,w/4,48]
Linear Embedding层则是再channel维度上降维到C
而Patch Merging层就是结合了上面的两个层,高宽减半,channel乘二
在这里插入图片描述
具体来说,是分割为不同的patches再在channel上拼接,经过normal层,最后经过linear层在channel维度上减半

再来看swin transformer的注意力部分
在这里插入图片描述
在传统的多头注意力中,在q@v时每个点都要与其他的所有点计算相似度,而在windows多头注意力中每个点只会与其所在的windows的点计算相似度,很显然这样虽然会降低计算量,但是却缺少了信息交互。

MSA和W-MSA的复杂度如下所示
在这里插入图片描述
MSA复杂度
计算注意力中的Q时,会首先将输入经过一个Wq生成Q,在这个过程中需要将[hw,C]的输入经过一个[C,C]的矩阵生成大小为[hw,C]的Q,复杂度是hw*C*C=hwC²,要生成QKV复杂度就是3hwC²
随后需要计算Q@KT@V,这个过程的复杂度是(hw)²C+(hw)²C=2(hw)²C
在多头注意力中最后还需要经过一个融合矩阵,和生成QKV一样需要纬度不变,因此复杂度也是hwC²
总的复杂度是4hwC²+2(hw)²C
W-MSA复杂度
W-MSA是将输入划分为许多个M*M的小windows窗口,在窗口内部做多头注意力计算
因此共有h/M*w/M 个 M*M 的小窗口,每个窗口的复杂度是
4M*M*C²+2(M * M)²C = 4M²C²+2(M²)²C
总的复杂度就是 h/M*w/M *(4M²C²+2(M²)²C) = 4hwC²+2(M)²hwC
在这里插入图片描述

W-MSA是在每个窗口内部做注意力,因此缺少了窗口与窗口之间的信息交互
为了解决这个问题,在上面的图中显示swin transformer中还有一个SW-MSA去计算注意力,W-MSA和SW-MSA是交替出现的,这就是stage 1-4 总是成偶数出现的原因

在Layer l中是W-MSA,那么在Layer l+1中就是SW-MSA,将原先的四个窗口又划分为九个窗口,这里的九个窗口中每个窗口就携带了更多的信息,划分之后不能直接做注意力
将上面的三个windows移到下面,再将左边的三个windows移到右边,就得到了新的feature map,将5和3,1和7,8 6 2 0合为一起,内部去做注意力,但是又会有一个新的问题,就是例如5和3,1和7本身就不在一起,强行去一起做注意力是不行的,因此就需要加上mask MSA
在这里插入图片描述

在这里插入图片描述

例如5和3,在对区域5中的0去做注意力时,将2 3 6 7 10 11 14 15的值减去100,这样在经过softmax后就相当于是0了。
在做完所有的操作后还会将移动过的windows再移动回去
在这里插入图片描述

思考题​

在ViT中要降低 Attention的计算量,有哪些方法?(提示:Swin的 Window attention,PVT的attention)​
swin 的windows 就是在windows内做注意力,再“打乱”一下做注意力,这个做的还比较厉害一点
PVT 就是在生成QKV之后,将K和V做一个下采样,例如QKV原本是[hw,C],KV就先变为[hw/R²,C],这样Q@KT再@V后维度不变,也实现了减小计算量的目的
我觉得,有些论文能发表在顶会上的很重要一个原因是实验做的足够多,还有一个可能是通讯有名了hhhhh

Swin体现了一种什么思路?对后来工作有哪些启发?(提示:先局部再整体)​
将较大的feature map分割成小的windows分别做注意力,为了防止信息交互少还添加了SW-MSA层,这样既减小了计算量效果也不会差
有些网络将CNN和Transformer结合,为什么一般把 CNN block放在面前,Transformer block放在后面?​
我认为CNN block有时候是用来作为embedding的替代或改进的,例如在VIT中是使用卷积分割patches+展平操作来模拟embedding层,可以在卷积分割这个地方来添加CNN层来更好的模拟embedding层
阅读并了解Restormer,思考:Transformer的基本结构为 attention+ FFN,这个工作分别做了哪些改进?​

这篇论文是CVPR2022上面的一篇,整体思想是:
对于图像修复来讲,CNN和transformer各有缺点
CNN由于其天生的受限的感受野,难以建模长距离像素依赖关系,也就是很难将一个区域与离它很远的区域联系起来:也许会有这样的情况,白云和湖面的倒影有很强的关系,但CNN的卷积核却很难理解
Transformer的注意力机制理论上会让每个像素与其他所有像素都有互动,但是它对于高分辨率的图像计算起来特别复杂,训练缓慢
在这里插入图片描述

Restormer的目标是设计一个高效的Transformer模型,适用于高分辨率图像恢复,其创新点在于:
1、多Dconv头转置注意力(MDTA)
我看了几篇博客的介绍,他们说这篇论文不是计算空间上的注意力而是通道上的注意力,但是都没有细讲,论文的结构图也没有很好的解决我的疑惑,于是就去看了一下源码
源码关于MDTA部分的代码如下所示

class Attention(nn.Module):
    def __init__(self, dim, num_heads, bias):
        super(Attention, self).__init__()
        self.num_heads = num_heads
        self.temperature = nn.Parameter(torch.ones(num_heads, 1, 1))

        self.qkv = nn.Conv2d(dim, dim*3, kernel_size=1, bias=bias)
        self.qkv_dwconv = nn.Conv2d(dim*3, dim*3, kernel_size=3, stride=1, padding=1, groups=dim*3, bias=bias)
        self.project_out = nn.Conv2d(dim, dim, kernel_size=1, bias=bias)
        


    def forward(self, x):
        b,c,h,w = x.shape

        qkv = self.qkv_dwconv(self.qkv(x))
        q,k,v = qkv.chunk(3, dim=1)   
        
        q = rearrange(q, 'b (head c) h w -> b head c (h w)', head=self.num_heads)
        k = rearrange(k, 'b (head c) h w -> b head c (h w)', head=self.num_heads)
        v = rearrange(v, 'b (head c) h w -> b head c (h w)', head=self.num_heads)

        q = torch.nn.functional.normalize(q, dim=-1)
        k = torch.nn.functional.normalize(k, dim=-1)

        attn = (q @ k.transpose(-2, -1)) * self.temperature
        attn = attn.softmax(dim=-1)

        out = (attn @ v)
        
        out = rearrange(out, 'b head c (h w) -> b (head c) h w', head=self.num_heads, h=h, w=w)

        out = self.project_out(out)
        return out

​一个1x1的卷积,一个3x3的卷积……等一下,不是深度可分离卷积吗?难道groups=3就完成了DW+PW的操作吗?好像还真的是这样,我发现好多论文中的复杂的故事在实现上都很简单,就像vit中分割patches的操作也可以使用conv2d实现。到底是首先想到了轻量化设计再提出了深度可分离卷积DW+PW,最后发现竟然conv2d可以直接支持这样做;还是发现了conv2d中的groups参数,又发现这个操作可以减少参数量和计算量,再想到可以用到轻量化上从而讲了一个好故事。
言归正传,从代码中可以看出,所谓的MDTA,实际上就是在生成qkv时使用了深度可分离卷积,让计算量和参数量变少

2、门控Dconv前馈网络(GDFN)

class FeedForward(nn.Module):
    def __init__(self, dim, ffn_expansion_factor, bias):
        super(FeedForward, self).__init__()

        hidden_features = int(dim*ffn_expansion_factor)

        self.project_in = nn.Conv2d(dim, hidden_features*2, kernel_size=1, bias=bias)

        self.dwconv = nn.Conv2d(hidden_features*2, hidden_features*2, kernel_size=3, stride=1, padding=1, groups=hidden_features*2, bias=bias)

        self.project_out = nn.Conv2d(hidden_features, dim, kernel_size=1, bias=bias)

    def forward(self, x):
        x = self.project_in(x)
        x1, x2 = self.dwconv(x).chunk(2, dim=1)
        x = F.gelu(x1) * x2
        x = self.project_out(x)
        return x

可以看出,这里所谓的GDFN就是用深度可分离卷积核去代替卷积核

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐