【Transformer】2、ViT:An image is worth 16x16: transformers for image recognition at scale


在这里插入图片描述

代码链接:https://github.com/lucidrains/vit-pytorch

论文连接:https://openreview.net/pdf?id=YicbFdNTTy

出处:ICLR2021

本文的名称是:一个图片等价于很多 16x16 的单词,就是说一个图片被打成多个 16x16 的 patch

ViT 的效果:

  • 在中型数据集上(如 ImageNet)训练时,不加强的约束,ViT 模型比同等大小的残差网络低几个点,但这是可以解释的,因为 Transformer 相比 CNN 是缺少了一些归纳偏置的,就是一些先验知识或者做好的假设,比如卷积神经网络中假设的局部相关性和平移不变性的偏置,就有了先验信息,使用相对少的数据就可以得到较好的效果,但 transformer 没有这些归纳偏置,所以在较小的数据集上还没有学习到归纳信息
  • 在更大的数据集 14M(ImageNet21k)和 300M(Google 的 JFT300 数据集)上训练时,就得到了和 CNN SOTA 近似的效果

一、背景和动机

Transformer 在 NLP 领域取得了很好的效果,但在计算机视觉领域还没有很多应用,在视觉中,自注意力一般是配合卷积神经网络来用,而不会改变原有的网络结构,所以作者想要借鉴其在 NLP 中的方法,在计算机视觉的分类任务中进行使用,而且作者证明了使用 CNN 不是必须的。

作者在摘要中说 transformer 需要的训练资源更少,但其实是需要 2500 天 TPU

Transformer 用到视觉问题上的难处:

  • 自注意里需要 token 之间两两相乘来计算相似度,计算复杂度是 N^2,BERT 的序列长度是 512,而且需要的输入是一个序列
  • 一般来说较小的分类图片输入大小 224x224,如果把像素点当做一个 token 拉平的话,序列长度就是 224x224=50776,计算复杂度非常大,更密集的检测任务的计算复杂度更大

在 ViT 之前,已经有一些方法尝试将 transformer 作用于计算机视觉,但还是没有打过 ResNet 网络:

  • 很多方法引入了自注意力模块,在 CNN 中引入了多个自注意力模块,而不是整个结构全部使用自注意力的形式
  • 还有一些方法使用特征图来作为 transformer 的输入(比如特征图是 14x14 的,计算复杂度就变小了)
  • 还有一些方法是将图像切块,在块内做自注意力

ViT 使用 transformer 是受到其在 NLP 中展现出来的强大的可扩展性的启发而来的,希望能使用 全 transformer 的结构来实现大规模图像任务

ViT 将图片切分成图片块,每个块看做一个单词,来将视觉看做语言任务实现

二、方法

模型整体过程:

  • 首先,使用步长为 patch 边长的卷积实现(这里卷积核处理时是不重叠的,可以看成切割),相当于对原图先切割成 patch,每个 patch 经过线性映射(patch embedding),得到一个一维的特征(如 32x32=1024),如粉色小框
  • 然后,在每个 patch 得到的一维特征上,加上位置编码(position embedding),并且 concat 一个 cls token(也要加上位置编码 0)来用作最后的分类,因为所有 token 之间都在做交互,所以作者认为 cls embedding 能学习到其他 token 的信息,所以是有能力做分类的
  • 接着,将 1x49x1024 的特征送入 Transformer,进行多层【多头注意力和 FFN 】进行特征提取
  • 最后,将 cls token(1x1024)送入 MLP,输出 1x1000 维的分类结果
    在这里插入图片描述
    由于 Transformer 在 NLP 中使用时,都是接受一维的输入,而图像是二维的结构,所以需要先把图像切分成大小相等的patch,然后再编码成一个序列,送入 Transformer

Vision Transformer 的过程:

1、输入图像分割成 patch,并使用可学习的 Linear Projection 将其拉伸成 D 维 (768),为输入 Transformer 做准备

  • 首先,输入图像为 x ∈ R H × W × C x\in R^{H \times W \times C} xRH×W×C,将其 reshape 为 x p ∈ R N × ( P 2 C ) x_p\in R{N \times (P^2 C)} xpRN×(P2C),切分后的 patch 大小为 P × P P \times P P×P N = H W / P 2 N=HW/P^2 N=HW/P2 为 patch 的个数。这里, N N N 也掌握着 Transformer 的效率。

  • Transformer 在每层都使用维度大小为 D D D 的向量输入,所以在送入 Transformer 之前,会使用可学习的线性影射(公式1)来将 patch 信息转换为 D 维。

  • 粉色长条大框 linear projection E 是怎么拉平 patch 的呢,E 的大小是 768x768 的,前面的 768 对应 patch 的像素大小,后面的 768 是 D 是会随着模型尺度大小而变化的

  • 196 个 token 经过 linear projection 的计算过程是:196x768x768x768,196x768 是有 196 个 patch 每个 patch 大小为 768, 经过 linear projection 后就得到 196x768

  • cls token 的大小也是 768,可以和图像信息拼接起来

  • 最后送入 transformer 的特征是 197x768

  • 这 197 个 token 特征还需要加上 positional embedding

  • 输入:3x224x224

  • patch 大小 P P P:16x16

  • patch 个数 N N N:14x14=196

  • D:768

