第6章 内積による類似度の計算:どのトークンに注目するかを決める

こんにちは。ゆうせいです。

第5章では、各トークンからQuery、Key、Valueという三つのベクトルが生成されることを確認しました。第6章では、いよいよそれらを使った計算に入ります。あるトークンのQueryと、他のすべてのトークンのKeyを照らし合わせ、どれだけ一致しているかを数値化する処理です。この照合には、内積という計算が使われます。Transformer Explainerで、QueryとKeyの列の間に現れる、格子状に並んだ数値の表が、この章で作られるものです。なぜ足し算でも引き算でもなく、内積なのでしょうか。内積という計算が「似ている度合い」を表せるのはなぜでしょうか。この章では、その理由を、数式と図形の両面から解説します。

内積とは何か

まず、内積の定義を確認します。

内積(dot product、ドット積)とは、二つのベクトルについて、対応する要素どうしを掛け合わせ、それらをすべて足し合わせた値です。

\mathbf{a} \cdot \mathbf{b} = a_1 b_1 + a_2 b_2 + \cdots + a_n b_n

具体例で確認します。次の二つのベクトルがあるとします。

\mathbf{a} = [2, 3, 1]

\mathbf{b} = [4, 1, 5]

内積は、次のように計算されます。

\mathbf{a} \cdot \mathbf{b} = 2 \times 4 + 3 \times 1 + 1 \times 5 = 8 + 3 + 5 = 16

計算そのものは単純です。掛けて、足すだけです。

なお、第5章までで登場したアダマール積とは異なる点に注意してください。アダマール積は、要素ごとに掛けた結果を、ベクトルのまま残します。内積は、それらをすべて足し合わせて、一つの数値にします。

なぜ内積が「似ている度合い」を表すのか

ここが、この章の核心です。三つの角度から説明します。

角度1:数値の符号に注目する

内積の計算では、対応する要素どうしを掛け合わせます。掛け算には、次の性質があります。

同じ符号どうし(プラスとプラス、またはマイナスとマイナス)を掛けると、結果はプラスになります。

異なる符号どうし(プラスとマイナス)を掛けると、結果はマイナスになります。

つまり、二つのベクトルの各次元の値が、同じ方向に振れていれば、その次元の寄与はプラスになります。逆方向に振れていれば、マイナスになります。

すべての次元で同じ方向に振れていれば、プラスの値ばかりが足し合わされ、内積は大きな正の値になります。

すべての次元で逆方向に振れていれば、マイナスの値ばかりが足し合わされ、内積は大きな負の値になります。

方向がばらばらであれば、プラスとマイナスが打ち消し合い、内積は0に近い値になります。

したがって、内積の大きさは、「二つのベクトルが、どれだけ同じ方向を向いているか」を表す指標になるのです。

角度2:幾何学的な意味

内積には、次の公式が成り立ちます。

\mathbf{a} \cdot \mathbf{b} = |\mathbf{a}| , |\mathbf{b}| \cos\theta

|\mathbf{a}| はベクトルaの長さ、\theta は二つのベクトルがなす角度です。

この式から、次のことが分かります。

二つのベクトルが同じ方向を向いているとき(角度が0度)、\cos 0 = 1 となり、内積は最大になります。

二つのベクトルが直交しているとき(角度が90度)、\cos 90° = 0 となり、内積は0になります。

二つのベクトルが正反対を向いているとき(角度が180度)、\cos 180° = -1 となり、内積は最小(最も大きな負の値)になります。

角度が近いほど内積が大きくなる、という性質が、そのまま「似ている度合い」の指標になります。

角度3:ベクトルの長さも影響する

上の公式には、|\mathbf{a}||\mathbf{b}| 、つまりベクトルの長さも含まれています。

これは、内積が、方向の一致度だけでなく、ベクトルの大きさにも影響されることを意味します。方向が同じでも、長いベクトルどうしの内積は、短いベクトルどうしの内積より大きくなります。

この性質は、後の章で問題になります。第7章のスケーリングは、この「値が大きくなりすぎる」という問題に対処するための処理です。

なお、方向の一致度だけを純粋に測りたい場合は、内積をベクトルの長さで割った、コサイン類似度という指標を使います。

\cos\theta = \frac{\mathbf{a} \cdot \mathbf{b}}{|\mathbf{a}| , |\mathbf{b}|}

