主要な知見
  • Transformer[1]のエンコーダ・デコーダアーキテクチャは、精密に設計された6つのコンポーネントで構成されます:マルチヘッドアテンション、フィードフォワードネットワーク、残差接続、Layer Normalization[2]、位置エンコーディング、マスキング機構
  • 3大事前学習パラダイム——マスク言語モデル(BERT[5])、因果言語モデル(GPT[4])、ノイズ除去再構成(T5[7]/BART[8])——はそれぞれ独自の強みを持ち、現代のすべての大規模言語モデルを生み出しました
  • TransformerはNLPからビジョン(ViT[11])、マルチモーダル(CLIP[13])、タンパク質折りたたみ、ロボット制御まで拡張され、AIの汎用計算プリミティブとなっています
  • 本記事にはGoogle Colab実践ラボ2本を収録:ゼロからTransformer機械翻訳モデルを構築、ViT画像分類ファインチューニング+特徴可視化

1. なぜTransformerはすべてを変えたのか

2017年、Vaswaniらが「Attention Is All You Need」[1]でTransformerを提案した時点では、それは単なる機械翻訳のためのモデルでした。しかしわずか数年のうちに、このアーキテクチャは人工知能の汎用インフラストラクチャへと変貌しました——GPT-4はテキスト生成に、DALL-Eは画像生成に、AlphaFoldはタンパク質構造予測に、RT-2はロボット制御に、すべてこのアーキテクチャを使用しています。

なぜTransformerはこれほど多様な分野を統一できたのでしょうか。それは極めて汎用的な計算プリミティブを提供しているからです:一連の要素がアテンション機構を通じて動的に互いにコミュニケーションすることを可能にする。これらの要素がテキストトークン、画像パッチ、アミノ酸残基、あるいはロボット動作のいずれであっても、Transformerはそれらの間の関係を学習できます。

本記事ではTransformerを完全なシステムとして深く解剖します——アテンション機構だけでなく(それについてはセルフアテンション機構完全ガイドで詳述しています)、Transformerを学習可能・スケーラブル・転移可能にするすべてのエンジニアリング上の設計判断も含めて解説します。

2. アーキテクチャ解剖:6つのコアコンポーネント

オリジナルのTransformerはEncoder-Decoderアーキテクチャですが、そのコアコンポーネントはすべてのバリエーションにおいて繰り返し登場します:

Transformer 完全アーキテクチャ:

Encoder (×N 層):                    Decoder (×N 層):
┌─────────────────────┐            ┌─────────────────────┐
│  Multi-Head          │            │  Masked Multi-Head   │
│  Self-Attention      │            │  Self-Attention       │
│  + Residual + LN     │            │  + Residual + LN     │
├─────────────────────┤            ├─────────────────────┤
│  Feed-Forward        │            │  Cross-Attention     │
│  Network (FFN)       │            │  (Q=Dec, K,V=Enc)   │
│  + Residual + LN     │            │  + Residual + LN     │
└─────────────────────┘            ├─────────────────────┤
                                    │  Feed-Forward        │
                                    │  Network (FFN)       │
                                    │  + Residual + LN     │
                                    └─────────────────────┘

コンポーネント1:マルチヘッドSelf-Attention

各位置がシーケンス内の他のすべての位置に直接アテンドすることを可能にします。エンコーダは双方向アテンションを使用し、デコーダは因果マスキング(過去のトークンのみ参照可能)を使用します。詳細な数学的導出はセルフアテンション機構完全ガイドをご参照ください。

コンポーネント2:フィードフォワードネットワーク(FFN)

各層のアテンション出力は2層の全結合ネットワークを通過します——これがTransformerの「思考」空間です:

FFN(x) = max(0, x · W₁ + b₁) · W₂ + b₂

元の構成: d_model=512, d_ff=2048 (4倍拡張)
現代の構成: ReLUの代わりにSwiGLU活性化(LLaMA)またはGELU(GPT)を使用

研究によると、FFN層は大量の「事実的知識」を蓄積していることが分かっています。GPTなどのモデルでは、FFNのパラメータが全パラメータの約2/3を占めており、モデルの記憶能力の主要な担い手となっています。

コンポーネント3:残差接続

各サブ層の入力がその出力に加算されます:output = sublayer(x) + x。これにより勾配が層を直接跨いで流れることが可能となり、深層Transformer(100層以上)の学習に不可欠です。

