BioPlayground

🧬
목록으로

Med-LLM 벤치마크 재현 — HealthBench · MedQA · PubMedQA · MMLU-Medical 자체 재현과 발표 숫자 vs 실측 delta 검증

OpenAI HealthBench · MedQA(USMLE) · PubMedQA · MMLU-Medical 공개 의료 벤치를 자체 재현. 여러 벤더(Claude · GPT · Gemini) + 오픈 모델(Meditron · MedGemma · Med42) 성능 실측 vs 논문·기업 발표 숫자 비교. Data contamination · LLM-as-judge 편향 · CoT 유무 impact · 라이선스 준수까지 완결 파이프라인.

중급
|
40
|
검증 완료 (2026-07)
진행률0/15 (0%)

Med-LLM 벤치마크 재현 — HealthBench · MedQA · PubMedQA · MMLU-Medical 자체 재현과 발표 숫자 vs 실측 delta 검증

Med-LLM 성능 발표는 매달 갱신되고 각 벤더는 자체 벤치에서 SOTA를 주장합니다. 그런데 정말 실측 재현 가능한가? 논문·기업 발표 숫자와 사용자 자체 벤치의 결과가 얼마나 일치하는가? 이 편은 공개 의료 벤치마크(HealthBench, MedQA-USMLE, PubMedQA, MMLU-Medical, MedMCQA)를 자체 재현하고, 여러 벤더 · 오픈 모델의 성능을 실측해 발표 숫자와 비교하는 실전 파이프라인입니다. 편 09가 "같은 실전 태스크로 벤더 비교"라면 이 편은 "같은 표준 벤치로 재현성 검증", 즉 벤더 발표를 신뢰할지 판단하는 도구입니다.

📚 선수 편 권고 (강력 권고)

이 편은 AI×바이오 하드코어 심화 편입니다. 진입 전에 DryBench의 다음 편들을 먼저 듣고·보시길 강력히 권고드립니다.

선수 편 없이 진입할 경우, 이 편에서 다루는 few-shot 프롬프트 재현 · Chain-of-Thought · LLM-as-judge 평가 · HuggingFace datasets 로딩의 재설명 없이 실전 코드부터 진행하므로 따라가기 어렵습니다.


우리 DryBench에서 이거 배웠잖아

DryBench ai-native #7에서 프롬프트가 결과를 크게 좌우한다는 것을, #11에서 할루시네이션이 벤치 평가에서 어떻게 편향을 만드는지를, #13에서 HuggingFace datasets · evaluate 라이브러리로 벤치를 표준 재현 가능하다는 것을 배웠습니다.

Med-LLM 벤치는 이 세 원리가 특히 예민한 영역입니다. 의료 QA는 정답이 있는 객관식(MedQA · MMLU-Medical · MedMCQA) · 근거 요약(PubMedQA) · 자유서술 진단(HealthBench) 등 다양한 태스크가 섞여 있고, 각 태스크의 평가 지표가 다릅니다. 그리고 벤더가 발표하는 숫자는 최적화된 프롬프트 · 특정 subset · self-consistency · CoT 등 여러 변형이 있어 재현이 항상 되지는 않습니다. 이 편은 그 재현성 문제를 하드코어로 정면 다룹니다.

하드코어 문제 정의

실전 요구

  • 표준 벤치 재현: MedQA · PubMedQA · MMLU-Medical · MedMCQA · HealthBench 정답 지표 계산.
  • 여러 모델 실행: Claude Opus/Sonnet · GPT-4o · o1 · Gemini 2.5 · Meditron · MedGemma · Med42 최소 6개 이상.
  • 재현성 pinning: 데이터셋 버전 · 모델 snapshot · 프롬프트 · 실행 시각 · CoT 유무 · few-shot 개수 모두 로그.
  • 발표 vs 실측 delta 리포트: "논문 X는 76% 발표, 우리 재현 71% (delta -5%p)" 매트릭스.
  • 오픈 vs 폐쇄 vs 특화 모델 비교: 상용 API vs 로컬 오픈 모델 vs 의료 특화 모델의 정확도 격차.

벤치별 특성

  • MedQA (USMLE) [1]: 미국 의사 국가고시 스타일 객관식. 4~5지 선다. Accuracy 지표.
  • PubMedQA [2]: PubMed 초록 기반 yes/no/maybe QA. 근거 요약 포함. Accuracy · F1.
  • MMLU-Medical (subset) [3]: MMLU 중 clinical knowledge · anatomy · college medicine · professional medicine 등 의료 subset. Accuracy.
  • MedMCQA [4]: 인도 의사고시 (AIIMS/NEET) 스타일 4지 선다. Accuracy.
  • HealthBench (OpenAI 2025) [5]: 실제 임상 시나리오 자유서술 응답. LLM-as-judge or rubric 기반 스코어링.
  • CasesMD · DiagnosisBench 등 신규 벤치도 존재.