2、给 D 维的 Transformer 输入后面连接一个 class token

将输入编码成 B × N × D B\times N \times D B×N×D 输入 Transformer 之前,会给 N N N 这个维度增加一个 class token,变成 N + 1 N+1 N+1 维,这个 class token 是一个大小为 1 × 1 × D 1\times 1 \times D 1×1×D 的可学习向量,表示 Transformer 的 Encoder 的输出( z L 0 z_L^0 zL0),也就是作为图像的特征表达 y。

3、给上面的结果加上位置编码

添加位置编码能够保留图像的位置信息,作者使用可学习的 1D 位置编码(因为从文献来看,使用 2D 编码和相对位置编码也没有带来理想的效果)。ViT 中的位置编码是随机生成可学习参数,没有做过多设计,这样的设计。

positional embedding 在示例框架图中是使用 0/1/2/3 这种来展示的,其实真正使用的并不是 0/1/2/3 这种序号,真正使用的位置编码是可以学习的 768 维的向量,也就是会有一个表格,0 行对应一个 768 维的向量,1 行对应一个 768 维的向量,一直到最后一个位置上都对应的 768 维的向量,768 是和 patch 尺寸对应的,方便相加

作者分析不同位置编码的效果都差不多的原因可能在于,ViT 的位置编码是对 patch 进行编码,而不是像素级别的编码,patch 是图像小块儿信息,所以想要对这些 patch 的位置信息进行编码还是比较容易的,所以使用简单的 1D 编码和更复杂的 2D 编码效果都差不多,但不使用位置编码的效果会差。

如过预训练后 fine-tuning 的时候图像变大,那么预训练好的位置编码就不够用了,那么作者使用了插值的方式来扩充了位置编码的大小

4、送入 Transformer Encoder

这里的 Encoder 由多层的 multiheaded self-attention(MSA)和 MLP 组成,每层之前都会使用 Layernorm,每个 block 之后都会使用残差连接。

在这里插入图片描述

  • x p x_p xp 是 patch,E 是 linear projection,z’
  • 197x768 的 patch embedding 进入 transformer encoder,先经过 layer norm
  • 然后进入多头自注意力,假设使用 base 版本,有 12 个多头,每个头的维度就变成了 768/12=64,每个头对应的 K/Q/V 的维度都是 197x64,最后再把 12 个头的输出拼接起来,还是得到 197x768 的特征
  • 在进入 layer norm
  • 在进入 MLP,MLP 会先变成 197x3072,然后再投射回 197x768,最终输出
  • 输入和输出的维度都是 197x768

在这里插入图片描述

归一化方法:

Transformer 中一般都使用 LayerNorm,LayerNorm 和 BatchNorm 的区别如下图所示:

  • LayerNorm:对一个 batch 的所有通道进行归一化(均值为 0,方差为 1)
  • BatchNorm:对一个通道的所有 batch 进行归一化(均值为 0,方差为 1)

在这里插入图片描述

为什么使用 cls token:

  • 之前使用 CNN 做图像分类时,会通过多层卷积进行特征的提取,得到比如 14x14xC 的特征,然后每个通道的特征使用全局平均池化 GAP 来得到一个最终的特征,用于这个类别的整体特征表达,用于最后的分类任务
  • 所以这里的 cls token 也是类似经过 GAP 得到的全局特征,因为 cls token 在 transformer encoder 的过程中是会和全部的 token 进行交互的,所以是有能力蕴含全局信息的
  • 那么 transformer 这种多个 token 输入-多个token 输出的结构,是不是可以使用全部 token 的平均来表示类别呢,就是也做一个 GAP,经过实验来看也是可以的,但是作者为了证明不引入任何视觉的 trick 就使用原始的 transformer 结构是可行的,所以就使用了 cls token