コンポーネント4:Layer Normalization

Baら[2]が提案したLayer Normalizationは、各トークンの表現をゼロ平均・単位分散に正規化します。オリジナルのTransformerはPost-LN(残差接続の後に正規化)を採用していましたが、Xiongら[3]Pre-LN(残差接続の前に正規化)が学習率ウォームアップの必要性を排除し、より安定した学習を実現することを証明しました。現代の大規模モデルはほぼ全てPre-LNまたはRMSNormを使用しています。

構成順序特性代表的モデル
Post-LNAttn → Add → LN学習率ウォームアップが必要だが、最終性能がより高い可能性オリジナルTransformer、BERT
Pre-LNLN → Attn → Add学習が安定、ウォームアップ不要GPT-2、GPT-3
RMSNormPre-LNと類似スケーリングのみ(中心化なし)、計算が高速LLaMA、PaLM

コンポーネント5:位置エンコーディング

シーケンスに順序情報を注入します。オリジナルのTransformerは正弦波位置エンコーディング[1]を使用していましたが、現代のモデルの多くは回転位置エンコーディング(RoPE)またはALiBiを採用しています。

コンポーネント6:クロスアテンションとマスキング

デコーダ内のクロスアテンションにより、生成プロセスがエンコーダの出力を「参照」できます:QueryはDecoderから、KeyとValueはEncoderから取得されます。因果マスキングにより、デコーダがt番目のトークンを生成する際に、最初のt-1個のトークンのみ参照可能であることが保証されます。

3. 学習の技法:Transformerを収束させる

Transformerの学習は従来のモデルよりも困難です。元論文[1]における一見地味に見えるいくつかのテクニックが、実際には極めて重要です:

テクニック詳細なぜ重要か
学習率ウォームアップ最初の4000ステップで線形に増加、その後平方根減衰学習初期のPost-LNにおける勾配爆発を防止[3]
Label Smoothing目標分布をone-hotから0.9/0.1の混合に変更汎化能力を向上、過度な確信を防止
Dropoutアテンション重み + FFN + 残差後にそれぞれ0.1過学習を防止、正則化を提供
Adam + β₂=0.98より高いモメンタム減衰アテンション行列の勾配を安定化
勾配クリッピンググローバル勾配ノルムを1.0にクリップ勾配爆発を防止

4. 事前学習パラダイム:三つの分岐する道

Transformerの真の力は「事前学習 → ファインチューニング」というパラダイムにおいて爆発的に発揮されます。3つの主要な事前学習戦略にはそれぞれ独自の強みがあります:

路線1:マスク言語モデル(MLM)— BERT

BERT[5]はEncoder-onlyアーキテクチャを使用し、15%のトークンをランダムにマスクしてモデルに予測させます:

Input: The [MASK] sat on the [MASK]
Target: Predict [MASK] = "cat" and [MASK] = "mat"

利点: 双方向コンテキスト — 各トークンが左右すべての近傍を参照可能
欠点: 学習時に[MASK]が存在するが推論時には存在しない → 事前学習-ファインチューニング間の不一致

路線2:因果言語モデル(CLM)— GPT

GPT[4]はDecoder-onlyアーキテクチャを使用し、次のトークンを予測します:

Input: The cat sat on the
Target: Predict the next token = "mat"

利点: 生成タスクに自然に適合、事前学習-ファインチューニング間の不一致なし
欠点: 単方向コンテキスト — 各トークンは左側のみ参照可能

GPT-3[6]の1750億パラメータバージョンはin-context learningを実証しました——ファインチューニングなしに、プロンプト内のわずかな例示だけでタスクを実行できます。これはAIの使用方法を根本的に変革しました。

路線3:ノイズ除去再構成 — T5 / BART

T5[7]はすべてのNLPタスクをtext-to-text形式に統一し、完全なEncoder-Decoderアーキテクチャを使用します:

Translation: "translate English to German: That is good" → "Das ist gut"
Summarization: "summarize: long text..." → "short summary"
Classification: "sentiment: This movie is great" → "positive"

事前学習: span corruption — ランダムに連続するspanをマスクしてモデルに再構成させる

BART[8]はより多様なノイズ除去戦略——削除、シャッフル、マスキング、回転——を使用し、Encoder-Decoderが損傷したテキストから元のテキストを再構成するよう学習させます。

