

从零搭建Pytorch模型教程(三)搭建Transformer网络 - CV技术指南(公众号)
source link: https://www.cnblogs.com/wxkang/p/16150868.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

从零搭建Pytorch模型教程(三)搭建Transformer网络
前言本文介绍了Transformer的基本流程,分块的两种实现方式,Position Emebdding的几种实现方式,Encoder的实现方式,最后分类的两种方式,以及最重要的数据格式的介绍。
本文来自公众号CV技术指南的技术总结系列
欢迎关注公众号CV技术指南,专注于计算机视觉的技术总结、最新技术跟踪、经典论文解读、CV招聘信息。
在讲如何搭建之前,先回顾一下Transformer在计算机视觉中的结构是怎样的。这里以最典型的ViT为例。
如图所示,对于一张图像,先将其分割成NxN个patches,把patches进行Flatten,再通过一个全连接层映射成tokens,对每一个tokens加入位置编码(position embedding),会随机初始化一个tokens,concate到通过图像生成的tokens后,再经过transformer的Encoder模块,经过多层Encoder后,取出最后的tokens(即随机初始化的tokens),再通过全连接层作为分类网络进行分类。
下面我们就根据这个流程来一步一步介绍如何搭建一个Transformer模型。、
分块
目前有两种方式实现分块,一种是直接分割,一种是通过卷积核和步长都为patch大小的卷积来分割。
直接分割
直接分割即把图像直接分成多块。在代码实现上需要使用einops这个库,完成的操作是将(B,C,H,W)的shape调整为(B,(H/P *W/P),P*P*C)。
from einops import rearrange, repeat
from einops.layers.torch import Rearrange
self.to_patch_embedding = nn.Sequential(
Rearrange('b c (h p1) (w p2) -> b (h w) (p1 p2 c)', p1 = patch_height, p2 = patch_width),
nn.Linear(patch_dim, dim),
)

这里简单介绍一下Rearrange。
Rearrange用于对张量的维度进行重新变换排序,可用于替换pytorch中的reshape,view,transpose和permute等操作。举几个例子
#假设images的shape为[32,200,400,3]
#实现view和reshape的功能
Rearrange(images,'b h w c -> (b h) w c')#shape变为(32*200, 400, 3)
#实现permute的功能
Rearrange(images, 'b h w c -> b c h w')#shape变为(32, 3, 200, 400)
#实现这几个都很难实现的功能
Rearrange(images, 'b h w c -> (b c w) h')#shape变为(32*3*400, 200)

从这几个例子看可以看出,Rearrange非常简单好用,这里的b, c, h, w都可以理解为表示符号,用来表示操作变化。通过这几个例子似乎也能理解下面这行代码是如何将图像分割的。
Rearrange('b c (h p1) (w p2) -> b (h w) (p1 p2 c)', p1 = patch_height, p2 = patch_width)

这里需要解释的是,一个括号内的两个变量相乘表示的是该维度的长度,因此不要把"h"和"w"理解成图像的宽和高。这里实际上h = H/p1, w = W/p2,代表的是高度上有几块,宽度上有几块。h和w都不需要赋值,代码会自动根据这个表达式计算,b和c也会自动对应到输入数据的B和C。
后面的"b (h w) (p1 p2 c)"表示了图像分块后的shape: (B,(H/P *W/P),P*P*C)
这种方式在分块后还需要通过一层全连接层将分块的向量映射为tokens。
在ViT中使用的就是这种直接分块方式。
卷积分割
卷积分割比较容易理解,使用卷积核和步长都为patch大小的卷积对图像卷积一次就可以了。
self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_size, stride=patch_size)
x = self.proj(x).flatten(2).transpose(1, 2) # B Ph*Pw C

在swin transformer中即使用的是这种卷积分块方式。在swin transformer中卷积后没有再加全连接层。
Position Embedding
Position Embedding可以分为absolute position embedding和relative position embedding。
在学习最初的transformer时,可能会注意到用的是正余弦编码的方式,但这只适用于语音、文字等1维数据,图像是高度结构化的数据,用正余弦不合适。
在ViT和swin transformer中都是直接随机初始化一组与tokens同shape的可学习参数,与tokens相加,即完成了absolute position embedding。
在ViT中实现方式:
self.pos_embedding = nn.Parameter(torch.randn(1, num_patches + 1, dim))
x += self.pos_embedding[:, :(n + 1)]
#之所以是n+1,是因为ViT中选择随机初始化一个class token,与分块得到的tokens拼接。所以patches的数量为num_patches+1。

在swin transformer中的实现方式:
from timm.models.layers import trunc_normal_
self.absolute_pos_embed = nn.Parameter(torch.zeros(1, num_patches, embed_dim))
trunc_normal_(self.absolute_pos_embed, std=.02)

在TimeSformer中的实现方式:
self.pos_emb = torch.nn.Embedding(num_positions + 1, dim)

