Transformer学习记录(7):Vision Transformer

背景

Transformer模型最初是使用在NLP中,但近几年Transformer模型在图像上的使用越来越频繁,最新的模型也出现了很多基于Transfomer的,而其中经典的是Vision Transformer(ViT),它是用于图像分类的,这里就以ViT-B/16这个模型来学习Transformer模型是如何在图像领域使用的。

整体框架

ViT的网络结构如图所示,可以将ViT分为三个部分:

  1. Embedding层:对原始的图像数据进行处理,转换为embedding(向量)序列,使其符合Transformer Encoder的输入要求
  2. Transformer Encoders层:包含多个Encoder Block,对输入的token进行处理
  3. MLP Head层:用于分类的层结构

在这里插入图片描述

各个部分的详解

Embedding层

对于标准的Transformer模块,要输入的是embedding(向量)序列,也就是二维矩阵[embedding_token,embedding_dim(向量的长度)],以ViT-B/16为例,embedding的长度为768。

而对于图像数据而言,其格式为[H,W,C],显然不是Transformer模块需要的,因此需要通过一个Embedding层来对数据格式进行转换

操作过程如下图所示,以ViT-B/16为例,首先将输入图像(224x224)切分为196个16x16的Patch,这些Patch的格式为[16,16,3]。接下来将这些Patch进行线性映射,映射到潜在空间,也就是转换为一维的向量:[16,16,3]-->[768]。不同ViT中patch映射的向量长度不同

在这里插入图片描述
在代码实现中,直接使用一个卷积层来实现卷积核大小为patch大小,卷积核个数为embed_dim的维度数,不同种类的ViT的值不同。若输入数据为[B,C,H,W],经过卷积并展平后,数据尺寸变为[B,embed_dim,num_patches],而为了符合输入要求,需要将第2与3维进行转置,变为[B,num_patches,embed_dim]
以ViT-B/16为例,使用一个卷积核为16x16,步距为16,卷积核个数为768的卷积层来实现。通过卷积层会得到[224,224,3]-->[14,14,768],然后在通过Flatten操作将[H,W]展平即可[14, 14, 768] -> [196, 768],此时正好为二维矩阵,符合Transformer模块的需要。
在这里插入图片描述
在输入到Transformer模块前,需要加上[class]token(token所属的类别)与位置编码Position Embedding

  • [class]token:在刚刚得到的一堆tokens中插入一个专门用于分类的[class]token,这个[class]token是一个可训练的参数,数据格式和其他token一样都是一个向量,以ViT-B/16为例,就是一个长度为768的向量,与之前从图片中生成的tokens拼接在一起Cat([1, 768], [196, 768]) -> [197, 768],增加了一个向量。
  • Position Embedding:就是之前Transformer中讲到的Positional Encoding,这里的Position Embedding采用的是一个可训练的参数,这里是直接叠加(add)到token上。以ViT-B/16为例,刚刚拼接[class]token后shape是[197, 768],那么这里的Position Embedding的shape也是[197, 768]

在这里插入图片描述

Embedding 层的整体结构如图所示
在这里插入图片描述

Transformer Encoder层

Transformer Encoder其实就是重复堆叠Encoder Block L次,Encoder Block由以下部分构成:

  • Layer Norm:对每个token进行标准化处理
  • Multi-Head Attention链接
  • Dropout/DropPath防止过拟合,在原论文的代码中是直接使用的Dropout层,但也有人使用DropPath
  • MLP Block:如图右侧所示,由全连接层(Liner)+GELU激活函数+DropOut层构成,需要注意的是第一个全连接层会将节点个数翻4倍[197,768]-->[197,3072],而第二个全连接层会将节点个数恢复原样[197,3072]-->[197,768],这样经过MPL Block不会修改数据的结构。

通过Transformer Encoder后输出的shape和输入的shape是保持不变的,以ViT-B/16为例,输入的是[197, 768]输出的还是[197, 768]
在这里插入图片描述
而在代码实现中,会在Transformer模块前面加一个Dropout层后面加一个Layer Norm层
在这里插入图片描述