パラダイムアーキテクチャ代表モデル最適なタスク
MLMEncoder-onlyBERT[5]分類、固有表現認識、質問応答
CLMDecoder-onlyGPT[4]、LLaMA[9]生成、対話、コード
ノイズ除去Encoder-DecoderT5[7]、BART[8]翻訳、要約、構造化生成

5. 主要バリエーションの進化マップ

2017年のオリジナルTransformerから2024年のGPT-4、Claude、Geminiまで、アーキテクチャは劇的な進化を遂げました:

モデルパラメータ数アーキテクチャ主な革新
Transformer[1]201765MEnc-DecSelf-AttentionがRNNを置換
GPT-1[4]2018117MDec-only生成的事前学習 + 判別的ファインチューニング
BERT[5]2019340MEnc-onlyマスク言語モデル、双方向コンテキスト
T5[7]202011BEnc-Dec統一text-to-textフレームワーク
GPT-3[6]2020175BDec-onlyFew-shot in-context learning
PaLM[10]2022540BDec-onlyPathwaysシステム、SwiGLU、RoPE
LLaMA[9]202365BDec-only公開データ、計算最適な学習

明確なトレンドが浮かび上がります:Decoder-onlyアーキテクチャが大規模言語モデルの主流となっています。LLaMA[9]は重要な結論を実証しました:同じ計算予算の下では、より少ないデータで巨大なモデルを学習するよりも、より多くのデータでより小さなモデルを学習する方が効果的であるということです。LLaMA-13Bは公開データのみで学習されたにもかかわらず、ほとんどのベンチマークでGPT-3(175B)を上回りました。

6. Transformerがビジョンとマルチモーダルを征服する

Vision Transformer(ViT)

ViT[11]は画像を16×16のパッチに分割し、各パッチを1つのトークンとして扱い、Transformer Encoderに直接入力します。DeiT[12]は知識蒸留技術を導入し、ViTが大量のデータ(JFT-300M)なしでは学習できないという問題を解決しました——ImageNetだけで競争力のある性能を達成しています。

CLIP:ビジョンと言語の統一

CLIP[13]は対比学習により画像TransformerとテキストTransformerを同時に学習し、アラインメントされた視覚-言語表現を獲得します。これにより、モデルは自然言語の記述で画像を「検索」できるようになりました——ゼロショット画像分類、画像検索、さらにはStable Diffusionの条件エンコーダとしても使用されています。

Flamingo:マルチモーダルFew-Shot

Flamingo[14]はゲート付きクロスアテンションにより、凍結された視覚エンコーダと凍結された言語モデルを橋渡しし、マルチモーダルのfew-shot学習を実現しました——わずかな例示画像と質問だけで、新しい画像に関する質問に回答できます。

7. 大規模学習:エンジニアリングの技法

千億パラメータのTransformerの学習には、モデル設計をはるかに超えるエンジニアリング上の課題が伴います。以下がその主要な技術です:

技術コアアイデア効果
データ並列各GPUにモデルのレプリカを1つ配置、データを分割線形スケーリングだが、各GPUにフルモデルのメモリが必要
テンソル並列[15]単一層の行列演算を複数のGPUに分割層内並列化、低レイテンシ
パイプライン並列異なる層を異なるGPUに配置GPU当たりのメモリ削減、ただしバブルが発生
ZeRO[16]オプティマイザ状態/勾配/パラメータをGPU間で分割メモリが1/Nに削減、兆パラメータモデルが実現可能に
混合精度計算にFP16/BF16、累積にFP32を使用メモリ半減、速度2倍
勾配チェックポイント一部の層のアクティベーションのみ保存、逆伝播時に再計算メモリO(√n)、追加の順伝播1回が代償

Megatron-LM[15]は512基のGPUで76%のスケーリング効率を達成しました。ZeRO[16]はデータ並列におけるメモリ冗長性を排除しました——従来のデータ並列では各GPUがオプティマイザ状態の完全なコピーを保持(Adamは16 bytes/パラメータを必要)していましたが、ZeROはこれをすべてのGPUに分割し、兆パラメータモデルを合理的なハードウェアで学習可能にしました。

8. 最前線の進展:MoEとポストTransformer時代

混合エキスパートモデル(Mixture of Experts)

