Skip to content
Published on

LLMベンチマークツールを分解する — 同じMMLUがハーネスごとに違うスコアを出す理由

シェア
Authors

はじめに — 同じモデル、同じMMLU、三つの違うスコア

2023年6月、Hugging FaceのOpen LLM Leaderboardチームは奇妙な苦情を大量に受け取りました。LLaMA 65BのMMLUスコアが原論文の63.4よりかなり低く出るというものでした。調査の結果はバグではありませんでした。三つのMMLU実装が、それぞれ違うやり方で同じデータセットを採点していただけだったのです。

モデルOriginal実装HELMlm-evaluation-harness (2023-01)
llama-65b0.6360.6370.488
llama-30b0.5840.5830.457
llama-13b0.4700.4710.377
llama-7b0.3510.3390.342
falcon-40b0.5580.5710.527

出典はHugging FaceのWhat's going on with the Open LLM Leaderboard?です。ここで注目すべきは差の大きさではなく、差がモデルごとに違うという事実です。65Bでは15点近く開くのに7Bではほぼ変わりません。つまりハーネスを変えるとスコアが一律にずれるのではなく、モデル間の順位が入れ替わるのです。一つの補正定数に換算できる問題ではないということです。

三つの実装の違いはこうです。OriginalはA/B/C/D四文字の確率だけを比較します。HELMはモデルに一文字を生成させ、それを答えとみなします。当時のharnessは選択肢の文全体の対数尤度を比較していました。プロンプトも違いました。Originalは科目名の行を入れ、HELMは「Question:」という接頭辞を付け、harnessは科目名を省いて「Choices:」を入れていました。

この記事の前提はここから来ます。ベンチマークのスコアはモデルの属性ではなく、特定の条件下で行われた測定の結果です。 ところが発表される数字の大半はその条件を省略しています。だから私たちが問い続けるべきことは一つだけです。この数字が見た目どおりの意味を持つには、何が真でなければならないか。

リーダーボードの数字をどこまで信じられるかの大きな見取り図はAIコーディングモデル評価におけるシグナルとノイズの見分け方で、どのベンチマークが何を測っているかの地図はAIエージェント & LLMベンチマーク2026ですでに扱いました。この記事はその下の層、ツールが実際に何をしているかを見ます。

ハーネスが実際にしていること — 六つの段階

「MMLUを回した」という文は、少なくとも六つの段階を圧縮した表現です。各段階に決めるべきことがあり、その決定が記録されなければ再現は不可能です。

1. データ読み込み  どのsplitか(test/validation)、何件か、順序は固定か
        |
2. few-shot構成    例をどのプールから、何個、どんな順序で抽くか、シードは
        |
3. プロンプト組み立て テンプレート、区切り文字、システムメッセージ、chat template適用の有無
        |
4. モデル呼び出し   温度、top_p、max_tokens、停止文字列、リトライ、バッチサイズ
        |
5. 正解パース      対数尤度比較か、正規表現抽出か、最後の数字か
        |
6. 採点・集計      完全一致、部分点、モデル採点、そして平均を取る単位

各段階がスコアをどう動かすかを一行でまとめると次のとおりです。

段階よく省略される設定スコアへの影響
データ読み込みtestとvalidationのどちらを使ったか科目ごとに数点の差、比較不能
few-shot構成例の個数、例の選択シード0-shotと5-shotが二桁差になるタスクが珍しくない
プロンプト組み立てchat template適用の有無指示チューニング済みモデルで特に大きい
モデル呼び出し温度とmax_tokens温度0.7なら同じコマンドでも毎回スコアが違う
正解パース抽出用の正規表現GSM8Kでstrictとflexibleがずれるポイント
採点・集計サブタスクの平均方式マクロ平均とマイクロ平均で違う順位になる

MMLU-Pro論文(arXiv 2406.01574、NeurIPS 2024)は、この感度を実際に測定しました。24種類のプロンプトスタイルで回したところ、MMLUではスコアの変動がおおむね4〜5ポイント、最大で10.98ポイントでした。選択肢を10個に増やしたMMLU-Proでは、その変動がおおむね2ポイント、最大3.74ポイントに縮みました。裏を返せば、元のMMLUではプロンプトを変えただけでリーダーボードの中位を貫くほどスコアが動くということです。