自己注意では、コサイン類似度ではなく、そのままの内積が使われます。長さの情報も、モデルにとって意味のある情報として扱われている、と理解してください。

自己注意における内積の計算

では、実際に自己注意の中で、どのように内積が計算されるのかを確認します。

計算されるのはすべての組み合わせ

あるトークンのQueryと、他のすべてのトークンのKeyとの内積を計算します。これを、すべてのトークンについて行います。

トークンが5個ある文章であれば、次のようになります。

1番目のトークンのQueryと、1番目から5番目までのKeyとの内積を、5個計算します。

2番目のトークンのQueryと、1番目から5番目までのKeyとの内積を、5個計算します。

これを5番目まで繰り返すと、合計で25個の値が得られます。

この25個の値を、5行5列の表として並べたものが、注意スコア(Attention Score)の行列です。

行と列の意味を正確に把握する

この行列を読むときには、行と列が何を表しているかを、正確に押さえてください。

行は、Queryを出しているトークン、つまり「情報を集める側」を表します。

列は、Keyを提示しているトークン、つまり「参照される側」を表します。

したがって、3行2列の位置にある値は、「3番目のトークンが、2番目のトークンにどれだけ注目しているか」を表します。

この行と列の対応を取り違えると、後の章で扱うマスク処理や、Softmaxをどの方向に適用するかといった話が、すべて逆になってしまいます。Transformer Explainerで表を見るときは、必ず縦軸と横軸のラベルを確認する習慣をつけてください。

行列の掛け算として一括で計算する

実装上は、この計算をひとつずつ行うのではなく、行列の掛け算として一括で処理します。

スコア行列 = Q K^T

Qは、すべてのトークンのQueryを縦に並べた行列です。Kは、すべてのトークンのKeyを縦に並べた行列です。K^T は、Kの転置、つまり行と列を入れ替えたものです。

この一つの行列積によって、すべての組み合わせの内積が、同時に計算されます。

行列の掛け算の定義を思い出してください。行列積の計算では、左の行列の各行と、右の行列の各列との内積を取ります。まさに、必要としている計算そのものです。

Kを転置するのは、Keyを列方向に並べ替えるためです。

形の確認

トークン数をnとします。GPT-2(small)では、1ヘッドあたりの次元数は64です(第5章で説明したとおり、768を12ヘッドで割った値です)。

Qの形は、n行64列です。

Kの形も、n行64列です。

K^T の形は、64行n列です。

したがって、Q K^T の形は、n行n列になります。

トークン数の2乗の大きさの行列ができる、という点が重要です。この性質が、Transformerの計算量の問題を生みます。

計算量がトークン数の2乗になる問題

スコア行列の大きさは、トークン数の2乗に比例します。

トークンが100個であれば、10000個の値を計算します。

トークンが1000個であれば、1000000個の値を計算します。

トークン数が10倍になると、計算量は100倍になります。

第4章で、GPT-2のコンテキスト長が1024であると説明しました。この上限は、位置埋め込み行列の大きさによる制約であると同時に、計算量の観点からも、無制限には伸ばせない理由があるのです。

近年の長いコンテキストを扱えるモデルでは、この2乗の計算量を削減するための、さまざまな工夫が導入されています。スパースアテンション、線形アテンション、FlashAttentionなど、複数のアプローチが研究されています。

新人エンジニアとして押さえておくべきは、「コンテキスト長を伸ばすことは、単にメモリを増やせば済む話ではなく、計算量が2乗で効いてくる難しい問題である」という点です。

Transformer Explainerで観察できること

Transformer Explainerでは、このスコア行列が、格子状の表として可視化されています。

次の点を確認してみてください。

一つ目は、表の大きさが、入力したトークン数の2乗になっているという点です。トークンを1つ増やすと、表の行と列が両方増えることを確認できます。

二つ目は、値がプラスにもマイナスにもなるという点です。この段階では、まだ確率にはなっていません。値の範囲に制限はなく、大きな正の値も、大きな負の値も現れます。

三つ目は、対角線上の値、つまり自分自身との内積が、必ずしも最大ではないという点です。

第5章で説明したとおり、QueryとKeyは異なる行列で変換されているため、同じトークンから作られたQueryとKeyは、同じベクトルにはなりません。したがって、自分自身への注目度が最大になるとは限らないのです。

