Hoppa till innehållet
AI-grafen
E· Universitetdeep-learning· ca 60 min· utvecklande· verifierad 2026-09-20

Sekvensmodeller för tidsserier

Kunna träna en sekvensmodell på tidsserier och jämföra med klassiska metoder.

Förkunskaper

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ättHurProblem
Rekursivtprognosticera ett steg, mata tillbaka, upprepafel ackumuleras
Direkt (multi-output)prognosticera hela horisonten i ettingen 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:

VillkorVarför
Många parallella serier (hundratals+)modellen kan dela mönster mellan dem
Lång historik per serieannars finns inget att lära
Externa variabler som spelar rollnätet kan väga ihop dem
Icke-linjära sambandlinjä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:

ModellStyrkaSvaghet
Boosting på lag-featuresstark baslinje, snabb, tolkbarinte en sekvensmodell alls
N-BEATS / N-HiTSstark på univariata serier, tolkbara blockmindre flexibel med externa variabler
TFT (Temporal Fusion Transformer)hanterar många kovariater, ger attention-tolkningtung, många hyperparametrar
DeepARprobabilistiska prognoser, många serierautoregressiv, långsam inferens
LSTM/GRUenkel, väl förståddslagen 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:

MetodGer
Kvantilregression (pinball loss)valda kvantiler, t.ex. 10 %, 50 %, 90 %
Parametrisk (negativ binomial, normal)hela fördelningen

Pinball loss för kvantil qq: max⁡(q(y−y^), (q−1)(y−y^))\max(q(y-\hat{y}),\, (q-1)(y-\hat{y})). 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

Alla källor och licenser