最近比较火,找了好多资料和源码研究了下,写下这篇读后感
在CV领域是怎么用呢?
先说注意力机制,注意力机制对人类来说其实很好理解,假如给定一张图片,我们会自动聚焦到一些关键信息位置,而不需要逐行扫描全图,此处的attention,其本质是对输入的自适应加权,senet模型中的se模块就是典型例子
se模块最终学出来是一个1X1xC的向量,然后逐通道乘以原始输入,即通道注意力。
其实都可以抽像成
# 假设q是(1,N,512),N就是最大标签化后的list长度,k是(1,M,512),M可以等于N,也可以不相等
# (1,N,512) x (1,512,M)-->(1,N,M)
attn = torch.matmul(q, k.transpose(2, 3))
# softmax转化为概率,输出(1,N,M),表示q中每个n和每个m的相关性
attn=F.softmax(attn, dim=-1)
# (1,N,M) x (1,M,512)-->(1,N,512),V和k的shape相同
output = torch.matmul(attn, v)
作者:深度眸
链接:https://zhuanlan.zhihu.com/p/308301901
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
1 CNN backbone 处理的是 [B,3,H,W] 图像,经过层层提取特征,输出是[B,C,H,W],backbone 只做这一件事。
2 Encoder部分:
先用1X1 卷积,将通道数从C 压缩到D,得到新的特征图[B,D,H,W]
将[B,D,H,W]的空间维度(高和宽)压缩成一个维度,得到序列化数据【B,D,HW】
在得到了【B,D,H,W】特征图之后,正式输入Encoder之前,需要进行位置编码,因为自注意力需要有表示位置的信息,得到positional encoding 需要以下几步:
1 在自然语言中得到PE向量公式是
这个式子中d,就是序列化数据中的D,pos属于【1,HW】范围,表示token在序列中的位置,i取值范围【0,.,.,d/2】,所以pos=1,式子可以写成:
所以 PE的输出张量是 hw行,d列,reshape成【B,d,H,W】,表示这个特征图的任意一点(h,w)都有个位置编码。然后得到位置编码和输入加权
率先使用Transformer模型是Vit,因为输入必须是要序列,比如说输入是【3,256,256】,打算分成64patch,每个patch 是32x32,也就是最终输入变成【64,3072】,输入长度是64个图像序列,每个序列采用3072编码
使用的代码就是
x = rearrange(img, 'b c (h p1) (w p2) -> b (h w) (p1 p2 c)', p1=p, p2=p)
考虑到3072有点大,可以先降维
# 将3072变成dim,假设是1024
self.patch_to_embedding = nn.Linear(patch_dim, dim)
x = self.patch_to_embedding(x)
现在输入确定了,然后是位置编码也必不可少,这里比较简单,没有采用sincos编码,而是这只成可学习,效果差不多
# num_patches=64,dim=1024,+1是因为多了一个cls开启解码标志
self.pos_embedding = nn.Parameter(torch.randn(1, num_patches + 1, dim))
将输入向量和位置编码相加输入进编码器,编码器的前向过程:
self.transformer = Transformer(dim, depth, heads, mlp_dim, dropout)
输入是【b,64,1024】,输出也是一样的维度。
然后编码器后接分类器
self.mlp_head = nn.Sequential(
nn.LayerNorm(dim),
nn.Linear(dim, mlp_dim),
nn.GELU(),
nn.Dropout(dropout),
nn.Linear(mlp_dim, num_classes)
)
# 65个输出里面只需要第0个输出进行后续分类即可
self.mlp_head(x[:, 0])
整体的是
class ViT(nn.Module):
def __init__(self, *, image_size, patch_size, num_classes, dim, depth, heads, mlp_dim, channels=3, dropout=0.,emb_dropout=0.):
super().__init__()
# image_size输入图片大小 256
# patch_size 每个patch的大小 32
num_patches = (image_size // patch_size) ** 2 # 一共有多少个patch 8x8=64
patch_dim = channels * patch_size ** 2 # 3x32x32=3072
self.patch_size = patch_size # 32
# 1,64+1,1024,+1是因为token,可学习变量,不是固定编码
self.pos_embedding = nn.Parameter(torch.randn(1, num_patches + 1, dim))
# 图片维度太大了,需要先降维
self.patch_to_embedding = nn.Linear(patch_dim, dim)
# 分类输出位置标志,否则分类输出不知道应该取哪个位置
self.cls_token = nn.Parameter(torch.randn(1, 1, dim))
self.dropout = nn.Dropout(emb_dropout)
# 编码器
self.transformer = Transformer(dim, depth, heads, mlp_dim, dropout)
# 输出头
self.mlp_head = nn.Sequential(
nn.LayerNorm(dim),
nn.Linear(dim, mlp_dim),
nn.GELU(),
nn.Dropout(dropout),
nn.Linear(mlp_dim, num_classes)
)
def forward(self, img, mask=None):
p = self.patch_size
# 先把图片变成64个patch,输出shape=b,64,3072
x = rearrange(img, 'b c (h p1) (w p2) -> b (h w) (p1 p2 c)', p1=p, p2=p)
# 输出 b,64,1024
x = self.patch_to_embedding(x)
b, n, _ = x.shape
# 输出 b,1,1024
cls_tokens = repeat(self.cls_token, '() n d -> b n d', b=b)
# 额外追加token,变成b,65,1024
x = torch.cat((cls_tokens, x), dim=1)
# 加上位置编码1,64+1,1024
x += self.pos_embedding[:, :(n + 1)]
x = self.dropout(x)
x = self.transformer(x, mask)
# 分类head,只需要x[0]即可
# x = self.to_cls_token(x[:, 0])
x = x[:, 0]
return self.mlp_head(x)