もしQueryとKeyを分けていなければ、対角線上の値が必ず最大になり、すべてのトークンが自分自身にばかり注目してしまいます。第5章で説明した「三つに分ける理由」の三つ目が、ここで実際に確認できます。

初学者がつまずきやすい点

つまずき1:内積の値をそのまま「確率」だと思ってしまう

この段階のスコアは、確率ではありません。合計が1になる保証もなく、マイナスの値も含まれます。

確率に変換するのは、第9章で扱うSoftmax関数の役割です。ここではまだ、「どれだけ一致しているか」を表す生の数値である、と押さえてください。

つまずき2:転置の意味が分からなくなる

QK^T という式で、なぜKを転置するのかが分からないという声をよく聞きます。

行列の掛け算は、「左の行列の行」と「右の行列の列」の内積を取る計算です。

Kは、各行にトークンのKeyが入っている状態です。このままでは、掛け算の右側に置いても、行方向に並んだKeyを使えません。

転置することで、各列にKeyが入った状態になり、行列積の定義どおりに、QueryとKeyの内積が取れるようになります。

紙に小さな行列(2行2列程度)を書いて、実際に手で計算してみると、この点は一度で腑に落ちます。

つまずき3:スコア行列が非対称であることに気づかない

スコア行列は、対称行列ではありません。

つまり、3行2列の値と、2行3列の値は、一般に異なります。

「3番目のトークンが2番目にどれだけ注目するか」と、「2番目のトークンが3番目にどれだけ注目するか」は、別の話だからです。

代名詞「it」は名詞「animal」に強く注目しますが、「animal」が「it」に同じだけ注目するとは限りません。注目という関係は、一方通行なのです。

これも、QueryとKeyを別々の行列で作っているからこそ実現できる性質です。

演習

演習1:内積を手計算する

次の二組のベクトルについて、それぞれ内積を計算してください。そのうえで、どちらの組み合わせのほうが「似ている」と判断されるかを答えてください。

組み合わせA:\mathbf{q} = [1, 2, -1]\mathbf{k} = [2, 3, -2]

組み合わせB:\mathbf{q} = [1, 2, -1]\mathbf{k} = [-2, 1, 3]

演習2:スコア行列の大きさを計算する

次の条件で、スコア行列に含まれる値の個数を計算してください。

入力トークン数が512、ヘッド数が12の場合。

ヘッドごとにスコア行列が作られる点に注意してください。

演習3:スコア行列を実際に計算する

from transformers import GPT2Tokenizer, GPT2Model
import torch

tokenizer = GPT2Tokenizer.from_pretrained("gpt2")
model = GPT2Model.from_pretrained("gpt2")

text = "The cat sat on the mat"
inputs = tokenizer(text, return_tensors="pt")
tokens = tokenizer.convert_ids_to_tokens(inputs["input_ids"][0])
print(f"トークン: {tokens}")

# 埋め込みを計算
token_emb = model.wte(inputs["input_ids"])
position_ids = torch.arange(inputs["input_ids"].shape[1]).unsqueeze(0)
position_emb = model.wpe(position_ids)
x = token_emb + position_emb

# レイヤー正規化を通す(第12章で扱います)
x = model.h[0].ln_1(x)

# QKVを生成して分割
qkv = model.h[0].attn.c_attn(x)
q, k, v = qkv.split(768, dim=2)

# 1つ目のヘッドだけを取り出す(先頭64次元)
q_head0 = q[0, :, :64]
k_head0 = k[0, :, :64]

# スコア行列を計算
scores = q_head0 @ k_head0.T
print(f"\nスコア行列の形: {scores.shape}")
print(f"スコア行列(小数点以下2桁):")
print(scores.detach().numpy().round(2))

# 対角成分が最大かどうかを確認
for i in range(len(tokens)):
    max_idx = scores[i].argmax().item()
    print(f"{tokens[i]} が最も注目するのは: {tokens[max_idx]}")

演習の解答例と解説

演習1の解答は、次の通りです。

組み合わせA:1 \times 2 + 2 \times 3 + (-1) \times (-2) = 2 + 6 + 2 = 10

組み合わせB:1 \times (-2) + 2 \times 1 + (-1) \times 3 = -2 + 2 - 3 = -3

組み合わせAのほうが、内積が大きく、より「似ている」と判断されます。

