Spaces:
Sleeping
Sleeping
Download src/evaluate-model.py from EbiAraz/semantic-rag: direct link, hf CLI and curl.
- Browser
- Download file 5.31 kB
-
https://huggingface.co/spaces/EbiAraz/semantic-rag/resolve/main/src/evaluate-model.py
- Command line
-
hf download hf://spaces/EbiAraz/semantic-rag/src/evaluate-model.py
-
curl -L -o evaluate-model.py https://huggingface.co/spaces/EbiAraz/semantic-rag/resolve/main/src/evaluate-model.py
5.31 kB
| import argparse | |
| import importlib.util | |
| import json | |
| import re | |
| import string | |
| from collections import Counter | |
| from pathlib import Path | |
| from sklearn.feature_extraction.text import TfidfVectorizer | |
| from config import CONFIG | |
| from inference import RAGEngine | |
| def _load_data_loader_module(): | |
| module_path = Path(__file__).with_name("data-loader.py") | |
| spec = importlib.util.spec_from_file_location("data_loader", module_path) | |
| if spec is None or spec.loader is None: | |
| raise RuntimeError("Could not load data-loader.py") | |
| module = importlib.util.module_from_spec(spec) | |
| spec.loader.exec_module(module) | |
| return module | |
| def normalize_text(text: str) -> str: | |
| text = text.lower() | |
| text = text.translate(str.maketrans("", "", string.punctuation)) | |
| text = re.sub(r"\b(a|an|the)\b", " ", text) | |
| text = re.sub(r"\s+", " ", text) | |
| return text.strip() | |
| def exact_match_score(prediction: str, ground_truth: str) -> float: | |
| return float(normalize_text(prediction) == normalize_text(ground_truth)) | |
| def f1_score(prediction: str, ground_truth: str) -> float: | |
| pred_tokens = normalize_text(prediction).split() | |
| truth_tokens = normalize_text(ground_truth).split() | |
| if not pred_tokens and not truth_tokens: | |
| return 1.0 | |
| if not pred_tokens or not truth_tokens: | |
| return 0.0 | |
| common = Counter(pred_tokens) & Counter(truth_tokens) | |
| num_same = sum(common.values()) | |
| if num_same == 0: | |
| return 0.0 | |
| precision = num_same / len(pred_tokens) | |
| recall = num_same / len(truth_tokens) | |
| return 2 * precision * recall / (precision + recall) | |
| def build_examples(limit: int) -> list[dict]: | |
| data_loader = _load_data_loader_module() | |
| from datasets import load_dataset | |
| dataset = load_dataset(CONFIG.dataset_name, split=CONFIG.dataset_question_split) | |
| examples = [] | |
| for row in dataset: | |
| answers = row.get("answers", {}) | |
| answer_texts = answers.get("text", []) if isinstance(answers, dict) else [] | |
| if not answer_texts: | |
| continue | |
| examples.append( | |
| { | |
| "question": str(row[CONFIG.dataset_question_field]), | |
| "answers": [str(answer) for answer in answer_texts], | |
| "context": str(row.get(CONFIG.dataset_context_field, "")), | |
| } | |
| ) | |
| if len(examples) >= limit: | |
| break | |
| if not examples: | |
| questions = data_loader.load_questions() | |
| for question in questions[:limit]: | |
| examples.append({"question": question, "answers": [question], "context": ""}) | |
| return examples | |
| def evaluate(limit: int, *, top_k: int | None = None) -> dict: | |
| engine = RAGEngine() | |
| examples = build_examples(limit) | |
| # Align retrieval corpus to evaluation contexts so the gold answers are reachable. | |
| eval_documents = [] | |
| seen_contexts = set() | |
| for example in examples: | |
| ctx = example.get("context", "").strip() | |
| if not ctx or ctx in seen_contexts: | |
| continue | |
| seen_contexts.add(ctx) | |
| eval_documents.append(ctx) | |
| if eval_documents: | |
| engine.documents = eval_documents | |
| engine.chunks = engine._chunk_documents(eval_documents) | |
| engine.vectorizer = TfidfVectorizer(stop_words="english") | |
| engine.chunk_tfidf_matrix = engine.vectorizer.fit_transform([chunk["text"] for chunk in engine.chunks]) | |
| engine.chunk_embeddings = engine._encode_texts([chunk["text"] for chunk in engine.chunks]) | |
| results = [] | |
| em_total = 0.0 | |
| f1_total = 0.0 | |
| for example in examples: | |
| prediction = engine.answer(example["question"], top_k=top_k) | |
| pred_text = prediction["answer"] | |
| answers = example["answers"] | |
| em = max(exact_match_score(pred_text, answer) for answer in answers) | |
| f1 = max(f1_score(pred_text, answer) for answer in answers) | |
| em_total += em | |
| f1_total += f1 | |
| results.append( | |
| { | |
| "question": example["question"], | |
| "prediction": pred_text, | |
| "gold_answers": answers, | |
| "gold_context": example.get("context", ""), | |
| "exact_match": em, | |
| "f1": f1, | |
| "answer_score": prediction["answer_score"], | |
| "context": prediction["context"], | |
| } | |
| ) | |
| count = len(results) or 1 | |
| report = { | |
| "dataset": CONFIG.dataset_name, | |
| "split": CONFIG.dataset_question_split, | |
| "examples": len(results), | |
| "exact_match": em_total / count, | |
| "f1": f1_total / count, | |
| "results": results, | |
| } | |
| return report | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description="Evaluate the RAG pipeline on a Hugging Face QA dataset.") | |
| parser.add_argument("--limit", type=int, default=25, help="Number of evaluation examples to score.") | |
| parser.add_argument("--top-k", type=int, default=None, help="Override the number of retrieved passages used during evaluation.") | |
| parser.add_argument("--fast", action="store_true", help="Run a smaller and faster evaluation using fewer examples and passages.") | |
| parser.add_argument( | |
| "--output", | |
| type=str, | |
| default=str(CONFIG.output_path.parent / "evaluation_report.json"), | |
| help="Path to write the evaluation report.", | |
| ) | |
| args = parser.parse_args() | |
| limit = args.limit | |
| top_k = args.top_k | |
| if args.fast: | |
| limit = min(limit, 2) | |
| top_k = 1 if top_k is None else min(top_k, 1) | |
| report = evaluate(limit, top_k=top_k) | |
| output_path = Path(args.output) | |
| output_path.parent.mkdir(parents=True, exist_ok=True) | |
| output_path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8") | |
| print(f"Evaluated {report['examples']} examples") | |
| print(f"Exact Match: {report['exact_match']:.4f}") | |
| print(f"F1: {report['f1']:.4f}") | |
| print(f"Saved report to {output_path}") | |
| if __name__ == "__main__": | |
| main() | |