视觉Transformer详解

2024/12/20 21:04:12

本文主要是介绍视觉Transformer详解,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

详解视觉Transformer系列 PyTorch中的视觉Transformer详解

自2017年提出《Attention is All You Need》¹以来,transformer模型已经成为自然语言处理(NLP)领域的标准技术。2021年,《An Image is Worth 16x16 Words》²一文成功地将transformer应用到计算机视觉任务。自此之后,许多基于transformer的架构被提出,用于计算机视觉任务。

本文介绍了《一张图片等于16x16个单词》² 中所述的视觉Transformer模型(ViT)。本文不仅包含ViT的开源代码,还详细解释了各个组件的概念。所有代码均基于PyTorch Python包。

照片由 Sahand Babali 拍摄,来自 Unsplash

本文是一系列深入探讨视觉Transformer内部工作机制的文章中的一部分文章。每一篇文章也都作为包含可执行代码的 Jupyter Notebook 提供。该系列中的其他文章有:

  • 视觉Transformer详解
    → Jupyter Notebook
  • 视觉Transformer注意力详解
    → Jupyter Notebook
  • 视觉Transformer位置编码详解
    → Jupyter Notebook
  • Token-to-Token视觉Transformer详解
    → Jupyter Notebook
  • 视觉Transformer详解系列的GitHub仓库页面
目录如下
  • 什么是视觉Transformer?
  • 模型概览
    — 图像标记
    — 标记处理
    — 编码模块
    — 神经网络模块
    — 预测
  • 完整代码
  • 结论
    — 更多阅读
    — 参考文献
什么是视觉变换器?

如《注意力机制就是你所需要的》¹ 所介绍,Transformer 是一种主要通过注意力机制进行学习的机器学习模型。Transformer 很快成为这些任务中的最新技术,如语言翻译等序列到序列类型的任务。

_一张图等于16x16个单词_²成功地修改了[1]中提出的变压器模型,以解决图像分类任务,创建了Vision Transformer(ViT)。ViT基于[1]中变压器相同的注意力机制。然而,尽管用于NLP任务的变压器由编码器注意力机制和解码器注意力机制组成,ViT仅使用编码器部分。编码器的输出传递给一个神经网络“头”进行预测。

ViT在[2]中的实现存在的一个问题是,其最佳性能需要在大规模数据集上进行预训练。在专有JFT-300M数据集上预训练的模型表现最好。在较小的开源ImageNet-21k数据集上预训练的模型表现与最先进的卷积ResNet模型相当。

_从头开始在ImageNet上训练视觉变换器的Tokens-to-Token ViT_³ 试图通过引入一种新颖的预处理方法来消除对预训练的需求,该方法将输入图像转换成一系列token。更多关于此方法的信息,可以在这里找到here。本文将专注于讨论在[2]中实现的ViT。

模型走查

本文遵循了《一张图值十六个字》² 这篇文章所描述的模型结构。然而,该论文的代码并未公开发布。较新的《Token-to-Token ViT》³ 论文的代码可在 GitHub 上获取。Tokens-to-Token ViT (T2T-ViT) 模型在普通 ViT 模型基础上添加了一个 Tokens-to-Token (T2T) 模块。本文的代码基于 Tokens-to-Token ViT ³ __ GitHub 代码中的 ViT 组件。本文对代码进行了以下修改,包括但不限于支持任意形状的输入图像以及去掉了 dropout 层。

下面是一个关于ViT模型的图解。

ViT 模型示意图 (作者画的图片)

图像分词

ViT的第一步是将输入图像分割成tokens。Transformer是在一个_序列_的_tokens_上工作的;在自然语言处理中,这通常是一个由_单词_组成的句子。对于计算机视觉而言,如何将输入分割成tokens还不太明确。

这个 __ViT 将图像分割成 token,每个 token 对应图像的一个局部区域(或称为 patch)。他们这样描述:将高度为 H,宽度为 W,通道数为 C 的图像重塑为 N 个 token,每个 token 的 patch 大小为 P

每个标记的长度为 P²∗C

让我们来看一下 Luis Zuno (@ansimuz) 的像素艺术作品《名为 Mountain at Dusk》的补丁分词化示例。原始艺术品已被裁剪并转换成了单通道图像。这意味着每个像素的值都在0到1的范围内。单通道图像通常以灰度显示;然而,我们将用紫色来显示,因为这样更易于观察。

