2D RoPEとは?
(画像は、Geminiで作成されたものです)
2D RoPEの概要
2次元RoPE(2D Rotary Position Embedding) は、主にマルチモーダル大規模言語モデル(VLM)の発展に伴い、画像とテキストをシームレスに融合させるための位置エンコーディング技術として導入されました。
特にMetaが開発した「LLaMA 3.2 Vision」や、オープンソースのマルチモーダルモデル「LLaVA-Next」などで採用されたことで大きな注目を集めています。
自然言語処理において、LLaMAをはじめとする多くのLLMは、トークン間の相対的な位置関係を動的かつ美しく表現できる「1次元のRoPE(1D RoPE)」を採用し、優れた成果を収めてきました。
しかし、この1次元RoPEを画像認識(Visionタスク)にそのまま適用しようとすると、大きな課題が生じました。
画像データをトランスフォーマーに入力する際、通常は2次元の画像を1列のシーケンスに平坦化(ラスタースキャン)します。
このため、1次元RoPEでは「左から右へ」の一方向の位置関係しか捉えられず、画像上で「縦方向に隣り合っているピクセル」の距離が、シーケンス上では非常に離れたものとして扱われてしまいます。
結果として、画像が持つ本質的な2次元の空間的連続性や幾何学的な構造をモデルが正確に理解できないというデメリットがありました。
また、画像の解像度やアスペクト比(縦横比)が動的に変化した場合、1次元の座標系のままでは、変化した空間の歪みを補正して位置情報を正しくマッピングすることが極めて困難でした。
2D RoPEは、RoPEが持つ「位置の差分を回転角の差分で表現する」という優れた特性を維持したまま、空間を「縦方向(軸)」と「横方向(軸)」の2つの独立した回転に分解して適用することで、この課題を解決しました。
具体的には、モデルの隠れ層のベクトル(特徴量)を前半と後半の2つに綺麗に分割し、前半には縦方向の位置に応じた回転を、後半には横方向の位置に応じた回転をそれぞれ施します。
これにより、QueryとKeyの内積(アテンション・スコア)を計算する際、数学的に「縦方向の距離」と「横方向の距離」の双方がアテンションの重みに直接反映されるようになります。
2D RoPEの導入によって、モデルは画像の解像度やアスペクト比の変更に対しても柔軟に対応できるようになり、高解像度な画像内の局所的なディテールから大域的な位置関係までを、極めて正確に捉えることが可能になりました。
この2次元データに対するRoPEの体系的な適用と拡張については、NAVER AI Labの研究チームによる論文 "Rotary Position Embedding for Vision Transformer" (ECCV 2024) などで詳細に議論されています。この論文では、次元を分割して縦横に個別の回転を施す手法(Axial 2D RoPE)の理論的分析に加え、対角方向の表現力を向上させる学習可能な周波数混合アプローチ(RoPE-Mixed)なども提案され、解像度の変化に対する高い頑健性(外挿性能)が実証されました。
1次元RoPE vs 2次元RoPEの比較
Queryベクトルを 、Keyベクトルを とします。
1次元RoPE(通常のテキスト等)
位置 (Query)と 位置 (Key)における回転行列 は、2次元の回転行列を対角線上に並べたブロック対角行列として定義されます。
次元 の場合の の例:
QueryとKeyの内積(アテンション・スコア)を計算すると、回転行列の性質()により、以下のように位置の差()だけが残る行列に変化します。
※ここでは以下です。
2次元RoPE(画像・マルチモーダル)
2D RoPEでは、隠れ層の次元 を前半の 次元(縦方向 )と、後半の 次元(横方向 )に分割し、それぞれの軸に対して独立した回転行列 を適用します。全体の回転行列 は、以下のように と を直和(結合)した形になります。
※ここで、 は縦の位置 を用いた の回転行列、 は横の位置 を用いた の回転行列です。
次元 の場合の の例:
Queryの座標を 、Keyの座標を としたときの内積は、次のように縦の差分行列と横の差分行列の足し算に美しく分解されます。
※ここで
コードによる1D RoPEと2D RoPEの比較
コードからQuery V0H0に対するAttentionを可視化することで、上記の数式で説明した1D RoPEと2D RoPEの挙動の違いを直感的に理解します。
ここでは、1次元RoPEと2次元RoPEの挙動の違いを視覚的に理解するため、簡単なシミュレーションを実装します。numpyを用いて4x4のグリッド上のパッチを想定し、特定のクエリ位置(左上のV0H0)に対する他の全パッチのアテンションスコアを計算します。最終的に、matplotlibを使って計算結果をヒートマップとして描画し、1次元RoPEがシーケンス順の距離しか考慮しないのに対し、2次元RoPEがグリッド上の縦横の距離を正しく反映できることを直感的に確認します。
import numpy as np
import matplotlib.pyplot as plt
# --- 1. 基本設定(4x4グリッド、隠れ層の次元 d=4) ---
grid_size = 4
num_patches = grid_size * grid_size
d = 4
# 位置情報の定義
# 1D RoPE用: シーケンス位置 m (0 ~ 15)
pos_1d = np.arange(num_patches)
# 2D RoPE用: (y, x) 座標 (0 ~ 3)
pos_y, pos_x = np.meshgrid(np.arange(grid_size), np.arange(grid_size), indexing='ij')
pos_y = pos_y.flatten()
pos_x = pos_x.flatten()
# --- 2. Query と Key のダミー特徴量ベクトルの定義 (d=4) ---
# 本来は画像の中身(セマンティクス)ですが、位置情報のみの影響を見るために
# 全パッチで共通の「すべての特徴が等しく含まれるベクトル」として初期化します。
q_base = np.array([1.0, 1.0, 1.0, 1.0])
k_base = np.array([1.0, 1.0, 1.0, 1.0])
# --- 3. 回転行列 (R) を生成する関数の定義 ---
# 1D RoPEの回転行列 (d=4)
def get_R_1d(m, theta1=0.4, theta2=0.1):
R = np.zeros((4, 4))
# 前半2次元 (θ1で回転)
R[0, 0], R[0, 1] = np.cos(m * theta1), -np.sin(m * theta1)
R[1, 0], R[1, 1] = np.sin(m * theta1), np.cos(m * theta1)
# 後半2次元 (θ2で回転)
R[2, 2], R[2, 3] = np.cos(m * theta2), -np.sin(m * theta2)
R[3, 2], R[3, 3] = np.sin(m * theta2), np.cos(m * theta2)
return R
# 2D RoPEの回転行列 (d=4)
def get_R_2d(x, y, theta=0.5):
R = np.zeros((4, 4))
# 前半2次元: 縦方向 y に応じた回転
R[0, 0], R[0, 1] = np.cos(y * theta), -np.sin(y * theta)
R[1, 0], R[1, 1] = np.sin(y * theta), np.cos(y * theta)
# 後半2次元: 横方向 x に応じた回転
R[2, 2], R[2, 3] = np.cos(x * theta), -np.sin(x * theta)
R[3, 2], R[3, 3] = np.sin(x * theta), np.cos(x * theta)
return R
# --- 4. Queryを左上 V0H0 (インデックス0) に固定してアテンション計算 ---
query_idx = 0
# 1D RoPE の Query ベクトル計算
R_q_1d = get_R_1d(pos_1d[query_idx])
q_1d = R_q_1d @ q_base
# 2D RoPE の Query ベクトル計算
R_q_2d = get_R_2d(pos_x[query_idx], pos_y[query_idx])
q_2d = R_q_2d @ q_base
attn_1d_flat = np.zeros(num_patches)
attn_2d_flat = np.zeros(num_patches)
for k_idx in range(num_patches):
# --- 1D RoPE の厳密計算 ---
R_k_1d = get_R_1d(pos_1d[k_idx])
k_1d = R_k_1d @ k_base
attn_1d_flat[k_idx] = np.dot(q_1d, k_1d) # 厳密な内積計算
# --- 2D RoPE の厳密計算 ---
R_k_2d = get_R_2d(pos_x[k_idx], pos_y[k_idx])
k_2d = R_k_2d @ k_base
attn_2d_flat[k_idx] = np.dot(q_2d, k_2d) # 厳密な内積計算
# 4x4の画像空間にリシェイプ
grid_attn_1d = attn_1d_flat.reshape(grid_size, grid_size)
grid_attn_2d = attn_2d_flat.reshape(grid_size, grid_size)
# --- 5. プロット描画 ---
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
labels = [f"V{v}H{h}" for v in range(grid_size) for h in range(grid_size)]
# 1D RoPE プロット
im1 = axes[0].imshow(grid_attn_1d, cmap='viridis', vmin=-1, vmax=4)
axes[0].set_title('1D RoPE ($q^T R_{m}^T R_{n} k$)\nQuery: V0H0', fontsize=12, pad=15)
# 2D RoPE プロット
im2 = axes[1].imshow(grid_attn_2d, cmap='viridis', vmin=-1, vmax=4)
axes[1].set_title('2D RoPE ($q^T R_{(x_Q,y_Q)}^T R_{(x_K,y_K)} k$)\nQuery: V0H0', fontsize=12, pad=15)
# 数値ラベルのマッピング
for v in range(grid_size):
for h in range(grid_size):
idx = v * grid_size + h
# 1D
val_1d = grid_attn_1d[v, h]
c_1d = "black" if val_1d > 1.5 else "white"
axes[0].text(h, v, f"{labels[idx]}\n{val_1d:.2f}", ha="center", va="center", color=c_1d, fontweight='bold')
# 2D
val_2d = grid_attn_2d[v, h]
c_2d = "black" if val_2d > 1.5 else "white"
axes[1].text(h, v, f"{labels[idx]}\n{val_2d:.2f}", ha="center", va="center", color=c_2d, fontweight='bold')
for ax in axes:
ax.set_xticks(np.arange(grid_size)); ax.set_yticks(np.arange(grid_size))
ax.set_xticklabels([f"H{i}" for i in range(grid_size)]); ax.set_yticklabels([f"V{i}" for i in range(grid_size)])
ax.set_xlabel("Horizontal"); ax.set_ylabel("Vertical")
fig.colorbar(im2, ax=axes.tolist(), shrink=0.75, label="Attention Score (Inner Product)")
plt.show()
このコードは、1次元RoPEと2次元RoPEのアテンションの計算方法の違いを、ステップ・バイ・ステップで可視化するものです。
-
基本設定: grid_size = 4で4x4の画像パッチ空間を、d = 4で各パッチの特徴ベクトルの次元数を定義します。1次元RoPE用にはpos_1dとして0から15までの通し番号を、2次元RoPE用にはpos_yとpos_xとして各パッチの(y, x)座標をそれぞれ準備します。
-
ダミー特徴量: QueryとKeyの元となるベクトルq_baseとk_baseを、全要素が1の単純なベクトルで初期化します。これにより、位置エンコーディングそのものの効果だけを純粋に観察できます。
-
回転行列の定義:
- get_R_1d関数は、1次元の位置mを受け取り、特徴ベクトルの前半2次元と後半2次元を異なる回転角(周波数)で回転させる4x4の回転行列を返します。これは通常のRoPEの実装に相当します。
- get_R_2d関数は、2次元の座標(x, y)を受け取ります。特徴ベクトルの前半2次元をy座標(縦方向)で、後半2次元をx座標(横方向)で独立して回転させる回転行列を返します。これが2D RoPEの核となるアイデアです。
-
アテンション計算: まず、Queryの位置を左上のパッチ(インデックス0、座標(0,0))に固定し、その位置エンコーディングを適用したQueryベクトルq_1dとq_2dを計算します。 次に、ループ処理ですべてのパッチ(Key)を順番に見ていき、各Keyの位置エンコーディングを適用したKeyベクトル(k_1d, k_2d)と、固定されたQueryベクトルとの内積を計算します。この内積がアテンションスコアとなり、attn_1d_flatとattn_2d_flatに格納されます。
-
整形と可視化: 最後に、1次元配列として得られたアテンションスコアをreshapeで4x4のグリッド形式に戻します。matplotlibのimshowを用いて、このスコアをヒートマップとして描画します。1D RoPEのプロットでは、アテンションがシーケンス順(0→1→2...)に近いほど高くなるのに対し、2D RoPEではQuery位置からのマンハッタン距離が近いパッチ(縦横に隣接するパッチ)でアテンションが高くなっていることが視覚的にわかります。
実行結果

