STEP 16:Vision Transformer(ViT)の理論

🔮 STEP 16: Vision Transformer(ViT)の理論

TransformerのCV応用、パッチ埋め込み、位置埋め込み、
Self-Attention、CNNとの比較を学びます

📋 このステップで学ぶこと

  • TransformerがCVに応用された背景
  • ViT(Vision Transformer)のアーキテクチャ全体像
  • パッチ埋め込み(Patch Embedding)の詳細
  • 位置埋め込み(Position Embedding)の必要性
  • [CLS]トークンの役割
  • Self-Attention Mechanismの画像への応用
  • Transformer Encoderの構造
  • CNNとViTの比較(受容野、Inductive Bias)
  • データ量による性能の違い
  • 実装:ViTをPyTorchで構築

🌟 1. TransformerがCVに応用された背景

Vision Transformer(ViT)を理解するには、まずNLP(自然言語処理)で大成功を収めたTransformerについて知る必要があります。なぜTransformerが画像処理に使われるようになったのか、その背景から見ていきましょう。

1-1. NLPでのTransformerの成功

【Transformerの歴史】 ■ 2017年: Transformer誕生 論文: “Attention is All You Need”(Google) 革新点: ・RNN(再帰ニューラルネットワーク)を使わない ・Self-Attentionで系列データを処理 ・並列計算が可能 → 学習が高速化 結果: 機械翻訳で最先端の性能を達成 ■ 2018年: BERT Bidirectional Encoder Representations from Transformers 革新点: ・事前学習 + ファインチューニングのパラダイム ・大量のテキストで事前学習 ・少量データでファインチューニング 結果: 11のNLPタスクで最先端を更新 ■ 2019-2020年: GPTシリーズ GPT-2: 15億パラメータ GPT-3: 1750億パラメータ 革新点: ・巨大モデル+大量データ = 驚異的な性能 ・Few-shot Learning(少数例で学習) 結果: NLPのあらゆるタスクで圧倒的な性能 ───────────────────────────────────────────────────── 【TransformerがNLPを支配した理由】 1. Self-Attention(自己注意機構) 従来のRNN: 「今日 は いい 天気 です」 → 1単語ずつ順番に処理 → 「今日」と「です」の関係を学ぶのが困難 Transformer: → すべての単語が他のすべての単語を直接参照 → 「今日」と「天気」の関係を直接学習 2. 並列化 RNN: 順番に処理 → 遅い Transformer: 同時に処理 → 速い(GPU活用) 3. スケーラビリティ モデルサイズ ↑ + データ量 ↑ = 性能 ↑↑ → 大規模化に強い ───────────────────────────────────────────────────── 【自然な疑問】 「TransformerをCVにも使えないか?」 NLPでこれほど成功したなら、 画像処理でも使えるはず… → Vision Transformer(ViT)の誕生へ

1-2. CVへのTransformer応用の課題

【画像をTransformerに入力する難しさ】 ■ 問題: 画像は「系列データ」ではない テキスト: 「今日 は いい 天気 です」 → 5個のトークン(単語) → Transformerに直接入力可能 画像: 224×224ピクセル = 50,176個の「ピクセル」 → そのまま入力すると計算量が爆発! ■ Self-Attentionの計算量 計算量: O(N²) N = 系列長(トークン数) テキスト(N=512): 512² = 262,144 → OK 画像ピクセル(N=50,176): 50,176² = 約25億 → 計算不可能! ───────────────────────────────────────────────────── 【初期の試み(2020年以前)】 試み1: ピクセルをそのまま系列に 224×224 = 50,176ピクセル → 計算量: 25億 → 失敗(メモリ不足、計算時間膨大) 試み2: CNNと組み合わせ CNN(ResNetなど)で特徴抽出 → 特徴マップ(例: 7×7=49個)をTransformerへ → 一定の成功 → でもCNNが必要…「純粋なTransformer」ではない 課題: 「CNNなしで、純粋なTransformerで画像を処理できないか?」

1-3. ViTの革新的アイデア

💡 ViTの核心:「画像をパッチに分割する」

2020年、Google Researchが発表した論文
“An Image is Worth 16×16 Words”
がコンピュータビジョンに革命をもたらしました。

【ViTの革新的アイデア】 ■ 核心: 画像を「パッチ」に分割 従来の考え: 画像 → ピクセル単位で処理 224×224 = 50,176個 → 計算不可能 ViTの発想: 画像 → パッチ(小さな領域)に分割 224×224画像 を 16×16のパッチに分割 → (224÷16) × (224÷16) = 14×14 = 196パッチ → 196個なら計算可能!(196² = 38,416) ■ パッチを「単語」として扱う NLPでの処理: 文章 → 単語に分割 → 各単語を埋め込み → Transformer ViTでの処理: 画像 → パッチに分割 → 各パッチを埋め込み → Transformer つまり: 1つのパッチ = 1つの「単語」 画像 = 「パッチの文章」 ───────────────────────────────────────────────────── 【なぜ16×16パッチ?】 計算量のバランス: パッチサイズ 8×8: → (224÷8)² = 784パッチ → 計算量: 784² ≈ 61万 → 計算量多いが、細かい情報を保持 パッチサイズ 16×16: → (224÷16)² = 196パッチ → 計算量: 196² ≈ 3.8万 → バランスが良い ★ViTの標準 パッチサイズ 32×32: → (224÷32)² = 49パッチ → 計算量: 49² ≈ 2400 → 計算量少ないが、情報が粗い ───────────────────────────────────────────────────── 【ViTの成果】 大規模データ(JFT-300M: 3億枚)で事前学習した場合: ResNet-152(CNN): 84.7% ViT-Large: 87.8% → CNNを大幅に上回る! 結論: 大規模データがあれば、 純粋なTransformerでCNNを超えられる

🏗️ 2. ViT(Vision Transformer)のアーキテクチャ

ViTの全体構造を理解しましょう。処理の流れを1つずつ追っていきます。

2-1. 全体構造の概要