さらに極端な測定もあります。SclarらのQuantifying Language Models' Sensitivity to Spurious Features in Prompt Design (arXiv 2310.11324)は、意味が同一の形式変更だけでLLaMA-2-13Bにおいて最大76点の正解率の差が出たと報告しています。2025年のA Single Character can Make or Break Your LLM Evals (arXiv 2510.05152)はさらに一歩進みます。例を区切る文字一つを変えるだけでMMLUのスコアが最大23パーセント動き、その操作だけで狙ったモデルを1位に押し上げられると報告しています。プロンプトの形式は実験設定ではなく測定装置の一部です。

正解の選び方そのものがスコアを作る

選択式タスクで「モデルが答えを選んだ」を実装する方法は、少なくとも三つあります。三つは異なる能力を測っています。

対数尤度比較。 モデルに生成をさせません。プロンプトの後ろに各選択肢の文字列を並べ、その文字列の対数確率を計算し、もっとも高いものを選んだ答えとみなします。生成がないので決定的で速いです。その代わり、指示に従う能力はまったく測りません。指示を無視するベースモデルでも通常のスコアを得ます。

文字確率比較。 プロンプトを「正解: 」まで組み立て、次のトークンがA、B、C、Dである確率だけを比較します。選択肢の長さの影響を受けませんが、モデルが実際には「Answer: B」のように答えていたかもしれない状況を無視します。

生成してからパース。 モデルに実際に答えさせ、テキストから答えを抽出します。実使用にもっとも近いです。その代わり、パースのルールが採点の一部になります。

対数尤度方式にはよく知られた罠がもう一つあります。長い文字列はトークンが多いので、対数確率の合計は自然と小さくなります。だから何の補正もなく合計を比較すると、短い選択肢が構造的に有利になります。lm-evaluation-harnessがaccと一緒にacc_normを報告する理由がこれです。acc_normは対数尤度を選択肢の長さで割って、このバイアスを相殺します。この二つの数字が大きく開いているタスクは、そのタスクが選択肢の長さの分布に敏感だというシグナルです。

韓国語で評価するときは、ここにもう一つ加わります。EleutherAIの正規化説明記事はこの補正をバイト長正規化と呼び、トークン化方式に依存しないことが利点だと説明しています。ところがissue 3278は、実際のコードが文字列の文字数で割っていると指摘しています。英語では両者は事実上同じですが、UTF-8で一文字が3バイトになる韓国語・日本語・中国語では違います。ハングルの選択肢と英文の選択肢が混ざった評価セットなら、この差がそのままスコアに入り込みます。正規化方式をドキュメントではなくコードで確認しなければならない理由がこれです。

生成してからパースする側の代表例はGSM8Kです。lm-evaluation-harnessのgsm8kタスクは、一回の実行で二つのスコアを出します。

フィルタ抽出ルール見落とすもの
strict-match指定された形式(たとえば四つの井桁の後ろの数字)に正確に合う答えのみ形式を守らなかった正解を全部誤答扱いする
flexible-extract出力中の最後の数字を答えとみなす計算の途中の値が最後に来ると誤答、通貨記号・カンマの正規化が不十分

同じ実行、同じ出力なのに、二つの数字が出ます。ところが論文やモデルカードに引用されるGSM8Kのスコアはたいていそのうちの一つだけで、どちらかを明示していません。flexible-extractフィルタが通貨記号やカンマを正しく正規化できず、正しい答えを誤りと判定するというissueもリポジトリに上がっています(issue 3214)。パーサーのバグがモデルのスコアとして記録される構造です。

代表的なハーネスの比較

2026年8月現在、実務や論文で実際に使われているツールをまとめると次のとおりです。バージョンは確認時点の最新リリースです。

