LLMリーダーボードはなぜ歪むのか:Bradley-Terryモデルと交絡・選択バイアスをシミュレーションで理解する
はじめに
LLMのランキングは、チェスと同じ Elo で決まっています。二つのモデルに同じ質問を投げ、人間がどちらの回答を好むかを投票し、その勝敗を積み上げてスコアを付ける。直感的で、わかりやすい仕組みです。
では、そのEloは信用できるのでしょうか。
結論を先に言えば、Eloという一つの数字の裏側には統計モデルの仮定があり、その仮定が破れると順位は系統的に歪みます。本記事では、アリーナ型のスコアが実は Bradley-Terryモデル であることを確認し、「回答スタイル(長さ・書式)による交絡」と「best-of-N の選択的開示によるバイアス」という二つの歪みを、合成データのシミュレーションで定量的に観察します。真の実力を既知に設定できるのがシミュレーションの強みで、素朴な推定がどれだけずれ、補正でどこまで戻るのかを数字で見られます。
対象読者:
- LLMの評価指標(ベンチマーク/アリーナ)の仕組みを数理で理解したい方
- Bradley-Terry / ロジスティック回帰の基礎がある方
- 交絡・選択バイアスに関心のあるデータ分析者
記事のポイント:
- ペア比較スコア(Arena Score)が Bradley-Terry モデルであり、そのMLEがロジスティック回帰と等価であることを理解する
- 回答スタイルが交絡因子として順位を歪めること、そして Style Control で補正できることを理解する
- 「best-of-N の選択的開示」が不偏サンプリングの仮定を破り、スコアを系統的に上振れさせることをシミュレーションで確認する
1. LLM評価の全体像 — なぜ単一スコアが危ういか
LLMの評価は、大きく二系統に分けられます。
- 実行ベース評価:
SWE-benchやHumanEvalのように、生成物を実際に動かして正解/不正解を機械的に判定するもの。指標は明快ですが、飽和(上位が満点に張り付く)や汚染(テスト問題の学習データ混入)の影響を受けます。 - 人間選好ベース評価:Chatbot Arena に代表される、二つの回答を人間が見比べて勝敗を付けるペア比較。多様な実タスクを反映できる一方、投票者の主観や回答の見た目が混ざり込みます。
本記事のスコープは後者、ペア比較から算出されるスコアに絞ります。ここには、飽和や汚染とは別種の、より見えにくい問題があります ——「順位そのものの歪み」です。勝敗データは確かに集まっているのに、そこから実力を復元する過程で系統誤差が入る。その構造を理解するには、まずスコアがどんな確率モデルなのかを知る必要があります。
2. ペア比較から実力スコアへ:Bradley-Terryモデル
各モデル に「実力」を表す実数パラメータ を割り当てます。Bradley-Terryモデルは、モデル が に勝つ確率を実力差のロジスティック関数で表します。
これはまさにEloレーティングの確率モデルそのものです。Eloで使う「点数」は、自然対数スケールの を定数倍しただけの表現にすぎません。
つまり実力差が のとき、Eloでちょうど400点差、勝率にして約91%になります。
逐次Elo と 一括MLE
古典的なEloは、対戦のたびにスコアを少しずつ更新する 逐次アルゴリズム です。実装は軽いのですが、更新結果が対戦の順序に依存し、初期値やK係数にも左右されます。一方アリーナのスコアは、蓄積した全対戦をまとめて 最尤推定(MLE) で解くのが標準です。順序に依存せず再現性があり、信頼区間も付けられます。
MLEはロジスティック回帰と等価
ここが本記事で最も効く要点です。Bradley-TerryのMLEは、設計行列を工夫したロジスティック回帰にほかなりません。
各対戦 について、勝者に ・敗者に ・それ以外に を立てた行ベクトル を作り、目的変数を「一列目のモデルが勝ったか()」とします。すると対数尤度は
となり、これは切片なし・係数が各モデルの実力 であるロジスティック回帰と一致します。「勝者 / 敗者 の設計行列を組んでロジスティック回帰にかける」——これだけでアリーナスコアが再現できるのです。後述の交絡補正も、この回帰に列を一本足すだけで実現できます。
実務上の注意
| 論点 | 内容 |
|---|---|
| 識別可能性 | は定数の足し引きで不変( でも勝率は同じ)。基準モデルを固定するか、平均を0に中心化して一意にする。 |
| 引き分けの扱い | 引き分けを 0.5勝0.5敗として割り振る簡便法のほか、Davidsonモデルで引き分け確率を明示的にモデル化する方法がある。 |
| 票数と信頼区間 | 票の少ないモデルほどスコアの分散が大きい。ブートストラップで信頼区間を出し、点推定だけを見ないことが重要。 |
3. 交絡因子① スタイル(長さ・書式)と Style Control
問題は、投票者が中身と無関係な手がかりで勝者を選びがちなことです。長い回答、箇条書きで整えられた回答、Markdownで見栄えのする回答は、内容が同等でも選ばれやすい。この「スタイルの効果」が実力パラメータ に混入すると、冗長なモデルの順位が実力以上に押し上げられます。
これを定式化しましょう。回答のスタイル量(例:正規化した長さ)を とし、勝敗のロジットを次のように書きます。
はスタイル効果の強さです。素朴なBTは第二項を無視するため、推定された は実力に ぶんの下駄を含んでしまいます。
この構図は、因果推論における 共変量調整 とまったく同型です。スタイル は「処置(どちらのモデルか)」と「結果(勝敗)」の両方に影響する交絡因子であり、これを回帰に入れて調整すれば効果を分離できます。交絡と調整の考え方そのものは、因果推論入門で扱った傾向スコアの議論と地続きです。
Style Control は、この を共変量として回帰に加え、 と を同時推定する手続きです。ランキングを出すときは (=スタイルを揃えた仮想的な条件)に固定して だけを使います。副産物として得られる は、「そのアリーナで長さバイアスがどれだけ強いか」を定量化する指標にもなります。
4. 交絡因子② 選択的開示(best-of-N)による上振れ
もう一つの歪みは、モデルの提供者側の運用から生まれます。匿名プレビューの枠で同じ素性のモデルを少しずつ変えた複数バリアントを同時に走らせ、最も高いスコアを出した一つだけを「正式版」として公開する——このような 選択的開示 です。個々のバリアントの測定に不正がなくても、公開スコアは系統的に上振れします。
数理的にはこうです。 個のバリアントの推定スコア が真の実力 のまわりに測定誤差をもって散らばるとき、その最大値の期待値は真の平均より必ず高くなります(順序統計量の性質)。
これは「同じモデルを何度も測って最良を採る=多重比較」という直感で捉えられます。統計的仮説検定で検定を繰り返すほど偽陽性が出やすくなるのと同じ理屈で、ランキングにおいては不偏サンプリングの仮定が破れているのです。素朴なBTの推定式には「観測された勝敗は各モデルの実力の不偏な標本である」という前提が暗に入っていますが、選択的開示はまさにこの前提を壊します。
5. Pythonによる実践例
真の実力 を既知に設定した合成対戦データで、素朴なBTがどう歪み、補正で真値に戻るかを見ます。使うのは numpy と scipy だけです(BT推定は前述のとおりロジスティック回帰なので、scipy.optimize で対数尤度を直接最大化します)。乱数は np.random.seed(42) で固定し、完全に再現可能です。
Step 1: データ生成
10モデルに真の実力 を与え、さらに各モデルに固有の「冗長さ」(長さ・書式の傾向)を持たせます。勝敗は のロジットから生成し、 のスタイル交絡を注入します。冗長さ は実力 とは独立に振っているので、「実力と無関係なスタイルだけ」が順位に効く状況を作れます。
Step 2: 素朴なBT推定
スタイルを無視して を推定すると、冗長なモデルが過大評価されます。下図の左パネルは、推定スコアを真の実力に対してプロットしたものです(点の色は冗長さ 、破線は真値=推定の45度線)。赤い(冗長な)点ほど45度線の上に飛び出し、青い(簡潔な)点は下に沈んでいるのがわかります。誤差と冗長さの相関は 0.99 に達し、最も過大評価されたモデルは真の 50 Elo に対して 130 Elo(+80 Elo)と推定されました。全体のRMSEは約 55 Elo です。
Step 3: Style Control
を共変量に加えて再推定すると、点はぴたりと45度線に戻ります(下図右パネル)。RMSEは 55 → 6 Elo に激減し、スタイル効果の推定値 は真値 0.45 をほぼ完全に回復しました。列を一本足すだけで、順位の歪みが解消できることが確認できます。