三、效果

  • 使用中等大小的数据集(如 ImageNet),Transformer 比 ResNet 的效果稍微差点,作者认为原因在于 Transformer 缺少了 CNN 中的归纳偏置(平移不变性和位置),泛化的也较差
  • 使用大型数据集训练时(约 14M-300M images),作者发现大型的数据训练会胜过归纳偏置带来的效果,ViT 在使用了大型数据集预训练(ImageNet-21k 或 in-house JFT-300M)然后迁移到其他任务时,效果优于 CNN。

归纳偏置是什么?

归纳偏置可以理解成在算法在设计之初就加入的一种人为偏好,将某种方式的解优于其他解,既包含低层数据分布假设,也包含模型设计。

在深度学习时代,这种归纳性偏好更为明显。比如深度神经网络结构就偏好性的认为,层次化处理信息有更好效果;卷积神经网络认为信息具有空间局部性(locality),可以用滑动卷积共享权重方式降低参数空间;反馈神经网络则将时序信息考虑进来强调顺序重要性;图网络则是认为中心节点与邻居节点的相似性会更好引导信息流动。不同的网络结构创新就体现了不同的归纳性偏。

之前计算机视觉任务大都依赖于 CNN,CNN 有两个内置的归纳偏置:

  • 局部相关性
  • 权重共享

但基于注意力模型的 Transformer 最小化了归纳偏置,所以在大数据集上进行训练时,效果甚至可以超过 CNN,但小数据集上因为缺少了这种归纳偏置,所以难以总结到有意义的特征。

CNN 有较好的归纳偏置,所以数据少的时候也能实现好的效果,但数据量大的时候,这些归纳偏置就会限制其效果,但 Transformer 不会被其限制,所以在大数据集上表现更好一些。

ViT 用了哪些图像归纳偏置:

  • 在切 patch 的时候用了
  • 加位置编码的时候用了(位置编码开始的时候也是随机初始化的)
    在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

四、Vision Transformer 学习到图像的哪些特征了

为了理解 Transformer 是如何学习到图像特征的,作者分析了其内部的特征表达:

  • Transformer 的第一层将 flattened patch 线性影射到了一个低维空间(公式 1),图 7 左侧可视化了前几个主要的学习到的 embedding filters,这些组件类似于每个patch内精细结构的低维表示的可信基函数。
  • 线性投影之后,加上位置编码,图 7 中间展示了模型学习了在位置嵌入相似度下对图像内距离进行编码,即离得近的 patches 更趋向于有相似的位置嵌入,然后就有了 row-column 结构,同一行或同一列的 patches 有相似的嵌入。
  • 自注意力机制能够提取整幅图像的信息,作者为了探究这种注意力给网络起了多大的作用,根据注意力的权重计算了其在空间中的平均距离(图 7 右),这种”注意力距离”类似于 CNN 中的感受野的大小。作者注意到,一些 heads 趋向于关注最底层的大部分图像,这表明模型确实使用了全局整合信息的能力。其他注意头在较低层上的注意距离一直很小。这种高度位置化的注意在混合模型中不太明显,这些模型在Transformer之前应用了ResNet(图7,右),这表明它可能具有与cnn中的低层卷积层类似的功能。注意距离随网络深度的增加而增加。从全局来看,发现模型关注与分类语义相关的图像区域(图6)。

在这里插入图片描述
在这里插入图片描述
上面中间的热力图可视化,某个位置和自己的余弦相似度肯定是最高的,然后和同行同列相似度次高,其他位置较低,这也能基本想通,因为位置本来就表示的某个像素在图像中的某行某列,符合可视化结果。
在这里插入图片描述

在这里插入图片描述

五、代码

总体过程:

  • 输入原图:[1, 3, 224, 224]
  • patch 编码:[1, 49, 1024],7x7=49, 32x32=1024,这里可以看做有 49 个块儿,每个块儿中有 1024 个元素,相当于把每个小块都变成了深度方向
  • cls_token:[1, 50, 1024],这里是分类任务,每个图像只会有一个输出类别,所以在块儿(49)的维度上增加一维(变成 50),这个 cls_token 可以看做是最终预测的类别
  • 位置编码:[1, 50, 1024],位置编码是使用在每个 patch 上的,位置编码是 ( N + 1 ) D (N+1)D (N+1)D 维的,这里的 D=1024
  • Transformer:Attention + FeedForward: [1, 50, 1024]
  • 取第一组向量(或均值)作为全局特征:[1, 1024]
  • MLP 输出预测类别:[1, 1000],1000为类别数
import torch
from torch import nn

from einops import rearrange, repeat
from einops.layers.torch import Rearrange

# helpers

def pair(t):
    return t if isinstance(t, tuple) else (t, t)

