Med-LLMベンチマークの再現:HealthBench、MedQA、PubMedQA、MMLU-Medical – 自己再現と公開された数値の検証、および測定されたデルタ
Med-LLMのパフォーマンスに関する発表は毎月更新され、各ベンダーは自社のベンチマークで最先端の結果を主張しています。しかし、これらの結果は本当に再現可能なのでしょうか?論文や企業発表で公開されている数値は、ユーザーが実際にベンチマークを実行した場合の結果とどれだけ一致しているのでしょうか?この記事では、公開されている医療ベンチマーク(HealthBench、MedQA-USMLE、PubMedQA、MMLU-Medical、MedMCQA)を自己再現し、さまざまなベンダーやオープンソースモデルのパフォーマンスを測定し、それらを公開された数値と比較するための、実践的なパイプラインを紹介します。もし第9章が「同じ実践的なタスクでベンダーを比較する」ことについてであれば、この記事は「同じ標準ベンチマークで再現性を検証する」ことについてです。これは、ベンダーの発表を信頼できるかどうかを判断するためのツールです。
📚 推奨される前提知識(強く推奨します)
この記事は、AIと生物学を深く掘り下げたものです。続行する前に、DryBenchの以下の記事をレビューすることをお勧めします。
- DryBench ai-native #7 プロンプトエンジニアリング
- DryBench ai-native #11 ハルシネーションとアライメント
- DryBench ai-native #13 Hugging Faceと商用API
前提知識の記事を省略すると、この記事の実用的なコードを理解することが難しくなります。なぜなら、few-shotプロンプトの再現、Chain-of-Thought(CoT)、LLM-as-judge評価、およびHugging Faceデータセットのロードについて、改めて説明することなく進めるからです。
これはすでに私たちのDryBenchで学びました
DryBench ai-native #7では、プロンプトが結果に大きな影響を与えることを学びました。#11では、ハルシネーションがベンチマーク評価にバイアスを生み出す可能性があることを学びました。そして#13では、ベンチマークをHugging Faceのdatasetsとevaluateライブラリを使用して標準化および再現できることを学びました。
Med-LLMベンチマークは、特にこれらの3つの原則に敏感です。医療QAには、明確な答えのある選択問題(MedQA、MMLU-Medical、MedMCQA)、根拠となる証拠の要約(PubMedQA)、および診断のための自由形式の応答(HealthBench)を含む、さまざまなタスクが混在しており、各タスクには異なる評価指標があります。さらに、ベンダーが公開する数値には、調整されたプロンプト、特定のサブセット、自己整合性、CoTなどの最適化が含まれていることが多く、完全な再現は困難です。この記事では、これらの再現性の問題に正面から取り組みます。
中核となる問題を定義する
実用的な要件
- 標準ベンチマークの再現: MedQA、PubMedQA、MMLU-Medical、MedMCQA、およびHealthBenchの精度指標を計算します。
- 複数のモデルの実行: Claude Opus/Sonnet、GPT-4o、o1、Gemini 2.5、Meditron、MedGemma、Med42など、少なくとも6つのモデルを実行します。
- 再現性のための記録: データセットのバージョン、モデルのスナップショット、プロンプト、実行時間、CoTの使用、およびfew-shotの例の数を記録します。
- 公開データと測定データのデルタレポート: 公開された結果と再現された結果を比較する行列を作成します(例:「論文Xでは76%と報告されており、私たちの再現では71%(デルタ-5%)でした」)。
- オープン、クローズド、および特殊なモデルの比較: 商用API、ローカルのオープンソースモデル、および医療に特化したモデルの精度を比較します。
ベンチマーク固有の特性
- MedQA (USMLE) [1]: 米国医師免許試験スタイルの選択問題。4つまたは5つの選択肢があります。評価指標は精度です。
- PubMedQA [2]: PubMedの抄録に基づいたYes/No/Maybeの質問応答。根拠となる証拠の要約が含まれます。評価指標は精度とF1スコアです。
- MMLU-Medical (サブセット) [3]: 臨床知識、解剖学、医学部、専門医学を含む、MMLUベンチマークのサブセット。評価指標は精度です。
- MedMCQA [4]: インドの医師国家試験(AIIMS/NEET)スタイルの選択問題。評価指標は精度です。
- HealthBench (OpenAI 2025) [5]: 実際の臨床シナリオに対する自由形式の応答。LLM-as-judgeまたはルーブリックに基づいてスコアリングされます。
- CasesMD、DiagnosisBench、およびその他の新しいベンチマークも存在します。
この記事の目標指標
- 少なくとも24〜40個のセルを含む行列。4〜5つのベンチマークと6〜8つのモデルを使用します。
- 各セルには、精度、95%信頼区間、および再現性デルタ(公開された結果と比較)を表示します。
- プロンプト、サブセット、自己整合性、およびCoTに焦点を当て、再現性の失敗の原因(公開された結果と比較して5%以上のデルタを持つセル)を分析します。
- このプロセス全体を、オープンソースコードを使用して再現可能にします(ただし、ライセンス条項を遵守します)。
ツールスタックとインフラ要件
| ツール | 役割 | ライセンス |
|---|---|---|
Hugging Face datasets | ベンチマークデータセットの読み込み | Apache 2.0 |
Hugging Face evaluate | 標準的な評価指標 | Apache 2.0 |
| Part 09's VendorAdapter (Anthropic, OpenAI, Google) | 商用APIの呼び出し | 各SDKのライセンス |
Hugging Face transformers + vLLM (オプション) | オープンソースモデルのローカル推論 | Apache 2.0 |
| pandas, matplotlib, seaborn | 表とヒートマップの作成 | BSD |
| jsonlines, pyyaml | 結果ログの保存 | MIT, MIT |
| scipy, statsmodels | 信頼区間、有意性検定 | BSD |
インフラ要件:
- GPUは必須ではありません(主に商用APIの使用に焦点を当てます)。オープンソースモデルをローカルで実行する場合、小規模から大規模のGPUが必要です(Meditron 7Bは小規模GPUで実行できますが、70Bには少なくとも24GBのVRAMを備えたデータセンターGPUが必要です)。
- 少なくとも16GBのRAMが必要です。
- ディスク容量: すべてのベンチマークデータセット用に約500MB、さらにオープンソースモデルの重み用に追加の容量が必要です(Meditron 70Bにはfp16形式で約140GBが必要です)。
再現のための推定学習コスト: 6ベンダー × 4ベンチマーク × 各ベンチマークの質問数(約500~2000)= 商用APIの使用で約30~120米ドル。
実用的なパイプラインの実装
全体の流れ:
ステップ 1. ベンチマークデータセットのロード
HuggingFace Hubから各ベンチマークをロードします。各ベンチマークのライセンスは個別に確認してください。異なる場合があります。
from dataclasses import dataclass, fieldfrom typing import Literal
from datasets import load_dataset
@dataclassclass BenchQuestion: bench_name: str qid: str question: str choices: list[str] | None # 多肢選択の場合はリスト、自由記述の場合はNone gold_answer: str # 多肢選択の場合は"A"/"B"/...、自由記述の場合はルーブリックJSON metadata: dict = field(default_factory=dict)
def load_medqa(split: str = "test", max_samples: int | None = None) -> list[BenchQuestion]: """MedQA(USMLEスタイル)をロードします。リポジトリ:bigbio/med_qa。""" ds = load_dataset("bigbio/med_qa", "med_qa_en_source", split=split) if max_samples: ds = ds.select(range(min(max_samples, len(ds)))) questions = [] for i, row in enumerate(ds): questions.append(BenchQuestion( bench_name="medqa", qid=f"medqa_{i}", question=row["question"], choices=[opt["value"] for opt in row["options"]], gold_answer=row["answer_idx"], # "A", "B", ... metadata={"meta_info": row.get("meta_info", "")}, )) return questions
def load_pubmedqa(split: str = "train", max_samples: int | None = None) -> list[BenchQuestion]: """PubMedQA(yes/no/maybe)をロードします。リポジトリ:qiaojin/PubMedQA labeled subset。""" ds = load_dataset("qiaojin/PubMedQA", "pqa_labeled", split=split) if max_samples: ds = ds.select(range(min(max_samples, len(ds)))) questions = [] for i, row in enumerate(ds): context = " ".join(row["context"]["contexts"]) questions.append(BenchQuestion( bench_name="pubmedqa", qid=f"pmqa_{i}", question=f"Context: {context}\n\nQuestion: {row['question']}\nAnswer one of: yes/no/maybe:", choices=["yes", "no", "maybe"], gold_answer=row["final_decision"], metadata={"pmid": row.get("pubid", "")}, )) return questions
def load_mmlu_medical(split: str = "test", max_samples: int | None = None) -> list[BenchQuestion]: """MMLUの医療サブセットをロードします。リポジトリ:cais/mmlu。""" subsets = ["clinical_knowledge", "college_medicine", "anatomy", "professional_medicine", "medical_genetics"] questions = [] for subset in subsets: ds = load_dataset("cais/mmlu", subset, split=split) if max_samples: ds = ds.select(range(min(max_samples // len(subsets), len(ds)))) for i, row in enumerate(ds): questions.append(BenchQuestion( bench_name=f"mmlu_{subset}", qid=f"mmlu_{subset}_{i}", question=row["question"], choices=row["choices"], gold_answer=["A", "B", "C", "D"][row["answer"]], metadata={"subset": subset}, )) return questions
def load_medmcqa(split: str = "validation", max_samples: int | None = None) -> list[BenchQuestion]: """MedMCQA(インドの医療試験)をロードします。リポジトリ:openlifescienceai/medmcqa。""" try: ds = load_dataset("openlifescienceai/medmcqa", split=split) except Exception: return [] if max_samples: ds = ds.select(range(min(max_samples, len(ds)))) questions = [] for i, row in enumerate(ds): questions.append(BenchQuestion( bench_name="medmcqa", qid=f"medmcqa_{i}", question=row["question"], choices=[row["opa"], row["opb"], row["opc"], row["opd"]], gold_answer=["A", "B", "C", "D"][row["cop"]], metadata={"subject_name": row.get("subject_name", "")}, )) return questions
def load_healthbench(split: str = "test", max_samples: int | None = None) -> list[BenchQuestion]: """HealthBench(OpenAI 2025)をロードします。リポジトリとライセンスは事前に確認する必要があります。
実際のレポジトリ名は、公開時に確認する必要があります。これは概念的なスタブです。 """ try: ds = load_dataset("openai/healthbench", split=split) except Exception: # 代替案:論文の付録からサンプルシナリオを手動でロードする return [] if max_samples: ds = ds.select(range(min(max_samples, len(ds)))) questions = [] for i, row in enumerate(ds): questions.append(BenchQuestion( bench_name="healthbench", qid=f"hb_{i}", question=row["scenario"], choices=None, gold_answer=row.get("rubric_json", "{}"), metadata=row.get("metadata", {}), )) return questionsステップ 2. プロンプトテンプレート(オリジナルの論文の再現)
オリジナルの論文で使用されたプロンプトをできるだけ正確に再現します。ゼロショット、少数ショット、CoTバリエーション用の個別のテンプレートを作成します。
ZERO_SHOT_MEDQA = """以下の医療国家試験の質問に答えてください。
質問:{question}
選択肢:{choices_formatted}
答え(1つの文字のみ:A/B/C/D/E):"""
ZERO_SHOT_COT_MEDQA = """以下の医療国家試験の質問に答えてください。まず、段階的に推論し、その後、最終的な答えを提供してください。
質問:{question}
選択肢:{choices_formatted}
段階的な推論:"""
FEW_SHOT_MEDQA = """医療国家試験の質問とその答えのいくつかの例を示します。
例1:質問:65歳の男性が、突然の胸の痛み、発汗、吐き気があり、3時間持続しています。心電図はST上昇を示しています。最も可能性の高い診断は何ですか?選択肢:A. 急性心筋梗塞 B. 不安定狭心症 C. 大動脈解離 D. 肺塞栓症 E. 心膜炎答え:A
例2:質問:45歳の女性が、夜間の発汗、体重減少、慢性的な咳があり、3か月持続しています。胸部X線写真では上葉に結節が見られます。最も適切な最初の検査は何ですか?選択肢:A. CTスキャン B. 喀痰抗酸菌塗抹 C. ツベルクリン皮膚反応 D. 気管支鏡 E. マンチュウ試験答え:B
次に、以下の質問に答えてください。
質問:{question}
選択肢:{choices_formatted}
答え(1文字のみ):"""
ZERO_SHOT_PUBMEDQA = """{question}"""
ZERO_SHOT_MMLU = """以下の質問に答えてください。
質問:{question}
選択肢:A. {a}B. {b}C. {c}D. {d}
答え(1文字のみ:A/B/C/D):"""
def format_question(q: BenchQuestion, prompt_type: str = "zero_shot") -> str: """各ベンチマークのプロンプトをレンダリングします。""" if q.bench_name == "medqa": choices_text = "\n".join(f"{chr(65+i)}. {c}" for i, c in enumerate(q.choices or [])) template = { "zero_shot": ZERO_SHOT_MEDQA, "zero_shot_cot": ZERO_SHOT_COT_MEDQA, "few_shot": FEW_SHOT_MEDQA, }.get(prompt_type, ZERO_SHOT_MEDQA) return template.format(question=q.question, choices_formatted=choices_text) elif q.bench_name == "pubmedqa": return ZERO_SHOT_PUBMEDQA.format(question=q.question) elif q.bench_name.startswith("mmlu_"): return ZERO_SHOT_MMLU.format( question=q.question, a=q.choices[0], b=q.choices[1], c=q.choices[2], d=q.choices[3], ) elif q.bench_name == "medmcqa": choices_text = "\n".join(f"{chr(65+i)}. {c}" for i, c in enumerate(q.choices or [])) return ZERO_SHOT_MEDQA.format(question=q.question, choices_formatted=choices_text) elif q.bench_name == "healthbench": return q.question # 自由記述 else: raise ValueError(f"不明なベンチマーク: {q.bench_name}")ステップ 3. 応答の解析(多肢選択の回答の抽出)
import re
def extract_choice(response: str, choices: list[str] | None) -> str | None: """LLMの応答から、選択肢の1つ(A/B/C/D)を抽出します。
CoT(Chain-of-Thought)応答(長い推論の後に答え)を処理します。 """ if not choices: return None upper = response.upper() # まず、"Answer: A"、"The answer is A"などのパターンを試します。 for pattern in [ r"Answer[:\s]+([A-E])", r"정답[은:\s]+([A-E])", r"answer[:\s]+([A-E])", r"the answer is\s+([A-E])", r"final answer[:\s]+([A-E])", ]: m = re.search(pattern, upper) if m: return m.group(1) # フォールバック:最後のスタンドアロンのA~E matches = re.findall(r"\b([A-E])\b", upper) if matches: return matches[-1] # フォールバック:選択肢のテキストとの一致 for i, choice in enumerate(choices): if choice and choice.lower() in response.lower(): return chr(65 + i) return None
def extract_yes_no_maybe(response: str) -> str | None: """PubMedQA用にyes/no/maybeを抽出します。""" text = response.lower().strip() for keyword in ["yes", "no", "maybe"]: if re.search(rf"\b{keyword}\b", text[:100]): return keyword return Noneステップ 4. LLMをジャッジとして使用(HealthBenchの自由記述)
HealthBenchは、ルーブリックベースのスコアリングを使用します。LLMジャッジでこれを自動化します。
JUDGE_PROMPT = """あなたは、臨床診断と治療評価の専門家です。与えられたシナリオに対して、ルーブリックに基づいて応答を評価してください。
シナリオ:{scenario}
応答:{response}
ルーブリック(JSON、各項目は0または1です):{rubric}
スコア(各ルーブリック項目の0または1)を含むJSONを出力します。{{ "criterion_1": 0 または 1, "criterion_2": 0 または 1, ... "total_score": 合計スコア, "max_score": ルーブリック項目の合計数}}"""
def judge_healthbench_response( scenario: str, response: str, rubric_json: str, judge_client, judge_model: str = "claude-opus-4-5-20250219",) -> dict: """ルーブリックを使用して、LLMをジャッジとして使用してスコアリングします。""" prompt = JUDGE_PROMPT.format(scenario=scenario, response=response, rubric=rubric_json) resp = judge_client.messages.create( model=judge_model, max_tokens=1024, messages=[{"role": "user", "content": prompt}], ) raw = resp.content[0].text match = re.search(r"\{.*\}", raw, re.DOTALL) if not match: return {"total_score": 0, "max_score": 1, "error": "評価の解析に失敗しました"} import json try: return json.loads(match.group(0)) except json.JSONDecodeError: return {"total_score": 0, "max_score": 1, "error": "無効なJSON"}ステップ 5. ベンチマークの実行と信頼区間
パート09のVendorAdapterを再利用します。
import asynciofrom dataclasses import dataclass
@dataclassclass BenchAnswerResult: question: BenchQuestion model: str model_snapshot: str response: str predicted_answer: str | None is_correct: bool latency_ms: float metadata: dict
async def run_bench_on_vendor( questions: list[BenchQuestion], adapter, # Part 09のVendorAdapter prompt_type: str = "zero_shot", max_concurrent: int = 5,) -> list[BenchAnswerResult]: """単一のモデルで、単一のベンチマークを実行します。""" semaphore = asyncio.Semaphore(max_concurrent) async def process(q: BenchQuestion) -> BenchAnswerResult: async with semaphore: prompt = format_question(q, prompt_type=prompt_type) resp = await adapter.query_async(prompt) if q.choices: if q.bench_name == "pubmedqa": predicted = extract_yes_no_maybe(resp.raw_text) else: predicted = extract_choice(resp.raw_text, q.choices) is_correct = predicted == q.gold_answer else: # HealthBenchのような自由記述タスクの場合、別のジャッジを使用します predicted = None is_correct = False return BenchAnswerResult( question=q, model=adapter.model, model_snapshot=resp.model_snapshot, response=resp.raw_text, predicted_answer=predicted, is_correct=is_correct, latency_ms=resp.latency_ms, metadata={ "input_tokens": resp.input_tokens, "output_tokens": resp.output_tokens, "prompt_type": prompt_type, }, ) return await asyncio.gather(*[process(q) for q in questions])ステップ 6. 公表された値と測定値の差のレポート
import numpy as npimport pandas as pd
# ベンダー番号と公表された数値(実行時に確認する必要があります)PUBLISHED_NUMBERS = { "medqa": { "claude-opus-4-5": 0.905, # 例、Anthropicによって公表 "gpt-4o": 0.876, # OpenAIによって公表 "o1": 0.947, # OpenAI o1によって公表 "gemini-2.5-pro": 0.895, # Googleによって公表 "med-gemini": 0.91, # Google Med-Gemini "meditron-70b": 0.72, # EPFL論文に公表 "med42-70b": 0.85, # M42によって公表 }, "pubmedqa": { "claude-opus-4-5": 0.788, "gpt-4o": 0.759, "o1": 0.82, "meditron-70b": 0.82, # 専門モデルの方が良い結果 "med-gemini": 0.81, }, "mmlu_medical": { "gpt-4o": 0.87, "claude-opus-4-5": 0.89, "gemini-2.5-pro": 0.88, "meditron-70b": 0.75, }, # ... 他のベンチマーク}
def compute_bench_score(results: list[BenchAnswerResult]) -> dict: """精度と95% CI(ウィルソンスコア区間)を計算します。""" n = len(results) if n == 0: return {"accuracy": 0.0, "ci_low": 0.0, "ci_high": 0.0, "n": 0} correct = sum(1 for r in results if r.is_correct) p = correct / n z = 1.96 denominator = 1 + z**2 / n center = (p + z**2 / (2 * n)) / denominator margin = z * np.sqrt(p * (1 - p) / n + z**2 / (4 * n**2)) / denominator return { "accuracy": p, "ci_low": max(0, center - margin), "ci_high": min(1, center + margin), "n": n, "correct": correct, }
def delta_report(all_results: dict[tuple[str, str], list[BenchAnswerResult]]) -> pd.DataFrame: """(bench, model) -> 測定値と公表値の差。""" rows = [] for (bench, model), results in all_results.items(): score = compute_bench_score(results) # 公表された数値からのモデルのエイリアスを一致させます(実行時に確認する必要があります) model_key = model.split("-2024")[0].split("-2025")[0] # スナップショットを削除します published = PUBLISHED_NUMBERS.get(bench, {}).get(model_key) delta = (score["accuracy"] - published) if published is not None else None reproducibility = "N/A" if delta is not None: if abs(delta) < 0.02: reproducibility = "再現 (Δ < 2%p)" elif abs(delta) < 0.05: reproducibility = "部分的に再現(Δ 2〜5%p)" else: reproducibility = f"再現に失敗(Δ {delta:.1%})" rows.append({ "bench": bench, "model": model, "n": score["n"], "correct": score["correct"], "accuracy_measured": round(score["accuracy"], 4), "ci_95": f"[{score['ci_low']:.3f}, {score['ci_high']:.3f}]", "published": published, "delta": round(delta, 4) if delta is not None else None, "reproducibility": reproducibility, }) return pd.DataFrame(rows)
def reproducibility_heatmap(df: pd.DataFrame, output_path: str = "reproducibility.png") -> None: """ベンチマークxモデルのヒートマップ(デルタの色)。""" import matplotlib.pyplot as plt import seaborn as sns pivot = df.pivot(index="model", columns="bench", values="delta") fig, ax = plt.subplots(figsize=(12, 8)) sns.heatmap( pivot, annot=True, cmap="RdYlGn_r", center=0, fmt=".3f", cbar_kws={"label": "デルタ(測定値-公表値)"}, ax=ax, ) ax.set_title("Med-LLMベンチマーク再現デルタマトリックス\n(正の値=測定値の方が良い、負の値=再現に失敗)") plt.tight_layout() plt.savefig(output_path, dpi=150)ステップ 7. 失敗の診断
デルタが大きい場合は、考えられる失敗の原因を確認します。
def diagnose_reproduction_failure( bench: str, model: str, delta: float, all_results: list[BenchAnswerResult],) -> dict: """再現の失敗の考えられる原因を診断します。""" diagnosis = { "bench": bench, "model": model, "delta": delta, "possible_causes": [], } if abs(delta) < 0.03: diagnosis["possible_causes"].append("再現されました。さらなる調査は不要です。") return diagnosis # 1. プロンプトの違い prompt_types = {r.metadata.get("prompt_type", "unknown") for r in all_results} if "zero_shot_cot" not in prompt_types: diagnosis["possible_causes"].append( "CoT(Chain-of-Thought)プロンプトは使用されていません。オリジナルの論文ではCoTが使用された可能性があります。zero_shot_cotプロンプトで再実行してください。" ) if "few_shot" not in prompt_types: diagnosis["possible_causes"].append( "少数ショットの例は含まれていません。オリジナルの論文では、5ショット、25ショットなどが使用されました。" ) # 2. データサブセットの違い total_n = len(all_results) diagnosis["possible_causes"].append( f"現在のサンプルサイズ:n={total_n}。オリジナルの論文では、おそらく完全なテストセット(数千のサンプル)が使用されました。サンプルサイズを増やしてください。" ) # 3. 自己整合性が使用されていません diagnosis["possible_causes"].append( "自己整合性(Nサンプルの多数決)は使用されていません。オリジナルの論文では、N=5、10などが使用されました。" ) # 4. データ汚染 diagnosis["possible_causes"].append( f"{bench}はオープンデータセットです。モデルの事前学習コーパスにこのデータが含まれている可能性があります(データ汚染)。より最近の、除外されたベンチマークで検証することをお勧めします。" ) # 5. モデルのスナップショットの違い snapshots = {r.model_snapshot for r in all_results} diagnosis["possible_causes"].append( f"測定されたスナップショット:{snapshots}。公開時のスナップショットと異なる場合があります。" ) return diagnosisステップ 8. 統合されたパイプライン
async def full_reproduction_bench( benches: list[str], # ["medqa", "pubmedqa", "mmlu", "medmcqa"] adapters: list, # VendorAdapterのリスト output_dir: Path, sample_size: int | None = 500, prompt_types: list[str] = ["zero_shot"],) -> pd.DataFrame: """完全な再現ベンチマークを実行します。""" output_dir.mkdir(parents=True, exist_ok=True) # ベンチマークをロードします all_questions = {} if "medqa" in benches: all_questions["medqa"] = load_medqa(max_samples=sample_size) if "pubmedqa" in benches: all_questions["pubmedqa"] = load_pubmedqa(max_samples=sample_size) if "mmlu" in benches: all_questions["mmlu_medical"] = load_mmlu_medical(max_samples=sample_size) if "medmcqa" in benches: all_questions["medmcqa"] = load_medmcqa(max_samples=sample_size) print(f"ロードが完了しました。ベンチマークごとのサンプル数:" + ", ".join(f"{k}={len(v)}" for k, v in all_questions.items())) all_results = {} for adapter in adapters: for bench_name, questions in all_questions.items(): for pt in prompt_types: print(f"[{adapter.model}] {bench_name} ({pt})") results = await run_bench_on_vendor(questions, adapter, prompt_type=pt) key = (bench_name, adapter.model) if pt != "zero_shot": key = (f"{bench_name}_{pt}", adapter.model) all_results[key] = results # レポート df = delta_report(all_results) df.to_csv(output_dir / "reproduction_report.csv", index=False) reproducibility_heatmap(df, str(output_dir / "reproducibility_heatmap.png")) # 失敗の診断 diagnoses = [] for (bench, model), results in all_results.items(): row = df[(df["bench"] == bench) & (df["model"] == model)] if not row.empty and row.iloc[0]["delta"] is not None: diag = diagnose_reproduction_failure(bench, model, row.iloc[0]["delta"], results) diagnoses.append(diag) with open(output_dir / "failure_diagnoses.json", "w", encoding="utf-8") as f: import json json.dump(diagnoses, f, ensure_ascii=False, indent=2) return df
## パフォーマンス、コスト、および既知の失敗事例
### パフォーマンスの参考(公開ベンチマークを使用)
Med-LLMベンチマークの最近の評価結果(概算、モデルと年による):
| モデル | MedQA (USMLE) | PubMedQA | MMLU-Medical | MedMCQA | HealthBench | ソース ||------|:-------------:|:--------:|:------------:|:-------:|:-----------:|------|| Meditron 70B | 0.72 | 0.82 | 0.75 | 0.65 | — | Chen et al., EPFL 2023 [6] || PMC-LLaMA 13B | 0.61 | 0.77 | 0.65 | 0.60 | — | Wu et al. 2023 [7] || Med-PaLM 2 | 0.86 | 0.79 | 0.85 | 0.72 | — | Singhal et al., Nature 2023 [8] || Med-Gemini | 0.91 | 0.81 | 0.88 | 0.75 | 0.62 | Saab et al., Nat Med 2024 [9] || Med42 70B | 0.85 | 0.79 | 0.83 | 0.71 | — | M42 release || GPT-4o | 0.88 | 0.76 | 0.87 | 0.72 | 0.65 | OpenAI release [10] || Claude Opus 4.5 | 0.91 | 0.79 | 0.89 | 0.76 | 0.72 | Anthropic release [11] || o1 | 0.95+ | 0.82 | 0.92 | 0.83 | 0.78 | OpenAI release [10] || o3 | 0.94 | 0.82 | 0.91 | 0.81 | 0.79 | OpenAI release [10] |
### 学習者が再現するための推定コスト
- 6〜8モデル × 4ベンチマーク × 500サンプル = 12,000〜16,000リクエスト。総額はおよそ30〜120米ドル(各モデルの価格は、その時点で確認してください)。- オープンモデルをローカルで実行する場合、別途GPU時間が必要です。
### 5つの既知の失敗事例(コミュニティや論文から収集)
1. **ベンチマークデータの汚染** 症状:モデルが、事前学習中にベンチマークの問題を目にした可能性がある → 公開された結果の精度が過大評価されている。再現した結果が予想以上に低い場合、必ずしも再現の失敗ではない(プロンプトまたはサブセットの違いによる)。 原因:MedQAとPubMedQAは古いオープンデータセットであるため、ウェブで収集されたコーパスが含まれている可能性が高い。 対策:(a)より新しいベンチマーク(HealthBenchなど)を優先する、(b)汚染検出ツール(n-gramの重複、メンバーシップ推論)を使用する、(c)新しい質問を含むホールドアウトベンチマークを作成する、(d)ベンチマークのリリース日よりも前の時点の事前学習スナップショットを使用する。 ソース:Xu et al. "Benchmark Data Contamination of Large Language Models." arXiv 2024 [12]。
2. **わずかなプロンプトの違いで5%以上精度が変動する** 症状:元の論文のプロンプトをわずかに変更しただけで、精度が大幅に低下または増加する。 原因:LLMはプロンプトの形式に敏感である。数ショットの例の順序、数、およびドメインとの関連性が影響する。 対策:(a)元の論文のプロンプトを正確に(付録から)コピーし、元のリポジトリからコードを再実行する、(b)複数のプロンプトのバリエーションをアンサンブルする(自己整合性N=5、10)、(c)結果をプロンプトとパラメータのログとともに保存する、(d)ベンチマークの実行条件を固定する(temperature=0など)。 ソース:Lu et al. "Fantastically Ordered Prompts and Where to Find Them." ACL 2022 [13]。
3. **LLM-as-Judgeバイアス(HealthBenchなど)** 症状:ジャッジモデルが、特定のベンダーのモデルからの応答をより好意的に評価する(自己優先バイアス)。 原因:ジャッジと評価対象のモデルが同じ系列である場合、ジャッジは自分のスタイルを好む可能性がある。 対策:(a)異なるベンダーのジャッジとターゲットモデルでクロスバリデーションを行う、(b)複数のジャッジをアンサンブルする(Claude + GPT + Gemini)、(c)定期的に人間の評価との相関関係をチェックする、(d)ルールベースの評価を使用する(主観的な判断を最小限に抑える)。 ソース:Zheng et al. "Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena." NeurIPS 2023 [14]。
4. **CoTを使用するかどうかで精度が大幅に異なる** 症状:GPT-4oのMedQAにおけるゼロショット設定での測定精度は0.79だが、CoTプロンプトを使用すると0.88(9%の差)になる。 原因:多くの医学的QA問題には、複数ステップの推論が必要である。CoTがない場合、モデルは表面的なパターンマッチングに依存し、誤った回答を出す可能性がある。 対策:(a)各ベンチマークに対して、最適なプロンプトをすべて報告する(ゼロショット/CoT/少数ショット/自己整合性)、(b)公開された論文のプロンプト条件を明確に確認する、(c)o1やo3など、独自のCoTを持つモデルの場合、ゼロショットが最適な設定になる可能性がある。 ソース:Wei et al. "Chain-of-Thought Prompting Elicits Reasoning in Large Language Models." NeurIPS 2022 [15]。
5. **データセットのライセンスとアクセスに関する問題** 症状:HealthBenchやMedMCQAなどの特定のベンチマークでは、登録と認証が必要であり、学習者がすぐにアクセスできない。 原因:臨床データと医学試験の質問には、著作権と規制の問題がある。 対策:(a)各ベンチマークのライセンスを事前に確認し、登録手順を明示する、(b)オープンライセンスのベンチマークを優先する(MMLU、PubMedQAなど)、(c)自身で作成したホールドアウトベンチマークで補完する、(d)学習者を登録プロセスに誘導する。 ソース:HuggingFace Hubの各データセットのデータカード、元の論文のライセンスセクション。
## 拡張のアイデア
- **韓国の医学ベンチマーク:** KMLE(韓国医学医師国家試験スタイルの)ベンチマークを作成し、再現する。韓国に特化して検証する。- **時間的ロバスト性:** 同じベンチマークを毎月再実行して、ベンダーモデルの更新を追跡する。回帰を検出する。- **タスク固有のファインチューニングベンチマーク:** オープンソースモデルをファインチューニングした後のパフォーマンスの改善を定量化する。- **Chain-of-Thoughtの比較:** CoTプロンプト、数ショットN、および自己整合性Nがベンチマークのパフォーマンスに与える影響を示す行列。- **RAG統合:** ベンチマークの問題に関連するPubMedの抄録を検索し、コンテキストとして注入する → パフォーマンスの改善を定量化する。- **大規模なオープンソースモデルのベンチマーク:** Meditron、Med42、MedGemma、Llama-3-Med、およびQwen-Medを含むすべてのオープンソースモデルを網羅する。
## 次のセクション
- セクション09 `llm-vendor-benchmark`: このセクションが標準的なベンチマークの再現に関するものであれば、セクション09は実際のタスクにおけるベンダーの比較について扱います。- セクション14 `bio-mcp-agent`: このベンチマークの結果をMCPツールに公開するために使用します。- セクション01 `clinical-notes-ie-llm`: セクション01の臨床IEのパフォーマンスを、ベンチマークの観点から再検討します。
## 参考文献
1. Jin D, Pan E, Oufattole N, et al. "What Disease Does This Patient Have? A Large-Scale Open Domain Question Answering Dataset from Medical Exams (MedQA)." arXiv 2020. `https://arxiv.org/abs/2009.13081`2. Jin Q, Dhingra B, Liu Z, et al. "PubMedQA: A Dataset for Biomedical Research Question Answering." EMNLP 2019.3. Hendrycks D, Burns C, Basart S, et al. "Measuring Massive Multitask Language Understanding (MMLU)." ICLR 2021. `https://arxiv.org/abs/2009.03300`4. Pal A, Umapathi LK, Sankarasubbu M. "MedMCQA: A Large-scale Multi-Subject Multi-Choice Dataset for Medical domain Question Answering." CHIL 2022.5. OpenAI HealthBench (2025年のリリース予定): `https://openai.com/index/healthbench/` (リリース時に実際のURLを確認してください)。6. Chen Z, Cano AH, Romanou A, et al. "Meditron-70B: Scaling Medical Pretraining for Large Language Models." EPFL 2023. `https://arxiv.org/abs/2311.16079`7. Wu C, Zhang X, Zhang Y, et al. "PMC-LLaMA: Toward Building Open-source Language Models for Medicine." arXiv 2023.8. Singhal K, Tu T, Gottweis J, et al. "Towards Expert-Level Medical Question Answering with Large Language Models (Med-PaLM 2)." Nature 2023. `https://www.nature.com/articles/s41586-023-06291-2`9. Saab K, Tu T, Weng W-H, et al. "Capabilities of Gemini Models in Medicine (Med-Gemini)." Nature Medicine 2024. `https://www.nature.com/articles/s41591-024-03246-6`10. OpenAIモデルのベンチマーク: `https://openai.com/index/gpt-4o-system-card/` · o1 · o3 システムカード11. Anthropicモデルのベンチマーク: `https://www.anthropic.com/news/claude-3-family` · Claude 4 システムカード12. Xu R et al. "Benchmark Data Contamination of Large Language Models: A Survey." arXiv 2024. `https://arxiv.org/abs/2406.04244`13. Lu Y, Bartolo M, Moore A, et al. "Fantastically Ordered Prompts and Where to Find Them." ACL 2022.14. Zheng L, Chiang W-L, Sheng Y, et al. "Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena." NeurIPS 2023.15. Wei J, Wang X, Schuurmans D, et al. "Chain-of-Thought Prompting Elicits Reasoning in Large Language Models." NeurIPS 2022.16. HuggingFaceデータセット — bigbio/med_qa: `https://huggingface.co/datasets/bigbio/med_qa`17. HuggingFaceデータセット — qiaojin/PubMedQA: `https://huggingface.co/datasets/qiaojin/PubMedQA`18. HuggingFaceデータセット — cais/mmlu: `https://huggingface.co/datasets/cais/mmlu`19. Meditron GitHub: `https://github.com/epfLLM/meditron`20. HELM (Holistic Evaluation of Language Models): `https://crfm.stanford.edu/helm/`