MLP Head

由于只需要分类的信息,因此只需要将[class]token生成的对应结果提取出来即可,即[197, 768]中抽取出[class]token对应的[1, 768],最后在使用MLP Head得到最终的分类结果。

在论文中,训练ImageNet21K(大型数据集)时,MLP Head是由Linear+tanh激活函数+Linear组成。但是迁移到ImageNet1K上或者你自己的数据上时,只用一个Linear即可。

以ViT-B/16为例
在这里插入图片描述

不同种类的ViT

论文中提供了三种ViT模型的参数

  • Layers:Transformer模块中Encoder模块的个数
  • Hidden Size:经过Embedding层后每个token的长度
  • MLP Size:MLP Block中第一个全连接层的输出节点个数,是Hidden Size的四倍。
  • Heads:Multi-Head Attention的heads数。

在这里插入图片描述

代码实现

Embedding层

在这里插入图片描述
在代码实现时,要注意输入数据的格式,一般的格式为(B,C,H,W),但后面需要的数据格式为(B,HW,C),即二三维需要调换,因此需要进行转置操作。

class Patch_Embedding(nn.Module):
    def __init__(self, image_size=224, in_channel=3, embed_dim=768, patch_size=16, dropout=0.1):
        """
        嵌入层
        @param image_size: 图像大小
        @param in_channel: 输入通道数
        @param embed_dim: 输出向量的维度
        @param patch_size: patch的大小
        @param dropout: dropout操作
        """
        super(self,Patch_Embedding).__init__()
        self.patcher = nn.Sequential(
            nn.Conv2d(
                in_channels=in_channel,
                out_channels=embed_dim,
                kernel_size=patch_size,
                stride=patch_size
            ),  # 通过卷积操作,提取出各个patch,并转为向量,(B,C,H,W)-->(B,embed_dim,num_patch/2,num_patch/2)
            nn.Flatten(2)  # 将patch的维度展平,-->(B,embed_dim,num_patch)
        )
        self.class_token = nn.Parameter(torch.randn(size=(1, 1, embed_dim)))  # 随机初始化class_token
        self.num_patch = (image_size // patch_size) * (image_size // patch_size)  # patch的个数
        self.position_embedding = nn.Parameter(torch.randn(size=(1,self.num_patch+1,embed_dim)))  # 随机初始化位置编码
        self.dropout = nn.Dropout(p=dropout)

    def forward(self,x):
        B,C,H,W = x.shape
        x = self.patcher(x)  # 通过卷积得到各个patch,并展平
        x = x.permute(0,2,1)  # 交换 (B,embed_dim,num_patch)-->(B,num_patch,embed_dim)
        x = torch.cat((x,self.class_token),dim=1)  # 与class_token拼接
        x = x + self.position_embedding  # 与位置编码相加
        return x

Transformer Encoder层

在这里插入图片描述

在这里插入图片描述

有两种实现方法:1.使用pytorch中的API;2.手动搭建

① 使用pytorch中的API
在pytorch中,有两个API:nn.TransformerEncoderLayernn.TransformerEnoder

其中TransformerEncoderLayer相当于图中的Encoder Block,而TransformerEncoder包含所有Encoder Block
因此使用时需要先构造出TransformerLayer对象,设置好参数,再使用TransformerLayer对象出构造TransformerEncoder

在这里插入图片描述
参数:

  • d-model(int)输入数据的特征维度,在vit中也就是第三维的数据个数
  • nhead(int):多头自注意力机制中,head的个数
  • dim_feedforward(int)前馈网络中隐藏层的维度,默认为2048
  • dropout(float):dropout的比例,默认为0.1
  • activation(str):使用的激活函数,如‘“relu”与“gelu”,默认为“relu”
  • layer_norm_eps (float) :在layer normalization中使用的,默认为1e-5
  • batch_first (bool):若为True,则在输入与输出的数据中,batch在第一个维度,反之在第二个维度。默认为Flase
  • norm_first (bool):是否在执行起始的layer normalization若为True则执行,默认为False
  • bias (bool):Liner与layer normalization是否使用bias。若为True则使用,默认为Flase。

在这里插入图片描述
参数:

  • encoder_layer (TransformerEncoderLayer) :传入TransformerEncoderLayer对象
  • num_layers(int):包含多少个Encoder Block
  • norm (Optional[Module]): layer normalization使用的模块,默认为None
self.TransformerEnoderLayer = nn.TransformerEncoderLayer(  # Encoder Block
     d_model=embed_dim,
     nhead=num_head,
     dropout=dropout,
     activation=activation,
     batch_first = True,
     norm_first = True,
)
self.TransformerEncoder = nn.TransformerEncoder(self.TransformerEnoderLayer,num_layers)  # Encoder

② 自己实现:
首先要实现Attention与MLP block,并构成Transformer Encoder Block。

Multi Attention

首先要初始化要使用的各种参数与层,其中使用一个全连接层来获得QKV,其输出维度为输入的3倍

    def __init__(self,
                 dim,  # 输入token的维度数,也就是自注意力中输入a的维度
                 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  # 每个head分到的维度数,一般是均分
        self.scale = qk_scale or head_dim ** -0.5  # 一般是根号(head_dim),对应注意力的公式
        self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)  # 通过一个全连接层生成QKV,将维度数*3
        self.attn_drop = nn.Dropout(attn_drop_ratio)
        self.proj = nn.Linear(dim, dim)  # 作为Wo,在融合结果时使用
        self.proj_drop = nn.Dropout(proj_drop_ratio)

(1)获得QKV:
在实现过程中,首先要获得QKV,这里使用了一个全连接层得到QKV。全连接层的输出:[B,num_patch+1,3*embed_dim]
然后需要从中提取出各个head的QKV,要经过一下步骤:

  1. reshape:[B,num_patch+1,3*embed_dim] -> [B, num_patch + 1, 3, num_head, embed_dim_per_head]
    也就是将3*embed_dim拆分成各个head的数据。
  2. permute:[B, num_patch + 1, 3, num_head, embed_dim_per_head]-> [3, B, num_head, num_patch + 1, embed_dim_per_head]
    调整顺序,将3放到前面,方便对QKV进行处理与提取;将num_heads与N调换,让N与数据放在一起,这样方便使用每个head的数据
  3. q, k, v = qkv[0], qkv[1], qkv[2]:提取各个head的QKV,每个的结构为[B, num_head, num_patch + 1, embed_dim_per_head]
    def forward(self, x):
        # [batch_size, num_patches + 1, total_embed_dim]
        # num_patches:划分出的patch个数,加1是加上class_token
        # embed_dim:经过embedding层后得到
        B, N, C = x.shape
        '''
        1.得到QKV
        先将输入数据复制三份,然后按照QKV中的每个head进行均分
        '''
        # qkv(): -> [batch_size, num_patches + 1, 3 * total_embed_dim] 维度数*3
        # reshape: -> [batch_size, num_patches + 1, 3, num_heads, embed_dim_per_head]
        # 将最后一维的数据进行拆分,拆分为QKV:
        # 3对应着QKV,num_heads对应着每个head,embed_dim_per_head对应着每个head中qkv的dim
        # permute: -> [3, batch_size, num_heads, num_patches + 1, embed_dim_per_head]
        # 调整数据中的顺序,将3放到前面,方便对QKV进行处理与提取;
        # 将num_heads与N调换,让N与数据放在一起,这样方便使用每个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]
        # 提取出QKV
        q, k, v = qkv[0], qkv[1], qkv[2]  # make torchscript happy (cannot use tensor as tuple)

提取出QKV后,要根据公式进行计算
在这里插入图片描述
(2)计算QK相乘:
Q与K后两维的数据进行相乘,得到[B, num_heads, num_patches + 1, num_patches + 1]。然后再继续softmax操作与下采样操作。

'''
2.计算Q*K的转置,计算attention
'''
 # transpose: -> [batch_size, num_heads, embed_dim_per_head, num_patches + 1] 对K进行转置,转置最后两维
# @: multiply -> [batch_size, num_heads, num_patches + 1, num_patches + 1]
attn = (q @ k.transpose(-2, -1)) * self.scale  # QK矩阵相乘,并进行scale操作。各个head中的qk相乘
# 对QK的结果运行softmax操作,也就是对结果的每一行进行softmax:dim=-1代表对每一行的数据进行?
attn = attn.softmax(dim=-1) 
# 最后进行一个下采样,得到V的权重 
attn = self.attn_drop(attn)  

(3)计算与V相乘:
得到[B, num_heads, num_patches + 1, embed_dim_per_head]
然后通过transpose与reshape要将尺寸恢复为原样,也就是[B, num_heads, num_patches + 1, embed_dim_per_head]->[B, num_patches + 1, total_embed_dim]

最后再将数据输入到全连接层进行融合

'''
3.与V相乘
 '''
# @: multiply -> [batch_size, num_heads, num_patches + 1, embed_dim_per_head]
x = (attn @ v)  # 与V相乘
# 将数据格式恢复为原样,注意力不会改变数据的尺寸
# transpose: -> [batch_size, num_patches + 1, num_heads, embed_dim_per_head]
# 将数据转变为对于每个patch中,各个head得到的结果
# reshape: -> [batch_size, num_patches + 1, total_embed_dim]
# 对最后两维的数据进行拼接,也就是对每个head的结果进行concat拼接
x = x.transpose(1, 2).reshape(B, N, C)
# 将拼接后的结果进行融合,也就是*Wo,最终得到每个patch的结果
x = self.proj(x)
x = self.proj_drop(x)
return x

完整代码

class Attention(nn.Module):
    def __init__(self,
                 dim,  # 输入token的维度数,也就是自注意力中输入a的维度
                 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  # 每个head分到的维度数,一般是均分
        self.scale = qk_scale or head_dim ** -0.5  # 一般是根号(head_dim),对应注意力的公式
        self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)  # 通过一个全连接层生成QKV,将维度数*3
        self.attn_drop = nn.Dropout(attn_drop_ratio)
        self.proj = nn.Linear(dim, dim)  # 作为Wo,在融合结果时使用
        self.proj_drop = nn.Dropout(proj_drop_ratio)

    def forward(self, x):
        # [batch_size, num_patches + 1, total_embed_dim]
        # num_patches:划分出的patch个数,加1是加上class_token
        # embed_dim:经过embedding层后得到
        B, N, C = x.shape

        '''
        1.得到QKV
        先将输入数据复制三份,然后按照QKV中的每个head进行均分
        '''
        # qkv(): -> [batch_size, num_patches + 1, 3 * total_embed_dim] 维度数*3
        # reshape: -> [batch_size, num_patches + 1, 3, num_heads, embed_dim_per_head]
        # 将最后一维的数据进行拆分,拆分为QKV,3对应着QKV,num_heads对应着每个head,embed_dim_per_head对应着每个head中qkv的dim
        # permute: -> [3, batch_size, num_heads, num_patches + 1, embed_dim_per_head]
        # 调整数据中的顺序,将3放到前面,方便对QKV进行处理与提取;将num_heads与N调换,让N与数据放在一起,这样方便使用每个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]
        # 提取出QKV
        q, k, v = qkv[0], qkv[1], qkv[2]  # make torchscript happy (cannot use tensor as tuple)

        '''
        2.计算Q*K的转置,计算attention
        '''
        # transpose: -> [batch_size, num_heads, embed_dim_per_head, num_patches + 1] 对K进行转置,转置最后两维
        # @: multiply -> [batch_size, num_heads, num_patches + 1, num_patches + 1]
        attn = (q @ k.transpose(-2, -1)) * self.scale  # QK矩阵  相乘,并进行scale操作。各个head中的qk相乘
        attn = attn.softmax(dim=-1)  # 对QK的结果运行softmax操作,也就是对结果的每一行进行softmax:dim=-1代表对每一行的数据进行?
        attn = self.attn_drop(attn)  # 最后进行一个下采样,得到V的权重

        '''
        3.与V相乘
        '''
        # @: multiply -> [batch_size, num_heads, num_patches + 1, embed_dim_per_head]
        x = (attn @ v)  # 与V相乘
        # 将数据格式恢复为原样,注意力不会改变数据的尺寸
        # transpose: -> [batch_size, num_patches + 1, num_heads, embed_dim_per_head]
        # 将数据转变为对于每个patch中,各个head得到的结果
        # reshape: -> [batch_size, num_patches + 1, total_embed_dim]
        # 对最后两维的数据进行拼接,也就是对每个head的结果进行concat拼接
        x = x.transpose(1, 2).reshape(B, N, C)
        # 将拼接后的结果进行融合,也就是*Wo,最终得到每个patch的结果
        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  # 一般为in_features
        hidden_features = hidden_features or in_features  # 一般是in_features*4
        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

Encoder Block

在这里插入图片描述

class Block(nn.Module):
    def __init__(self,
                 dim,
                 num_heads,  # head个数
                 mlp_ratio=4.,  # MLP中hidden_dim与in_dim的倍数关系
                 qkv_bias=False,
                 qk_scale=None,
                 drop_ratio=0.,  # 作用于Attention与MLP
                 attn_drop_ratio=0.,  # attention得到softmax(QK)后的dropout层
                 drop_path_ratio=0.,  # Encoder Block中使用的两个droppath层
                 act_layer=nn.GELU,
                 norm_layer=nn.LayerNorm):
        super(Block, self).__init__()

        self.norm1 = norm_layer(dim)  # 第一个norm层
        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)  # 第二个norm层
        mlp_hidden_dim = int(dim * mlp_ratio)  # MLP中通道数翻倍的数量
        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