Switch Transformer[17]は大胆なアイデアを提案しました:すべてのトークンがすべてのパラメータを通過する必要はない。MoEは各FFN層を複数の「エキスパート」ネットワークに置き換え、各トークンを1つのエキスパートにのみルーティングします:

MoE Layer:
  入力トークン → Router (小型分類器) → Top-1エキスパートを選択
  Expert 1: FFN₁(x)  ← 一部のトークンを処理
  Expert 2: FFN₂(x)  ← 別のトークンを処理
  ...
  Expert N: FFNₙ(x)

結果: 兆パラメータ、しかし各トークンは約1/Nのパラメータのみ活性化
      → パラメータ数は巨大だが、計算量は密なモデルと同等

Mixtral 8x7BはMoEの成功例です——8つの7Bエキスパートで構成された46Bパラメータモデルで、実際には各トークンが約12Bパラメータのみを使用しますが、70Bの密なモデルと同等の性能を発揮します。

ポストTransformer:状態空間モデル

Mamba[18]は選択的状態空間モデル(Selective SSM)でTransformerの優位性に挑戦しています。線形時間計算量でシーケンスを処理し、同等のスケールでTransformerの5倍のスループットを実現します。Mamba-3Bの性能はTransformer-6Bに匹敵し、Self-Attentionが唯一の道ではない可能性を示唆しています。

しかしTransformerも反撃しています:JambaなどのハイブリッドアーキテクチャはTransformer層とMamba層を交互に使用し、両者の利点を組み合わせています。今後の主流は純粋なTransformerでも純粋なSSMでもなく、ハイブリッドアーキテクチャかもしれません。

9. 実践ラボ1:ゼロからTransformer翻訳モデルを構築する(Google Colab)

以下の実験では、完全なEncoder-Decoder Transformerアーキテクチャをゼロから実装し、簡単な英独翻訳タスクを行います。

# ============================================================
# ラボ1:ゼロからTransformerを構築 — 英独翻訳モデル
# 環境:Google Colab (GPU)
# ============================================================
# --- 0. インストール ---
!pip install -q datasets tokenizers

import torch
import torch.nn as nn
import torch.nn.functional as F
import math
import numpy as np
from torch.utils.data import DataLoader, Dataset
from datasets import load_dataset

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Device: {device}")

# --- 1. データ準備 ---
# Tatoeba英独文ペア(簡単な短文)を使用
dataset = load_dataset("Helsinki-NLP/tatoeba_mt", "deu-eng", split="test")
pairs = [(ex["sourceString"], ex["targetString"]) for ex in dataset]
pairs = [p for p in pairs if len(p[0].split()) <= 15 and len(p[1].split()) <= 15]
pairs = pairs[:8000]
print(f"Sentence pairs: {len(pairs)}")
print(f"Example: '{pairs[0][0]}' → '{pairs[0][1]}'")

# 簡易文字レベルトークナイザー
class SimpleTokenizer:
    def __init__(self):
        self.char2idx = {"<pad>": 0, "<bos>": 1, "<eos>": 2, "<unk>": 3}
        self.idx2char = {0: "<pad>", 1: "<bos>", 2: "<eos>", 3: "<unk>"}

    def fit(self, texts):
        for text in texts:
            for ch in text:
                if ch not in self.char2idx:
                    idx = len(self.char2idx)
                    self.char2idx[ch] = idx
                    self.idx2char[idx] = ch

    def encode(self, text, max_len=80):
        ids = [1] + [self.char2idx.get(ch, 3) for ch in text[:max_len-2]] + [2]
        return ids

    def decode(self, ids):
        chars = []
        for idx in ids:
            if idx == 2: break
            if idx > 2: chars.append(self.idx2char.get(idx, "?"))
        return "".join(chars)

    @property
    def vocab_size(self):
        return len(self.char2idx)

src_tokenizer = SimpleTokenizer()
tgt_tokenizer = SimpleTokenizer()
src_tokenizer.fit([p[0] for p in pairs])
tgt_tokenizer.fit([p[1] for p in pairs])
print(f"Source vocab: {src_tokenizer.vocab_size}, Target vocab: {tgt_tokenizer.vocab_size}")

MAX_LEN = 80

