Sekvensmodeller för tidsserier
Kunna träna en sekvensmodell på tidsserier och jämföra med klassiska metoder.
Förkunskaper
- ESekvensmodeller före transformerskrävs
- ETidsserierkrävs
Intuition
En sekvensmodell tar ett fönster av historik och producerar en prognos.
indata: [y_{t-63} ... y_{t-1}] + kalender, externa
utdata: [y_t ... y_{t+13}] ← hela horisonten på en gång
Två sätt att prognosticera flera steg:
| Sätt | Hur | Problem |
|---|---|---|
| Rekursivt | prognosticera ett steg, mata tillbaka, upprepa | fel ackumuleras |
| Direkt (multi-output) | prognosticera hela horisonten i ett | ingen felackumulering |
Direkt är nästan alltid bättre för längre horisonter, och är standard i moderna modeller.
Men börja inte här. Sekvensmodeller lönar sig först under vissa villkor, och de är färre än man tror:
| Villkor | Varför |
|---|---|
| Många parallella serier (hundratals+) | modellen kan dela mönster mellan dem |
| Lång historik per serie | annars finns inget att lära |
| Externa variabler som spelar roll | nätet kan väga ihop dem |
| Icke-linjära samband | linjära metoder räcker annars |
Med en serie och tre års data vinner gradient boosting på lag-features nästan alltid.
Formellt
Arkitekturer, i ungefärlig ordning efter hur ofta de är rätt val:
| Modell | Styrka | Svaghet |
|---|---|---|
| Boosting på lag-features | stark baslinje, snabb, tolkbar | inte en sekvensmodell alls |
| N-BEATS / N-HiTS | stark på univariata serier, tolkbara block | mindre flexibel med externa variabler |
| TFT (Temporal Fusion Transformer) | hanterar många kovariater, ger attention-tolkning | tung, många hyperparametrar |
| DeepAR | probabilistiska prognoser, många serier | autoregressiv, långsam inferens |
| LSTM/GRU | enkel, väl förstådd | slagen av ovanstående |
Skalning per serie är nödvändig när serierna har olika nivå: en butik som säljer 10 enheter och en som säljer 10 000 kan inte dela modell utan normalisering. Standard är att dela varje serie med sitt eget medelvärde i träningsfönstret — och att göra det per fönster, inte globalt, för att undvika läckage.
Probabilistiska prognoser är ofta mer värdefulla än punktprognoser. En prognos på 100 enheter säger inget om man ska lagerhålla 100 eller 140. Två sätt:
| Metod | Ger |
|---|---|
| Kvantilregression (pinball loss) | valda kvantiler, t.ex. 10 %, 50 %, 90 % |
| Parametrisk (negativ binomial, normal) | hela fördelningen |
Pinball loss för kvantil : . Den straffar underprognos och överprognos olika, vilket är precis vad lagerhållning kräver.
En rättvis jämförelse kräver samma sak här som överallt: samma uppdelning, samma horisont, baslinjer med, flera frön och spridning rapporterad. Litteraturen på området har historiskt haft problem med detta — M-tävlingarna infördes delvis som ett svar, och i M4 (2018) slog en hybrid av statistik och neurala nät, medan rena neurala metoder hamnade under de statistiska baslinjerna.
Kod
import numpy as np, torch, torch.nn as nn
FONSTER, HORISONT = 63, 14
def gor_fonster(y, fonster=FONSTER, horisont=HORISONT):
"""Glidande fönster. y: (T,) → X: (N, fonster), Y: (N, horisont)."""
X, Y = [], []
for i in range(len(y) - fonster - horisont + 1):
X.append(y[i:i + fonster])
Y.append(y[i + fonster:i + fonster + horisont])
return np.array(X, dtype="float32"), np.array(Y, dtype="float32")
class Prognos(nn.Module):
"""Direkt multi-output: hela horisonten i ett, ingen felackumulering."""
def __init__(self, fonster=FONSTER, horisont=HORISONT, dolt=256, kvantiler=(0.1, 0.5, 0.9)):
super().__init__()
self.kvantiler = kvantiler
self.f = nn.Sequential(
nn.Linear(fonster, dolt), nn.ReLU(), nn.Dropout(0.1),
nn.Linear(dolt, dolt), nn.ReLU(),
nn.Linear(dolt, horisont * len(kvantiler)),
)
self.horisont = horisont
def forward(self, x):
return self.f(x).view(-1, len(self.kvantiler), self.horisont)
def pinball(pred, mal, kvantiler):
"""pred: (B, Q, H), mal: (B, H)"""
fel = mal.unsqueeze(1) - pred
q = torch.tensor(kvantiler, device=pred.device).view(1, -1, 1)
return torch.maximum(q * fel, (q - 1) * fel).mean()
# Skalning PER FÖNSTER — inte globalt, annars läcker framtiden in
def skala(X, Y):
m = X.mean(axis=1, keepdims=True)
s = X.std(axis=1, keepdims=True) + 1e-6
return (X - m) / s, (Y - m) / s, m, s
# Kronologisk uppdelning, aldrig slumpmässig
def dela(X, Y, andel_train=0.7, andel_val=0.15):
n = len(X)
a, b = int(n * andel_train), int(n * (andel_train + andel_val))
return (X[:a], Y[:a]), (X[a:b], Y[a:b]), (X[b:], Y[b:])
# Rättvis jämförelse: samma uppdelning, samma horisont, baslinjer med
def jamfor(y, modeller: dict):
X, Y = gor_fonster(y)
(_, _), (_, _), (Xte, Yte) = dela(X, Y)
resultat = {"säsongsnaiv": float(np.abs(Yte - Xte[:, -7:][:, :1]).mean())}
for namn, fn in modeller.items():
resultat[namn] = float(np.abs(Yte - fn(Xte)).mean())
naiv = resultat["säsongsnaiv"]
return {k: {"MAE": round(v, 3), "MASE": round(v / naiv, 3)} for k, v in resultat.items()}
skala per fönster är den detalj som oftast görs fel: normaliserar man med hela seriens medelvärde har man använt framtida värden för att skala träningsdatan, och valideringen blir för optimistisk.
Behärskning innebär
- Formar tidsseriedata för en sekvensmodell
- Jämför rättvist mot klassiska baslinjer
- Vet när djupa modeller lönar sig
Logga in för att göra övningarna och bygga upp din behärskning.
Källor
- arXiv — N-BEATS: Neural basis expansion analysis for interpretable time series forecasting — arXiv (öppen åtkomst; licens per artikel)
- arXiv — Temporal Fusion Transformers for Interpretable Multi-horizon Time Series Forecasting — arXiv (öppen åtkomst; licens per artikel)
- Hyndman & Athanasopoulos — Forecasting: Principles and Practice — fri att läsa online