0
点赞
收藏
分享

微信扫一扫

Transformer——PVT_V1

Star英 2022-01-13 阅读 23

前言

论文地址:PVT1
代码地址:github
作者很厉害…各种cv的顶会收割机…

动机

出发点:改进Vit的不足。

  • 不足一:Vit输出的特征图是single-scale的,也就是不像resnet那样有4个block可以输出四个尺度的特征图。多尺度的特征图对下游任务来说是很有用的,主要是因为之前主流的backbone是resnet,因此很多结构都是根据resnet来设计的(比如FPN,结合不同尺度的特征融合得到一个包含深浅层语义的特征),这样的话可以很好的将transformer的主干网络替换之前的resnet。
  • 不足二:即使正常的输入尺寸(作者举例:短边800像素在COCO标准上)对于Vit来说也要花费大量的计算开销以及内存开销。

网络分析

H * W * 3 -> stage1 block -> H/4 * W/4 * C1 -> stage2 block -> H/8 * W/8 * C2 -> stage3 block -> H/16 * W/16 * C3 -> stage3 block -> H/32 * W/32 * C4

因此只要理解stage block中做的事情就了解清楚了整个网络结构。本文结合作者代码讲解下图部分,其余基本相同不做赘述:
在这里插入图片描述

Patch Emb:

1、首先输入的data的shape是(bs,channal,H,W),为了方便直接用batchsize是1的图片做例子,因此输入是(13224224)
code对应:
model = pvt_small(**cfg)
data = torch.randn((1, 3, 224, 224))
output = model(data)
2、输入数据首先经过stage 1 block的Patch emb操作,这个操作首先把224*224的图像分成4*4的一个个小patch,这个实现是用卷积实现的,用4*4的卷积和对224*224的图像进行卷积,步长为4即可。
code对应:
self.proj = nn.Conv2d(in_chans=3, embed_dim=64, kernel_size=4, stride=4)# 其中64是网络第一层输出特征的维度对应图中的C1
print(x.shape)# torch.Size([1, 3, 224, 224])
x = self.proj(x)
print(x.shape)# torch.Size([1, 64, 56, 56])
这样就可以用56*56的矩阵每一个点表示原来4*4的patch
3、对1*64*56*56的矩阵在进行第二个维度展平
code对应:
print(x.shape) # torch.Size([1, 64, 56, 56])
x = x.flatten(2)
print(x.shape) # torch.Size([1, 64, 3136])
这时候就可以用3136这个一维的向量来表示224*224的图像了
4、为了方便计算调换下第二第三两个维度,然后对数据进行layer norm。
code对应:
print(x.shape) # torch.Size([1, 64, 3136])
x = x.transpose(1, 2)
print(x.shape) # torch.Size([1, 3136, 64])
x = self.norm(x)
print(x.shape) # torch.Size([1, 3136, 64])

以上就完成了Patch emb的操作,完整代码对应:

def forward(self, x):
    B, C, H, W = x.shape # 1,3,224,224
    x = self.proj(x) # 卷积操作,输出1,64,56,56
    x = x.flatten(2) # 展平操作,输出1,64,3136
    x = x.transpose(1, 2) # 交换维度,输出 1,3136,64
    x = self.norm(x) # layer normal,输出 1,3136,64
    H, W = H // 4, W // 4 # 最终的高宽变成56,56
    return x, (H, W)

图示如下:
在这里插入图片描述

position embedding部分

1、这部分和Vit的位置编码基本是一样的,创建一个可学习的参数,大小和patch emb出来的tensor的大小一致就是(1,3136,64),这是个可学习的参数。
code对应:
pos_embed = nn.Parameter(torch.zeros(1, 3136, 64))
2、位置编码的使用也是和Vit一样,直接和输出的x进行矩阵加,因此shape不变化。
code对应:
print(x.shape) # torch.Size([1, 3136, 64])
x = x + pos_embed
print(x.shape) # torch.Size([1, 3136, 64])
3、相加完后,作者加了个dropout进行正则化。
code对应:
pos_drop = nn.Dropout(p=drop_rate)
x = pos_drop(x)

以上就完成了position embedding的操作,完整代码对应:

x = x + pos_embed
x = pos_drop(x)

完整图示对应:
在这里插入图片描述

Encoder部分

第i个stage的encoder部分由depth[i]个block构成,对于pvt_tiny到pvt_large来说主要就是depth的参数的不同:
在这里插入图片描述
例如对于pvt_tiny来说,每个encoder都是由两个block构成,每个block的结构如下图所示:
在这里插入图片描述
对于第一个encoder的第一个block的输入就是我们前面分析的经过position embedding后拿到的tensor,因此他的输入的大小是(1,3136,64),与此同时图像经过Patch emb后变成了56*56的大小。

1、首先从上图可以看出先对输入拷贝一份,给残差结构用。然后输入的x先经过一层layer norm层,此时维度不变,然后经过作者修改的Multi head attention层(SRA,后面再讲)与之前拷贝的输入叠加。
code对应:
print(x.shape) # (1,3136,64)
x = x + self.drop_path(self.attn(self.norm1(x), H, W))
print(x.shape) # (1,3136,64)
2、经过SRA层的特征拷贝一份留给残差结构,然后将输入经过layer norm层,维度不变,再送入feed forward层(后面再讲),之后与之前拷贝的输入叠加。
code对应:
print(x.shape) # (1,3136,64)
x = x + self.drop_path(self.mlp(self.norm2(x)))
print(x.shape) # (1,3136,64)

因此可以发现经过一个block,tensor的shape是不发生变化的。完整的代码对应:

def forward(self, x, H, W):
      x = x + self.drop_path(self.attn(self.norm1(x), H, W)) # SRA层
      x = x + self.drop_path(self.mlp(self.norm2(x))) # feed forward层
      return x
3、这样经过depth[i]个block之后拿到的tensor的大小仍然是(1,3136,64),只需要将它的shape还原成图像的形状就可以输入给下一个stage了。而还原shape,直接调用reshape函数即可,这时候的特征就还原成(bs,channal,H,W)了,数值为(1,64,56,56)
code对应:
print(x.shape) # 1,3136,64
x = x.reshape(B, H, W, -1)
print(x.shape) # 1,56,56,64
x = x.permute(0, 3, 1, 2).contiguous()
print(x.shape) # 1,64,56,56

这时候stage2输入的tensor就是(1,64,56,56),就完成了数据输出第一个stage的完整分析。
最后只要在不同的encoder中堆叠不同个数的block就可以构建出pvt_tiny、pvt_small、pvt_medium、pvt_large了。
完整图示如下:
在这里插入图片描述

SRA

未完待续…

feed forward

未完待续…

举报

相关推荐

0 条评论