class TranslationDataset(Dataset):
    def __init__(self, pairs, src_tok, tgt_tok):
        self.pairs = pairs
        self.src_tok = src_tok
        self.tgt_tok = tgt_tok

    def __len__(self):
        return len(self.pairs)

    def __getitem__(self, idx):
        src, tgt = self.pairs[idx]
        return self.src_tok.encode(src, MAX_LEN), self.tgt_tok.encode(tgt, MAX_LEN)

def collate_fn(batch):
    src_batch, tgt_batch = zip(*batch)
    src_max = max(len(s) for s in src_batch)
    tgt_max = max(len(t) for t in tgt_batch)
    src_padded = torch.zeros(len(batch), src_max, dtype=torch.long)
    tgt_padded = torch.zeros(len(batch), tgt_max, dtype=torch.long)
    for i, (s, t) in enumerate(zip(src_batch, tgt_batch)):
        src_padded[i, :len(s)] = torch.tensor(s)
        tgt_padded[i, :len(t)] = torch.tensor(t)
    return src_padded, tgt_padded

train_ds = TranslationDataset(pairs[:7000], src_tokenizer, tgt_tokenizer)
val_ds = TranslationDataset(pairs[7000:], src_tokenizer, tgt_tokenizer)
train_loader = DataLoader(train_ds, batch_size=64, shuffle=True, collate_fn=collate_fn)
val_loader = DataLoader(val_ds, batch_size=64, collate_fn=collate_fn)

# --- 2. Transformerモデル(完全なEncoder-Decoder)---
class PositionalEncoding(nn.Module):
    def __init__(self, d_model, max_len=200):
        super().__init__()
        pe = torch.zeros(max_len, d_model)
        pos = torch.arange(0, max_len).unsqueeze(1).float()
        div = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
        pe[:, 0::2] = torch.sin(pos * div)
        pe[:, 1::2] = torch.cos(pos * div)
        self.register_buffer('pe', pe.unsqueeze(0))

    def forward(self, x):
        return x + self.pe[:, :x.size(1)]

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, n_heads):
        super().__init__()
        self.d_k = d_model // n_heads
        self.n_heads = n_heads
        self.W_Q = nn.Linear(d_model, d_model)
        self.W_K = nn.Linear(d_model, d_model)
        self.W_V = nn.Linear(d_model, d_model)
        self.W_O = nn.Linear(d_model, d_model)

    def forward(self, Q, K, V, mask=None):
        B = Q.size(0)
        Q = self.W_Q(Q).view(B, -1, self.n_heads, self.d_k).transpose(1, 2)
        K = self.W_K(K).view(B, -1, self.n_heads, self.d_k).transpose(1, 2)
        V = self.W_V(V).view(B, -1, self.n_heads, self.d_k).transpose(1, 2)
        scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
        if mask is not None:
            scores = scores.masked_fill(mask == 0, float('-inf'))
        attn = F.softmax(scores, dim=-1)
        out = torch.matmul(attn, V)
        out = out.transpose(1, 2).contiguous().view(B, -1, self.n_heads * self.d_k)
        return self.W_O(out)

class EncoderLayer(nn.Module):
    def __init__(self, d_model, n_heads, d_ff, dropout=0.1):
        super().__init__()
        self.self_attn = MultiHeadAttention(d_model, n_heads)
        self.ffn = nn.Sequential(nn.Linear(d_model, d_ff), nn.ReLU(), nn.Linear(d_ff, d_model))
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.drop = nn.Dropout(dropout)

    def forward(self, x, src_mask):
        x = self.norm1(x + self.drop(self.self_attn(x, x, x, src_mask)))
        x = self.norm2(x + self.drop(self.ffn(x)))
        return x

class DecoderLayer(nn.Module):
    def __init__(self, d_model, n_heads, d_ff, dropout=0.1):
        super().__init__()
        self.self_attn = MultiHeadAttention(d_model, n_heads)
        self.cross_attn = MultiHeadAttention(d_model, n_heads)
        self.ffn = nn.Sequential(nn.Linear(d_model, d_ff), nn.ReLU(), nn.Linear(d_ff, d_model))
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.norm3 = nn.LayerNorm(d_model)
        self.drop = nn.Dropout(dropout)

    def forward(self, x, enc_out, src_mask, tgt_mask):
        x = self.norm1(x + self.drop(self.self_attn(x, x, x, tgt_mask)))
        x = self.norm2(x + self.drop(self.cross_attn(x, enc_out, enc_out, src_mask)))
        x = self.norm3(x + self.drop(self.ffn(x)))
        return x

