File size: 8,466 Bytes
85a749e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 | """
scripts/evaluate.py
Evaluation script using lm-evaluation-harness (lm_eval==0.4.3).
Run:
# Evaluate a merged model (no LoRA adapter):
python scripts/evaluate.py --model-path ./final_model --tasks gsm8k mmlu
# Evaluate a LoRA checkpoint on top of base model:
python scripts/evaluate.py \
--model-path microsoft/Phi-3-mini-4k-instruct \
--peft-path ./checkpoints/grpo \
--tasks gsm8k mmlu strategy_qa
# Run full ablation across all checkpoints:
python scripts/evaluate.py --ablation
HALLUCINATION NOTES:
- lm_eval.simple_evaluate() signature verified for lm_eval==0.4.3.
Key params: model, model_args, tasks, num_fewshot, batch_size, limit, log_samples.
- Task names in lm_eval 0.4.3:
GSM8K: "gsm8k"
MMLU: "mmlu" (evaluates all MMLU subsets, averages)
StrategyQA: "strategy_qa"
These names were verified from lm_eval 0.4.3 task registry. If a task name
fails, run: lm_eval --tasks list | grep -i <name>
- MMLU num_fewshot=5 is standard practice from the original MMLU paper.
- model_args string format for PEFT: "pretrained=<base>,peft=<adapter_path>"
This is the documented lm_eval HF model_args format.
- 'limit' parameter accepts int (number of examples) or float (fraction).
"""
from __future__ import annotations
import argparse
import json
import logging
import os
import sys
from pathlib import Path
import pandas as pd
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
logger = logging.getLogger(__name__)
TASK_FEWSHOT = {
"gsm8k": 0,
"mmlu": 5,
"strategy_qa": 0,
}
# Metric keys to extract from lm_eval results
# HALLUCINATION NOTE: these metric key names are from lm_eval 0.4.3 output format.
# GSM8K uses 'exact_match,strict-match' or 'exact_match,flexible-extract'.
# MMLU uses 'acc,none'. StrategyQA uses 'acc,none'.
# If keys differ, print(results['results']) to inspect.
TASK_METRIC_KEY = {
"gsm8k": "exact_match,flexible-extract",
"mmlu": "acc,none",
"strategy_qa": "acc,none",
}
def parse_args():
parser = argparse.ArgumentParser(description="Evaluate SLM checkpoints")
parser.add_argument("--model-path", default="microsoft/Phi-3-mini-4k-instruct",
help="Path to merged model or HF model ID")
parser.add_argument("--peft-path", default=None,
help="Path to LoRA adapter checkpoint (optional)")
parser.add_argument("--tasks", nargs="+",
default=["gsm8k", "mmlu", "strategy_qa"])
parser.add_argument("--batch-size", type=int, default=4)
parser.add_argument("--limit", type=int, default=None,
help="Limit number of examples per task (for quick testing)")
parser.add_argument("--output-dir", default="results")
parser.add_argument("--run-name", default="eval",
help="Name for the output JSON file")
parser.add_argument("--ablation", action="store_true",
help="Run ablation across all standard checkpoints")
parser.add_argument("--device", default="cuda",
help="Device: 'cuda', 'cuda:0', 'cpu'")
return parser.parse_args()
def build_model_args(model_path: str, peft_path: str | None = None) -> str:
"""Build the model_args string for lm_eval simple_evaluate."""
args = f"pretrained={model_path}"
if peft_path:
args += f",peft={peft_path}"
args += ",trust_remote_code=True"
return args
def run_evaluation(
model_path: str,
peft_path: str | None,
tasks: list[str],
batch_size: int = 4,
limit: int | None = None,
device: str = "cuda",
) -> dict:
"""
Run lm_eval evaluation and return results dict.
Returns a flat dict: {task_name: score, ...}
"""
from lm_eval import simple_evaluate
model_args = build_model_args(model_path, peft_path)
logger.info("Evaluating: %s", model_args)
logger.info("Tasks: %s", tasks)
# num_fewshot: pass per-task as a list aligned with tasks
num_fewshot = [TASK_FEWSHOT.get(t, 0) for t in tasks]
raw_results = simple_evaluate(
model="hf",
model_args=model_args,
tasks=tasks,
num_fewshot=num_fewshot,
batch_size=batch_size,
limit=limit,
log_samples=False,
device=device,
)
# Extract primary metric per task
scores = {}
for task in tasks:
if task not in raw_results["results"]:
logger.warning("Task '%s' not found in results.", task)
continue
task_results = raw_results["results"][task]
metric_key = TASK_METRIC_KEY.get(task)
if metric_key and metric_key in task_results:
scores[task] = task_results[metric_key]
else:
# Fallback: print all available keys so user can find the right one
logger.warning(
"Metric key '%s' not found for task '%s'. Available: %s",
metric_key, task, list(task_results.keys()),
)
# Try common fallbacks
for fallback in ["acc,none", "exact_match,flexible-extract",
"exact_match,strict-match"]:
if fallback in task_results:
scores[task] = task_results[fallback]
logger.info("Used fallback metric '%s' for task '%s'", fallback, task)
break
return scores, raw_results
def save_results(
scores: dict,
raw_results: dict,
output_dir: str,
run_name: str,
) -> None:
os.makedirs(output_dir, exist_ok=True)
out_path = os.path.join(output_dir, f"{run_name}.json")
with open(out_path, "w") as f:
json.dump({"scores": scores, "raw": raw_results["results"]}, f, indent=2)
logger.info("Results saved to %s", out_path)
def run_ablation(tasks: list[str], batch_size: int, limit: int | None, device: str):
"""
Run evaluation across all standard checkpoints and print the ablation table.
Checkpoints expected (adjust paths to match your training output):
- Baseline: microsoft/Phi-3-mini-4k-instruct (no peft)
- SFT: checkpoints/sft (LoRA on base)
- GRPO: checkpoints/grpo (LoRA on base)
- Curriculum GRPO: checkpoints/curriculum_grpo (LoRA on base)
"""
base_model = "microsoft/Phi-3-mini-4k-instruct"
runs = [
("Baseline", base_model, None),
("SFT", base_model, "checkpoints/sft"),
("SFT + GRPO", base_model, "checkpoints/grpo"),
("SFT + Curriculum", base_model, "checkpoints/curriculum_grpo"),
]
all_scores = []
for name, model_path, peft_path in runs:
if peft_path and not os.path.exists(peft_path):
logger.warning("Checkpoint not found: %s — skipping %s", peft_path, name)
continue
logger.info("Evaluating: %s", name)
scores, raw = run_evaluation(model_path, peft_path, tasks, batch_size, limit, device)
scores["run"] = name
all_scores.append(scores)
save_results(scores, raw, "results", name.replace(" ", "_").lower())
if all_scores:
df = pd.DataFrame(all_scores).set_index("run")
# Format as percentage
for col in df.columns:
df[col] = (df[col] * 100).round(2).astype(str) + "%"
print("\n" + "="*60)
print("ABLATION TABLE")
print("="*60)
print(df.to_string())
print("="*60 + "\n")
df.to_csv("results/ablation_table.csv")
logger.info("Ablation table saved to results/ablation_table.csv")
def main():
args = parse_args()
if args.ablation:
run_ablation(
tasks=args.tasks,
batch_size=args.batch_size,
limit=args.limit,
device=args.device,
)
return
scores, raw_results = run_evaluation(
model_path=args.model_path,
peft_path=args.peft_path,
tasks=args.tasks,
batch_size=args.batch_size,
limit=args.limit,
device=args.device,
)
print("\n=== SCORES ===")
for task, score in scores.items():
print(f" {task:20s}: {score*100:.2f}%")
save_results(scores, raw_results, args.output_dir, args.run_name)
if __name__ == "__main__":
main() |