【ViTの全体フロー】 入力画像(224×224×3) │ ↓ ┌─────────────────────────────────────────────────────┐ │ STEP 1: パッチ分割 │ │ │ │ 画像を16×16のパッチに分割 │ │ → 14×14 = 196個のパッチ │ │ 各パッチ: 16×16×3 = 768要素 │ └─────────────────────────────────────────────────────┘ │ ↓ ┌─────────────────────────────────────────────────────┐ │ STEP 2: パッチ埋め込み(Linear Projection) │ │ │ │ 各パッチを768次元ベクトルに変換 │ │ → 196個の768次元ベクトル │ │ 形状: (196, 768) │ └─────────────────────────────────────────────────────┘ │ ↓ ┌─────────────────────────────────────────────────────┐ │ STEP 3: [CLS]トークンの追加 │ │ │ │ 分類用の特別なトークンを先頭に追加 │ │ → 197個のトークン(1 + 196) │ │ 形状: (197, 768) │ └─────────────────────────────────────────────────────┘ │ ↓ ┌─────────────────────────────────────────────────────┐ │ STEP 4: 位置埋め込み(Position Embedding) │ │ │ │ 各トークンに位置情報を追加 │ │ 学習可能な197個の位置ベクトル │ │ 形状: (197, 768) + (197, 768) = (197, 768) │ └─────────────────────────────────────────────────────┘ │ ↓ ┌─────────────────────────────────────────────────────┐ │ STEP 5: Transformer Encoder(12層、ViT-Baseの場合)│ │ │ │ 各層の構造: │ │ ├─ Layer Normalization │ │ ├─ Multi-Head Self-Attention │ │ ├─ Residual Connection(残差接続) │ │ ├─ Layer Normalization │ │ ├─ MLP(Feed-Forward Network) │ │ └─ Residual Connection(残差接続) │ │ │ │ 形状: (197, 768) → (197, 768) │ └─────────────────────────────────────────────────────┘ │ ↓ ┌─────────────────────────────────────────────────────┐ │ STEP 6: [CLS]トークンの抽出 │ │ │ │ 最終層の[CLS]トークンのみを取り出す │ │ → 画像全体の情報が集約されたベクトル │ │ 形状: (768,) │ └─────────────────────────────────────────────────────┘ │ ↓ ┌─────────────────────────────────────────────────────┐ │ STEP 7: 分類ヘッド(MLP) │ │ │ │ 768次元 → クラス数(例: 1000クラス) │ │ 形状: (768,) → (1000,) │ └─────────────────────────────────────────────────────┘ │ ↓ 出力: クラス確率(1000クラス)

2-2. パッチ埋め込み(Patch Embedding)の詳細

💡 パッチ埋め込みとは

画像を小さなパッチに分割し、各パッチを固定次元のベクトルに変換する処理です。
NLPで単語を埋め込みベクトルに変換するのと同じ役割です。

【パッチ埋め込みの処理手順】 ■ 入力画像 サイズ: H × W × C = 224 × 224 × 3(RGB) ■ STEP 1: パッチ分割 パッチサイズ: P × P = 16 × 16 パッチ数の計算: 縦方向: 224 ÷ 16 = 14個 横方向: 224 ÷ 16 = 14個 合計: 14 × 14 = 196パッチ 視覚的なイメージ: ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐ │ 1│ 2│ 3│ 4│ 5│ 6│ 7│ 8│ 9│10│11│12│13│14│ ├──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤ │15│16│17│… │ ├──┼──┼──┤ │ │ │ │ │ │ │ … │ │ │ │ … 196│ └───────────────────────────────────────────┘ 各パッチのサイズ: 16 × 16 × 3 = 768要素 ■ STEP 2: パッチの展開(Flatten) 各パッチ(16×16×3の3D配列)を1D配列に展開 パッチ1: [r1,g1,b1, r2,g2,b2, …, r256,g256,b256] → 768次元のベクトル 196パッチ → 196個の768次元ベクトル ■ STEP 3: 線形変換(Linear Projection) 各パッチベクトルを埋め込み次元に変換 入力: 768次元(16×16×3) 出力: D次元(例: D=768、ViT-Base) 数式: x_embed = x_patch × E + b x_patch: (768,) 展開されたパッチ E: (768, D) 学習可能な重み行列 b: (D,) バイアス x_embed: (D,) 埋め込みベクトル ※ 今回は768→768なので次元は変わらないが、 異なる次元に変換することも可能 ■ 最終出力 形状: (196, 768) → 196個のパッチ、各768次元
【なぜ畳み込みで実装するのか】 実際の実装では、パッチ埋め込みを「畳み込み」で効率的に計算します。 ■ ナイーブな実装(遅い) for i in range(196): patch = image[パッチiの範囲] # 16×16×3 patch_flat = patch.flatten() # 768 embed[i] = linear(patch_flat) # 768次元 ■ 畳み込みでの実装(速い) Conv2d(in_channels=3, out_channels=768, kernel_size=16, stride=16) なぜこれがパッチ埋め込みと同じか: 1. kernel_size=16, stride=16 → 16ピクセルずつジャンプ → 重なりなしで16×16領域を処理 2. out_channels=768 → 各16×16領域を768次元に変換 3. 入力: (B, 3, 224, 224) 出力: (B, 768, 14, 14) → 14×14 = 196個の768次元ベクトル 処理のイメージ: 入力画像 (224×224×3) ↓ stride=16の畳み込み ↓ 特徴マップ (14×14×768) ↓ flatten ↓ (196, 768)

2-3. [CLS]トークンの役割

【[CLS]トークンとは】 ■ 起源 BERT(2018年)で導入された特別なトークン CLS = Classification(分類)の略 ■ NLPでの使用 入力: [CLS] The cat sat on the mat [SEP] [CLS]の役割: ・文全体の情報を集約 ・分類タスクに使用 ■ ViTでの使用 入力: [CLS] + パッチ1 + パッチ2 + … + パッチ196 [CLS]の役割: ・画像全体の情報を集約 ・分類に使用 ───────────────────────────────────────────────────── 【なぜ[CLS]トークンを使うのか】 疑問: 「全パッチの平均を使えばいいのでは?」 ■ 全パッチ平均の問題点 1. 情報の希釈 画像: 猫が右下にいる パッチ1〜150: 背景 パッチ151〜196: 猫 平均: (150×背景 + 46×猫) / 196 → 猫の情報が薄まる 2. 適応的でない すべてのパッチに等しい重み → 画像によらず固定 ■ [CLS]トークンの利点 1. 情報の集約 Self-Attentionにより、[CLS]は重要なパッチに 高いAttentionを与える [CLS]’ = 0.05×背景パッチ + 0.70×猫パッチ → 猫の情報が強調される 2. 適応的 画像ごとに最適な重み付けを学習 猫の画像 → 猫パッチに高Attention 犬の画像 → 犬パッチに高Attention 3. タスク固有の表現 [CLS]は分類タスクに最適化された表現を学習 ───────────────────────────────────────────────────── 【[CLS]トークンの処理】 初期状態: [CLS]トークン: 学習可能なパラメータ 形状: (1, 768) 初期値: ゼロまたはランダム 追加: tokens = concat([CLS], パッチ埋め込み) 形状: (1, 768) + (196, 768) = (197, 768) Transformer通過後: [CLS]の出力のみを分類に使用 cls_output = tokens[0] # 最初のトークン 形状: (768,)

2-4. 位置埋め込み(Position Embedding)の詳細

💡 なぜ位置埋め込みが必要か

Transformerは順序不変(permutation-invariant)です。
つまり、入力の順番を入れ替えても同じ出力になってしまいます。
画像では「どのパッチがどこにあるか」が重要なので、位置情報を明示的に追加します。