이 편의 목표 지표

  • 45개 벤치 × 68개 모델 = 최소 24~40 셀 매트릭스 완성.
  • 각 셀에 정확도 · 95% 신뢰구간 · 재현 델타(발표 대비) 표기.
  • 재현 실패 셀(발표 대비 5%p 이상 차이)의 원인 분석 (프롬프트 · subset · self-consistency · CoT).
  • 오픈 소스 코드로 누구나 재현 가능 (라이선스 준수).

도구 스택과 인프라 요구

도구역할라이선스
HuggingFace datasets벤치 데이터셋 로딩Apache 2.0
HuggingFace evaluate표준 평가 지표Apache 2.0
편 09의 VendorAdapter (Anthropic · OpenAI · Google)상용 API 호출각 SDK 라이선스
HuggingFace transformers + vLLM (선택)오픈 모델 로컬 추론Apache 2.0
pandas · matplotlib · seaborn결과 표 · heatmapBSD
jsonlines · pyyaml결과 로그 저장MIT · MIT
scipy · statsmodels신뢰구간 · 유의성 검정BSD

인프라 요구:

  • No GPU (상용 API 중심). 오픈 모델 로컬 실행 시 소형~중대형 GPU (Meditron 7B는 소형 GPU, 70B는 24GB+ VRAM 데이터센터 GPU).
  • RAM 16GB 이상.
  • 디스크: 각 벤치 데이터셋 총 약 500MB, 오픈 모델 가중치 별도 (Meditron 70B 약 140GB fp16).

학습자 재현 예상 비용: 6개 벤더 × 4개 벤치 × 각 벤치 문항 수(5002000) = 상용 API 총 약 30~120 USD.

파이프라인 실전 구현

전체 흐름:

mermaid

Step 1. 벤치 데이터셋 로딩

각 벤치를 HuggingFace Hub에서 로딩. 라이선스는 벤치별 상이하니 개별 확인 필수.

python
from dataclasses import dataclass, field
from typing import Literal
from datasets import load_dataset
@dataclass
class BenchQuestion:
bench_name: str
qid: str
question: str
choices: list[str] | None # 객관식이면 리스트, 자유서술이면 None
gold_answer: str # 객관식은 "A"/"B"/..., 자유서술은 rubric JSON
metadata: dict = field(default_factory=dict)
def load_medqa(split: str = "test", max_samples: int | None = None) -> list[BenchQuestion]:
"""MedQA (USMLE style) 로딩. 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}\n\n질문: {row['question']}\n답변은 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의 의료 subset. 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:
# 대체: 논문 부록 sample scenarios 수동 로딩
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

Step 2. 프롬프트 템플릿 (원 논문 재현)

원 논문에서 사용한 프롬프트를 최대한 그대로 복제. Zero-shot · Few-shot · CoT 변형별 별도.

python
ZERO_SHOT_MEDQA = """다음 의사 국가고시 문제에 답하세요.
문제: {question}
선택지:
{choices_formatted}
답 (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. Mantoux 검사
답: B
이제 다음 문제에 답하세요.
문제: {question}
선택지:
{choices_formatted}
답 (한 글자만):
"""
ZERO_SHOT_PUBMEDQA = """{question}
"""
ZERO_SHOT_MMLU = """다음 문제에 답하세요.
문제: {question}
선택지:
A. {a}
B. {b}
C. {c}
D. {d}
답 (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"Unknown bench: {q.bench_name}")

Step 3. 응답 파싱 (객관식 정답 추출)

python
import re
def extract_choice(response: str, choices: list[str] | None) -> str | None:
"""LLM 응답에서 A/B/C/D 하나 추출.
CoT 응답 (긴 추론 뒤 답)도 처리.
"""
if not choices:
return None
upper = response.upper()
# "답: A" · "정답은 A" · "The answer is A" 같은 패턴 우선
for pattern in [
r"답[:\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)
# Fallback: 마지막에 등장하는 단독 A~E
matches = re.findall(r"\b([A-E])\b", upper)
if matches:
return matches[-1]
# Fallback: 선택지 텍스트 매칭
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

Step 4. LLM-as-Judge (HealthBench 자유서술)

HealthBench는 rubric 기반 스코어링. Judge LLM으로 자동화.

python
JUDGE_PROMPT = """당신은 임상 진단 · 치료 평가 전문가입니다.
아래 시나리오와 응답을 rubric에 따라 평가하세요.
시나리오:
{scenario}
응답:
{response}
Rubric (JSON, 각 항목이 0 or 1로 평가):
{rubric}
각 rubric 항목에 대해 0(미충족) · 1(충족) 점수를 JSON으로 출력:
{{
"criterion_1": 0 or 1,
"criterion_2": 0 or 1,
...
"total_score": 합계,
"max_score": rubric 총 항목 수
}}
"""
def judge_healthbench_response(
scenario: str,
response: str,
rubric_json: str,
judge_client,
judge_model: str = "claude-opus-4-5-20250219",
) -> dict:
"""LLM-as-judge로 rubric 스코어링."""
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 invalid"}

Step 5. 벤치 실행 · 신뢰구간

편 09의 VendorAdapter 재사용.

python
import asyncio
from dataclasses import dataclass
@dataclass
class 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, # 편 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 등 자유서술은 별도 judge
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])

