基于两两对比的 Elo 评分排行榜


文档摘要

源文件:chapter6/elo-leaderboard/README.md 基于两两对比的 Elo 评分排行榜 实验 6-6:从两两对比数据构建模型排行榜 本项目从零实现一套 Elo 评分系统,基于 Chatbot Arena 公开投票数据分析模型表现。该实现演示了 Bradley-Terry 模型如何从数百万次两两对比投票中提取模型的相对能力。 概览 Elo 评分系统是一种在零和博弈中计算参赛者(本项目里即 AI 模型)相对水平的方法。它最初为国际象棋而设计,后来被改造为依据用户投票的头对头对比,对 AI 语言模型进行排名。

源文件:chapter6/elo-leaderboard/README.md

基于两两对比的 Elo 评分排行榜

实验 6-6:从两两对比数据构建模型排行榜

本项目从零实现一套 Elo 评分系统,基于 Chatbot Arena 公开投票数据分析模型表现。该实现演示了 Bradley-Terry 模型如何从数百万次两两对比投票中提取模型的相对能力。

概览

Elo 评分系统是一种在零和博弈中计算参赛者(本项目里即 AI 模型)相对水平的方法。它最初为国际象棋而设计,后来被改造为依据用户投票的头对头对比,对 AI 语言模型进行排名。

核心特性

  • 高性能实现:NumPy + Numba JIT + 并行处理,速度最优
  • 真实投票数据分析:使用真实的 Chatbot Arena 投票数据,包含数百万次两两对比
  • 胜率预测:计算任意两个模型之间的预期胜率
  • 历史追踪:构建时间序列快照,呈现排名的演化
  • 交互式可视化:多种可视化形式,包括动画条形图竞速
  • 可扩展:高效处理 2GB、数十万场比赛的数据集

数学基础

Elo 系统基于 Bradley-Terry 模型,该模型把"模型 A 战胜模型 B"的概率建模为:

P(A beats B) = 1 / (1 + 10^((R_B - R_A) / 400))

每场比赛后,评分按如下方式更新:

R_A_new = R_A + K * (S_A - E_A)

其中:

  • R_A 是模型 A 的当前评分
  • K 是学习率(K 因子)
  • S_A 是实际得分(胜=1,负=0,平=0.5)
  • E_A 是预期得分(预测的胜率)

环境要求

  • 磁盘空间:至少 3GB 空闲(2GB 数据文件 + 1GB 处理空间)
  • 内存:处理完整数据集建议 4GB 以上
  • 网络:稳定连接,约 2GB 下载量
  • Python:3.8+

安装

cd projects/week6/elo-leaderboard pip install -r requirements.txt

命令行工具 / Command-Line Interface (cli.py)

cli.py 是本实验统一的 argparse 命令行入口(中文 --help),把整条流水线拆成子命令:
对战 (battle) -> 计算评分 (elo) -> 展示排行榜 (leaderboard),并提供 pipeline 一步到位。

python cli.py --help # 查看全部子命令 python cli.py battle --help # 查看某个子命令的参数 # 默认离线端到端演示:模拟对战 -> 在线 Elo -> 最终排行榜表格(无需任何数据/API) python cli.py # 等价于 python cli.py pipeline

子命令

子命令 作用 关键参数
battle 生成两两对战结果 --source {simulate,arena,llm}--num-battles--tie-prob--seed--sample--output
elo 从对战结果计算评分 --method {online-elo,bradley-terry}--k--bootstrap--input--output
leaderboard 渲染最终排行榜表格 --input(对战或评分文件)、--method--bootstrap--top-n
pipeline 一步跑完 对战 -> Elo -> 排行榜 上述参数的并集

