マルチヘッドアテンションと位置エンコーディング
一つのアテンションヘッドは、一つの基準でしか前の文字を見られません。ヘッドをいくつか開けば、いくつものことを同時に見られます。アテンションそのものは文字の順序も区別できないので、位置を教える必要があります。この課では実験でこの二つをはっきり見て、予想外の結果にも出会います。
- 約 40 分
- 難易度:上級
- 検証:2026-09-15 torch 2.14、CPU、乱数シード固定
コードと実行結果は実際に動かしたときのまま載せているため、コメントと出力は中国語です。
前の課でアテンションヘッドを一つ書きました。それで GPT を組み立てるには、あと二つ足りません。マルチヘッドと、位置の情報です。
python heads_positions.py
この課の実験では gpt.py のモデルを使いますが、学習は不要で、構造そのものの性質を見ます。
マルチヘッド:いくつものことを同時に見る
一つの文字が前から集めるべき情報は、たいてい一種類ではありません。「白日依山尽」(白い日は山に寄り添って沈む)の「尽」を書くとき、モデルは同時にいくつものことを知る必要があるかもしれません。前にどんな風景があるか(白日、山)、これが何文字目か(五言詩なら 5 文字目で韻を踏むか句を切る)、前の句でどんな語を使ったか。
一つのアテンションヘッドには q と k が一組しかなく、一つの基準でしか採点できず、一組の重みしか得られません。マルチヘッドアテンションのやり方はこうです。ベクトルをいくつかの区間に切り、区間ごとに一度ずつアテンションをして、最後につなぎ戻す。各ヘッドは自分の q、k、v を持ち、違うものに注目するよう学べます。
== 2. 多头注意力:32 维的向量切成 4 个头,每个头 8 维
q 的形状 (1, 5, 32)
拆成多个头之后 (1, 4, 5, 8):(句子数, 头数, 字数, 每个头的维度)
注意力分数 (1, 4, 5, 5):每个头都有自己的一张 5×5 的表
一个注意力层的参数:qkv 3168 个,proj 1056 个,头数多少都不影响这个数
gpt.py では、実装は形を変える処理が数回増えるだけです。
q, k, v = self.qkv(x).split(C, dim=2)
# 拆成多个头:(B, T, C) -> (B, 头数, T, 每个头的维度)
q, k, v = (t.view(B, T, self.n_head, C // self.n_head).transpose(1, 2) for t in (q, k, v))
self.qkv は線形層一つで、q、k、v の三つのベクトルを一度に計算し、split で分けます。それから 32 次元のベクトルを 8 次元ずつの 4 区間とみなし、「ヘッド」の次元を前に移します。その後の採点、マスク、softmax、重み付きの和は前の課とまったく同じで、ただ PyTorch の行列の掛け算がヘッドごとに自動で別々に計算し、4 つのヘッドを一度に計算し終えます。
計算し終えたら、4 つのヘッドの結果を 32 次元につなぎ戻し、さらに線形層 proj を通して混ぜ合わせ、各ヘッドが集めた情報を互いに結びつけられるようにします。
out = out.transpose(1, 2).contiguous().view(B, T, C) # 各个头拼回去
return self.proj(out)
最後の行の出力に注目してください。パラメータの数はヘッドの数と関係ありません。8 次元のヘッド 4 つでも、32 次元のヘッド 1 つでも、使う行列の大きさは同じで、切り方が違うだけです。マルチヘッドはパラメータを増やさず、計算量もほとんど増やさないのに、モデルがいくつかの違う注目の仕方を同時に学べるようにします。
私たちの GPT はヘッドを 4 つ使い、各ヘッドは 32 次元です(ベクトル全体で 128 次元)。LLM にはふつう数十のヘッドがあり、各ヘッドは 64 か 128 次元です。
アテンションは順序を区別できない
前の課のアテンションの式を振り返ってください。採点は q と k の内容しか見ず、重み付きの和も v の内容しか見ず、「この文字が何番目の位置にあるか」はどこにも使われていません。
つまりアテンションにとっては、「白日依山尽」と「山依日白尽」に違いがないかもしれないということです。
実験です。モデルにこの二つの句を読ませます。どちらも最後の文字は「尽」で、前の 4 文字は順序が入れ替わっています。最後の位置でのモデルの出力(つまり次の文字への採点)がどれだけ違うかを比べます。
model.pos_emb.weight.zero_() # 把位置嵌入清零,等于没有位置信息
a = model(torch.tensor([tok.encode("白日依山尽")]))[0][0, -1]
b = model(torch.tensor([tok.encode("山依日白尽")]))[0][0, -1]
== 1. '白日依山尽' 和 '山依日白尽',最后一个位置的输出差多少
1 层:没有位置嵌入 3.7e-08,有位置嵌入 2.8e-03
2 层:没有位置嵌入 1.9e-02,有位置嵌入 1.5e-02
まず 1 行目、1 層だけのモデルを見ます。位置の情報がないとき、二つの句の出力の差は 3.7e-08 で、浮動小数点の丸め誤差しか残っておらず、まったく同じです。最後の位置にとって、目の前にあるのは同じ 5 文字で、ただ順序が違うだけです。アテンションはそれらを採点して重み付きで足し合わせ、結果は順序と関係ありません。
これは言語モデルにとって致命的です。「白日依山尽」は詩ですが、「山依日白尽」は意味をなしません。「我打你」(私があなたを殴る)と「你打我」(あなたが私を殴る)は意味が正反対です。モデルは順序を知っていなければなりません。
予想外の 2 行目
2 行目は 2 層のモデルです。上の理屈なら、位置の埋め込みがなければ二つの句の出力は同じになるはずです。ところが実験の結果は 1.9e-02 の差があり、位置の埋め込みがあるときの 1.5e-02 とほぼ同じ大きさでした。
この実験を書いたとき、私も予想していませんでした。原因は因果マスクにあります。
- 1 層目では、各位置は自分と前の文字しか見られません。「白日依山尽」の 2 番目の位置が見るのは「白日」で、「山依日白尽」の 2 番目の位置が見るのは「山依」です。ですから 1 層目の 2、3、4 番目の位置での出力は、二つの句で違います。
- 2 層目では、最後の位置が前の各位置の 1 層目の出力を見ますが、それらの出力はすでに違っています。
つまり、因果マスクそのものが順序を漏らしているのです。ある位置がいくつの文字を見られるかが、自分が何番目の位置にいるかを間接的に教えています。2 層以上あれば、モデルはそこからある程度の位置の情報を得られます。
これは私たちのコードの偶然の現象ではありません。2022 年に、まさにこのことを研究した論文があります(Haviv ら、『Transformer Language Models without Positional Encodings Still Learn Positional Information』)。位置エンコーディングを一切加えない因果的な言語モデルでも位置の情報を学べ、効果は位置エンコーディングを加えたものより少し劣るだけだとわかりました。
とはいえ、マスクから「推測」した位置の情報は間接的で、あいまいです。実際のモデルはどれも、位置の情報を明示的に加えています。
位置の埋め込み
最も簡単な方法で、GPT-2 が使っているものです。各位置にもベクトルを一つ用意し、文字のベクトルに足します。
self.pos_emb = nn.Embedding(cfg.block_size, cfg.n_embd)
...
pos = torch.arange(start, start + T, device=idx.device)
x = self.drop(self.tok_emb(idx) + self.pos_emb(pos))
pos_emb は 128 行の表で、0 行目は 0 番目の位置のベクトル、1 行目は 1 番目の位置のベクトル、というように続きます。これらのベクトルはほかのパラメータと同じく、学習で得られます。
足し合わせると、同じ文字でも位置が違えばベクトルが違うようになります。「白」が 1 番目の位置にあるときと 4 番目の位置にあるときでは、違う二つのベクトルになり、アテンションは順序を区別できるようになります。上の実験で、1 層のモデルに位置の埋め込みを加えると、二つの句の出力の差は 3.7e-08 から 2.8e-03 に変わりました。モデルはこの二つの句を区別できるのです(このモデルはまだ学習していないので、差は大きくなく、まだ意味もありません)。
このやり方には制限が一つあります。表が 128 行しかないので、モデルは最大 128 個の位置しか扱えません。学習中に見たことのない位置には、対応するベクトルがありません。
回転位置エンコーディング
2026 年 9 月時点で、主流のオープンソース LLM(Llama、通義千問、DeepSeek など)のほとんどは GPT-2 のような位置の埋め込みを使わず、回転位置エンコーディング(RoPE、2021 年の論文 RoFormer に由来)を使っています。
その考え方はこうです。位置を文字のベクトルに足すのではなく、アテンションのスコアを計算する前に、q と k をそれぞれの位置に応じた角度だけ「回転」させます。位置が後ろにあるほど、多く回転させます。そうすると、二つの位置の q と k の内積の結果は、両者がどれだけ離れているかだけに関係し、それぞれの絶対的な位置とは関係しなくなります。
「月」は「明」の 1 文字後ろにあり、この二文字が詩の冒頭に現れても末尾に現れても、両者の関係は同じです。相対的な位置のほうが絶対的な位置より言語の規則に合っていて、しかも学習時より長い文章にも一般化しやすいのです。
このコースの GPT は、最も簡単な位置の埋め込みを使い続けます。私たちの実験ではその効果で十分で、しかも理解しやすいからです。練習問題 3 で RoPE を実際に試せます。
練習問題
heads_positions.pyにもう一組加えてください。3 層のモデルで、位置の埋め込みがないとき、二つの句の出力はどれだけ違いますか。gpt.pyで学習させるモデルのヘッドを 4 つから 1 つ、8 つに変えて(n_embdは 128 のまま)、次の課のtrain.pyでそれぞれ 1000 ステップ学習させ、検証の損失を比べてください。- 挑戦:RoFormer の論文かオープンソースのモデルのコードを参考に、
CausalSelfAttentionに回転位置エンコーディングを加え、pos_embを取り除いて、学習の効果を見てください。
確認テスト
1. マルチヘッドアテンションはどうやっていますか?パラメータは増えますか?
各文字のベクトルをいくつかの区間に切り、区間ごとに一度ずつアテンションをして(それぞれ自分の q、k、v と重みを持つ)、結果をつなぎ戻し、線形層を通して混ぜ合わせます。パラメータの数はヘッドが一つのときと同じで、行列の切り方が違うだけです。利点は、各ヘッドが違うものに注目するよう学べることです。
2. アテンションに位置の情報が必要なのはなぜですか?
アテンションの採点と重み付きの和は、ベクトルの内容しか見ず、位置は見ません。1 層だけのモデルなら、前の文字の順序を入れ替えても、最後の位置の出力はまったく変わりません。しかし言語の意味は順序に依存するので、位置の情報を加える必要があります。
3. 実験で、位置の埋め込みのない 2 層のモデルでも二つの句の順序を区別できたのはなぜですか?
因果マスクによって各位置は自分と前の文字しか見られず、1 層目では位置ごとに見える文字の数が違うので、出力が順序に依存するようになります。2 層目がその出力を読むとき、間接的に位置の情報を得るのです。位置エンコーディングを加えない因果的な言語モデルでも位置の情報を学べるという研究がありますが、実際のモデルはやはり位置エンコーディングを明示的に加えています。
質問と議論
このレッスンでつまずいたところは、ここで質問してください。他の人の質問に答えるのも歓迎です。
質問で 3 ポイント、回答で 6 ポイント。審査を通過すると公開されます。
議論を読み込んでいます…