Spaces:
Sleeping
Sleeping
File size: 8,164 Bytes
b3515de | 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 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | """
scripts/experiment.py
---------------------
Experiment: k-sensitivity analysis β how does retrieval depth (k) affect
Precision@k across all three retrieval strategies?
This is a retrieval-only experiment β no OpenAI API calls are made.
Results show how many correct chunks each retriever finds as k increases.
Run (from project root):
.venv/bin/python scripts/experiment.py catan
.venv/bin/python scripts/experiment.py monopoly
.venv/bin/python scripts/experiment.py all
Outputs:
data/outputs/{game}_experiment_k_sensitivity.csv
"""
import csv
import json
import os
import sys
import random
from pathlib import Path
K_VALUES = [1, 2, 3, 5, 7, 10]
RETRIEVERS = ["random", "tfidf", "dense"]
RANDOM_SEED = 42
# ---------------------------------------------------------------------------
# Retrieval-only implementations (no OpenAI needed)
# ---------------------------------------------------------------------------
def _load_game(game: str) -> dict:
"""Load all retrieval artifacts for the given game."""
chunks_path = f"models/{game}/chunks.json"
index_path = f"models/{game}/faiss.index"
if not os.path.exists(chunks_path) or not os.path.exists(index_path):
print(f"ERROR: Run build_features.py {game} first.", file=sys.stderr)
sys.exit(1)
with open(chunks_path) as f:
chunks = json.load(f)
try:
import faiss
from sentence_transformers import SentenceTransformer
except ImportError as e:
print(f"Missing dependency: {e}", file=sys.stderr)
sys.exit(1)
print(f" Loading dense model + FAISS index for '{game}'...")
dense_model = SentenceTransformer("all-MiniLM-L6-v2")
index = faiss.read_index(index_path)
entry = {"chunks": chunks, "dense_model": dense_model, "index": index}
tfidf_matrix_path = f"models/{game}/tfidf_matrix.npz"
tfidf_vectorizer_path = f"models/{game}/tfidf_vectorizer.pkl"
if os.path.exists(tfidf_matrix_path) and os.path.exists(tfidf_vectorizer_path):
import pickle
import scipy.sparse
entry["tfidf_matrix"] = scipy.sparse.load_npz(tfidf_matrix_path)
with open(tfidf_vectorizer_path, "rb") as f:
entry["tfidf_vectorizer"] = pickle.load(f)
print(f" Loaded TF-IDF index for '{game}'.")
else:
print(f" WARNING: TF-IDF index not found for '{game}' β skipping tfidf retriever.", file=sys.stderr)
return entry
def _retrieve_indices(query: str, k: int, retriever: str, reg: dict) -> list[int]:
"""Return the chunk indices selected by the given retriever for this query."""
chunks = reg["chunks"]
if retriever == "random":
rng = random.Random(RANDOM_SEED)
return rng.sample(range(len(chunks)), min(k, len(chunks)))
elif retriever == "tfidf":
if "tfidf_vectorizer" not in reg:
return []
from sklearn.metrics.pairwise import cosine_similarity
query_vec = reg["tfidf_vectorizer"].transform([query])
scores = cosine_similarity(query_vec, reg["tfidf_matrix"])[0]
return list(scores.argsort()[::-1][:k])
else: # dense
import faiss
query_emb = reg["dense_model"].encode([query], convert_to_numpy=True)
faiss.normalize_L2(query_emb)
_, indices = reg["index"].search(query_emb, k)
return [int(i) for i in indices[0] if i >= 0]
def _precision_at_k(indices: list[int], keywords: list[str], chunks: list[dict]) -> float:
"""1 if at least one keyword appears in any retrieved chunk title, else 0."""
if not keywords:
return 0.0
retrieved_titles = [chunks[i]["title"].lower() for i in indices]
for kw in keywords:
if any(kw.lower() in t for t in retrieved_titles):
return 1.0
return 0.0
def _coverage(indices: list[int], keywords: list[str], chunks: list[dict]) -> float:
"""Fraction of required keywords found across retrieved chunks."""
if not keywords:
return 0.0
retrieved_titles = [chunks[i]["title"].lower() for i in indices]
hits = sum(1 for kw in keywords if any(kw.lower() in t for t in retrieved_titles))
return hits / len(keywords)
# ---------------------------------------------------------------------------
# Experiment runner
# ---------------------------------------------------------------------------
def run_experiment(game: str) -> list[dict]:
eval_path = f"data/raw/{game}_eval.json"
if not os.path.exists(eval_path):
print(f"ERROR: {eval_path} not found.", file=sys.stderr)
sys.exit(1)
print(f"\nLoading artifacts for '{game}'...")
reg = _load_game(game)
with open(eval_path) as f:
data = json.load(f)
questions = data.get("correctness_questions", []) + data.get("stress_test_questions", [])
print(f"\nRunning k-sensitivity experiment β {len(questions)} questions Γ {len(K_VALUES)} k-values Γ {len(RETRIEVERS)} retrievers")
print("=" * 70)
rows = []
for retriever in RETRIEVERS:
if retriever == "tfidf" and "tfidf_vectorizer" not in reg:
print(f" Skipping '{retriever}' β TF-IDF index not built.")
continue
for k in K_VALUES:
precisions = []
coverages = []
for q in questions:
indices = _retrieve_indices(q["question"], k, retriever, reg)
keywords = q.get("source_keywords", [])
precisions.append(_precision_at_k(indices, keywords, reg["chunks"]))
coverages.append(_coverage(indices, keywords, reg["chunks"]))
avg_prec = sum(precisions) / len(precisions) * 100
avg_cov = sum(coverages) / len(coverages) * 100
rows.append({
"game": game,
"retriever": retriever,
"k": k,
"precision_at_k": round(avg_prec, 1),
"coverage": round(avg_cov, 1),
"n_questions": len(questions),
})
print(f" [{retriever:<6}] k={k:2d} Precision@k={avg_prec:5.1f}% Coverage={avg_cov:5.1f}%")
# Save CSV
Path("data/outputs").mkdir(parents=True, exist_ok=True)
out_path = f"data/outputs/{game}_experiment_k_sensitivity.csv"
with open(out_path, "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
writer.writeheader()
writer.writerows(rows)
print(f"\nSaved β '{out_path}'")
_print_experiment_summary(rows, game)
return rows
def _print_experiment_summary(rows: list[dict], game: str):
print(f"\n{'='*70}")
print(f"K-SENSITIVITY SUMMARY β {game.upper()}")
print(f"{'='*70}")
print(f"\n{'Retriever':<10} {'k':>4} {'Precision@k':>12} {'Coverage':>10}")
print("-" * 44)
for row in rows:
print(
f" {row['retriever']:<8} {row['k']:>4} "
f"{row['precision_at_k']:>10.1f}% {row['coverage']:>8.1f}%"
)
print()
# Highlight key finding
dense_k3 = next((r for r in rows if r["retriever"] == "dense" and r["k"] == 3), None)
tfidf_k3 = next((r for r in rows if r["retriever"] == "tfidf" and r["k"] == 3), None)
random_k3 = next((r for r in rows if r["retriever"] == "random" and r["k"] == 3), None)
print("Key comparison at k=3:")
for label, row in [("Dense (DL)", dense_k3), ("TF-IDF (ML)", tfidf_k3), ("Random", random_k3)]:
if row:
print(f" {label:<14} Precision@3={row['precision_at_k']:.1f}% Coverage={row['coverage']:.1f}%")
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="k-sensitivity experiment for retriever comparison.")
parser.add_argument("game", help="Game to run experiment on, or 'all' for both")
args = parser.parse_args()
games = ["catan", "monopoly"] if args.game == "all" else [args.game]
for game in games:
run_experiment(game)
|