以上就是简单的使用方法,这种方法属于absolute position embedding。
还有更复杂一点的方法,以后有机会单独搞一篇文章来介绍。
感兴趣的读者可以先去看看这篇论文《ICCV2021 | Vision Transformer中相对位置编码的反思与改进》。
Encoder
Encoder由Multi-head Self-attention和FeedForward组成。
Multi-head Self-attention
Multi-head Self-attention主要是先把tokens分成q、k、v,再计算q和k的点积,经过softmax后获得加权值,给v加权,再经过全连接层。
用公式表示如下:
所谓Multi-head是指把q、k、v再dim维度上分成head份,公式里的dk为每个head的维度。
具体代码如下:
class Attention(nn.Module):
def __init__(self, dim, heads = 8, dim_head = 64, dropout = 0.):
super().__init__()
inner_dim = dim_head * heads
project_out = not (heads == 1 and dim_head == dim)
self.heads = heads
self.scale = dim_head ** -0.5
self.attend = nn.Softmax(dim = -1)
self.dropout = nn.Dropout(dropout)
self.to_qkv = nn.Linear(dim, inner_dim * 3, bias = False)
self.to_out = nn.Sequential(
nn.Linear(inner_dim, dim),
nn.Dropout(dropout)
) if project_out else nn.Identity()
def forward(self, x):
qkv = self.to_qkv(x).chunk(3, dim = -1)
q, k, v = map(lambda t: rearrange(t, 'b n (h d) -> b h n d', h = self.heads), qkv)
dots = torch.matmul(q, k.transpose(-1, -2)) * self.scale
attn = self.attend(dots)
attn = self.dropout(attn)
out = torch.matmul(attn, v)
out = rearrange(out, 'b h n d -> b n (h d)')
return self.to_out(out)

这里没有太多可以解释的地方,介绍一下q、k、v的来源,由于这是self-attention,因此q=k=v(即tokens),若是普通attention,则k= v,而q是其它的东西,例如可以是另一个尺度的tokens,或视频领域中的其它帧的tokens。
FeedForward
这里不用多介绍。
class FeedForward(nn.Module):
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了。
class Transformer(nn.Module):
def __init__(self, dim, depth, heads, dim_head, mlp_dim, dropout = 0.):
super().__init__()
self.layers = nn.ModuleList([])
for _ in range(depth):
self.layers.append(nn.ModuleList([
PreNorm(dim, Attention(dim, heads = heads, dim_head = dim_head, dropout = dropout)),
PreNorm(dim, FeedForward(dim, mlp_dim, dropout = dropout))
]))
def forward(self, x):
for attn, ff in self.layers:
x = attn(x) + x
x = ff(x) + x
return x

depth指的是Encoder的数量。PreNorm指的是层归一化。
class PreNorm(nn.Module):
def __init__(self, dim, fn):
super().__init__()
self.norm = nn.LayerNorm(dim)
self.fn = fn
def forward(self, x, **kwargs):
return self.fn(self.norm(x), **kwargs)

分类方法
数据通过Encoder后获得最后的预测向量的方法有两种典型。在ViT中是随机初始化一个cls_token,concate到分块后的token后,经过Encoder后取出cls_token,最后将cls_token通过全连接层映射到最后的预测维度。
#生成cls_token部分
from einops import repeat
self.cls_token = nn.Parameter(torch.randn(1, 1, dim))
cls_tokens = repeat(self.cls_token, '1 n d -> b n d', b = b)
x = torch.cat((cls_tokens, x), dim=1)
################################
#分类部分
self.mlp_head = nn.Sequential(
nn.LayerNorm(dim),
nn.Linear(dim, num_classes)
)
x = x.mean(dim = 1) if self.pool == 'mean' else x[:, 0]
x = self.to_latent(x)
return self.mlp_head(x)

在swin transformer中,没有选择cls_token。而是直接在经过Encoder后将所有数据取了个平均池化,再通过全连接层。
self.avgpool = nn.AdaptiveAvgPool1d(1)
self.head = nn.Linear(self.num_features, num_classes) if num_classes > 0 else nn.Identity()
x = self.avgpool(x.transpose(1, 2)) # B C 1
x = torch.flatten(x, 1)
x = self.head(x)