class Transformer(nn.Module):
    def __init__(self, src_vocab, tgt_vocab, d_model=128, n_heads=4,
                 n_layers=3, d_ff=256, dropout=0.1):
        super().__init__()
        self.src_emb = nn.Embedding(src_vocab, d_model, padding_idx=0)
        self.tgt_emb = nn.Embedding(tgt_vocab, d_model, padding_idx=0)
        self.pos_enc = PositionalEncoding(d_model)
        self.encoder = nn.ModuleList([EncoderLayer(d_model, n_heads, d_ff, dropout) for _ in range(n_layers)])
        self.decoder = nn.ModuleList([DecoderLayer(d_model, n_heads, d_ff, dropout) for _ in range(n_layers)])
        self.output_proj = nn.Linear(d_model, tgt_vocab)
        self.dropout = nn.Dropout(dropout)
        self.d_model = d_model

    def make_src_mask(self, src):
        return (src != 0).unsqueeze(1).unsqueeze(2)

    def make_tgt_mask(self, tgt):
        B, N = tgt.shape
        pad_mask = (tgt != 0).unsqueeze(1).unsqueeze(2)
        causal_mask = torch.tril(torch.ones(N, N, device=tgt.device)).bool()
        return pad_mask & causal_mask.unsqueeze(0).unsqueeze(0)

    def encode(self, src):
        src_mask = self.make_src_mask(src)
        x = self.dropout(self.pos_enc(self.src_emb(src) * math.sqrt(self.d_model)))
        for layer in self.encoder:
            x = layer(x, src_mask)
        return x, src_mask

    def decode(self, tgt, enc_out, src_mask):
        tgt_mask = self.make_tgt_mask(tgt)
        x = self.dropout(self.pos_enc(self.tgt_emb(tgt) * math.sqrt(self.d_model)))
        for layer in self.decoder:
            x = layer(x, enc_out, src_mask, tgt_mask)
        return self.output_proj(x)

    def forward(self, src, tgt):
        enc_out, src_mask = self.encode(src)
        return self.decode(tgt, enc_out, src_mask)

# --- 3. 学習 ---
model = Transformer(src_tokenizer.vocab_size, tgt_tokenizer.vocab_size).to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, betas=(0.9, 0.98), eps=1e-9)
criterion = nn.CrossEntropyLoss(ignore_index=0, label_smoothing=0.1)
print(f"Parameters: {sum(p.numel() for p in model.parameters()):,}")

for epoch in range(20):
    model.train()
    total_loss = 0
    for src, tgt in train_loader:
        src, tgt = src.to(device), tgt.to(device)
        tgt_input = tgt[:, :-1]
        tgt_target = tgt[:, 1:]
        logits = model(src, tgt_input)
        loss = criterion(logits.reshape(-1, logits.size(-1)), tgt_target.reshape(-1))
        optimizer.zero_grad()
        loss.backward()
        nn.utils.clip_grad_norm_(model.parameters(), 1.0)
        optimizer.step()
        total_loss += loss.item()
    if (epoch + 1) % 5 == 0:
        print(f"Epoch {epoch+1}: loss={total_loss/len(train_loader):.4f}")

# --- 4. 翻訳推論(Greedy Decoding)---
def translate(text, model, src_tok, tgt_tok, max_len=80):
    model.eval()
    src = torch.tensor([src_tok.encode(text, max_len)]).to(device)
    enc_out, src_mask = model.encode(src)

    tgt_ids = [1]  # <bos>
    for _ in range(max_len):
        tgt = torch.tensor([tgt_ids]).to(device)
        logits = model.decode(tgt, enc_out, src_mask)
        next_id = logits[0, -1].argmax().item()
        if next_id == 2: break  # <eos>
        tgt_ids.append(next_id)

    return tgt_tok.decode(tgt_ids)

# --- 5. 翻訳テスト ---
print("\n=== Translation Results ===")
test_sentences = [p[0] for p in pairs[7000:7010]]
for src_text in test_sentences:
    pred = translate(src_text, model, src_tokenizer, tgt_tokenizer)
    print(f"  EN: {src_text}")
    print(f"  DE: {pred}")
    print()