組み合わせAでは、各次元の符号がすべて一致しています(プラス、プラス、マイナス)。そのため、掛け算の結果がすべてプラスになり、大きな正の値が得られます。

組み合わせBでは、符号が一致していない次元があるため、マイナスの寄与が生じ、全体として負の値になっています。

演習2の解答は、次の通りです。

1ヘッドあたりのスコア行列は、512行512列です。

512 \times 512 = 262144

これがヘッドの数だけ存在します。

262144 \times 12 = 3145728

約315万個の値が、1つのTransformerブロックの中で計算されます。ブロックは12個ありますから、モデル全体では、この12倍にあたる約3775万個の内積が、1回の推論で計算されることになります。

この計算量の大きさが、大規模言語モデルの推論に強力な計算資源が必要とされる理由の一つです。

演習3では、各トークンが最も注目する相手を確認できます。文法的に関連の強いトークンどうしが高いスコアを示すか、観察してみてください。ただし、1つ目のヘッドだけを見ているため、必ずしも直感に合う結果になるとは限りません。ヘッドごとに異なる観点で注目先が決まるという点は、第10章で扱います。

講師向けの補足

内積が類似度を表す理由は、受講者の数学的な背景によって、響く説明が異なります。

高校数学の内積を覚えている受講者には、|\mathbf{a}| |\mathbf{b}| \cos\theta の公式から入るのが最短です。

数学に苦手意識がある受講者には、符号の掛け算の話から入ることを推奨します。「同じ方向に振れていればプラスが積み上がる」という説明は、公式を知らなくても直感的に理解できます。

また、2次元や3次元の小さなベクトルで、実際に紙の上に矢印を描いて、角度と内積の関係を確認する時間を取ると、理解が定着します。768次元は想像できませんが、2次元であれば目で見えます。

理解度を確認する問い

第6章の内容を、次の問いで確認してみてください。

内積という計算が、二つのベクトルの「似ている度合い」を表せるのはなぜでしょうか。符号の掛け算に着目して説明してみてください。

スコア行列の行と列は、それぞれ何を表しているでしょうか。

スコア行列が対称行列にならないのは、なぜでしょうか。

トークン数が2倍になると、スコア行列の計算量は何倍になるでしょうか。

まとめ

自己注意では、あるトークンのQueryと、すべてのトークンのKeyとの内積を計算することで、どのトークンにどれだけ注目すべきかを表すスコアを求めます。

内積が類似度の指標になるのは、対応する要素の符号が一致していればプラスの値が積み上がり、一致していなければ打ち消し合うという性質によるものです。幾何学的には、二つのベクトルがなす角度が小さいほど、内積が大きくなります。

計算は、QK^T という一つの行列積で一括して行われ、トークン数をnとするとn行n列のスコア行列が得られます。行はQueryを出す側、列はKeyを提示する側を表し、この行列は対称にはなりません。

スコア行列の大きさはトークン数の2乗に比例するため、コンテキスト長を伸ばすことは、計算量の面で大きな負担になります。

この段階のスコアは、まだ確率ではありません。値の範囲に制限はなく、マイナスの値も含まれます。

次の第7章では、このスコアに対して行われるスケーリングという処理を扱います。なぜ、わざわざルートd_kという値で割る必要があるのか、割らなかった場合に何が起きるのかを解説します。

第7章に進む前に、Transformer Explainerでスコア行列の値の範囲を確認しておいてください。どのくらいの大きさの値が現れているかを見ておくと、次章でスケーリングが必要とされる理由が、実感を持って理解できるはずです。

セイ・コンサルティング・グループでは新人エンジニア研修のアシスタント講師を募集しています。

投稿者プロフィール

山崎講師
山崎講師代表取締役
セイ・コンサルティング・グループ株式会社代表取締役。
岐阜県出身。
海外放浪の末、2000年創業、2004年会社設立。
IT企業向け人材育成研修歴業界歴20年以上。
すべての無駄を省いた費用対効果の高い「筋肉質」な研修を提供します!
この記事に間違い等ありましたらぜひお知らせください。

学生時代は趣味と実益を兼ねてリゾートバイトにいそしむ。長野県白馬村に始まり、志賀高原でのスキーインストラクター、沖縄石垣島、北海道トマム。高じてオーストラリアのゴールドコーストでツアーガイドなど。現在は野菜作りにはまっている。