Spaces:
Sleeping
Sleeping
File size: 3,416 Bytes
39ff835 | 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 | """Run offline evaluation (Precision@K / Recall / F1 / latency) across methods.
Uses the prebuilt index at ``artifacts/index.pkl`` if present, otherwise builds
one from the committed sample. Relevance is judged with pseudo-qrels derived from
the dataset's category labels (see ``news_search.evaluate``).
Usage
-----
python scripts/evaluate.py
python scripts/evaluate.py --index artifacts/index.pkl --top-k 10
"""
from __future__ import annotations
import argparse
import pickle
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))
from news_search import SearchEngine, build_index, load_corpus # noqa: E402
from news_search.index import InvertedIndex # noqa: E402
from news_search.evaluate import evaluate # noqa: E402
ALL_METHODS = ["bm25", "tfidf", "prf", "wordnet", "bert", "hybrid"]
BASE_METHODS = ["bm25", "tfidf", "prf", "wordnet"]
def _load_dense(index_path: Path):
"""Load the dense (BERT) retriever sitting next to the index, if present."""
dense_path = Path(index_path).with_name("dense.pkl")
if not dense_path.exists():
return None
try:
with dense_path.open("rb") as fh:
return pickle.load(fh)
except Exception as exc: # pragma: no cover
print(f"[warn] could not load {dense_path}: {exc}")
return None
DEFAULT_QUERIES = [
"covid vaccine health",
"election president vote",
"movie film",
"stock market money",
"game sport",
"travel food recipe",
]
def main() -> int:
ap = argparse.ArgumentParser(description="Offline IR evaluation across methods.")
ap.add_argument("--index", default=str(ROOT / "artifacts" / "index.pkl"),
help="Prebuilt index to evaluate (falls back to the sample).")
ap.add_argument("--data", default=str(ROOT / "data" / "sample_news.jsonl"),
help="Dataset used when no prebuilt index exists.")
ap.add_argument("--top-k", type=int, default=10)
ap.add_argument("--methods", nargs="*", default=None,
help="Subset of methods to evaluate (default: bm25 tfidf prf wordnet).")
args = ap.parse_args()
if Path(args.index).exists():
print(f"Loading index from {args.index}")
engine = SearchEngine(InvertedIndex.load(args.index), dense=_load_dense(Path(args.index)))
else:
print(f"No index at {args.index}; building from {args.data}")
engine = SearchEngine(build_index(load_corpus(args.data), verbose=False))
# Evaluate BERT methods too when embeddings are available.
methods = args.methods or (ALL_METHODS if engine.dense is not None else BASE_METHODS)
print(f"Index: {engine.index.num_docs:,} docs | "
f"BERT {'enabled' if engine.dense is not None else 'disabled'} | methods: {methods}")
# Warm up the BERT model so its one-time load isn't charged to the first
# timed query (gives a fair steady-state latency).
if engine.dense is not None:
engine.search("warmup query", method="bert", top_k=1)
rows = evaluate(engine, DEFAULT_QUERIES, methods=methods, top_k=args.top_k)
print(f"\n{'method':<12}{'P@K':>8}{'Recall':>9}{'F1':>8}{'avg ms':>9}")
print("-" * 46)
for r in rows:
print(f"{r.method:<12}{r.precision:>8.3f}{r.recall:>9.3f}{r.f1:>8.3f}{r.avg_ms:>9.1f}")
return 0
if __name__ == "__main__":
sys.exit(main())
|