RoPEとは?

RoPE概要
自然言語処理の基盤となったTransformerは、単語の並び順(位置情報)を認識できないため、何らかの方法で位置情報を付与する必要があります。初期のTransformer(BERTやGPT-2など)では、各位置に固定のベクトルをそのまま足し算する「絶対位置埋め込み(Absolute Position Embedding)」が採用されました。この方式は計算がシンプルで高速というメリットがあるものの、単語同士の「相対的な距離(2つの単語がどれくらい離れているか)」をモデルが直接捉えるのが苦手でした。また、モデルの学習時よりも長い文章(未知の長さの入力)が与えられると、対応する位置ベクトルが存在しないために著しく精度が低下するという、外挿性(長文への対応力)の低さが大きな課題となっていました。
この課題を解決するため、単語間の距離に注目して計算する「相対位置埋め込み(Relative Position Embedding)」も提案されました。しかし、この方式はアテンション(Attention)の計算途中で複雑な位置情報の操作を挟み込む必要があり、数千〜数万トークンといった長文を扱うLLMにおいては、計算量とメモリ消費量が膨大になり、学習や推論の速度を著しく低下させるというデメリットがありました。
RoPEは、単語ベクトルを位置に応じた角度だけ回転させる「回転位置埋め込み」を取り入れることで、従来の課題を美しく解決しました。実装上は各単語を個別に回転させる「絶対位置」の手軽さを保ちながら、アテンション(内積)を計算する段階になると、数学的に自動で単語間の「相対位置」へと変換される仕組みを実現したのです。これにより、計算コストを極限まで抑えつつ、「近くの単語同士は強く結びつき、遠くの単語は緩やかに減衰する」という言語の自然な性質をモデルに学習させることが可能となり、現在のLLMにおける劇的なコンテキストウィンドウ(扱える文脈長)の長大化へと貢献しました。
各位置埋め込み方式の比較
Queryベクトルを (位置 )、Keyベクトルを (位置 )とし、Attentionで最も重要な内積()がどのように変化するかを見ます。
① 絶対位置埋め込み (Absolute Position Embedding)
単語ベクトル に位置ベクトル を足し算します。
-
QueryベクトルとKeyベクトルの位置ベクトルの加算:
-
AttentionによるQueryベクトルとKeyベクトルの内積:
内積を展開すると「単語×単語」「単語×位置」「位置×位置」の4つの項が出現します。位置 と がバラバラに計算されるため、「 と の距離(相対位置)」が直接計算されません。
② 相対位置埋め込み (Relative Position Embedding - Shaw等)
位置の「差」に応じた埋め込みベクトル をアテンションスコアに後から足し算(または掛け算)します。
- アテンションスコア(内積)への位置ベクトルの加算:
相対距離 を直接関数やルックアップテーブルから取得して足すため正確ですが、Attentionの行列計算の途中でループ処理やインデックス操作を挟む必要があり、計算効率が悪化します。
③ RoPE (Rotary Position Embedding)
ベクトルを2次元ずつのペアに分け、位置 に応じた角度 で回転(行列の掛け算)させます。
-
QueryベクトルとKeyベクトルの回転(※ は位置 に応じた回転行列。):
-
AttentionによるQueryベクトルとKeyベクトルの内積:
転置の基本性質
より
回転前のQueryベクトルとKeyベクトルを、とすると、
回転行列の性質により、内積を計算すると自動的に角度の差(相対距離 )の回転だけが残ります。 個別の単語を回転させるだけ(絶対位置の実装)で、結果は相対位置になるという美しい数式です。
コードによる挙動の確認
PyTorchのコードを使って、それぞれの位置埋め込みがベクトルの内積(アテンションスコア)にどのような影響を与えるのか、実際に見ていきましょう。
まずはQueryベクトルとKeyベクトルを定義します。
import torch
import torch.nn as nn
import math
# 実験設定
dim = 64 # 64次元ベクトル
pos_m = 2 # Queryの位置
pos_n1 = 3 # Key位置1 (距離 1)
pos_n2 = 5 # Key位置2 (距離 3)
pos_n3 = 20 # Key位置3 (距離 18)
# テスト用のクエリとキー(全く同じベクトルにする)
x_q = torch.ones(dim)
x_k = torch.ones(dim)
print("--- 準備 ---")
print(f"Queryベクトル: {x_q}")
print(f"Keyベクトル : {x_k}\n")
実行結果
--- 準備 ---
Queryベクトル: tensor([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
1., 1., 1., 1., 1., 1., 1., 1., 1., 1.])
Keyベクトル : tensor([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
1., 1., 1., 1., 1., 1., 1., 1., 1., 1.])
絶対位置埋め込み (Absolute Position Embedding)
以下のコードでは、PyTorchのnn.Embeddingを用いて、古典的な「絶対位置埋め込み」をシミュレートします。各位置に対応する学習可能なベクトルを生成し、それをQueryおよびKeyベクトルに直接加算します。そして、位置の異なる複数のKeyベクトルとQueryベクトルとの内積(アテンションスコア)を計算することで、単語間の距離とスコアの関連性がどのように表れるかを確認します。
torch.manual_seed(3)
pos_emb_abs = nn.Embedding(21, dim)
nn.init.normal_(pos_emb_abs.weight)
# 位置ベクトルをそのまま足し算
q_abs = x_q + pos_emb_abs(torch.tensor(pos_m))
k_abs_n1 = x_k + pos_emb_abs(torch.tensor(pos_n1))
k_abs_n2 = x_k + pos_emb_abs(torch.tensor(pos_n2))
k_abs_n3 = x_k + pos_emb_abs(torch.tensor(pos_n3))
score_abs_1 = torch.dot(q_abs, k_abs_n1).item()
score_abs_2 = torch.dot(q_abs, k_abs_n2).item()
score_abs_3 = torch.dot(q_abs, k_abs_n3).item()
print(f"距離1 (位置 {pos_m}x{pos_n1}) のスコア: {score_abs_1:.4f}")
print(f"距離3 (位置 {pos_m}x{pos_n2}) のスコア: {score_abs_2:.4f}")
print(f"距離18 (位置 {pos_m}x{pos_n3}) のスコア: {score_abs_3:.4f}")
print("❌ 課題: 距離が開いてもスコアが不規則(位置ベクトルの相性だけで決まる)。\n")
実行結果
距離1 (位置 2x3) のスコア: 48.2677
距離3 (位置 2x5) のスコア: 58.1076
距離18 (位置 2x20) のスコア: 53.2920
❌ 課題: 距離が開いてもスコアが不規則(位置ベクトルの相性だけで決まる)。
まず、torch.manual_seed(3)で乱数のシードを固定し、再現性を確保します。 pos_emb_abs = nn.Embedding(21, dim)では、位置番号(0から20まで)を、それぞれdim次元のベクトルに変換するための埋め込み層を作成します。これは、各位置にユニークなベクトルを割り当てるための辞書のようなものです。nn.init.normal_(pos_emb_abs.weight)で、これらの位置ベクトルの初期値を正規分布に従う乱数で初期化しています。
次に、q_abs = x_q + pos_emb_abs(torch.tensor(pos_m))のように、元の単語ベクトルx_qに対して、その位置pos_mに対応する位置ベクトルをpos_emb_absから取り出して加算します。これをQueryベクトルと、3つの異なる位置にあるKeyベクトル(k_abs_n1, k_abs_n2, k_abs_n3)に対して行います。
最後に、torch.dot(q_abs, k_abs_n1).item()で、Queryベクトルと各Keyベクトルとの内積を計算します。これがアテンションスコアに相当します。結果を見ると、Keyベクトルとの距離(1, 3, 18)が大きくなっても、スコアが規則的に増減せず、ランダムな値を示しています。これは、絶対位置埋め込みでは、2つの位置ベクトルの内積がその位置の組み合わせのみに依存し、相対的な距離が考慮されないためです。
相対位置埋め込み (Relative Position Embedding - Shaw等)
以下のコードでは、PyTorchを用いてShaw等による「相対位置埋め込み」を簡易的に実装・検証します。ベースとなる単語間の内積に対して、QueryとKeyの相対距離に応じた学習可能なバイアスをnn.Embeddingから取得して加算し、距離の違いがアテンションスコアにどのように反映されるかを確認します。
# 相対距離(-20 から +20 など)に応じた埋め込み辞書を用意
# 本来は距離の上限(clip)を設けますが、簡易化のためそのまま保持
rel_emb = nn.Embedding(41, 1) # 出力はスコアに足すためのスカラー(1次元)
nn.init.normal_(rel_emb.weight)
# ベースとなる単語同士の内積(位置なし)
base_score = torch.dot(x_q, x_k).item()
# 相対距離 (n - m) をインデックスに変換して足し算する
# 例: 距離0をインデックス10とする
def get_rel_score(base, m, n):
dist = n - m
idx = torch.tensor(dist + 20) # 負の数を防ぐオフセット
bias = rel_emb(idx).item()
return base + bias
score_rel_1 = get_rel_score(base_score, pos_m, pos_n1)
score_rel_2 = get_rel_score(base_score, pos_m, pos_n2)
score_rel_3 = get_rel_score(base_score, pos_m, pos_n3)
print(f"距離1 (位置 {pos_m}x{pos_n1}) のスコア: {score_rel_1:.4f}")
print(f"距離3 (位置 {pos_m}x{pos_n2}) のスコア: {score_rel_2:.4f}")
print(f"距離18 (位置 {pos_m}x{pos_n3}) のスコア: {score_rel_3:.4f}")
print("⭕ 利点: 距離に応じた正確なバイアスを足せる。")
print("❌ 課題: Attention行列(全単語ペア)の計算中に、この『距離の引き算とルックアップ』を挟むため重い。\n")
実行結果
距離1 (位置 2x3) のスコア: 63.5860
距離3 (位置 2x5) のスコア: 63.9219
距離18 (位置 2x20) のスコア: 63.3074
⭕ 利点: 距離に応じた正確なバイアスを足せる。
❌ 課題: Attention行列(全単語ペア)の計算中に、この『距離の引き算とルックアップ』を挟むため重い。
まず、rel_emb = nn.Embedding(41, 1)で、相対的な位置の差(-20から+20まで)を、それぞれ学習可能な1次元のバイアス値に変換するための埋め込み層を作成します。これは、距離ごとに異なるスコア調整値を格納する辞書のような役割を果たします。その重みはnn.init.normal_で正規分布に従う乱数で初期化されます。
base_score = torch.dot(x_q, x_k).item()では、位置情報を全く考慮しない、純粋な単語ベクトル同士の内積を計算し、これをアテンションの基本スコアとします。
get_rel_score関数がこの方式の核となります。Queryの位置mとKeyの位置nを受け取り、まず相対距離dist = n - mを計算します。この距離は負の値も取るため、dist + 20のようにオフセットを加えて埋め込み層のインデックスとして使える非負の整数に変換します。そして、このインデックスidxを使ってrel_emb(idx)を呼び出し、対応するバイアス値を取得します。最後に、このバイアス値を基本スコアbaseに加算することで、距離情報が反映された最終的なアテンションスコアを算出します。
結果を見ると、絶対位置埋め込みとは異なり、スコアは距離に応じて何らかの値が加算されていますが、この加算処理をすべてのアテンション計算中に行う必要があるため、計算コストが高いという課題があります。
回転位置埋め込み (Rotary Position Embedding)
以下のコードでは、PyTorchとPythonのmathライブラリを用いて、RoPE(回転位置埋め込み)のコアロジックを実装します。ベクトルを2次元ずつのペアに分割し、位置に応じて異なる速度で回転させる関数を定義して、実際にベクトルを回転させ、その内積(アテンションスコア)を計算します。
def apply_rope_correct(v, pos, dim):
rotated_v = torch.zeros_like(v)
# 2次元(1ペア)ずつループ
for i in range(0, dim, 2):
# 論文の定義に基づく、このペアの回転スピード(theta_i)の計算
# インデックスはペア単位なので i ではなく i // 2
pair_idx = i // 2
theta_i = 10000 ** (- (2 * pair_idx) / dim)
# 現在の位置(pos)に応じた最終的な回転角度
m_theta = pos * theta_i
# 回転処理
x = v[i]
y = v[i+1]
rotated_v[i] = x * math.cos(m_theta) - y * math.sin(m_theta)
rotated_v[i+1] = x * math.sin(m_theta) + y * math.cos(m_theta)
return rotated_v
# 各位置で回転を適用(プリント出力でスピードの差がわかります)
q_rope = apply_rope_correct(x_q, pos_m, dim)
k_rope_n1 = apply_rope_correct(x_k, pos_n1, dim)
k_rope_n2 = apply_rope_correct(x_k, pos_n2, dim)
k_rope_n3 = apply_rope_correct(x_k, pos_n3, dim)
# アテンションスコア(内積)の計算
score_rope_1 = torch.dot(q_rope, k_rope_n1).item()
score_rope_2 = torch.dot(q_rope, k_rope_n2).item()
score_rope_3 = torch.dot(q_rope, k_rope_n3).item()
print("\n--- Attention スコア(内積)の結果 ---")
print(f"距離1 (位置 {pos_m}x{pos_n1}) のスコア: {score_rope_1:.4f}")
print(f"距離3 (位置 {pos_m}x{pos_n2}) のスコア: {score_rope_2:.4f}")
print(f"距離18 (位置 {pos_m}x{pos_n3}) のスコア: {score_rope_3:.4f}")
print("🏆 結論: 事前に単語を回転させただけなのに、距離が開くほどスコアが綺麗に減衰している!")
実行結果
--- Attention スコア(内積)の結果 ---
距離1 (位置 2x3) のスコア: 61.8337
距離3 (位置 2x5) のスコア: 51.1741
距離18 (位置 2x20) のスコア: 40.5986
🏆 結論: 事前に単語を回転させただけなのに、距離が開くほどスコアが綺麗に減衰している!
apply_rope_correct関数は、RoPEの中核となる回転処理を実装しています。入力ベクトルv、その位置pos、ベクトルの次元dimを受け取ります。
for i in range(0, dim, 2):のループで、ベクトルを2次元ずつのペアで処理します。各ペアに対して、theta_i = 10000 ** (- (2 * pair_idx) / dim)という式で、回転の速度(周波数)を計算します。ここでpair_idxはペアのインデックスで、ベクトルの次元が後ろになるほどtheta_iが小さく(回転が遅く)なるように設計されています。これは、高周波(速い回転)と低周波(遅い回転)の情報を組み合わせるためです。
m_theta = pos * theta_iでは、現在の単語の位置posと回転速度theta_iを掛け合わせて、このペアが実際に回転する角度を決定します。
ループ内の最後の部分rotated_v[i] = x * math.cos(m_theta) - y * math.sin(m_theta)とrotated_v[i+1] = x * math.sin(m_theta) + y * math.cos(m_theta)は、2次元の回転行列を適用しているのと同じ操作です。元のベクトル成分xとyを、計算した角度m_thetaだけ回転させ、新しいベクトルrotated_vに格納します。
この関数を、Queryベクトルx_q(位置pos_m)と、3つの異なる位置にあるKeyベクトルx_k(位置pos_n1, pos_n2, pos_n3)にそれぞれ適用し、回転後のベクトルq_rope、k_rope_n1などを得ます。
最後にtorch.dot(...)でこれらの回転済みベクトル間の内積を計算します。結果を見ると、QueryとKeyの距離が1, 3, 18と離れるにつれて、アテンションスコアが61.8から51.1、40.5へと滑らかに減衰していることがわかります。これは、個々のベクトルを絶対位置で回転させるだけで、内積を計算する段階で自動的に相対的な距離が考慮され、自然な減衰特性が生まれるというRoPEの最も重要な利点を実証しています。
RoPEの長期減衰特性
上記の回転位置埋め込みのコードでは、距離が1, 3, 18の場合のAttentionスコアの結果を確認しましたが、以下のコードでは位置0のQueryベクトルに対してKeyの位置が0〜100まで変化した時の減衰特性について確認します。
import torch
import math
import matplotlib.pyplot as plt
import japanize_matplotlib
# 1. 修正版RoPE関数の定義 (print出力を除いた計算処理のみ)
def apply_rope_correct(v, pos, dim):
rotated_v = torch.zeros_like(v)
for i in range(0, dim, 2):
pair_idx = i // 2
# 論文の定義: 後ろの次元ほど回転スピードが遅くなる
theta_i = 10000 ** (- (2 * pair_idx) / dim)
m_theta = pos * theta_i
x = v[i]
y = v[i+1]
rotated_v[i] = x * math.cos(m_theta) - y * math.sin(m_theta)
rotated_v[i+1] = x * math.sin(m_theta) + y * math.cos(m_theta)
return rotated_v
# 2. 実験設定
dim = 64 # 64次元ベクトル
max_distance = 100 # 距離0から100まで離していく
# トークンの初期ベクトル (すべて1.0の同一ベクトルと仮定)
x_q = torch.ones(dim)
x_k = torch.ones(dim)
# 3. 位置m=0におけるQueryのRoPE適用
# (位置0なので回転角度は0となり、実質回転しない)
q_rope = apply_rope_correct(x_q, pos=0, dim=dim)
# 4. Keyの位置 n を 0 から max_distance まで変化させて内積を計算
distances = list(range(max_distance + 1))
scores = []
for n in distances:
# 位置 n のKeyにRoPEを適用
k_rope = apply_rope_correct(x_k, pos=n, dim=dim)
# 内積(アテンションスコア)を計算
score = torch.dot(q_rope, k_rope).item()
scores.append(score)
# 5. グラフの描画
plt.figure(figsize=(10, 6))
plt.plot(distances, scores, marker='o', markersize=3, color='#1f77b4', linewidth=2)
plt.title("RoPEにおけるトークン間距離とアテンションスコアの長期減衰 (m = 0)", fontsize=14, fontweight='bold')
plt.xlabel("Keyの位置 n (Queryの位置 m=0 からの距離)", fontsize=12)
plt.ylabel("Attention スコア (内積)", fontsize=12)
plt.grid(True, linestyle='--', alpha=0.6)
# グラフにRoPEの特徴的な挙動の注釈を追加
plt.annotate('近くのトークンとは\n高いスコアで結合', xy=(2, scores[2]), xytext=(15, scores[0]*0.9),
arrowprops=dict(facecolor='black', shrink=0.05, width=1, headwidth=6))
plt.annotate('距離が離れるにつれて\nスコアが自然に減衰', xy=(50, scores[50]), xytext=(55, scores[0]*0.5),
arrowprops=dict(facecolor='black', shrink=0.05, width=1, headwidth=6))
plt.show()
このコードブロックでは、RoPE(回転位置埋め込み)が持つ最も重要かつ強力な性質である「長距離減衰性(Long-term Decay)」、すなわち「近くの単語同士は強く結びつき、遠くの単語の影響度は自然に減衰する」という性質をシミュレーションし、グラフ化しています。
まず、前述の解説と同様に定義されたapply_rope_correct関数を用いて、回転処理を行います。
実験設定として、ベクトルの次元数dim = 64とし、QueryとKeyの最大距離をmax_distance = 100(すなわち位置0から100まで)に設定しています。初期状態のトークンベクトルとして、要素がすべて1.0のx_qとx_kを定義します。
次に、位置m = 0のQueryに対して、apply_rope_correct(x_q, pos=0, dim=dim)を実行し、回転済みのベクトルq_ropeを取得します。位置が0であるため、回転角度m_thetaは常に0となり、実質的にベクトルは回転しません。
その後、Keyの位置nを0から100まで1ずつ変化させながらループ処理を行います。各位置において、k_rope = apply_rope_correct(x_k, pos=n, dim=dim)によりKeyベクトルを回転させ、torch.dot(q_rope, k_rope).item()でQueryベクトルとの内積(アテンションスコア)を計算し、scoresリストに格納していきます。これにより、Queryの位置0と、0〜100まで離れていくKeyとの間の相対的なアテンションスコアの変化を記録します。
最後に、描画ライブラリであるmatplotlib.pyplotおよび日本語化のためのjapanize_matplotlibを使用し、横軸を「Keyの位置 n(距離)」、縦軸を「Attention スコア(内積)」とした折れ線グラフを描画します。plt.annotateを用いることで、グラフ中の特定の座標(距離が近い箇所での高いスコアと、距離が離れるにつれてスコアが自然に減衰する箇所)に矢印付きの説明(注釈)を追加し、視覚的なわかりやすさを高めています。
上記コードを実行すると、次のような特徴を持ったきれいな曲線グラフが描画されます。

