Download src/models/classical.py from yashb18/deepreads: direct link, hf CLI and curl.
- Browser
- Download file 6.2 kB
-
https://huggingface.co/spaces/yashb18/deepreads/resolve/main/src/models/classical.py
- Command line
-
hf download hf://spaces/yashb18/deepreads/src/models/classical.py
-
curl -L -o classical.py https://huggingface.co/spaces/yashb18/deepreads/resolve/main/src/models/classical.py
6.2 kB
| """ | |
| Classical ML model: LightGBM-based recommendation reranker. | |
| Formulates recommendation as binary classification: | |
| label = 1 if the user interacted positively with the item, else 0. | |
| At inference, for each user we score a candidate pool of items (retrieved | |
| by the naive popularity model) and return the top-K by predicted probability. | |
| Explainability is provided via: | |
| - Global feature importance (gain-based) | |
| - Per-prediction SHAP values (computed by the explainability module) | |
| """ | |
| from __future__ import annotations | |
| import pickle | |
| from pathlib import Path | |
| import lightgbm as lgb | |
| import numpy as np | |
| import pandas as pd | |
| from src.utils.logging import get_logger | |
| logger = get_logger(__name__) | |
| class LightGBMReranker: | |
| """ | |
| LightGBM binary classifier used as a recommendation reranker. | |
| Args: | |
| n_estimators: Number of boosting rounds. | |
| learning_rate: Gradient boosting learning rate. | |
| num_leaves: Maximum number of leaves per tree. | |
| max_depth: Maximum tree depth (-1 = unlimited). | |
| min_child_samples: Minimum samples per leaf. | |
| subsample: Row sub-sampling ratio. | |
| colsample_bytree: Column sub-sampling ratio. | |
| reg_alpha: L1 regularisation. | |
| reg_lambda: L2 regularisation. | |
| n_jobs: Parallel threads. | |
| random_state: Random seed. | |
| early_stopping_rounds: Early stopping patience. | |
| """ | |
| def __init__( | |
| self, | |
| n_estimators: int = 500, | |
| learning_rate: float = 0.05, | |
| num_leaves: int = 63, | |
| max_depth: int = -1, | |
| min_child_samples: int = 20, | |
| subsample: float = 0.8, | |
| colsample_bytree: float = 0.8, | |
| reg_alpha: float = 0.1, | |
| reg_lambda: float = 0.1, | |
| n_jobs: int = -1, | |
| random_state: int = 42, | |
| early_stopping_rounds: int = 50, | |
| ) -> None: | |
| self.params = dict( | |
| n_estimators=n_estimators, | |
| learning_rate=learning_rate, | |
| num_leaves=num_leaves, | |
| max_depth=max_depth, | |
| min_child_samples=min_child_samples, | |
| subsample=subsample, | |
| colsample_bytree=colsample_bytree, | |
| reg_alpha=reg_alpha, | |
| reg_lambda=reg_lambda, | |
| n_jobs=n_jobs, | |
| random_state=random_state, | |
| objective="binary", | |
| metric="binary_logloss", | |
| verbose=-1, | |
| ) | |
| self.early_stopping_rounds = early_stopping_rounds | |
| self.model: lgb.LGBMClassifier | None = None | |
| self.feature_names: list[str] = [] | |
| def fit( | |
| self, | |
| train_df: pd.DataFrame, | |
| val_df: pd.DataFrame, | |
| feature_cols: list[str], | |
| ) -> "LightGBMReranker": | |
| """ | |
| Train the LightGBM classifier. | |
| Args: | |
| train_df: Training feature matrix with a 'label' column. | |
| val_df: Validation feature matrix for early stopping. | |
| feature_cols: Column names to use as features. | |
| Returns: | |
| self | |
| """ | |
| self.feature_names = feature_cols | |
| X_train = train_df[feature_cols].values.astype(np.float32) | |
| y_train = train_df["label"].values | |
| X_val = val_df[feature_cols].values.astype(np.float32) | |
| y_val = val_df["label"].values | |
| self.model = lgb.LGBMClassifier(**self.params) | |
| callbacks = [lgb.early_stopping(self.early_stopping_rounds, verbose=False), | |
| lgb.log_evaluation(100)] | |
| self.model.fit( | |
| X_train, y_train, | |
| eval_set=[(X_val, y_val)], | |
| callbacks=callbacks, | |
| feature_name=feature_cols, | |
| ) | |
| logger.info( | |
| "LightGBM trained for %d rounds (best: %d)", | |
| self.params["n_estimators"], | |
| self.model.best_iteration_, | |
| ) | |
| return self | |
| def predict_proba(self, df: pd.DataFrame) -> np.ndarray: | |
| """ | |
| Return positive-class probabilities for rows in df. | |
| Args: | |
| df: Feature DataFrame with columns matching feature_names. | |
| Returns: | |
| 1-D numpy array of probabilities. | |
| """ | |
| if self.model is None: | |
| raise RuntimeError("Model not fitted.") | |
| X = df[self.feature_names].values.astype(np.float32) | |
| return self.model.predict_proba(X)[:, 1] | |
| def recommend( | |
| self, | |
| user_idx: int, | |
| candidate_item_idxs: list[int], | |
| feature_df: pd.DataFrame, | |
| top_k: int = 10, | |
| ) -> list[tuple[int, float]]: | |
| """ | |
| Score a set of candidate items for a user and return the top-K. | |
| Args: | |
| user_idx: Encoded user index. | |
| candidate_item_idxs: Pool of item indices to score. | |
| feature_df: Pre-built feature rows for (user_idx, item_idx) pairs. | |
| Must contain all feature_names columns. | |
| top_k: Number of recommendations. | |
| Returns: | |
| List of (item_idx, score) sorted descending. | |
| """ | |
| scores = self.predict_proba(feature_df) | |
| ranked = sorted( | |
| zip(candidate_item_idxs, scores.tolist()), | |
| key=lambda x: x[1], | |
| reverse=True, | |
| ) | |
| return ranked[:top_k] | |
| def feature_importance(self, importance_type: str = "gain") -> pd.DataFrame: | |
| """ | |
| Return a DataFrame of feature importances sorted descending. | |
| Args: | |
| importance_type: 'gain' or 'split'. | |
| Returns: | |
| DataFrame with columns [feature, importance]. | |
| """ | |
| if self.model is None: | |
| raise RuntimeError("Model not fitted.") | |
| importances = self.model.feature_importances_ | |
| return ( | |
| pd.DataFrame({"feature": self.feature_names, "importance": importances}) | |
| .sort_values("importance", ascending=False) | |
| .reset_index(drop=True) | |
| ) | |
| def save(self, path: Path) -> None: | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| with open(path, "wb") as f: | |
| pickle.dump(self, f) | |
| logger.info("Saved LightGBMReranker → %s", path) | |
| def load(cls, path: Path) -> "LightGBMReranker": | |
| with open(path, "rb") as f: | |
| return pickle.load(f) | |