Download evaluate.py from nandutt/mini-llm-tinystories: direct link, hf CLI and curl.
- Browser
- Download file 6.29 kB
-
https://huggingface.co/nandutt/mini-llm-tinystories/resolve/main/evaluate.py
- Command line
-
hf download hf://nandutt/mini-llm-tinystories/evaluate.py
-
curl -L -o evaluate.py https://huggingface.co/nandutt/mini-llm-tinystories/resolve/main/evaluate.py
6.29 kB
| """ | |
| Stage 5 — EVALUATION (lecture: Evaluation, for pretraining AND post-training). | |
| Compare base vs SFT vs DPO on three axes: | |
| 1. Perplexity on the pretraining validation set -> pure "language modeling" ability. | |
| Usually rises a bit after SFT/DPO (the "alignment tax"). | |
| 2. Instruction following on held-out prompts -> the goal of post-training. | |
| Metrics: keyword hit rate (required words used), all-3-words rate, finish rate (story closed with EOS). | |
| 3. Reward-hacking detectors — things the reward does NOT see: | |
| - fluency ppl: perplexity of the generated story under the frozen BASE model (lower = more natural text) | |
| - mentions/word: how often each required word appears (~1-2 natural, higher = keyword stuffing) | |
| - repeated trigrams: fraction of word trigrams that repeat (looping text) | |
| Output: results/eval.json + results/eval.md (table + example generations). | |
| Run: python evaluate.py | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import math | |
| import os | |
| import numpy as np | |
| import torch | |
| from common import (autocast_ctx, ensure_dir, get_device, keyword_hit_rate, keyword_mentions, load_model, | |
| repeated_trigram_rate, set_seed) | |
| from config import get_config | |
| from data import TokenDataset | |
| from generation import sample_responses | |
| from pretrain import evaluate_loss | |
| from tokenizer import BPETokenizer | |
| def fluency_nll(base_model, tok: BPETokenizer, outs: list[dict], device: str, batch_size: int = 32) -> float: | |
| """Mean per-token NLL of the generated stories under the frozen base model (story only, no prompt).""" | |
| from dpo import seq_logprob | |
| ctx, _ = autocast_ctx(device) | |
| total, n_tok = 0.0, 0 | |
| resp = [o["ids"] for o in outs if o["ids"]] | |
| for i in range(0, len(resp), batch_size): | |
| b = resp[i:i + batch_size] | |
| lp = seq_logprob(base_model, [[tok.eos_id]] * len(b), b, tok.eos_id, device, ctx) | |
| total += -lp.sum().item() | |
| n_tok += sum(len(r) for r in b) | |
| return total / max(n_tok, 1) | |
| def evaluate_model(model, tok, cfg, val_prompts, base_model, device, n_samples_kept: int = 3): | |
| e = cfg.eval | |
| val_ds = TokenDataset(os.path.join(cfg.data.data_dir, "val.bin"), model.cfg.context_len) | |
| vl = evaluate_loss(model, val_ds, cfg.pretrain.batch_size, cfg.pretrain.eval_batches, device) | |
| set_seed(cfg.seed) # identical sampling across models -> fair comparison | |
| _, outs = sample_responses(model, tok, val_prompts, 1, e.gen_max_tokens, e.temperature, e.top_k, device) | |
| outs = [o[0] for o in outs] | |
| hits = [keyword_hit_rate(o["text"], w) for o, w in zip(outs, val_prompts)] | |
| metrics = { | |
| "val_loss": vl, | |
| "val_ppl": math.exp(vl), | |
| "keyword_hit_rate": float(np.mean(hits)), | |
| "all_keywords_rate": float(np.mean([h == 1.0 for h in hits])), | |
| "finish_rate": float(np.mean([o["finished"] for o in outs])), | |
| "avg_tokens": float(np.mean([len(o["ids"]) for o in outs])), | |
| "fluency_ppl": math.exp(fluency_nll(base_model, tok, outs, device)), | |
| "mentions_per_word": float(np.mean([keyword_mentions(o["text"], w) for o, w in zip(outs, val_prompts)])), | |
| "repeated_trigrams": float(np.mean([repeated_trigram_rate(o["text"]) for o in outs])), | |
| } | |
| samples = [{"words": w, "text": o["text"]} for w, o in zip(val_prompts[:n_samples_kept], outs[:n_samples_kept])] | |
| return metrics, samples | |
| def format_row(name: str, r: dict) -> str: | |
| return (f"| {name} | {r['val_ppl']:.2f} | {r['keyword_hit_rate']:.1%} | {r['all_keywords_rate']:.1%} | " | |
| f"{r['finish_rate']:.1%} | {r['fluency_ppl']:.2f} | {r['mentions_per_word']:.2f} | " | |
| f"{r['repeated_trigrams']:.1%} | {r['avg_tokens']:.0f} |") | |
| TABLE_HEADER = ["| Model | Val ppl ↓ | Keyword hit ↑ | All 3 words ↑ | Finished ↑ | Fluency ppl ↓ | " | |
| "Mentions/word | Repeated trigrams ↓ | Avg tokens |", | |
| "|---|---|---|---|---|---|---|---|---|"] | |
| def load_val_prompts(cfg) -> list[list[str]]: | |
| with open(os.path.join(cfg.data.data_dir, "sft_val.json")) as f: | |
| return [x["words"] for x in json.load(f)][: cfg.eval.n_prompts] | |
| def main() -> None: | |
| cfg = get_config() | |
| e = cfg.eval | |
| device = get_device() | |
| tok = BPETokenizer.load(os.path.join(cfg.data.data_dir, "tokenizer.json")) | |
| val_prompts = load_val_prompts(cfg) | |
| base_model, _ = load_model(cfg.pretrain.ckpt_path, device) | |
| base_model.eval() | |
| ckpts = {"base": cfg.pretrain.ckpt_path, "sft": cfg.sft.ckpt_path, "dpo": cfg.dpo.ckpt_path} | |
| results, samples = {}, {} | |
| for name, path in ckpts.items(): | |
| if not os.path.exists(path): | |
| print(f"Skipping {name}: {path} not found") | |
| continue | |
| model, _ = load_model(path, device) | |
| model.eval() | |
| results[name], samples[name] = evaluate_model(model, tok, cfg, val_prompts, base_model, device) | |
| r = results[name] | |
| print(f"{name:5s} | ppl {r['val_ppl']:6.2f} | keyword hit {r['keyword_hit_rate']:.1%} | " | |
| f"all-3-words {r['all_keywords_rate']:.1%} | finished {r['finish_rate']:.1%} | " | |
| f"fluency ppl {r['fluency_ppl']:.2f} | mentions/word {r['mentions_per_word']:.2f} | " | |
| f"rep-trigrams {r['repeated_trigrams']:.1%} | {r['avg_tokens']:.0f} tok") | |
| del model | |
| if device == "cuda": | |
| torch.cuda.empty_cache() | |
| ensure_dir("results/eval.json") | |
| with open("results/eval.json", "w") as f: | |
| json.dump({"n_prompts": len(val_prompts), "temperature": e.temperature, "top_k": e.top_k, | |
| "metrics": results, "samples": samples}, f, indent=2) | |
| lines = ["# Evaluation results", "", | |
| f"Held-out prompts: {len(val_prompts)} | temperature {e.temperature} | top-k {e.top_k}", "", | |
| *TABLE_HEADER] | |
| lines += [format_row(n, r) for n, r in results.items()] | |
| lines += ["", "## Example generations", ""] | |
| for n, ss in samples.items(): | |
| lines.append(f"### {n}") | |
| for s in ss: | |
| lines += [f"**Required words:** {', '.join(s['words'])}", "", f"> {s['text'].strip()[:600]}", ""] | |
| with open("results/eval.md", "w") as f: | |
| f.write("\n".join(lines)) | |
| print("\nSaved: results/eval.json, results/eval.md") | |
| if __name__ == "__main__": | |
| main() | |