【位置埋め込みの必要性】 ■ Transformerの性質 Self-Attentionの計算: Attention(Q, K, V) = softmax(QK^T / √d) V この計算は入力の順番に依存しない! 例: 入力1: [パッチA, パッチB, パッチC] 入力2: [パッチC, パッチA, パッチB] → Attentionの計算結果は同じ → パッチの位置情報が失われる ■ 画像での問題 元画像: ┌────────────┐ │ 空 空 │ │ 猫 猫 │ │ 草 草 │ └────────────┘ パッチの順番を入れ替えると: ┌────────────┐ │ 猫 草 │ ← 意味不明な画像 │ 空 猫 │ │ 草 空 │ └────────────┘ でもTransformerは同じと認識してしまう! → 位置情報が必要 ───────────────────────────────────────────────────── 【位置埋め込みの種類】 ■ 方式1: 学習可能な埋め込み(ViTで採用) 仕組み: 各位置に学習可能なベクトルを割り当て pos_embed = Parameter(shape=(197, 768)) 位置0([CLS]): 768次元ベクトル(学習) 位置1(パッチ1): 768次元ベクトル(学習) … 位置196(パッチ196): 768次元ベクトル(学習) 利点: ・柔軟(最適な位置表現を学習) ・2D構造を暗黙的に学習可能 欠点: ・固定サイズの画像にのみ対応 ・訓練時と異なるサイズは要補間 ■ 方式2: 固定の正弦波埋め込み(Transformerの元論文) 仕組み: PE(pos, 2i) = sin(pos / 10000^(2i/d)) PE(pos, 2i+1) = cos(pos / 10000^(2i/d)) 利点: ・任意の長さに対応 ・学習不要 欠点: ・1D位置のみ(2D構造を考慮しない) ■ 方式3: 2D位置埋め込み 仕組み: 行と列で別々の位置埋め込み pos_embed = row_embed + col_embed 利点: ・2D構造を明示的に考慮 ViTの実験結果: 学習可能な1D埋め込みと2D埋め込みで 精度差はほとんどなかった ───────────────────────────────────────────────────── 【位置埋め込みの適用】 追加方法: x = パッチ埋め込み + 位置埋め込み 形状: パッチ埋め込み: (197, 768) 位置埋め込み: (197, 768) 結果: (197, 768) 重要ポイント: ・足し算で追加(連結ではない) ・各トークンに対応する位置ベクトルを加算

2-5. Transformer Encoderの詳細

【Transformer Encoder 1層の構造】 入力: x ∈ (197, 768) │ ↓ ┌─────────────────────────────────────────────────────┐ │ Layer Normalization 1 │ │ │ │ 各トークンを正規化(平均0、分散1) │ │ x_norm1 = LayerNorm(x) │ └─────────────────────────────────────────────────────┘ │ ↓ ┌─────────────────────────────────────────────────────┐ │ Multi-Head Self-Attention(MHSA) │ │ │ │ 全トークン間の関係を学習 │ │ attn_out = MHSA(x_norm1) │ │ 詳細は次のセクションで説明 │ └─────────────────────────────────────────────────────┘ │ ↓ ┌─────────────────────────────────────────────────────┐ │ Residual Connection 1(残差接続) │ │ │ │ 入力をスキップ接続で追加 │ │ x = x + attn_out │ │ │ │ 効果: 勾配消失を防ぐ、学習を安定化 │ └─────────────────────────────────────────────────────┘ │ ↓ ┌─────────────────────────────────────────────────────┐ │ Layer Normalization 2 │ │ │ │ x_norm2 = LayerNorm(x) │ └─────────────────────────────────────────────────────┘ │ ↓ ┌─────────────────────────────────────────────────────┐ │ MLP(Feed-Forward Network) │ │ │ │ 2層の全結合ネットワーク │ │ 768 → 3072 → 768 │ │ 活性化関数: GELU │ │ │ │ mlp_out = Linear(GELU(Linear(x_norm2))) │ └─────────────────────────────────────────────────────┘ │ ↓ ┌─────────────────────────────────────────────────────┐ │ Residual Connection 2 │ │ │ │ x = x + mlp_out │ └─────────────────────────────────────────────────────┘ │ ↓ 出力: x ∈ (197, 768) これを12回繰り返す(ViT-Baseの場合)
【Layer NormalizationとBatch Normalizationの違い】 ■ Batch Normalization(CNNでよく使用) 正規化の方向: バッチ方向 入力: (Batch, Channel, Height, Width) 正規化: 同じチャンネルの全サンプルで平均・分散を計算 問題: ・バッチサイズに依存 ・小さいバッチで不安定 ■ Layer Normalization(Transformerで使用) 正規化の方向: 特徴方向 入力: (Batch, Sequence, Feature) 正規化: 各サンプル・各トークンの全特徴で平均・分散を計算 利点: ・バッチサイズに依存しない ・単一サンプルでも動作 ・系列長が変わっても動作 ───────────────────────────────────────────────────── 【MLPの構造】 MLP = Multi-Layer Perceptron 入力: (197, 768) ↓ Linear(768, 3072) # 4倍に拡大 ↓ GELU activation # 活性化関数 ↓ Dropout ↓ Linear(3072, 768) # 元の次元に戻す ↓ Dropout ↓ 出力: (197, 768) なぜ4倍に拡大? ・より複雑な変換を学習可能 ・論文の実験で効果的だった

2-6. Multi-Head Self-Attention(MHSA)の詳細

💡 Self-Attentionとは

各トークンが他のすべてのトークンとの「関連度」を計算し、
関連度の高いトークンの情報を重み付けて集約する仕組みです。
これにより、画像全体の文脈を捉えることができます。

【Self-Attentionの計算手順】 ■ 入力 X ∈ (N, D) = (197, 768) N: トークン数([CLS] + 196パッチ) D: 埋め込み次元 ■ STEP 1: Q, K, V の計算 Query, Key, Value を線形変換で計算 Q = X × W_Q (197, 768) × (768, 768) = (197, 768) K = X × W_K (197, 768) × (768, 768) = (197, 768) V = X × W_V (197, 768) × (768, 768) = (197, 768) W_Q, W_K, W_V: 学習可能な重み行列 ■ STEP 2: Attentionスコアの計算 Attention = softmax(Q × K^T / √d_k) 計算の詳細: 1. Q × K^T: (197, 768) × (768, 197) = (197, 197) → 各トークン間の類似度(内積) 2. / √d_k: スケーリング → d_k = 768 / 12 = 64(ヘッドあたりの次元) → 大きすぎる値を防ぐ 3. softmax: 行方向に適用 → 各行の合計が1になる → 確率(重み)に変換 ■ STEP 3: Attentionの適用 Output = Attention × V (197, 197) × (197, 768) = (197, 768) 意味: 各トークンが、他のトークンの情報を Attention重みに従って集約 ───────────────────────────────────────────────────── 【Attentionの直感的理解】 例: パッチ50(猫の顔)のAttention Attention[50] = [0.01, 0.02, …, 0.15, …, 0.01] ↑ ↑ 背景 猫の体 パッチ50の出力: = 0.01×背景パッチ + 0.02×… + 0.15×猫の体 + … → 猫の顔は猫の体に注目している → 関連するパーツ同士が情報を共有
【Multi-Head Attentionの仕組み】 ■ なぜMulti-Headか 単一のAttentionでは1種類の関係しか学習できない 例: Head 1: 色の関係を学習 Head 2: 形の関係を学習 Head 3: 位置の関係を学習 … 複数のヘッドで異なる関係を並列に学習 ■ 計算方法 1. 入力を分割 X ∈ (197, 768) → h個のヘッドに分割 h = 12(ViT-Base) 各ヘッド: (197, 64) ← 768 ÷ 12 = 64 2. 各ヘッドでAttention計算 head_i = Attention(Q_i, K_i, V_i) 各ヘッドの出力: (197, 64) 3. 結合 concat(head_1, …, head_12): (197, 768) 4. 線形変換 output = concat × W_O W_O: (768, 768) ■ 計算の効率化 実際には、12個のヘッドを 1つの大きな行列演算で計算 → GPU並列化で高速 ───────────────────────────────────────────────────── 【Attention Mapの可視化】 Attention Map: どのパッチがどのパッチに注目しているか 例: 猫の画像 [CLS]トークンのAttention: ┌─────────────┐ │ 0.02 0.03 │ ← 空(低Attention) │ 0.15 0.20 │ ← 猫の顔(高Attention) │ 0.18 0.12 │ ← 猫の体(高Attention) │ 0.01 0.02 │ ← 草(低Attention) └─────────────┘ 解釈: [CLS]は猫に関連するパッチに注目 → 分類に重要な情報を集約