ツールバージョン(確認日 2026-08-02)哲学採点方式の特徴
lm-evaluation-harness (EleutherAI)lm-eval 0.4.12, 2026-05-11タスクをYAMLで宣言、60以上のベンチマーク対数尤度・生成の両方、フィルタチェーンでパース
HELM (Stanford CRFM)crfm-helm 0.5.16, 2026-04-30一つのタスクを複数指標で同時に正確さに加え校正・頑健性・公平性・毒性・効率を一緒に
Inspect AI (UK AISI, Meridian Labs)inspect-ai 0.3.251, 2026-07-29エージェント・ツール使用まで第一級の対象にsolverとscorerを分離、モデル採点とサンドボックスを内蔵
promptfoonpm 0.121.20, 2026-07-31YAML宣言型、プロンプト・モデルのグリッド比較アサーションベース、レッドチームスキャナー付き
DeepEval4.1.5, 2026-07-31pytestスタイル、アプリケーション指標G-Evalと決定木型の判定(DAG)
LightEval (Hugging Face)0.13.0, 2025-11-24バックエンド非依存の軽量パイプラインInspect AIを優先バックエンドとして採用
OpenCompass (Shanghai AI Lab)0.5.3, 2026-06-29大規模な総合評価ツールモデル採点ユーティリティを含む
Ragas0.4.3, 2026-01-13RAG専用の指標集忠実性・回答関連性・コンテキスト精度
EvalPlus0.3.1, 2024-10-20HumanEval・MBPPのテストを強化テストを大幅に増やして弱い採点を補う
OpenAI simple-evalsリリースタグなし参照実装の公開が目的0-shot chain-of-thoughtに固定

いくつか押さえておきたい点があります。

HELMの視点は違います。 HELMは「このモデルのMMLUスコアは何点か」ではなく、「このシナリオでこのモデルは正確さ・校正・頑健性・公平性・毒性・効率のそれぞれにおいてどうか」を問います。原論文(arXiv 2211.09110)の表現どおり、16のコアシナリオごとに7つの指標を一緒に測る多指標アプローチです。実行はhelm-runで行い、helm-summarizeで集計し、helm-serverで結果をブラウザで見ます。

ただし状態の変化を知っておく必要があります。HELMは2026年6月1日からメンテナンスモードに移行しました。コードとリーダーボードは公開され続けますが、新機能と新しい評価は追加されません。ツールとして新規に採用する際はこの点を考慮する必要があり、逆に過去のHELMスコアを引用するときは、その時点のリーダーボードが今も更新され続けていると仮定してはいけません。

HELMが選択式を扱う方式も知っておく価値があります。選択肢を全部提示してモデルに文字を生成させる方式(joint)、選択肢ごとに個別にスコアを付ける方式(separate)、そして選択肢自体の事前確率で補正する方式(separate-calibrated)があります。最近のAPIがトークン確率を公開しないケースが増えるにつれjointへ傾いていますが、この選択一つが前の表で見た15点差の軸でした。

Inspect AIはエージェント評価を前提に設計されています。 タスクをDataset、Solver、Scorerの三つに分けます。Solverは単純な一回の生成でもよいし、ツールを何ターンも使う完全なエージェントでもよく、Scorerは文字列比較からモデル採点まで付けられます。モデルが作ったコードを安全に動かすためのDocker・Kubernetesサンドボックスが標準で付属します。英国AI Security InstituteとMeridian Labsが共同で開発しており、安全性評価の分野で事実上の標準です。Hugging FaceのLightEvalがこれを優先バックエンドとして採用したことは、2026年の目立った収斂のシグナルです。

OpenAI simple-evalsはツールというより参照実装です。 存在理由は、OpenAIがモデルと一緒に発表する正確さの数字をどんなプロンプトとどんな設定で出したのかを公開することです。0-shot chain-of-thoughtにこだわっていますが、それはfew-shotプロンプトがベースモデル時代の遺産であり、実使用をあまり反映しないという判断からです。この選択自体が、他のハーネスとの比較不能性を生みます。面白い細部もあります。サンプラーの実装ごとにデフォルトの温度が違い、チャットAPI用のサンプラーは温度0.5で最大1024トークン、Claude用のサンプラーは温度0.0で最大4096トークンがデフォルトです。同じリポジトリのなかでもモデルによってサンプリング条件が違います。

混同しやすい二つを区別しておきます。GitHubのopenai/evalsリポジトリはアーカイブされておらず廃止告知もありませんが、実質的には休眠状態です(PyPIパッケージは2024年以降更新なし)。一方でOpenAIがホストしていたEvalsダッシュボードとアプリケーションプログラミングインターフェースは2026年6月3日に廃止が告知され、2026年10月31日に読み取り専用、11月30日に終了する日程です。リポジトリとプラットフォームは別物です。

