tiny-forecaster / model.py
limved's picture
Upload model.py with huggingface_hub
bffb943 verified
Raw History Blame Contribute Delete
3.06 kB
"""
A small, patch-based transformer for zero-shot time series forecasting —
a toy-scale version of the same general recipe used by real time-series
foundation models (Chronos, TimesFM, t0-alpha, etc.):
1. Per-series normalization: each input window is normalized by its
OWN mean and std before entering the model, and de-normalized
using those same stats afterward. This is the single most
important design choice for generalization — it means the model
learns SHAPE (trend, seasonality, noise structure), not absolute
numbers, so a model trained on synthetic series scaled 0.5-500 can
meaningfully forecast a real series scaled in the thousands.
2. Patching: rather than feeding every single timestep as a separate
token (expensive, and treats each point independently), the input
window is split into small chunks ("patches") and each patch is
projected into the model's embedding space as one token. This is
the same idea PatchTST and most modern time-series transformers
use — it's both more efficient and tends to generalize better
than point-wise tokenization.
3. A standard transformer encoder processes the patch tokens, and a
linear head maps the final representation directly to the
forecast horizon.
CONTEXT_LEN and HORIZON are fixed (96 and 42) to match the evaluation
setup already used for SARIMA/Prophet/statsforecast/t0-alpha in the
sibling demand-forecasting project, so results are directly comparable.
"""
import torch
import torch.nn as nn
CONTEXT_LEN = 96
HORIZON = 42
PATCH_SIZE = 8
N_PATCHES = CONTEXT_LEN // PATCH_SIZE # 12
D_MODEL = 32
N_HEADS = 4
N_LAYERS = 2
DIM_FEEDFORWARD = 64
class TinyForecaster(nn.Module):
def __init__(self):
super().__init__()
self.patch_embed = nn.Linear(PATCH_SIZE, D_MODEL)
self.pos_embed = nn.Parameter(torch.randn(1, N_PATCHES, D_MODEL) * 0.02)
encoder_layer = nn.TransformerEncoderLayer(
d_model=D_MODEL, nhead=N_HEADS, dim_feedforward=DIM_FEEDFORWARD,
batch_first=True, dropout=0.1,
)
self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=N_LAYERS)
self.head = nn.Sequential(
nn.Linear(D_MODEL * N_PATCHES, 128),
nn.ReLU(),
nn.Linear(128, HORIZON),
)
def forward(self, context_raw: torch.Tensor) -> torch.Tensor:
"""
context_raw: (batch, CONTEXT_LEN) raw (unnormalized) values.
Returns: (batch, HORIZON) forecast, in the ORIGINAL scale.
"""
mean = context_raw.mean(dim=1, keepdim=True)
std = context_raw.std(dim=1, keepdim=True).clamp(min=1e-3)
normed = (context_raw - mean) / std
patches = normed.view(-1, N_PATCHES, PATCH_SIZE)
tokens = self.patch_embed(patches) + self.pos_embed
encoded = self.encoder(tokens)
flat = encoded.reshape(encoded.size(0), -1)
forecast_normed = self.head(flat)
forecast = forecast_normed * std + mean
return forecast