0


使用Pytorch手写ViT — VisionTransformer

《The Attention is all you need》的论文彻底改变了自然语言处理的世界,基于Transformer的架构成为自然语言处理任务的的标准。

尽管基于卷积的架构在图像分类任务中仍然是最先进的技术,但论文《An image is worth 16x16 words: transformer for image recognition at scale》表明,计算机视觉中CNNs的依赖也不是必要的,直接对图像进行分块,然后使用序纯transformer可以很好地完成图像分类任务。

在ViT中,图像被分割成小块,并将这些小块的线性嵌入序列作为Transformer的输入。对图像进行补丁处理方式与NLP应用程序中的标记(单词)相同。

由于缺乏 CNN 固有的归纳偏差(如局部性),Transformers 在数据量不足的情况下不能很好地泛化。但是当在大型数据集上进行训练时,它在多个图像识别基准上确实达到或超过了最先进的水平。在深入本文之前,如果你从未听说过 Transformer 架构,我强烈建议你查看 The Illustrated Transformer。

在开始实现之前,我们先看看ViT架构

可以看到输入图像被分解成 16x16 的扁平化块,然后使用普通的全连接层对这些块进行嵌入操作,并在它们前面包含特殊的 cls token 和位置嵌入。

线性投影的张量被传递给标准的 Transformer 编码器,最后传递给 MLP 头,用于分类目的。

首先我们从导入库开始,一步一步实现论文中提到的ViT模型:

  1. import matplotlib.pyplot as plt
  2. from PIL import Image
  3. import torch
  4. import torch.nn.functional as F
  5. from torch import Tensor, nn
  6. from torchsummary import summary
  7. from torchvision.transforms import Compose, Resize, ToTensor
  8. from einops import rearrange, reduce, repeat
  9. from einops.layers.torch import Rearrange, Reduce

为了调试我们的模型,还需要一张图片来进行测试:

  1. img = Image.open('penguin.jpg')
  2. fig = plt.figure()
  3. plt.imshow(img)
  4. plt.show()

图片还需要一些预处理:

  1. transform = Compose([
  2. Resize((224, 224)),
  3. ToTensor(),
  4. ])
  5. x = transform(img)
  6. x = x.unsqueeze(0)
  7. print(x.shape)

通过上面的预处理,我们的张量大小为torch.Size([1,3,224,224])。接下来,我们开始按照论文实现ViT。

切分补丁和投影

将图像分成多个补丁,并将它们展平。

以下是论文的原话:

我们可以很容易地使用 einops 来实现它。

  1. patch_size = 16
  2. patches = rearrange(x, 'b c (h s1) (w s2) -> b (h w) (s1 s2 c)', s1=patch_size, s2=patch_size)

下一步是对它们进行投影:这可以使用标准线性层轻松实现,但本文中使用卷积层(使用 kernel_size 和 stride 等于 patch_size 获得的,这压根以提高性能)。让我们在 PatchEmbedding 类中解决问题。

  1. class PatchEmbedding(nn.Module):
  2. def __init__(self, in_channels: int = 3, patch_size: int = 16, emb_size: int = 768):
  3. self.patch_size = patch_size
  4. super().__init__()
  5. self.projection = nn.Sequential(
  6. nn.Conv2d(in_channels, emb_size, kernel_size=patch_size, stride=patch_size),
  7. Rearrange('b e (h) (w) -> b (h w) e'),
  8. ) # this breaks down the image in s1xs2 patches, and then flat them
  9. def forward(self, x: Tensor) -> Tensor:
  10. x = self.projection(x)
  11. return x

为了测试我们的代码,可以调用 PatchEmbedding()(x).shape ,得到:

  1. torch.Size([1, 196, 768])

CLS 令牌和位置嵌入

