Spaces:
Sleeping
Sleeping
| """ | |
| save_models.py β TemporalDrift-ETM | Run this cell inside your Jupyter notebook | |
| after training to export all artifacts needed for Hugging Face deployment. | |
| VARIABLE NAME MAP (adjust the left side to match your notebook): | |
| ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| ensemble_model β your VotingClassifier / trained ensemble | |
| scaler β StandardScaler fitted on training data | |
| le β LabelEncoder fitted on y_train | |
| feature_cols β list of feature column names used in training | |
| X_train_scaled β scaled training feature matrix (numpy array) | |
| y_train_encoded β encoded training labels (numpy array, integers) | |
| y_train_labels β decoded training labels (numpy array, strings) | |
| """ | |
| import os | |
| import numpy as np | |
| import joblib | |
| # ββ OUTPUT DIRECTORY ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SAVE_DIR = "models" # relative to where you run this script | |
| os.makedirs(SAVE_DIR, exist_ok=True) | |
| # ββ β RENAME YOUR VARIABLES HERE βββββββββββββββββββββββββββββββββββββββββββββ | |
| # (Only change the right-hand side to match your notebook's variable names) | |
| # _ensemble = ensemble_model # your trained VotingClassifier / ensemble | |
| # _scaler = scaler # your fitted StandardScaler | |
| # _le = le # your fitted LabelEncoder | |
| # _feature_names = feature_cols # list[str] β columns used during training | |
| # _X_train = X_train_scaled # np.ndarray, shape (n_samples, n_features) β SCALED | |
| # _y_encoded = y_train_encoded # np.ndarray, shape (n_samples,) β integer labels | |
| # _y_labels = y_train_labels # np.ndarray, shape (n_samples,) β string labels | |
| # ββββ Replace with ββββ | |
| # | |
| # β οΈ CRITICAL β use the BASE sklearn VotingClassifier, NOT a custom wrapper. | |
| # | |
| # DO NOT use: _ensemble = ensemble_retrained | |
| # WHY: ensemble_retrained is an EnsembleWrapperRetrained object defined | |
| # only in this notebook's __main__ scope. joblib pickles a *reference* | |
| # to that class, so loading it anywhere outside this notebook raises: | |
| # "Can't get attribute 'EnsembleWrapperRetrained' | |
| # on <module '__main__' from '/app/app.py'>" | |
| # AND the object serialises with no model state (55 bytes) β all | |
| # trained weights are silently lost. | |
| # | |
| # USE: The raw sklearn VotingClassifier (ensemble / voting_clf / etc.). | |
| # It contains .estimators_ (RF + XGBoost + LightGBM + CatBoost) | |
| # and pickles cleanly because all sklearn/xgboost classes are | |
| # importable from their own packages on any machine. | |
| # | |
| _ensemble = ensemble # β the base VotingClassifier from training | |
| _scaler = scaler # already correct | |
| _le = le # already correct | |
| _feature_names = FEATURE_COLS # capital, matches your notebook | |
| _X_train = X_sm_scaled[:, top_idx] # SMOTE-resampled + feature-selected | |
| _y_encoded = y_train # integer labels from Cell 16/17 | |
| _y_labels = le.inverse_transform(y_train) | |
| # ββ β‘ VALIDATE before saving ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Catches the wrapper-class mistake immediately instead of at deploy time. | |
| from sklearn.base import BaseEstimator | |
| if not isinstance(_ensemble, BaseEstimator): | |
| raise TypeError( | |
| f"\n\n_ensemble is {type(_ensemble).__name__!r}, not a sklearn BaseEstimator.\n" | |
| "You are probably passing a custom wrapper class. Assign the raw\n" | |
| "VotingClassifier (or equivalent) to _ensemble instead.\n" | |
| "Check: hasattr(_ensemble, 'estimators_') should be True.\n" | |
| ) | |
| print(f"β _ensemble type : {type(_ensemble).__name__}") | |
| print(f" estimators : {[type(e).__name__ for e in _ensemble.estimators_]}") | |
| print(f" classes_ : {list(_ensemble.classes_)}") | |
| # ββ β’ SAVE CORE ARTIFACTS βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| print("\nSaving ensemble model β¦") | |
| joblib.dump(_ensemble, os.path.join(SAVE_DIR, "ensemble_model.pkl"), compress=3) | |
| print("Saving scaler β¦") | |
| joblib.dump(_scaler, os.path.join(SAVE_DIR, "scaler.pkl"), compress=3) | |
| print("Saving label encoder β¦") | |
| joblib.dump(_le, os.path.join(SAVE_DIR, "label_encoder.pkl"), compress=3) | |
| print("Saving feature names β¦") | |
| joblib.dump(list(_feature_names), os.path.join(SAVE_DIR, "feature_names.pkl")) | |
| # ββ β£ BUILD BASELINE PROFILES βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Each family profile stores: | |
| # β’ feature_samples β per-feature value subsample (for JS-divergence drift detection) | |
| # β’ sample_X β feature matrix subsample (for combined retraining) | |
| # β’ sample_y β integer label subsample (for combined retraining) | |
| # β’ n_total β total training samples for this family | |
| SUBSAMPLE = 500 # samples retained per family (balance size vs accuracy) | |
| SEED = 42 | |
| rng = np.random.RandomState(SEED) | |
| baseline_profiles = {} | |
| families = np.unique(_y_labels) | |
| print(f"\nBuilding baseline profiles for {len(families)} families β¦") | |
| for family in families: | |
| mask = (_y_labels == family) | |
| fX = _X_train[mask] | |
| fy = _y_encoded[mask] | |
| n = min(SUBSAMPLE, len(fX)) | |
| idx = rng.choice(len(fX), n, replace=False) | |
| baseline_profiles[family] = { | |
| # Dict of feature_name β list of sample values (for JS divergence) | |
| "feature_samples": { | |
| feat: fX[idx, i].tolist() | |
| for i, feat in enumerate(_feature_names) | |
| }, | |
| "sample_X": fX[idx], # np.ndarray (n, d) β for retraining | |
| "sample_y": fy[idx], # np.ndarray (n,) β for retraining | |
| "n_total": int(mask.sum()), | |
| } | |
| print(f" [{family}] total={mask.sum():>6,} saved={n}") | |
| print("\nSaving baseline profiles β¦") | |
| joblib.dump(baseline_profiles, os.path.join(SAVE_DIR, "baseline_profiles.pkl"), compress=3) | |
| # ββ β€ SUMMARY βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| print("\n" + "β" * 55) | |
| print(" All artifacts saved to:", os.path.abspath(SAVE_DIR)) | |
| print("β" * 55) | |
| for fname in sorted(os.listdir(SAVE_DIR)): | |
| if fname.endswith(".pkl"): | |
| size_mb = os.path.getsize(os.path.join(SAVE_DIR, fname)) / 1e6 | |
| print(f" {fname:<30} {size_mb:>7.2f} MB") | |
| print("β" * 55) | |
| print("\nNext step: copy the 'models/' folder into your HF Space repo.") | |