AI手帖

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)は実際に混ぜる値に当たります。

まずは文章を離れ、手で置いた小さな数値で計算します。

Attentionの計算を3段階で見る
QとKから重みを作り、その重みでVを混ぜる計算。(概念図)

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は各スコアの指数を求め、行の合計で割る処理です。コードで先に最大値を引いているのは、指数の計算が大きくなりすぎるのを抑えるためで、得られる割合は変わりません。

計算した重みをヒートマップで見る
教材用のQとKから計算した、各位置への参照の重み。
重みの数値をテキストで見る
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になっています。

最初の出力を、重み付き平均で作る
最初の位置の重みでVを混ぜた計算結果。

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ではありません。

トークンから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のどちらか一方だけを変更し、重みと出力を別々に見比べると、計算の役割を確かめやすくなります。

計算と実装を確認した公式資料

確認日:2026年10月20日。本文の数値は、教材用の入力と記載したライブラリの版で実行した結果です。現在の資料にある既定値や、未知データ・実モデルの性能を示すものではありません。