⚖️ 3. CNNとViTの比較

CNNとViTは根本的に異なるアプローチで画像を処理します。それぞれの特徴と使い分けを理解しましょう。

3-1. アーキテクチャの違い

項目 CNN ViT
基本操作 畳み込み(Convolution) Self-Attention
受容野 局所的 → 階層的に拡大
(3×3 → 7×7 → …)
グローバル
(最初から全体を見る)
Inductive Bias 強い
(局所性、平行移動不変性)
弱い
(ほぼなし)
データ量 少量でも学習可能 大量データが必要
計算量 O(k² × C × H × W)
k: カーネルサイズ
O(N² × D)
N: パッチ数
位置情報 暗黙的
(畳み込みの位置)
明示的
(位置埋め込み)

3-2. Inductive Bias(帰納バイアス)とは

💡 Inductive Biasの定義

Inductive Bias(帰納バイアス)とは、
モデルが学習する前から持っている「仮定」や「先入観」のことです。
これにより、少ないデータでも効率的に学習できますが、柔軟性が制限されます。

【CNNの強いInductive Bias】 ■ 仮定1: 局所性(Locality) 「近くのピクセルは関連性が高い」 実装: 畳み込みは局所領域のみを処理 3×3カーネル → 周囲9ピクセルのみ参照 効果: ・エッジ、テクスチャなど局所パターンを効率的に学習 ・パラメータ数が少ない(3×3 = 9パラメータ) 例: 画像内の「猫の耳」を認識 → 耳の周辺ピクセルだけ見れば十分 → 遠くのピクセル(空など)は不要 ■ 仮定2: 平行移動不変性(Translation Invariance) 「物体の位置が変わっても同じ特徴」 実装: 同じフィルタを画像全体に適用(重み共有) 効果: ・位置に依存しない認識 ・「猫」が左にいても右にいても認識可能 ・パラメータ数削減 例: エッジ検出フィルタ → 画像のどの位置でもエッジを検出 ■ 仮定3: 階層性(Hierarchy) 「低レベル特徴 → 高レベル特徴」 実装: 層を重ねて受容野を拡大 効果: 第1層: エッジ、色 第2層: テクスチャ、パターン 第3層: パーツ(耳、目) 第4層: 物体(猫) → 段階的に抽象化 ───────────────────────────────────────────────────── 【ViTの弱いInductive Bias】 ■ ViTの仮定 ほぼなし! ・位置埋め込み以外、画像特有の仮定なし ・各パッチは他のすべてのパッチと同等に関連 ・近くも遠くも同じように処理 ■ これが意味すること 良い点: ・データから最適な処理方法を学習 ・CNNの仮定が間違っている場合でも対応可能 ・長距離依存性を直接捕捉 悪い点: ・すべてをデータから学習する必要 ・大量のデータが必要 ・少量データでは過学習 ───────────────────────────────────────────────────── 【トレードオフの図解】 柔軟性 ↑ │ ViT ● │ │ CNN ● │ │ └─────────→ 必要データ量 少量 大量 CNN: 少量データで学習可能、でも柔軟性低い ViT: 柔軟性高い、でも大量データ必要

3-3. データ量による性能比較

【データ量と精度の関係】 ■ 少量データ(ImageNet-1K: 130万枚) ┌─────────────────────────────────────────┐ │ モデル │ Top-1 Accuracy │ ├─────────────────┼───────────────────────┤ │ ResNet-50 │ 76.5% │ │ ViT-Base │ 76.3% │ └─────────────────┴───────────────────────┘ 結果: CNN ≒ ViT(ほぼ同等) 理由: ・130万枚ではViTのパラメータ(86M)を 十分に学習できない ・CNNはInductive Biasで効率的に学習 ■ 中規模データ(ImageNet-21K: 1400万枚) 事前学習 → ImageNet-1Kでファインチューニング ┌─────────────────────────────────────────┐ │ モデル │ Top-1 Accuracy │ ├─────────────────┼───────────────────────┤ │ ResNet-50 │ 78.3% │ │ ViT-Base │ 81.8% │ └─────────────────┴───────────────────────┘ 結果: CNN < ViT(ViTが上回る) 理由: ・1400万枚でViTの学習が進む ・データから最適な表現を学習 ■ 大規模データ(JFT-300M: 3億枚) ┌─────────────────────────────────────────┐ │ モデル │ Top-1 Accuracy │ ├─────────────────┼───────────────────────┤ │ ResNet-152 │ 84.7% │ │ ViT-Large │ 87.8% │ └─────────────────┴───────────────────────┘ 結果: CNN << ViT(ViTが大幅に上回る) 理由: ・3億枚でViTの真価が発揮 ・CNNはInductive Biasで限界がある ・ViTはデータに応じてスケール ───────────────────────────────────────────────────── 【グラフによる理解】 精度 ↑ │ ViT ● │ / │ / │ / │ CNN ─────────● │ / │/ └─────────────────────────→ データ量 少量 中量 大量 CNN: 早期に飽和(Inductive Biasの限界) ViT: データに応じてスケール(柔軟性) ───────────────────────────────────────────────────── 【結論】 データ量が少ない(<100万枚): → CNN推奨 → または事前学習済みViT + 転移学習 データ量が多い(>1000万枚): → ViT推奨 → スケーラビリティを活かす

3-4. 受容野の違い

【受容野(Receptive Field)の比較】 ■ CNNの受容野 第1層(3×3畳み込み): 3×3 = 9ピクセル 第2層: 5×5 = 25ピクセル 第3層: 7×7 = 49ピクセル … 視覚的なイメージ: 入力画像 ┌─────────────────────────┐ │ │ │ ┌───┐ │ ← 第1層の受容野(局所的) │ │ │ │ │ └───┘ │ │ │ └─────────────────────────┘ → 層を重ねるごとに徐々に拡大 → 深い層でやっと画像全体を見る ■ ViTの受容野 第1層(Self-Attention): 画像全体 視覚的なイメージ: 入力画像 ┌─────────────────────────┐ │█████████████████████████│ ← 第1層から画像全体を参照 │█████████████████████████│ │█████████████████████████│ │█████████████████████████│ │█████████████████████████│ └─────────────────────────┘ → 最初から画像全体を見る → 長距離依存性を直接捕捉 ───────────────────────────────────────────────────── 【具体例: 猫の認識】 画像: 猫が右下、尻尾が左上にある CNNの場合: 第1層: 猫の顔の一部、尻尾の一部を別々に認識 第2層: 猫の顔全体、尻尾全体を認識 … 深い層: やっと「顔と尻尾は同じ猫」と認識 → 多層が必要 ViTの場合: 第1層: 「顔パッチと尻尾パッチは関連」を直接学習 → 1層目から長距離の関係を捉える ───────────────────────────────────────────────────── 【それぞれの利点】 CNNの利点: ・局所パターン(エッジ、テクスチャ)に強い ・計算効率が良い ・少量データで学習可能 ViTの利点: ・長距離依存性を直接捉える ・グローバルな文脈を理解 ・大規模データでスケール