- 滑らかな減衰曲線
距離()が 0 のときが最もスコアが高く、離れるにつれて波打ちながらも**全体として右肩下がりにスコアが小さくなっていきます。**これが「近くの単語を重視し、遠くの単語の影響度を自然に薄める」という長距離減衰性(Long-term Decay)です。 - なぜ波打つのか(オシレーション)
グラフを見ると、直線的に減衰するのではなく、少し波打ちながら減衰していきます。これは一部の高速回転する次元(手前の次元)が何周もすることによる周期的な影響ですが、低速回転する次元(後ろの次元)がブレーキの役割を果たすため、決して元の高いスコアまで逆戻りすることはありません。 - 明示的なルールなしでの減衰の実現
「距離が○以上ならスコアを引く」といった条件分岐(if文)や上限の設定を一切していないにもかかわらず、ベクトルの回転と内積という純粋な数学の性質だけでこの美しい減衰がシミュレーションされています。
まとめ
本記事では、大規模言語モデル(LLM)の性能を飛躍的に向上させた位置埋め込み技術であるRoPE(Rotary Position Embedding) について、その理論的背景から具体的な挙動までを解説しました。
本記事では、以下の点について解説しました。
- RoPEのコアコンセプトの理解: 従来の絶対位置埋め込みや相対位置埋め込みが抱えていた課題を、ベクトルを回転させるというシンプルなアイデアでどのように解決したかを学びました。
- PyTorchによる挙動の比較検証: 各位置埋め込み方式をコードでシミュレートし、RoPEが事前のベクトル回転だけで、Attention計算時に単語間の相対的な距離を反映できることを確認しました。
- 長期減衰特性の可視化: QueryとKeyの距離が離れるほどAttentionスコアが滑らかに減衰する様子をグラフで確認し、RoPEが「近くの単語を重視する」という自然な言語の性質をモデルに与える仕組みを直感的に理解しました。
RoPEは、実装の手軽さと計算効率を維持しながら、モデルの長文対応能力(外挿性)を劇的に改善した、非常にエレガントな技術です。現在のLLMの基盤となっているこの重要な概念を理解することは、Transformerベースのモデルをより深く知る上で不可欠と言えるでしょう。
本記事の文章・構成の一部に生成AIを使用しています。