mindspore打卡第十二天VIT
模型特点
ViT模型主要应用于图像分类领域。因此,其模型结构相较于传统的Transformer有以下几个特点:
数据集的原图像被划分为多个patch(图像块)后,将二维patch(不考虑channel)转换为一维向量,再加上类别向量与位置向量作为模型输入。
模型主体的Block结构是基于Transformer的Encoder结构,但是调整了Normalization的位置,其中,最主要的结构依然是Multi-head Attention结构。
模型在Blocks堆叠后接全连接层,接受类别向量的输出作为输入并用于分类。通常情况下,我们将最后的全连接层称为Head,Transformer Encoder部分为backbone。
下面将通过代码实例来详细解释基于ViT实现ImageNet分类任务。
注意,本教程在CPU上运行时间过长,不建议使用CPU运行。
# 查看当前 mindspore 版本
!pip show mindspore
Name: mindspore
Version: 2.2.14
Summary: MindSpore is a new open source deep learning training/inference framework that could be used for mobile, edge and cloud scenarios.
Home-page: https://www.mindspore.cn
Author: The MindSpore Authors
Author-email: contact@mindspore.cn
License: Apache 2.0
Location: /home/nginx/miniconda/envs/jupyter/lib/python3.9/site-packages
Requires: asttokens, astunparse, numpy, packaging, pillow, protobuf, psutil, scipy
Required-by:
from download import download
dataset_url = "https://mindspore-website.obs.cn-north-4.myhuaweicloud.com/notebook/datasets/vit_imagenet_dataset.zip"
path = "./"
path = download(dataset_url, path, kind="zip", replace=True)
Downloading data from https://mindspore-website.obs.cn-north-4.myhuaweicloud.com/notebook/datasets/vit_imagenet_dataset.zip (489.1 MB)
file_sizes: 100%|█████████████████████████████| 513M/513M [00:02<00:00, 227MB/s]
Extracting zip file...
Successfully downloaded / unzipped to ./
import os
import mindspore as ms
from mindspore.dataset import ImageFolderDataset
import mindspore.dataset.vision as transforms
data_path = './dataset/'
mean = [0.485 * 255, 0.456 * 255, 0.406 * 255]
std = [0.229 * 255, 0.224 * 255, 0.225 * 255]
dataset_train = ImageFolderDataset(os.path.join(data_path, "train"), shuffle=True)
trans_train = [
transforms.RandomCropDecodeResize(size=224,
scale=(0.08, 1.0),
ratio=(0.75, 1.333)),
transforms.RandomHorizontalFlip(prob=0.5),
transforms.Normalize(mean=mean, std=std),
transforms.HWC2CHW()
]
dataset_train = dataset_train.map(operations=trans_train, input_columns=["image"])
dataset_train = dataset_train.batch(batch_size=16, drop_remainder=True)
Attention模块
以下是Self-Attention的解释,其核心内容是为输入向量的每个单词学习一个权重。通过给定一个任务相关的查询向量Query向量,计算Query和各个Key的相似性或者相关性得到注意力分布,即得到每个Key对应Value的权重系数,然后对Value进行加权求和得到最终的Attention数值。
在Self-Attention中:
- 最初的输入向量首先会经过Embedding层映射成Q(Query),K(Key),V(Value)三个向量,由于是并行操作,所以代码中是映射成为dim x 3的向量然后进行分割,换言之,如果你的输入向量为一个向量序列( x 1 x_1 x1, x 2 x_2 x2, x 3 x_3 x3),其中的 x 1 x_1 x1, x 2 x_2 x2, x 3 x_3 x3都是一维向量,那么每一个一维向量都会经过Embedding层映射出Q,K,V三个向量,只是Embedding矩阵不同,矩阵参数也是通过学习得到的。这里大家可以认为,Q,K,V三个矩阵是发现向量之间关联信息的一种手段,需要经过学习得到,至于为什么是Q,K,V三个,主要是因为需要两个向量点乘以获得权重,又需要另一个向量来承载权重向加的结果,所以,最少需要3个矩阵。
{ q i = W q ⋅ x i k i = W k ⋅ x i , i = 1 , 2 , 3 … v i = W v ⋅ x i (1) \begin{cases} q_i = W_q \cdot x_i & \\ k_i = W_k \cdot x_i,\hspace{1em} &i = 1,2,3 \ldots \\ v_i = W_v \cdot x_i & \end{cases} \tag{1} ⎩ ⎨ ⎧qi=Wq⋅xiki=Wk⋅xi,vi=Wv⋅xii=1,2,3…(1)
- 自注意力机制的自注意主要体现在它的Q,K,V都来源于其自身,也就是该过程是在提取输入的不同顺序的向量的联系与特征,最终通过不同顺序向量之间的联系紧密性(Q与K乘积经过Softmax的结果)来表现出来。Q,K,V得到后就需要获取向量间权重,需要对Q和K进行点乘并除以维度的平方根,对所有向量的结果进行Softmax处理,通过公式(2)的操作,我们获得了向量之间的关系权重。
{ a 1 , 1 = q 1 ⋅ k 1 / d a 1 , 2 = q 1 ⋅ k 2 / d a 1 , 3 = q 1 ⋅ k 3 / d (2) \begin{cases} a_{1,1} = q_1 \cdot k_1 / \sqrt d \\ a_{1,2} = q_1 \cdot k_2 / \sqrt d \\ a_{1,3} = q_1 \cdot k_3 / \sqrt d \end{cases} \tag{2} ⎩ ⎨ ⎧a1,1=q1⋅k1/da1,2=q1⋅k2/da1,3=q1⋅k3/d(2)
S o f t m a x : a ^ 1 , i = e x p ( a 1 , i ) / ∑ j e x p ( a 1 , j ) , j = 1 , 2 , 3 … (3) Softmax: \hat a_{1,i} = exp(a_{1,i}) / \sum_j exp(a_{1,j}),\hspace{1em} j = 1,2,3 \ldots \tag{3} Softmax:a^1,i=exp(a1,i)/j∑exp(a1,j),j=1,2,3…(3)
- 其最终输出则是通过V这个映射后的向量与Q,K经过Softmax结果进行weight sum获得,这个过程可以理解为在全局上进行自注意表示。每一组Q,K,V最后都有一个V输出,这是Self-Attention得到的最终结果,是当前向量在结合了它与其他向量关联权重后得到的结果。
b 1 = ∑ i a ^ 1 , i v i , i = 1 , 2 , 3... (4) b_1 = \sum_i \hat a_{1,i}v_i,\hspace{1em} i = 1,2,3... \tag{4} b1=i∑a^1,ivi,i=1,2,3...(4)
通过下图可以整体把握Self-Attention的全部过程。
多头注意力机制就是将原本self-Attention处理的向量分割为多个Head进行处理,这一点也可以从代码中体现,这也是attention结构可以进行并行加速的一个方面。
总结来说,多头注意力机制在保持参数总量不变的情况下,将同样的query, key和value映射到原来的高维空间(Q,K,V)的不同子空间(Q_0,K_0,V_0)中进行自注意力的计算,最后再合并不同子空间中的注意力信息。
所以,对于同一个输入向量,多个注意力机制可以同时对其进行处理,即利用并行计算加速处理过程,又在处理的时候更充分的分析和利用了向量特征。下图展示了多头注意力机制,其并行能力的主要体现在下图中的 a 1 a_1 a1和 a 2 a_2 a2是同一个向量进行分割获得的。
以下是Multi-Head Attention代码,结合上文的解释,代码清晰的展现了这一过程。
from mindspore import nn, ops
class Attention(nn.Cell):
def __init__(self,
dim: int,
num_heads: int = 8,
keep_prob: float = 1.0,
attention_keep_prob: float = 1.0):
super(Attention, self).__init__()
self.num_heads = num_heads
head_dim = dim // num_heads
self.scale = ms.Tensor(head_dim ** -0.5)
self.qkv = nn.Dense(dim, dim * 3)
self.attn_drop = nn.Dropout(p=1.0-attention_keep_prob)
self.out = nn.Dense(dim, dim)
self.out_drop = nn.Dropout(p=1.0-keep_prob)
self.attn_matmul_v = ops.BatchMatMul()
self.q_matmul_k = ops.BatchMatMul(transpose_b=True)
self.softmax = nn.Softmax(axis=-1)
def construct(self, x):
"""Attention construct."""
b, n, c = x.shape
qkv = self.qkv(x)
qkv = ops.reshape(qkv, (b, n, 3, self.num_heads, c // self.num_heads))
qkv = ops.transpose(qkv, (2, 0, 3, 1, 4))
q, k, v = ops.unstack(qkv, axis=0) ####分成3块数据了
attn = self.q_matmul_k(q, k)
attn = ops.mul(attn, self.scale)
attn = self.softmax(attn)
attn = self.attn_drop(attn)
out = self.attn_matmul_v(attn, v)
out = ops.transpose(out, (0, 2, 1, 3))
out = ops.reshape(out, (b, n, c))
out = self.out(out)
out = self.out_drop(out)
return out
from typing import Optional, Dict
class FeedForward(nn.Cell):
def __init__(self,
in_features: int,
hidden_features: Optional[int] = None,
out_features: Optional[int] = None,
activation: nn.Cell = nn.GELU,
keep_prob: float = 1.0):
super(FeedForward, self).__init__()
out_features = out_features or in_features
hidden_features = hidden_features or in_features
self.dense1 = nn.Dense(in_features, hidden_features)
self.activation = activation()
self.dense2 = nn.Dense(hidden_features, out_features)
self.dropout = nn.Dropout(p=1.0-keep_prob)
def construct(self, x):
"""Feed Forward construct."""
x = self.dense1(x)
x = self.activation(x)
x = self.dropout(x)
x = self.dense2(x)
x = self.dropout(x)
return x
class ResidualCell(nn.Cell):
def __init__(self, cell):
super(ResidualCell, self).__init__()
self.cell = cell
def construct(self, x):
"""ResidualCell construct."""
return self.cell(x) + x
class TransformerEncoder(nn.Cell):
def __init__(self,
dim: int,
num_layers: int,
num_heads: int,
mlp_dim: int,
keep_prob: float = 1.,
attention_keep_prob: float = 1.0,
drop_path_keep_prob: float = 1.0,
activation: nn.Cell = nn.GELU,
norm: nn.Cell = nn.LayerNorm):
super(TransformerEncoder, self).__init__()
layers = []
for _ in range(num_layers):
normalization1 = norm((dim,))
normalization2 = norm((dim,))
attention = Attention(dim=dim,
num_heads=num_heads,
keep_prob=keep_prob,
attention_keep_prob=attention_keep_prob)
feedforward = FeedForward(in_features=dim,
hidden_features=mlp_dim,
activation=activation,
keep_prob=keep_prob)
layers.append(
nn.SequentialCell([
ResidualCell(nn.SequentialCell([normalization1, attention])),
ResidualCell(nn.SequentialCell([normalization2, feedforward]))
])
)
self.layers = nn.SequentialCell(layers)
def construct(self, x):
"""Transformer construct."""
return self.layers(x)
ViT模型的输入
传统的Transformer结构主要用于处理自然语言领域的词向量(Word Embedding or Word Vector),词向量与传统图像数据的主要区别在于,词向量通常是一维向量进行堆叠,而图片则是二维矩阵的堆叠,多头注意力机制在处理一维词向量的堆叠时会提取词向量之间的联系也就是上下文语义,这使得Transformer在自然语言处理领域非常好用,而二维图片矩阵如何与一维词向量进行转化就成为了Transformer进军图像处理领域的一个小门槛。
在ViT模型中:
-
通过将输入图像在每个channel上划分为16*16个patch,这一步是通过卷积操作来完成的,当然也可以人工进行划分,但卷积操作也可以达到目的同时还可以进行一次而外的数据处理;例如一幅输入224 x 224的图像,首先经过卷积处理得到16 x 16个patch,那么每一个patch的大小就是14 x 14。
-
再将每一个patch的矩阵拉伸成为一个一维向量,从而获得了近似词向量堆叠的效果。上一步得到的14 x 14的patch就转换为长度为196的向量。
这是图像输入网络经过的第一步处理。具体Patch Embedding的代码如下所示:
14*14 ###patch数量
196
224//16
14
class PatchEmbedding(nn.Cell):
MIN_NUM_PATCHES = 4
def __init__(self,
image_size: int = 224,
patch_size: int = 16,
embed_dim: int = 768,
input_channels: int = 3):
super(PatchEmbedding, self).__init__()
self.image_size = image_size
self.patch_size = patch_size
self.num_patches = (image_size // patch_size) ** 2 #####数量是14*14
self.conv = nn.Conv2d(input_channels, embed_dim, kernel_size=patch_size, stride=patch_size, has_bias=True)
####被映射为 768 的dmodel [max_len,d]
def construct(self, x):
"""Path Embedding construct."""
x = self.conv(x)
b, c, h, w = x.shape
x = ops.reshape(x, (b, c, h * w))
x = ops.transpose(x, (0, 2, 1))
return x
输入图像在划分为patch之后,会经过pos_embedding 和 class_embedding两个过程。
-
class_embedding主要借鉴了BERT模型的用于文本分类时的思想,在每一个word vector之前增加一个类别值,通常是加在向量的第一位,上一步得到的196维的向量加上class_embedding后变为197维。
-
增加的class_embedding是一个可以学习的参数,经过网络的不断训练,最终以输出向量的第一个维度的输出来决定最后的输出类别;由于输入是16 x 16的patch,所以输出进行分类时是取 16 x 16个class_embedding进行分类。
-
pos_embedding也是一组可以学习的参数,会被加入到经过处理的patch矩阵中。
-
由于pos_embedding也是可以学习的参数,所以它的加入类似于全链接网络和卷积的bias。这一步就是创造一个长度维197的可训练向量加入到经过class_embedding的向量中。
实际上,pos_embedding总共有4种方案。但是经过作者的论证,只有加上pos_embedding和不加pos_embedding有明显影响,至于pos_embedding是一维还是二维对分类结果影响不大,所以,在我们的代码中,也是采用了一维的pos_embedding,由于class_embedding是加在pos_embedding之前,所以pos_embedding的维度会比patch拉伸后的维度加1。
总的而言,ViT模型还是利用了Transformer模型在处理上下文语义时的优势,将图像转换为一种“变种词向量”然后进行处理,而这样转换的意义在于,多个patch之间本身具有空间联系,这类似于一种“空间语义”,从而获得了比较好的处理效果。
整体构建ViT
以下代码构建了一个完整的ViT模型。
这段代码定义了一个名为 PatchEmbedding
的类,它是深度学习模型中用于图像处理的组件,尤其是为 Vision Transformer (ViT) 类型的模型准备输入数据。下面是对这段代码的详细解析:
类定义与初始化
- 类名:
PatchEmbedding
- 父类:
nn.Cell
,这是 MindSpore(一个开源的深度学习框架)中神经网络模块的基本单元。
初始化参数
image_size
: 图像的输入尺寸,默认为 224x224 像素。patch_size
: 每个 patch 的边长,默认为 16 像素。embed_dim
: patch embedding 的维度,即每个 patch 转换后的向量维度,默认为 768。input_channels
: 输入图像的通道数,默认为 3,适合 RGB 图像。
属性计算
num_patches
: 计算出图像分割成多少个 patch。公式基于图像尺寸除以 patch 尺寸的平方,此处的注释有误,应为 ((\frac{image_size}{patch_size})^2),即 ((\frac{224}{16})^2 = 14^2 = 196),而不是14*14。
层定义
nn.Conv2d
: 定义了一个卷积层,用于将图像数据转化为 patch embeddings。它接收input_channels
通道的输入,输出维度为embed_dim
,卷积核大小与步长均为patch_size
,并且包含偏置(has_bias=True
)。
构造函数 (construct
方法)
- 输入:
x
,预期为形状为(batch_size, input_channels, image_size, image_size)
的四维张量,即一批图像数据。 - 处理过程:
- 使用定义的卷积层
self.conv
对输入图像进行卷积操作,将图像分割成 patches 并映射到embed_dim
维度。 - 调整形状(
ops.reshape
),将输出从(batch_size, embed_dim, new_height, new_width)
调整为(batch_size, embed_dim, num_patches)
,其中new_height
和new_width
是卷积后每个维度上的 patch 数量。 - 转置(
ops.transpose
)操作,将形状进一步调整为(batch_size, num_patches, embed_dim)
,以便每个样本的 patches 排列为序列,且每个 patch 是一个embed_dim
维度的向量,这正是Transformer架构所需的序列输入格式。
- 使用定义的卷积层
总结
这段代码实现了 Vision Transformer 输入部分的关键组件,即如何将原始图像分割成 patches,并通过卷积操作转换为固定维度的向量表示,为后续的 Transformer 层处理做准备。注意,代码注释中提到的 “数量是14*14” 应修正为基于正确计算的 num_patches
值,即196。
在您的描述中,存在一些误解,让我澄清并提供正确的理解:
-
关于Class Embedding的维度:您提到的“上一步得到的196维的向量”是基于对patch像素的直接计数,这是不准确的。在ViT中,每个patch通过一个线性层(patch embedding layer)转换成一个高维向量,比如转换成768维,而不是196维。因此,当添加一个class embedding时,这个class embedding也会是768维,与patch embeddings的维度匹配,而不是将196维的向量加上class embedding变为197维。
-
关于分类过程:您说“输出进行分类时是取 16 x 16个class_embedding进行分类”,这也是错误的。实际上,整个序列中只有一个class embedding,它被放置在所有patch embeddings的前面,起到聚合所有patch信息的作用。在分类时,仅使用序列中的class token对应的输出进行分类,而不是每个patch都有一个class embedding。
-
关于Positional Embedding的处理:您提到的“创造一个长度维197的可训练向量加入到经过class_embedding的向量中”是基于错误的维度理解。正确的是,positional embeddings和每个patch embedding的维度相同(例如768维),并且直接加到每个对应的patch embedding上,包括class token对应的向量。这意味着,positional embeddings的加入是为了提供位置信息,而不是增加新的维度,因此不会使向量维度变为197维。
综上所述,ViT模型的基本流程是这样的:
- 图像被划分为多个patch。
- 每个patch通过一个线性层转换成一个固定维度的向量(如768维)。
- 在所有patch embeddings前添加一个单独的class token,同样为768维。
- 对每个patch embedding(包括class token)加上一个维度相同的positional embedding。
- 将这个序列输入Transformer进行特征学习。
- 分类时,只使用class token的输出通过一个分类头进行分类。
基于以上纠正,如果您的代码构建了符合上述流程的ViT模型,那么整体框架是对的,但请确保代码中正确实施了上述每个步骤,特别是在处理class embedding和positional embedding时,避免了上述提及的误解。
如果按照您的要求调整patch的大小和数量,即图像被划分为 (14 \times 14 = 196) 个patch,每个patch的大小为 (16 \times 16) 像素,我们可以相应地调整示例说明:
更新后的理解:
-
图像分割:现在,图像被切分为 (14 \times 14 = 196) 个小块,每个小块称为一个patch,意味着每个patch的尺寸变大为 (16 \times 16) 像素。
-
处理每个patch的像素:尽管每个patch的像素数量增多((16 \times 16 = 256) 个像素),处理方式不变,即这些像素通过一个线性层(patch embedding layer)转换成一个固定维度的向量,例如768维。
-
Class Token的作用及Positional Embeddings:这部分处理方式与之前描述一致。Class Token依旧作为一个单独的768维向量添加在所有patch embeddings的前面,且每个patch embedding(包括class token)会加上与其维度相匹配的positional embeddings,保持每个向量维度为768维。
-
输入和处理流程更新:
- 输入:一张 (H \times W = 224 \times 224) 的单通道图像,现在分成 (14 \times 14 = 196) 个 (16 \times 16) 的patch。
- 处理:每个patch通过线性层转换成768维向量。
- 添加Class Token:在所有patch embeddings前添加一个768维的class token,使得序列长度变为197。
- 加入Positional Embeddings:每个patch embedding(包括class token)加上对应的positional embeddings,每个向量依旧是768维。
- Transformer处理和分类:序列进入Transformer进行特征提取,最终利用class token的输出通过分类头进行分类,预测图像类别。
请注意,这种调整意味着图像被划分得更加精细,每个patch覆盖的图像面积更小,但每个patch的分辨率提高,这可能会对模型捕捉细节的能力产生影响。同时,处理的序列长度减少,反映了模型在处理更高分辨率图像细分时的配置。
from mindspore.common.initializer import Normal
from mindspore.common.initializer import initializer
from mindspore import Parameter
def init(init_type, shape, dtype, name, requires_grad):
"""Init."""
initial = initializer(init_type, shape, dtype).init_data()
return Parameter(initial, name=name, requires_grad=requires_grad)
class ViT(nn.Cell):
def __init__(self,
image_size: int = 224,
input_channels: int = 3,
patch_size: int = 16,
embed_dim: int = 768,
num_layers: int = 12,
num_heads: int = 12,
mlp_dim: int = 3072,
keep_prob: float = 1.0,
attention_keep_prob: float = 1.0,
drop_path_keep_prob: float = 1.0,
activation: nn.Cell = nn.GELU,
norm: Optional[nn.Cell] = nn.LayerNorm,
pool: str = 'cls') -> None:
super(ViT, self).__init__()
self.patch_embedding = PatchEmbedding(image_size=image_size,
patch_size=patch_size,
embed_dim=embed_dim,
input_channels=input_channels)
num_patches = self.patch_embedding.num_patches
self.cls_token = init(init_type=Normal(sigma=1.0),
shape=(1, 1, embed_dim),
dtype=ms.float32,
name='cls',
requires_grad=True)
self.pos_embedding = init(init_type=Normal(sigma=1.0),
shape=(1, num_patches + 1, embed_dim),
dtype=ms.float32,
name='pos_embedding',
requires_grad=True)
self.pool = pool
self.pos_dropout = nn.Dropout(p=1.0-keep_prob)
self.norm = norm((embed_dim,))
self.transformer = TransformerEncoder(dim=embed_dim,
num_layers=num_layers,
num_heads=num_heads,
mlp_dim=mlp_dim,
keep_prob=keep_prob,
attention_keep_prob=attention_keep_prob,
drop_path_keep_prob=drop_path_keep_prob,
activation=activation,
norm=norm)
self.dropout = nn.Dropout(p=1.0-keep_prob)
self.dense = nn.Dense(embed_dim, num_classes)
def construct(self, x):
"""ViT construct."""
x = self.patch_embedding(x)
cls_tokens = ops.tile(self.cls_token.astype(x.dtype), (x.shape[0], 1, 1))
x = ops.concat((cls_tokens, x), axis=1)
x += self.pos_embedding
x = self.pos_dropout(x)
x = self.transformer(x)
x = self.norm(x)
x = x[:, 0]
if self.training:
x = self.dropout(x)
x = self.dense(x)
return x
整体流程图如下所示:
from mindspore.nn import LossBase
from mindspore.train import LossMonitor, TimeMonitor, CheckpointConfig, ModelCheckpoint
from mindspore import train
# define super parameter
epoch_size = 10
momentum = 0.9
num_classes = 1000
resize = 224
step_size = dataset_train.get_dataset_size()
# construct model
network = ViT()
# load ckpt
vit_url = "https://download.mindspore.cn/vision/classification/vit_b_16_224.ckpt"
path = "./ckpt/vit_b_16_224.ckpt"
vit_path = download(vit_url, path, replace=True)
param_dict = ms.load_checkpoint(vit_path)
ms.load_param_into_net(network, param_dict)
# define learning rate
lr = nn.cosine_decay_lr(min_lr=float(0),
max_lr=0.00005,
total_step=epoch_size * step_size,
step_per_epoch=step_size,
decay_epoch=10)
# define optimizer
network_opt = nn.Adam(network.trainable_params(), lr, momentum)
# define loss function
class CrossEntropySmooth(LossBase):
"""CrossEntropy."""
def __init__(self, sparse=True, reduction='mean', smooth_factor=0., num_classes=1000):
super(CrossEntropySmooth, self).__init__()
self.onehot = ops.OneHot()
self.sparse = sparse
self.on_value = ms.Tensor(1.0 - smooth_factor, ms.float32)
self.off_value = ms.Tensor(1.0 * smooth_factor / (num_classes - 1), ms.float32)
self.ce = nn.SoftmaxCrossEntropyWithLogits(reduction=reduction)
def construct(self, logit, label):
if self.sparse:
label = self.onehot(label, ops.shape(logit)[1], self.on_value, self.off_value)
loss = self.ce(logit, label)
return loss
network_loss = CrossEntropySmooth(sparse=True,
reduction="mean",
smooth_factor=0.1,
num_classes=num_classes)
# set checkpoint
ckpt_config = CheckpointConfig(save_checkpoint_steps=step_size, keep_checkpoint_max=100)
ckpt_callback = ModelCheckpoint(prefix='vit_b_16', directory='./ViT', config=ckpt_config)
# initialize model
# "Ascend + mixed precision" can improve performance
ascend_target = (ms.get_context("device_target") == "Ascend")
if ascend_target:
model = train.Model(network, loss_fn=network_loss, optimizer=network_opt, metrics={"acc"}, amp_level="O2")
else:
model = train.Model(network, loss_fn=network_loss, optimizer=network_opt, metrics={"acc"}, amp_level="O0")
# train model
model.train(epoch_size,
dataset_train,
callbacks=[ckpt_callback, LossMonitor(125), TimeMonitor(125)],
dataset_sink_mode=False,)
Downloading data from https://download-mindspore.osinfra.cn/vision/classification/vit_b_16_224.ckpt (330.2 MB)
file_sizes: 100%|████████████████████████████| 346M/346M [00:20<00:00, 17.2MB/s]
Successfully downloaded file to ./ckpt/vit_b_16_224.ckpt
epoch: 1 step: 125, loss is 2.099737
Train epoch time: 301994.758 ms, per step time: 2415.958 ms
epoch: 2 step: 125, loss is 1.3709877
Train epoch time: 24101.688 ms, per step time: 192.814 ms
epoch: 3 step: 125, loss is 2.2235644
Train epoch time: 24227.373 ms, per step time: 193.819 ms
epoch: 4 step: 125, loss is 1.4042783
Train epoch time: 23832.873 ms, per step time: 190.663 ms
epoch: 5 step: 125, loss is 1.1897094
Train epoch time: 23965.551 ms, per step time: 191.724 ms
epoch: 6 step: 125, loss is 1.8742183
Train epoch time: 23831.645 ms, per step time: 190.653 ms
epoch: 7 step: 125, loss is 1.2478522
Train epoch time: 23663.779 ms, per step time: 189.310 ms
epoch: 8 step: 125, loss is 1.1760094
Train epoch time: 24017.155 ms, per step time: 192.137 ms
epoch: 9 step: 125, loss is 1.1438241
Train epoch time: 23742.300 ms, per step time: 189.938 ms
epoch: 10 step: 125, loss is 1.401642
Train epoch time: 23578.345 ms, per step time: 188.627 ms
在给出的代码中,确实体现了输入到Transformer的序列是1个class token加上256个patch embeddings,共计257个向量。以下是具体的分析和示例说明:
代码分析
-
初始化: 代码首先定义了用于初始化参数的方法
init
,用以创建具有指定类型的参数(如正态分布初始化),并设置参数的名称、是否需要梯度等属性。 -
ViT类构造方法: 在
ViT
类的构造方法中,完成了以下关键步骤:- Patch Embedding: 调用了之前定义的
PatchEmbedding
类,它将图像分割成 (16 \times 16) 大小的patches,并通过卷积层将每个patch映射到768维的向量。对于 (224 \times 224) 的图像,分成 (14 \times 14 = 256) 个patch。 - Class Token: 初始化了一个单独的class token,形状为 ((1, 1, 768)),并会在训练时复制到每个batch的样本中,使得每个样本都有这个相同的class token。
- Positional Embedding: 初始化了与patch数量加1(包括class token)相对应的位置嵌入,形状为 ((1, 257, 768)),确保每个输入(包括class token)都有位置信息。
- 构造函数内部:
- 拼接Class Token: 将class token通过tile操作复制并拼接到patch embeddings序列的前面,形成 ((batch_size, 257, 768)) 的张量,其中第一个向量是class token,其余是patch embeddings。
- 加入Positional Embedding: 将上述张量与位置嵌入相加,使得每个向量不仅包含图像局部信息,还有位置信息。
- Transformer处理: 将得到的序列输入到Transformer Encoder中进行特征提取和编码。Transformer通过自我注意力机制和多层处理,使class token能学习到整个序列(所有patch信息)的综合表示。
- 分类前处理: 经过Transformer处理后的输出先经过LayerNorm,然后只提取第一个元素(即class token的输出),如果在训练模式下,还会经过Dropout层。
- 分类: 最后,class token的输出通过一个全连接层(Dense layer)转换为分类概率分布,完成图像的分类任务。
- Patch Embedding: 调用了之前定义的
示例说明
假设我们有一批图像(记为 (B) 个样本),每张图像被切割成 (14 \times 14 = 256) 个patch,每个patch通过 PatchEmbedding
转换为 (768) 维的向量。此时,每个样本有 (256) 个这样的向量。对于每个样本,我们还会添加一个初始的class token,它同样是一个 (768) 维的向量。因此,对于每个样本,我们最终得到一个 (257) 项(包括1个class token和256个patch embeddings)的序列,每项都是 (768) 维的。
这个序列 ([CLS, patch_1, …, patch_{256}]) 通过加上位置嵌入,进入Transformer层。Transformer处理后,序列中的信息经过混合和传递,class token累积了整个序列的信息,成为了图像的综合表示。在模型的输出阶段,我们只关注class token对应的输出,因为它蕴含了整个图像的全局信息。这个输出向量通过全连接层(self.dense
)转换为各个类别的概率分布,从而实现对图像的分类。
模型验证
模型验证过程主要应用了ImageFolderDataset,CrossEntropySmooth和Model等接口。
ImageFolderDataset主要用于读取数据集。
CrossEntropySmooth是损失函数实例化接口。
Model主要用于编译模型。
与训练过程相似,首先进行数据增强,然后定义ViT网络结构,加载预训练模型参数。随后设置损失函数,评价指标等,编译模型后进行验证。本案例采用了业界通用的评价标准Top_1_Accuracy和Top_5_Accuracy评价指标来评价模型表现。
在本案例中,这两个指标代表了在输出的1000维向量中,以最大值或前5的输出值所代表的类别为预测结果时,模型预测的准确率。这两个指标的值越大,代表模型准确率越高。
dataset_val = ImageFolderDataset(os.path.join(data_path, "val"), shuffle=True)
trans_val = [
transforms.Decode(),
transforms.Resize(224 + 32),
transforms.CenterCrop(224),
transforms.Normalize(mean=mean, std=std),
transforms.HWC2CHW()
]
dataset_val = dataset_val.map(operations=trans_val, input_columns=["image"])
dataset_val = dataset_val.batch(batch_size=16, drop_remainder=True)
# construct model
network = ViT()
# load ckpt
param_dict = ms.load_checkpoint(vit_path)
ms.load_param_into_net(network, param_dict)
network_loss = CrossEntropySmooth(sparse=True,
reduction="mean",
smooth_factor=0.1,
num_classes=num_classes)
# define metric
eval_metrics = {'Top_1_Accuracy': train.Top1CategoricalAccuracy(),
'Top_5_Accuracy': train.Top5CategoricalAccuracy()}
if ascend_target:
model = train.Model(network, loss_fn=network_loss, optimizer=network_opt, metrics=eval_metrics, amp_level="O2")
else:
model = train.Model(network, loss_fn=network_loss, optimizer=network_opt, metrics=eval_metrics, amp_level="O0")
# evaluate model
result = model.eval(dataset_val)
print(result)
{'Top_1_Accuracy': 0.7495, 'Top_5_Accuracy': 0.928}
从结果可以看出,由于我们加载了预训练模型参数,模型的Top_1_Accuracy和Top_5_Accuracy达到了很高的水平,实际项目中也可以以此准确率为标准。如果未使用预训练模型参数,则需要更多的epoch来训练。
模型推理
在进行模型推理之前,首先要定义一个对推理图片进行数据预处理的方法。该方法可以对我们的推理图片进行resize和normalize处理,这样才能与我们训练时的输入数据匹配。
本案例采用了一张Doberman的图片作为推理图片来测试模型表现,期望模型可以给出正确的预测结果。
dataset_infer = ImageFolderDataset(os.path.join(data_path, "infer"), shuffle=True)
trans_infer = [
transforms.Decode(),
transforms.Resize([224, 224]),
transforms.Normalize(mean=mean, std=std),
transforms.HWC2CHW()
]
dataset_infer = dataset_infer.map(operations=trans_infer,
input_columns=["image"],
num_parallel_workers=1)
dataset_infer = dataset_infer.batch(1)
接下来,我们将调用模型的predict方法进行模型。
在推理过程中,通过index2label就可以获取对应标签,再通过自定义的show_result接口将结果写在对应图片上。
import os
import pathlib
import cv2
import numpy as np
from PIL import Image
from enum import Enum
from scipy import io
class Color(Enum):
"""dedine enum color."""
red = (0, 0, 255)
green = (0, 255, 0)
blue = (255, 0, 0)
cyan = (255, 255, 0)
yellow = (0, 255, 255)
magenta = (255, 0, 255)
white = (255, 255, 255)
black = (0, 0, 0)
def check_file_exist(file_name: str):
"""check_file_exist."""
if not os.path.isfile(file_name):
raise FileNotFoundError(f"File `{file_name}` does not exist.")
def color_val(color):
"""color_val."""
if isinstance(color, str):
return Color[color].value
if isinstance(color, Color):
return color.value
if isinstance(color, tuple):
assert len(color) == 3
for channel in color:
assert 0 <= channel <= 255
return color
if isinstance(color, int):
assert 0 <= color <= 255
return color, color, color
if isinstance(color, np.ndarray):
assert color.ndim == 1 and color.size == 3
assert np.all((color >= 0) & (color <= 255))
color = color.astype(np.uint8)
return tuple(color)
raise TypeError(f'Invalid type for color: {type(color)}')
def imread(image, mode=None):
"""imread."""
if isinstance(image, pathlib.Path):
image = str(image)
if isinstance(image, np.ndarray):
pass
elif isinstance(image, str):
check_file_exist(image)
image = Image.open(image)
if mode:
image = np.array(image.convert(mode))
else:
raise TypeError("Image must be a `ndarray`, `str` or Path object.")
return image
def imwrite(image, image_path, auto_mkdir=True):
"""imwrite."""
if auto_mkdir:
dir_name = os.path.abspath(os.path.dirname(image_path))
if dir_name != '':
dir_name = os.path.expanduser(dir_name)
os.makedirs(dir_name, mode=777, exist_ok=True)
image = Image.fromarray(image)
image.save(image_path)
def imshow(img, win_name='', wait_time=0):
"""imshow"""
cv2.imshow(win_name, imread(img))
if wait_time == 0: # prevent from hanging if windows was closed
while True:
ret = cv2.waitKey(1)
closed = cv2.getWindowProperty(win_name, cv2.WND_PROP_VISIBLE) < 1
# if user closed window or if some key pressed
if closed or ret != -1:
break
else:
ret = cv2.waitKey(wait_time)
def show_result(img: str,
result: Dict[int, float],
text_color: str = 'green',
font_scale: float = 0.5,
row_width: int = 20,
show: bool = False,
win_name: str = '',
wait_time: int = 0,
out_file: Optional[str] = None) -> None:
"""Mark the prediction results on the picture."""
img = imread(img, mode="RGB")
img = img.copy()
x, y = 0, row_width
text_color = color_val(text_color)
for k, v in result.items():
if isinstance(v, float):
v = f'{v:.2f}'
label_text = f'{k}: {v}'
cv2.putText(img, label_text, (x, y), cv2.FONT_HERSHEY_COMPLEX,
font_scale, text_color)
y += row_width
if out_file:
show = False
imwrite(img, out_file)
if show:
imshow(img, win_name, wait_time)
def index2label():
"""Dictionary output for image numbers and categories of the ImageNet dataset."""
metafile = os.path.join(data_path, "ILSVRC2012_devkit_t12/data/meta.mat")
meta = io.loadmat(metafile, squeeze_me=True)['synsets']
nums_children = list(zip(*meta))[4]
meta = [meta[idx] for idx, num_children in enumerate(nums_children) if num_children == 0]
_, wnids, classes = list(zip(*meta))[:3]
clssname = [tuple(clss.split(', ')) for clss in classes]
wnid2class = {wnid: clss for wnid, clss in zip(wnids, clssname)}
wind2class_name = sorted(wnid2class.items(), key=lambda x: x[0])
mapping = {}
for index, (_, class_name) in enumerate(wind2class_name):
mapping[index] = class_name[0]
return mapping
# Read data for inference
for i, image in enumerate(dataset_infer.create_dict_iterator(output_numpy=True)):
image = image["image"]
image = ms.Tensor(image)
prob = model.predict(image)
label = np.argmax(prob.asnumpy(), axis=1)
mapping = index2label()
output = {int(label): mapping[int(label)]}
print(output)
show_result(img="./dataset/infer/n01440764/ILSVRC2012_test_00000279.JPEG",
result=output,
out_file="./dataset/infer/ILSVRC2012_test_00000279.JPEG")
{236: 'Doberman'}
print("yangge mindspore 打卡第十二天 trainsformer之VIT 2024-07-02")
yangge mindspore 打卡第十二天 trainsformer之VIT 2024-07-02