promptfooは2026年3月9日にOpenAIに買収されました(OpenAIの発表)。オープンソースライセンスは維持されると告知されています。ツール選定の際、メンテナンスの主体が変わったことは知っておく価値があります。

もう一つ。Hugging FaceのOpen LLM Leaderboardは2025年3月13日に運営を終了しました。この記事の冒頭のMMLU表を作った、まさにそのリーダーボードです。リーダーボードが消えても、そこから出たスコアは論文やモデルカードに引用され続けるので、引用されたスコアの出典が今も生きているかを確認する習慣が必要です。

最初から最後まで一回動かしてみる

説明よりも一回動かしてみるほうが早いです。ノートPCのCPUでも終わるサイズにしてあります。

python3 -m venv .venv
source .venv/bin/activate
pip install "lm-eval[api]==0.4.12"

# どんなタスクがあるかまず確認
lm_eval --tasks list | head -40

小さなモデルでARC-Easyを200問だけ回します。シードとfew-shot数を明示するのが核心です。

lm_eval \
  --model hf \
  --model_args pretrained=EleutherAI/pythia-160m,dtype=float32 \
  --tasks arc_easy \
  --num_fewshot 5 \
  --batch_size 8 \
  --device cpu \
  --seed 1234 \
  --limit 200 \
  --output_path ./results/pythia-160m \
  --log_samples

ここで各フラグが何を固定しているかが重要です。

  • --num_fewshot 5なしで回すと、タスクYAMLのデフォルト値が使われます。タスクごとに違い、バージョンが変わると一緒に変わります。
  • --seedはfew-shot例の抽出とシャッフルを固定します。外すと実行のたびに違うプロンプトが作られます。
  • --limit 200は先頭200個だけを切り出します。無作為標本ではないので、この数字を全体スコアとして引用してはいけません。デバッグ用です。
  • --log_samplesが本当に重要です。プロンプト原文、モデルの出力、パース結果がすべて保存されます。これがないと、スコアがなぜそうなったのかを事後に知る術がありません。

実行が終わると結果ディレクトリにJSONができます。構造はおおよそこうです。正確なキー名はバージョンによって変わり得るので、直接開いて確認することを勧めます。

{
  "results": {
    "arc_easy": {
      "alias": "arc_easy",
      "acc,none": 0.395,
      "acc_stderr,none": 0.0346,
      "acc_norm,none": 0.365,
      "acc_norm_stderr,none": 0.0341
    }
  },
  "configs": {
    "arc_easy": {
      "task": "arc_easy",
      "output_type": "multiple_choice",
      "num_fewshot": 5,
      "metric_list": [{ "metric": "acc" }, { "metric": "acc_norm" }]
    }
  },
  "config": {
    "model": "hf",
    "model_args": "pretrained=EleutherAI/pythia-160m,dtype=float32",
    "batch_size": 8,
    "random_seed": 1234,
    "limit": 200
  },
  "git_hash": "…",
  "date": 1785000000
}

このJSONで実際に見るべきなのはresultsではなくconfigsとconfigです。スコア一つを引用するときに一緒に添えるべき情報が全部ここに入っています。accとacc_normが3ポイント開いているという事実も、ここでしか見えません。

指示チューニング済みモデルをAPIで測る場合はコマンドが変わります。vLLMやOllamaのようなOpenAI互換エンドポイントを立ててから、こう接続します。

lm_eval \
  --model local-chat-completions \
  --model_args model=qwen3-8b,base_url=http://localhost:8000/v1/chat/completions,num_concurrent=8,max_retries=3,tokenized_requests=False \
  --tasks gsm8k \
  --num_fewshot 5 \
  --apply_chat_template \
  --fewshot_as_multiturn \
  --gen_kwargs temperature=0,max_gen_toks=512 \
  --output_path ./results/qwen3-8b \
  --log_samples

--apply_chat_template--fewshot_as_multiturnは、指示チューニング済みモデルを測るときほぼ必ず必要です。前者はプロンプトをモデルが学習した対話形式で包み、後者はfew-shot例を一つの長いテキストではなく実際にやり取りされた対話のターンとして入れます。この二つのフラグの有無だけで、指示チューニング済みモデルのスコアが大きく分かれます。ところが発表された数字でこの情報を見つけられることはまれです。