3-5. 実務での使い分け

条件 CNN推奨 ViT推奨
データ量 少量〜中量
(数千〜数十万枚)
大量
(数百万枚以上)
計算資源 限られたGPU 豊富なGPU
推論速度 速度重視 精度重視
タスク 物体検出、セグメンテーション 画像分類、大規模事前学習
転移学習 ImageNet事前学習で十分 大規模事前学習ViTを利用
🎯 使い分けのポイント

CNN選択: 少量データ、効率重視、実績豊富
ViT選択: 大規模データ、精度重視、最先端
ハイブリッド: CNNとViTの組み合わせも有効
事前学習済みViT: 転移学習で少量データにも対応可能

📊 4. ViTのバリエーション

4-1. モデルサイズのバリエーション

モデル 層数 埋め込み次元 ヘッド数 MLP次元 パラメータ数
ViT-Tiny 12 192 3 768 5.7M
ViT-Small 12 384 6 1536 22M
ViT-Base 12 768 12 3072 86M
ViT-Large 24 1024 16 4096 307M
ViT-Huge 32 1280 16 5120 632M

4-2. パッチサイズのバリエーション

【パッチサイズの影響】 ■ ViT-Base/16(標準) パッチサイズ: 16×16 224×224画像 → 14×14 = 196パッチ 計算量: 196² = 38,416(Attention) 特徴: ・バランスが良い ・標準的な設定 ■ ViT-Base/32(軽量) パッチサイズ: 32×32 224×224画像 → 7×7 = 49パッチ 計算量: 49² = 2,401(Attention) 特徴: ・計算量が少ない(約1/16) ・精度は低下 ■ ViT-Base/8(高精度) パッチサイズ: 8×8 224×224画像 → 28×28 = 784パッチ 計算量: 784² = 614,656(Attention) 特徴: ・細かい情報を保持 ・計算量が多い(約16倍) ───────────────────────────────────────────────────── 【トレードオフ】 パッチサイズ ↓: ・パッチ数 ↑ ・計算量 ↑↑(O(N²)なので二乗で増加) ・細かい情報を保持 ・精度 ↑ パッチサイズ ↑: ・パッチ数 ↓ ・計算量 ↓↓ ・情報が粗くなる ・精度 ↓ 一般的な傾向: /16 が最もバランスが良い

💻 5. 実装:ViTの構築

PyTorchを使ってVision Transformerをゼロから実装します。各コンポーネントを順番に構築していきましょう。

5-1. パッチ埋め込み層の実装

※ コードが横に長い場合は横スクロールできます

# =================================================== # Vision Transformer(ViT)の実装 # =================================================== import torch import torch.nn as nn import torch.nn.functional as F # =================================================== # 1. パッチ埋め込み層 # =================================================== class PatchEmbedding(nn.Module): “”” 画像をパッチに分割して埋め込みベクトルに変換 処理の流れ: 1. 画像を16×16のパッチに分割 2. 各パッチを768次元ベクトルに変換(線形変換) “”” def __init__(self, img_size=224, patch_size=16, in_channels=3, embed_dim=768): “”” Args: img_size: 入力画像のサイズ(正方形を想定) patch_size: パッチのサイズ(16×16が標準) in_channels: 入力チャンネル数(RGBなら3) embed_dim: 埋め込み次元(ViT-Baseは768) “”” super().__init__() self.img_size = img_size self.patch_size = patch_size # パッチ数を計算: (224÷16) × (224÷16) = 14×14 = 196 self.num_patches = (img_size // patch_size) ** 2 # 線形変換を畳み込みで実装 # kernel_size=stride=patch_size により、重なりなしでパッチを処理 # これは各パッチを展開して線形変換するのと数学的に同じ self.proj = nn.Conv2d( in_channels, # 入力: 3チャンネル(RGB) embed_dim, # 出力: 768チャンネル kernel_size=patch_size, # 16×16のカーネル stride=patch_size # 16ピクセルずつ移動(重なりなし) ) def forward(self, x): “”” Args: x: 入力画像 (batch_size, 3, 224, 224) Returns: パッチ埋め込み (batch_size, num_patches, embed_dim) = (batch_size, 196, 768) “”” # 畳み込みでパッチ埋め込み # (B, 3, 224, 224) → (B, 768, 14, 14) x = self.proj(x) # 空間次元を1次元に展開 # (B, 768, 14, 14) → (B, 768, 196) x = x.flatten(2) # 次元を入れ替えてTransformerの入力形式に # (B, 768, 196) → (B, 196, 768) x = x.transpose(1, 2) return x

5-2. Multi-Head Self-Attentionの実装

# =================================================== # 2. Multi-Head Self-Attention # =================================================== class MultiHeadSelfAttention(nn.Module): “”” Multi-Head Self-Attention 処理の流れ: 1. 入力からQ(Query), K(Key), V(Value)を計算 2. Q×K^Tで各トークン間のAttentionスコアを計算 3. softmaxで正規化 4. Attention重みでVを重み付け和 5. 複数ヘッドの結果を結合 “”” def __init__(self, embed_dim=768, num_heads=12, dropout=0.0): “”” Args: embed_dim: 埋め込み次元(768) num_heads: Attentionヘッド数(12) dropout: ドロップアウト率 “”” super().__init__() self.embed_dim = embed_dim self.num_heads = num_heads # 各ヘッドの次元: 768 ÷ 12 = 64 self.head_dim = embed_dim // num_heads # embed_dimがnum_headsで割り切れることを確認 assert embed_dim % num_heads == 0, \ f”embed_dim ({embed_dim}) must be divisible by num_heads ({num_heads})” # Q, K, V を一度に計算する線形層 # 768次元 → 768×3 = 2304次元 # これを後でQ, K, Vに分割 self.qkv = nn.Linear(embed_dim, embed_dim * 3) # 出力の線形変換 self.proj = nn.Linear(embed_dim, embed_dim) # ドロップアウト self.attn_dropout = nn.Dropout(dropout) self.proj_dropout = nn.Dropout(dropout) # スケーリング係数: 1/√(head_dim) = 1/√64 = 0.125 self.scale = self.head_dim ** -0.5 def forward(self, x): “”” Args: x: 入力 (batch_size, num_tokens, embed_dim) = (B, 197, 768) Returns: 出力 (batch_size, num_tokens, embed_dim) “”” B, N, C = x.shape # B=batch, N=197, C=768 # Q, K, V を計算 # (B, N, C) → (B, N, 3*C) → (B, N, 3, num_heads, head_dim) qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, self.head_dim) # 次元を入れ替え: (3, B, num_heads, N, head_dim) qkv = qkv.permute(2, 0, 3, 1, 4) # Q, K, V に分割(それぞれ (B, num_heads, N, head_dim)) q, k, v = qkv[0], qkv[1], qkv[2] # Attentionスコアを計算 # Q × K^T: (B, h, N, d) × (B, h, d, N) = (B, h, N, N) # スケーリングで値が大きくなりすぎるのを防ぐ attn = (q @ k.transpose(-2, -1)) * self.scale # softmaxで正規化(各行の合計が1になる) attn = attn.softmax(dim=-1) # ドロップアウト(正則化) attn = self.attn_dropout(attn) # Attention重みでVを重み付け和 # (B, h, N, N) × (B, h, N, d) = (B, h, N, d) x = attn @ v # ヘッドを結合 # (B, h, N, d) → (B, N, h, d) → (B, N, C) x = x.transpose(1, 2).reshape(B, N, C) # 出力の線形変換 x = self.proj(x) x = self.proj_dropout(x) return x

