AttentionのQ・K・VをPythonで計算する — 重みと出力を3つの位置で追う
Attentionの説明に出てくるQ・K・Vは、どんな順序で計算されるのでしょうか。3つの位置に手で2次元の値を置いて計算すると、最初の位置が使う重みは約[0.401, 0.198, 0.401]、出力は約[0.802, 0.599]になりました。
同じ配列でQだけ、Vだけを変え、重みと出力のどちらが動くかを確かめます。
QとKで重みを求め、Vを混ぜる
Attentionは、参照先ごとの重みを求め、その重みで値を混ぜて出力を作る計算です。Q(Query)は参照する側の情報、K(Key)は各参照先と比べる手がかり、V(Value)は実際に混ぜる値に当たります。
まずは文章を離れ、手で置いた小さな数値で計算します。
3つの位置を用意し、それぞれに2次元の値を置きます。下の配列では、各行が一つの位置に対応しています。見通しをよくするためQ・K・Vを同じ値にしていますが、常に同じ値を使う必要があるわけではありません。
Q = np.array([[1., 0.], [0., 1.], [1., 1.]])
K = Q.copy()
V = np.array([[1., 0.], [0., 1.], [1., 1.]])
scores = Q @ K.T / np.sqrt(2)
weights = np.exp(scores - scores.max(axis=1, keepdims=True))
weights /= weights.sum(axis=1, keepdims=True)
output = weights @ V
Q @ K.T は、Qの各行とKの各行の内積を並べる計算です。参照する位置が行、参照先が列となり、スコアは3行3列になります。原論文のscaled dot-product attentionに沿って、Kの次元数の平方根で割ります。今回は2次元なので、割る数は√2です。
最初の行の内積は [1, 0, 1]。√2で割ったスコアにsoftmaxを適用すると、重みは約 [0.401, 0.198, 0.401] になります。softmaxは各スコアの指数を求め、行の合計で割る処理です。コードで先に最大値を引いているのは、指数の計算が大きくなりすぎるのを抑えるためで、得られる割合は変わりません。
重みの数値をテキストで見る
0.401 0.198 0.401
0.198 0.401 0.401
0.248 0.248 0.503最初の位置では、Vの1行目と3行目をそれぞれ約0.401、2行目を約0.198の割合で混ぜます。計算は 0.401 × [1, 0] + 0.198 × [0, 1] + 0.401 × [1, 1] に相当し、出力は約 [0.802, 0.599] です。実際の出力は、丸める前の重みで計算しています。
各行の重みの合計は1です。表示を小数点以下3桁に丸めると、3行目は 0.248 + 0.248 + 0.503 = 0.999 になりますが、丸める前の合計は1になっています。
Qだけ、Vだけを変えて比べる
どの値が重みを変えるのかを確かめるため、基準の配列から二つの条件を独立して試しました。
一つ目は、Qの1行目だけを [0., 1.] に替える条件です。KとVは基準の値のままにします。二つ目は、QとKを基準の値に戻し、Vの1行目だけを [2., 0.] に替える条件です。表では、どちらも最初の位置の重みと出力を比べています。
| 条件 | 最初の行の重み(位置1, 2, 3) | 最初の出力 |
|---|---|---|
| 基準 | [0.401, 0.198, 0.401] | [0.802, 0.599] |
| Qの1行目だけ変更 | [0.198, 0.401, 0.401] | [0.599, 0.802] |
| Vの1行目だけ変更 | [0.401, 0.198, 0.401] | [1.203, 0.599] |
Qを替えた条件では、最初の位置が使う重みが変わり、出力の2成分も入れ替わりました。Vだけを替えた条件では、重みは変わらず、出力だけが変わっています。重みはQとKから求め、出力にはVの値を使うためです。
Vを替えたときの出力には、1を超える成分が現れました。重みの各行の合計は1ですが、出力ベクトルそのものが確率分布になるわけではありません。
文章を扱うモデルにつながる部分
文章を扱うモデルでは、まず文章をトークンに区切り、各トークンをベクトルに変えます。そのベクトルを、学習した変換によってQ・K・Vへ写します。今回の実習は、その後の内積・softmax・重み付き平均の部分を取り出したものです。
下の図は、文章から出力までの流れを示す概念図です。今回の数値は、モデルから取り出したQ・K・Vではありません。
この例は、3つの位置がすべての位置を参照できる設定です。後の位置を見えなくする因果マスクは入れていません。次のトークンを順に生成するTransformerでは、この参照範囲の制御も必要になります。
複数のAttentionヘッド、位置の情報、残差接続なども省略しています。そのため、この計算から実際のLLMがどの言葉を重視するか、どんな回答を生成するかは判断できません。
Pythonで配列を変えて確かめる
ノートブックでは、「依存ライブラリ → 共通準備 → Attention → 結果を保存」の順に実行します。XORのセルは飛ばせます。基準・Qのみ変更・Vのみ変更の3条件が出力されます。
ノートブックを使わない場合は、下のZIPを展開し、python -m pip install -r requirements.txt でライブラリを準備してから、python run-foundations.py --out foundation-results-local.json を実行してください。このスクリプトはXORの小さな学習例も一緒に実行します。APIキーや外部LLMは使わず、初回のライブラリ取得時に通信します。
記事の数値は2026年10月20日JSTにサーバーCPUで再計算した結果です。計算環境はPython 3.11.2、NumPy 2.4.6。ColabやGPUで取得した記録ではありません。配布する教材のライブラリ指定と、丸める前の配列も保存しています。
自分で値を変えるときも、まずはQかVのどちらか一方だけを変更し、重みと出力を別々に見比べると、計算の役割を確かめやすくなります。
計算と実装を確認した公式資料
- Attention Is All You Need — 3.2.1、3.2.3
scaled dot-product attentionの式と、生成時の参照範囲の制御。記事の配列はAI手帖が教材用に用意したものです。
確認日:2026年10月20日。本文の数値は、教材用の入力と記載したライブラリの版で実行した結果です。現在の資料にある既定値や、未知データ・実モデルの性能を示すものではありません。