対数尤度をサポートしないAPIもあります。その場合、選択式タスクを対数尤度で回すことができないので、generate_until系列しか使えません。つまりモデルをAPIで測るか重みで測るかによって、そもそも使える採点方式が変わります。 これだけで同じベンチマークの二つのスコアが比較不能になり得ます。

タスク定義を直接読んで書き換えてみる

lm-evaluation-harnessでは、タスクはYAMLファイル一つです。社内データで同じハーネスを使いたいなら、このファイルだけ書けばよいのです。まず選択式から見ます。

task: internal_mcq
dataset_path: json
dataset_kwargs:
  data_files:
    test: ./data/internal_mcq.jsonl
output_type: multiple_choice
test_split: test
doc_to_text: "次の質問に答えてください。\n質問: {{question}}\n答え:"
doc_to_choice: "{{choices}}"
doc_to_target: "{{label}}"
num_fewshot: 5
fewshot_config:
  sampler: default
metric_list:
  - metric: acc
    aggregation: mean
    higher_is_better: true
  - metric: acc_norm
    aggregation: mean
    higher_is_better: true
should_decontaminate: false
metadata:
  version: 1.0

ここでdoc_to_textの文字列を一文字変えるだけでもスコアは動きます。先のMMLUの事例がまさにその現象でした。だからこのファイルは必ずバージョン管理下に置かねばならず、metadataのversionは定義を変えるたびに上げなければなりません。このフィールドが存在する理由は、「先月のスコアと今月のスコアが同じ定義から出たか」を確認するためです。

生成式タスクにはフィルタチェーンが付きます。パースのルールが明示的に現れる箇所です。

task: internal_math
dataset_path: json
dataset_kwargs:
  data_files:
    test: ./data/internal_math.jsonl
output_type: generate_until
test_split: test
doc_to_text: "問題: {{question}}\n解法を書き、最後の行に「答え: 数字」の形式でのみ答えてください。\n"
doc_to_target: "{{answer}}"
generation_kwargs:
  until:
    - "\n\n"
  do_sample: false
  temperature: 0.0
  max_gen_toks: 512
filter_list:
  - name: strict-match
    filter:
      - function: regex
        regex_pattern: "答え:\\s*(-?[0-9][0-9,]*(?:\\.[0-9]+)?)"
      - function: take_first
  - name: last-number
    filter:
      - function: regex
        regex_pattern: "(-?[0-9][0-9,]*(?:\\.[0-9]+)?)"
        group_select: -1
      - function: take_first
metric_list:
  - metric: exact_match
    aggregation: mean
    higher_is_better: true
    ignore_case: true
    ignore_punctuation: false
metadata:
  version: 1.0

同じ実行から二つのフィルタがそれぞれスコアを出します。二つのスコアの差はモデルの数学の実力ではなく、モデルが指示した出力形式をどれだけ守るかです。この二つの数字を並べて見る習慣が重要です。strictが低くlast-numberが高いなら、モデルは計算はできるが形式を守っていないということで、これはプロンプトで直せる問題です。両方低いなら計算ができないということで、これはモデルを変えなければならない問題です。一つの数字だけを報告していたら、この区別は消えます。

カンマの処理を見てください。正規表現に[0-9,]*が入っています。これがないと、モデルが「1,024」と答えたとき「1」だけが抽出され誤答になります。正解パースのルールはこうした決定の積み重ねであり、その積み重ねがスコアそのものです。

再現に必要な最小条件のリスト

スコア一つを再現するには、以下がすべて必要です。一つでも欠ければ、それは再現ではなく近似です。