5-3. MLP(Feed-Forward Network)の実装

# =================================================== # 3. MLP(Feed-Forward Network) # =================================================== class MLP(nn.Module): “”” Feed-Forward Network 構造: Linear(D, 4D) → GELU → Dropout → Linear(4D, D) → Dropout 例: 768 → 3072 → 768 “”” def __init__(self, in_features, hidden_features=None, dropout=0.0): “”” Args: in_features: 入力次元(768) hidden_features: 隠れ層の次元(デフォルト: 入力の4倍 = 3072) dropout: ドロップアウト率 “”” super().__init__() # 隠れ層の次元(指定がなければ入力の4倍) hidden_features = hidden_features or in_features * 4 # 第1線形層: 768 → 3072 self.fc1 = nn.Linear(in_features, hidden_features) # 活性化関数: GELU(ReLUより滑らか) # GELU(x) = x × Φ(x) where Φ is the CDF of standard normal self.act = nn.GELU() # 第2線形層: 3072 → 768 self.fc2 = nn.Linear(hidden_features, in_features) # ドロップアウト self.dropout = nn.Dropout(dropout) def forward(self, x): “”” Args: x: (batch_size, num_tokens, embed_dim) Returns: (batch_size, num_tokens, embed_dim) “”” x = self.fc1(x) # 768 → 3072 x = self.act(x) # GELU活性化 x = self.dropout(x) # ドロップアウト x = self.fc2(x) # 3072 → 768 x = self.dropout(x) # ドロップアウト return x

5-4. Transformer Blockの実装

# =================================================== # 4. Transformer Block(Encoder Layer) # =================================================== class TransformerBlock(nn.Module): “”” Transformer Encoder Block 構造: Input │ ├──→ LayerNorm → MHSA ─┐ │ │ └──────────────────────┼──→ Add(残差接続) │ ├──→ LayerNorm → MLP ─┐ │ │ └──────────────────────┼──→ Add(残差接続) │ Output “”” def __init__(self, embed_dim=768, num_heads=12, mlp_ratio=4.0, dropout=0.0): “”” Args: embed_dim: 埋め込み次元(768) num_heads: Attentionヘッド数(12) mlp_ratio: MLPの拡大率(4倍) dropout: ドロップアウト率 “”” super().__init__() # Layer Normalization 1(Attention前) self.norm1 = nn.LayerNorm(embed_dim) # Multi-Head Self-Attention self.attn = MultiHeadSelfAttention(embed_dim, num_heads, dropout) # Layer Normalization 2(MLP前) self.norm2 = nn.LayerNorm(embed_dim) # MLP(Feed-Forward Network) mlp_hidden_dim = int(embed_dim * mlp_ratio) # 768 × 4 = 3072 self.mlp = MLP(embed_dim, mlp_hidden_dim, dropout) def forward(self, x): “”” Args: x: (batch_size, num_tokens, embed_dim) Returns: (batch_size, num_tokens, embed_dim) “”” # Attention + 残差接続 # Pre-Norm: 正規化を先に行う(オリジナルTransformerはPost-Norm) x = x + self.attn(self.norm1(x)) # MLP + 残差接続 x = x + self.mlp(self.norm2(x)) return x

5-5. Vision Transformer(完全版)の実装

# =================================================== # 5. Vision Transformer(完全版) # =================================================== class VisionTransformer(nn.Module): “”” Vision Transformer (ViT) 全体の流れ: 1. 画像をパッチに分割して埋め込み 2. [CLS]トークンを追加 3. 位置埋め込みを追加 4. Transformer Encoderで処理(12層) 5. [CLS]トークンの出力を分類ヘッドに入力 “”” def __init__( self, img_size=224, # 入力画像サイズ patch_size=16, # パッチサイズ in_channels=3, # 入力チャンネル数(RGB) num_classes=1000, # 分類クラス数(ImageNet) embed_dim=768, # 埋め込み次元 depth=12, # Transformer層数 num_heads=12, # Attentionヘッド数 mlp_ratio=4.0, # MLP拡大率 dropout=0.1 # ドロップアウト率 ): super().__init__() # パッチ埋め込み self.patch_embed = PatchEmbedding( img_size, patch_size, in_channels, embed_dim ) num_patches = self.patch_embed.num_patches # 196 # [CLS]トークン(学習可能なパラメータ) # 形状: (1, 1, 768) → バッチサイズ分複製して使用 self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim)) # 位置埋め込み(学習可能なパラメータ) # 形状: (1, 197, 768) → [CLS] + 196パッチ self.pos_embed = nn.Parameter(torch.zeros(1, num_patches + 1, embed_dim)) # ドロップアウト(位置埋め込み後) self.pos_drop = nn.Dropout(dropout) # Transformer Encoder(12層のブロック) self.blocks = nn.ModuleList([ TransformerBlock(embed_dim, num_heads, mlp_ratio, dropout) for _ in range(depth) ]) # 最終的なLayer Normalization self.norm = nn.LayerNorm(embed_dim) # 分類ヘッド: 768 → 1000クラス self.head = nn.Linear(embed_dim, num_classes) # 重みの初期化 self._init_weights() def _init_weights(self): “””重みの初期化””” # 位置埋め込みと[CLS]トークンの初期化 # truncated normal: 正規分布から外れ値を切り捨て nn.init.trunc_normal_(self.pos_embed, std=0.02) nn.init.trunc_normal_(self.cls_token, std=0.02) # 他のパラメータの初期化 self.apply(self._init_module_weights) def _init_module_weights(self, m): “””モジュールごとの重み初期化””” if isinstance(m, nn.Linear): nn.init.trunc_normal_(m.weight, std=0.02) if m.bias is not None: nn.init.constant_(m.bias, 0) elif isinstance(m, nn.LayerNorm): nn.init.constant_(m.bias, 0) nn.init.constant_(m.weight, 1.0) def forward(self, x): “”” Args: x: 入力画像 (batch_size, 3, 224, 224) Returns: クラス確率 (batch_size, num_classes) “”” B = x.shape[0] # バッチサイズ # 1. パッチ埋め込み # (B, 3, 224, 224) → (B, 196, 768) x = self.patch_embed(x) # 2. [CLS]トークンを追加 # (1, 1, 768) → (B, 1, 768) cls_tokens = self.cls_token.expand(B, -1, -1) # (B, 1, 768) + (B, 196, 768) → (B, 197, 768) x = torch.cat((cls_tokens, x), dim=1) # 3. 位置埋め込みを追加 # (B, 197, 768) + (1, 197, 768) → (B, 197, 768) x = x + self.pos_embed x = self.pos_drop(x) # 4. Transformer Encoder(12層) for block in self.blocks: x = block(x) # 5. Layer Normalization x = self.norm(x) # 6. [CLS]トークンの出力を取得 # (B, 197, 768) → (B, 768) cls_token_final = x[:, 0] # 7. 分類ヘッド # (B, 768) → (B, num_classes) out = self.head(cls_token_final) return out