# classes

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)

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)

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.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):
        import pdb; pdb.set_trace()
        qkv = self.to_qkv(x).chunk(3, dim = -1) # len=3, qkv[0].shape=qkv[1].shape=qkv[2].shape=[1, 50, 1024]
        q, k, v = map(lambda t: rearrange(t, 'b n (h d) -> b h n d', h = self.heads), qkv) # q.shape=k.shape=v.shape=[1, 16, 50, 64]

        dots = torch.matmul(q, k.transpose(-1, -2)) * self.scale # [1, 16, 50, 50]

        attn = self.attend(dots) # [1, 16, 50, 50]

        out = torch.matmul(attn, v) # [1, 16, 50, 64]
        out = rearrange(out, 'b h n d -> b n (h d)') # [1, 50, 1024]
        return self.to_out(out) # [1, 50, 1024]

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:
            # attn: attention
            # ff: feedforward
            x = attn(x) + x
            x = ff(x) + x
        return 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__()
        import pdb;
        pdb.set_trace()
        image_height, image_width = pair(image_size) # 224, 224
        patch_height, patch_width = pair(patch_size) # 32, 32

        assert image_height % patch_height == 0 and image_width % patch_width == 0, 'Image dimensions must be divisible by the patch size.'

        num_patches = (image_height // patch_height) * (image_width // patch_width) # 7*7=49
        patch_dim = channels * patch_height * patch_width # 3072
        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), # Linear(in_features=3072, out_features=1024, bias=True)
        )

        self.pos_embedding = nn.Parameter(torch.randn(1, num_patches + 1, dim)) # [1, 50, 1024]
        self.cls_token = nn.Parameter(torch.randn(1, 1, dim)) # [1, 1, 1024]
        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):
        # img: [1, 3, 224, 224]
        x = self.to_patch_embedding(img) # [1, 49, 1024]
        b, n, _ = x.shape

        cls_tokens = repeat(self.cls_token, '() n d -> b n d', b = b) # [1, 1, 1024] ->[b, 1, 1024]
        x = torch.cat((cls_tokens, x), dim=1)
        x += self.pos_embedding[:, :(n + 1)] # [1, 50, 1024]
        x = self.dropout(x) # [1, 50, 1024]

        x = self.transformer(x)

        x = x.mean(dim = 1) if self.pool == 'mean' else x[:, 0] # [1, 1024]

        x = self.to_latent(x)
        return self.mlp_head(x)

调用 ViT 的方法:

pip install vit-pytorch
import torch
from vit_pytorch import ViT

v = ViT(
    image_size = 256,
    patch_size = 32,
    num_classes = 1000,
    dim = 1024,
    depth = 6,
    heads = 16,
    mlp_dim = 2048,
    dropout = 0.1,
    emb_dropout = 0.1
)

img = torch.randn(1, 3, 256, 256)

preds = v(img) # (1, 1000)

Patch embedding 结构:

Sequential(
  (0): Rearrange('b c (h p1) (w p2) -> b (h w) (p1 p2 c)', p1=32, p2=32)
  (1): Linear(in_features=3072, out_features=1024, bias=True)
)

Transformer 结构:

