位置エンコーディングの理論 — sin/cos・RoPE・ALiBi

Self-Attentionには本質的な問題があります。入力トークンの順番を入れ替えても、出力が変わらないのです。「猫が犬を追いかけた」と「犬が猫を追いかけた」は全く異なる意味ですが、単語の集合としては同じであり、Self-Attention単体では区別できません。

Self-Attentionの計算 $\text{softmax}(\bm{Q}\bm{K}^\top/\sqrt{d_k})\bm{V}$ は、入力の順列に対して等変(equivariant)です。つまり、入力の順序を入れ替えると出力も同じ順序で入れ替わりますが、各位置の出力値自体は変わりません。

この問題を解決するのが位置エンコーディング(Positional Encoding)です。各トークンの「位置」の情報を入力に追加することで、Self-Attentionが順序を認識できるようにします。

原論文の正弦波とデファクトのRoPEを「なぜ移り変わったか」で比較したい方へ:本記事は各手法を実装まで詳しく扱う詳細記事です。「加算する絶対位置」から「回転する相対位置」への移行の理由を軸にした比較は、以下の記事にまとめています。

画像なし
位置エンコーディング比較:原論文の正弦波からデファクトのRoPEへ — なぜ絶対位置から回転位置に移ったか
Transformer原論文の正弦波位置エンコーディングと現在のLLMのデファクトRoPEを、移行の理由を軸に数式・図・Python実装で正面から比較。

位置エンコーディングを理解すると、以下のことが可能になります。

  • Transformerの完全理解: なぜ位置情報が必要で、どう実装されているか
  • 長文処理の設計: 訓練時より長い系列を処理するための外挿性の理解
  • 最新モデルの理解: LLaMAやGPT-4で使われるRoPE、ALiBiの原理
  • カスタムモデルの設計: タスクに応じた位置エンコーディングの選択

本記事の内容

  • なぜ位置エンコーディングが必要か
  • sin/cos 絶対位置エンコーディング(Vaswani et al., 2017)
  • 学習可能な位置埋め込み
  • RoPE(Rotary Position Embedding)
  • ALiBi(Attention with Linear Biases)
  • Pythonでの実装と比較

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

Sin/Cos 絶対位置エンコーディング

Vaswaniらの提案

原論文「Attention Is All You Need」(Vaswani et al., 2017)では、sin関数とcos関数を使った位置エンコーディングが提案されました。

位置 $\text{pos}$ の $i$ 番目の次元の値は

$$ \begin{align} \text{PE}(\text{pos}, 2i) &= \sin\left(\frac{\text{pos}}{10000^{2i/d}}\right) \\ \text{PE}(\text{pos}, 2i+1) &= \cos\left(\frac{\text{pos}}{10000^{2i/d}}\right) \end{align} $$

$d$ はモデルの次元、$i = 0, 1, \ldots, d/2-1$ です。

なぜsin/cosなのか

この設計には3つの重要な性質があります。

性質1: 各次元が異なる周波数を持つ

周波数 $\omega_i = 1/10000^{2i/d}$ は次元 $i$ が大きくなるほど小さくなります。$i = 0$ では波長 $2\pi$(最短)、$i = d/2 – 1$ では波長 $2\pi \times 10000$(最長)です。これにより、位置情報が粗い粒度(長距離)から細かい粒度(短距離)まで多スケールで表現されます。

性質2: 相対位置が線形変換で表せる

任意の固定オフセット $k$ に対して、$\text{PE}(\text{pos} + k)$ は $\text{PE}(\text{pos})$ の線形変換で表せます。

$$ \begin{pmatrix} \sin(\omega(\text{pos}+k)) \\ \cos(\omega(\text{pos}+k)) \end{pmatrix} = \begin{pmatrix} \cos(\omega k) & \sin(\omega k) \\ -\sin(\omega k) & \cos(\omega k) \end{pmatrix} \begin{pmatrix} \sin(\omega \cdot \text{pos}) \\ \cos(\omega \cdot \text{pos}) \end{pmatrix} $$

