「犬が猫を追う」と「猫が犬を追う」。使われている単語は全く同じなのに、意味は正反対です。私たちは語順でこの違いを読み取っています。ところが、Transformer の心臓部である Self-Attention は、実は単語の順番をまったく見ていません。入力を単なる「単語の集合」として扱うため、そのままでは上の2文を区別できないのです。
この弱点を、驚くほどシンプルな方法で補ったのが 2017 年の論文「Attention Is All You Need」(Vaswani ら) でした。彼らのアイデアは、各単語の埋め込みベクトルに「位置ごとに決まった波の模様」を足し込むというもの。これが正弦波位置エンコーディング (sinusoidal positional encoding) です。
位置エンコーディングを理解すると、次のようなことが見えてきます。
- Transformer の全体像: なぜ Attention だけでは足りず、位置情報を「足し算」で注入するのか
- 長文への強さ: 訓練で見たより長い文でも、なぜ正弦波なら位置を計算できるのか(外挿性)
- 後続手法の土台: RoPE や ALiBi といった現代的な位置表現も、すべてこの正弦波の発想を出発点にしている
本記事の内容
- なぜ位置情報が必要なのか(Self-Attention が順序を見ない理由)
- 正弦波位置エンコーディングの数式と、その「気持ち」
- 時計・二進数カウンタのアナロジーで掴む直感
- 具体的な数値例で「位置ごとのベクトル」を実際に見る
- 相対位置が回転(線形変換)で表せることの意味
- Python での実装と可視化
数式を追う前に、まずは絵で全体像をつかみましょう。

やることは本当にこれだけです。各単語の埋め込みベクトル(青)に、その位置に応じた位置エンコーディング(赤)を足し合わせ、位置つき埋め込み(緑)を作る。ベクトルの次元数は変わりません。この「足すだけ」の操作で、Attention が順序を認識できるようになります。以降、なぜこれで上手くいくのかを一つずつ解きほぐしていきます。
前提知識
この記事を読む前に、以下の記事に目を通しておくと理解がスムーズです。
なぜ位置情報が必要なのか
まずは「Self-Attention が順序を見ない」という事実を、腹の底から納得しておきましょう。ここが位置エンコーディングの出発点だからです。
Self-Attention は、各単語について「他のどの単語にどれだけ注目するか」を内積で計算し、その重みで値ベクトルを混ぜ合わせます。ポイントは、この計算に「その単語が何番目か」という情報がどこにも入っていないことです。単語ベクトルの集まりから、どの2つの内積を取るかを総当たりで見るだけ。順番はまったく考慮されません。
具体的に見てみましょう。

「犬 が 猫 を 追う」も「猫 が 犬 を 追う」も、単語の集合としては {犬, が, 猫, を, 追う} でまったく同じです。Self-Attention は集合演算なので、入力の順番を並べ替えると出力も同じ順番で並べ替わるだけ(この性質を置換等変性と呼びます)。各位置での出力の中身は変わりません。つまり素の Self-Attention には、「犬」が主語なのか目的語なのかを見分ける手がかりが原理的に存在しないのです。
一方、RNN(再帰型ニューラルネット)は単語を1つずつ順番に読み込むので、順序は構造そのものに刻まれていました。Transformer は並列計算のためにこの逐次処理を捨てた代わりに、順序情報を失ってしまった。そこで位置情報を明示的に外から与える必要が出てきます。
ではどうやって与えるか。最も素直なのは「各単語ベクトルに位置番号を表すベクトルを足す」ことです。問題は「位置をどんなベクトルで表すか」。ここで正弦波の出番になります。次のセクションで、その具体的な形を見ていきましょう。
正弦波位置エンコーディングの定義
いきなり数式を出す前に、設計者が何を欲しかったのかを想像してみます。位置を表すベクトルには、こんな性質があると嬉しいはずです。
- 位置ごとに違う値になる(位置0と位置1が同じでは困る)
- 値が有界(際限なく大きくならない。単語埋め込みに足すので暴れてほしくない)
- 近い位置は近いベクトルになる(位置5と位置6は似ていて、位置5と位置50は遠い)
- どんな長さの文にも対応できる(訓練で見なかった長い位置でも計算できる)
「位置ごとに違い、有界で、滑らかに変化する関数」——ここで sin と cos が自然に候補に挙がります。Vaswani らは、次のように定義しました。
$$ \begin{equation} \begin{aligned} PE_{(pos,\, 2i)} &= \sin\!\left(\frac{pos}{10000^{2i/d_{\text{model}}}}\right) \\[4pt] PE_{(pos,\, 2i+1)} &= \cos\!\left(\frac{pos}{10000^{2i/d_{\text{model}}}}\right) \end{aligned} \end{equation} $$
記号の意味を確認します。
- $pos$:単語の位置(0, 1, 2, …)
- $i$:次元のインデックス($0, 1, \dots, d_{\text{model}}/2 – 1$)
- $d_{\text{model}}$:埋め込みの次元数(原論文では 512)
- $10000$:波長のスケールを決める定数
ベクトルの偶数番目の次元 $2i$ には sin、奇数番目の次元 $2i+1$ には cos を割り当てます。つまり次元を2つずつペアにして、同じ周波数の sin と cos を並べているわけです。この「ペアで持つ」ことが後で効いてきます。
一番のポイントは、分母の $10000^{2i/d_{\text{model}}}$ です。次元 $i$ が大きくなるほど分母が大きくなり、$pos$ を割る量が増えます。角度 $\theta = pos / 10000^{2i/d_{\text{model}}}$ の変化がゆっくりになる——つまり次元が深くなるほど波長が長く(周波数が低く)なるのです。
まず、この行列全体を眺めてみましょう。

