Visual Transformer Classic Papers - ViT、DeiT、および原則の解釈と実装

Visual Transformer Classic Papers - ViT、DeiT、および原則の解釈と実装

最近ではChatGPTやWenxin Yiyanなどの大型モデルが普及していますが、その原理を探る上で2017年に提案されたTransformer構造は避けて通れません。Transformer アルゴリズムが提案されて以来、さまざまな分野で関連する研究がまだ数多くありますが、ここでは他のプラットフォームからのメモを共有し、CV 分野における 2 つの古典的な Transformer シリーズの研究である ViT と DeiT について詳しく説明します。

ViTアルゴリズムの概要

論文アドレス:画像は 16x16 ワードの価値がある: 大規模な画像認識のためのトランスフォーマー

以前のアルゴリズムのほとんどは、CNN の全体的な構造を変更せずに維持し、アテンション モジュールを CNN に追加するか、アテンション モジュールを使用して CNN の一部を置き換えていました。ViT アルゴリズムでは、常に CNN に依存する必要はなく、Transformer 構造を使用するだけで画像分類タスクも適切に実行できると著者は提案しています。

NLP 分野での Transformer の適用の成功に触発された ViT アルゴリズムは、標準の Transformer 構造を画像に直接適用しようとし、画像分類プロセス全体に最小限の変更を加えます。具体的には、ViT アルゴリズムでは、画像全体が小さな画像ブロックに分割され、これらの小さな画像ブロックの線形埋め込みシーケンスが Transformer の入力としてネットワークに送信され、教師あり学習を使用して画像分類トレーニングが実行されます。 。ViTアルゴリズム図 1 に示します。


図 1: ViT アルゴリズム構造の概略図

このアルゴリズムは、中規模 (ImageNet など) および大規模 (ImageNet-21K、JFT-300M など) のデータセットで実験的に検証され、次のことがわかりました。

  • CNN 構造と比較して、Tranformer には特定の翻訳不変性とローカル認識が欠けているため、データ量が不十分な場合には同じ効果を達成することが困難です。具体的には、中型の ImageNet でトレーニングされた Transformer は、ResNet よりも精度が数パーセント低くなります。
  • トレーニング サンプルの数が多い場合、結果は変化します。大規模なデータセットで事前トレーニングした後、転移学習によって他のデータセットに適用でき、現在の SOTA レベルに達するか、それを超える可能性があります。

図 2 は、大規模データセットを使用して事前トレーニングされ、トレーニングのために他の小規模データセットに移行された ViT アルゴリズムを示し、CNN 構造を使用した SOTA アルゴリズムの精度と比較しています。


図 2: ViT モデルの精度比較

図の最初の 3 つの列は、さまざまなスケールの ViT モデルであり、さまざまな大規模データ セットを使用して事前トレーニングされ、各サブタスクの結果に移行されます。4 番目の列は、JFT-300M データセットに基づく事前トレーニング後の BiT アルゴリズムの各サブタスクへの移行の結果です。列 5 は、ImageNet および ImageNet Real データセットに対して 2020 年に提案された半教師ありアルゴリズム Noisy Student の結果を示しています。


例証します:

BiTとNoisy Studentはどちらも2020年に提案されたSOTAアルゴリズムです。

BiT アルゴリズム: ResNet 構造の事前学習に大規模データセット JFT-300M を使用します. その中で、著者はモデルが大きいほど事前学習効果が高いことを発見しました. 最終的な最高のインデックスは幅 4 倍で、深さ 152 層 ResNet 152 × 4 ResNet152 \times 4レスネット152 _ _ _×論文アドレス:Big Transfer (BiT): 一般的な視覚表現の学習

Noisy Student Algorithm: EfficientNet 構造に基づく知識蒸留テクノロジーを使用し、ラベルなしのデータを使用してトレーニングの精度を向上させます。論文のアドレス: Noisy Student による自己トレーニングにより ImageNet 分類が改善されました


次に、ViT アルゴリズムの各コンポーネントを個別に見ていきます。

画像チャンクの埋め込み

前のコースで学んだことを考慮すると、Transformer 構造では、入力は 2 次元行列である必要があり、行列の形状は( N , D ) (N,D)として表すことができます。( N D )、ここでNNNはシーケンスの長さ、DDDは、シーケンス内の各ベクトルの次元です。したがって、ViT アルゴリズムでは、まずH × W × CH \times W \times C の変換を試みる必要があります。H×W×Cの 3 次元画像は( N , D ) (N,D)に変換されます。( N D ) 2次元入力。