加法定理を適用すると、位置のシフトが回転行列で表現されます。これにより、モデルは相対位置を容易に学習できます。

性質3: 有界性

sin/cos の値は $[-1, 1]$ に収まるため、位置に依存して値が発散することがなく、学習が安定します。

入力への加算

位置エンコーディングは、トークン埋め込みに加算されます。

$$ \bm{z}_i = \bm{x}_i + \text{PE}(i) $$

加算であるため、トークンの意味情報と位置情報が共存します。モデルは学習を通じて、どの次元が位置情報を、どの次元が意味情報を主に担うかを自動的に分離します。

学習可能な位置埋め込み

BERTとGPTのアプローチ

BERT(Devlin et al., 2018)とGPT(Radford et al., 2018)は、sin/cosの代わりに学習可能な位置埋め込みを使用しています。

$$ \text{PE}(\text{pos}) = \bm{E}_\text{pos}[\text{pos}] \in \mathbb{R}^d $$

$\bm{E}_\text{pos} \in \mathbb{R}^{L_\text{max} \times d}$ は位置埋め込み行列で、$L_\text{max}$ は最大系列長です。

sin/cos vs 学習可能な埋め込み

性質 sin/cos 学習可能
パラメータ数 0(固定) $L_\text{max} \times d$
外挿性 理論上は任意長に対応 訓練時の最大長に限定
表現力 固定パターン タスクに適応
実用的な性能 学習可能とほぼ同等 sin/cosとほぼ同等

実験的には、両者の性能差はほとんどないことが報告されています。しかし、最大系列長を超える外挿が必要な場合は、sin/cosの方が有利です。

これらの絶対位置エンコーディングには、長系列への外挿が困難という共通の限界があります。次に、この問題を解決する相対位置エンコーディングの手法を見ていきましょう。

RoPE(Rotary Position Embedding)

相対位置をAttentionスコアに埋め込む

RoPE(Su et al., 2021)は、LLaMA、PaLM、Mistralなど現代の大規模言語モデルで広く採用されている位置エンコーディングです。

RoPEの核心的なアイデアは、QueryとKeyに位置依存の回転を適用することで、Attentionスコアが相対位置のみに依存するようにすることです。

位置 $m$ のQuery $\bm{q}_m$ と位置 $n$ のKey $\bm{k}_n$ に回転行列を適用します。

$$ \begin{align} \tilde{\bm{q}}_m &= R_\Theta(m) \bm{q}_m \\ \tilde{\bm{k}}_n &= R_\Theta(n) \bm{k}_n \end{align} $$

すると、Attentionスコアは

$$ \tilde{\bm{q}}_m^\top \tilde{\bm{k}}_n = \bm{q}_m^\top R_\Theta(m)^\top R_\Theta(n) \bm{k}_n = \bm{q}_m^\top R_\Theta(n – m) \bm{k}_n $$

回転行列の性質 $R(\theta)^\top R(\phi) = R(\phi – \theta)$ により、スコアは相対位置 $n – m$ のみに依存します。

回転の具体的な計算

2次元の場合、位置 $m$ での回転は

$$ R_\theta(m) = \begin{pmatrix} \cos(m\theta) & -\sin(m\theta) \\ \sin(m\theta) & \cos(m\theta) \end{pmatrix} $$

$d$ 次元では、ベクトルを2次元ペアに分割し、各ペアに異なる角速度の回転を適用します。

$$ R_\Theta(m) = \text{diag}(R_{\theta_1}(m), R_{\theta_2}(m), \ldots, R_{\theta_{d/2}}(m)) $$

角速度は $\theta_i = 10000^{-2i/d}$ で、sin/cos位置エンコーディングと同じ周波数スケジュールです。

RoPEの利点

外挿性: RoPEは相対位置を内積に直接エンコードするため、訓練時より長い系列にもある程度対応できます。NTK-awareスケーリングやYaRNなどの拡張手法と組み合わせて、さらに外挿性を改善できます。