补丁分词处理未包含在[3]中的代码内。本节中的所有代码均为作者原创编写。

    mountains = np.load(os.path.join(figure_path, 'mountains.npy'))  

    H = mountains.shape[0]  
    W = mountains.shape[1]  
    print('黄昏中的山脉图像尺寸为 H =', H, '和 W =', W, '像素。')  
    print()  

    fig = plt.figure(figsize=(10,6))  
    plt.imshow(mountains, cmap='Purples_r')  
    plt.xticks(np.arange(-0.5, W+1, 10), labels=np.arange(0, W+1, 10))  
    plt.yticks(np.arange(-0.5, H+1, 10), labels=np.arange(0, H+1, 10))  
    plt.clim([0,1])  
    cbar_ax = fig.add_axes([0.95, .11, 0.05, 0.77])  # 将颜色条轴添加到图像中
    plt.colorbar(cax=cbar_ax);  
    #plt.savefig(os.path.join(figure_path, 'mountains.png'))
    黄昏时的山高 60 像素 宽 100 像素。

代码生成的图片,作者供图

这张图的高度是 H=60,宽度是 W=100。我们将把 P 设为 20,因为它可以同时平分 HW

P = 20  
N = int((H*W)/(P**2))  
print('将会生成', N, '个补丁,每个补丁的大小为', P, '乘以', P, '.')  
print()  

fig = plt.figure(figsize=(10,6))  
plt.imshow(mountains, cmap='Purples_r')  
plt.hlines(np.arange(P, H, P)-0.5, -0.5, W-0.5, color='w')  
plt.vlines(np.arange(P, W, P)-0.5, -0.5, H-0.5, color='w')  
plt.xticks(np.arange(-0.5, W+1, 10), labels=np.arange(0, W+1, 10))  
plt.yticks(np.arange(-0.5, H+1, 10), labels=np.arange(0, H+1, 10))  
生成一系列从9.5到W,步长为P的数组,重复3次
生成一系列从9.5到H,步长为P的数组,重复5次
对于从1到N的每个i:  
plt.text(x_text[i-1], y_text[i-1], str(i), color='w', fontsize='xx-large', ha='center')  
plt.text(x_text[2], y_text[2], str(3), color='k', fontsize='xx-large', ha='center');  
# plt.savefig(os.path.join(figure_path, 'mountain_patches.png'), bbox_inches='tight')

会有15个20乘20的补丁包,每个都是20乘20的大小。

代码结果(由作者提供图片)

通过平铺这些区域,我们看到了生成的标记。我们以第12个区域为例,因为它包含了四种不同的色调。

    print('每个补丁都会生成一个长度为' + str(P**2) + '的token。')  
    print('\n')  

    patch12 = mountains[40:60, 20:40]  
    token12 = patch12.reshape(1, P**2)  

    fig = plt.figure(figsize=(10,1))  
    plt.imshow(token12, aspect=10, cmap='Purples_r')  
    plt.clim([0,1])  
    plt.xticks(np.arange(-0.5, 401, 50), labels=np.arange(0, 401, 50))  
    plt.yticks([]);  
    #plt.savefig(os.path.join(figure_path, 'mountain_token12.png'), bbox_inches='tight')

每个补丁都会创建一个长度为400的凭证。

代码生成的图片(作者提供的图片)

提取图像中的 token 后,通常会使用线性变换来改变 token 的长度。这通常通过一个可学习的线性层来实现。新的 token 长度被称为 潜在维度通道维token 长度。经过变换后,这些 token 就不再能直接看出是来自原始图像的补丁了。

既然我们理解了这个概念,我们来看看代码中是如何实现补丁分词的。

    class Patch_Tokenization(nn.Module):  
        def __init__(self,  
                    img_size: tuple[int, int, int]=(1, 1, 60, 100),  
                    patch_size: int=50,  
                    token_len: int=768):  

            """ Patch Tokenization Module  
                Args:  
                    img_size (元组[int, int, int]):输入尺寸(通道数,高度,宽度),即图像的尺寸  
                    patch_size (int):补丁的边长(为方形)  
                    token_len (int):令牌长度  
            """  
            super().__init__()  

            ## 定义层结构  
            self.img_size = img_size  
            C, H, W = self.img_size  
            self.patch_size = patch_size  
            self.token_len = token_len  
            assert H % self.patch_size == 0, '图像的高度必须能被补丁尺寸整除,否则会抛出异常.'  
            assert W % self.patch_size == 0, '图像的宽度必须能被补丁尺寸整除,否则会抛出异常.'  
            self.num_tokens = (H / self.patch_size) * (W / self.patch_size)  

            ## 定义层结构  
            self.split = nn.Unfold(kernel_size=self.patch_size, stride=self.patch_size, padding=0)  
            self.project = nn.Linear((self.patch_size**2)*C, token_len)  

        def forward(self, x):  
            x = self.split(x).transpose(1,0)  
            x = self.project(x)  
            return x