縦軸が次元、横軸が位置です。上(浅い次元)ほど色が細かく縞状に変化し、下(深い次元)ほどゆったりと変化しているのが見て取れます。各位置(縦の1列)は固有の色パターンを持っており、これがその位置の「指紋」になります。位置が違えば指紋が違い、位置が近ければ指紋も似る。これが先ほど欲しかった性質そのものです。
なぜ「浅い次元=高周波、深い次元=低周波」という設計が効くのか。次のセクションで、次元ごとの波を1本ずつ取り出して確認します。
なぜ sin/cos なのか — 多重周波数という発想
先ほどのヒートマップを、次元ごとに1本の波として描き直してみます。

次元0(赤)は波長が約6と非常に短く、位置が1進むだけで大きく値が変わります。一方、次元60(青)は波長が数千に及び、位置100の範囲ではほとんど直線的にしか変化しません。浅い次元は「位置の細かい違い」を、深い次元は「位置の大きな違い」を担当しているわけです。
この「複数の周波数を組み合わせて1つの数を表す」という発想は、実はとても身近なものと同じ構造をしています。二進数のカウンタです。

左は0から15までの整数を二進数で並べたものです。一番下のビット(bit0)は1ずつ増えるたびに 0→1→0→1 と目まぐるしく反転します。一つ上のビットは2回に1回、さらに上は4回に1回…と、上位ビットほどゆっくり切り替わる。この「桁ごとに違う速さで変化するパターンの組み合わせ」で、一つの整数を一意に表現しているのです。
右は正弦波PEの浅い8次元です。浅い次元ほど速く色が変わり、深い次元ほどゆっくり変わる——構造がそっくりですね。正弦波位置エンコーディングは、いわば「連続版の二進数カウンタ」なのです。二進数が 0/1 の離散ビットで整数を表すのに対し、正弦波は $-1$ から $+1$ まで滑らかに変化する連続値で位置を表す。滑らかであることには理由があり、近い位置どうしのベクトルが近くなる(性質3)ため、モデルが位置の遠近を扱いやすくなります。
ここまでで「なぜ周波数を混ぜるのか」は掴めました。では sin と cos をペアで持つことにどんな意味があるのか。ここに正弦波PEの最も美しい性質が隠れています。それを見る前に、まず具体的な数値でベクトルの中身を確認しておきましょう。
具体例 — 各位置のベクトルを実際に計算する
抽象的な話が続いたので、実際に数値を入れて位置ベクトルを作ってみます。$d_{\text{model}} = 128$ として、先頭12次元だけを取り出します。
まず $pos = 0$ のとき。角度 $\theta = 0 / (\text{何か}) = 0$ なので、すべての次元で
$$ \sin(0) = 0, \quad \cos(0) = 1 $$
となります。つまり位置0のベクトルは $(0, 1, 0, 1, 0, 1, \dots)$ という、sin と cos が交互に並んだきれいなパターンです。
次に $pos = 1$。最も浅い次元ペア($i=0$)では分母が $10000^{0} = 1$ なので $\theta = 1/1 = 1\,\text{rad}$。よって
$$ \sin(1) \approx 0.841, \quad \cos(1) \approx 0.540 $$
深い次元ほど分母が大きく $\theta$ が0に近づくので、$\sin \approx 0$、$\cos \approx 1$ のまま。位置が1つ進んだとき、まず浅い次元から変化が始まるのがポイントです。数値をグラフにしてみます。

