Med-LLM 벤치마크 재현 — HealthBench · MedQA · PubMedQA · MMLU-Medical 자체 재현과 발표 숫자 vs 실측 delta 검증
Med-LLM 성능 발표는 매달 갱신되고 각 벤더는 자체 벤치에서 SOTA를 주장합니다. 그런데 정말 실측 재현 가능한가? 논문·기업 발표 숫자와 사용자 자체 벤치의 결과가 얼마나 일치하는가? 이 편은 공개 의료 벤치마크(HealthBench, MedQA-USMLE, PubMedQA, MMLU-Medical, MedMCQA)를 자체 재현하고, 여러 벤더 · 오픈 모델의 성능을 실측해 발표 숫자와 비교하는 실전 파이프라인입니다. 편 09가 "같은 실전 태스크로 벤더 비교"라면 이 편은 "같은 표준 벤치로 재현성 검증", 즉 벤더 발표를 신뢰할지 판단하는 도구입니다.
📚 선수 편 권고 (강력 권고)
이 편은 AI×바이오 하드코어 심화 편입니다. 진입 전에 DryBench의 다음 편들을 먼저 듣고·보시길 강력히 권고드립니다.
- DryBench ai-native #7 프롬프트 엔지니어링
- DryBench ai-native #11 할루시네이션·정렬
- DryBench ai-native #13 HuggingFace와 상용 API
선수 편 없이 진입할 경우, 이 편에서 다루는 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 등 신규 벤치도 존재.
이 편의 목표 지표
- 4
5개 벤치 × 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 | 결과 표 · heatmap | BSD |
| 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.
파이프라인 실전 구현
전체 흐름:
Step 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"/..., 자유서술은 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 questionsStep 2. 프롬프트 템플릿 (원 논문 재현)
원 논문에서 사용한 프롬프트를 최대한 그대로 복제. Zero-shot · Few-shot · CoT 변형별 별도.
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. 응답 파싱 (객관식 정답 추출)
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 NoneStep 4. LLM-as-Judge (HealthBench 자유서술)
HealthBench는 rubric 기반 스코어링. Judge LLM으로 자동화.
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 재사용.
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, # 편 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 리포트
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 (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가 크면 왜 그런지 원인 후보 확인.
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 diagnosisStep 8. 통합 파이프라인
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) | 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 발표 |
| GPT-4o | 0.88 | 0.76 | 0.87 | 0.72 | 0.65 | OpenAI 발표 [10] |
| Claude Opus 4.5 | 0.91 | 0.79 | 0.89 | 0.76 | 0.72 | Anthropic 발표 [11] |
| o1 | 0.95+ | 0.82 | 0.92 | 0.83 | 0.78 | OpenAI 발표 [10] |
| o3 | 0.94 | 0.82 | 0.91 | 0.81 | 0.79 | OpenAI 발표 [10] |
학습자 재현 예상 비용
- 6
8개 모델 × 4개 벤치 × 500 sample = 12,00016,000 요청, 총 약 30~120 USD (모델별 요금표 시점 확인). - 오픈 모델 로컬 실행 시 GPU 시간 별도.
알려진 실패 케이스 5건 (커뮤니티·논문 수집형)
-
벤치 데이터 오염 (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]. -
프롬프트 미묘한 차이로 정확도 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]. -
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]. -
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]. -
데이터셋 라이선스·접근 문제
증상: 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 성능을 벤치 관점에서 재조명.
참고 문헌
- 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 - Jin Q, Dhingra B, Liu Z, et al. "PubMedQA: A Dataset for Biomedical Research Question Answering." EMNLP 2019.
- Hendrycks D, Burns C, Basart S, et al. "Measuring Massive Multitask Language Understanding (MMLU)." ICLR 2021.
https://arxiv.org/abs/2009.03300 - Pal A, Umapathi LK, Sankarasubbu M. "MedMCQA: A Large-scale Multi-Subject Multi-Choice Dataset for Medical domain Question Answering." CHIL 2022.
- OpenAI HealthBench (2025 발표):
https://openai.com/index/healthbench/(실제 URL은 발표 시점 재확인) - 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 - Wu C, Zhang X, Zhang Y, et al. "PMC-LLaMA: Toward Building Open-source Language Models for Medicine." arXiv 2023.
- 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 - 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 - OpenAI model benchmarks:
https://openai.com/index/gpt-4o-system-card/· o1 · o3 system cards - Anthropic model benchmarks:
https://www.anthropic.com/news/claude-3-family· Claude 4 system card - Xu R et al. "Benchmark Data Contamination of Large Language Models: A Survey." arXiv 2024.
https://arxiv.org/abs/2406.04244 - Lu Y, Bartolo M, Moore A, et al. "Fantastically Ordered Prompts and Where to Find Them." ACL 2022.
- Zheng L, Chiang W-L, Sheng Y, et al. "Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena." NeurIPS 2023.
- Wei J, Wang X, Schuurmans D, et al. "Chain-of-Thought Prompting Elicits Reasoning in Large Language Models." NeurIPS 2022.
- HuggingFace datasets — bigbio/med_qa:
https://huggingface.co/datasets/bigbio/med_qa - HuggingFace datasets — qiaojin/PubMedQA:
https://huggingface.co/datasets/qiaojin/PubMedQA - HuggingFace datasets — cais/mmlu:
https://huggingface.co/datasets/cais/mmlu - Meditron GitHub:
https://github.com/epfLLM/meditron - HELM (Holistic Evaluation of Language Models):
https://crfm.stanford.edu/helm/