本文是博主跟着b站up霹雳吧啦Wz的视频稿件学习的自我记录,图片均为该视频截图。
代码来源timm库(PyTorchImageModels,简称timm)是一个巨大的PyTorch代码集合,已经被官方使用了。
放一些链接:up霹雳吧啦Wz针对ViT写的博客,论文原文链接,timm库作者的GitHub主页,timm库链接,timm库的官方指南,以及一个非官方的timm库的推荐文章。
模型示意图(Base16为例)
图片来自视频截图和论文截图
PatchEmbed模块
class PatchEmbed(nn.Module):
""" 2D Image to Patch Embedding
"""
def __init__(self, img_size=224, patch_size=16, in_chans=3, embed_dim=768, norm_layer=None, flatten=True):
super().__init__()
img_size = to_2tuple(img_size)
patch_size = to_2tuple(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]) #grid_size=224÷16=14
self.num_patches = self.grid_size[0] * self.grid_size[1]
#num_patches=14*14
self.flatten = flatten
self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_size, stride=patch_size)
#proj使用卷积,embed_dimension这一参数在vision transformer的base16模型用到的是768,所以默认是768。但是如果是large或者huge模型的话embed_dim也会变。
self.norm = norm_layer(embed_dim) if norm_layer else nn.Identity()
#norm_layer默认是None,就是进行nn.Identity()也就是不做任何操作;如果有传入(非None),则会进行初始化一个norm_layer。
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]})."
#assert:进行判断,如果代码模型定义和实际输入尺寸不同则会报错
x = self.proj(x) #用卷积实现序列化
if self.flatten:
x = x.flatten(2).transpose(1, 2) # BCHW -> BNC
#flatten(2)操作实现了[B,C,H,W,]->[B,C,HW],指从维度2开始进行展平
#transpose(1,2)操作实现了[B,C,HW]->[B,HW,C]
x = self.norm(x)
#通过norm层输出
return x
Attention模块
该模块实现多头注意力机制。
class Attention(nn.Module):
def __init__(self,
dim, #输入token的dim
num_heads=8, #多头注意力中head的个数
qkv_bias=False, #在生成qkv时是否使用偏置,默认否
attn_drop=0.,
proj_drop=0.):
super().__init__()
self.num_heads = num_heads
head_dim = dim // num_heads #计算每一个head需要传入的dim
self.scale = head_dim ** -0.5 #head_dim的-0.5次方,即1/根号d_k,即理论公式里的分母根号d_k
self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias) #qkv是通过1个全连接层参数为dim和3dim进行初始化的,也可以使用3个全连接层参数为dim和dim进行初始化,二者没有区别,
self.attn_drop = nn.Dropout(attn_drop)#定义dp层 比率attn_drop
self.proj = nn.Linear(dim, dim) #再定义一个全连接层,是 将每一个head的结果进行拼接的时候乘的那个矩阵W^O
self.proj_drop = nn.Dropout(proj_drop)#定义dp层 比率proj_drop
def forward(self, x):#正向传播过程
#输入是[batch_size,
# num_patches+1, (base16模型的这个数是14*14)
# total_embed_dim(base16模型的这个数是768)]
B, N, C = x.shape
qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4)
#qkv->[batchsize, num_patches+1, 3*total_embed_dim]
#reshape->[batchsize, num_patches+1, 3, num_heads, embed_dim_per_head]
#permute->[3, batchsize, 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)
#q、k、v大小均[batchsize, num_heads, num_patches+1, embed_dim_per_head]
attn = (q @ k.transpose(-2, -1)) * self.scale
#现在的操作都是对每个head进行操作
#transpose是转置最后2个维度,@就是矩阵乘法的意思
#q [batchsize, num_heads, num_patches+1, embed_dim_per_head]
#k^T[batchsize, num_heads, embed_dim_per_head, num_patches+1]
#q*k^T=[batchsize, num_heads, num_patches+1, num_patches+1]
#self.scale=head_dim的-0.5次方
#至此完成了(Q*K^T)/根号d_k的操作
attn = attn.softmax(dim=-1)
#dim=-1表示在得到的结果的每一行上进行softmax处理,-1就是最后1个维度
#至此完成了softmax[(Q*K^T)/根号d_k]的操作
attn = self.attn_drop(attn)
x = (attn @ v).transpose(1, 2).reshape(B, N, C)
#@->[batchsize, num_heads, num_patches+1, embed_dim_per_head]
#这一步矩阵乘积就是加权求和
#transpose->[batchsize, num_patches+1, num_heads, embed_dim_per_head]
#reshape->[batchsize, num_patches+1, num_heads*embed_dim_per_head]即[batchsize, num_patches+1, total_embed_dim]
#reshape实际上就实现了concat拼接
x = self.proj(x)
#将上一步concat的结果通过1个线性映射,通常叫做W,此处用全连接层实现
x = self.proj_drop(x)
#dropout
#至此完成了softmax[(Q*K^T)/根号d_k]*V的操作
#一个head的attention的全部操作就实现了
return x
MLP Block(图中的名字)/FeedForward类(代码中的实现)
class FeedForward(nn.Module):
#全连接层1+GELU+dropout+全连接层2+dropout
#全连接层1的输出节点个数是输入节点个数的4倍,即mlp_ratio=4.
#全连接层2的输入节点个数是输出节点个数的1/4
def __init__(self, dim, hidden_dim, dropout = 0.):
super().__init__()
self.net = nn.Sequential(
nn.Linear(dim, hidden_dim),
nn.GELU(),
nn.Dropout(dropout),
nn.Linear(hidden_dim, dim),
nn.Dropout(dropout)
)
def forward(self, x):
return self.net(x)
Encoder Block主模块
实现了对EncoderBlock重复堆叠L次的时候的每一次的结构,如下:
左图为视频截图,右图为原论文截图
class Block(nn.Module):
def __init__(self,
dim,
num_heads,
mlp_ratio=4.,
qkv_bias=False,
drop=0.,
#多头注意力模块中的最后的全连接层之后的dropout层对应的drop比率
attn_drop=0.,
#多头注意力模块中softmax[Q*K^T/根号d_k]之后的dropout层的drop比率
drop_path=0.,
#本代码用到的是DropPath方法(上面右图的DropPath),所以上面右图的两个droppath层有这个比率
act_layer=nn.GELU,
norm_layer=nn.LayerNorm):
super().__init__()
self.norm1 = norm_layer(dim)
#第一层LN
self.attn