$pos=0$(青)は、偶数次元(sin)がすべて0、奇数次元(cos)がすべて1で、階段状に並んでいます。位置が $1, 2, 3$ と進むにつれて、まず左側(浅い次元)が大きく動き、右側(深い次元)はほとんど動きません。深い次元は周波数が低いので、位置が数個変わったくらいでは値がほぼ変わらないのです。この「浅い次元が細かく、深い次元がゆっくり」という役割分担が、ベクトルの中身のレベルで確認できます。
さて、ここまでは各次元を独立に見てきました。しかし sin と cos をペアにすると、2次元平面上での鮮やかな幾何学的意味が現れます。次のセクションが本記事のクライマックスです。
相対位置が「回転」で表せる — 正弦波PEの核心
sin と cos をペアで持つことの意味を考えます。次元ペア $(2i, 2i+1)$ を、2次元平面上の点 $(\sin\theta, \cos\theta)$ とみなしてみましょう。$\theta = pos / 10000^{2i/d_{\text{model}}}$ ですから、$pos$ が増えると $\theta$ が増え、この点は単位円の上を回転していきます。
つまり各次元ペアは「一定の速さで回る時計の針」なのです。

4つの次元ペアについて、位置0から7までの針の向きを描きました。左端の次元ペア0/1は1トークン進むごとに約57度も回る「速い針」。右へ行くほど針の回転はゆっくりになり、次元60/61ではほとんど動きません。Transformer は複数の速さで回る針を同時に持つことで、時・分・秒針のように多重の分解能で位置を表しているのです。
この「回る針」という見方から、決定的な性質が導けます。位置 $pos$ の針の角度を $\theta$ とすると、位置 $pos+k$ の針の角度は $\theta + k\omega$($\omega$ は1歩あたりの回転角)。つまり $k$ 個先の位置は、今の針を一定角 $k\omega$ だけ回したものにすぎません。

これを数式で確認します。回転は行列で書けるのでした。加法定理
$$ \begin{aligned} \sin(\theta + k\omega) &= \sin\theta\cos(k\omega) + \cos\theta\sin(k\omega) \\ \cos(\theta + k\omega) &= \cos\theta\cos(k\omega) – \sin\theta\sin(k\omega) \end{aligned} $$
を行列の形にまとめると、
$$ \begin{equation} \begin{pmatrix} \sin(\theta + k\omega) \\ \cos(\theta + k\omega) \end{pmatrix} = \underbrace{\begin{pmatrix} \cos(k\omega) & \sin(k\omega) \\ -\sin(k\omega) & \cos(k\omega) \end{pmatrix}}_{\text{回転行列 } R_k} \begin{pmatrix} \sin\theta \\ \cos\theta \end{pmatrix} \end{equation} $$
ここで注目すべきは、回転行列 $R_k$ の中身が $k$(相対距離)だけで決まり、$pos$(絶対位置)を含まないことです。言い換えると、
$$ PE_{pos+k} = R_k \cdot PE_{pos} $$
という関係が、どの絶対位置 $pos$ からでも同じ行列 $R_k$ で成り立ちます。「$k$ 個隣を見る」という操作が、位置に依らない固定の線形変換で表現できる。これはモデルにとって非常に扱いやすい性質です。というのも、Attention の内部は行列の掛け算(線形変換)でできているため、「$k$ 語隣に注目する」パターンを重み行列として素直に学習できるからです。
この回転構造は、後の RoPE(回転位置埋め込み) が「足す」のではなく「クエリとキーを直接回す」方向に推し進めたアイデアの原型でもあります。正弦波PEを理解しておくと、現代的な位置表現の系譜がすっと繋がります。
回転で相対位置が表せることのもう一つの帰結が、内積の振る舞いです。次に見てみましょう。
内積が相対距離の物差しになる
Attention はベクトルの内積で「どれだけ関係が深いか」を測ります。では位置エンコーディングどうしの内積 $PE_{pos} \cdot PE_{pos+k}$ はどうなるでしょうか。
各次元ペアは単位ベクトル $(\sin\theta, \cos\theta)$ で、$k$ 個先はそれを $k\omega$ 回転したもの。2つの単位ベクトルの内積は、その間の角度の余弦なので、次元ペアごとに $\cos(k\omega)$ になります。全次元ペアで足し合わせると、
$$ PE_{pos} \cdot PE_{pos+k} = \sum_{i} \cos\!\left(k\, \omega_i\right), \qquad \omega_i = \frac{1}{10000^{2i/d_{\text{model}}}} $$
右辺に $pos$ が一切現れないことに注目してください。内積は絶対位置に依らず、相対距離 $k$ だけで決まるのです。実際に計算して確かめます。

