Spaces:
Configuration error
Configuration error
Download evaluate.py from mutoni1923/AgroChat: direct link, hf CLI and curl.
- Browser
- Download file 5.36 kB
-
https://huggingface.co/spaces/mutoni1923/AgroChat/resolve/main/evaluate.py
- Command line
-
hf download hf://spaces/mutoni1923/AgroChat/evaluate.py
-
curl -L -o evaluate.py https://huggingface.co/spaces/mutoni1923/AgroChat/resolve/main/evaluate.py
5.36 kB
| """Evaluate baseline vs. fine-tuned embedder across thresholds. | |
| Run: | |
| python evaluate.py | |
| Writes: | |
| eval_results.csv | |
| Uses the held-out test split (data/qa_test.csv) for retrieval accuracy and | |
| a fixed list of out-of-domain questions for rejection rate. | |
| """ | |
| from __future__ import annotations | |
| import csv | |
| import time | |
| from pathlib import Path | |
| import numpy as np | |
| from sentence_transformers import SentenceTransformer | |
| from sklearn.metrics import f1_score, precision_score, recall_score | |
| BASE_DIR = Path(__file__).resolve().parent | |
| DATA = BASE_DIR / "data" | |
| OUT_OF_DOMAIN_TESTS = [ | |
| "Who won the football match yesterday?", | |
| "Explain the history of ancient Rome.", | |
| "How do I write a Python web scraper?", | |
| "What is the capital of Japan?", | |
| "Recommend a romantic movie.", | |
| "How do I fix a broken laptop screen?", | |
| "Who is the current president of France?", | |
| "Write a poem about the ocean.", | |
| "What is the stock price of a technology company?", | |
| "Explain the rules of chess.", | |
| "What is the boiling point of mercury?", | |
| "How do I install Docker on Windows?", | |
| ] | |
| THRESHOLDS = [0.30, 0.35, 0.40, 0.42, 0.45, 0.50, 0.55, 0.60] | |
| CONFIGS = [ | |
| ("baseline (pretrained MiniLM)", | |
| "sentence-transformers/all-MiniLM-L6-v2"), | |
| ("fine-tuned MiniLM", | |
| str(BASE_DIR / "models" / "agri-minilm")), | |
| ] | |
| def load_csv(path: Path) -> list[dict]: | |
| with open(path, newline="", encoding="utf-8") as f: | |
| return list(csv.DictReader(f)) | |
| def build(embedder_path: str): | |
| path = embedder_path | |
| if path.startswith("models/") or path.startswith(str(BASE_DIR)): | |
| if not Path(path).exists(): | |
| print(f"[skip] {path} missing — run train_embedder.py first") | |
| return None, None | |
| model = SentenceTransformer(path) | |
| test_rows = load_csv(DATA / "qa_test.csv") | |
| corpus = [r["answer"] for r in test_rows] | |
| corpus_emb = model.encode( | |
| corpus, normalize_embeddings=True, convert_to_numpy=True | |
| ) | |
| return model, (test_rows, corpus_emb) | |
| def evaluate(model, payload, threshold: float): | |
| test_rows, corpus_emb = payload | |
| if not test_rows: | |
| raise ValueError("The test dataset is empty.") | |
| if len(OUT_OF_DOMAIN_TESTS) == 0: | |
| raise ValueError("The out-of-domain test set is empty.") | |
| questions = [r["question"] for r in test_rows] | |
| expected_answers = [r["answer"] for r in test_rows] | |
| # Encode each in-domain question once. | |
| t0 = time.perf_counter() | |
| question_emb = model.encode( | |
| questions, | |
| normalize_embeddings=True, | |
| convert_to_numpy=True, | |
| show_progress_bar=False, | |
| ) | |
| elapsed_ms = (time.perf_counter() - t0) * 1000 | |
| times = [elapsed_ms / len(questions)] * len(questions) | |
| # Similarity between every question and every answer in the corpus. | |
| scores_in = question_emb @ corpus_emb.T | |
| best_indices = np.argmax(scores_in, axis=1) | |
| best_scores = scores_in[np.arange(len(test_rows)), best_indices] | |
| # Correct if the retrieved answer matches the expected answer. | |
| top1 = sum( | |
| corpus_answer == expected | |
| for corpus_answer, expected in zip( | |
| [test_rows[i]["answer"] for i in best_indices], | |
| expected_answers, | |
| ) | |
| ) | |
| accepted_in = int(np.sum(best_scores >= threshold)) | |
| # Encode out-of-domain questions once. | |
| ood_emb = model.encode( | |
| OUT_OF_DOMAIN_TESTS, | |
| normalize_embeddings=True, | |
| convert_to_numpy=True, | |
| show_progress_bar=False, | |
| ) | |
| scores_ood = ood_emb @ corpus_emb.T | |
| best_ood_scores = np.max(scores_ood, axis=1) | |
| rejected = int(np.sum(best_ood_scores < threshold)) | |
| n = len(test_rows) | |
| n_ood = len(OUT_OF_DOMAIN_TESTS) | |
| # 1 = in-domain, 0 = out-of-domain. | |
| y_true = [1] * n + [0] * n_ood | |
| y_pred = ( | |
| (best_scores >= threshold).astype(int).tolist() | |
| + (best_ood_scores >= threshold).astype(int).tolist() | |
| ) | |
| p = precision_score(y_true, y_pred, zero_division=0) | |
| r = recall_score(y_true, y_pred, zero_division=0) | |
| f = f1_score(y_true, y_pred, zero_division=0) | |
| return { | |
| "top1_accuracy": round(top1 / n, 4), | |
| "in_domain_acceptance": round(accepted_in / n, 4), | |
| "ood_rejection_rate": round(rejected / n_ood, 4), | |
| "precision": round(p, 4), | |
| "recall": round(r, 4), | |
| "f1": round(f, 4), | |
| "mean_ms": round(float(np.mean(times)), 2), | |
| } | |
| def main(): | |
| results = [] | |
| for label, path in CONFIGS: | |
| model, payload = build(path) | |
| if model is None: | |
| continue | |
| print(f"\n=== {label} ===") | |
| for th in THRESHOLDS: | |
| m = evaluate(model, payload, th) | |
| print( | |
| f" th={th:.2f} top1={m['top1_accuracy']:.3f} " | |
| f"in={m['in_domain_acceptance']:.3f} " | |
| f"ood_rej={m['ood_rejection_rate']:.3f} " | |
| f"P={m['precision']:.3f} R={m['recall']:.3f} " | |
| f"F1={m['f1']:.3f} ms={m['mean_ms']:.1f}" | |
| ) | |
| results.append({"config": label, "threshold": th, **m}) | |
| out = BASE_DIR / "eval_results.csv" | |
| with open(out, "w", newline="", encoding="utf-8") as f: | |
| w = csv.DictWriter(f, fieldnames=results[0].keys()) | |
| w.writeheader() | |
| w.writerows(results) | |
| print(f"\n[done] written to {out}") | |
| if __name__ == "__main__": | |
| main() |