項目なぜ必要かどこに記録するか
モデル識別子とリビジョン同じ名前のAPIモデルが静かに更新される重みはコミットハッシュ、APIはスナップショットID
ハーネスのバージョンタスク定義とフィルタがリリースごとに変わるlm-eval 0.4.12のように正確に
タスク定義のバージョンプロンプトの一文字がスコアを動かすYAMLのmetadata.versionとファイルハッシュ
few-shot数とシード例が変われば別の試験問題コマンドラインフラグを原文のまま
サンプリングパラメータ温度が0でなければ毎回違うスコアtemperature、top_p、max_tokens、停止文字列
プロンプト組み立て方式chat template有無が特に大きいapply_chat_template、システムメッセージ原文
正解パースのルールパーサーが採点の一部フィルタ名と正規表現の原文
集計単位マクロとマイクロで違う順位になるサブタスクの重み付け方式
評価対象の部分集合limitやサブサンプリングは全体ではない問題数と選択方式
実行回数と分散一回回した数字は幅を隠す反復回数、平均と標準誤差
実行環境バッチサイズと精度が結果を変えるdtype、バッチサイズ、推論バックエンドとバージョン

最後の項目は見落とされがちですが、実際に影響があります。同じ重みでもfloat16とbfloat16は違う答えを出すことがあり、バッチサイズによってパディングとカーネルの選択が変わり、vLLMとtransformersは同じプロンプトに違うトークンを出すことがあります。この層の差はたいてい小さいものの、リーダーボードの1位と3位を分ける幅より大きいことが珍しくありません。

実務でこのリストを扱うもっとも安上がりな方法は、評価の実行を人が打つコマンドではなくファイルにすることです。

# evals/run-2026-08-02.yaml — このファイル自体をコミットする
harness: lm-eval==0.4.12
model:
  provider: local-chat-completions
  base_url: http://localhost:8000/v1/chat/completions
  name: qwen3-8b
  weights_revision: 8f2c1d9
sampling:
  temperature: 0
  max_gen_toks: 512
tasks:
  - name: gsm8k
    num_fewshot: 5
    apply_chat_template: true
    fewshot_as_multiturn: true
    report_filters: [strict-match, flexible-extract]
repeats: 3
seed: 1234

このファイルがあれば、「そのスコア、どうやって出したんですか」という問いにコミットハッシュ一つで答えられます。なければ、どれだけ誠実な人でも三ヶ月後には答えられません。

採点器こそがベンチマークです

ここまで来ると結論は一つに収まります。ベンチマークのアイデンティティはデータセットではなく採点器にあります。同じ14,000問を前にしても、採点器が違えば違う試験です。

採点方式は大きく三つに分かれ、それぞれ違う壊れ方をします。

採点方式動作強み静かに崩れる地点
完全一致文字列比較決定的、再現可能正解なのに表記が違って誤答扱い
正規表現抽出パターンで答えだけ抽出形式の自由度を許容パターンが拾えない表現は全部0点
モデル採点別のLLMが判定自由記述の採点が可能審査者のバイアスがスコアに混ざる
実行ベーステストを回して判定意味的に強いテストが弱いと間違った答えも通る

正規表現抽出の脆さは先ほどGSM8Kで見ました。モデル採点のバイアスは次の記事で詳しく扱います。実行ベース採点の罠 — テストが弱くて間違ったパッチが通る問題 — はSWE-bench系列で実際に測定されており、これも次の記事のテーマです。

ここでは実務的なルールを一つだけ残します。新しいベンチマークを導入するときは、データセットより先に採点コードを読んでください。 問題を数問目で見るより、採点関数30行を読むほうが、そのベンチマークが何を測っているかをはるかに正確に教えてくれます。そして自分のチームの失敗事例10個をその採点器に通してみてください。人間から見て明らかに正しい答えが0点になるケースが一つでも出たら、そのベンチマークのスコアはあなたが思っているものとは違うものを測っています。

おわりに — 条件のない数字は数字ではありません

LLaMA 65BのMMLUが63.6であり同時に48.8でもあった事件は、例外的な事故ではなく評価の基本的な性質でした。ベンチマークのスコアは、モデル・ハーネス・プロンプト・パーサー・サンプリング設定が一緒になって作り出した一つの観測値です。そのうち一つが変わるだけで別の観測値になります。

だから実務で守るべきことは二つだけです。第一に、外部のスコアを引用するときは、そのスコアがどんな条件から出たのか確認できるかをまず見てください。確認できないなら、それはデータではなく主張です。第二に、自分のチームのスコアを出すときは、条件をファイルに残してください。コマンドラインに打ったフラグは三ヶ月で消えますが、コミットされたYAMLは残ります。

数字を信頼する方法は、数字をもっと集めることではなく、その数字を作った条件を書き残すことです。

参考資料