print("Lab 1 Complete!")

10. 実践ラボ2:ViT画像分類ファインチューニング+特徴可視化(Google Colab)

以下の実験では、事前学習済みViTをCIFAR-10でファインチューニングして画像分類を行い、Transformerが学習したパッチ埋め込みと[CLS]トークン特徴を可視化します。

# ============================================================
# ラボ2:ViT CIFAR-10ファインチューニング + 特徴可視化
# 環境:Google Colab (GPU)
# ============================================================
# --- 0. インストール ---
!pip install -q transformers datasets timm

import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import matplotlib.pyplot as plt
from datasets import load_dataset
from transformers import ViTForImageClassification, ViTFeatureExtractor
from torch.utils.data import DataLoader
from torchvision import transforms
from sklearn.decomposition import PCA
import warnings
warnings.filterwarnings('ignore')

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Device: {device}")

# --- 1. データ準備 ---
dataset = load_dataset("cifar10")
train_data = dataset["train"].shuffle(seed=42).select(range(5000))
test_data = dataset["test"].shuffle(seed=42).select(range(1000))

class_names = ['airplane', 'automobile', 'bird', 'cat', 'deer',
               'dog', 'frog', 'horse', 'ship', 'truck']

transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])

def collate_fn(batch):
    images = torch.stack([transform(ex["img"]) for ex in batch])
    labels = torch.tensor([ex["label"] for ex in batch])
    return images, labels

train_loader = DataLoader(train_data, batch_size=32, shuffle=True, collate_fn=collate_fn)
test_loader = DataLoader(test_data, batch_size=64, shuffle=False, collate_fn=collate_fn)
print(f"Train: {len(train_data)}, Test: {len(test_data)}")

# --- 2. 事前学習済みViTの読み込みと分類ヘッドの交換 ---
model_name = "google/vit-base-patch16-224-in21k"
model = ViTForImageClassification.from_pretrained(
    model_name,
    num_labels=10,
    ignore_mismatched_sizes=True
).to(device)

# 最後の2層と分類ヘッド以外のすべてのパラメータを凍結
for name, param in model.named_parameters():
    if "classifier" not in name and "layernorm" not in name and "encoder.layer.11" not in name:
        param.requires_grad = False

trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
total = sum(p.numel() for p in model.parameters())
print(f"Trainable: {trainable:,} / {total:,} ({trainable/total*100:.1f}%)")

# --- 3. ファインチューニング学習 ---
optimizer = torch.optim.AdamW(
    [p for p in model.parameters() if p.requires_grad],
    lr=2e-4, weight_decay=0.01
)

for epoch in range(5):
    model.train()
    total_loss, correct, total = 0, 0, 0
    for images, labels in train_loader:
        images, labels = images.to(device), labels.to(device)
        outputs = model(pixel_values=images, labels=labels)
        loss = outputs.loss
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        total_loss += loss.item() * images.size(0)
        correct += (outputs.logits.argmax(1) == labels).sum().item()
        total += images.size(0)
    print(f"Epoch {epoch+1}: loss={total_loss/total:.4f}, acc={correct/total:.4f}")

# --- 4. テスト ---
model.eval()
correct, total = 0, 0
all_features, all_labels = [], []
with torch.no_grad():
    for images, labels in test_loader:
        images, labels = images.to(device), labels.to(device)
        outputs = model(pixel_values=images, output_hidden_states=True)
        correct += (outputs.logits.argmax(1) == labels).sum().item()
        total += images.size(0)
        # [CLS]トークン特徴を収集
        cls_features = outputs.hidden_states[-1][:, 0]  # [B, 768]
        all_features.append(cls_features.cpu())
        all_labels.append(labels.cpu())

print(f"\nTest Accuracy: {correct/total:.4f}")

# --- 5. [CLS]トークン特徴のPCA可視化 ---
features = torch.cat(all_features).numpy()
labels = torch.cat(all_labels).numpy()

pca = PCA(n_components=2)
features_2d = pca.fit_transform(features)

plt.figure(figsize=(12, 8))
scatter = plt.scatter(features_2d[:, 0], features_2d[:, 1],
                      c=labels, cmap='tab10', alpha=0.6, s=15)
plt.colorbar(scatter, ticks=range(10), label='Class')
plt.clim(-0.5, 9.5)