Step 6. 발표 vs 실측 delta 리포트

python
import numpy as np
import 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 (Wilson score interval)."""
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) → 실측 vs 발표 delta."""
rows = []
for (bench, model), results in all_results.items():
score = compute_bench_score(results)
# 발표 숫자에서 model alias 매칭
model_key = model.split("-2024")[0].split("-2025")[0] # snapshot 제거
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:
"""벤치 × 모델 heatmap (delta 색상)."""
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 벤치마크 재현 delta 매트릭스\n(양수=실측 우수, 음수=재현 실패)")
plt.tight_layout()
plt.savefig(output_path, dpi=150)

Step 7. 재현 실패 원인 분석

Delta가 크면 왜 그런지 원인 후보 확인.

python
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 차이 후보
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(
"Few-shot 예시 미포함. 발표는 5-shot · 25-shot 등 사용했을 가능성."
)
# 2. 데이터 subset 차이
total_n = len(all_results)
diagnosis["possible_causes"].append(
f"현재 sample n={total_n}. 발표는 full test set (수천 개)에서 측정했을 가능성. sample 확대 필요."
)
# 3. Self-consistency 미사용
diagnosis["possible_causes"].append(
"Self-consistency (majority vote of N samples) 미사용. 발표는 N=5, 10 등 사용했을 가능성."
)
# 4. Data contamination
diagnosis["possible_causes"].append(
f"{bench}는 오픈 데이터. 모델 pre-training corpus에 포함됐을 가능성 (data contamination). "
"최신 held-out 벤치로 검증 권장."
)
# 5. 모델 snapshot 차이
snapshots = {r.model_snapshot for r in all_results}
diagnosis["possible_causes"].append(
f"실측 snapshot: {snapshots}. 발표 시점 snapshot과 다를 수 있음."
)
return diagnosis

Step 8. 통합 파이프라인

python
async def full_reproduction_bench(
benches: list[str], # ["medqa", "pubmedqa", "mmlu", "medmcqa"]
adapters: list, # 편 09의 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"로딩 완료. 벤치별 sample: " +
", ".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)PubMedQAMMLU-MedicalMedMCQAHealthBench출처
Meditron 70B0.720.820.750.65Chen et al., EPFL 2023 [6]
PMC-LLaMA 13B0.610.770.650.60Wu et al. 2023 [7]
Med-PaLM 20.860.790.850.72Singhal et al., Nature 2023 [8]
Med-Gemini0.910.810.880.750.62Saab et al., Nat Med 2024 [9]
Med42 70B0.850.790.830.71M42 발표
GPT-4o0.880.760.870.720.65OpenAI 발표 [10]
Claude Opus 4.50.910.790.890.760.72Anthropic 발표 [11]
o10.95+0.820.920.830.78OpenAI 발표 [10]
o30.940.820.910.810.79OpenAI 발표 [10]

학습자 재현 예상 비용

  • 68개 모델 × 4개 벤치 × 500 sample = 12,00016,000 요청, 총 약 30~120 USD (모델별 요금표 시점 확인).
  • 오픈 모델 로컬 실행 시 GPU 시간 별도.