Step 4: 選択的開示のシミュレート
真の実力が集団の中央()にある一つのモデルについて、 個のバリアントを試し、各バリアントを 30票ずつで測定して最良スコアだけを公開する運用をシミュレートします(スタイル交絡は入れず、選択効果だけを取り出します)。横軸を 、縦軸を公開スコアの上振れ量(Elo)としてプロットすると、実力は変えていないのにスコアだけが押し上がっていく様子が見えます。

Step 5: 結果の比較
| 推定方法 | 推定スコアの挙動 | 真のスコア |
|---|---|---|
| 素朴なBT(スタイル無視) | 過大評価(RMSE ≈ 55、最大 +80 Elo) | 基準 |
| Style Control 適用 | ほぼ真値(RMSE ≈ 6) | 基準 |
| best-of-N(N=10) | +約106 Elo の上振れ | 基準 |
| best-of-N(N=27) | +約139 Elo の上振れ | 基準 |
6. 結果の解釈
シミュレーションからわかったことを整理します。
- スタイル交絡の大きさ:実力とは無関係な冗長さだけで、最大 +80 Elo・全体RMSE 約55 Elo の系統誤差が生じた。上位数モデルの順位が容易に入れ替わる規模である。
- Style Controlの回復効果:共変量を一本加えるだけでRMSEが 55 → 6 Elo に縮み、スタイル効果 自体も正確に推定できた。歪みの原因が観測できていれば、回帰で消せる。
- best-of-N の上振れ量: で約106 Elo、 で約139 Elo。測定に一切の不正がなくても、「最良を選んで公開する」だけでこの規模の下駄が履ける。
注意点と限界
- 補正できる交絡と、できないバイアスは別物です。スタイル(長さ・書式)、回答の提示位置、言語などの観測できる交絡は、共変量として回帰に入れれば調整できます。しかし選択的開示・サンプリングの非対称性・非公開なバリアント削除といった構造的バイアスは、回帰では消せません。データの生成過程そのものが歪んでいるからです。
- Bradley-Terryは推移性を仮定します。 かつ なら という順序が一貫している前提であり、「Aにはめっぽう強いがCには弱い」といった非推移な選好は原理的に表現できません。
- 未観測交絡は残ります。回答の自信度、ユーザーへの迎合(sycophancy)、特定の質問への拒否挙動などは、共変量に入れない限りスコアに紛れ込んだままです。「調整済み」という言葉は、あくまで「入れた変数について調整した」という意味でしかありません。
7. まとめ
Eloという一つの数字の裏には、Bradley-Terryモデルという確率モデルと、その暗黙の仮定(不偏なサンプリング、スタイル非依存、推移性)があります。仮定が破れれば順位は歪み、しかもその歪みは点推定を眺めているだけでは見えません。
実務的な指針はシンプルです。単一スコアを鵜呑みにしないこと。汚染対策済みの静的ベンチマークと、自分たちの実タスク分布での評価を併用し、アリーナのスコアは「スタイル補正の有無」「票数と信頼区間」「プレビュー運用の透明性」とセットで読む。数字の大小ではなく、その数字がどんな仮定の上に立っているかを見るべきです。
発展の入口として、次回は LLM-as-Judge の信頼性(人間投票の代わりにLLMを審判に使うときのバイアス)と、投票者の重み付けによる頑健化を扱う予定です。統計モデルの前提を意識する姿勢は、頻度主義統計学とベイズ統計学の議論とも通じるところがあります。
コード
以下は Step 1〜5 を統合した、再現可能な全コードです。numpy と scipy のみで動作します(日本語ラベルのため Noto Sans JP を指定していますが、環境に応じて変更してください)。
import numpy as np
from scipy.optimize import minimize
from scipy.special import expit # ロジスティックシグモイド
import matplotlib.pyplot as plt
# ---- 描画設定(日本語フォントは環境に合わせて変更可)----
ACCENT, NAVY, RED = "#2f5c96", "#14243c", "#9c3b46"
plt.rcParams.update({
"font.family": "Noto Sans JP", "axes.unicode_minus": False,
"axes.grid": True, "grid.color": "#e4e2dc", "savefig.dpi": 150,
"savefig.bbox": "tight",
})
SCALE = 400.0 / np.log(10.0) # 自然対数スケール -> Elo点
np.random.seed(42)
# ============================================================
# Step 1: データ生成(スタイル交絡を注入)
# ============================================================
M = 10
beta_true = np.random.normal(0, 0.9, M); beta_true -= beta_true.mean()
verbosity = np.random.normal(0, 1.0, M) # 各モデル固有の冗長さ(長さ・書式)
gamma_true = 0.45 # スタイル効果の強さ
n_battles = 12000
I = np.random.randint(0, M, n_battles); J = np.random.randint(0, M, n_battles)
keep = I != J; I, J = I[keep], J[keep]; nB = len(I)
zI = np.random.normal(verbosity[I], 0.5) # 各対戦での回答スタイル量
zJ = np.random.normal(verbosity[J], 0.5)
dz = zI - zJ
pI = expit((beta_true[I] - beta_true[J]) + gamma_true * dz)
y = (np.random.random(nB) < pI).astype(float) # 一列目(I)が勝ったか
# 設計行列: 勝者候補 I に +1, 敗者候補 J に -1
X = np.zeros((nB, M)); X[np.arange(nB), I] += 1; X[np.arange(nB), J] -= 1
# ============================================================
# Bradley-Terry の MLE = ロジスティック回帰
# ============================================================
def fit_bt(X, y, extra=None, ridge=1e-6):
"""extra に共変量(例: スタイル差 dz)を渡すと Style Control になる。"""
Xall = X if extra is None else np.hstack([X, extra])
M = X.shape[1]
def nll(w):
p = np.clip(expit(Xall @ w), 1e-12, 1 - 1e-12)
return -(y * np.log(p) + (1 - y) * np.log(1 - p)).sum() + ridge * (w @ w)
def grad(w):
return Xall.T @ (expit(Xall @ w) - y) + 2 * ridge * w
w = minimize(nll, np.zeros(Xall.shape[1]), jac=grad, method="L-BFGS-B").x
beta = w[:M] - w[:M].mean() # 識別可能性: 平均0に中心化
return beta, (w[M:] if extra is not None else None)
# ============================================================
# Step 2 & 3: 素朴なBT と Style Control
# ============================================================
beta_naive, _ = fit_bt(X, y) # スタイル無視
beta_style, gamma_hat = fit_bt(X, y, extra=dz[:, None]) # スタイル制御
bt, bn, bs = beta_true * SCALE, beta_naive * SCALE, beta_style * SCALE
rmse_naive = np.sqrt(np.mean((bn - bt) ** 2))
rmse_style = np.sqrt(np.mean((bs - bt) ** 2))
print(f"gamma_hat={gamma_hat[0]:.3f} (true {gamma_true})")
print(f"RMSE naive={rmse_naive:.1f} style={rmse_style:.1f} [Elo]")
# ============================================================
# Step 4: 選択的開示 (best-of-N) をベクトル化して評価
# ============================================================
beta0, V, n_trials, maxN = 0.0, 30, 8000, 30
G = n_trials * maxN
opp = beta_true[np.random.randint(0, M, size=(G, V))] # 各バリアントの対戦相手
win = (np.random.random((G, V)) < expit(beta0 - opp)).astype(float)
b = np.zeros(G); lam = 1e-3 # 1次元MLEをNewton法で
for _ in range(60):
p = expit(b[:, None] - opp)
step = ((p - win).sum(1) + lam * b) / ((p * (1 - p)).sum(1) + lam)
b -= step
if np.max(np.abs(step)) < 1e-9:
break
meas = np.clip(b, -8, 8).reshape(n_trials, maxN)
upcurve = np.maximum.accumulate(meas, axis=1).mean(0) * SCALE # best-of-N の期待上振れ
for N in [1, 5, 10, 27]:
print(f"best-of-{N:<2d} +{upcurve[N-1]:.1f} Elo")
# ============================================================
# Step 5: 可視化
# ============================================================
def panel(ax, est, title):
lim = [min(bt.min(), est.min()) - 30, max(bt.max(), est.max()) + 30]
ax.plot(lim, lim, "--", color="#9a988f", lw=1.2, label="真値 = 推定 (45°)")
sc = ax.scatter(bt, est, c=verbosity, cmap="RdBu_r", vmin=-2, vmax=2,
s=90, edgecolor=NAVY, linewidth=0.6, zorder=3)
ax.set_xlim(lim); ax.set_ylim(lim); ax.set_aspect("equal")
ax.set_xlabel("真の実力 (Elo)"); ax.set_ylabel("推定スコア (Elo)"); ax.set_title(title)
return sc
fig, ax = plt.subplots(1, 2, figsize=(11, 5.2))
sc = panel(ax[0], bn, f"素朴なBT(スタイル無視) RMSE={rmse_naive:.0f}")
panel(ax[1], bs, f"Style Control 適用後 RMSE={rmse_style:.0f}")
ax[0].legend(loc="upper left", fontsize=9)
fig.colorbar(sc, ax=ax, fraction=0.035, pad=0.02).set_label("モデルの冗長さ z")
fig.savefig("bt_bias_style.png"); plt.close(fig)
fig, ax = plt.subplots(figsize=(8.5, 5.0))
xs = np.arange(1, maxN + 1)
ax.plot(xs, upcurve, color=ACCENT, lw=2.2)
for N in [1, 5, 10, 27]:
ax.scatter([N], [upcurve[N-1]], color=RED, s=60, zorder=3)
ax.axhline(0, color="#9a988f", ls="--", lw=1)
ax.set_xlabel("非公開で試したバリアント数 N")
ax.set_ylabel("公開スコアの系統的上振れ (Elo)")
ax.set_title(f"選択的開示 (best-of-N) V={V}票/バリアント")
fig.savefig("bt_bias_bestofn.png"); plt.close(fig)