Spaces:
Configuration error
Configuration error
File size: 5,360 Bytes
ee140b6 | 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 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | """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() |