注意,这两个assert语句确保图像尺寸能被补丁大小整除。补丁的实际分割是通过torch.nn.Unfold层来实现的。

我们将使用裁剪过的单通道版本的《黄昏山景》⁴来运行这段代码。我们应该看到与之前相同的token数量和初始token大小的值。我们将使用_token_len=768_作为投影长度,这与ViT²基础变体的大小一致。

以下代码块中的第一行将 "《日落之山》" 的数据类型从 NumPy 数组更改为 Torch 张量。我们将使用 unsqueeze 函数对张量进行操作以创建通道维度和批次大小维度。正如前面提到的,我们只需要一个通道。由于只有一个图像,因此 batchsize=1

    x = torch.from_numpy(mountains).unsqueeze(0).unsqueeze(0).to(torch.float32)  
    token_len = 768  
    print('输入维度如下:\n\t批大小:', x.shape[0], '\n\t输入通道数:', x.shape[1], '\n\t图像尺寸:', (x.shape[2], x.shape[3]))  

    # 定义模块如下:  
    patch_tokens = Patch_Tokenization(img_size=(x.shape[1], x.shape[2], x.shape[3]),  
                                        patch_size = P,  
                                        token_len = token_len)
输入维度如下:
每个批次的批量大小:1  
输入通道数:1个  
图像尺寸:(60, 100)

现在,我们将图像分割成若干标记/碎片。

    x = patch_tokens.split(x).transpose(2,1)  
    print('补丁标记化之后,维度如下:\n\t批次大小:', x.shape[0], '\n\t标记个数:', x.shape[1], '\n\t标记维度:', x.shape[2])
    在分词处理后,维度如下  
       批量大小:1   
       token数量:15   
       每个token的长度:400.

如我们在示例中所见,共有_N=15_个标记,每个长度为400。最后,将这些标记投影为长度_tokenlen

    x = patch_tokens.project(x)  
    print('投影后,维度如下:\n\t批次大小:', x.shape[0], '\n\ttoken的数量:', x.shape[1], '\n\ttoken的长度:', x.shape[2])
    投影之后,维度是  
       批量大小: 1   
       标记数量: 15   
       token长度: 768

我们现在有了 token,就可以进入 ViT 的步骤了。

令牌处理过程

我们将ViT编码块之前的两个步骤指定为“令牌处理阶段”。下面展示了ViT图中的令牌处理部分。

ViT Token处理组件图(作者绘制)

第一步是向图像标记前添加一个空白标记,称为预测令牌。这个标记将在编码块输出时用于进行预测。它一开始是空白的(或等同于零),这样它就能从其他图像令牌中获取信息。

我们将从175个token开始。每个token的长度为768,这是ViT²基础版本的维度。我们选择了13作为批次大小,因为它是一个素数,不会被误认为是其他参数。

    # 定义输入
    num_tokens = 175
    token_len = 768
    batch = 13
    x = torch.rand(batch, num_tokens, token_len)
    打印('输入维度为\n\t批量大小:', x.shape[0], '\n\ttoken个数:', x.shape[1], '\n\ttoken长度:', x.shape[2])

    # 添加预测token
    pred_token = torch.zeros(1, 1, token_len).expand(-1, -1, -1)
    打印('预测token的维度为\n\t批量大小:', pred_token.shape[0], '\n\ttoken个数:', pred_token.shape[1], '\n\ttoken长度:', pred_token.shape[2])

    x = torch.cat((pred_token, x), dim=1)
    打印('加上预测token后的维度为\n\t批量大小:', x.shape[0], '\n\ttoken个数:', x.shape[1], '\n\ttoken长度:', x.shape[2])
    输入维度如下  
       批量大小: 13  
       令牌数: 175  
       令牌长度: 768  
    预测令牌的维度如下  
       批量大小: 13  
       令牌数: 1  
       令牌长度: 768  
    包含预测令牌的维度如下  
       批量大小: 13  
       令牌数: 176  
       令牌长度: 768