组合以上这些就成了一个完整的模型
class ViT(nn.Module):
def __init__(self, *, image_size, patch_size, num_classes, dim, depth, heads, mlp_dim, pool = 'cls', channels = 3, dim_head = 64, dropout = 0., emb_dropout = 0.):
super().__init__()
image_height, image_width = pair(image_size)
patch_height, patch_width = pair(patch_size)
num_patches = (image_height // patch_height) * (image_width // patch_width)
patch_dim = channels * patch_height * patch_width
assert pool in {'cls', 'mean'}, 'pool type must be either cls (cls token) or mean (mean pooling)'
self.to_patch_embedding = nn.Sequential(
Rearrange('b c (h p1) (w p2) -> b (h w) (p1 p2 c)', p1 = patch_height, p2 = patch_width),
nn.Linear(patch_dim, dim),
)
self.pos_embedding = nn.Parameter(torch.randn(1, num_patches + 1, dim))
self.cls_token = nn.Parameter(torch.randn(1, 1, dim))
self.dropout = nn.Dropout(emb_dropout)
self.transformer = Transformer(dim, depth, heads, dim_head, mlp_dim, dropout)
self.pool = pool
self.to_latent = nn.Identity()
self.mlp_head = nn.Sequential(
nn.LayerNorm(dim),
nn.Linear(dim, num_classes)
)
def forward(self, img):
x = self.to_patch_embedding(img)
b, n, _ = x.shape
cls_tokens = repeat(self.cls_token, '1 n d -> b n d', b = b)
x = torch.cat((cls_tokens, x), dim=1)
x += self.pos_embedding[:, :(n + 1)]
x = self.dropout(x)
x = self.transformer(x)
x = x.mean(dim = 1) if self.pool == 'mean' else x[:, 0]
x = self.to_latent(x)
return self.mlp_head(x)

数据的变换
以上的代码都是比较简单的,整体上最麻烦的地方在于理解数据的变换。
首先输入的数据为(B, C, H, W),在经过分块后,变成了(B, n, d)。
在CNN模型中,很好理解(H,W)就是feature map,C是指feature map的数量,那这里的n,d哪个是通道,哪个是图像特征?
回顾一下分块的部分
Rearrange('b c (h p1) (w p2) -> b (h w) (p1 p2 c)', p1 = patch_height, p2 = patch_width)

根据这个可以知道n为分块的数量,d为每一块的内容。因此,这里的n相当于CNN模型中的C,而d相当于features。
一般情况下,在Encoder中,我们都是以(B, n, d)的形式。
在swin transformer中这种以卷积的形式分块,获得的形式为(B, C, L),然后做了一个transpose得到(B, L, C),这与ViT通过直接分块方式获得的形式实际上完全一样,在Swin transformer中的L即为ViT中的n,而C为ViT中的d。
因此,要注意的是在Multi-head self-attention中,数据的形式是(Batchsize, Channel, Features),分成多个head的是Features。
前面提到,在ViT中会concate一个随机生成的cls_token,该cls_token的维度即为(B, 1, d)。可以理解为通道数多了个1。
以上就是Transformer的模型搭建细节了,整体上比较简单,大家看完这篇文章后可以找几篇Transformer的代码来理解理解。如ViT, swin transformer, TimeSformer等。
ViT:https://github.com/lucidrains/vit-pytorch/blob/main/vit_pytorch/vit.py
swin: https://github.com/microsoft/Swin-Transformer/blob/main/models/swin_transformer.py
TimeSformer:https://github.com/lucidrains/TimeSformer-pytorch/blob/main/timesformer_pytorch/timesformer_pytorch.py

下一篇我们将介绍如何写train函数,以及包括设置优化方式,设置学习率,不同层设置不同学习率,解析参数等。
欢迎关注公众号CV技术指南,专注于计算机视觉的技术总结、最新技术跟踪、经典论文解读、CV招聘信息。
CV技术指南创建了一个交流氛围很不错的群,除了太偏僻的问题,几乎有问必答。关注公众号添加编辑的微信号可邀请加交流群。
其它文章
StyleGAN大汇总 | 全面了解SOTA方法、架构新进展
Recommend
-
58
-
43
作为预训练模型,BERT目前常充当系统的重要模块,并衍生出大量 NLP 项目。但是BERT官方实现基于 TensorFLow 框架,因此那些借助 PyTorch 实现的 NLP 系统可能并不方便嵌入它。为此,开发者从每一个 OP 开始重新用
-
68
PyTorch 作为 Facebook 开发和维护的一个开源框架,近来的发展势头相当强劲。自 2017 年初首发以来,PyTorch 灵活、动态的编程环境及对用户友好的界面使其非常适用于快速实验,因此在社区内迅速发展壮大。 2018 年 12 月,PyTorch...
-
71
README.md former Simple transformer implementation from scratch in pytorch. See http://peterbloem.nl/b...
-
39
磐创AI分享
-
9
论文题目:Attention Is All You Need论文链接:https://arxiv.org/pdf/1706.03762.pdf 本文的代码主要参考wmathor的
-
9
5G网络骨干:小蜂窝技术指南 2022-10-27 15:04:43 小型蜂窝技术与电信公司多年来使用的基站具有相同的特性。然而,它们可以处理移动宽带和消费者的高数据速率,以及用于物联网的低速、低功耗设备的高密度。...
-
11
以 LLM 为核心 LLM@Core:程序员的大语言模型技术指南 Posted by: Phodal Huang June 2, 2023, 10:23 p.m. 过去几个月里,我们对于大语言模型的一系列探索,如 ChatGPT 端到端...
-
4
使用Transformer 模型进行时间序列预测的Pytorch代码示例 作者:Kaan Aslan 2024-01-30 01:12:37 人工智能 时间序列预测是一个经久不衰的主题...
-
2
关于Transformer原理与论文的介绍:详细了解Transformer:Attention Is All You Need 对于论文给出的模型架构,使用 PyTorch 分别实现各个部分。
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK