モジュール 09 · 第 2 回

アテンション機構

一つの文字が自分の意味を理解するには、前にどんな文字があるかを見る必要があります。最も単純な「前のすべての文字を平均する」から出発し、クエリ、キー、値、因果マスク、スケーリングを一歩ずつ加えて、完全なアテンションヘッドを一つ書き、PyTorch の実装と照らし合わせます。

  • 約 50 分
  • 難易度:上級
  • 検証:2026-09-15 torch 2.14、CPU、乱数シード固定

コードと実行結果は実際に動かしたときのまま載せているため、コメントと出力は中国語です。

「明月」(明るい月)の「明」と「明天」(明日)の「明」は、意味が違います。一つの文字をどう理解すべきかは、周りにどんな文字があるかで決まります。

私たちが学習させるモデルは、各位置で次の文字を予測するのが仕事です。「床前明月」(寝台の前の明るい月)の後に「光」(光)を書くには、前に「明月」が出てきたことを知っていなければなりません。ですから、どの位置にも前の文字から情報を集める方法が必要です。

アテンション(attention)がその方法で、Transformer の核心です。この課では最も単純なやり方から始めて、一歩ずつ組み立てていきます。

python attention.py

最も単純な方法:平均を取る

各文字はまずベクトルで表します(モジュール 01 第 5 課で埋め込みを扱いました)。最も単純な「前の情報を集める」方法は、自分と前のすべての文字のベクトルの平均を取ることです。

これは 1 回の行列の掛け算でできます。下三角の行列を作り、各行は前のいくつかの位置にだけ値があって、しかも合計が 1 になるようにします。

x = torch.randn(T, 4)  # 5 个词,每个词一个 4 维向量(先随便取)
weights = torch.ones(T, T).tril()  # 下三角:第 i 行只有前 i+1 个位置是 1
weights = weights / weights.sum(dim=1, keepdim=True)  # 每行除以个数,变成平均
out = weights @ x
== 1. 用一个矩阵乘法,让每个位置得到自己和前面所有位置的平均
tensor([[1.00, 0.00, 0.00, 0.00, 0.00],
        [0.50, 0.50, 0.00, 0.00, 0.00],
        [0.33, 0.33, 0.33, 0.00, 0.00],
        [0.25, 0.25, 0.25, 0.25, 0.00],
        [0.20, 0.20, 0.20, 0.20, 0.20]])
  第 3 个位置('明')的结果 = 前三个向量的平均?True

i 行目は、i 番目の文字が各位置からどれだけ取るかを表しています。1 番目の文字は自分しか見られず、3 番目の文字は前の三つを均等に見て、5 番目の文字は五つすべてを均等に見ます。weights @ x で、すべての位置の結果が一度に計算されます。

この行列の掛け算の書き方を覚えておいてください。後のアテンションも同じ形です。重みの行列 × ベクトルの束。違うのは、重みがどこから来るかだけです。

平均の問題ははっきりしています。すべての文字を同じに扱ってしまうのです。「光」が知りたいのは前に「月」があるかどうかで、「床」や「前」はそれほど重要ではありません。重みは内容によって決まるべきです。

クエリとキー:各文字に誰を見るべきかを決めさせる

方法は、各文字に二つのベクトルを用意することです。

  • クエリ(query、q):自分はどんな情報を探しているか。
  • キー(key、k):自分のところにはどんな情報があるか。

ある文字が別の文字にどれだけ「注目」するかは、自分の q と相手の k の内積で採点します。二つのベクトルの向きが近いほど、内積は大きくなります。

まず手作りのベクトルで効果を見てみましょう。ベクトルは 2 次元で、1 次元目は「空に関係する」、2 次元目は「位置に関係する」を表すとします。「光」のクエリは [1, 0] で、空に関係するものを探していることを表します。

k = torch.tensor([[0.1, 0.9],   # 床:一个地点
                  [0.0, 1.0],   # 前:一个方位
                  [0.9, 0.1],   # 明:和天空、光有关
                  [1.0, 0.0],   # 月:天空里的东西
                  [0.8, 0.2]])  # 光
q_guang = torch.tensor([1.0, 0.0])  # "光"想找的是:和天空有关的东西
scores = k @ q_guang
== 2. 点积打分:查询(q)和每个键(k)越像,分数越高
  '光' 对每个词的分数: {'床': 0.1, '前': 0.0, '明': 0.9, '月': 1.0, '光': 0.8}
  softmax 之后的权重:   {'床': 0.12, '前': 0.11, '明': 0.26, '月': 0.29, '光': 0.23}
  分数放大 5 倍再 softmax: {'床': 0.01, '前': 0.0, '明': 0.3, '月': 0.5, '光': 0.18}

スコアは softmax で重みに変わり(モジュール 08 第 5 課で softmax を扱いました)、合計は 1 になります。「月」のスコアが最も高く、最も大きな重みを得ています。

最後の行に注目してください。スコアを全体で 5 倍に拡大すると、softmax 後の重みはずっと集中し、「月」一つで半分を持っていきます。スコアの大きさが、アテンションが「まんべんなく行き渡る」か「一つだけを見つめる」かを決めるのです。この点は後でスケーリングを扱うときに重要になります。

本物のモデルでは、q と k は手作りではなく、二つの行列で各文字のベクトルから計算され、行列の中のパラメータは学習で得られます。どんな文字がどんな情報を探すべきかを、モデルは自分で学ぶのです。

値:実際に伝える内容

採点して、各位置からどれだけ取るかが決まりました。では取るのは何でしょうか。三つ目のベクトル、(value、v)です。「私に注目するなら、この情報をあげる」という意味です。

なぜ元のベクトルをそのまま取らないのでしょうか。「照合に使う情報」と「伝えるための情報」は同じとは限らないからです。二つを分ければ、モデルはより柔軟になります。

三つのベクトルを合わせたものが、アテンションのすべてです。

权重 = softmax(q 和每个 k 的点积)
输出 = 用这些权重,把每个位置的 v 加起来

因果マスク:後ろを覗いてはいけない

学習のとき、モデルは各位置で次の文字を予測しなければなりません。3 番目の位置が 4 番目の文字を見られれば、「答えを写す」だけで済み、何も学べません。ですから各位置は自分と前の位置しか見られないようにします。

やり方は、softmax の前に、後ろの位置のスコアを負の無限大にすることです。負の無限大の指数は 0 なので、softmax 後の重みは 0 になります。

scores = q @ k.T / math.sqrt(8)
mask = torch.ones(T, T, dtype=torch.bool).tril()
scores = scores.masked_fill(~mask, float("-inf"))
== 3. 因果掩码:把后面位置的分数设成负无穷,softmax 之后权重就是 0
tensor([[1.00, 0.00, 0.00, 0.00, 0.00],
        [0.96, 0.04, 0.00, 0.00, 0.00],
        [0.24, 0.26, 0.50, 0.00, 0.00],
        [0.09, 0.39, 0.43, 0.09, 0.00],
        [0.02, 0.14, 0.02, 0.09, 0.73]])

最初の節の平均の行列と比べてみてください。形は同じで、どちらも下三角で、各行の合計はどちらも 1 です。違いは、各行の重みがもう均等ではなく、q と k によって決まっていることです。ここの q と k はランダムなので、重みには規則性がないように見えます。学習した後には、意味を持つようになります。

このマスクがあるので、この種のアテンションは因果的自己アテンションと呼ばれます。自己アテンションとは q、k、v がすべて同じ文章から来ること、因果的とは過去しか見られず未来は見られないことを指します。GPT のような「1 文字ずつ後ろへ書いていく」モデルは、どれもこれを使っています。

なぜ次元の平方根で割るのか

上のコードに / math.sqrt(8) がありました。8 は q と k の次元です。このステップをスケーリングと呼びます。しないとどうなるか見てみましょう。

== 4. 为什么要除以 √d:维度越大,点积的数值越大,softmax 会变得非常极端
  d=   16  点积的标准差   3.89(√d =  4.00)  每行最大权重的平均:不缩放 0.79,除以 √d 后 0.53
  d=   64  点积的标准差   8.17(√d =  8.00)  每行最大权重的平均:不缩放 0.86,除以 √d 后 0.47
  d=  256  点积的标准差  16.08(√d = 16.00)  每行最大权重的平均:不缩放 0.95,除以 √d 后 0.47
  d= 1024  点积的标准差  32.53(√d = 32.00)  每行最大权重的平均:不缩放 0.92,除以 √d 后 0.39

二つのランダムなベクトルの内積は、d 個の積を足し合わせたものです。足す項が多いほど結果の揺らぎは大きくなり、標準偏差はおよそ d の平方根になります。d=256 のとき、内積の標準偏差は 16 です。

