この記事の対象読者
- LLMやTransformerという言葉は知っているが、内部の仕組みがわからない方
- ニューラルネットワークの基礎知識(層、重み、活性化関数)がある方
- 数式アレルギーだが、コードなら読める方
この記事で得られること
- Self-Attentionの直感的理解: Query/Key/Valueを「図書館の検索」の比喩で理解
- 数式をコードで再現: NumPyでScaled Dot-Product Attentionを30行で実装
- 実務への接続: なぜAttentionが必要か、Multi-Head AttentionやFlash Attentionとの関係
この記事で扱わないこと
- Transformerの全アーキテクチャ(Encoder/Decoderの構成全体)
- Attentionの数学的な厳密な導出
- Flash Attention / Ring Attention等の高速化手法の実装詳細
1. Self-Attentionとの出会い
LLMを使っていて、ずっと不思議だったことがある。「なぜAIは長い文章の中から、質問に関係する部分だけを的確に拾えるのだろう?」
例えば「日本の首都はどこですか?」と聞くと、AIは数千トークンのコンテキストの中から「東京」に関連する情報だけを引っ張り出してくる。全ての単語を均等に見ているわけではなく、「どの単語に注目すべきか」を自分で判断している。
この「どこに注目するか」を決める仕組みが、Self-Attention(自己注意機構)だ。
Self-Attentionを一言で表すなら、「文章の中の全ての単語が、他の全ての単語との関連性を計算して、自分の意味を文脈に合わせて更新する仕組み」。
比喩で説明しよう。会議室を想像してほしい。10人の参加者(=10個の単語)がいる。Self-Attentionでは、全員が他の全員に「あなたの発言は、私の理解にどれくらい重要ですか?」と聞いて回る。そして、重要度の高い人の情報を重点的に取り込んで、自分の理解を更新する。
ここまでで、Self-Attentionがどんなものか、なんとなくイメージできたでしょうか。次は、この記事で使う用語を整理しておきましょう。
2. 前提知識の確認
本題に入る前に、この記事で登場する用語を確認します。
2.1 トークンとは
テキストを分割した最小単位。「東京は日本の首都です」は、トークナイザーによって ["東京", "は", "日本", "の", "首都", "です"] のように分割される。Self-Attentionはこのトークン単位で動作する。
2.2 エンベディング(Embedding)とは
各トークンを固定長の数値ベクトルに変換したもの。例えば「猫」というトークンが [0.2, -0.5, 0.8, ...] のような768次元のベクトルに変換される。意味が近い単語ほど、ベクトルも近くなるように学習される。
2.3 行列積(Matrix Multiplication)とは
2つの行列の掛け算。Self-Attentionの核心は行列積で計算される。Pythonでは numpy.dot() や @ 演算子で実行できる。
2.4 Softmax関数とは
任意の実数のリストを「確率分布」に変換する関数。全ての値が0以上になり、合計が1になる。Self-Attentionでは、各トークンへの注目度(重み)をSoftmaxで正規化する。
import numpy as np
scores = [2.0, 1.0, 0.1]
weights = np.exp(scores) / np.sum(np.exp(scores))
# → [0.659, 0.243, 0.099] 合計 = 1.0
これらの用語が押さえられたら、Self-Attentionの背景を見ていきましょう。
3. Self-Attentionが生まれた背景
3.1 RNNの限界
Self-Attention以前、テキスト処理の主流はRNN(Recurrent Neural Network)だった。RNNは単語を1つずつ順番に処理するため、2つの致命的な弱点があった。
| 弱点 | 説明 |
|---|---|
| 長距離依存の困難 | 文の先頭と末尾の関係を捉えにくい。100単語離れた主語と動詞の対応が崩れる |
| 並列計算の不可能 | 「前の単語の処理結果」が必要なので、GPUの並列計算能力を活かせない |
3.2 「Attention Is All You Need」
2017年、Googleの研究者Vaswaniらが論文「Attention Is All You Need」を発表。RNNを完全に排除し、Self-Attentionだけでテキスト処理を行うTransformerアーキテクチャを提案した。これがGPT、BERT、Claude、Geminiなど全ての現代LLMの基盤となった。
3.3 なぜSelf-Attentionが革命的だったのか
| 特性 | RNN | Self-Attention |
|---|---|---|
| 計算順序 | 逐次的(直列) | 並列 |
| 長距離依存 | 苦手(勾配消失) | 得意(直接参照) |
| 計算量(N=系列長) | O(N) | O(N²)(ただしGPU並列で高速) |
| 学習速度 | 遅い | 速い |
背景がわかったところで、基本的な仕組みを見ていきましょう。
4. 基本概念と仕組み
4.1 Query / Key / Value — 図書館の比喩
Self-Attentionを理解する鍵は、Query(検索クエリ)、Key(索引)、Value(中身)の3つの概念だ。
図書館で本を探す場面を想像してほしい。
| 概念 | 図書館の比喩 | 役割 |
|---|---|---|
| Query (Q) | 「ニューラルネットワークの入門書が欲しい」 | 「私は何を探しているか?」 |
| Key (K) | 本の背表紙のラベル・タグ | 「私はどんな情報を持っているか?」 |
| Value (V) | 本の中身のテキスト | 「私が実際に提供できる情報」 |
Self-Attentionのプロセスは、こうだ。
- 各トークンが「検索クエリ(Q)」を出す — 「私に関連する情報は何か?」
- 各トークンが「索引ラベル(K)」を出す — 「私はこういう情報を持っている」
- QとKの類似度を計算する — 「このラベルは私の探しているものに近い?」
- 類似度をSoftmaxで正規化 — 「各本への注目度(重み)」
- 重みに基づいてV(本の中身)を集約 — 「重要な本の情報を重点的に読む」
4.2 Scaled Dot-Product Attention — 数式
数式で表すとこうなる:
Attention(Q, K, V) = softmax(Q × K^T / √d_k) × V
| 記号 | 意味 |
|---|---|
| Q | Queryベクトル行列(各トークンの「何を探しているか」) |
| K | Keyベクトル行列(各トークンの「何を持っているか」) |
| V | Valueベクトル行列(各トークンの「実際の情報」) |
| K^T | Kの転置行列 |
| d_k | KeyベクトルのKの次元数 |
| √d_k | スケーリング因子(値が大きくなりすぎてSoftmaxが飽和するのを防ぐ) |
4.3 なぜQ/K/Vに分けるのか?
「エンベディングをそのまま比較すればいいのでは?」と思うかもしれない。しかし、1つのベクトルには文法的・意味的・構造的な情報が混在している。Q/K/Vに分離することで:
- Q: 「今、何が知りたいか」に特化した表現を学習
- K: 「自分はどう見つけてもらうか」に特化した表現を学習
- V: 「実際に伝える情報」に特化した表現を学習
この分離が、単純なベクトル類似度比較では捉えられない複雑な関係性を学習可能にしている。
基本概念が理解できたところで、実際にコードを書いて動かしてみましょう。
5. 実践:Self-AttentionをNumPyで実装
5.1 環境構築
pip install numpy torch
5.2 環境別の設定ファイル
開発環境用(config.yaml)
# config.yaml - 学習・実験用
model:
d_model: 64 # エンベディング次元(小さくして高速に)
n_heads: 4
d_k: 16 # d_model / n_heads
seq_length: 32
device: "cpu"
debug: true
log_attention_weights: true # Attention重みを可視化
本番環境用(config.production.yaml)
# config.production.yaml - 推論用
model:
d_model: 768
n_heads: 12
d_k: 64
seq_length: 512
device: "cuda"
debug: false
log_attention_weights: false
torch_dtype: "float16"
テスト環境用(config.test.yaml)
# config.test.yaml - ユニットテスト用
model:
d_model: 8
n_heads: 2
d_k: 4
seq_length: 4
device: "cpu"
debug: false
log_attention_weights: false
5.3 NumPyによるSelf-Attention実装
"""
Scaled Dot-Product Self-Attention のNumPy実装
実行方法: python self_attention_numpy.py
前提条件: pip install numpy
"""
import numpy as np
np.random.seed(42)
def softmax(x: np.ndarray) -> np.ndarray:
"""数値安定性を考慮したSoftmax"""
exp_x = np.exp(x - np.max(x, axis=-1, keepdims=True))
return exp_x / np.sum(exp_x, axis=-1, keepdims=True)
def scaled_dot_product_attention(
Q: np.ndarray, K: np.ndarray, V: np.ndarray
) -> tuple[np.ndarray, np.ndarray]:
"""
Scaled Dot-Product Attention
Args:
Q: Query行列 (seq_len, d_k)
K: Key行列 (seq_len, d_k)
V: Value行列 (seq_len, d_v)
Returns:
output: Attention適用後の出力 (seq_len, d_v)
weights: Attention重み (seq_len, seq_len)
"""
d_k = K.shape[-1]
# Step 1: QとKの内積(類似度スコア計算)
scores = Q @ K.T # (seq_len, seq_len)
# Step 2: スケーリング(√d_k で割る)
scores = scores / np.sqrt(d_k)
# Step 3: Softmaxで正規化(確率分布に変換)
weights = softmax(scores) # (seq_len, seq_len)
# Step 4: 重みでValueを集約
output = weights @ V # (seq_len, d_v)
return output, weights
def demo_self_attention():
"""Self-Attentionのデモ"""
# === 設定 ===
seq_len = 4 # トークン数(「猫が魚を食べた」→ 4トークン)
d_model = 8 # エンベディング次元
d_k = 4 # Query/Key次元
d_v = 4 # Value次元
# === Step 0: 入力エンベディング(ランダムで代用) ===
tokens = ["猫が", "魚を", "食べ", "た"]
X = np.random.randn(seq_len, d_model)
print(f"入力: {tokens}")
print(f"入力shape: {X.shape} (seq_len={seq_len}, d_model={d_model})")
# === Step 1: 線形変換(重み行列でQ, K, Vを生成) ===
W_Q = np.random.randn(d_model, d_k) # Query用の重み
W_K = np.random.randn(d_model, d_k) # Key用の重み
W_V = np.random.randn(d_model, d_v) # Value用の重み
Q = X @ W_Q # (seq_len, d_k)
K = X @ W_K # (seq_len, d_k)
V = X @ W_V # (seq_len, d_v)
print(f"\nQ shape: {Q.shape} (各トークンの「何を探している?」)")
print(f"K shape: {K.shape} (各トークンの「何を持っている?」)")
print(f"V shape: {V.shape} (各トークンの「提供する情報」)")
# === Step 2: Attention計算 ===
output, weights = scaled_dot_product_attention(Q, K, V)
print(f"\n--- Attention重み ---")
print("(各トークンが他のトークンにどれだけ注目しているか)")
print(f"{'':>6}", end="")
for t in tokens:
print(f"{t:>8}", end="")
print()
for i, t in enumerate(tokens):
print(f"{t:>6}", end="")
for j in range(seq_len):
print(f"{weights[i][j]:>8.3f}", end="")
print()
print(f"\n出力shape: {output.shape} (文脈を反映した新しいエンベディング)")
print("\n💡 各行の合計 = 1.0(Softmaxで正規化済み)")
for i, t in enumerate(tokens):
print(f" {t}: {np.sum(weights[i]):.4f}")
if __name__ == "__main__":
print("=" * 60)
print("Scaled Dot-Product Self-Attention(NumPy実装)")
print("=" * 60)
demo_self_attention()
5.4 PyTorchによるMulti-Head Attention実装
"""
Multi-Head Self-Attention のPyTorch実装
実行方法: python multi_head_attention.py
前提条件: pip install torch
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
class MultiHeadSelfAttention(nn.Module):
"""Multi-Head Self-Attention"""
def __init__(self, d_model: int, n_heads: int):
super().__init__()
assert d_model % n_heads == 0, "d_modelはn_headsで割り切れる必要あり"
self.d_model = d_model
self.n_heads = n_heads
self.d_k = d_model // n_heads
# Q, K, V の線形変換
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, x: torch.Tensor) -> torch.Tensor:
"""
Args:
x: (batch_size, seq_len, d_model)
Returns:
output: (batch_size, seq_len, d_model)
"""
batch_size, seq_len, _ = x.shape
# Q, K, V を計算
Q = self.W_Q(x) # (batch, seq, d_model)
K = self.W_K(x)
V = self.W_V(x)
# Multi-Head: (batch, seq, d_model) → (batch, n_heads, seq, d_k)
Q = Q.view(batch_size, seq_len, self.n_heads, self.d_k).transpose(1, 2)
K = K.view(batch_size, seq_len, self.n_heads, self.d_k).transpose(1, 2)
V = V.view(batch_size, seq_len, self.n_heads, self.d_k).transpose(1, 2)
# Scaled Dot-Product Attention
scores = torch.matmul(Q, K.transpose(-2, -1)) / (self.d_k ** 0.5)
weights = F.softmax(scores, dim=-1)
context = torch.matmul(weights, V)
# ヘッド結合: (batch, n_heads, seq, d_k) → (batch, seq, d_model)
context = context.transpose(1, 2).contiguous().view(batch_size, seq_len, self.d_model)
# 出力線形変換
output = self.W_O(context)
return output
def demo():
# パラメータ
batch_size = 1
seq_len = 6
d_model = 64
n_heads = 4
# ダミー入力
tokens = ["The", "cat", "sat", "on", "the", "mat"]
x = torch.randn(batch_size, seq_len, d_model)
# Multi-Head Attention
mha = MultiHeadSelfAttention(d_model, n_heads)
output = mha(x)
print(f"入力: {tokens}")
print(f"入力shape: {x.shape} (batch={batch_size}, seq={seq_len}, d={d_model})")
print(f"出力shape: {output.shape}")
print(f"ヘッド数: {n_heads}")
print(f"ヘッド次元: {d_model // n_heads}")
print(f"パラメータ数: {sum(p.numel() for p in mha.parameters()):,}")
if __name__ == "__main__":
print("=" * 50)
print("Multi-Head Self-Attention(PyTorch実装)")
print("=" * 50)
demo()
5.5 実行結果
$ python self_attention_numpy.py
============================================================
Scaled Dot-Product Self-Attention(NumPy実装)
============================================================
入力: ['猫が', '魚を', '食べ', 'た']
入力shape: (4, 8) (seq_len=4, d_model=8)
Q shape: (4, 4) (各トークンの「何を探している?」)
K shape: (4, 4) (各トークンの「何を持っている?」)
V shape: (4, 4) (各トークンの「提供する情報」)
--- Attention重み ---
(各トークンが他のトークンにどれだけ注目しているか)
猫が 魚を 食べ た
猫が 0.427 0.148 0.210 0.215
魚を 0.113 0.555 0.087 0.245
食べ 0.302 0.074 0.416 0.208
た 0.198 0.307 0.192 0.303
出力shape: (4, 4) (文脈を反映した新しいエンベディング)
💡 各行の合計 = 1.0(Softmaxで正規化済み)
猫が: 1.0000
魚を: 1.0000
食べ: 1.0000
た: 1.0000
$ python multi_head_attention.py
==================================================
Multi-Head Self-Attention(PyTorch実装)
==================================================
入力: ['The', 'cat', 'sat', 'on', 'the', 'mat']
入力shape: torch.Size([1, 6, 64]) (batch=1, seq=6, d=64)
出力shape: torch.Size([1, 6, 64])
ヘッド数: 4
ヘッド次元: 16
パラメータ数: 16,640
5.6 よくあるエラーと対処法
| エラー/症状 | 原因 | 対処法 |
|---|---|---|
| Attention重みが均一(全部0.25) | スケーリング忘れ or 学習不足 |
/ sqrt(d_k) を確認 |
RuntimeError: size mismatch |
Q, K, Vの次元不一致 |
d_model % n_heads == 0 を確認 |
| Softmax出力がNaN | スコアが大きすぎ | 数値安定Softmax(x - max(x))を使用 |
| VRAM不足(長文入力時) | O(N²)のメモリ | Flash Attention or シーケンス分割 |
| 推論が遅い | Attentionの二次計算量 |
torch.nn.functional.scaled_dot_product_attention を使用 |
| 出力が入力と同じ | 重み行列が単位行列に近い |
nn.init.xavier_uniform_ で初期化 |
5.7 環境診断スクリプト
#!/usr/bin/env python3
"""Self-Attention学習環境の診断 — 実行: python check_attention.py"""
import sys
def check():
print(f" Python: {sys.version.split()[0]}")
try:
import numpy as np
print(f" ✅ numpy: {np.__version__}")
except ImportError:
print(" ❌ numpy 未インストール")
try:
import torch
print(f" ✅ torch: {torch.__version__}")
if torch.cuda.is_available():
name = torch.cuda.get_device_name(0)
vram = torch.cuda.get_device_properties(0).total_mem / 1024**3
print(f" ✅ GPU: {name} ({vram:.0f}GB)")
# Flash Attentionサポート確認
if hasattr(torch.nn.functional, "scaled_dot_product_attention"):
print(" ✅ Flash Attention: 利用可能")
else:
print(" ⚠️ GPU: 利用不可(CPU推論のみ)")
except ImportError:
print(" ❌ torch 未インストール")
# 最大推奨シーケンス長の推定
try:
import torch
if torch.cuda.is_available():
vram_gb = torch.cuda.get_device_properties(0).total_mem / 1024**3
# 概算: Attention行列 = seq^2 × 2bytes(fp16) × n_heads
max_seq = int((vram_gb * 1024**3 / (12 * 2 * 4)) ** 0.5)
print(f" 📏 概算最大系列長: ~{max_seq:,} tokens (12-head, fp16)")
except:
pass
print("\n🎉 診断完了")
if __name__ == "__main__":
print("=" * 40)
print("Self-Attention 環境診断")
print("=" * 40)
check()
実装方法がわかったので、次は具体的なユースケースを見ていきます。
6. ユースケース別ガイド
6.1 ユースケース1: Attention重みの可視化
想定読者: Self-Attentionが「何に注目しているか」を可視化して理解を深めたい学習者
サンプルコード:
import numpy as np
def visualize_attention_ascii(tokens: list[str], weights: np.ndarray):
"""Attention重みをASCIIアートで可視化"""
print(f"\n{'':>8}", end="")
for t in tokens:
print(f"{t:>8}", end="")
print()
print(" " + "-" * (8 * len(tokens) + 8))
for i, t in enumerate(tokens):
print(f"{t:>8}|", end="")
for j in range(len(tokens)):
w = weights[i][j]
bar = "█" * int(w * 10)
print(f" {bar:<7}", end="")
print(f" ({weights[i].max():.2f})")
# 使用例
tokens = ["I", "love", "this", "movie"]
weights = np.array([
[0.1, 0.3, 0.2, 0.4],
[0.05, 0.6, 0.15, 0.2],
[0.1, 0.2, 0.5, 0.2],
[0.15, 0.35, 0.15, 0.35],
])
visualize_attention_ascii(tokens, weights)
6.2 ユースケース2: BERTのAttention重みを抽出
想定読者: 実際のTransformerモデルのAttention動作を確認したい開発者
サンプルコード:
from transformers import BertTokenizer, BertModel
import torch
tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
model = BertModel.from_pretrained("bert-base-uncased", output_attentions=True)
text = "The cat sat on the mat"
inputs = tokenizer(text, return_tensors="pt")
with torch.no_grad():
outputs = model(**inputs)
# Attention重み: (n_layers, batch, n_heads, seq, seq)
attentions = outputs.attentions
print(f"レイヤー数: {len(attentions)}")
print(f"各レイヤーのshape: {attentions[0].shape}")
# → torch.Size([1, 12, 8, 8]) # batch=1, heads=12, seq=8
# 最終層の最初のヘッドのAttention重みを表示
tokens_decoded = tokenizer.convert_ids_to_tokens(inputs["input_ids"][0])
last_layer = attentions[-1][0, 0].numpy() # head 0
print(f"\nトークン: {tokens_decoded}")
print(f"'sat'が注目するトークン:")
sat_idx = tokens_decoded.index("sat")
for i, t in enumerate(tokens_decoded):
print(f" {t}: {last_layer[sat_idx][i]:.3f}")
6.3 ユースケース3: カスタムAttentionレイヤーの組み込み
想定読者: 独自モデルにSelf-Attentionを組み込みたいMLエンジニア
サンプルコード:
import torch
import torch.nn as nn
class SimpleTransformerBlock(nn.Module):
"""Self-Attention + Feed-Forward の基本ブロック"""
def __init__(self, d_model: int, n_heads: int, d_ff: int, dropout: float = 0.1):
super().__init__()
self.attention = nn.MultiheadAttention(d_model, n_heads, batch_first=True)
self.ffn = nn.Sequential(
nn.Linear(d_model, d_ff),
nn.GELU(),
nn.Linear(d_ff, d_model),
)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x: torch.Tensor) -> torch.Tensor:
# Self-Attention + Residual + LayerNorm
attn_out, _ = self.attention(x, x, x)
x = self.norm1(x + self.dropout(attn_out))
# Feed-Forward + Residual + LayerNorm
ff_out = self.ffn(x)
x = self.norm2(x + self.dropout(ff_out))
return x
# 使用例
block = SimpleTransformerBlock(d_model=128, n_heads=4, d_ff=512)
x = torch.randn(2, 10, 128) # (batch=2, seq=10, d=128)
out = block(x)
print(f"入力: {x.shape} → 出力: {out.shape}")
# → 入力: torch.Size([2, 10, 128]) → 出力: torch.Size([2, 10, 128])
ユースケースを把握できたところで、この先の学習パスを確認しましょう。
7. 学習ロードマップ
初級者向け(まずはここから)
- NumPyでScaled Dot-Product Attentionを手で実装して動かす
- Illustrated Transformer で図解を見て理解を固める
- Andrej Karpathy: Let's build GPT で動画学習
中級者向け(実践に進む)
-
PyTorchの
nn.MultiheadAttentionを自分のモデルに組み込む - Hugging Face Transformersのソースコードを読む(
modeling_bert.py) - Causal Masking(因果マスク)の仕組みを理解・実装
上級者向け(さらに深く)
- Flash Attention の論文と実装を読む(Dao et al., 2022)
- KV Cache、Grouped Query Attention(GQA)を理解
- Ring Attention、Sliding Window Attentionで長文処理を最適化
8. まとめ
この記事では、Self-Attentionについて以下を解説しました:
- 図書館の比喩でQ/K/V理解 — Query(検索)、Key(索引)、Value(中身)の3つ組
- NumPy 30行で実装 — Scaled Dot-Product Attentionの全プロセスをコードで再現
- Multi-Head Attentionまで — 複数のヘッドで異なる関係性を同時に捉える仕組み
私の所感
Self-Attentionは、初めて数式を見ると難しく感じるが、「全部の単語が他の全部の単語との関連性を計算して、重要な情報を集約する」という本質はシンプルだ。
個人的に感動したのは、学習されたAttention重みを可視化した時だ。「sat」という動詞が「cat」(主語)に強く注目し、「mat」(場所)にもそこそこ注目している様子が、人間の読み方とほぼ同じだった。数式と行列積の積み重ねが、言語の構造を自然に学んでしまう。これがTransformerの美しさだと思う。
参考文献
- Vaswani et al., "Attention Is All You Need" (2017)
- Jay Alammar, "The Illustrated Transformer"
- Dive into Deep Learning: Queries, Keys, and Values
- Towards Data Science: What are Q, K, V in the Transformer Architecture
この記事が参考になったら、いいね・ストックをお願いします!
Xでも技術情報を発信しています → https://x.com/geneLab_999