源文件:chapter6/elo-leaderboard/README.md 基于两两对比的 Elo 评分排行榜 实验 6-6:从两两对比数据构建模型排行榜 本项目从零实现一套 Elo 评分系统,基于 Chatbot Arena 公开投票数据分析模型表现。该实现演示了 Bradley-Terry 模型如何从数百万次两两对比投票中提取模型的相对能力。 概览 Elo 评分系统是一种在零和博弈中计算参赛者(本项目里即 AI 模型)相对水平的方法。它最初为国际象棋而设计,后来被改造为依据用户投票的头对头对比,对 AI 语言模型进行排名。
源文件:chapter6/elo-leaderboard/README.md
实验 6-6:从两两对比数据构建模型排行榜
本项目从零实现一套 Elo 评分系统,基于 Chatbot Arena 公开投票数据分析模型表现。该实现演示了 Bradley-Terry 模型如何从数百万次两两对比投票中提取模型的相对能力。
Elo 评分系统是一种在零和博弈中计算参赛者(本项目里即 AI 模型)相对水平的方法。它最初为国际象棋而设计,后来被改造为依据用户投票的头对头对比,对 AI 语言模型进行排名。
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 是预期得分(预测的胜率)cd projects/week6/elo-leaderboard pip install -r requirements.txt
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-8 → anthropic/claude-opus-4.8,claude-haiku-4-5 → anthropic/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 的做法,实现了两种排名方法:
python main.py # or explicitly: python main.py bradley-terry
适用场景:官方排行榜、稳定的排名、生产环境使用
关键特性:
处理时间:约 2-3 分钟(含 bootstrap)
python main.py online-elo
适用场景:理解 Elo 机制、教学用途、计算更快
关键特性:
| 特性 | Bradley-Terry | 在线 Elo |
|---|---|---|
| 稳定性 | 高(MLE 拟合) | 中(顺序式) |
| 顺序依赖 | 无 | 高 |
| 置信区间 | 有(bootstrap) | 无 |
| 速度 | 较慢(约 3 分钟) | 较快(约 30 秒) |
| 官方方法 | ✅ 是 | 仅用于对比 |
| 推荐用途 | ✅ 生产 | 教学 |
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
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 后会生成以下文件:
leaderboard.png —— 按 Elo 评分排名的当前前 20 名模型rating_distribution.png —— 评分分布的直方图与箱线图win_rate_matrix.png —— 两两胜率的热力图rating_history.png —— 评分随时间演化的折线图interactive_rating_evolution.html —— 支持缩放/平移的交互式图表interactive_rank_evolution.html —— 交互式排名追踪leaderboard_animation.html —— 动画条形图竞速,展示排名演化进行历史分析时,可调整时间粒度:
'D' —— 每日快照'W' —— 每周快照(推荐)'M' —— 每月快照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 模型对数据拟合良好。
本项目有助于揭示:
该实现专门针对大型数据集(2GB+)做了高性能优化。
使用 NumPy 数组与 Numba 的即时编译:
把相互独立的操作并行到所有 CPU 核上:
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 )
通过智能数据类型降低内存占用:
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)
若自动下载失败:
arena_data.jsonpython 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 以上内存。
pip install -r requirements.txt%matplotlib inline 或保存到文件本实验演示了:
Elo 评分计算天然是顺序的:每场比赛的评分更新都依赖于当前评分,而当前评分又由此前所有比赛修改而来。因此我们不能简单地把比赛切块后独立处理。
不过,我们仍可通过以下方式获得显著提速:
核心的 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。
潜在的增强方向:
本项目是 AI Agent 实战训练课程资料的一部分。
如有问题,请参阅课程资料或讨论区。