Download scripts/run_experiment.py from yashb18/deepreads: direct link, hf CLI and curl.
- Browser
- Download file 7.94 kB
-
https://huggingface.co/spaces/yashb18/deepreads/resolve/main/scripts/run_experiment.py
- Command line
-
hf download hf://spaces/yashb18/deepreads/scripts/run_experiment.py
-
curl -L -o run_experiment.py https://huggingface.co/spaces/yashb18/deepreads/resolve/main/scripts/run_experiment.py
7.94 kB
| """ | |
| Script: run_experiment.py | |
| Feature Ablation Experiment. | |
| Compares LightGBM performance across four feature tiers: | |
| 1. id_only β encoded user/item IDs | |
| 2. id_metadata β + item stats, category, brand, price | |
| 3. id_metadata_text β + user-item title cosine similarity | |
| 4. id_metadata_text_history β + user history aggregates (full set) | |
| For each tier: train LightGBM β evaluate on test β record NDCG@10 and Recall@10. | |
| Outputs: | |
| data/outputs/experiment/ablation_results.csv | |
| data/outputs/experiment/ablation_ndcg.png | |
| data/outputs/experiment/ablation_recall.png | |
| Usage: | |
| python scripts/run_experiment.py | |
| python scripts/run_experiment.py --debug | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import pickle | |
| import sys | |
| from pathlib import Path | |
| sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) | |
| import matplotlib.pyplot as plt | |
| import pandas as pd | |
| from src.config import MODELS_DIR, PROCESSED_DIR, ensure_dirs, get_config | |
| from src.evaluation.metrics import compute_all_metrics | |
| from src.features.builder import FEATURE_SETS, build_feature_matrix | |
| from src.features.text_features import build_user_text_profiles | |
| from src.models.classical import LightGBMReranker | |
| from src.models.naive import GlobalPopularityRecommender | |
| from src.utils.logging import get_logger | |
| logger = get_logger(__name__) | |
| def parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser(description="Run feature ablation experiment.") | |
| parser.add_argument("--debug", action="store_true") | |
| parser.add_argument("--k", type=int, default=10, help="Primary K for the experiment table.") | |
| return parser.parse_args() | |
| def train_and_evaluate_tier( | |
| feature_set: str, | |
| train_df: pd.DataFrame, | |
| val_df: pd.DataFrame, | |
| test_df: pd.DataFrame, | |
| user_features: pd.DataFrame, | |
| item_features: pd.DataFrame, | |
| user_profiles: pd.DataFrame, | |
| vectorizer, | |
| naive_model: GlobalPopularityRecommender, | |
| cfg, | |
| candidate_pool_size: int = 100, | |
| k: int = 10, | |
| ) -> dict[str, float]: | |
| """Train a LightGBM model for a given feature tier and evaluate it.""" | |
| logger.info(" Feature set: %s", feature_set) | |
| # Build feature matrices for this tier | |
| train_feat, feat_cols = build_feature_matrix( | |
| train_df, user_features, item_features, user_profiles, vectorizer, | |
| feature_set=feature_set, | |
| ) | |
| val_feat, _ = build_feature_matrix( | |
| val_df, user_features, item_features, user_profiles, vectorizer, | |
| feature_set=feature_set, | |
| ) | |
| # Train | |
| model = LightGBMReranker( | |
| n_estimators=200 if cfg.data.debug else 500, | |
| learning_rate=0.05, | |
| num_leaves=31, | |
| random_state=cfg.data.random_seed, | |
| early_stopping_rounds=30, | |
| ) | |
| model.fit(train_feat, val_feat, feat_cols) | |
| # Evaluate using candidate pool from naive model | |
| gt_map = test_df.groupby("user_idx")["item_idx"].apply(list).to_dict() | |
| all_recs: list[list[int]] = [] | |
| all_gts: list[list[int]] = [] | |
| for uid, gt in gt_map.items(): | |
| candidates = naive_model.recommend(uid, top_k=candidate_pool_size) | |
| if not candidates: | |
| all_recs.append([]) | |
| all_gts.append(gt) | |
| continue | |
| candidate_items = [iid for iid, _ in candidates] | |
| rows = pd.DataFrame({"user_idx": uid, "item_idx": candidate_items, "label": 0}) | |
| feat, _ = build_feature_matrix( | |
| rows, user_features, item_features, user_profiles, vectorizer, | |
| feature_set=feature_set, | |
| ) | |
| scores = model.predict_proba(feat) | |
| ranked = sorted(zip(candidate_items, scores.tolist()), key=lambda x: x[1], reverse=True) | |
| all_recs.append([i for i, _ in ranked[:k]]) | |
| all_gts.append(gt) | |
| metrics = compute_all_metrics(all_recs, all_gts, k_values=[k]) | |
| logger.info(" NDCG@%d=%.4f Recall@%d=%.4f", k, metrics[f"NDCG@{k}"], k, metrics[f"Recall@{k}"]) | |
| return metrics | |
| def plot_ablation(results_df: pd.DataFrame, metric: str, output_path: Path) -> None: | |
| fig, ax = plt.subplots(figsize=(9, 5)) | |
| bars = ax.bar(results_df["feature_set"], results_df[metric], color="#3b82f6", edgecolor="white") | |
| for bar, val in zip(bars, results_df[metric]): | |
| ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.002, | |
| f"{val:.4f}", ha="center", va="bottom", fontsize=10) | |
| ax.set_xlabel("Feature Set") | |
| ax.set_ylabel(metric) | |
| ax.set_title(f"Feature Ablation Experiment β {metric}") | |
| ax.set_ylim(0, results_df[metric].max() * 1.25) | |
| plt.xticks(rotation=15, ha="right") | |
| plt.tight_layout() | |
| output_path.parent.mkdir(parents=True, exist_ok=True) | |
| fig.savefig(output_path, dpi=150) | |
| plt.close(fig) | |
| logger.info("Ablation plot saved β %s", output_path) | |
| def main() -> None: | |
| args = parse_args() | |
| cfg = get_config(debug=args.debug) | |
| ensure_dirs(cfg) | |
| exp_dir = cfg.experiment.output_dir | |
| exp_dir.mkdir(parents=True, exist_ok=True) | |
| logger.info("Loading dataβ¦") | |
| train_df = pd.read_parquet(cfg.data.train_path) | |
| val_df = pd.read_parquet(cfg.data.val_path) | |
| test_df = pd.read_parquet(cfg.data.test_path) | |
| item_features = pd.read_parquet(cfg.data.item_features_path) | |
| user_features = pd.read_parquet(cfg.data.user_features_path) | |
| for split in (val_df, test_df): | |
| if "label" not in split.columns: | |
| split["label"] = (split["rating"] >= cfg.data.positive_rating_threshold).astype(int) | |
| # Load feature artifacts | |
| with open(PROCESSED_DIR / "feature_artifacts.pkl", "rb") as f: | |
| artifacts = pickle.load(f) | |
| vectorizer = artifacts["vectorizer"] | |
| user_profiles = build_user_text_profiles(train_df, item_features, cfg.data.positive_rating_threshold) | |
| # Load naive model for candidate generation | |
| naive_model = GlobalPopularityRecommender.load(MODELS_DIR / "naive_baseline.pkl") | |
| logger.info("=== Feature Ablation Experiment ===") | |
| rows = [] | |
| for fset in FEATURE_SETS: | |
| metrics = train_and_evaluate_tier( | |
| feature_set=fset, | |
| train_df=train_df, | |
| val_df=val_df, | |
| test_df=test_df, | |
| user_features=user_features, | |
| item_features=item_features, | |
| user_profiles=user_profiles, | |
| vectorizer=vectorizer, | |
| naive_model=naive_model, | |
| cfg=cfg, | |
| candidate_pool_size=cfg.classical.candidate_pool_size, | |
| k=args.k, | |
| ) | |
| rows.append({"feature_set": fset, **metrics}) | |
| results_df = pd.DataFrame(rows) | |
| results_path = exp_dir / "ablation_results.csv" | |
| results_df.to_csv(results_path, index=False) | |
| logger.info("Ablation results saved β %s", results_path) | |
| # Plots | |
| ndcg_col = f"NDCG@{args.k}" | |
| recall_col = f"Recall@{args.k}" | |
| if ndcg_col in results_df.columns: | |
| plot_ablation(results_df, ndcg_col, exp_dir / "ablation_ndcg.png") | |
| if recall_col in results_df.columns: | |
| plot_ablation(results_df, recall_col, exp_dir / "ablation_recall.png") | |
| print("\n=== Ablation Results ===") | |
| print(results_df.to_string(index=False)) | |
| # Interpretation summary | |
| summary_path = exp_dir / "ablation_interpretation.md" | |
| best_row = results_df.loc[results_df[ndcg_col].idxmax()] if ndcg_col in results_df else None | |
| summary_lines = [ | |
| "# Feature Ablation Experiment β Interpretation\n", | |
| f"Metric: {ndcg_col} on test set.\n", | |
| "## Results\n", | |
| results_df.to_markdown(index=False), | |
| "", | |
| "## Key Findings\n", | |
| ] | |
| if best_row is not None: | |
| summary_lines.append( | |
| f"- Best feature set: **{best_row['feature_set']}** " | |
| f"({ndcg_col} = {best_row[ndcg_col]:.4f})" | |
| ) | |
| summary_path.write_text("\n".join(summary_lines)) | |
| logger.info("Interpretation saved β %s", summary_path) | |
| logger.info("Experiment complete.") | |
| if __name__ == "__main__": | |
| main() | |