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

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

三つの推定を並べる
| 推定方法 | 推定スコアの挙動 | 真のスコア |
|---|---|---|
| 素朴なBT(スタイル無視) | 過大評価(RMSE ≈ 55、最大 +80 Elo) | 基準 |
| Style Control 適用 | ほぼ真値(RMSE ≈ 6) | 基準 |
| best-of-N(N=10) | +約106 Elo の上振れ | 基準 |
| best-of-N(N=27) | +約139 Elo の上振れ | 基準 |
80 Eloの歪みが順位に何をするか
実力とはまったく無関係な冗長さだけで、最大 +80 Elo、全体RMSE 約55 Elo の系統誤差が生じました。上位数モデルの順位が容易に入れ替わる規模です。そして、共変量を一本加えるだけでRMSEは 55 → 6 Elo に縮み、スタイル効果 自体も正確に推定できています。歪みの原因が観測できていれば、回帰で消せます。
選択的開示のほうは、 で約106 Elo、 で約139 Elo の上振れに達しました。測定に一切の不正がなくても、複数バリアントから最良を選んで公開するだけで、スコアはこの規模で押し上がります。
回帰で消せない歪みと、BTが置く仮定
補正できる交絡と、できないバイアスは別物です。スタイル(長さ・書式)、回答の提示位置、言語などの観測できる交絡は、共変量として回帰に入れれば調整できます。しかし選択的開示・サンプリングの非対称性・非公開なバリアント削除といった構造的バイアスは、回帰では消せません。データの生成過程そのものが歪んでいるからです。
Bradley-Terryが置く仮定にも限界があります。このモデルは推移性、つまり かつ なら という順序の一貫性を前提にしており、「Aには一貫して勝つがCには勝てない」といった非推移な選好は原理的に表現できません。
未観測の交絡も残ります。回答の自信度、ユーザーへの迎合(sycophancy)、特定の質問への拒否挙動などは、共変量に入れない限りスコアに紛れ込んだままです。「調整済み」という言葉は、あくまで「入れた変数について調整した」という意味でしかありません。
まとめ
アリーナのEloスコアは、Bradley-Terryモデルの推定値であり、不偏なサンプリング・スタイル非依存・推移性という暗黙の仮定の上に立っています。仮定が破れれば順位は歪み、しかもその歪みは点推定を眺めているだけでは見えません。
モデル選定でリーダーボードを参照するなら、単一のスコアを鵜呑みにしないことです。汚染対策済みの静的ベンチマークと、自分たちの実タスク分布での評価を併用し、アリーナのスコアはスタイル補正の有無、票数と信頼区間、プレビュー運用の透明性とセットで読みます。手始めに、スタイル補正前と補正後の順位表を並べて、位置が大きく動くモデルがどれかを確認してみてください。そこで大きく動くモデルは、中身よりスタイルで順位を押し上げられていた可能性が高い候補です。
コード
以下は、ここまでの手順を統合した再現可能な全コードです。numpy と scipy のみで動作します。
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({
"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="True = estimate (45deg)")
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("True skill (Elo)"); ax.set_ylabel("Estimated score (Elo)")
ax.set_title(title)
return sc
fig, ax = plt.subplots(1, 2, figsize=(11, 5.2))
sc = panel(ax[0], bn, f"Naive BT (style ignored) RMSE={rmse_naive:.0f}")
panel(ax[1], bs, f"After Style Control RMSE={rmse_style:.0f}")
ax[0].legend(loc="upper left", fontsize=9)
fig.suptitle("Style confounding: verbose models (red) are overrated by naive BT",
fontsize=13)
fig.colorbar(sc, ax=ax, fraction=0.035, pad=0.02).set_label(
"Model verbosity z (length / formatting)")
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.annotate(f"N={N}\n+{upcurve[N-1]:.0f}", (N, upcurve[N-1]),
textcoords="offset points", xytext=(8, -18), color=RED, fontsize=9)
ax.axhline(0, color="#9a988f", ls="--", lw=1)
ax.set_xlabel("Number of privately tried variants N")
ax.set_ylabel("Systematic inflation of published score (Elo)")
ax.set_title(f"Selective disclosure (best-of-N), V={V} votes/variant")
fig.savefig("bt_bias_bestofn.png"); plt.close(fig)