Spaces:
Sleeping
Sleeping
| """ | |
| evaluate_tft.py — Load trained TFT checkpoint and compute real metrics. | |
| NO retraining. Just inference on the held-out test set. | |
| """ | |
| import pandas as pd | |
| import numpy as np | |
| import torch | |
| import lightning.pytorch as pl | |
| from pytorch_forecasting import TimeSeriesDataSet, TemporalFusionTransformer | |
| from pytorch_forecasting.data import GroupNormalizer | |
| from pytorch_forecasting.metrics import QuantileLoss | |
| from pathlib import Path | |
| import warnings | |
| warnings.filterwarnings("ignore") | |
| # ── Paths ── | |
| DATA_PATH = Path("data/processed/dl_30_features_data.csv") | |
| CKPT_PATH = Path("epoch=9-step=43310.ckpt") | |
| OUT_DIR = Path("outputs/eval") | |
| def mae(y_true, y_pred): | |
| return float(np.mean(np.abs(y_true - y_pred))) | |
| def rmse(y_true, y_pred): | |
| return float(np.sqrt(np.mean((y_true - y_pred) ** 2))) | |
| def mape(y_true, y_pred): | |
| eps = 1e-6 | |
| return float(np.mean(np.abs((y_true - y_pred) / (np.abs(y_true) + eps))) * 100.0) | |
| def smape(y_true, y_pred): | |
| y_true = np.asarray(y_true, dtype=float) | |
| y_pred = np.asarray(y_pred, dtype=float) | |
| denom = (np.abs(y_true) + np.abs(y_pred)) / 2.0 | |
| denom = np.where(denom == 0, 1e-6, denom) | |
| return float(np.mean(np.abs(y_pred - y_true) / denom) * 100.0) | |
| def main(): | |
| print("=" * 60) | |
| print(" TFT Evaluation — Inference Only (No Retraining)") | |
| print("=" * 60) | |
| # 1. Load data (same as training script) | |
| print(f"\n1. Loading data from {DATA_PATH} ...") | |
| df = pd.read_csv(DATA_PATH) | |
| df["date"] = pd.to_datetime(df["date"]) | |
| df = df.sort_values(["Mandi", "Commodity", "date"]) | |
| # Create time index per group (same as training) | |
| df["time_idx"] = df.groupby(["Mandi", "Commodity"]).cumcount() | |
| df["target_price"] = df["target_price"].clip(lower=1.0) | |
| df["group_id"] = df["Mandi"].astype(str) + "_" + df["Commodity"].astype(str) | |
| # Drop rows with missing required columns | |
| required_cols = ["target_price", "temp_avg", "humidity", "rainfall", | |
| "rolling_mean_7", "volatility_7", "momentum_7", | |
| "day_of_year", "sin1", "cos1"] | |
| existing_required = [c for c in required_cols if c in df.columns] | |
| df = df.dropna(subset=existing_required) | |
| # Fill any remaining NaNs in feature columns | |
| for col in ["temp_avg", "humidity", "rainfall", "rolling_mean_7", | |
| "volatility_7", "momentum_7"]: | |
| if col in df.columns: | |
| df[col] = df[col].fillna(df[col].median()) | |
| print(f" Data shape: {df.shape}") | |
| print(f" Date range: {df['date'].min().date()} — {df['date'].max().date()}") | |
| print(f" Unique groups: {df['group_id'].nunique()}") | |
| max_prediction_length = 14 | |
| max_encoder_length = 30 | |
| training_cutoff = df["time_idx"].max() - max_prediction_length | |
| print(f" Training cutoff time_idx: {training_cutoff}") | |
| print(f" Max time_idx: {df['time_idx'].max()}") | |
| # 2. Rebuild EXACT same TimeSeriesDataSet as training | |
| print("\n2. Rebuilding TimeSeriesDataSet (exact same config as training)...") | |
| # Check which columns actually exist | |
| time_varying_known = ["time_idx"] | |
| for col in ["day_of_year", "sin1", "cos1"]: | |
| if col in df.columns: | |
| time_varying_known.append(col) | |
| time_varying_unknown = ["target_price"] | |
| for col in ["temp_avg", "humidity", "rainfall", "rolling_mean_7", | |
| "volatility_7", "momentum_7"]: | |
| if col in df.columns: | |
| time_varying_unknown.append(col) | |
| print(f" Known reals: {time_varying_known}") | |
| print(f" Unknown reals: {time_varying_unknown}") | |
| training = TimeSeriesDataSet( | |
| df[lambda x: x.time_idx <= training_cutoff], | |
| time_idx="time_idx", | |
| target="target_price", | |
| group_ids=["group_id"], | |
| min_encoder_length=max_encoder_length, | |
| max_encoder_length=max_encoder_length, | |
| min_prediction_length=max_prediction_length, | |
| max_prediction_length=max_prediction_length, | |
| static_categoricals=["Mandi", "Commodity"], | |
| time_varying_known_reals=time_varying_known, | |
| time_varying_unknown_reals=time_varying_unknown, | |
| target_normalizer=GroupNormalizer( | |
| groups=["group_id"], transformation="softplus" | |
| ), | |
| add_relative_time_idx=True, | |
| add_target_scales=True, | |
| add_encoder_length=True, | |
| ) | |
| validation = TimeSeriesDataSet.from_dataset( | |
| training, df, predict=True, stop_randomization=True | |
| ) | |
| val_dataloader = validation.to_dataloader( | |
| train=False, batch_size=128, num_workers=0 | |
| ) | |
| print(f" Validation samples: {len(validation)}") | |
| # 3. Load TFT from checkpoint (NO training) | |
| print(f"\n3. Loading TFT from checkpoint: {CKPT_PATH}") | |
| # The checkpoint was trained on CUDA GPU. torchmetrics.Metric._apply() | |
| # tries to create a dummy tensor on self.device (cuda) before moving to | |
| # the target device, which crashes on Mac without CUDA. | |
| # Fix: monkey-patch torchmetrics to skip the problematic _apply. | |
| import torchmetrics | |
| _original_apply = torchmetrics.Metric._apply | |
| def _safe_apply(self, fn, *args, **kwargs): | |
| """Patched _apply that forces device to CPU before applying fn.""" | |
| self._device = torch.device("cpu") | |
| return torch.nn.Module._apply(self, fn) | |
| torchmetrics.Metric._apply = _safe_apply | |
| try: | |
| raw_ckpt = torch.load(str(CKPT_PATH), map_location="cpu", weights_only=False) | |
| # Log checkpoint metadata | |
| if "epoch" in raw_ckpt: | |
| print(f" Checkpoint epoch: {raw_ckpt['epoch']}") | |
| if "global_step" in raw_ckpt: | |
| print(f" Global step: {raw_ckpt['global_step']}") | |
| if "hyper_parameters" in raw_ckpt: | |
| hp = raw_ckpt["hyper_parameters"] | |
| print(f" hidden_size: {hp.get('hidden_size')}") | |
| print(f" attention_head_size: {hp.get('attention_head_size')}") | |
| print(f" dropout: {hp.get('dropout')}") | |
| print(f" output_size: {hp.get('output_size')}") | |
| print(f" learning_rate: {hp.get('learning_rate')}") | |
| # Remove callbacks that may hold CUDA references | |
| if "callbacks" in raw_ckpt: | |
| raw_ckpt["callbacks"] = {} | |
| # Save patched checkpoint | |
| import os | |
| tmp_ckpt = str(CKPT_PATH) + ".cpu_tmp.ckpt" | |
| torch.save(raw_ckpt, tmp_ckpt) | |
| best_model = TemporalFusionTransformer.load_from_checkpoint( | |
| tmp_ckpt, map_location="cpu" | |
| ) | |
| finally: | |
| torchmetrics.Metric._apply = _original_apply | |
| if os.path.exists(tmp_ckpt): | |
| os.remove(tmp_ckpt) | |
| best_model.eval() | |
| print(" ✓ Model loaded successfully (inference mode)") | |
| # 4. Run predictions | |
| print("\n4. Running inference on validation set...") | |
| predictions = best_model.predict( | |
| val_dataloader, | |
| mode="prediction", # returns point predictions (median quantile) | |
| return_x=True | |
| ) | |
| # Get raw predictions (point forecasts from median quantile) | |
| raw_preds = best_model.predict(val_dataloader, mode="raw") | |
| # 5. Compute actuals vs predictions | |
| print("\n5. Computing metrics...") | |
| actuals_list = [] | |
| preds_list = [] | |
| for batch_idx, (x, y) in enumerate(val_dataloader): | |
| actuals_list.append(y[0]) # y is (target, weight) tuple | |
| actuals = torch.cat(actuals_list, dim=0).numpy() # shape: (N, 14) | |
| if isinstance(predictions, tuple): | |
| preds = predictions[0].numpy() | |
| else: | |
| preds = predictions.numpy() | |
| # Flatten for global metrics | |
| y_true = actuals.flatten() | |
| y_pred = preds.flatten() | |
| # Remove any zero/negative actuals for cleaner metrics | |
| mask = y_true > 0 | |
| y_true = y_true[mask] | |
| y_pred = y_pred[mask] | |
| n_samples = len(actuals) | |
| n_points = len(y_true) | |
| # Naive baseline: last encoder value repeated | |
| naive_list = [] | |
| for batch_idx, (x, y) in enumerate(val_dataloader): | |
| encoder_target = x["encoder_target"] # (batch, encoder_len) | |
| last_val = encoder_target[:, -1].unsqueeze(1).expand(-1, max_prediction_length) | |
| naive_list.append(last_val) | |
| naive_all = torch.cat(naive_list, dim=0).numpy().flatten() | |
| naive_all = naive_all[mask] | |
| # Compute all metrics | |
| tft_mae = mae(y_true, y_pred) | |
| tft_rmse = rmse(y_true, y_pred) | |
| tft_mape = mape(y_true, y_pred) | |
| tft_smape = smape(y_true, y_pred) | |
| tft_acc = 100.0 - tft_smape | |
| naive_mae_val = mae(y_true, naive_all) | |
| naive_rmse_val = rmse(y_true, naive_all) | |
| naive_mape_val = mape(y_true, naive_all) | |
| naive_smape_val = smape(y_true, naive_all) | |
| naive_acc = 100.0 - naive_smape_val | |
| print("\n" + "=" * 60) | |
| print(" ACTUAL TFT EVALUATION RESULTS") | |
| print("=" * 60) | |
| print(f"\n Validation samples: {n_samples}") | |
| print(f" Total prediction points: {n_points}") | |
| print(f" Prediction horizon: {max_prediction_length} days") | |
| print(f"\n --- Naive Baseline (last-value repeat) ---") | |
| print(f" MAE : {naive_mae_val:.2f} ₹/quintal") | |
| print(f" RMSE : {naive_rmse_val:.2f}") | |
| print(f" MAPE : {naive_mape_val:.2f}%") | |
| print(f" SMAPE : {naive_smape_val:.2f}% (Accuracy ≈ {naive_acc:.2f}%)") | |
| print(f"\n --- TFT (from checkpoint) ---") | |
| print(f" MAE : {tft_mae:.2f} ₹/quintal") | |
| print(f" RMSE : {tft_rmse:.2f}") | |
| print(f" MAPE : {tft_mape:.2f}%") | |
| print(f" SMAPE : {tft_smape:.2f}% (Accuracy ≈ {tft_acc:.2f}%)") | |
| print(f"\n Improvement over naive:") | |
| print(f" MAE reduction : {naive_mae_val - tft_mae:.2f} ₹/quintal ({(1 - tft_mae/naive_mae_val)*100:.1f}%)") | |
| print(f" SMAPE reduction: {naive_smape_val - tft_smape:.2f} pp") | |
| print("=" * 60) | |
| # 6. Save results | |
| OUT_DIR.mkdir(parents=True, exist_ok=True) | |
| summary = pd.DataFrame({ | |
| "model": ["naive_tft_eval", "tft_checkpoint"], | |
| "mae": [naive_mae_val, tft_mae], | |
| "rmse": [naive_rmse_val, tft_rmse], | |
| "mape": [naive_mape_val, tft_mape], | |
| "smape": [naive_smape_val, tft_smape], | |
| "accuracy_pct": [naive_acc, tft_acc], | |
| "n_val_samples": [n_samples, n_samples], | |
| "n_prediction_points": [n_points, n_points], | |
| }) | |
| out_path = OUT_DIR / "eval_tft_checkpoint_actual.csv" | |
| summary.to_csv(out_path, index=False) | |
| print(f"\n Saved REAL metrics to: {out_path}") | |
| if __name__ == "__main__": | |
| main() | |