ViT での具体的な実装は次のとおりです: H × W × CH \times W \times CH×W×Cの画像は N × ( P 2 ∗ C ) N \times (P^2 * C) になりますN×( P2C )シーケンス。このシーケンスは、一連の平坦化された画像ブロックとみなすことができます。つまり、画像が小さなブロックに分割された後、平坦化されます。シーケンスには合計N = HW / P 2 N=HW/P^2 がN=HW / P _2 つの画像ブロック。各画像ブロックの寸法は( P 2 ∗ C ) (P^2*C)( P2C )うちPPPは画像ブロックのサイズ、CCCはチャネル数です。上記の変換後、 NN は次のようになります。Nは系列の長さとみなされます。

ただし、このときの各画像ブロックの次元は( P 2 ∗ C ) (P^2*C)となります。( P2C )、実際に必要なベクトル次元はDDDなので、画像ブロックを埋め込む必要もあります。ここでの埋め込み方法は非常に簡単で、それぞれ( P 2 ∗ C ) (P^2*C)( P2C )画像ブロックは線形変換を実行して次元をDDDで十分です。

上述した画像のブロック分割と埋め込み図3に示す。


図 3: 画像ブロック埋め込みの概略図

具体的なコード実装は以下の通りです。PPサイズを使用した場合代わりに、各サイズPPに対してP畳み込みが使用されます。Pイメージ ブロックがフラット化された後、完全な接続を使用して操作を実行するプロセス。

# coding=utf-8
# 导入环境
import os
import numpy as np
import cv2
from PIL import Image
import paddle
from paddle.io import Dataset
from paddle.nn import Conv2D, MaxPool2D, Linear, Dropout, BatchNorm, AdaptiveAvgPool2D, AvgPool2D
import paddle.nn.functional as F
import paddle.nn as nn

