注意機構(Attention)
Self-Attention と Transformer の基礎
注意機構(Attention Mechanism)は、入力系列の中で「どこに注目すべきか」を 動的に学習する仕組みである。これにより、RNNの長距離依存問題を解決し、 Transformerアーキテクチャの基盤となった。
注意機構の誕生
Seq2Seq モデルの課題
機械翻訳などのSequence-to-Sequence(Seq2Seq)タスクでは、エンコーダが入力系列を 固定長のコンテキストベクトルに圧縮していた。これには問題があった。
Attention の解決策
Attention機構(Bahdanau et al., 2014)は、デコーダが各ステップで エンコーダのすべての隠れ状態を参照できるようにした。
Attention の計算
デコーダの状態 \(s_t\) とエンコーダの隠れ状態 \(h_1, ..., h_n\) に対して:
- スコア計算:\(e_{ti} = \text{score}(s_t, h_i)\)
- 正規化(Softmax):\(\alpha_{ti} = \dfrac{\exp(e_{ti})}{\displaystyle\sum_j \exp(e_{tj})}\)
- コンテキストベクトル:\(c_t = \displaystyle\sum_i \alpha_{ti} h_i\)
スコア関数の種類
1. Dot Product(内積)
\[ \text{score}(s, h) = s^\top h \]2. General(一般化内積)
\[ \text{score}(s, h) = s^\top W h \]3. Additive(加法的、Bahdanau)
\[ \text{score}(s, h) = v^\top \tanh(W_s s + W_h h) \]ここで \(s\) はデコーダ側の状態(「いま何を探しているか」=クエリ)、\(h\) はエンコーダの各隠れ状態(「各位置に何があるか」=キー)である。\(W,\ W_s,\ W_h\) は学習で獲得する重み行列で、\(s\) と \(h\) を比較しやすい空間へ線形変換する役割を持つ。\(v\) は学習で獲得する重みベクトルで、\(\tanh\) が出すベクトルを 1 つのスコア(スカラー値)に変換する。いずれも訓練データから勾配降下で最適化されるパラメータである。
- Dot Product:学習パラメータを持たず高速。ただし \(s\) と \(h\) が同じ次元・同じ空間にあることが前提。
- General:あいだに \(W\) を挟むので、異なる空間の \(s\) と \(h\) でも比較でき柔軟。
- Additive:小さなニューラルネット(\(W_s, W_h, v\))でスコアを計算するため表現力が高い。
Self-Attention
Self-Attention(自己注意)は、入力系列が自分自身に対して Attentionを計算する仕組みである。系列内の任意の位置間の関係を直接モデル化できる。
入力 \(X\) から3つの表現を作成:
- Query (Q):「何を探しているか」 → \(Q = XW^Q\)
- Key (K):「何があるか」 → \(K = XW^K\)
- Value (V):「実際の情報」 → \(V = XW^V\)
\(\sqrt{d_k}\) で割ってスケーリングし、内積が大きくなりすぎて Softmax が飽和する(勾配が消える)のを防ぐ。
なぜスケーリングが必要か
\(d_k\) が大きい場合、内積 \(q \cdot k\) の値が大きくなりやすく、 Softmaxの出力が極端な値(ほぼ0か1)になる。 これにより勾配が消失するため、\(\sqrt{d_k}\) で割ってスケーリングする。
Multi-Head Attention
単一のAttentionでは表現力が限られるため、複数の「ヘッド」で 異なる観点からAttentionを計算し、結果を結合する。
ヘッドはどう分割する?
- 入力(各トークン \(d_{model}\) 次元)を重み行列 \(W^Q, W^K, W^V\) で線形変換し、全体の Q, K, V(\(d_{model}\) 次元)を作る。
- その \(d_{model}\) 次元ベクトルを \(h\) 個に等分し、各ヘッドが \(d_k = d_{model}/h\) 次元の小さな Q, K, V を担当する(実装上は reshape:\((\text{系列長},\, d_{model}) \to (\text{系列長},\, h,\, d_k)\))。別々の重みを \(h\) 組用意するのではなく、1 つの大きな射影を \(h\) 分割しているだけで、数式の \(W_i^Q\) は \(W^Q\) のうち各ヘッドが担当する列に対応する。
- 各ヘッドで独立に Scaled Dot-Product Attention を計算し、得られた出力(各 \(d_k\) 次元)を連結して \(d_{model}\) 次元へ戻し、最後に \(W^O\) で混ぜ合わせる。
ヘッド数 \(h\) の決め方:\(h\) は人が決めるハイパーパラメータで、\(d_k = d_{model}/h\) が整数になるよう \(d_{model}\) の約数から選ぶ。ヘッドを増やしても各ヘッドが細くなるぶん計算量はほぼ一定だが、増やしすぎると 1 ヘッドあたりの次元が小さくなり表現力が落ちる。経験的に 8〜16 が定番である。
素朴な「連番での等分」で十分な理由
分割そのものは、射影後のベクトルを連続した要素番号で区切るだけ(\(0\sim d_k-1\) を第1ヘッド、\(d_k\sim 2d_k-1\) を第2ヘッド…)の素朴なやり方で十分である。理由は、分割の直前に学習可能な射影 \(W^Q\)(\(d_{model}\times d_{model}\) の全結合)があるからである。\(W^Q\) の各出力次元は入力全体の線形結合なので、「どの特徴をどのヘッドの担当範囲に送るか」は \(W^Q\) が学習で自由に決められる。固定的なスライス位置や並べ替えは \(W^Q\) に吸収でき、表現力は変わらない。
したがって、ヘッドごとの個性(一方が構文、他方が意味…)は分け方ではなく訓練から生まれる。ランダム初期化+勾配降下で対称性が破れ、各ヘッドの担当列が異なる部分空間へ収束する。「素朴な等分+学習する射影」は「\(h\) 個の任意の射影を独立に学習する」ことと数学的に等価で、これで十分なのである。
典型的な設定
- ヘッド数 h:8〜16
- 各ヘッドの次元:\(d_k = d_v = d_{model} / h\)
- 例:\(d_{model} = 512, h = 8 \Rightarrow d_k = 64\)
位置エンコーディング
Self-Attentionは順序を考慮しない(並べ替え不変)。 そのため、位置情報を明示的に追加する必要がある。
\(pos\):位置、\(i\):次元のインデックス
なぜ sin・cos を使うのか
- 値が有界(\([-1, 1]\)):位置番号 \(pos\) をそのまま足すと値が際限なく大きくなり単語ベクトルを壊すが、sin/cos なら系列のどこでも一定の大きさに収まる。
- 各位置が一意:次元ごとに波長を変えた sin/cos を並べるので、各位置が固有の「波形の組み合わせ」を持ち互いに区別できる(時計の秒針・分針・時針で時刻が一意に決まるのと同じ発想)。
- 相対位置を表しやすい:三角関数の加法定理により、\(k\) だけ離れた位置の \(PE_{pos+k}\) は \(PE_{pos}\) の線形変換(回転)で書ける(下の導出を参照)。そのためモデルは「\(k\) 個離れている」という相対関係を学習しやすい。
- 未知の長さにも外挿できる:学習時より長い系列でも同じ式で位置を計算できる(位置ごとに値を丸暗記する方式では未知の位置に対応できない)。
1 つの周波数 \(\omega = 1/10000^{2i/d_{model}}\) について、位置 \(pos\) のエンコーディングは \((\sin\omega pos,\ \cos\omega pos)\) というペアである。位置を \(k\) だけずらすと、三角関数の加法定理より
\[ \begin{aligned} \sin\omega(pos+k) &= \sin\omega pos\,\cos\omega k + \cos\omega pos\,\sin\omega k,\\ \cos\omega(pos+k) &= \cos\omega pos\,\cos\omega k - \sin\omega pos\,\sin\omega k. \end{aligned} \]行列でまとめると、回転行列 \(R(\omega k)\) を用いて次のように書ける。
\[ \begin{pmatrix}\sin\omega(pos+k)\\ \cos\omega(pos+k)\end{pmatrix} = \underbrace{\begin{pmatrix}\cos\omega k & \sin\omega k\\ -\sin\omega k & \cos\omega k\end{pmatrix}}_{R(\omega k)} \begin{pmatrix}\sin\omega pos\\ \cos\omega pos\end{pmatrix} \]この \(R(\omega k)\) はずれ \(k\) だけで決まり、\(pos\) には依存しない。つまり「\(k\) だけ離れる」という操作は、いまどの位置にいるかによらず常に同じ角度 \(\omega k\) の回転になる。だからモデルは固定の線形変換ひとつで「\(k\) 離れている」という相対関係を捉えられる。
イメージ:時計の針。いまが何時であっても「\(k\) 時間進む」は針を一定角度だけ回す操作で表せる——これと同じことが各周波数のペアで起きている。
\(\omega k\) が \(2\pi\) を超えると混同しない?
1 つの波長だけを見れば確かに混同する。\(\sin,\cos\) は引数について周期 \(2\pi\) だが、ここでは引数が \(\omega\cdot pos\) なので、位置 \(pos\) に対する波長(周期)は \(2\pi/\omega\) になる(\(\sin\omega(pos+2\pi/\omega)=\sin(\omega\,pos+2\pi)=\sin\omega\,pos\))。つまり波長ぶん(\(2\pi/\omega\))だけ離れた 2 つの位置は同じ値になり区別できない(エイリアシング)。
これを防ぐのが「波長の異なる波を多数同時に使う」という設計である。波長は \(2\pi\cdot 10000^{2i/d_{model}}\) で、\(i\) が 1 増えるごとに一定の倍率を掛けて伸びる(等比数列的)。具体的には \(i=0\) の波長 \(\approx 2\pi\)(細かく速く振動)から、\(i\) 最大の波長 \(\approx 2\pi\cdot 10000 \approx 6.3\times10^4\)(ゆっくり振動)まで広がる。最も長い波長は約 6 万位置ぶんもあり、実際の系列長(ふつう数千以下)では一周しない。役割分担を整理すると——長い波長は一周しないので遠く離れた位置の混同(エイリアシング)を防ぎ、短い波長は細かく振動するので隣り合う位置(\(pos\) と \(pos+1\))をはっきり区別する。短い波長=局所の解像度、長い波長=広域での一意性であり、全波長を合わせれば全位置が一意に決まる。
イメージ:秒・分・時の針(桁の違う歯車のオドメーター)。速い針(短波長)は隣り合う瞬間の細かい違いを、遅い針(長波長)は大まかな時刻を表す。両方を合わせて読めば広い範囲のどの時刻でも一意に分かる——位置エンコーディングも全波長の位相の組で各位置を一意に表している。
位置エンコーディングの特性
- 任意の固定オフセット \(k\) に対して、\(PE_{pos+k}\) は \(PE_{pos}\) の線形関数で表現可能
- これにより相対位置の情報を学習しやすい
- 学習済みモデルより長い系列にも対応可能(外挿可能)
PyTorchでの実装
Scaled Dot-Product Attention
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
def scaled_dot_product_attention(query, key, value, mask=None):
"""
Scaled Dot-Product Attention
Args:
query: (batch, heads, seq_len, d_k)
key: (batch, heads, seq_len, d_k)
value: (batch, heads, seq_len, d_v)
mask: (batch, 1, 1, seq_len) or (batch, 1, seq_len, seq_len)
Returns:
output: (batch, heads, seq_len, d_v)
attention_weights: (batch, heads, seq_len, seq_len)
"""
d_k = query.size(-1)
# QK^T / sqrt(d_k)
scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
# マスク適用(オプション)
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))
# Softmax
attention_weights = F.softmax(scores, dim=-1)
# Attention × V
output = torch.matmul(attention_weights, value)
return output, attention_weights
Multi-Head Attention
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, num_heads):
super(MultiHeadAttention, self).__init__()
assert d_model % num_heads == 0
self.d_model = d_model
self.num_heads = num_heads
self.d_k = d_model // num_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 split_heads(self, x, batch_size):
"""(batch, seq_len, d_model) -> (batch, heads, seq_len, d_k)"""
x = x.view(batch_size, -1, self.num_heads, self.d_k)
return x.transpose(1, 2)
def forward(self, query, key, value, mask=None):
batch_size = query.size(0)
# 線形変換
Q = self.W_q(query)
K = self.W_k(key)
V = self.W_v(value)
# ヘッドに分割
Q = self.split_heads(Q, batch_size)
K = self.split_heads(K, batch_size)
V = self.split_heads(V, batch_size)
# Scaled Dot-Product Attention
attn_output, attn_weights = scaled_dot_product_attention(Q, K, V, mask)
# ヘッドを結合
attn_output = attn_output.transpose(1, 2).contiguous()
attn_output = attn_output.view(batch_size, -1, self.d_model)
# 最終線形変換
output = self.W_o(attn_output)
return output, attn_weights
# 使用例
d_model = 512
num_heads = 8
seq_len = 100
batch_size = 32
mha = MultiHeadAttention(d_model, num_heads)
x = torch.randn(batch_size, seq_len, d_model)
output, attention = mha(x, x, x) # Self-Attention
print(f"Output shape: {output.shape}") # (32, 100, 512)
print(f"Attention shape: {attention.shape}") # (32, 8, 100, 100)
位置エンコーディング
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=5000, dropout=0.1):
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(p=dropout)
# 位置エンコーディングを事前計算
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2).float() *
(-math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term) # 偶数次元
pe[:, 1::2] = torch.cos(position * div_term) # 奇数次元
pe = pe.unsqueeze(0) # (1, max_len, d_model)
self.register_buffer('pe', pe)
def forward(self, x):
"""
Args:
x: (batch, seq_len, d_model)
"""
x = x + self.pe[:, :x.size(1), :]
return self.dropout(x)
# 使用例
pe = PositionalEncoding(d_model=512)
x = torch.randn(32, 100, 512)
output = pe(x)
print(f"Output shape: {output.shape}") # (32, 100, 512)
Attentionの可視化
Attention重みを可視化することで、モデルが「どこに注目しているか」を理解できる。
まとめ
- Attentionは「どこに注目するか」を動的に学習する機構
- Self-Attentionは系列内の任意の位置間の関係を直接モデル化
- Query, Key, Valueの3つの表現を使用
- Scaled Dot-Productでスコアを計算し、Softmaxで正規化
- Multi-Headで複数の観点からAttentionを計算
- 位置エンコーディングで順序情報を追加
- これらがTransformerアーキテクチャの基盤となる
よくある質問(FAQ)
Q1. アテンション機構とは何か
入力の各位置に対してどれほど注目するかを重み付けして集約する機構である。Query・Key・Valueの三要素からなり、QとKの類似度でValueを重み付け和する(\(\text{Attention}(Q,K,V)=\text{softmax}(QK^\top/\sqrt{d_k})\,V\))。Seq2Seqモデルの長距離依存問題を解決した。
Q2. Self-Attentionとは何か
Q・K・Vがすべて同一系列から生成されるアテンションである。各位置が系列内の全位置との関連性を計算でき、長距離依存を定数時間で捉えられる。Transformerの中核要素であり、文章内の単語間の意味的関係を動的に学習する。
Q3. Multi-Head Attentionはなぜ有効か
異なる表現部分空間で並列にアテンションを計算し、連結して投影することで多様な依存関係を同時に捉える。例えば一方のヘッドが構文的関係、別のヘッドが意味的関係を学習するという分業が生まれる。