スコアの揺らぎが大きくなると、2 番目の節の「5 倍に拡大」と同じように、softmax が極端になります。スケーリングしないと、各行で最も大きな重みは平均 0.8 から 0.95 になり、ほぼすべての注目が一つの位置に向かいます。これは学習を始めたばかりのときにはとてもまずいことです。重みが 0 や 1 に近いところでは softmax の勾配がとても小さく、モデルが学習しにくくなるからです。

d の平方根で割れば、スコアの標準偏差は 1 前後に戻り、各行で最も大きな重みは 0.4 から 0.5 の間になって、注目は分散し、勾配も正常で、モデルは誰に注目すべきかを少しずつ学べます。

完全なアテンションヘッド

これまでのものを合わせます。

d_model, d_head = 16, 8
x = torch.randn(T, d_model)
W_q, W_k, W_v = (torch.randn(d_model, d_head) / math.sqrt(d_model) for _ in range(3))
q, k, v = x @ W_q, x @ W_k, x @ W_v
scores = (q @ k.T / math.sqrt(d_head)).masked_fill(~mask, float("-inf"))
out = F.softmax(scores, dim=-1) @ v
== 5. 一个完整的注意力头:q、k、v 都由同一个输入经过不同的矩阵得到
  输入 (5, 16) → q、k、v 各 (5, 8) → 输出 (5, 8)
  和 PyTorch 自带的 scaled_dot_product_attention 比,最大差别 2.4e-07

5 行のコードです。三つの行列で入力を q、k、v に変え、採点し、スケーリングし、マスクし、softmax を取り、重み付きで足し合わせる。結果は PyTorch に組み込まれた scaled_dot_product_attention と一致します(差は小数点以下 7 桁目で、浮動小数点の計算順序の違いによる誤差です)。

W_qW_kW_v が、このアテンションヘッドが学ぶパラメータです。学習のときは、ほかのパラメータと同じように、誤差逆伝播法と勾配降下法で更新されます。

この式の出典は 2017 年の論文『Attention Is All You Need』で、数式で書くと softmax(QKᵀ/√d)V です。今ではその中の記号一つ一つが何をしているのかわかるはずです。

アテンションの代償

どの位置も、前のすべての位置と採点しなければなりません。系列の長さが T なら、スコアの表は T×T の大きさです。長さが倍になれば、計算量とこの表が占めるメモリは 4 倍になります。

これが長いコンテキストが高くつく根本的な理由の一つです(モジュール 01 第 4 課でコンテキストとコストを扱いました)。各社のモデルは数十万、数百万トークンのコンテキストに対応するため、アテンションの計算方法に大量の最適化を施していますが、基本的な考え方はやはりこの課の数行です。

練習問題

  1. 2 番目の節で、「光」のクエリベクトルを [0, 1](位置に関係するものを探す)に替えてください。重みはどう変わりますか。
  2. 5 番目の節で因果マスクを取り除き、scaled_dot_product_attention(..., is_causal=False) と比べてください。
  3. 5 番目の節の系列の長さを 5 から 1000、2000、4000 に変え、time.time() でそれぞれの長さにかかる時間を測ってください。おおよそ二乗で増えていますか。

確認テスト

1. クエリ、キー、値はそれぞれどんな役割を果たしていますか?

クエリは「自分は何を探しているか」、キーは「自分のところには何があるか」を表します。ある位置のクエリとすべての位置のキーの内積を取ると、各位置への注目スコアが得られ、softmax で重みに変わります。値は「私に注目したときに得られる情報」を表し、出力はそれらの重みですべての位置の値を足し合わせたものです。

2. 因果マスクはどう実装されていますか?なぜ必要なのですか?

softmax の前に、各位置より後ろのスコアを負の無限大にし、softmax 後にそれらの位置の重みが 0 になるようにします。必要なのは、モデルが次の文字の予測を学ぼうとしているからで、後ろの文字が見えてしまえば答えを写すだけで済み、何も学べません。

3. アテンションのスコアを次元の平方根で割るのはなぜですか?

d 次元の二つのベクトルの内積は、標準偏差がおよそ d の平方根になり、次元が大きいほどスコアの揺らぎが大きくなって、softmax 後の重みが一つの位置に極端に集中し、勾配がとても小さくなって、モデルが学習しにくくなります。d の平方根で割ると、スコアの標準偏差が 1 前後に戻ります。

質問と議論

このレッスンでつまずいたところは、ここで質問してください。他の人の質問に答えるのも歓迎です。

質問で 3 ポイント、回答で 6 ポイント。審査を通過すると公開されます。

議論を読み込んでいます…