agrisense-backend / MandiPricePredictionSystem /scripts /tft_multivariate_training.py
akashg7's picture
feat: Upgrade TFT architecture and multivariate feature engineering
81f5bb2
Raw
History Blame Contribute Delete
6.66 kB
import pandas as pd
import matplotlib.pyplot as plt
import lightning.pytorch as pl
from lightning.pytorch.callbacks import EarlyStopping, ModelCheckpoint
from lightning.pytorch.callbacks import LearningRateMonitor
from pytorch_forecasting import TimeSeriesDataSet, TemporalFusionTransformer
from pytorch_forecasting.data import GroupNormalizer
from pytorch_forecasting.metrics import QuantileLoss, MAE, SMAPE, RMSE
import torch
def train_multivariate():
print("=== Advanced Deep Learning Pipeline (TFT Edition) ===")
# 1. Load the advanced feature dataset
print("Loading prepared dataset...")
# Adjusting path to match typical execution from project root
try:
df = pd.read_csv("data/processed/dl_advanced_features_data.csv")
except:
df = pd.read_csv("../data/processed/dl_advanced_features_data.csv") # Fallback
# Add time index required by PyTorch Forecasting
df["date"] = pd.to_datetime(df["date"])
df = df.sort_values(["Mandi", "Commodity", "date"])
# Create purely sequential integer time index per group
df["time_idx"] = df.groupby(["Mandi", "Commodity"]).cumcount()
# Ensure all targets are positive for relative scaling
df["target_price"] = df["target_price"].clip(lower=1.0)
# Create string format for groupings
df["Mandi"] = df["Mandi"].astype(str)
df["Commodity"] = df["Commodity"].astype(str)
max_prediction_length = 14 # Predict next 14 days
max_encoder_length = 60 # Elevated context lookback (60 days)
# Time-based exact split (NO random)
training_cutoff = df["time_idx"].max() - max_prediction_length
print("Configuring TimeSeriesDataSet...")
# Unknown reals that depend strictly on historic values
unknown_reals = [
"Arrivals", "temp_avg", "temp_max", "temp_min", "rainfall", "humidity", "solar_radiation", "wind_speed",
"modal_lag_1", "modal_lag_3", "modal_lag_7", "modal_lag_14", "modal_lag_30", "modal_lag_60",
"arrivals_lag_1", "arrivals_lag_7", "arrivals_lag_30", "arrivals_lag_60",
"rolling_mean_7", "rolling_mean_30", "rolling_mean_60",
"rolling_std_7", "rolling_std_30", "rolling_std_60",
"volatility_7", "volatility_30", "momentum_7", "momentum_14",
"arrival_change_7", "price_change_1",
"temp_anomaly", "rain_anomaly",
"price_x_arrivals", "roll_mean_x_volatility", "rainfall_x_arrivals", "temp_x_price",
"high_price_regime", "high_arrival_regime", "volatility_regime",
"sudden_spike_flag", "arrival_shock_flag",
"price_residual_30", "normalized_dev_30"
]
# Filter to only columns that actually exist to avoid crash if some are all-NaN/dropped
unknown_reals = [f for f in unknown_reals if f in df.columns]
# Define exact TimeSeries boundaries
training = TimeSeriesDataSet(
df[lambda x: x.time_idx <= training_cutoff],
time_idx="time_idx",
target="target_price",
group_ids=["Mandi", "Commodity"],
min_encoder_length=max_encoder_length // 2,
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_idx", "day_of_year", "day_of_week", "month", "sin1", "cos1", "sin2", "cos2"],
time_varying_unknown_reals=["target_price"] + unknown_reals,
target_normalizer=GroupNormalizer(
groups=["Mandi", "Commodity"], transformation="softplus"
),
add_relative_time_idx=True,
add_target_scales=True,
add_encoder_length=True,
)
# 2. Creating dataloaders for the GPU/MPS
validation = TimeSeriesDataSet.from_dataset(training, df, predict=True, stop_randomization=True)
batch_size = 128
train_dataloader = training.to_dataloader(train=True, batch_size=batch_size, num_workers=4)
val_dataloader = validation.to_dataloader(train=False, batch_size=batch_size * 2, num_workers=4)
# 3. Architect TFT (Temporal Fusion Transformer)
print("Instantiating Multi-variate TFT Model for exact external shock modeling...")
pl.seed_everything(42)
net = TemporalFusionTransformer.from_dataset(
training,
learning_rate=1e-3, # Tuned learning rate
hidden_size=64, # Strengthened hidden representation
attention_head_size=4, # Expanded attention mapping
dropout=0.2, # Prevent Overfitting
hidden_continuous_size=16,
output_size=7, # Quantiles for confidence bands!
loss=QuantileLoss(),
log_interval=10,
reduce_on_plateau_patience=4,
)
# 4. Spin up the Lightning Trainer
early_stop_callback = EarlyStopping(
monitor="val_loss",
min_delta=1e-4,
patience=10,
verbose=True,
mode="min"
)
lr_logger = LearningRateMonitor() # Logs the learning rate
checkpoint_callback = ModelCheckpoint(
monitor="val_loss",
mode="min",
save_top_k=1,
filename="best-advanced-tft-{epoch:02d}-{val_loss:.2f}"
)
trainer = pl.Trainer(
max_epochs=50,
accelerator="auto", # auto detects MPS/GPU/CPU
devices=1,
gradient_clip_val=0.1, # Gradient clipping
callbacks=[early_stop_callback, checkpoint_callback, lr_logger],
)
# 5. Execute Training!
print(f"Executing High-Performance Multi-variate Training...")
trainer.fit(
net,
train_dataloaders=train_dataloader,
val_dataloaders=val_dataloader,
)
print("Optimization Complete! Best multi-variate model saved.")
# --- INTERPRETABILITY TOOLS ---
print("Generating Global Feature Importance and Attention...")
best_model_path = trainer.checkpoint_callback.best_model_path
if best_model_path:
best_tft = TemporalFusionTransformer.load_from_checkpoint(best_model_path)
else:
best_tft = net
# Run evaluation across sample
raw_predictions = best_tft.predict(val_dataloader, mode="raw", return_x=True)
# 1. Feature Importance plotting
interpretation = best_tft.interpret_output(raw_predictions.output, reduction="sum")
figs = best_tft.plot_interpretation(interpretation)
for key, fig in figs.items():
fig.savefig(f"tft_feature_importance_{key}.png")
print("✅ Feature importance successfully mapped out to PNGs. Model represents State-of-the-Art config setup.")
if __name__ == "__main__":
train_multivariate()