Spaces:
Running
Running
| """Retrieval -> answer loop: Exact-Match / F1 on HotpotQA gold answers. | |
| For each architecture, on the **same sample**: nDCG@10 (retrieval quality) and | |
| EM / F1 (generated answer quality), against the HotpotQA gold answers. No judge, | |
| deterministic (the model runs at temperature 0). Lets you answer: "does better | |
| retrieval lead to better answers?" and compare a small vs a large model | |
| (--model). | |
| python -m eval.answer_eval --max-queries 100 --model llama3.2:1b | |
| python -m eval.answer_eval --max-queries 100 --model llama3.2:3b | |
| """ | |
| import argparse | |
| import json | |
| import os | |
| import sys | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parent.parent | |
| sys.path.insert(0, str(ROOT / "src")) | |
| from shared.answer_metrics import exact_match, f1_score # noqa: E402 | |
| from shared.ir_metrics import ndcg_at_k # noqa: E402 | |
| from pipeline import assemble_stacks # noqa: E402 | |
| from eval.beir_eval import K_MAX, _ranked_doc_ids # noqa: E402 | |
| def load_hotpot_with_answers(n_questions: int, split: str = "validation"): | |
| """Like the HotpotQA distractor loader, but also returns the gold answers. | |
| Returns (texts, metadata, queries, qrels, golds): the corpus = union of the | |
| paragraphs (support + distractors), qrels = support titles, golds[id] = answer. | |
| """ | |
| from datasets import load_dataset | |
| ds = load_dataset("hotpotqa/hotpot_qa", "distractor", split=split) | |
| ds = ds.select(range(min(n_questions, len(ds)))) | |
| corpus: dict[str, str] = {} | |
| queries, qrels, golds = [], {}, {} | |
| for ex in ds: | |
| for title, sentences in zip(ex["context"]["title"], ex["context"]["sentences"]): | |
| corpus.setdefault(title, " ".join(sentences).strip()) | |
| qrels[ex["id"]] = {t: 1.0 for t in set(ex["supporting_facts"]["title"])} | |
| queries.append((ex["id"], ex["question"])) | |
| golds[ex["id"]] = ex["answer"] | |
| titles = list(corpus) | |
| texts = [corpus[t] for t in titles] | |
| metadata = [{"doc_id": t, "title": t} for t in titles] | |
| return texts, metadata, queries, qrels, golds | |
| def run(max_queries: int, model: str | None, output: Path) -> dict: | |
| if model: | |
| os.environ["OLLAMA_MODEL"] = model | |
| texts, metadata, queries, qrels, golds = load_hotpot_with_answers(max_queries or 100) | |
| print(f"HotpotQA: {len(texts)} docs, {len(queries)} questions - indexing + generation " | |
| f"(model {model or os.getenv('OLLAMA_MODEL', '?')}, temperature 0)...", flush=True) | |
| stacks = assemble_stacks(texts, metadata) | |
| report = {} | |
| for sname, rag in stacks.items(): | |
| agg = {"ndcg@10": 0.0, "em": 0.0, "f1": 0.0} | |
| for qid, qtext in queries: | |
| out = rag.query(qtext, k=K_MAX) | |
| agg["em"] += exact_match(out["answer"], golds[qid]) | |
| agg["f1"] += f1_score(out["answer"], golds[qid]) | |
| agg["ndcg@10"] += ndcg_at_k(_ranked_doc_ids(out["contexts"]), qrels[qid], 10) | |
| n = len(queries) | |
| report[sname] = {m: round(v / n, 4) for m, v in agg.items()} | {"n_queries": n} | |
| payload = {"config": {"dataset": "hotpotqa-distractor", | |
| "model": model or os.getenv("OLLAMA_MODEL", "?"), | |
| "n_docs": len(texts), "n_queries": len(queries)}, | |
| "stacks": report} | |
| output.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8") | |
| print(f"\nHotpotQA - gold answers · {len(queries)} questions · model {payload['config']['model']}\n") | |
| print(f" {'architecture':28s} {'nDCG@10':>8s} {'EM':>7s} {'F1':>7s}") | |
| for sname, m in report.items(): | |
| print(f" {sname:28s} {m['ndcg@10']:8.3f} {m['em']:7.3f} {m['f1']:7.3f}") | |
| print(f"\n✅ Details written to {output}") | |
| return payload | |
| def main() -> None: | |
| ap = argparse.ArgumentParser(description="EM/F1 on HotpotQA gold answers (no judge, temperature 0).") | |
| ap.add_argument("--max-queries", type=int, default=100, help="number of questions (default 100)") | |
| ap.add_argument("--model", default=None, help="Ollama model (e.g. llama3.2:1b or llama3.2:3b)") | |
| ap.add_argument("--output", type=Path, default=ROOT / "eval" / "answer_results.json") | |
| args = ap.parse_args() | |
| run(args.max_queries, args.model, args.output) | |
| if __name__ == "__main__": | |
| main() | |