与 BERT 的分类令牌类似,一个可学习的嵌入被预先添加到嵌入补丁的序列中。然后将位置嵌入添加到补丁嵌入中以保留位置信息。这里使用标准可学习的一维位置嵌入。

  1. class PatchEmbedding(nn.Module):
  2. def __init__(self, in_channels: int = 3, patch_size: int = 16, emb_size: int = 768):
  3. self.patch_size = patch_size
  4. super().__init__()
  5. self.projection = nn.Sequential(
  6. nn.Conv2d(in_channels, emb_size, kernel_size=patch_size, stride=patch_size),
  7. Rearrange('b e (h) (w) -> b (h w) e'),
  8. ) # this breaks down the image in s1xs2 patches, and then flat them
  9. self.cls_token = nn.Parameter(torch.randn(1,1, emb_size))
  10. self.positions = nn.Parameter(torch.randn((img_size // patch_size) **2 + 1, emb_size))
  11. def forward(self, x: Tensor) -> Tensor:
  12. b, _, _, _ = x.shape
  13. x = self.projection(x)
  14. cls_tokens = repeat(self.cls_token, '() n e -> b n e', b=b)
  15. x = torch.cat([cls_tokens, x], dim=1) #prepending the cls token
  16. x += self.positions
  17. return x

这样生成的嵌入向量序列用将作编码器的输入。

Transformer 编码器 (Vaswani et al., 2017) 由多头自注意力和 MLP 块的交替层组成。在每个块之前应用Layer Norm (LN),并在每个块之后添加残差连接。

注意力机制

注意力机制需要三个输入:查询、键和值。然后它使用查询和键计算注意力矩阵。

这里将实现一个多头注意力机制,主要概念是使用查询和键之间的乘积来了解序列中的每个元素对其余元素的重要性。稍后将使用这些信息对值进行缩放。可以为查询、键和值矩阵使用 3 个不同的线性层,也可以将它们融合为一个。

  1. class MultiHeadAttention(nn.Module):
  2. def __init__(self, emb_size: int = 768, num_heads: int = 8, dropout: float = 0):
  3. super().__init__()
  4. self.emb_size = emb_size
  5. self.num_heads = num_heads
  6. self.qkv = nn.Linear(emb_size, emb_size * 3) # queries, keys and values matrix
  7. self.att_drop = nn.Dropout(dropout)
  8. self.projection = nn.Linear(emb_size, emb_size)
  9. def forward(self, x : Tensor, mask: Tensor = None) -> Tensor:
  10. # split keys, queries and values in num_heads
  11. qkv = rearrange(self.qkv(x), "b n (h d qkv) -> (qkv) b h n d", h=self.num_heads, qkv=3)
  12. queries, keys, values = qkv[0], qkv[1], qkv[2]
  13. # sum up over the last axis
  14. energy = torch.einsum('bhqd, bhkd -> bhqk', queries, keys) # batch, num_heads, query_len, key_len
  15. if mask is not None:
  16. fill_value = torch.finfo(torch.float32).min
  17. energy.mask_fill(~mask, fill_value)
  18. scaling = self.emb_size ** (1/2)
  19. att = F.softmax(energy, dim=-1) / scaling
  20. att = self.att_drop(att)
  21. out = torch.einsum('bhal, bhlv -> bhav ', att, values) # sum over the third axis
  22. out = rearrange(out, "b h n d -> b n (h d)")
  23. out = self.projection(out)
  24. return out

残差连接

从上图中可以看出,transformer 块有残差连接。

我们可使用一个包装器来执行残差加法,这样可以复用:

  1. class ResidualAdd(nn.Module):
  2. def __init__(self, fn):
  3. super().__init__()
  4. self.fn = fn
  5. def forward(self, x, **kwargs):
  6. res = x
  7. x = self.fn(x, **kwargs)
  8. x += res
  9. return x

注意力块的输出被传递到一个全连接层。最后一层由两层组成,它们通过因子 L 进行上采样:

  1. class FeedForwardBlock(nn.Sequential):
  2. def __init__(self, emb_size: int, L: int = 4, drop_p: float = 0.):
  3. super().__init__(
  4. nn.Linear(emb_size, L * emb_size),
  5. nn.GELU(),
  6. nn.Dropout(drop_p),
  7. nn.Linear(L * emb_size, emb_size),
  8. )

Transformer 编码器块

上面的分块步骤都已经完成了,下面我们将这些块整合成编码器:

  1. class TransformerEncoderBlock(nn.Sequential):
  2. def __init__(self, emb_size: int = 768, drop_p: float = 0., forward_expansion: int = 4,
  3. forward_drop_p: float = 0.,
  4. **kwargs):
  5. super().__init__(
  6. ResidualAdd(nn.Sequential(
  7. nn.LayerNorm(emb_size),
  8. MultiHeadAttention(emb_size, **kwargs),
  9. nn.Dropout(drop_p)
  10. )),
  11. ResidualAdd(nn.Sequential(
  12. nn.LayerNorm(emb_size),
  13. FeedForwardBlock(
  14. emb_size, L=forward_expansion, drop_p=forward_drop_p),
  15. nn.Dropout(drop_p)
  16. )
  17. ))

要测试这部分代码,可以直接调用:

  1. patches_embedded = PatchEmbedding()(x)
  2. print(TransformerEncoderBlock()(patches_embedded).shape)

这样会返回 torch.Size([1,197,768])

Transformer编码器

因为只需要编码器,所以可以使用上面编写的 TransformerEncoderBlock 进行构建

  1. class TransformerEncoder(nn.Sequential):
  2. def __init__(self, depth: int = 12, **kwargs):
  3. super().__init__(*[TransformerEncoderBlock(**kwargs) for _ in range(depth)])

分类头

因为ViT是分类任务,所以最后要有一个进行分类人物的分类头,这个非常简单:计算整个序列的简单平均值之后是一个标准的全连接,它给出了类概率。

  1. class ClassificationHead(nn.Sequential):
  2. def __init__(self, emb_size: int = 768, n_classes: int = 1000):
  3. super().__init__(
  4. Reduce('b n e -> b e', reduction='mean'),
  5. nn.LayerNorm(emb_size),
  6. nn.Linear(emb_size, n_classes))

整合所有的组件——VisionTransformer

将我们上面构建的所有内容整合,最终就可以得到 ViT 了。

  1. class ViT(nn.Sequential):
  2. def __init__(self,
  3. in_channels: int = 3,
  4. patch_size: int = 16,
  5. emb_size: int = 768,
  6. img_size: int = 224,
  7. depth: int = 12,
  8. n_classes: int = 1000,
  9. **kwargs):
  10. super().__init__(
  11. PatchEmbedding(in_channels, patch_size, emb_size, img_size),
  12. TransformerEncoder(depth, emb_size=emb_size, **kwargs),
  13. ClassificationHead(emb_size, n_classes)
  14. )

查看我们构建的模型,可以使用 torchsummary 来检查结果:

  1. print(summary(ViT(), (3,224,224), device='cpu'))

将得到:

  1. ================================================================
  2. Total params: 86,415,592
  3. Trainable params: 86,415,592
  4. Non-trainable params: 0
  5. ----------------------------------------------------------------
  6. Input size (MB): 0.57
  7. Forward/backward pass size (MB): 364.33
  8. Params size (MB): 329.65
  9. Estimated Total Size (MB): 694.56
  10. ----------------------------------------------------------------

总结

本篇文章使用 Pytorch 中实现 Vision Transformer,通过我们自己的手动实现可以更好的理解ViT的架构,为了加深印象我们再看下论文中提供的与现有技术的比较:

本文代码:https://github.com/alessandrolamberti/ViT

作者:Alessandro Lamberti

“使用Pytorch手写ViT — VisionTransformer”的评论:

还没有评论