5-6. モデルの使用例

# =================================================== # 6. 使用例 # =================================================== # ViT-Base/16 モデルを作成 model = VisionTransformer( img_size=224, patch_size=16, num_classes=1000, embed_dim=768, depth=12, num_heads=12, mlp_ratio=4.0 ) # パラメータ数を確認 total_params = sum(p.numel() for p in model.parameters()) print(f”Total parameters: {total_params:,}”)

実行結果:

Total parameters: 86,567,656
# 推論テスト # ダミーの入力画像を作成 x = torch.randn(2, 3, 224, 224) # バッチサイズ2 # 推論 model.eval() # 評価モード with torch.no_grad(): output = model(x) print(f”Input shape: {x.shape}”) print(f”Output shape: {output.shape}”)

実行結果:

Input shape: torch.Size([2, 3, 224, 224]) Output shape: torch.Size([2, 1000])

5-7. 事前学習済みモデルの使用(timm)

# =================================================== # 7. 事前学習済みモデルの使用(timmライブラリ) # =================================================== # timmのインストール(Google Colabの場合) # !pip install timm import timm # 利用可能なViTモデルを確認 vit_models = timm.list_models(‘vit*’, pretrained=True) print(“利用可能なViTモデル(一部):”) for model_name in vit_models[:10]: print(f” {model_name}”)

実行結果:

利用可能なViTモデル(一部): vit_base_patch14_dinov2.lvd142m vit_base_patch14_reg4_dinov2.lvd142m vit_base_patch16_224 vit_base_patch16_224.augreg_in1k vit_base_patch16_224.augreg_in21k vit_base_patch16_224.augreg_in21k_ft_in1k vit_base_patch16_224.dino vit_base_patch16_224.mae vit_base_patch16_224.orig_in21k_ft_in1k vit_base_patch16_224.sam
# 事前学習済みViT-Base/16をロード model_pretrained = timm.create_model( ‘vit_base_patch16_224’, # モデル名 pretrained=True # ImageNetで事前学習済み ) model_pretrained.eval() # モデル情報を確認 print(f”Model: vit_base_patch16_224″) print(f”Number of parameters: {sum(p.numel() for p in model_pretrained.parameters()):,}”)

実行結果:

Model: vit_base_patch16_224 Number of parameters: 86,567,656
# 推論の実行 import torch from PIL import Image import requests from io import BytesIO # サンプル画像をダウンロード(例: 猫の画像) url = “https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg” response = requests.get(url) img = Image.open(BytesIO(response.content)).convert(‘RGB’) # 前処理 # timmの設定に合わせた前処理を取得 data_config = timm.data.resolve_model_data_config(model_pretrained) transforms = timm.data.create_transform(**data_config, is_training=False) # 画像を前処理 img_tensor = transforms(img).unsqueeze(0) # バッチ次元を追加 # 推論 with torch.no_grad(): output = model_pretrained(img_tensor) probabilities = torch.softmax(output, dim=1) # Top-5予測を取得 top5_prob, top5_idx = torch.topk(probabilities, 5) # ImageNetのクラス名を取得(簡易版) # 実際にはimagenet_classes.txtなどから読み込む print(“\nTop-5 predictions:”) for i in range(5): print(f” Class {top5_idx[0][i].item()}: {top5_prob[0][i].item()*100:.2f}%”)

実行結果:

Top-5 predictions: Class 281: 45.23% Class 282: 18.67% Class 285: 12.34% Class 287: 8.91% Class 283: 5.12%
💡 実装のポイントまとめ

1. パッチ埋め込み:Conv2d(stride=patch_size)で効率的に実装
2. [CLS]トークン:学習可能なパラメータとして追加
3. 位置埋め込み:各トークンに足し算で追加
4. Transformer Block:Pre-Norm(LayerNorm → Attention)を採用
5. 分類:[CLS]トークンの出力のみを使用

📝 練習問題

問題1:パッチ数の計算(基礎)

以下の条件でViTのパッチ数を計算してください。

画像サイズ: 384×384 パッチサイズ: 16×16
解答:

パッチ数の計算:

パッチ数 = (画像の高さ ÷ パッチサイズ) × (画像の幅 ÷ パッチサイズ) パッチ数 = (384 ÷ 16) × (384 ÷ 16) = 24 × 24 = 576パッチ

関連する計算:

各パッチのサイズ: 16 × 16 × 3(RGB)= 768要素 [CLS]トークン追加後: 576 + 1 = 577トークン Self-Attentionの計算量: O(N²) = 577² = 332,929 比較(224×224画像の場合): パッチ数: 196 計算量: 196² = 38,416 → 384×384は約8.6倍の計算量

問題2:CNNとViTのInductive Bias(中級)

CNNの「局所性」と「平行移動不変性」というInductive Biasがそれぞれどのように実装されているか、またViTにこれらがない理由を説明してください。

解答:

1. CNNの局所性(Locality)

定義: 「近くのピクセルは関連性が高い」という仮定 実装: 畳み込み層は局所領域のみを処理 例: 3×3カーネル ┌─┬─┬─┐ │×│×│×│ ← 9ピクセルのみ参照 ├─┼─┼─┤ 遠くのピクセルは見ない │×│●│×│ ├─┼─┼─┤ │×│×│×│ └─┴─┴─┘ 効果: ・少ないパラメータ(9個)で局所パターン学習 ・エッジ、テクスチャを効率的に検出 ・少量データで学習可能

2. CNNの平行移動不変性(Translation Invariance)

定義: 「物体の位置が変わっても同じ特徴を検出」という仮定 実装: 同じフィルタを画像全体に適用(重み共有) フィルタ(例: 縦エッジ検出): [[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]] このフィルタを全位置に適用 → どの位置でも同じ縦エッジを検出 効果: ・位置に依存しない認識 ・パラメータ数削減 ・汎化性能の向上

3. ViTにこれらがない理由

Self-Attentionの性質: 各パッチは全パッチとの関係を計算 Attention(パッチ1) = Σ w_i × パッチ_i → 近いパッチも遠いパッチも同等に扱う → 局所性の仮定なし 位置情報は位置埋め込みで追加 → 位置埋め込みなしではパッチの順番を区別不可 → 平行移動不変性の仮定なし トレードオフ: CNN(強いInductive Bias): + 少量データで学習可能 + 効率的 – 柔軟性が低い ViT(弱いInductive Bias): + 柔軟性が高い + 大規模データでスケール – 大量データが必要