Transformer(
  (layers): ModuleList(
    (0): ModuleList(
      (0): PreNorm(
        (norm): LayerNorm((1024,), eps=1e-05, elementwise_affine=True)
        (fn): Attention(
          (attend): Softmax(dim=-1)
          (to_qkv): Linear(in_features=1024, out_features=3072, bias=False)
          (to_out): Sequential(
            (0): Linear(in_features=1024, out_features=1024, bias=True)
            (1): Dropout(p=0.1, inplace=False)
          )
        )
      )
      (1): PreNorm(
        (norm): LayerNorm((1024,), eps=1e-05, elementwise_affine=True)
        (fn): FeedForward(
          (net): Sequential(
            (0): Linear(in_features=1024, out_features=2048, bias=True)
            (1): GELU()
            (2): Dropout(p=0.1, inplace=False)
            (3): Linear(in_features=2048, out_features=1024, bias=True)
            (4): Dropout(p=0.1, inplace=False)
          )
        )
      )
    )
    (1): ModuleList(
      (0): PreNorm(
        (norm): LayerNorm((1024,), eps=1e-05, elementwise_affine=True)
        (fn): Attention(
          (attend): Softmax(dim=-1)
          (to_qkv): Linear(in_features=1024, out_features=3072, bias=False)
          (to_out): Sequential(
            (0): Linear(in_features=1024, out_features=1024, bias=True)
            (1): Dropout(p=0.1, inplace=False)
          )
        )
      )
      (1): PreNorm(
        (norm): LayerNorm((1024,), eps=1e-05, elementwise_affine=True)
        (fn): FeedForward(
          (net): Sequential(
            (0): Linear(in_features=1024, out_features=2048, bias=True)
            (1): GELU()
            (2): Dropout(p=0.1, inplace=False)
            (3): Linear(in_features=2048, out_features=1024, bias=True)
            (4): Dropout(p=0.1, inplace=False)
          )
        )
      )
    )
    (2): ModuleList(
      (0): PreNorm(
        (norm): LayerNorm((1024,), eps=1e-05, elementwise_affine=True)
        (fn): Attention(
          (attend): Softmax(dim=-1)
          (to_qkv): Linear(in_features=1024, out_features=3072, bias=False)
          (to_out): Sequential(
            (0): Linear(in_features=1024, out_features=1024, bias=True)
            (1): Dropout(p=0.1, inplace=False)
          )
        )
      )
      (1): PreNorm(
        (norm): LayerNorm((1024,), eps=1e-05, elementwise_affine=True)
        (fn): FeedForward(
          (net): Sequential(
            (0): Linear(in_features=1024, out_features=2048, bias=True)
            (1): GELU()
            (2): Dropout(p=0.1, inplace=False)
            (3): Linear(in_features=2048, out_features=1024, bias=True)
            (4): Dropout(p=0.1, inplace=False)
          )
        )
      )
    )
    (3): ModuleList(
      (0): PreNorm(
        (norm): LayerNorm((1024,), eps=1e-05, elementwise_affine=True)
        (fn): Attention(
          (attend): Softmax(dim=-1)
          (to_qkv): Linear(in_features=1024, out_features=3072, bias=False)
          (to_out): Sequential(
            (0): Linear(in_features=1024, out_features=1024, bias=True)
            (1): Dropout(p=0.1, inplace=False)
          )
        )
      )
      (1): PreNorm(
        (norm): LayerNorm((1024,), eps=1e-05, elementwise_affine=True)
        (fn): FeedForward(
          (net): Sequential(
            (0): Linear(in_features=1024, out_features=2048, bias=True)
            (1): GELU()
            (2): Dropout(p=0.1, inplace=False)
            (3): Linear(in_features=2048, out_features=1024, bias=True)
            (4): Dropout(p=0.1, inplace=False)
          )
        )
      )
    )
    (4): ModuleList(
      (0): PreNorm(
        (norm): LayerNorm((1024,), eps=1e-05, elementwise_affine=True)
        (fn): Attention(
          (attend): Softmax(dim=-1)
          (to_qkv): Linear(in_features=1024, out_features=3072, bias=False)
          (to_out): Sequential(
            (0): Linear(in_features=1024, out_features=1024, bias=True)
            (1): Dropout(p=0.1, inplace=False)
          )
        )
      )
      (1): PreNorm(
        (norm): LayerNorm((1024,), eps=1e-05, elementwise_affine=True)
        (fn): FeedForward(
          (net): Sequential(
            (0): Linear(in_features=1024, out_features=2048, bias=True)
            (1): GELU()
            (2): Dropout(p=0.1, inplace=False)
            (3): Linear(in_features=2048, out_features=1024, bias=True)
            (4): Dropout(p=0.1, inplace=False)
          )
        )
      )
    )
    (5): ModuleList(
      (0): PreNorm(
        (norm): LayerNorm((1024,), eps=1e-05, elementwise_affine=True)
        (fn): Attention(
          (attend): Softmax(dim=-1)
          (to_qkv): Linear(in_features=1024, out_features=3072, bias=False)
          (to_out): Sequential(
            (0): Linear(in_features=1024, out_features=1024, bias=True)
            (1): Dropout(p=0.1, inplace=False)
          )
        )
      )
      (1): PreNorm(
        (norm): LayerNorm((1024,), eps=1e-05, elementwise_affine=True)
        (fn): FeedForward(
          (net): Sequential(
            (0): Linear(in_features=1024, out_features=2048, bias=True)
            (1): GELU()
            (2): Dropout(p=0.1, inplace=False)
            (3): Linear(in_features=2048, out_features=1024, bias=True)
            (4): Dropout(p=0.1, inplace=False)
          )
        )
      )
    )
  )
)

前传方式:

def forward(self, x):
    for attn, ff in self.layers:
        # attn: attention
        # ff: feedforward
        x = attn(x) + x
        x = ff(x) + x
    return x

MLP 结构:

Sequential(
  (0): LayerNorm((1024,), eps=1e-05, elementwise_affine=True)
  (1): Linear(in_features=1024, out_features=1000, bias=True)
)
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

呆呆的猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值