基準位置を $pos = 5, 20, 40$ の3通りで変えて、$k$ 個先との内積を描きました。驚くべきことに、3本の線が完全に重なって1本にしか見えません。基準位置がどこであっても、内積は距離 $k$ だけの関数になっているという先ほどの式が、そのまま可視化されています。しかも $k=0$(同じ位置)で最大値をとり、離れるほど(多少の波打ちを伴いつつ)減衰していく。つまり内積が「2つの位置がどれだけ近いか」を測る物差しとして機能しているのです。
近い位置ほど内積が大きい——この性質があるおかげで、Attention は「近くの単語ほど関係しやすい」といった位置的な傾向を自然に学習できます。
ここまでで正弦波PEの数学的な美点は出そろいました。あとは、これを実際の単語埋め込みにどう組み込むかです。
埋め込みへの加算と、長文への外挿性
位置エンコーディングは、単語埋め込みに足し算で組み込みます。「連結(concat)ではなく加算で情報が混ざって大丈夫なのか」と不安になるかもしれませんが、高次元空間では埋め込みとPEが概ね別々の方向を使うため、モデルは両者を分離して読み取れることが知られています。加算なら次元数が増えず計算コストも変わらない、というのも利点です。

左がランダムな単語埋め込み(位置情報なし、どの行も似たようなノイズ)、中央が位置エンコーディング、右がその和です。加算後は、もとの埋め込みの情報を保ちつつ、位置ごとに固有の縞模様が重ね書きされているのがわかります。次元数(横幅)は変わっていません。この足し算だけで、Attention は各トークンの位置を識別できるようになります。
正弦波PEにはもう一つ実用上の強みがあります。学習で見なかった長さの文にも対応できることです。位置エンコーディングを「学習可能なパラメータ」として持つ方式(BERT などが採用)だと、訓練時に用意した最大長を超える位置には値が存在せず、長文で破綻します。一方、正弦波は決まった数式なので、どんな大きな $pos$ でも計算できます。