# 图像分块、Embedding
class PatchEmbed(nn.Layer):
    def __init__(self, img_size=224, patch_size=16, in_chans=3, embed_dim=768):
        super().__init__()
        # 原始大小为int,转为tuple,即:img_size原始输入224,变换后为[224,224]
        img_size = to_2tuple(img_size)
        patch_size = to_2tuple(patch_size)
        # 图像块的个数
        num_patches = (img_size[1] // patch_size[1]) * \
            (img_size[0] // patch_size[0])
        self.img_size = img_size
        self.patch_size = patch_size
        self.num_patches = num_patches
        # kernel_size=块大小,即每个块输出一个值,类似每个块展平后使用相同的全连接层进行处理
        # 输入维度为3,输出维度为块向量长度
        # 与原文中:分块、展平、全连接降维保持一致
        # 输出为[B, C, H, W]
        self.proj = nn.Conv2D(
            in_chans, embed_dim, kernel_size=patch_size, stride=patch_size)

    def forward(self, x):
        B, C, H, W = x.shape
        assert H == self.img_size[0] and W == self.img_size[1], \
            "Input image size ({H}*{W}) doesn't match model ({self.img_size[0]}*{self.img_size[1]})."
        # [B, C, H, W] -> [B, C, H*W] ->[B, H*W, C]
        x = self.proj(x).flatten(2).transpose((0, 2, 1))
        return x

マルチヘッドアテンション

画像をN × ( P 2 ∗ C ) N \times (P^2 * C)に変換しますN×( P2C )シーケンスの後、特徴抽出のために Transformer 構造に入力できます。Transformer 構造の中で最も重要な構造は、図 4 に示すように


図 4: マルチヘッド アテンションの概略図

2 つのヘッドを図 5 に示します。「aia^i」と入力してくださいあるi は伝達行列を通過し、分割されてq ( i , 1 ) q^{(i,1)}q( i , 1 )q ( i , 2 ) q^{(i,2)}q( i , 2 )k ( i , 1 ) k^{(i,1)}k( i , 1 )k ( i , 2 ) k^{(i,2)}k( i , 2 )v ( i , 1 ) v^{(i,1)}v( i , 1 )v ( i , 2 ) v^{(i,2)}v(i,2),然后 q ( i , 1 ) q^{(i,1)} q(i,1) k ( i , 1 ) k^{(i,1)} k( i , 1 )に注目して重みベクトルα \alphaαα \alphaα v ( i , 1 ) v^{(i,1)} v( i , 1 )加重合計を計算して最終的なb ( i , 1 ) ( i = 1 , 2 , … , N ) b^{(i,1)}(i=1,2,…,N)b( i , 1 ) (i=1 2 N )、同様に、b ( i , 2 ) ( i = 1 , 2 , … , N ) b^{(i,2)}(i=1,2,…,N)b( i , 2 ) (i=1 2 N 次に、それらは連結され、線形レイヤーを通じて処理されて、最終結果が得られます。


図 5: マルチヘッド アテンション構造

ここで、q ( i , j ) q^{(i,j)} を使用します。q( i , j )k ( i , j ) k^{(i,j)}k(i,j) v ( i , j ) v^{(i,j)} v( i , j )计算b ( i , j ) ( i = 1 , 2 , … , N ) b^{(i,j)}(i=1,2,…,N)b( i , j ) (i=1 2 N )メソッドは、スケーリングされたドット積アテンションです。構造を図 6 に示します。最初に各q ( i , j ) q^{(i,j)} をq( i , j )は k ( i , j ) k^{(i,j)}と一致しますk( i , j )に注意してください。ここで言及する注意は、2 つのベクトルがどの程度一致しているかです。具体的な方法は、ベクトルの重み付き内積を計算してα ( i , j ) \alpha_{(i,j) を}ある( i , j )ここでの加重内積の計算方法は次のとおりです。

α ( 1 , i ) = q 1 ∗ ki / d \alpha_{(1,i)} = q^1 * k^i / \sqrt{d}ある( 1 i )=q1k私は/d

その中で、dddはqqですQKq ∗ kq*kであるため、 kの次元qkの値は次元が増加するにつれて増加するため、d \sqrt{d}d の値は正規化の効果と等価です。

次に、計算されたα ( i , j ) \alpha_{(i,j)}ある( i , j )ソフトマックス演算を実行し、それをv ( i , j ) v^{(i,j)}と組み合わせます。v( i , j )を乗算します。


図6:スケーリングされたドット積の注意

具体的なコード実装は以下の通りです。

# Multi-head Attention
class Attention(nn.Layer):
    def __init__(self,
                 dim,
                 num_heads=8,
                 qkv_bias=False,
                 qk_scale=None,
                 attn_drop=0.,
                 proj_drop=0.):
        super().__init__()
        self.num_heads = num_heads
        head_dim = dim // num_heads
        self.scale = qk_scale or head_dim**-0.5
        # 计算 q,k,v 的转移矩阵
        self.qkv = nn.Linear(dim, dim * 3, bias_attr=qkv_bias)
        self.attn_drop = nn.Dropout(attn_drop)
        # 最终的线性层
        self.proj = nn.Linear(dim, dim)
        self.proj_drop = nn.Dropout(proj_drop)

    def forward(self, x):
        N, C = x.shape[1:]
        # 线性变换
        qkv = self.qkv(x).reshape((-1, N, 3, self.num_heads, C //
                                   self.num_heads)).transpose((2, 0, 3, 1, 4))
        # 分割 query key value
        q, k, v = qkv[0], qkv[1], qkv[2]
        # Scaled Dot-Product Attention
        # Matmul + Scale
        attn = (q.matmul(k.transpose((0, 1, 3, 2)))) * self.scale
        # SoftMax
        attn = nn.functional.softmax(attn, axis=-1)
        attn = self.attn_drop(attn)
        # Matmul
        x = (attn.matmul(v)).transpose((0, 2, 1, 3)).reshape((-1, N, C))
        # 线性变换
        x = self.proj(x)
        x = self.proj_drop(x)
        return x

多層パーセプトロン (MLP)

図 7 に示すように、Tranformer 構造におけるもう 1 つの重要な構造は MLP、つまり多層パーセプトロンです


図 7: 多層パーセプトロン

多層パーセプトロンは、入力層、出力層、および少なくとも 1 つの隠れ層で構成されます。ネットワーク内の各隠れ層のニューロンは、隣接する前の隠れ層のすべてのニューロンによって送信された情報を受信し、処理後に隣接する後続の隠れ層のすべてのニューロンに情報を出力できます。多層パーセプトロンでは、通常、隣接する層のニューロンは「完全接続」方式を使用して接続されます。多層パーセプトロンは複雑な非線形関数をシミュレートできます。シミュレートされる関数の複雑さは、ネットワークの隠れ層の数と各層のニューロンの数に依存します。多層パーセプトロン図 8 に示します。


図 8: 多層パーセプトロン構造

具体的なコード実装は以下の通りです。

class Mlp(nn.Layer):
    def __init__(self,
                 in_features,
                 hidden_features=None,
                 out_features=None,
                 act_layer=nn.GELU,
                 drop=0.):
        super().__init__()
        out_features = out_features or in_features
        hidden_features = hidden_features or in_features
        self.fc1 = nn.Linear(in_features, hidden_features)
        self.act = act_layer()
        self.fc2 = nn.Linear(hidden_features, out_features)
        self.drop = nn.Dropout(drop)

    def forward(self, x):
        # 输入层:线性变换
        x = self.fc1(x)
        # 应用激活函数
        x = self.act(x)
        # Dropout
        x = self.drop(x)
        # 输出层:线性变换
        x = self.fc2(x)
        # Dropout
        x = self.drop(x)
        return x

基本モジュール

図 9 に示すように、上で実装した Attendance、MLP、および DropPath モジュールに基づいて、Vision Transformer モデルの基本モジュールを組み合わせることができます。


図 9: 変圧器基本モジュール

ここでは、従来の Dropout 構造を置き換えるために DropPath (Stochastic Depth) が使用されており、DropPath は特別な Dropout として理解できます。その役割は、トレーニング中にレイヤーのサブセットをランダムに削除し、予測中に通常は完全なグラフを使用することです。

具体的な実装は以下の通りです。

def drop_path(x, drop_prob=0., training=False):
    if drop_prob == 0. or not training:
        return x
    keep_prob = paddle.to_tensor(1 - drop_prob)
    shape = (paddle.shape(x)[0], ) + (1, ) * (x.ndim - 1)
    random_tensor = keep_prob + paddle.rand(shape, dtype=x.dtype)
    random_tensor = paddle.floor(random_tensor)
    output = x.divide(keep_prob) * random_tensor
    return output

class DropPath(nn.Layer):
    def __init__(self, drop_prob=None):
        super(DropPath, self).__init__()
        self.drop_prob = drop_prob

    def forward(self, x):
        return drop_path(x, self.drop_prob, self.training)

基本モジュールの具体的な実装は次のとおりです。

class Block(nn.Layer):
    def __init__(self,
                 dim,
                 num_heads,
                 mlp_ratio=4.,
                 qkv_bias=False,
                 qk_scale=None,
                 drop=0.,
                 attn_drop=0.,
                 drop_path=0.,
                 act_layer=nn.GELU,
                 norm_layer='nn.LayerNorm',
                 epsilon=1e-5):
        super().__init__()
        self.norm1 = eval(norm_layer)(dim, epsilon=epsilon)
        # Multi-head Self-attention
        self.attn = Attention(
            dim,
            num_heads=num_heads,
            qkv_bias=qkv_bias,
            qk_scale=qk_scale,
            attn_drop=attn_drop,
            proj_drop=drop)
        # DropPath
        self.drop_path = DropPath(drop_path) if drop_path > 0. else Identity()
        self.norm2 = eval(norm_layer)(dim, epsilon=epsilon)
        mlp_hidden_dim = int(dim * mlp_ratio)
        self.mlp = Mlp(in_features=dim,
                       hidden_features=mlp_hidden_dim,
                       act_layer=act_layer,
                       drop=drop)

    def forward(self, x):
        # Multi-head Self-attention, Add, LayerNorm
        x = x + self.drop_path(self.attn(self.norm1(x)))
        # Feed Forward, Add, LayerNorm
        x = x + self.drop_path(self.mlp(self.norm2(x)))
        return x

ViT ネットワークを定義する

基本モジュールが構築された後、完全な ViT ネットワークを構築できます。ViTの完全な構造を図 10 に示します。


図 10: ViT ネットワーク構造

完全なネットワーク構造を理解する前に、いくつかのモジュールを紹介する必要があります。

  1. クラストークン

ご覧のとおり、元の画像を3 × 3 3 \times 3に分割するとします。3×3合計 9 つの小さな画像ブロックがありますが、最終的な入力シーケンスの長さは 10 です。これは、ここで入力用のベクトルを人為的に追加することを意味します。通常、この人為的に追加されたベクトルをクラス トークンと呼びます。では、このクラス トークンの機能は何でしょうか?

このベクトルがない場合、つまりN = 9 N=9 であると想像できます。N=9 つのベクトルがエンコードのために Transformer 構造に入力され、最終的に 9 つのエンコード ベクトルが得られますが、画像分類タスクの場合、後続の分類にはどの出力ベクトルを選択すればよいでしょうか?

9 つのうちのどれかを選択するのは適切ではないため、ViT アルゴリズムでは学習可能な埋め込みベクトル クラス トークンが提案され、9 つのベクトルとともに Transformer 構造に入力され、10 個のエンコード ベクトルが出力され、このクラスはトークンに使用されるものを分類して予測することができます。

実際、ここでも理解できます。ViT は実際にはトランスフォーマーのエンコーダーのみを使用し、デコーダーは使用しません。クラス トークンの役割は、他の 9 つの入力ベクトルに対応するカテゴリを見つけることです。

  1. 位置エンコーディング

Transformer 構造の位置エンコーディング規則に従って、この作業でも位置エンコーディングが使用されます。違いは、ViT の位置エンコーディングでは、元の Transformer のsincos sincos が使用されないことです。scosエンコーディングですが、学習可能な位置エンコーディングに直接設定されます。図 11 に示すように、トレーニングされた位置エンコーディングを視覚化します位置が近いほど、位置エンコーディングが類似する傾向があることがわかります。さらに、行と列の構造が現れ、同じ行/列内のパッチは同様の位置エンコーディングを持ちます。


図 11: 位置エンコーディング

  1. MLP 責任者

出力が得られた後、ViT では MLP ヘッドを使用して出力を分類します。ここでの MLP ヘッドは、LayerNorm と 2 つの全結合層で構成され、GELU 活性化関数が使用されます。

具体的なコードは以下の通りです。

まず、パラメータの初期化構成、操作を実行しない独立したネットワーク層などの基本モジュール部分を構築します。

# 参数初始化配置
trunc_normal_ = nn.initializer.TruncatedNormal(std=.02)
zeros_ = nn.initializer.Constant(value=0.)
ones_ = nn.initializer.Constant(value=1.)

# 将输入 x 由 int 类型转为 tuple 类型
def to_2tuple(x):
    return tuple([x] * 2)

# 定义一个什么操作都不进行的网络层
class Identity(nn.Layer):
    def __init__(self):
        super(Identity, self).__init__()

    def forward(self, input):
        return input

完全なコードを以下に示します。

class VisionTransformer(nn.Layer):
    def __init__(self,
                 img_size=384,
                 patch_size=16,
                 in_chans=3,
                 class_dim=1000,
                 embed_dim=768,
                 depth=12,
                 num_heads=12,
                 mlp_ratio=4,
                 qkv_bias=False,
                 qk_scale=None,
                 drop_rate=0.,
                 attn_drop_rate=0.,
                 drop_path_rate=0.,
                 norm_layer='nn.LayerNorm',
                 epsilon=1e-5,
                 **args):
        super().__init__()
        self.class_dim = class_dim

        self.num_features = self.embed_dim = embed_dim
        # 图片分块和降维,块大小为patch_size,最终块向量维度为768
        self.patch_embed = PatchEmbed(
            img_size=img_size,
            patch_size=patch_size,
            in_chans=in_chans,
            embed_dim=embed_dim)
        # 分块数量
        num_patches = self.patch_embed.num_patches
        # 可学习的位置编码
        self.pos_embed = self.create_parameter(
            shape=(1, num_patches + 1, embed_dim), default_initializer=zeros_)
        self.add_parameter("pos_embed", self.pos_embed)
        # 人为追加class token,并使用该向量进行分类预测
        self.cls_token = self.create_parameter(
            shape=(1, 1, embed_dim), default_initializer=zeros_)
        self.add_parameter("cls_token", self.cls_token)
        self.pos_drop = nn.Dropout(p=drop_rate)

        dpr = np.linspace(0, drop_path_rate, depth)
        # transformer
        self.blocks = nn.LayerList([
            Block(
                dim=embed_dim,
                num_heads=num_heads,
                mlp_ratio=mlp_ratio,
                qkv_bias=qkv_bias,
                qk_scale=qk_scale,
                drop=drop_rate,
                attn_drop=attn_drop_rate,
                drop_path=dpr[i],
                norm_layer=norm_layer,
                epsilon=epsilon) for i in range(depth)
        ])

        self.norm = eval(norm_layer)(embed_dim, epsilon=epsilon)

        # Classifier head
        self.head = nn.Linear(embed_dim,
                              class_dim) if class_dim > 0 else Identity()

        trunc_normal_(self.pos_embed)
        trunc_normal_(self.cls_token)
        self.apply(self._init_weights)
    # 参数初始化
    def _init_weights(self, m):
        if isinstance(m, nn.Linear):
            trunc_normal_(m.weight)
            if isinstance(m, nn.Linear) and m.bias is not None:
                zeros_(m.bias)
        elif isinstance(m, nn.LayerNorm):
            zeros_(m.bias)
            ones_(m.weight)
    # 获取图像特征
    def forward_features(self, x):
        B = paddle.shape(x)[0]
        # 将图片分块,并调整每个块向量的维度
        x = self.patch_embed(x)
        # 将class token与前面的分块进行拼接
        cls_tokens = self.cls_token.expand((B, -1, -1))
        x = paddle.concat((cls_tokens, x), axis=1)
        # 将编码向量中加入位置编码
        x = x + self.pos_embed
        x = self.pos_drop(x)
        # 堆叠 transformer 结构
        for blk in self.blocks:
            x = blk(x)
        # LayerNorm
        x = self.norm(x)
        # 提取分类 tokens 的输出
        return x[:, 0]

    def forward(self, x):
        # 获取图像特征
        x = self.forward_features(x)
        # 图像分类
        x = self.head(x)
        return x

DeiT

DeiTアルゴリズムの概要

論文のアドレス:注意によるデータ効率の高い画像変換と蒸留のトレーニング

ViT の導入で、ViT アルゴリズムがより良いインデックスを取得したい場合は、事前トレーニングに JFT-300 や ImageNet-21K などの非常に大規模なデータセットを使用し、その後他のデータセットに移行する必要があることを学びました。中規模または比較的大きなデータセット、小さなデータセット。JFT-300 のような巨大なデータセットを使用しない場合、その効果は CNN モデルほど良くありません。これは、CV フィールドの Transformer 構造の制限を反映しています。ほとんどの研究者にとって、このような大規模なデータセットを使用するということは、非常に高価なコンピューティングリソースが必要であることを意味しており、これらのコンピューティングリソースが入手できなくなり、このような大規模なデータセットを事前学習に使用できなくなると、そのデータセットを再現することは不可能になります。アルゴリズムが持つべき効果。したがって、この動機のために研究者らは ViT アルゴリズムを改良し、DeiT を提案しました。

DeiT では、著者は ViT に基づいてトレーニング戦略を改良し、蒸留学習の手法を使用しました。ImageNet 上でトレーニングするだけで、競争力のある Transformer モデルを取得でき、単一のコンピューター上でトレーニングにかかる​​時間は 3 時間未満です。日々。図 13 に示すように、ImageNet データセットの DeiT と以前の ViT および CNN アルゴリズムの EfficientNet の精度比較を簡単に示します。


図 13: DeiT モデルの精度

上図の指標は、ImageNet データセットでのトレーニングと ImageNet データセットでの評価の結果です。このうち、Ours(Deit) は ViT と同じネットワーク構造を使用していますが、学習戦略を改善していますが、Ours⚗(DeiT⚗) は DeiT に基づいて改善するために蒸留学習の手法を使用し続けています。このような中規模のデータセットでは、ViT アルゴリズムは CNN ネットワーク EfficientNet に比べてはるかに劣っていることがわかりますが、学習戦略を変更し、蒸留学習を使用することで、ネットワーク構造が基本的に同じ DeiT のパフォーマンスが向上します。 ViTの性能が大幅に向上し、EfficientNetを超えました。

ネットワーク最適化手法

  1. 蒸留学習

蒸留には、軟蒸留(軟蒸留)と硬蒸留(硬蒸留)の2種類があります。ソフト蒸留は、生徒ネットワークの出力と教師ネットワークのソフトマックス出力から KL 損失を取得することであり、ハード蒸留は、生徒ネットワークの出力と教師ネットワークのラベルからクロスエントロピー損失を取得することです。式は以下の通りです。DeiT では、ネットワーク上で 2 つの蒸留戦略が比較実験に使用され、最終的に硬蒸留法が選択されました。

L global S oft D istill = ( 1 − λ ) LCE ( ψ ( Z s ) , y ) + λ τ 2 KL ( ψ ( Z s / τ ) , ψ ( Z t / τ ) ) L_{global}^{ SoftDistill} = (1-\lambda)L_{CE}(\psi(Z_s),y) + \lambda \tau^2 KL(\psi(Z_s/\tau),\psi(Z_t/\tau))Lグロバル_ _ _ _ソフトディスティル_ _ _ _ _ _ _=( 1l ) LCE( ψ ( Zsy +lt _2 KL(ψ(Zs/ t ) ψ ( Z/ τ ))
L グローバル ハード蒸留 = 1 2 LCE ( ψ ( Z s ) , y ) + 1 2 LCE ( ψ ( Z s ) , yt ) L_{global}^{HardDistill} = \frac{1} {2}L_{CE}(\psi(Z_s),y) + \frac{1}{2}L_{CE}(\psi(Z_s),y_t)Lグロバル_ _ _ _ハードディスティルまだ続く_ _ _ _ _=21LCE( ψ ( Zsy +21LCE( ψ ( Zsy)

上式中、yt = argmaxc Z t ( c ) y_t=argmax_cZ_t(c)y=最大_ _ _cZ( c )ここでのハード ラベルは、ラベル スムージングによってソフト ラベルに変換することもできます。この場合、真の値に対応するラベルは1 − イプシロン 1-イプシロンを持つと見なされます。1epsilon確率残りepsilon epsilon _e psil o n残りのカテゴリで共有されますイプシロン イプシロンe psil o nハイパーパラメータで、ここで0.1 です。

  1. ネットワーク構造

次に、図 14 に示すように


図 14: DeiT ネットワーク構造

図 14図 1の ViT のネットワーク構造を比較すると、DeiT と ViT の主な違いは、主にネットワーク トレーニングでの蒸留学習に使用される蒸留トークンの導入であることがわかります。この蒸留トークンはクラス トークンに非常に似ており、セルフ アテンション レイヤー内のクラス トークンおよびイメージ パッチと継続的に対話します。蒸留トークンとクラス トークンの唯一の違いは、クラス トークンの目標は実際のラベルと一致することであるのに対し、蒸留トークンは蒸留学習において教師ネットワークによって予測されたラベルと一致することであることです。この蒸留トークンを使用すると、通常の蒸留と同様に、モデルが教師ネットワークの出力から学習できるようになりますが、クラス トークンの一種の補完としても機能します。

蒸留トークンにより、DeiT は ViT と比較して損失関数の点でも変更されました。

  • ViT アルゴリズムでは、モデルの最終出力は、ソフトマックス計算にクラス トークンを使用して、各カテゴリに属する​​予測結果の確率分布を取得することです。現時点では、損失関数の計算方法は従来の多分類タスクと一致しており、クロスエントロピー損失を直接使用できます。
  • DeiTアルゴリズムでは蒸留学習法を用いるため、クロスエントロピー損失に基づいて蒸留損失を加算する必要がある。

論文で実験が行われたところ、クラストークンと蒸留トークンが異なる方向に収束するという興味深い現象が発見され、当初、2つのトークンのコサイン類似度はわずか0.006でした。ネットワーク層の数が最後の層まで増加すると、2 つのトークンのコサイン類似度は 0.93 になります。また、似ているが同じではない 2 つのトークンとみなすこともできます。

同時に、蒸留トークンがモデルに有益な情報を追加することを検証するために、論文内で実験も行われました。著者は、蒸留トークンを単純なクラス トークンに置き換えたところ、2 つのクラス トークンが個別にランダムに初期化されたとしても、最終的にはほぼ同一の結果 (コサイン類似度は 0.999) にランダムに収束する一方で、最終的なパフォーマンスはそれほど大きくないことを発見しました。改善されました。

ここで、最後の質問があります。ネットワークはクラス トークンの結果だけでなく、蒸留トークンの結果も出力します。最終予測では、誰を最終結果として採用すればよいでしょうか? 答えは、2 つのソフトマックスの結果を加算して、単純にアルゴリズムの最終予測結果を取得することです。

論文実験

DeiT の論文では、最適なトレーニング戦略を選択するために一連の実験が行われました。実験における、さまざまなサイズの DeiT構造図 15 に示します。このうち最大の構造は DeiT-B で、ViT-B と同じですが、埋め込み次元は 768 に調整されており、ヘッド数は 12 で、各ヘッドに対応する埋め込み次元は 64 です。DeiT-S と DeiT-Ti は 2 つの小型モデルで、ヘッドの数を調整しますが、各ヘッドに対応する埋め込み寸法は変更されません。


図 15: DeiT 実験パラメータ

  • 実験 1: CNN 構造と Transformer 構造ではどちらが教師モデルに適していますか?

実験図16に示す。実験では、教師ネットワークが DeiT-B と異なる RegNetY を使用した場合、生徒ネットワークの事前トレーニングのパフォーマンスと微調整のパフォーマンスが比較されました。このうち、右側の最初の列は事前トレーニングされたネットワーク インジケーターで、2 番目の列は384 × 384 384 \times 384を使用しています。384×ファインチューン後の384サイズ解像度モデルインデックス。CNN を教師ネットワークとして使用すると、Transformer 構造を教師ネットワークとして使用するよりも良い結果が得られることがわかります。


図 16: 教師モデルの選択

  • 実験 2: どの蒸留戦略がより効果的ですか?

実験図17に示す。


図 17: さまざまな蒸留戦略の結果の比較

表の最初の 3 行は、蒸留トークンなしでトレーニングした場合の、蒸留なしの学習、ソフト蒸留の使用、およびハード蒸留の使用のパフォーマンス比較に対応します。このとき図 18 に示すとおりであり、元の ViT を基に損失関数に昇圧部分を追加したものに相当し、その他の構造は変更されません。ハード蒸留を使用したネットワークのパフォーマンスは、蒸留を使用しない場合やソフト蒸留を使用したネットワークに比べて大幅に優れていることがわかります。現時点では、蒸留トークンを使用しない場合でも、ハード蒸留はトップ 1 の精度 83.0 を達成できます。 %。


図 18: 蒸留トークンを使用しない 3 つのトレーニング形式

表の最後の 3 行は、クラス トークンのみを使用した場合、蒸留トークンのみを使用した場合、およびクラス トークンと蒸留トークンの両方を使用した場合のパフォーマンスの比較に対応します。このとき図19に示します。

この時点で、結果から次のことがわかります。

  1. クラス トークンと蒸留トークンはどちらも、分類に役立つ情報を提供します。
  2. 蒸留トークンのみを使用すると、クラス トークンのみを使用するよりもわずかに効果的です。
  3. クラストークンと蒸留トークンの両方を使用すると、最大の効果が得られます。

図 19: 異なるトークンを使用した 3 つの形式のトレーニング

同時に、学生モデルの最終的な効果が教師のネットワークを上回る可能性があることは興味深いことです。

  • 実験 3: Transformer は CNN の帰納的仮説を学習できますか?

最終的な結論は論文には示されていませんが、図 20の表を通じて簡単な分析を行うこともできます。表内の数値は、異なる設定間の決定の不一致を反映しています。行 2 の最後の 3 列から、蒸留トークンで分類された DeiT と CNN の間の不一致は、クラス トークンで分類された DeiT よりも小さく、両方を含む DeiT は中間にあることがわかります。2 行目の 3 列目を最後の 3 列と比較すると、Distilled DeiT と CNN の不一致が Distilled DeiT よりも小さいことがわかります。


図 20: 蒸留前の DeiT、蒸留後の CNN 教師および DeiT 間の意思決定の不一致

  • 実験4: 性能比較

この論文では、図 21 に示すようにパラメーターの量が等しい場合、CNN 構造は Transformer 構造よりも遅いことがわかります。これは実際には、Transformer での大規模な行列乗算が、CNN 構造での小さな畳み込みよりも多くの最適化の機会を提供するためです。EffcientNet-B4 と DeiT-B⚗ の速度は同等であり、3 つのデータセットのパフォーマンスも比較的近いです。


図 21: さまざまなモデルのパフォーマンスの数値比較

  • 実験5: 転移学習の性能比較

また、図 22 に示すように


図 22: さまざまなモデルの転移学習機能の比較

  • 実験6: トレーニング戦略の比較実験

この論文では、ネットワーク トレーニングにさまざまなトレーニング戦略が使用されており、そのパフォーマンスの比較が図 23 に示されています。ご存知のとおり、Transformer ネットワークのトレーニングには大量のデータが必要であり、より小さなデータ セットでパフォーマンスを向上させたい場合は、大量のデータ拡張処理が必要になります。そこで、論文の著者らは一連のデータ拡張実験を実施しました。評価されたほとんどすべてのデータ拡張手法がアルゴリズムのパフォーマンスを向上させることができることがわかります。同時に、著者は、さまざまなオプティマイザーと正則化戦略を使用した場合のアルゴリズムのパフォーマンスも比較しました。一般に、AdamW を使用すると、SGD を使用するよりもパフォーマンスが向上し、Stochastic Depth を使用すると収束に優れ、Mixup と CutMix もパフォーマンスを向上させることができます。


図 23: トレーニング戦略の比較実験

ネットワーク実装の最終コードを以下に示します。

# DeiT 结构,继承了 ViT 结构
class DistilledVisionTransformer(VisionTransformer):
    def __init__(self,
                 img_size=384,
                 patch_size=16,
                 class_dim=1000,
                 embed_dim=768,
                 depth=12,
                 num_heads=12,
                 mlp_ratio=4,
                 qkv_bias=False,
                 norm_layer='nn.LayerNorm',
                 epsilon=1e-5,
                 **kwargs):
        # ViT 结构
        super().__init__(
            img_size=img_size,
            patch_size=patch_size,
            class_dim=class_dim,
            embed_dim=embed_dim,
            depth=depth,
            num_heads=num_heads,
            mlp_ratio=mlp_ratio,
            qkv_bias=qkv_bias,
            norm_layer=norm_layer,
            epsilon=epsilon,
            **kwargs)
        # 由于增加了 distillation token,所以也需要调整位置编码的长度
        self.pos_embed = self.create_parameter(
            shape=(1, self.patch_embed.num_patches + 2, self.embed_dim),
            default_initializer=zeros_)
        self.add_parameter("pos_embed", self.pos_embed)
        # distillation token
        self.dist_token = self.create_parameter(
            shape=(1, 1, self.embed_dim), default_initializer=zeros_)
        self.add_parameter("cls_token", self.cls_token)
        # Classifier head
        self.head_dist = nn.Linear(
            self.embed_dim,
            self.class_dim) if self.class_dim > 0 else Identity()

        trunc_normal_(self.dist_token)
        trunc_normal_(self.pos_embed)
        self.head_dist.apply(self._init_weights)
    # 获取图像特征
    def forward_features(self, x):
        B = paddle.shape(x)[0]
        # 将图片分块,并调整每个块向量的维度
        x = self.patch_embed(x)
        # 将class token、distillation token与前面的分块进行拼接
        cls_tokens = self.cls_token.expand((B, -1, -1))
        dist_token = self.dist_token.expand((B, -1, -1))
        x = paddle.concat((cls_tokens, dist_token, x), axis=1)
        # 将编码向量中加入位置编码
        x = x + self.pos_embed
        x = self.pos_drop(x)
        # 堆叠 transformer 结构
        for blk in self.blocks:
            x = blk(x)
        # LayerNorm
        x = self.norm(x)
        # 提取class token以及distillation token的输出
        return x[:, 0], x[:, 1]

    def forward(self, x):
        # 获取图像特征
        x, x_dist = self.forward_features(x)
        # 图像分类
        x = self.head(x)
        x_dist = self.head_dist(x_dist)
        # 取 class token以及distillation token 的平均值作为结果
        return (x + x_dist) / 2

おすすめ

転載: blog.csdn.net/weixin_43273742/article/details/129782057