Downloads · 30 days
0
ahmed-farhanur-rashid/psa-net
psa-net is a time series forecasting model from ahmed-farhanur-rashid. Use it for the time series forecasting task on the model card, and read the license before you ship it in a product. The card lists the license as cc-by-4.0.
- Model Name: PSA-Net (Patch-Spectral Attention Network) - Model Type: Configurable, Multi-Branch Deep Transformer for Multivariate Time-Series Forecasting - Framework: PyTorch - License: MIT - Primary Task: Multi-ste…
Downloads · 30 days
0
Access
Public
Updated Jul 25, 2026
Repo size
103 MB
Likes
0
Public
Click a slice to open those files.
.pt10.9 MB · 100%
From the Hugging Face model README
Status: pre-release / under active evaluation. Initial evaluation on a distribution-shifted test set surfaced a calibration issue (see Evaluation Results below) that is currently being diagnosed and fixed. Numbers below are the actual current results, not aspirational ones — check back or see the linked repo for updates before relying on this model's uncertainty estimates in particular.
PSA-Net is a dataset-agnostic architecture designed for time-series forecasting on metrics exhibiting high seasonality and sudden trend spikes. It integrates four core components:
Patch Tokenization:
patch_len=15, patch_stride=15).Dual Time / Spectral (FFT) Branch:
Explicit Seasonal Embeddings:
Spike Pattern Cross-Attention Bank:
Multi-Quantile Output Head:
Input History [Batch, input_window, N_features]
│
├──► Patch Tokenization (patch_len, patch_stride)
├──► FFT Spectral Branch (n_freq_bins)
├──► Time-of-Day / Day-of-Week Embeddings
└──► Spike Pattern Cross-Attention Bank (n_spike_patterns)
│
Transformer Blocks (n_layers, d_model, n_heads)
│
Quantile Output Head
│
▼
Forecast Tensor [Batch, forecast_horizon, N_features, n_quantiles]
| Hyperparameter | Default | Description |
|---|---|---|
n_features | Configurable ($N$) | Number of input time-series signals |
input_window | 120 | Length of historical input sequence (timesteps) |
forecast_horizon | 15 or 60 | Number of future timesteps to predict |
patch_len | 15 | Timesteps per patch |
patch_stride | 15 | Stride between consecutive patches |
d_model | 128 | Model hidden embedding dimension |
n_heads | 4 | Number of Multi-Head Attention heads |
n_layers | 3 | Number of Transformer encoder blocks |
use_spectral_branch | True | Enables FFT frequency-domain fusion branch |
use_seasonal_embed | True | Enables Time-of-Day and Day-of-Week embeddings |
use_spike_bank | True | Enables Spike-Pattern Cross-Attention memory bank |
n_quantiles | 3 | Number of output quantiles ($q_{0.1}, q_{0.5}, q_{0.9}$) |
Training run: 2,628,000 rows, 13 columns, 10 epochs, batch size 128, 6,265,711 parameters. Training and validation loss decreased smoothly and consistently across all 10 epochs (val loss: 0.01315 → 0.01107), showing no signs of divergence or overfitting during training itself.
Test set: synthetic_hpa_traffic_shifted_test.csv — a distribution-shifted
test set (evaluating generalization under shift, not just an ordinary
held-out chronological split; results below should be interpreted with that
in mind).
| Feature | MAE | RMSE | WAPE (%) | 80% Interval Coverage |
|---|---|---|---|---|
| requests_per_second | 295.25 | 594.59 | 27.42% | 10.19% |
| concurrent_users | 341.39 | 1209.18 | 15.10% | 41.65% |
| cpu_utilization_pct | 8.26 | 12.04 | 28.03% | 20.45% |
| memory_utilization_pct | 7.80 | 9.03 | 15.02% | 1.19% |
| gpu_utilization_pct | 3.53 | 6.85 | 6.05% | 40.04% |
| pod_count | 2.88 | 9.52 | 13.61% | 15.56% |
Known issue — quantile calibration is currently broken. The 80% interval
should contain the true value ~80% of the time; observed coverage ranges
1-42%, far below target across every feature. This specific pattern (healthy
training loss alongside badly miscalibrated eval coverage) is consistent
with a denormalization or quantile-ordering bug in the evaluation pipeline
rather than necessarily a model-quality problem — this is under active
investigation. Do not rely on this model's quantile/uncertainty outputs
(q_0.1, q_0.9) until this is resolved and re-verified. Point forecasts
(q_0.5 / MAE / RMSE) may also be affected by the same root cause and should
be treated with the same caution until confirmed.
RMSE substantially exceeding MAE on requests_per_second and concurrent_users
specifically (RMSE ~2-3.5x MAE) suggests a subset of large errors — plausibly
missed or mistimed spike predictions — pulling the average up, rather than
uniformly-distributed small errors across all predictions.
This section will be updated once the calibration issue is diagnosed and a corrected evaluation run is available. Comparison against PatchTST and Prophet baselines (trained on the same data split) is planned but not yet included below.
import torch
from psanet.model import PSANet, PSANetConfig
# 1. Define Model Configuration
cfg = PSANetConfig(
n_features=6, # Works for any N_features
input_window=120, # 2-hour lookback @ 1-min resolution
forecast_horizon=15, # 15-minute future forecast
patch_len=15,
patch_stride=15,
d_model=128,
n_heads=4,
n_layers=3,
n_quantiles=3 # [q_0.1, q_0.5, q_0.9]
)
# 2. Instantiate PyTorch Model
device = "cuda" if torch.cuda.is_available() else "cpu"
model = PSANet(cfg).to(device)
print(f"PSA-Net Parameter Count: {model.param_count():,}")
import torch
from psanet.losses import quantile_loss
# Dummy input tensors: [Batch, input_window, N_features]
batch_size = 32
x_hist = torch.randn(batch_size, cfg.input_window, cfg.n_features).to(device)
y_target = torch.randn(batch_size, cfg.forecast_horizon, cfg.n_features).to(device)
# Time indices (optional for seasonal embeddings)
n_patches = (cfg.input_window - cfg.patch_len) // cfg.patch_stride + 1
tod_idx = torch.randint(0, cfg.steps_per_day, (batch_size, n_patches)).to(device)
dow_idx = torch.randint(0, cfg.days_per_week, (batch_size, n_patches)).to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)
# Training Step
model.train()
optimizer.zero_grad()
# Forward Pass -> Output shape: [batch_size, forecast_horizon, n_features, n_quantiles]
preds = model(x_hist, tod_idx, dow_idx)
# Compute Quantile Pinball Loss for q=[0.1, 0.5, 0.9]
loss = quantile_loss(preds, y_target, quantiles=[0.1, 0.5, 0.9])
loss.backward()
optimizer.step()
print(f"Training Step Loss: {loss.item():.4f}")
import torch
import numpy as np
# Set model to evaluation mode
model.eval()
# Assume mean and std are normalization statistics fit on training data
mean = np.zeros(cfg.n_features)
std = np.ones(cfg.n_features)
# Historical context window: [1, input_window, n_features]
context_raw = np.random.randn(cfg.input_window, cfg.n_features)
context_norm = (context_raw - mean) / std
context_tensor = torch.tensor(context_norm, dtype=torch.float32).unsqueeze(0).to(device)
with torch.no_grad():
raw_preds = model(context_tensor) # [1, forecast_horizon, n_features, 3]
# Unscale back to original metric units
preds_np = raw_preds.squeeze(0).cpu().numpy() # [forecast_horizon, n_features, 3]
preds_unscaled = preds_np * std[None, :, None] + mean[None, :, None]
# Extract Quantile Curves
q_10 = preds_unscaled[..., 0] # Lower Bound (q=0.1)
q_50 = preds_unscaled[..., 1] # Median Point Forecast (q=0.5)
q_90 = preds_unscaled[..., 2] # Upper Bound (q=0.9)
import torch
# Save Checkpoint
checkpoint_dict = {
"model_state": model.state_dict(),
"config": cfg,
"mean": mean,
"std": std
}
torch.save(checkpoint_dict, "psanet_checkpoint.pt")
# Load Checkpoint
loaded_ckpt = torch.load("psanet_checkpoint.pt", map_location="cpu", weights_only=False)
loaded_cfg = loaded_ckpt["config"]
loaded_model = PSANet(loaded_cfg)
loaded_model.load_state_dict(loaded_ckpt["model_state"])
loaded_model.eval()
input_window continuous historical timesteps before generating forecasts.