Views
No views yet
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.
patch_len=15, patch_stride=15).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}$) |
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% |
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.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.1import torch
2from psanet.model import PSANet, PSANetConfig
3
4# 1. Define Model Configuration
5cfg = PSANetConfig(
6 n_features=6, # Works for any N_features
7 input_window=120, # 2-hour lookback @ 1-min resolution
8 forecast_horizon=15, # 15-minute future forecast
9 patch_len=15,
10 patch_stride=15,
11 d_model=128,
12 n_heads=4,
13 n_layers=3,
14 n_quantiles=3 # [q_0.1, q_0.5, q_0.9]
15)
16
17# 2. Instantiate PyTorch Model
18device = "cuda" if torch.cuda.is_available() else "cpu"
19model = PSANet(cfg).to(device)
20
21print(f"PSA-Net Parameter Count: {model.param_count():,}")1import torch
2from psanet.losses import quantile_loss
3
4# Dummy input tensors: [Batch, input_window, N_features]
5batch_size = 32
6x_hist = torch.randn(batch_size, cfg.input_window, cfg.n_features).to(device)
7y_target = torch.randn(batch_size, cfg.forecast_horizon, cfg.n_features).to(device)
8
9# Time indices (optional for seasonal embeddings)
10n_patches = (cfg.input_window - cfg.patch_len) // cfg.patch_stride + 1
11tod_idx = torch.randint(0, cfg.steps_per_day, (batch_size, n_patches)).to(device)
12dow_idx = torch.randint(0, cfg.days_per_week, (batch_size, n_patches)).to(device)
13
14optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)
15
16# Training Step
17model.train()
18optimizer.zero_grad()
19
20# Forward Pass -> Output shape: [batch_size, forecast_horizon, n_features, n_quantiles]
21preds = model(x_hist, tod_idx, dow_idx)
22
23# Compute Quantile Pinball Loss for q=[0.1, 0.5, 0.9]
24loss = quantile_loss(preds, y_target, quantiles=[0.1, 0.5, 0.9])
25loss.backward()
26optimizer.step()
27
28print(f"Training Step Loss: {loss.item():.4f}")1import torch
2import numpy as np
3
4# Set model to evaluation mode
5model.eval()
6
7# Assume mean and std are normalization statistics fit on training data
8mean = np.zeros(cfg.n_features)
9std = np.ones(cfg.n_features)
10
11# Historical context window: [1, input_window, n_features]
12context_raw = np.random.randn(cfg.input_window, cfg.n_features)
13context_norm = (context_raw - mean) / std
14context_tensor = torch.tensor(context_norm, dtype=torch.float32).unsqueeze(0).to(device)
15
16with torch.no_grad():
17 raw_preds = model(context_tensor) # [1, forecast_horizon, n_features, 3]
18
19# Unscale back to original metric units
20preds_np = raw_preds.squeeze(0).cpu().numpy() # [forecast_horizon, n_features, 3]
21preds_unscaled = preds_np * std[None, :, None] + mean[None, :, None]
22
23# Extract Quantile Curves
24q_10 = preds_unscaled[..., 0] # Lower Bound (q=0.1)
25q_50 = preds_unscaled[..., 1] # Median Point Forecast (q=0.5)
26q_90 = preds_unscaled[..., 2] # Upper Bound (q=0.9)1import torch
2
3# Save Checkpoint
4checkpoint_dict = {
5 "model_state": model.state_dict(),
6 "config": cfg,
7 "mean": mean,
8 "std": std
9}
10torch.save(checkpoint_dict, "psanet_checkpoint.pt")
11
12# Load Checkpoint
13loaded_ckpt = torch.load("psanet_checkpoint.pt", map_location="cpu", weights_only=False)
14loaded_cfg = loaded_ckpt["config"]
15
16loaded_model = PSANet(loaded_cfg)
17loaded_model.load_state_dict(loaded_ckpt["model_state"])
18loaded_model.eval()input_window continuous historical timesteps before generating forecasts.