三种对战来源(--source

  • simulate(默认,纯离线):从已知的潜在实力分模拟对战。因为真值已知,可用来校验恢复出的排行榜排序是否正确;--tie-prob 控制平局比例,用于演练平局处理。

  • arena(离线):加载真实 Chatbot Arena 投票数据(默认 arena_data.json,约 2GB),可用 --sample N 抽样。

  • llm(需 API):用 LLM 做配对评判,并内置位置偏差消除——每对交换顺序各评一次,两次判决一致才计胜负、否则记为平局(对应书中 6.4 位置偏差讨论)。仅此来源需要 LLM API Key。

    两种评判后端(--judge-backend {anthropic,openrouter,auto},默认 auto

    • anthropic:官方 anthropic SDK,用 ANTHROPIC_API_KEY
    • openrouter:OpenAI 兼容 SDK 指向 https://openrouter.ai/api/v1,用 OPENROUTER_API_KEY。内部 Claude 名字会自动映射为 OpenRouter id(claude-opus-4-8anthropic/claude-opus-4.8claude-haiku-4-5anthropic/claude-haiku-4.5);已含 / 的 id(如 openai/gpt-5.6-luna)原样透传。当直连 Anthropic key 缺失或失效时用它兜底。
    • auto(默认):有 ANTHROPIC_API_KEY 走 anthropic,否则回退 openrouter。注意 auto 只看 key 是否存在、不校验有效性;若 ANTHROPIC_API_KEY 存在但已失效,请显式 --judge-backend openrouter

    位置偏差消除与 A/B/tie 解析逻辑与后端无关,两条路径完全一致。

分步示例

# 1) 模拟 5000 场对战(含 10% 平局) python cli.py battle --source simulate --num-battles 5000 --output battles.json # 2) 用官方 Bradley-Terry MLE + 100 轮 bootstrap 置信区间计算评分 python cli.py elo --input battles.json --method bradley-terry --bootstrap 100 # 3) 展示前 20 名排行榜(也可直接读评分文件) python cli.py leaderboard --input battles.json --top-n 20 # 用真实 Arena 数据抽样跑(离线) python cli.py pipeline --source arena --arena-file arena_data.json --sample 50000 --method bradley-terry --bootstrap 100 # LLM 评判对战(需要 API Key)——官方 Anthropic export ANTHROPIC_API_KEY=sk-... python cli.py battle --source llm --candidate-models claude-opus-4-8 claude-haiku-4-5 # LLM 评判对战——通过 OpenRouter 兜底(直连 Anthropic key 缺失/失效时) export OPENROUTER_API_KEY=sk-or-... python cli.py battle --source llm --judge-backend openrouter \ --judge-model claude-opus-4-8 \ --candidate-models anthropic/claude-haiku-4.5 openai/gpt-5.6-luna

模拟来源会同时打印真值潜在实力,方便和恢复出的排行榜对照;在线 Elo 与 Bradley-Terry 两种方法都应恢复出与真值一致的排名(分值不必精确对齐,见下文说明)。

快速开始

本项目遵循官方 Chatbot Arena 的做法,实现了两种排名方法

1. Bradley-Terry 模型(默认,推荐)

python main.py # or explicitly: python main.py bradley-terry

适用场景:官方排行榜、稳定的排名、生产环境使用

关键特性

  • ✅ Chatbot Arena 官方方法
  • ✅ 使用 sklearn 的 LogisticRegression 做最大似然估计
  • ✅ 与顺序无关(同时处理所有比赛)
  • ✅ 通过 bootstrap(100 个样本)给出 95% 置信区间
  • ✅ 排名更稳定、更可靠

处理时间:约 2-3 分钟(含 bootstrap)

2. 在线 Elo(K=4)

python main.py online-elo

适用场景:理解 Elo 机制、教学用途、计算更快

关键特性

  • ✅ K 因子 = 4(Chatbot Arena 使用的官方值)
  • ✅ 简单的顺序式评分更新
  • ✅ 与顺序相关(按时间顺序处理比赛)
  • ✅ 计算更快(约 30 秒)
  • ⚠️ 稳定性较差,会受比赛顺序影响

方法对比

特性 Bradley-Terry 在线 Elo
稳定性 高(MLE 拟合) 中(顺序式)
顺序依赖
置信区间 有(bootstrap)
速度 较慢(约 3 分钟) 较快(约 30 秒)
官方方法 ✅ 是 仅用于对比
推荐用途 ✅ 生产 教学

两种方法都做了什么

  1. 下载 Chatbot Arena 投票数据(约 2GB,视网络 5-15 分钟)
  2. 应用官方过滤器:
    • 仅保留匿名投票(盲评)
    • 去重(移除前 0.1% 冗余 prompt)
  3. 用所选方法计算模型评分
  4. 计算所有模型两两之间的预测胜率
  5. 生成可视化:
    • leaderboard.png —— 按评分排名的前 20 名模型
    • rating_distribution.png —— 评分直方图与统计量
    • win_rate_matrix.png —— 预测胜率(前 30 名模型)

注意:首次数据下载约 2GB,可能耗时数分钟。下载过程有进度条。

快速演示(合成数据)

若不想下载 2GB 数据就能快速理解 Elo 机制:

python quickstart.py

它会跑一个小型演示,使用 GPT-4、Claude、Llama、Gemini 之间的合成对战。

基准对比

对比两种方法:

python benchmark.py

它展示了在线 Elo 与 Bradley-Terry 两种方法在性能与精度上的差异。

项目结构

elo-leaderboard/ ├── cli.py # Unified argparse CLI (battle / elo / leaderboard / pipeline) ├── battle_simulator.py # Offline synthetic pairwise-battle generator ├── llm_judge.py # LLM-as-judge battles with position-bias mitigation (needs API) ├── main.py # Main analysis script ├── optimized_elo.py # NumPy + Numba Elo rating system ├── parallel_processing.py # Multi-core parallel processing utilities ├── data_loader.py # Data download and preprocessing ├── leaderboard.py # Leaderboard calculation and analysis ├── visualization.py # Static and interactive visualizations ├── animation.py # Animated bar chart race generator ├── benchmark.py # Performance benchmark tool ├── quickstart.py # Quick demo with synthetic data ├── elo_rating.py # Reference implementation (for comparison) ├── test_elo.py # Unit tests ├── requirements.txt # Python dependencies └── README.md # This file

用法示例

构建 Elo 排行榜

from optimized_elo import build_leaderboard_optimized # Build Elo leaderboard from DataFrame elo = build_leaderboard_optimized( df, # DataFrame with columns: model_a, model_b, winner initial_rating=1000.0, k_factor=32.0, show_progress=True ) # Get leaderboard leaderboard = elo.get_leaderboard() for rank, (model, rating, matches, wins) in enumerate(leaderboard[:10], 1): win_rate = wins / matches * 100 if matches > 0 else 0 print(f"{rank}. {model}: {rating:.1f} ({matches} matches, {win_rate:.1f}% win rate)")

加载并过滤数据

from data_loader import load_arena_data, filter_data # Load data df = load_arena_data("arena_data.json") # Filter for blind votes only (reduces bias) df_filtered = filter_data( df, anony_only=True, # Only anonymous votes language="English", # Specific language min_turn=1 # Minimum conversation turn )

构建历史排行榜

from data_loader import get_time_slices from leaderboard import build_historical_leaderboards, get_rating_history # Create weekly time slices time_slices = get_time_slices(df, interval='W') # Build leaderboard for each time point historical_leaderboards = build_historical_leaderboards( df, time_slices, initial_rating=1000.0, k_factor=32.0 ) # Get rating history DataFrame history_df = get_rating_history(historical_leaderboards)

创建可视化

from visualization import ( plot_leaderboard, plot_win_rate_matrix, plot_rating_history, create_interactive_leaderboard ) # Static leaderboard chart plot_leaderboard(leaderboard, top_n=20, save_path="images/leaderboard.webp") # Win rate heatmap win_rate_df = calculate_win_rate_matrix_from_data(df) plot_win_rate_matrix(win_rate_df, top_n=15, save_path="images/matrix.webp") # Rating evolution plot_rating_history(history_df, models=["gpt-4", "claude-v1"], save_path="images/history.webp") # Interactive chart fig = create_interactive_leaderboard(history_df, top_n=15) fig.write_html("interactive.html")

创建动画条形图竞速

from animation import create_simple_animation # Generate animated HTML animation_file = create_simple_animation( history_df, output_path="animation.html", top_n=15 ) # Open animation.html in browser to view

输出文件

运行 main.py 后会生成以下文件:

静态图片(PNG)

  • leaderboard.png —— 按 Elo 评分排名的当前前 20 名模型
  • rating_distribution.png —— 评分分布的直方图与箱线图
  • win_rate_matrix.png —— 两两胜率的热力图
  • rating_history.png —— 评分随时间演化的折线图

交互式可视化(HTML)

  • interactive_rating_evolution.html —— 支持缩放/平移的交互式图表
  • interactive_rank_evolution.html —— 交互式排名追踪
  • leaderboard_animation.html —— 动画条形图竞速,展示排名演化

关键参数

Elo 系统参数(在线 Elo 方法)

  • initial_rating(默认:1000.0):所有模型的起始评分
  • k_factor(默认:4.0):控制更新幅度的学习率
    • 官方 Chatbot Arena 使用 K=4 以保证稳定性
    • 较高的 K 因子(例如 32):更敏感,对新数据适应更快
    • 较低的 K 因子(例如 4):更稳定,受近期比赛影响更小

Bradley-Terry 参数

  • SCALE(400):Elo 尺度参数 —— 决定评分点的含义
  • BASE(10):逻辑函数的底数 —— Elo 计算的标准
  • INIT_RATING(1000):所有模型的初始评分
  • bootstrap_rounds(100):用于置信区间的 bootstrap 样本数

时间切片间隔

进行历史分析时,可调整时间粒度:

  • 'D' —— 每日快照
  • 'W' —— 每周快照(推荐)
  • 'M' —— 每月快照

可视化参数

  • top_n:要展示的头部模型数(推荐 10-20)
  • 动画速度:可在 HTML 界面中调节(1x 到 10x)

数据格式

Chatbot Arena 数据包含以下字段:

  • model_a:第一个模型的标识
  • model_b:第二个模型的标识
  • winner:比赛结果('model_a'、'model_b' 或 'tie')
  • tstamp:投票的 Unix 时间戳
  • judge:投票用户
  • turn:对话轮次
  • anony:投票是否匿名/盲评
  • language:对话使用的语言

校验

该实现会把 Elo 预测值与经验胜率进行对比校验:

from leaderboard import compare_win_rates # Compare predicted vs actual win rates comparison = compare_win_rates(elo_system, empirical_win_rates) mean_error = comparison['error'].mean() print(f"Mean Absolute Error: {mean_error:.4f}")

较低的 MAE(< 0.05)说明 Elo 模型对数据拟合良好。

分析洞察

本项目有助于揭示:

  1. 当前排名:哪些模型目前最强
  2. 评分趋势:模型表现如何随时间演化
  3. 突破时刻:新模型何时进入或搅动排名
  4. 竞争态势:哪些模型实力相近
  5. 长期轨迹:处于上升期还是衰退期的模型
  6. 评分稳定性:模型表现的波动程度

性能架构

该实现专门针对大型数据集(2GB+)做了高性能优化。

核心优化

1. NumPy + Numba JIT 编译

使用 NumPy 数组与 Numba 的即时编译:

  • NumPy 数组实现 O(1) 整数索引(相比字典查找的 O(n))
  • Numba JIT 把热点循环编译为机器码(提速 50-100 倍)
  • 预分配数组消除动态内存分配的开销
  • 整数索引取代字符串模型名,访问对缓存更友好

2. 多核并行处理

把相互独立的操作并行到所有 CPU 核上:

  • 历史分析:每个时间切片独立处理
  • 胜率矩阵:模型两两配对按并行块计算
  • 数据过滤:DataFrame 操作分发到各核
from parallel_processing import build_historical_leaderboards_parallel # Automatically uses all available CPU cores historical_lb = build_historical_leaderboards_parallel( df, time_slices, n_jobs=-1 )

3. 内存优化

通过智能数据类型降低内存占用:

  • 对数值类型做向下转换(int64 → int32,float64 → float32)
  • 把重复字符串转为分类类型
  • 内存占用降低 30-50%
from parallel_processing import optimize_dataframe df = optimize_dataframe(df) # Automatic memory optimization

性能表现

在典型硬件(4-8 核 CPU)上处理完整 2GB 数据集:

组件 技术 效果
Elo 计算 NumPy + Numba JIT 快 50-100 倍
历史分析 多核并行 快 4-8 倍
胜率矩阵 并行处理 快 4-8 倍
内存占用 类型优化 降低 30-50%
总体 综合 约 10-15 倍提速

处理时间:完整数据集(数十万场比赛)约 1-2 分钟

进阶用法

定制分析

模块化设计支持灵活的定制:

聚焦分析

from optimized_elo import build_leaderboard_optimized from data_loader import load_arena_data, filter_data df = load_arena_data("arena_data.json") # Analyze only recent data df_recent = filter_data(df, min_date="2024-01-01") elo_recent = build_leaderboard_optimized(df_recent) # Analyze specific model family gpt_models = [m for m in df['model_a'].unique() if 'gpt' in m.lower()] df_gpt = df[df['model_a'].isin(gpt_models) & df['model_b'].isin(gpt_models)] elo_gpt = build_leaderboard_optimized(df_gpt)

导出结果

import pandas as pd # Export leaderboard to CSV lb_df = pd.DataFrame(leaderboard, columns=['model', 'rating', 'matches', 'wins']) lb_df.to_csv('leaderboard.csv', index=False) # Export rating history history_df.to_csv('rating_history.csv', index=False)

故障排查

数据下载问题

若自动下载失败:

  1. 手动从该地址下载:https://storage.googleapis.com/arena_external_data/public/clean_battle_20240814_public.json
  2. 保存为项目目录下的 arena_data.json
  3. 重新运行 python main.py

内存问题

数据集很大(约 2GB,数十万场比赛)。若在内存有限的机器上遇到内存问题:

from data_loader import load_arena_data, filter_data, get_time_slices from optimized_elo import build_leaderboard_optimized # Load and immediately filter to reduce memory usage df = load_arena_data("arena_data.json") # Filter to recent data only df_filtered = filter_data(df, min_date="2024-01-01", anony_only=True) # Use monthly instead of weekly intervals for historical analysis time_slices = get_time_slices(df_filtered, interval='M') # vs 'W' for weekly # Analyze with smaller top_n for visualizations elo = build_leaderboard_optimized(df_filtered)

内置的内存优化可降低 30-50% 占用,但非常大的分析仍可能需要 4GB 以上内存。

可视化问题

  • 确保已安装 matplotlib、seaborn 与 plotly:pip install -r requirements.txt
  • HTML 动画请使用现代浏览器(Chrome、Firefox、Safari、Edge)
  • 若图表在 Jupyter 中不显示,使用 %matplotlib inline 或保存到文件

参考资料

学习目标

本实验演示了:

  1. 统计建模:如何从两两对比中揭示相对能力
  2. 在线学习:新数据到来时增量更新评分
  3. 概率预测:把评分差转化为胜率
  4. 数据可视化:展示时间动态的有效技巧
  5. 模型评测:传统基准方法之外的替代方案

技术细节

为什么 Elo 计算难以并行

Elo 评分计算天然是顺序的:每场比赛的评分更新都依赖于当前评分,而当前评分又由此前所有比赛修改而来。因此我们不能简单地把比赛切块后独立处理。

不过,我们仍可通过以下方式获得显著提速:

  1. 算法优化:NumPy 数组 + Numba JIT
  2. 并行化相互独立的操作:历史分析、胜率矩阵
  3. 内存效率:更好地利用缓存
  4. 数据结构优化:整数索引、预分配

Numba JIT 编译

核心的 Elo 更新循环用 Numba 编译为机器码:

@jit(nopython=True) def process_elo_updates_vectorized(ratings, model_a_indices, model_b_indices, outcomes, k_factor, match_counts, win_counts): for i in range(len(model_a_indices)): # This loop runs at C speed, not Python speed # Typical speedup: 50-100x over pure Python ...

内存布局

使用 NumPy 数组并配合合适的数据类型:

  • ratings:float64 数组(每个模型 8 字节)
  • match_counts:int32 数组(每个模型 4 字节)
  • model_indices:int32 数组(每场比赛 4 字节)

对 500 个模型、50 万场比赛:约 10 MB,而字典方案约 500 MB。

扩展方向

潜在的增强方向:

  • 实现 Glicko 或 Glicko-2 评分系统(考虑评分的不确定性)
  • 为评分估计添加置信区间
  • 按语言或任务类型分析评分
  • 与其他排名方法对比(例如 TrueSkill、PageRank)
  • 为旧比赛实现时间衰减
  • 增加统计显著性检验
  • 构建未来排名的预测模型
  • 使用 CuPy 做 GPU 加速以处理更大数据集
  • 使用 Dask 做分布式处理以实现多机扩展

许可证

本项目是 AI Agent 实战训练课程资料的一部分。

联系方式

如有问题,请参阅课程资料或讨论区。


发布者: 作者: bojieli 转发
评论区 (0)
U