알려진 실패 케이스 5건 (커뮤니티·논문 수집형)

  1. 벤치 데이터 오염 (data contamination)
    증상: 모델이 pre-training 단계에서 벤치 문항을 봤을 가능성 → 발표 정확도 부풀림. 재현 시 예상 밖 낮게 나오면 오염 아니라 재현 실패 (프롬프트·subset 차이 등).
    원인: MedQA · PubMedQA 등은 오래된 오픈 데이터라 웹 크롤 corpus 포함 확률 높음.
    회피: (a) 최신 벤치(HealthBench 등) 우선, (b) contamination detection 도구 (n-gram overlap · membership inference), (c) 새 문항으로 held-out 벤치 자체 제작, (d) 벤치 발표 시점보다 이전 pre-training snapshot 사용.
    출처: Xu et al. "Benchmark Data Contamination of Large Language Models." arXiv 2024 [12].

  2. 프롬프트 미묘한 차이로 정확도 5%p 이상 변동
    증상: 원 논문 프롬프트와 조금만 달라도 정확도 급락 or 급등.
    원인: LLM은 프롬프트 표현에 민감. Few-shot 예시 순서·개수·domain relevance 모두 영향.
    회피: (a) 원 논문 부록 프롬프트 그대로 복제 · 원 저장소 코드 재실행, (b) 여러 프롬프트 변형 앙상블 (self-consistency N=5, 10), (c) 프롬프트 · 파라미터 log와 함께 결과 저장, (d) 벤치 실행 조건 pinning (temperature=0 등).
    출처: Lu et al. "Fantastically Ordered Prompts and Where to Find Them." ACL 2022 [13].

  3. LLM-as-judge 편향 (HealthBench 등)
    증상: Judge 모델이 특정 벤더 모델 응답에 유리하게 채점 (self-preference bias).
    원인: Judge와 채점 대상이 같은 계보 모델이면 자기 style 선호.
    회피: (a) Judge와 대상 모델을 다른 벤더로 교차, (b) 여러 judge 앙상블 (Claude + GPT + Gemini), (c) 사람 채점과 상관계수 정기 검증, (d) rubric-based 스코어링 (자유 판단 최소화).
    출처: Zheng et al. "Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena." NeurIPS 2023 [14].

  4. CoT 유무로 정확도 큰 격차
    증상: Zero-shot으로 GPT-4o MedQA 실측 0.79, CoT 프롬프트로 0.88 (9%p 차이).
    원인: 의료 QA는 다단계 추론이 필요한 문제 많음. CoT 없으면 표면 pattern matching으로 오답.
    회피: (a) 벤치별 최적 프롬프트 모두 리포트 (zero-shot / CoT / few-shot / self-consistency), (b) 발표 논문의 명시 프롬프트 조건 확인, (c) o1 · o3 같은 reasoning 모델은 자체 CoT라 zero-shot이 최적.
    출처: Wei et al. "Chain-of-Thought Prompting Elicits Reasoning in Large Language Models." NeurIPS 2022 [15].

  5. 데이터셋 라이선스·접근 문제
    증상: HealthBench · MedMCQA 등 특정 벤치가 등록·인증 필요, 학습자 즉시 접근 실패.
    원인: 임상 데이터·의사고시 문항은 저작권·규제 이슈.
    회피: (a) 벤치별 라이선스 사전 확인 · 등록 절차 명시, (b) 오픈 라이선스 벤치 (MMLU · PubMedQA 등) 우선, (c) 자체 held-out 벤치 자체 제작으로 보완, (d) 학습자에게 등록 절차 안내.
    출처: HuggingFace Hub 각 데이터셋 카드 · 원 논문 라이선스 섹션.

확장 아이디어

  • 한국어 의료 벤치: KMLE(한국 의사고시 스타일) 자체 벤치 구축·재현. 국내 특화 검증.
  • Temporal robustness: 같은 벤치를 매월 재실행해 벤더 모델 갱신 추적. Regression 감지.
  • Task-specific fine-tuning 벤치: 오픈 모델 fine-tuning 후 벤치 성능 향상 정량화.
  • Chain-of-thought 유무 비교: CoT 프롬프트 유무 · few-shot N · self-consistency N이 벤치별 성능에 미치는 영향 매트릭스.
  • RAG 결합: 벤치 문항에 관련 PubMed 초록 검색 후 컨텍스트로 주입 → 성능 향상 정량.
  • 오픈 모델 대량 벤치: Meditron · Med42 · MedGemma · Llama-3-Med · Qwen-Med 등 오픈 계열 총망라.

다음 편

  • 편 09 llm-vendor-benchmark: 이 편이 표준 벤치 재현이라면 편 09는 실전 태스크 벤더 비교.
  • 편 14 bio-mcp-agent: 이 편의 벤치 결과를 MCP tool로 노출.
  • 편 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 model benchmarks: https://openai.com/index/gpt-4o-system-card/ · o1 · o3 system cards
  11. Anthropic model benchmarks: https://www.anthropic.com/news/claude-3-family · Claude 4 system card
  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 datasets — bigbio/med_qa: https://huggingface.co/datasets/bigbio/med_qa
  17. HuggingFace datasets — qiaojin/PubMedQA: https://huggingface.co/datasets/qiaojin/PubMedQA
  18. HuggingFace datasets — 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/