Attention機構を1から理解する — Transformerの心臓部を絵解きで
ChatGPTの中核technology「自己注意機構」を、前提知識ゼロから比喩→直感→行列計算→numpyコードの順で解説。数式アレルギーでも読める入門記事。
なぜ「注意」が必要なのか
「銀行の口座を開いた」と「川の土手に座った」— 英語ではどちらも bank です。この単語の意味を決めているのは、単語そのものではなく周りの文脈です。
昔のニューラルネット(RNN)は文を左から右へ1単語ずつ読み、記憶を1本のベクトルに詰め込んでいました。文が長くなると最初のほうの情報が薄れてしまう、いわば「伝言ゲーム」方式です。
この方式の弱点は2つあります。1つは距離の問題です。離れた2単語を結びつけるには、その間にある単語の数だけ処理を通り抜けなければならず、間が長いほど関係が薄まります。もう1つは速度の問題です。1単語目の計算が終わらないと2単語目に進めないので、文の中の単語を同時に処理できません。GPUは同じ形の計算をまとめて一気に片づけるのが得意なのに、その得意を活かせない構造だったわけです。
Attention(注意機構)はこれを根本から変えました。すべての単語が、文中のすべての単語を同時に見渡して、「自分の意味を決めるのにどの単語が重要か」を重み付けする仕組みです。2017年の論文 "Attention Is All You Need" は、この仕組みだけで言語モデルが作れることを示し、Transformerと名付けました。ChatGPTのT(Transformer)はここから来ています。
比喩: 会議室での情報収集
自己注意を会議に例えてみます。あなた(ある単語)は会議室で発言をまとめる係です。
- あなたは「こういう情報が欲しい」という質問(Query)を持っている
- 参加者全員が「私はこういう情報を持っています」という名札(Key) を掲げている
- あなたは自分の質問と各人の名札を見比べ、関連が深い人の話を長く聞き、関連が薄い人の話は聞き流す
- 最終的なメモは、各人の発言内容(Value)を「どれだけ聞いたか」の割合で混ぜたもの
これがQuery・Key・Valueの3役です。全単語が同時にこの「質問して、照合して、混ぜる」を行うのが自己注意(Self-Attention)です。
ここで大事なのは、3役が同じ単語から作られた別々の見え方だという点です。同じ単語が、質問する側では「何を探しているか」、照合される側では「何を持っているか」、混ぜられる側では「何を渡すか」を表します。役割を3つに分けているので、「AがBを気にする強さ」と「BがAを気にする強さ」は別々の値になります。会議でも、聞きたいことと言えることは人によってずれますが、それと同じです。
直感: 重み付き平均にすぎない
数式で身構える前に、結論を言うと自己注意の出力はValueベクトルの重み付き平均です。
- 重みは「QueryとKeyがどれだけ似ているか」で決まる
- 似ている度合いは内積(ベクトル同士の掛け算の和)で測る
- 重みは合計1になるよう softmax で正規化する
「似ているベクトル同士は内積が大きい」— これだけ覚えれば本質は掴めています。
内積が大きいというのは、2本のベクトルが同じ向きを指しているということです。単語をどうやって「向きを持った数値の並び」に変えるのかは埋め込み(Embedding)を1から理解するで扱っていますが、ここで必要な前提は一行だけです。意味が近い単語は近い向きになるように学習されている。注意機構は、その向きの近さを「どれだけ聞くか」の割合に変換する装置だと思ってください。
仕組み: 行列で一気に計算する
単語はまず埋め込みベクトル(例: 512次元の数値の並び)に変換されます。文が6単語なら、入力は 6×512 の行列 X です。
自己注意は3つの学習可能な重み行列 W_q, W_k, W_v を使って、Xから3つの行列を作ります。
Q = X @ W_q # (6, 64) 各行が各単語の「質問」
K = X @ W_k # (6, 64) 各行が各単語の「名札」
V = X @ W_v # (6, 64) 各行が各単語の「発言内容」
次に「全Queryと全Keyの内積」を1回の行列積で計算します。
scores = Q @ K.T # (6, 6) scores[i][j] = 単語iが単語jをどれだけ気にするか
この 6×6 の表が注意の設計図です。i行目を見れば「単語iがどの単語を見ているか」が分かります。
読み方には少しコツがあります。行が「見る側」、列が「見られる側」です。3行目で2列目の値が大きければ、「3番目の単語は2番目の単語を強く参照している」という意味になります。そして正規化は行ごとにかけるので、各行の合計は必ず1になる一方、列の合計は1になりません。ある単語が多くの単語から参照されることもあれば、誰からも見られないこともあるからです。表が正方形でも、対称ではないわけです。
ここまでを1本の式にまとめたものが、論文に登場する Scaled Dot-Product Attention です。
見た目は威圧的ですが、中身はいま説明した3ステップそのものです。 が「質問×名札」の内積表、( はKeyの次元数、例では64)で割るのは内積が次元数に比例して大きくなり softmax が飽和するのを防ぐため、最後の 倍が「発言内容の重み付き平均」です。
つまりこの式は、「全員の質問と全員の名札を突き合わせて点数表を作り、点数を割り算でならしてから割合に変え、その割合で全員の発言を混ぜる」と言っているだけです。記号が4つ並んでいても、やっていることは会議のメモ取りと変わりません。
コードで書く自己注意(numpy擬似コード)
全体はこれだけです。
import numpy as np
def self_attention(X, W_q, W_k, W_v):
Q = X @ W_q
K = X @ W_k
V = X @ W_v
d_k = K.shape[-1] # Keyの次元数(例: 64)
scores = Q @ K.T / np.sqrt(d_k) # スケール付き内積
weights = softmax(scores, axis=-1) # 各行の合計を1に
return weights @ V # Valueの重み付き平均
def softmax(x, axis=-1):
e = np.exp(x - x.max(axis=axis, keepdims=True))
return e / e.sum(axis=axis, keepdims=True)
np.sqrt(d_k) で割るのがポイントです。次元数が大きいと内積の値が大きくなりすぎ、softmaxが「ほぼ1個の単語だけを見る」極端な分布になって学習が進みにくくなります。これを防ぐためのスケーリングで、論文ではこの一式を Scaled Dot-Product Attention と呼びます。
もう1つ、コードにあって数式にない処理があります。softmax の中の x.max(...) を引く操作です。指数関数は入力が少し大きくなるだけで値が跳ね上がるので、そのまま np.exp に渡すと桁があふれて inf や nan になります。最大値を引いてから指数を取れば、分子と分母に同じ定数がかかるだけなので結果は変わりません。数式には現れないのに実装には必ず要る、この種の安定化はTransformerの周辺に何度も出てきます。
マルチヘッド: 複数の視点を持つ
1組のQKVでは「1種類の関係」しか捉えられません。実際のTransformerは、小さい次元のQKVを複数セット(例: 8ヘッド)並列に走らせ、結果を連結します。
heads = [self_attention(X, Wq[i], Wk[i], Wv[i]) for i in range(8)]
out = np.concatenate(heads, axis=-1) @ W_o # 連結して最後に混ぜる
あるヘッドは文法的な係り受けを、別のヘッドは指示語の参照先を、という具合に役割分担が自然に生まれることが観察されています。
次元の勘定を押さえておくと混乱しません。埋め込みが512次元で8ヘッドなら、各ヘッドのQKVは64次元ずつです。8個の出力(各64次元)を横に連結すると512次元に戻り、W_o で混ぜてから次の層へ渡します。入口と出口の形が同じなので、同じブロックを何段でも積み重ねられるわけです。そしてヘッドを増やしても総計算量はほとんど変わりません。1つあたりの次元が狭くなるからで、増えるのは「同時に見分けられる関係の種類」の方です。
実務で効く3つのポイント
1. 因果マスク(Causal Mask)
GPTのような文章生成モデルでは「未来の単語をカンニングしない」よう、scoresの右上三角を -inf で埋めてからsoftmaxします。これで各単語は自分より前しか見られなくなります。
0 ではなく -inf を入れるのは、マスクをかける場所がsoftmaxの前だからです。softmaxは指数関数を通してから正規化するので、 となって重みがちょうど0になります。ここを 0 で埋めると「点数0の候補」になってしまい、正規化後にはわずかな重みが残ります。モデルは未来の単語を薄く覗けてしまい、学習時の損失はきれいに下がるのに生成させると挙動がおかしい、という気づきにくいバグになります。
2. 計算量は文長の2乗 scoresは (文長×文長) の行列なので、文が2倍になると計算とメモリは4倍。長文コンテキストが高価な理由はここにあります。FlashAttentionなどの高速化はこの部分の工夫です。
数にすると実感しやすくなります。文長1,000ならこの表は100万マス、文長10,000なら1億マスです。長さを10倍にすると100倍になる。さらに厄介なのは、この表が計算の途中でメモリに載ることです。最終的な出力より中間結果の方が大きい、という逆転が起きます。FlashAttentionのような手法は、表を丸ごと作らずに小さなブロックへ分けて処理し、この中間結果を持たずに済ませる方向の工夫です。
3. KVキャッシュ 生成時、過去の単語のK・Vは変わらないので使い回せます。これがKVキャッシュで、推論を速くする代わりにメモリを食う—というLLM運用の定番トレードオフの正体です。
覚えておくと効くのは、キャッシュに載るのはKとVだけで、Qは載らないという点です。Qは「いま生成しようとしている1トークン」の分しか要らないのに対し、KとVは過去の全トークン分を持ち続けなければなりません。だから文脈が長いほど、同時に捌くリクエストが多いほどメモリを食い、GPUに載る同時実行数が頭打ちになります。この見積もり方はKVキャッシュを1から理解するで詳しく扱っています。
まとめ
- 自己注意は「Query と Key の内積で重みを決めた、Value の重み付き平均」
- 全単語が同時に全単語を見るので、長距離の依存関係も1ステップで届く
- スケーリング・マスク・マルチヘッドは、この基本形への実用的な補強
この仕組みが原論文の中でどう提示され、どのアブレーション(部品を1つずつ外して効果を測る実験)で設計が正当化されたのかは、論文解説 Attention Is All You Needで本文に沿って読み解いています。
次回はこのAttentionを重ねた「Transformerブロック全体」を、残差接続とLayerNormまで含めて分解します。
コメント
コメントにはログインが必要です