問題3:Self-Attentionの計算量(中級)

ViT-Base(224×224画像、16×16パッチ)の1層のSelf-Attentionの計算量を概算してください。

解答:

与えられた情報:

画像サイズ: 224×224 パッチサイズ: 16×16 埋め込み次元: D = 768 ヘッド数: h = 12 ヘッドあたり次元: d_k = 768/12 = 64

1. トークン数の計算

パッチ数: (224/16)² = 14² = 196 [CLS]トークン追加: N = 197

2. Q, K, V の計算量

各行列の計算: X × W X: (N, D) = (197, 768) W: (D, D) = (768, 768) 計算量: N × D × D = 197 × 768² ≈ 116M FLOPs Q, K, V の3つ: 3 × 116M = 348M FLOPs

3. Attentionスコアの計算量

Q × K^T: Q: (h, N, d_k) = (12, 197, 64) K^T: (h, d_k, N) = (12, 64, 197) 計算量: h × N × d_k × N = 12 × 197 × 64 × 197 ≈ 30M FLOPs

4. Attention × V の計算量

Attention × V: Attention: (h, N, N) = (12, 197, 197) V: (h, N, d_k) = (12, 197, 64) 計算量: h × N × N × d_k = 12 × 197 × 197 × 64 ≈ 30M FLOPs

5. 出力の線形変換

出力変換: X × W_O X: (N, D) = (197, 768) W_O: (D, D) = (768, 768) 計算量: N × D × D = 197 × 768² ≈ 116M FLOPs

6. 合計

1層のSelf-Attention合計: Q, K, V: 348M Q × K^T: 30M Attn × V: 30M 出力変換: 116M ───────────── 合計: 約524M FLOPs ≈ 0.5G FLOPs 12層の場合: 12 × 0.5G = 約6G FLOPs(Attention部分のみ) ※ MLPも含めると約17G FLOPs

問題4:[CLS]トークンの役割(上級)

ViTで[CLS]トークンを使う理由と、全パッチの平均を使わない理由を説明してください。

解答:

1. [CLS]トークンの役割

■ 情報の集約 Self-Attentionにより、[CLS]はすべてのパッチを「見る」 [CLS]_out = Σ Attention([CLS], パッチ_i) × パッチ_i → 画像全体の情報が[CLS]に集約される ■ タスク固有の表現 [CLS]は学習可能なパラメータ → 分類タスクに最適化された表現を学習 → 「猫か犬か」を判別する情報を抽出

2. 全パッチ平均を使わない理由

■ 問題点1: 情報の希釈 画像: 猫が右下にいる 平均化: 出力 = (150×背景 + 46×猫) / 196 → 猫の情報が背景で薄まる [CLS]の場合: [CLS]_out = 0.05×背景 + 0.70×猫 → 重要な猫に高いAttention ■ 問題点2: 適応的でない 平均化: すべてのパッチに等しい重み(1/196) → 画像によらず固定 [CLS]の場合: 画像ごとに最適な重み付けを学習 猫画像 → 猫パッチに高Attention 犬画像 → 犬パッチに高Attention ■ 問題点3: タスク固有の最適化なし 各パッチの出力: 視覚的特徴を表現(汎用的) 平均化: 汎用表現の平均 → タスク固有の最適化なし [CLS]の場合: 分類に最適化された表現を学習

3. 実験結果

ViT論文の実験(ImageNet): [CLS]トークン使用: 81.8% 全パッチ平均: 79.5% → [CLS]が約2%高い 理由: [CLS]は適応的に重要な情報を集約 平均化は情報を均一に扱い、重要な情報が希釈

問題5:データ量と性能の関係(上級)

ViTが大規模データで真価を発揮する理由をInductive Biasの観点から説明してください。

解答:

1. Inductive Biasと学習の関係

Inductive Bias = モデルが持つ「事前知識」 強いInductive Bias(CNN): 「近くのピクセルは関連」 「位置が変わっても同じ特徴」 → この仮定が正しければ、少ないデータで学習可能 → でも仮定に縛られる 弱いInductive Bias(ViT): ほぼ仮定なし → すべてをデータから学習 → 大量データが必要 → でも柔軟

2. 少量データでCNNが強い理由

例: 1000枚で犬猫分類 CNN: 第1層: 局所性の仮定で効率的にエッジ学習 第2層: 同じ仮定でテクスチャ学習 … → 仮定が学習をガイド → 1000枚で十分な精度(95%) ViT: 各パッチが全パッチとの関係を学習 → 196² = 38,416の関係 → 12層 × 12ヘッド = 大量のパラメータ → 1000枚では学習データ不足 → 過学習(70%)

3. 大規模データでViTが強い理由

例: 3億枚で学習 CNN: 仮定(局所性など)が性能の上限を決める データを増やしても、仮定を超えられない → 飽和(84.7%) ViT: 仮定なし → データから最適な処理を学習 学習されること: ・どのパッチが重要か(Attention) ・長距離の関係(顔と尻尾) ・CNNの仮定を超えた表現 → データに応じてスケール(87.8%)

4. 具体例

長距離依存性の学習: 画像: 猫の顔が左上、尻尾が右下 CNN: 局所性の仮定 → 顔と尻尾を直接関連付けられない → 多層を重ねて徐々に受容野拡大 → 効率が悪い ViT: Self-Attention → 第1層から「顔パッチと尻尾パッチは関連」を学習 → 直接、長距離の関係を捉える 大規模データで: 多様な猫の画像から 「顔と尻尾の関係」を正確に学習

5. 結論

データ量と性能: 少量データ: CNN > ViT(Inductive Biasが有効) 大量データ: CNN < ViT(柔軟性が有効) 本質: CNN: 「仮定に基づく効率的な学習」 ViT: 「データに基づく柔軟な学習」 データが少ない → CNNの仮定が有利 データが多い → ViTの柔軟性が有利

📝 STEP 16 のまとめ

✅ このステップで学んだこと

1. ViTの革新
・画像をパッチに分割し、「単語」として扱う
・CNNを使わない純粋なTransformerアーキテクチャ

2. アーキテクチャの詳細
・パッチ埋め込み:16×16パッチを768次元ベクトルに変換
・位置埋め込み:学習可能な位置情報を追加
・[CLS]トークン:画像全体の情報を集約

3. CNNとの比較
・CNN:強いInductive Bias、少量データで有効
・ViT:弱いInductive Bias、大規模データで真価

4. 実装
・PyTorchでViTをゼロから構築
・timmライブラリで事前学習済みモデルを利用

💡 重要ポイント

ViT(Vision Transformer)は、画像をパッチに分割し、Self-Attentionで処理する革新的なアーキテクチャです。

大規模データで事前学習されたViTは、転移学習により少量データでも高い性能を発揮します。

次のSTEP 17では、「ViTの実装と応用」を学びます。
HuggingFace Transformersでの実装、ファインチューニング、Attention Mapの可視化を習得します!

📝

学習メモ

コンピュータビジョン(CV) - Step 16

📋 過去のメモ一覧
#artnasekai #学習メモ
LINE