📋 このステップで学ぶこと
- 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の可視化を習得します!