# クラス中心のラベル付け
for i in range(10):
    mask = labels == i
    cx, cy = features_2d[mask, 0].mean(), features_2d[mask, 1].mean()
    plt.annotate(class_names[i], (cx, cy), fontsize=10,
                 fontweight='bold', ha='center',
                 bbox=dict(boxstyle='round,pad=0.3', facecolor='white', alpha=0.8))

plt.title('ViT [CLS] Token Features — PCA Projection (CIFAR-10)', fontsize=14)
plt.xlabel(f'PC1 ({pca.explained_variance_ratio_[0]:.1%} variance)')
plt.ylabel(f'PC2 ({pca.explained_variance_ratio_[1]:.1%} variance)')
plt.tight_layout()
plt.show()

# --- 6. Position Embedding可視化 ---
pos_embed = model.vit.embeddings.position_embeddings[0].detach().cpu()  # [197, 768]
print(f"\nPosition embeddings shape: {pos_embed.shape}")

# パッチ位置エンベディング間のコサイン類似度を計算
patch_pos = pos_embed[1:]  # [CLS]を除外、[196, 768]
sim_matrix = F.cosine_similarity(
    patch_pos.unsqueeze(0), patch_pos.unsqueeze(1), dim=-1
)  # [196, 196]

# いくつかのパッチ位置を選択し、他のすべての位置との類似度を可視化
fig, axes = plt.subplots(2, 4, figsize=(16, 8))
positions = [0, 7, 48, 97, 112, 140, 175, 195]  # 異なる位置
for idx, pos in enumerate(positions):
    ax = axes[idx // 4][idx % 4]
    sim = sim_matrix[pos].reshape(14, 14).numpy()
    im = ax.imshow(sim, cmap='RdBu_r', vmin=-1, vmax=1)
    row, col = pos // 14, pos % 14
    ax.plot(col, row, 'k*', markersize=10)
    ax.set_title(f'Patch ({row},{col})', fontsize=10)
    ax.axis('off')

plt.suptitle('Position Embedding Cosine Similarity (ViT)', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()

# --- 7. クラスごとの予測信頼度分布 ---
model.eval()
all_probs, all_preds, all_true = [], [], []
with torch.no_grad():
    for images, labels in test_loader:
        images = images.to(device)
        logits = model(pixel_values=images).logits
        probs = F.softmax(logits, dim=-1)
        all_probs.append(probs.cpu())
        all_preds.append(logits.argmax(1).cpu())
        all_true.append(labels)

all_probs = torch.cat(all_probs)
all_preds = torch.cat(all_preds)
all_true = torch.cat(all_true)

fig, axes = plt.subplots(2, 5, figsize=(20, 8))
for i in range(10):
    ax = axes[i // 5][i % 5]
    mask = all_true == i
    correct_conf = all_probs[mask & (all_preds == all_true)].max(dim=1).values
    wrong_conf = all_probs[mask & (all_preds != all_true)].max(dim=1).values
    if len(correct_conf) > 0:
        ax.hist(correct_conf.numpy(), bins=20, alpha=0.7, color='#0077b6', label='Correct')
    if len(wrong_conf) > 0:
        ax.hist(wrong_conf.numpy(), bins=20, alpha=0.7, color='#e63946', label='Wrong')
    ax.set_title(class_names[i], fontsize=11)
    ax.set_xlim(0, 1)
    ax.legend(fontsize=8)

plt.suptitle('Prediction Confidence Distribution per Class', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()

print("\nLab 2 Complete!")

11. 結論:Transformerは汎用計算プリミティブへ

Transformerの影響力は「優れたモデルアーキテクチャ」の範疇をはるかに超えています。それは人工知能の汎用計算プリミティブへと進化しつつあります——CPUが従来の計算に対して、GPUがグラフィックスレンダリングに対して果たした役割と同様に、Transformerは知的計算のコア実行ユニットとなりつつあります。

本記事のコアスレッドを振り返ります:

Transformerはいつの日か超越されるかもしれませんが、それが確立した「事前学習 → 転移」のパラダイム、「Scaling Laws」の思想、そして「汎用アーキテクチャによるマルチモーダル統一」というビジョンは、今後長きにわたりAIの発展方向を形作り続けるでしょう。Transformerを理解することは、この時代のAIの基底ロジックを理解することです。