入力埋め込みを変更しない: 位置情報はAttentionのQ, Kにのみ適用され、Valueや埋め込み層には影響しません。

ALiBi(Attention with Linear Biases)

スコアにバイアスを加算する

ALiBi(Press et al., 2021)は、位置エンコーディングを使わず、Attentionスコアに距離に比例するバイアスを加算するシンプルな手法です。

$$ \text{score}(i, j) = \bm{q}_i^\top \bm{k}_j – m \cdot |i – j| $$

$m$ はヘッドごとに異なる固定のスロープ(傾き)で、学習パラメータではありません。

$m$ の値は $h$ 個のヘッドに対して幾何数列で設定されます。

$$ m_i = 2^{-8i/h}, \quad i = 1, 2, \ldots, h $$

距離が離れるほどスコアが減少するため、近いトークンへの注目が自然に促されます。スロープが小さいヘッドは遠くまで見え、大きいヘッドは近くに集中します。

ALiBiの利点

外挿性が非常に高い: 学習時の最大長を大幅に超える系列でも性能が維持されます。バイアス項は単に距離に比例するだけなので、新しい距離にも自然に一般化されます。

追加パラメータなし: スロープ $m$ は固定値であり、学習パラメータが一切増えません。

実装の簡潔さ: Attentionスコアにバイアスを加算するだけで、既存のAttention実装をほとんど変更せずに適用できます。

Pythonでの実装と比較

import numpy as np
import matplotlib.pyplot as plt

np.random.seed(42)

# --- 各位置エンコーディングの実装と可視化 ---

def sinusoidal_pe(max_len, d_model):
    """Sin/Cos 位置エンコーディング"""
    PE = np.zeros((max_len, d_model))
    pos = np.arange(max_len)[:, None]
    div = np.exp(np.arange(0, d_model, 2) * -(np.log(10000.0) / d_model))
    PE[:, 0::2] = np.sin(pos * div)
    PE[:, 1::2] = np.cos(pos * div)
    return PE

def rope_rotation(q, k, positions, d_k):
    """RoPE: Query, Keyに回転を適用"""
    theta = 10000.0 ** (-2 * np.arange(d_k // 2) / d_k)
    q_rot = np.zeros_like(q)
    k_rot = np.zeros_like(k)

    for i, pos in enumerate(positions):
        angles = pos * theta
        cos_a = np.cos(angles)
        sin_a = np.sin(angles)

        q_even = q[i, 0::2]
        q_odd = q[i, 1::2]
        q_rot[i, 0::2] = q_even * cos_a - q_odd * sin_a
        q_rot[i, 1::2] = q_even * sin_a + q_odd * cos_a

        k_even = k[i, 0::2]
        k_odd = k[i, 1::2]
        k_rot[i, 0::2] = k_even * cos_a - k_odd * sin_a
        k_rot[i, 1::2] = k_even * sin_a + k_odd * cos_a

    return q_rot, k_rot

def alibi_bias(n_heads, seq_len):
    """ALiBi: Attention バイアス行列"""
    slopes = 2.0 ** (-8 * np.arange(1, n_heads + 1) / n_heads)
    biases = []
    for m in slopes:
        bias = np.zeros((seq_len, seq_len))
        for i in range(seq_len):
            for j in range(seq_len):
                bias[i, j] = -m * abs(i - j)
        biases.append(bias)
    return biases, slopes

# --- 可視化 ---
fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# (a) Sin/Cos PEのヒートマップ
ax = axes[0, 0]
d_model = 64
max_len = 50
PE = sinusoidal_pe(max_len, d_model)
im = ax.imshow(PE, cmap="RdBu_r", aspect="auto", vmin=-1, vmax=1)
ax.set_xlabel("Dimension", fontsize=11)
ax.set_ylabel("Position", fontsize=11)
ax.set_title("Sinusoidal Positional Encoding", fontsize=13)
plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)

# (b) Sin/Cosの内積(位置間の類似度)
ax = axes[0, 1]
similarity = PE @ PE.T
im = ax.imshow(similarity, cmap="viridis", aspect="auto")
ax.set_xlabel("Position j", fontsize=11)
ax.set_ylabel("Position i", fontsize=11)
ax.set_title("PE Similarity: PE(i) . PE(j)", fontsize=13)
plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)