画像データへの2D RoPEの適用
以下のコードでは、PyTorchを用いてダミーの画像テンソルを生成し、それを画像パッチへと分割した上で、線形投影(Linear Projection)により特徴量ベクトルへとマッピングする一連の流れを実装します。さらに、分割された5x5のグリッド座標に対して、縦横それぞれの位置に応じた2D RoPEの回転行列を定義・適用し、最終的にTransformerブロックに入力可能な1次元トークンシーケンスに平坦化するまでのプロセスをシミュレートします。
import torch
import numpy as np
# --- 1. 入力画像の設定 (10x10x3) ---
# Batch=1, Channel=3, Height=10, Width=10
torch.manual_seed(42)
image = torch.randn(1, 3, 10, 10)
# パッチサイズの設定 (2x2ピクセルを1パッチとする)
patch_size = 2
hidden_dim = 4 # 隠れ層の次元数 (d=4)
# --- 2. 画像をパッチに分割してベクトル化 (Linear Projection) ---
# 10x10の画像から、2x2のパッチが 縦5個 × 横5個 = 計25パッチ 作られます
# 各パッチの元の画素数は 2 * 2 * 3(RGB) = 12次元 です
# ① まず縦方向(Dim 2)をサイズ2、ストライド2で切り出す
patches = image.unfold(2, patch_size, patch_size)
# 形状は [1, 3, 5, 10, 2] になります。
# (縦10ピクセルが、2ピクセルずつの塊5個に分解され、末尾に「パッチ内の縦2ピクセル」の次元が追加された)
# ② 次に横方向(Dim 3)をサイズ2、ストライド2で切り出す
patches = patches.unfold(3, patch_size, patch_size)
# 形状は [1, 3, 5, 5, 2, 2] になります。
# (横10ピクセルも同様に、2ピクセルずつの塊5個に分解された)
# [1, 3, 5, 5, 2, 2]を[1, 5, 5, 3, 2, 2]に変換
patches = patches.permute(0, 2, 3, 1, 4, 5)
# 形状を [Batch, 縦のパッチ数(5), 横のパッチ数(5), パッチ内の画素数(12)] に整理
patches = patches.flatten(3)
# 線形投影(Linear Projection)により、画素数12からモデルの隠れ層次元数4へ変換
# これにより [Batch, 5, 5, 4] のテンソル(特徴量ベクトル)が得られます
projection_layer = torch.nn.Linear(patch_size * patch_size * 3, hidden_dim)
patch_features = projection_layer(patches) # 形状: [1, 5, 5, 4]
# --- 3. 2D RoPE の回転行列の生成 ---
# 縦5マス、横5マスのグリッド座標に対する回転行列を構築します
grid_h, grid_w = patch_features.shape[1], patch_features.shape[2]
theta = 0.5
# 各座標用の回転行列を格納するテンソル [5, 5, 4, 4]
R_2d_grid = torch.zeros(grid_h, grid_w, hidden_dim, hidden_dim)
for y in range(grid_h):
for x in range(grid_w):
# 前半2次元: 縦方向 y に応じた回転
R_2d_grid[y, x, 0, 0] = np.cos(y * theta)
R_2d_grid[y, x, 0, 1] = -np.sin(y * theta)
R_2d_grid[y, x, 1, 0] = np.sin(y * theta)
R_2d_grid[y, x, 1, 1] = np.cos(y * theta)
# 後半2次元: 横方向 x に応じた回転
R_2d_grid[y, x, 2, 2] = np.cos(x * theta)
R_2d_grid[y, x, 2, 3] = -np.sin(x * theta)
R_2d_grid[y, x, 3, 2] = np.sin(x * theta)
R_2d_grid[y, x, 3, 3] = np.cos(x * theta)
# --- 4. 2D RoPE の適用 (ベクトルの回転) ---
# 各パッチの特徴量ベクトルに対して、その座標に対応する回転行列を掛け合わせます
embedded_features = torch.zeros_like(patch_features)
for y in range(grid_h):
for x in range(grid_w):
# 特徴量ベクトル [Batch=1, 4] 取得
q_or_k = patch_features[0, y, x, :]
# 対応する4x4の回転行列を取得
R = R_2d_grid[y, x, :, :]
# 行列掛け算(回転の適用)
embedded_features[0, y, x, :] = R @ q_or_k
# 最終的にトランスフォーマーブロックに渡すために1列のシーケンスに平坦化
# [Batch=1, パッチ数=25, 次元数=4]
final_sequence = embedded_features.flatten(1, 2)
print("元の画像形状:", image.shape)
print("パッチ分割後のグリッド形状 (縦, 横, 次元):", patch_features.shape[1:])
print("2D RoPE適用後の最終入力シーケンス形状:", final_sequence.shape)
実行結果
元の画像形状: torch.Size([1, 3, 10, 10])
パッチ分割後のグリッド形状 (縦, 横, 次元): torch.Size([5, 5, 4])
2D RoPE適用後の最終入力シーケンス形状: torch.Size([1, 25, 4])
このコードは、実際の2次元画像データを入力として受け取り、パッチ分割から2D RoPEを適用してTransformer用の1次元シーケンスに変換するまでのデータパイプラインの流れを忠実に再現したものです。
-
パッチ分割と次元整理
まず、入力画像 image (形状:[1, 3, 10, 10])を、patch_size = 2 (2x2ピクセル)で分割します。PyTorchの unfold メソッドを縦方向(Dim 2)と横方向(Dim 3)に対して連続して実行することで、画像を重複なくパッチ化し、テンソルの形状を[1, 3, 5, 5, 2, 2]に分解します。
次に、patches.permute(0, 2, 3, 1, 4, 5) によって各パッチの座標と色チャネルの順序を整理し、patches.flatten(3) でパッチ内の全画素情報(2 * 2 * 3 = 12次元)を平坦化して、形状を[1, 5, 5, 12]とします。 -
線形投影(Linear Projection)
画素レベルの平坦化ベクトルに対して、torch.nn.Linear である projection_layer を適用し、モデルの隠れ層の次元数 hidden_dim = 4 に圧縮します。これにより、パッチごとの特徴量を表すテンソル patch_features (形状:[1, 5, 5, 4])が得られます。 -
2D RoPE 回転行列の生成
縦5パッチ、横5パッチのグリッド(計25パッチ)における各パッチの位置座標に応じて、個別に4x4の回転行列を構築します。
R_2d_grid テンソル(形状:[5, 5, 4, 4])を用意し、各座標 (x, y) で二重ループを回します。特徴ベクトルの前半2次元(インデックス0, 1)には縦方向の座標 y に応じた回転を適用し、後半2次元(インデックス2, 3)には横方向の座標 x に応じた回転を個別にセットします。 -
回転の適用とシーケンス化
構築した回転行列を用いて、各パッチの特徴量ベクトル q_or_k に対し、行列乗算(R @ q_or_k)を計算して回転を施します。これにより、2次元空間の幾何学的な構造が各特徴量の回転角度として綺麗にエンコードされます。
最後に、embedded_features.flatten(1, 2) を実行して、グリッド形状[1, 5, 5, 4]を一列のシーケンス [1, 25, 4]([Batch, パッチ数, 次元数])へと平坦化します。これにより、縦横の相対的な位置情報を内包した状態で、TransformerのSelf-Attentionブロックへと入力できるようになります。
まとめ
本記事では、マルチモーダルモデルにおける画像の位置エンコーディング技術である2D RoPE(2次元Rotary Position Embedding) について、その仕組みと効果を解説しました。
記事を通じて、以下の内容を学びました。
- 1D RoPEの課題と2D RoPEの優位性: 1次元シーケンスに平坦化された画像データでは、1D RoPEが本来の2次元空間情報を損失してしまう問題を学び、2D RoPEが縦横の座標を独立して扱うことでこれを解決する仕組みを理解しました。
- 数式による理論的背景: 2D RoPEがアテンション計算時に、内積の結果を「縦方向の相対距離」と「横方向の相対距離」の関数に美しく分解できることを数式レベルで確認しました。
- コードによる視覚的な検証:
numpyを用いたシミュレーションを通じて、Queryを固定した際のアテンションスコアを計算しました。その結果をmatplotlibで可視化し、1D RoPEがシーケンス順の距離に依存するのに対し、2D RoPEがグリッド上の幾何学的な距離を正しく反映できることを直感的に確認しました。 - 画像データへの適用プロセス:
PyTorchを用いた画像パッチ分割や線形投影プロセスに加え、5x5のグリッド座標に対応する2D RoPE回転行列を定義・適用し、Transformerに入力可能な1次元シーケンスに整形する具体的なデータパイプラインの実装方法を学びました。
2D RoPEは、LLaVA-Nextのような先進的なマルチモーダルモデルで採用されており、高解像度でアスペクト比の異なる多様な画像を扱う上で不可欠な技術となっています。本記事が、その核心的なアイデアを理解する一助となれば幸いです。
本記事の文章・構成の一部に生成AIを使用しています。