オレンジの帯が訓練で見た範囲(位置0〜100)だとしても、その先の位置200でも300でも、正弦波は同じ数式でそのまま値を返します。もっとも、実際には周波数の組み合わせパターンが訓練範囲外で新奇になるため、外挿が完璧に効くわけではありません。この長文外挿をより強くしようとする試みが、後の ALiBi や RoPE といった手法につながっていきます。
理屈は十分に見てきました。最後に、これまでの図をすべて生成したコードをまとめて示します。
Pythonでの実装
正弦波位置エンコーディングは数十行で実装できます。まず本体を書き、これまでの性質を数値で確かめましょう。
import numpy as np
def positional_encoding(length, d_model, n=10000.0):
"""PE[pos, 2i]=sin(pos/n^(2i/d)), PE[pos, 2i+1]=cos(pos/n^(2i/d))"""
pe = np.zeros((length, d_model))
pos = np.arange(length)[:, None] # (length, 1)
i = np.arange(0, d_model, 2)[None, :] # 0,2,4,... の偶数次元
div = np.power(n, i / d_model) # 分母 n^(2i/d)
pe[:, 0::2] = np.sin(pos / div) # 偶数次元は sin
pe[:, 1::2] = np.cos(pos / div) # 奇数次元は cos
return pe
PE = positional_encoding(100, 128)
print("PE の形状:", PE.shape)
print("pos=0 の先頭6次元:", np.round(PE[0, :6], 3))
print("pos=1 の先頭6次元:", np.round(PE[1, :6], 3))
このコードの出力は次のようになります。pos=0 では [0, 1, 0, 1, 0, 1](sin=0, cos=1 が交互)、pos=1 では [0.841, 0.54, 0.762, 0.648, 0.682, 0.732] のように浅い次元から値が動き始めます。手計算した $\sin(1)\approx0.841$, $\cos(1)\approx0.540$ と一致しており、定義どおりに実装できていることが確認できます。
次に、本記事の核心である「内積が相対距離だけで決まる」性質をコードで検証します。
# 基準位置を変えても、内積が k だけで決まるか確認
for base in [5, 20, 40]:
dots = [PE[base] @ PE[base + k] for k in range(5)]
print(f"pos={base:2d} からの内積(k=0..4):", np.round(dots, 3))
出力は3行とも [64.0, 62.094, 57.382, 52.186, 48.586] と、まったく同じ数値の並びになります。基準位置 $pos$ を 5, 20, 40 と変えても内積の系列がぴたりと一致する——これが図9で3本の線が重なって見えた理由です。絶対位置に依らず相対距離 $k$ だけで内積が決まる、という数式の結論が数値でも裏付けられました。
最後に、相対位置が回転行列で表せることを確かめます。
d_model = 128
i = 20 # ある次元ペアを選ぶ
omega = 1.0 / np.power(10000.0, 2*i/d_model) # このペアの角速度
k = 9 # 相対距離
R = np.array([[np.cos(k*omega), np.sin(k*omega)],
[-np.sin(k*omega), np.cos(k*omega)]])
pos = 15
v_pos = np.array([np.sin(pos*omega), np.cos(pos*omega)]) # PE(pos)
v_posk = np.array([np.sin((pos+k)*omega), np.cos((pos+k)*omega)]) # PE(pos+k)
print("回転で作った PE(pos+k):", np.round(R @ v_pos, 4))
print("直接計算した PE(pos+k):", np.round(v_posk, 4))
2行の出力は小数第4位まで完全に一致します。つまり $PE_{pos+k} = R_k \cdot PE_{pos}$ が、実際の数値でも成り立っている。相対位置のシフトが、絶対位置に依らない固定の回転行列で表せることを、手を動かして確認できました。
PyTorch のモデルに組み込む場合は、次のようにバッファとして持たせ、埋め込みに足すだけです。
import torch, torch.nn as nn
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=5000):
super().__init__()
pe = torch.zeros(max_len, d_model)
pos = torch.arange(max_len).unsqueeze(1).float()
div = torch.pow(10000.0, torch.arange(0, d_model, 2).float() / d_model)
pe[:, 0::2] = torch.sin(pos / div)
pe[:, 1::2] = torch.cos(pos / div)
self.register_buffer("pe", pe) # 学習しないパラメータとして保持
def forward(self, x): # x: (batch, seq_len, d_model)
return x + self.pe[:x.size(1)] # 位置エンコーディングを加算
pe = PositionalEncoding(128)
x = torch.zeros(2, 10, 128) # ダミーの埋め込み(バッチ2, 長さ10)
print("出力の形状:", pe(x).shape)
register_buffer を使うのがポイントです。正弦波PEは学習不要(勾配を持たない)なので、モデルのパラメータではなくバッファとして登録します。出力形状は入力と同じ (2, 10, 128) で、埋め込みに位置情報が足されただけで次元は変わりません。これが Transformer の各層の入り口で行われている処理そのものです。
まとめ
本記事では、「Attention Is All You Need」の正弦波位置エンコーディングを、具体例と図で解きほぐしました。
- なぜ必要か: Self-Attention は入力を集合として扱い順序を見ないため、位置情報を外から足し込む必要がある
- 定義: 次元ペアごとに周波数を変えた sin/cos を並べる。浅い次元は高周波(細かい位置)、深い次元は低周波(大きな位置)を担当する
- 直感: 「連続版の二進数カウンタ」であり、また「多重の速さで回る時計の針」でもある
- 核心: sin と cos をペアにすることで、$k$ 個先の位置が絶対位置に依らない回転行列 $R_k$ で表せる($PE_{pos+k}=R_k\,PE_{pos}$)。その帰結として内積が相対距離だけで決まる
- 実用性: 埋め込みに足すだけで次元が増えず、決定的な数式なので長文にも外挿できる
正弦波PEは「位置を絶対的に表す」古典的手法ですが、そこで見た回転で相対位置を表すという発想は、現代の位置表現へまっすぐ受け継がれています。次のステップとして、以下の記事も参考にしてください。