现在,我们为我们的 token 添加了一个位置编码。位置编码使 transformer 能够理解图像 token 的位置。需要注意的是,这是相加,而不是相连接。关于位置编码的细节,留待另一个时间解释。

    def get_sinusoid_encoding(num_tokens, token_len):  
        """ 生成正弦位置编码表  

            参数:  
                num_tokens (int): token的数量  
                token_len (int): token的长度  

            返回:  
                (torch.FloatTensor) 正弦位置编码表  
        """  

        def get_position_angle_vec(i):  
            """ 获取位置角度向量 """  
            return [i / np.power(10000, 2 * (j // 2) / token_len) for j in range(token_len)]  

        sinusoid_table = np.array([get_position_angle_vec(i) for i in range(num_tokens)])  
        sinusoid_table[:, 0::2] = np.sin(sinusoid_table[:, 0::2])  
        sinusoid_table[:, 1::2] = np.cos(sinusoid_table[:, 1::2])   

        return torch.FloatTensor(sinusoid_table).unsqueeze(0)  

    PE = get_sinusoid_encoding(num_tokens+1, token_len)  
    # 计算位置嵌入 PE  
    print('位置嵌入 PE 的维度如下\n\ttoken数量:', PE.shape[1], '\n\ttoken长度:', PE.shape[2])  

    x = x + PE  
    print('加上位置嵌入后的维度如下\n\t批次大小:', x.shape[0], '\n\ttoken数量:', x.shape[1], '\n\ttoken长度:', x.shape[2])
    位置嵌入的维度是,  
       令牌数量:176 (tokens)  
       令牌长度:768  
    具有位置嵌入的维度是,  
       批大小:13  
       令牌数量:176 (tokens)  
       令牌长度:768

我们的令牌们准备好了,现在可以进入编码阶段了。

编码区块

编码块是模型实际从图像令牌中学习的地方。用户可以设置编码块的数量,这是一个超参数。如图所示,下面是编码块的示意图。

编码区块

以下是一个编码块的代码。

    class Encoding(nn.Module):  

        def __init__(self,  
           dim: int,  
           num_heads: int=1,  
           hidden_chan_mul: float=4.,  
           qkv_bias: bool=False,  
           qk_scale: NoneFloat=None,  
           act_layer=nn.GELU,   
           norm_layer=nn.LayerNorm):  

            """ 编码块  

                参数:  
                    dim (int): 单个token的大小  
                    num_heads (int): MSA 中注意力头的数量  
                    hidden_chan_mul (float): 神经网络部分中隐藏通道数量的乘数  
                    qkv_bias (bool): qkv 层是否学习一个加性偏差  
                    qk_scale (NoneFloat): 用于缩放查询和键的值;  
                                        如果为 None,则查询和键按 ``head_dim ** -0.5`` 缩放  
                    act_layer (nn.modules.activation): 用于激活层的 torch 神经网络层类型  
                    norm_layer (nn.modules.normalization): 用于归一化层的 torch 神经网络层类型  
            """  

            super().__init__()  

            ## 定义层  
            self.norm1 = norm_layer(dim)  
            self.attn = Attention(dim=dim,  
                                chan=dim,  
                                num_heads=num_heads,  
                                qkv_bias=qkv_bias,  
                                qk_scale=qk_scale)  
            self.norm2 = norm_layer(dim)  
            self.neuralnet = NeuralNet(in_chan=dim,  
                                    hidden_chan=int(dim*hidden_chan_mul),  
                                    out_chan=dim,  
                                    act_layer=act_layer)  

        def forward(self, x):  
            x = x + self.attn(self.norm1(x))  
            x = x + self.neuralnet(self.norm2(x))  
            return x

_numheads、_qkvbias 和 _qkscale 参数定义了 Attention 模块的组成部分。关于视觉变压器中的注意力机制的深入探讨留待另文详述。

两个参数 _hidden_chan_mul__act_layer_ 定义了 神经网络 模块中的组件。激活层可以是任何 torch.nn.modules.activation 中的层。稍后我们将更详细地讨论 神经网络 模块。

norm_layer,可以是任何一种torch.nn.modules.normalization层。

我们现在将依次浏览图表中的每个蓝色方块及其对应的代码。我们将使用176个token,每个长度为768。我们将使用批处理大小为13,这是因为13是质数,不会与其他参数混淆。我们将使用4个attention head,因为它能均匀地划分token长度;但在编码块中,你不会看到注意力头的维度。

    # 定义输入
    num_tokens = 176
    token_len = 768
    batch = 13
    heads = 4
    x = torch.rand(batch, num_tokens, token_len)
    print('输入的尺寸为\n\t批次大小:', x.shape[0], '\n\ttoken数:', x.shape[1], '\n\ttoken长度:', x.shape[2])

    # 定义模块
    E = Encoding(dim=token_len, num_heads=heads, hidden_chan_mul=1.5, qkv_bias=False, qk_scale=None, act_layer=nn.GELU, norm_layer=nn.LayerNorm)
    # 将模型设置为评估模式
    E.eval();
输入维度为:  
批大小:13  
令牌数:176  
令牌长度:768.

现在,我们将通过一个规范层和一个注意力模块。编码块中的注意力模块进行了参数化,以保持词的长度不变。在注意力模块之后,我们实现了第一个跳过连接。

    y = E.norm1(x)  
    print('归一化后的维度为\n\t批量大小:', y.shape[0], '\n\ttoken数量:', y.shape[1], '\n\ttoken维度:', y.shape[2])  
    y = E.attn(y)  
    print('注意力机制后的维度为\n\t批量大小:', y.shape[0], '\n\ttoken数量:', y.shape[1], '\n\ttoken维度:', y.shape[2])  
    y = y + x  
    print('跳过连接后的维度为\n\t批量大小:', y.shape[0], '\n\ttoken数量:', y.shape[1], '\n\ttoken维度:', y.shape[2])
规范化后,维度是  
       批量大小: 13   
       数量: 176   
       大小: 768  
注意力后,维度是  
       批量大小: 13   
       数量: 176   
       大小: 768  
残差链接后,维度是  
       批量大小: 13   
       数量: 176   
       大小: 768

现在,我们经过另一个标准化层,然后是神经网络模块。然后我们通过第二个分叉连接结束。

    z = E.norm2(y)  
    print('归一化后的维度是\n\t批大小:', z.shape[0], '\n\ttoken的数量:', z.shape[1], '\n\ttoken的大小:', z.shape[2])  
    z = E.neuralnet(z)  
    print('经过神经网络处理后的维度是\n\t批大小:', z.shape[0], '\n\ttoken的数量:', z.shape[1], '\n\ttoken的大小:', z.shape[2])  
    z = z + y  
    print('残差连接后的维度是\n\t批大小:', z.shape[0], '\n\ttoken的数量:', z.shape[1], '\n\ttoken的大小:', z.shape[2])
    标准化后  
       批大小:13   
       标记数量:176   
       标记维度:768  
    经过神经网络后  
       批大小:13   
       标记数量:176   
       标记维度:768  
    残差连接后  
       批大小:13   
       标记数量:176   
       标记维度:768

单个编码块的内容就是这样!由于最终维度和初始维度相同,模型可以轻松地将令牌通过多个编码块进行传递,这些编码块的数量由超参数 depth 决定。

神经网络模块

神经网络(NN)模块是编码模块中的一个子组件。NN模块非常简单,由一个全连接层、一个激活层和另一个全连接层组成。激活层可以是任何torch.nn.modules.activation中的层,该层作为输入传递到模块。可以通过配置NN模块来改变输入的形状,或者保持形状不变。由于神经网络在机器学习中很常见,不是本文的重点,我们不会详细讲解这段代码。不过,这里展示了NN模块的代码。

    class NeuralNet(nn.Module):  
        def __init__(self,  
           in_chan: int,  
           hidden_chan: NoneFloat=None,  
           out_chan: NoneFloat=None,  
           act_layer = nn.GELU):  
            """神经网络模块

                参数:
                    in_chan (int): 输入的通道数(特征数)
                    hidden_chan (NoneFloat): 隐藏层中的通道数;如果为 None,则隐藏层的通道数与输入层相同
                    out_chan (NoneFloat): 输出的通道数;如果为 None,则输出层的通道数与输入层相同
                    act_layer(nn.modules.activation): 作为激活函数的 torch 神经网络层类
            """  

            super().__init__()  

            ## 定义各层的通道数  
            hidden_chan = hidden_chan or in_chan  
            out_chan = out_chan or in_chan  

            ## 定义各层  
            self.fc1 = nn.Linear(in_chan, hidden_chan)  
            self.act = act_layer()  
            self.fc2 = nn.Linear(hidden_chan, out_chan)  

        def forward(self, x):  
            x = self.fc1(x)  
            x = self.act(x)  
            x = self.fc2(x)  
            return x
预测加工

经过编码块处理后,模型必须做的最后一件事是进行预测。如下图所示的预测阶段是ViT图的一部分。

ViT预测处理组件图示(作者绘制)

我们将查看这个过程的每一步。我们将继续使用长度为768的176个tokens。我们将采用批量大小为1来演示如何进行单个预测。批量大小大于1则表示以并行方式计算这些预测。

    # 定义一个输入  
    num_tokens = 176  
    token_len = 768  
    batch = 1  
    x = torch.rand(batch, num_tokens, token_len)  
    print('输入的维度是\n\t批次大小:', x.shape[0], '\n\ttoken个数:', x.shape[1], '\n\ttoken的长度:', x.shape[2])
输入维度如下:
批量大小:1个
令牌数:176
令牌长度:768个单位

首先,所有的tokens会经过一个规范层处理。

    norm = nn.LayerNorm(token_len)  
    x = norm(x)  
    print('经过归一化处理后,维度如下:\n\t批次大小:', x.shape[0], '\n\ttoken数量:', x.shape[1], '\n\ttoken维度:', x.shape[2])
    标准化之后,维度如下  
       批次大小:1,  
       令牌数量:1001,  
       令牌维度:768

接下来,我们将预测令牌从其余令牌中分离出来。在整个编码块过程中,预测令牌不再为零,并从输入图像中获取了信息。我们将仅用它来做最终预测。

    将预测令牌赋值为 x[:, 0]  
    打印预测令牌的长度:pred_token.shape[-1]
预测 token 的长度是 768

最后,预测标记通过_head_来进行预测。_head_通常是某种神经网络的变体,根据模型的不同而有所变化。在《一图胜千言》²这篇文章里,他们在预训练阶段使用了具有一个隐藏层的多层感知器(MLP),并在微调阶段使用了一个单层线性模型。在《Token-to-Token ViT³》这篇文章里,他们使用了一个单层线性模型作为head。这里我们继续使用一个单层线性模型。

注意,输出形状是根据学习问题的参数设定的。对于分类任务,它通常是长度为_类别数量_的向量,采用所谓的独热编码。对于回归任务,输出形状可以是任意数量的预测参数(例如整数)。这里我们使用输出形状为1,来表示单个估计的回归值。

    head = nn.Linear(token_len, 1)  
    pred = head(pred_token)  
    print('预测的长度为:', (pred.shape[0], pred.shape[1]))  
    print('预测的具体数值为:', float(pred))
预测区间:(1, 1)  
预测值:-0.5474240779876709

就这样好了!模型做出了预测!

全代码

为了创建完整的ViT模块,我们使用了上面提到的Patch Tokenization模块和VIT_BACKBONE模块。VIT_BACKBONE模块定义如下,它包含令牌处理、编码块等组件。

    class ViT_Backbone(nn.Module):  
        def __init__(self,  
                    preds: int=1,  
                    token_len: int=768,  
                    num_heads: int=1,  
                    Encoding_hidden_chan_mul: float=4.,  
                    depth: int=12,  
                    qkv_bias=False,  
                    qk_scale=None,  
                    act_layer=nn.GELU,  
                    norm_layer=nn.LayerNorm):  

            """ 视觉Transformer骨干网络
                参数:
                    preds (int): 输出的预测数量
                    token_len (int): 一个token的长度
                    num_heads (int): 多头注意力层中的注意力头数量
                    Encoding_hidden_chan_mul (float): 编码模块隐藏通道数的乘数
                    depth (int): 模型中的编码块数量
                    qkv_bias (bool): qkv是否使用偏置
                    qk_scale (float/None): 用于缩放查询和键的值;如果为None,则查询和键通过`head_dim ** -0.5`进行缩放
                    act_layer (nn.modules.activation): 用作激活的torch神经网络层类
                    norm_layer (nn.modules.normalization): 用作归一化的torch神经网络层类
            """  

            super().__init__()  

            ## 定义参数
            self.num_heads = num_heads
            self.Encoding_hidden_chan_mul = Encoding_hidden_chan_mul
            self.depth = depth

            ## 定义token处理组件
            self.cls_token = nn.Parameter(torch.zeros(1, 1, self.token_len))
            self.pos_embed = nn.Parameter(data=get_sinusoid_encoding(num_tokens=self.num_tokens+1, token_len=self.token_len), requires_grad=False)

            ## 定义编码块
            self.blocks = nn.ModuleList([Encoding(dim=self.token_len,   
                                                   num_heads=self.num_heads,  
                                                   hidden_chan_mul=self.Encoding_hidden_chan_mul,  
                                                   qkv_bias=qkv_bias,  
                                                   qk_scale=qk_scale,  
                                                   act_layer=act_layer,  
                                                   norm_layer=norm_layer)  
                 for i in range(self.depth)])

            ## 定义预测处理
            self.norm = norm_layer(self.token_len)
            self.head = nn.Linear(self.token_len, preds)

            ## 从截断的正态分布中采样类token
            timm.layers.trunc_normal_(self.cls_token, std=.02)

        def forward(self, x):  
            ## 假设x已经分词

            ## 获取批次大小
            B = x.shape[0]
            ## 拼接类别token
            x = torch.cat((self.cls_token.expand(B, -1, -1), x), dim=1)
            ## 添加位置嵌入
            x = x + self.pos_embed
            ## 依次通过每个编码块
            for blk in self.blocks:
                x = blk(x)
            ## 进行归一化
            x = self.norm(x)
            ## 对类别token进行预测
            x = self.head(x[:, 0])
            return x

ViT Backbone 模块中,我们可以定义ViT模型。

    class ViT_Model(nn.Module): 
     def __init__(self, 
        img_size: tuple[int, int, int]=(1, 400, 100), 
        patch_size: int=50, 
        token_len: int=768, 
        preds: int=1, 
        num_heads: int=1, 
        Encoding_hidden_chan_mul: float=4., 
        depth: int=12, 
        qkv_bias=False, 
        qk_scale=None, 
        act_layer=nn.GELU, 
        norm_layer=nn.LayerNorm): 

      """ 视觉Transformer模型

       参数:
        img_size (tuple[int, int, int]): 输入图像的尺寸 (通道数, 高度, 宽度)
        patch_size (int): 方形补丁的边长
        token_len (int): 令牌的输出长度
        preds (int): 输出预测的数量
        num_heads (int): 多头注意力机制中的注意力头的数量
        Encoding_hidden_chan_mul (float): 决定编码器模块中隐藏通道(特征)数量的乘数
        depth (int): 模型中的编码器块的数量
        qkv_bias (bool): qkv层是否学习可加偏差
        qk_scale (None 或 float): 用于缩放查询和键值的值;如果为 `None`,则查询和键值将被缩放为`head_dim ** -0.5`
        act_layer (nn.modules.activation): 作为激活层使用的torch神经网络层类
        norm_layer (nn.modules.normalization): 作为正则化层使用的torch神经网络层类
      """ 
      super().__init__() 

      ## 定义参数
      self.img_size = img_size 
      C, H, W = self.img_size 
      self.patch_size = patch_size 
      self.token_len = token_len 
      self.num_heads = num_heads 
      self.Encoding_hidden_chan_mul = Encoding_hidden_chan_mul 
      self.depth = depth 

      ## 定义Patch Embedding模块
      self.patch_tokens = Patch_Tokenization(img_size, 
               patch_size, 
               token_len) 

      ## 定义ViT骨干网络
      self.backbone = ViT_Backbone(preds, 
             self.token_len, 
             self.num_heads, 
             self.Encoding_hidden_chan_mul, 
             self.depth, 
             qkv_bias, 
             qk_scale, 
             act_layer, 
             norm_layer) 
      ## 初始化权重参数
      self.apply(self._init_weights) 

     def _init_weights(self, m): 
      """ 初始化线性层和层归一化层的权重
      """ 
      ## 对于线性层
      if isinstance(m, nn.Linear): 
       ## 权重从截断正态分布中初始化
       timm.layers.trunc_normal_(m.weight, std=.02) 
       if isinstance(m, nn.Linear) and m.bias is not None: 
        ## 如果存在偏差,将偏差初始化为零
        nn.init.constant_(m.bias, 0) 
      ## 对于层归一化层
      elif isinstance(m, nn.LayerNorm): 
       ## 权重初始化为1
       nn.init.constant_(m.weight, 1.0) 
       ## 偏差初始化为零
       nn.init.constant_(m.bias, 0) 

     @torch.jit.ignore ## 告诉PyTorch不要编译为TorchScript
     def no_weight_decay(self): 
      """ 在优化器中忽略类令牌的权重衰减项
      """ 
      return {'cls_token'} 

     def forward(self, x): 
      x = self.patch_tokens(x) 
      x = self.backbone(x) 
      return x

ViT 模型 中,_imgsize、_patchsize 和 _tokenlen 这些参数定义了 Patch 编码 模块。

The _numheads ,_Encoding_hidden_channelmul ,_qkvbias ,_qkscale ,和 _actlayer 参数定义了 编码块 模块。_actlayer 可以是 torch.nn.modules.activation 中的任何层。depth 参数决定了模型包含多少个编码块。

_the_normlayer 参数定义了编码块模块内部和外部的归一化方式。它可以是任何可用的 torch.nn.modules.normalization 层,例如 BatchNorm 或 LayerNorm。

__init_weights_ 方法名来源于 T2T-ViT³ 代码。此方法可以删除以随机初始化所有学习到的权重和偏差。按照当前的实现,线性层的权重初始化为截断正态分布;线性层的偏差初始化为0;归一化层的权重初始化为1;归一化层的偏差初始化为0。

结论如下

现在,你有了对ViT模型机制的深入了解,可以去训练模型了!下面是一些下载ViT模型代码的链接。其中一些允许更多的自定义设置。去吧,祝你训练愉快!

  • 本文系列的GitHub仓库链接
  • GitHub仓库 用于《一张图值16x16个词》²
    → 包含预训练模型以及微调代码,但不包含模型定义
  • 在 PyTorch Image Models (timm) 中实现的ViT模型⁹
    timm.create_model('vit_base_patch16_224', pretrained=True)
  • Phil Wang 的 vit-pytorch

本文由洛斯阿拉莫斯国家实验室批准发布,编号为LA-UR-23–33876。相关代码获批采用BSD-3开源许可证,编号O#4693。

更多阅读

要了解更多关于NLP环境中transformers的信息,可以参阅

  • 视觉解释变压器 第 1 部分 功能概要:https://towardsdatascience.com/transformers-explained-visually-part-1-overview-of-functionality-95a6dd460452
  • 视觉解释变压器 第 2 部分 工作原理详解:https://towardsdatascience.com/transformers-explained-visually-part-2-how-it-works-step-by-step-b49fa4a64f34

有关视觉Transformer的全面视频讲座,请参阅

  • 视觉Transformer及其应用领域:请参见 https://youtu.be/hPb6A92LROc?si=GaGYiZoyDg0PcdSP
参考文献:

[1] Vaswani 等人(等人)(2017)Attention Is All You Need. (网址:https://doi.org/10.48550/arXiv.1706.03762)

[2] Dosovitskiy 等 (2020). 一张图片抵得上16x16个单词:大规模图像识别中的变压器. https://doi.org/10.48550/arXiv.2010.11929.

[3] 元等人 (2021). Tokens-to-Token ViT: 从零开始在 ImageNet 上训练视觉变压器,. https://doi.org/10.48550/arXiv.2101.11986
→ GitHub 代码: 链接: https://github.com/yitu-opensource/T2T-ViT

[4] Luis Zuno (@ansimuz)。黄昏山景背景图。CC0: https://opengameart.org/content/mountain-at-dusk-background

[5] PyTorch. 展开. https://pytorch.org/docs/stable/generated/torch.nn.Unfold.html#torch.nn.Unfold

[6] PyTorch. unsqueeze(unsqueeze). 链接

[7] PyTorch. 非线性激活函数(加权求和,非线性)https://pytorch.org/docs/stable/nn.html#non-linear-activations-weighted-sum-nonlinearity

[8] PyTorch. 归一化层。 https://pytorch.org/docs/stable/nn.html#normalization-layers

[9] Ross Wightman. PyTorch 图像模型项目.https://github.com/huggingface/pytorch-image-models



这篇关于视觉Transformer详解的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程