Transformer Encoder

self.blocks = nn.Sequential(*[
	# 使用循环,加入各个Encoder Block,构造Transformer Encoder
	 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)  # 包含depth个Encoder Block
])

Classifier

在这里插入图片描述

'''
MLP Head
'''
x = self.norm(x)  # [B,197,768]
if self.dist_token is None:
	# 提取class_token x[:,0]=[B,embed_dim],在第二维度中提取
	return self.pre_logits(x[:, 0])  # [B,768]
else:  # 兼容 DeiT
	return x[:, 0], x[:, 1]
'''
MLP Head
'''
if self.head_dist is not None:  # 用于兼容DeiT模型
	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)  # 输入到分类头,得到分类结果 [B,768]->[B,num_classes]
return x

CNN与Transformer的混合网络:Hybrid

Hybrid将传统CNN的特征提取和Transformer进行结合。
下图是以ResNet50作为特征提取器的Hybrid的网络结构,与ViT区别在于开始阶段会使用ResNet50网络进行特征提取。但这里的ResNet50与普通的ResNet50不同。

首先这里的R50的卷积层采用的StdConv2d不是传统的Conv2d,然后将所有的BatchNorm层替换成GroupNorm层
而且在原Resnet50网络中,stage1重复堆叠3次,stage2重复堆叠4次,stage3重复堆叠6次,stage4重复堆叠3次。但是在这里的R50中,stage4的3次被集成到stage3中,因此stage3堆叠了9次

通过R50 Backbone进行特征提取后,得到的特征矩阵为[14,14,1024],然后输入到Embedding层,要注意的是这个Embedding层中的卷积变为卷积核为1x1,步长为1只是调整了channel为768

后面的处理与ViT一样。

在这里插入图片描述

模型训练时一些注意事项

ViT在训练过程中,一般会进行预训练或加载已经训练好的权重,来减少训练的要求,接着针对任务进行模型的微调。
而在微调过程中,部分层是不需要训练的。越靠前的Transformer层越倾向于提取特征,而越靠后的则倾向于完成分类任务。
提取特征是一个客观的需求,无论任务如何,提取特征大差不差,因此在微调过程中,前面的层是可以冻结的。而后面的层是针对于当前任务的,因此需要进行微调。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值