# (c) RoPEの効果: 相対位置とスコアの関係
ax = axes[1, 0]
d_k = 16
seq_len = 30
Q = np.random.randn(seq_len, d_k) * 0.5
K = np.random.randn(seq_len, d_k) * 0.5

positions = np.arange(seq_len)
Q_rot, K_rot = rope_rotation(Q, K, positions, d_k)

scores_no_rope = Q @ K.T / np.sqrt(d_k)
scores_rope = Q_rot @ K_rot.T / np.sqrt(d_k)

# 位置0からの各位置へのスコア
ax.plot(range(seq_len), scores_no_rope[0], "o-", linewidth=1.5,
        markersize=3, alpha=0.7, label="Without RoPE")
ax.plot(range(seq_len), scores_rope[0], "s-", linewidth=1.5,
        markersize=3, alpha=0.7, label="With RoPE")
ax.set_xlabel("Key position", fontsize=11)
ax.set_ylabel("Attention score (position 0)", fontsize=11)
ax.set_title("RoPE: Attention Score from Position 0", fontsize=13)
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)

# (d) ALiBiのバイアスパターン
ax = axes[1, 1]
n_heads = 4
seq_len_alibi = 20
biases, slopes = alibi_bias(n_heads, seq_len_alibi)

for h in range(n_heads):
    ax.plot(range(seq_len_alibi), biases[h][0],
            linewidth=2, label=f"Head {h+1} (m={slopes[h]:.4f})")
ax.set_xlabel("Key position", fontsize=11)
ax.set_ylabel("Bias (from position 0)", fontsize=11)
ax.set_title("ALiBi: Attention Bias per Head", fontsize=13)
ax.legend(fontsize=9)
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig("positional_encoding.png", dpi=150, bbox_inches="tight")
plt.show()

この可視化から、各位置エンコーディング手法の特徴が理解できます。

  1. 左上(Sin/Cos PE): 低次元(左側)では高周波の正弦波、高次元(右側)では低周波の正弦波が観察されます。各位置が異なるパターンを持つため、位置の識別が可能です

  2. 右上(PE間の内積): 対角線(同じ位置同士)が最大で、距離が離れるほど内積が減少しています。ただし、sin/cosの周期性により、特定の距離で再び類似度が上がるパターンも見られます

  3. 左下(RoPEの効果): RoPEなし(青)のスコアはランダムに分布していますが、RoPEあり(オレンジ)のスコアは位置関係を反映した構造を持っています。RoPEにより、距離の近いトークン間のスコアが体系的に影響されています

  4. 右下(ALiBiのバイアス): 各ヘッドが異なるスロープを持つ線形バイアスを適用しています。Head 1(スロープが最大)は距離に対して急速にペナルティが増加し、Head 4(スロープが最小)は緩やかに増加します。これにより、異なるヘッドが異なるスケールの依存関係を捉えます

まとめ

本記事では、Transformerの位置エンコーディングをsin/cosからRoPE、ALiBiまで体系的に解説しました。

  • Self-Attentionは入力の順序に不変であるため、位置情報の明示的な導入が必要
  • Sin/Cos PE: 異なる周波数の正弦波で位置をエンコード。相対位置が線形変換で表現される美しい性質を持つ
  • 学習可能PE: タスクに適応できるが、訓練長を超える外挿が困難
  • RoPE: Q, Kに位置依存の回転を適用し、Attentionスコアが相対位置に依存するようにする。LLaMA, Mistralなどで採用
  • ALiBi: スコアに距離比例のバイアスを加算。パラメータ増加なし、高い外挿性

次のステップとして、以下の記事も参考にしてください。