系列 · 时间序列预测 · 第 7 篇

时间序列模型(七):N-BEATS——可解释的深度架构

N-BEATS 把深度学习的表达力和经典分解的可解释性合二为一:基函数展开、双重残差堆叠、M4 竞赛分析,以及完整的 PyTorch 代码。

2018 年 M4 预测竞赛提供了覆盖六种频率的 10 万条时间序列作为统一基准。排行榜一度被基于数十年统计预测经验手工调优的集成模型所主导,但最终胜出的却是一个纯神经网络——它无需任何统计预处理、特征工程或循环结构。这个模型正是 Oreshkin 等人提出的 N-BEATS:一个由全连接块堆叠而成、带有两条残差路径的架构。其可解释变体进一步将预测显式分解为多项式趋势与傅里叶季节性分量,使得经典统计学家梦寐以求的“可读分解”变得唾手可得。

时间序列模型(七):N-BEATS——可解释的深度架构 — 章节概览图

本章将深入剖析为何如此简洁的架构能同时击败 LSTMs 和 ARIMA 风格的集成模型,并指导你如何在自己的数据上实现与调优。


你将学到什么#

  • 双重残差堆叠如何将普通 MLP 转化为层级分解器。
  • 基函数展开:用多项式基建模趋势,傅里叶基捕捉季节性,学习基应对“通用”变体。
  • N-BEATS 为何能同时成为最准确且最可解释的模型。
  • M4 竞赛结果:N-BEATS 具体击败了哪些模型,优势有多大。
  • 完整的 PyTorch 实现,以及可直接复用的零售销量与电力需求案例。

前置要求:熟悉前馈网络与 PyTorch;了解经典分解(趋势 / 季节性 / 残差)会有帮助,但非必需。


为什么全连接堆栈就够用了#

大多数深度时序模型都会引入结构先验:卷积假设平移等变性,RNN 假设顺序隐藏状态,注意力机制则假设成对相关性。N-BEATS 却反其道而行之——它直接将整个输入窗口喂给 MLP,让网络自行学习最有用的分解方式。关键不在于层的类型,而在于信息流动的路径。

具体而言,N-BEATS 做了三个明确的设计选择:

  1. 堆叠相同结构的模块,每个模块同时预测 backcast(对输入窗口的重构)和 forecast(对未来值的预测)。
  2. 双重残差流:每个模块从当前输入残差中减去自身的 backcast,同时将其 forecast 加入累积预测。下一个模块仅看到前一个模块未能解释的部分。
  3. 基函数输出头:模块不直接输出预测值,而是生成一个小系数向量 $\theta$ ,再将其与一个固定(可解释)或可学习(通用)的基矩阵相乘。

这三点组合起来,足以让 N-BEATS 登顶 M4 排行榜。


架构:双重残差流#

想象网络中有两条自上而下并行的管道。左侧是残差流:从输入窗口 $x \in \mathbb{R}^{H}$ 开始,每经过一个模块就减去其 backcast,逐渐缩小。右侧是预测累加器:从零开始,每经过一个模块就加上其 forecast,逐步增长。

N-BEATS 的双重残差堆叠

$$r^{(b)} = r^{(b-1)} - \hat{x}^{(b)}, \qquad \hat{y} = \sum_{b=1}^{B} \hat{y}^{(b)},$$

其中 $r^{(0)} = x$ 。每个模块面对的残差越来越小,因此会专注于剩余未被解释的模式。宏观结构(如整体趋势、主季节周期)由早期模块捕获,精细修正则由后期模块完成。

这与梯度提升思想一致,但被嵌入到一个端到端可微的网络中。顺序至关重要:第一个模块任务最简单(信号完整),最后一个模块任务最难(只剩噪声与细微结构)。


块的内部#

每个 N-BEATS 块结构相同。给定残差输入 $r \in \mathbb{R}^{H}$

  1. 特征提取器——四层宽度为 256–512 的全连接 ReLU 层: $ h_1 = \mathrm{ReLU}(W_1 r + b_1), \quad \ldots, \quad h_4 = \mathrm{ReLU}(W_4 h_3 + b_4). $
  2. 系数投影——两个线性头分别输出 backcast 与 forecast 的系数: $ \theta^{b} = W_b h_4, \qquad \theta^{f} = W_f h_4. $
  3. 基函数映射——通过固定或可学习矩阵 $V$ 将系数映射回时域: $ \hat{x} = V_b \, \theta^{b}, \qquad \hat{y} = V_f \, \theta^{f}. $ 两种变体的区别仅在于 $V$ 的定义。

可解释:趋势 + 季节性基#

$$ V_{\text{trend}} = \begin{pmatrix} 1 & \tau & \tau^{2} & \cdots & \tau^{p} \end{pmatrix}, \qquad \hat{y}_{\text{trend}} = \sum_{i=0}^{p} \theta_i \, \tau^{i}. $$

通常取 $p = 2$$3$ ,足以拟合“先平缓上升后加速”的趋势,又避免过拟合抖动。

$$ V_{\text{seas}} = \begin{pmatrix} \sin(2\pi \cdot 1 \cdot \tau / T) & \cos(2\pi \cdot 1 \cdot \tau / T) & \cdots & \sin(2\pi K \tau / T) & \cos(2\pi K \tau / T) \end{pmatrix}. $$

其中 $K = 1, 2, 3$ 为谐波数量,$T$ 为已知周期(月度数据 $T=12$ ,小时数据 $T=24$ ),可捕捉任意形状的周期信号。

可解释架构先堆叠若干趋势块,再接若干季节性块。训练后可分别绘制各栈贡献,向业务方清晰解释:“这部分来自底层趋势,那部分源于每周循环。”

可解释 N-BEATS 给出的趋势 + 季节性分解

通用(Generic):学习到的基#

通用(Generic)变体允许 $V_b$$V_f$ 为可学习矩阵。模块不再受限于趋势/季节性的语义约束,而是根据梯度信号自主发现有效基函数。此举虽小幅提升精度,却牺牲了可读分解。

可解释 vs 通用的栈结构

论文指出一条实用经验:M4 上的最佳结果来自可解释与通用模型的集成,且各自采用不同回看长度。我们将在“集成策略”一节详述。


为什么基函数输出头很重要#

模块本可直接预测:$\hat{y} = W_y h_4$ 。为何要绕道 $\theta$ 向量与基矩阵?原因有三:

  • 归纳偏置:强制用少量平滑基的线性组合表达预测,有效抑制噪声拟合。例如,720 步输出配 3 阶多项式,趋势仅 4 个自由度,物理上无法振荡。这种正则化使可解释变体具备良好泛化能力。
  • 免费可解释性:趋势栈的 $\theta_0, \theta_1, \theta_2, \theta_3$ 分别对应基线、斜率、曲率与加加速度(jerk);季节性栈系数即特定谐波的振幅。这些参数可直接绘图分析。
  • 参数高效:直接从 512 维隐状态映射至 720 步需 $512 \times 720 = 369K$ 参数;而基函数头仅需两小层($512 \to p$ ,再经固定基 $p \to 720$ ),通常每头不足 10K 参数。

该思想如今已被广泛采纳(PatchTST、N-HiTS、TSMixer 均采用某种分解头),而 N-BEATS 是其推广者。


PyTorch 实现#

以下是一个简洁完整的实现,全模型约 120 行代码。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import torch
import torch.nn as nn
import numpy as np

class TrendBasis(nn.Module):
    """多项式基:V[i, t] = (t / horizon) ** i"""

    def __init__(self, degree: int, backcast_size: int, forecast_size: int):
        super().__init__()
        self.degree = degree
        tb = torch.stack([torch.linspace(0, 1, backcast_size) ** i
                          for i in range(degree + 1)], dim=0)
        tf = torch.stack([torch.linspace(0, 1, forecast_size) ** i
                          for i in range(degree + 1)], dim=0)
        self.register_buffer("V_b", tb)  # (degree+1, H)
        self.register_buffer("V_f", tf)  # (degree+1, F)

    @property
    def theta_size(self) -> int:
        return self.degree + 1

    def forward(self, theta_b, theta_f):
        return theta_b @ self.V_b, theta_f @ self.V_f

class SeasonalityBasis(nn.Module):
    """Fourier 基:取前 floor((H 或 F) / 2) 阶谐波"""

    def __init__(self, backcast_size: int, forecast_size: int):
        super().__init__()
        K = forecast_size // 2 + 1
        tb = torch.linspace(0, 1, backcast_size)
        tf = torch.linspace(0, 1, forecast_size)
        ks = torch.arange(K).unsqueeze(1).float()
        Vb = torch.cat([torch.cos(2 * np.pi * ks * tb),
                        torch.sin(2 * np.pi * ks * tb)], dim=0)
        Vf = torch.cat([torch.cos(2 * np.pi * ks * tf),
                        torch.sin(2 * np.pi * ks * tf)], dim=0)
        self.register_buffer("V_b", Vb)  # (2K, H)
        self.register_buffer("V_f", Vf)  # (2K, F)

    @property
    def theta_size(self) -> int:
        return self.V_b.shape[0]

    def forward(self, theta_b, theta_f):
        return theta_b @ self.V_b, theta_f @ self.V_f

class GenericBasis(nn.Module):
    """学习基:backcast/forecast 关于 theta 是线性的"""

    def __init__(self, theta_size: int, backcast_size: int,
                 forecast_size: int):
        super().__init__()
        self._theta_size = theta_size
        self.linear_b = nn.Linear(theta_size, backcast_size, bias=False)
        self.linear_f = nn.Linear(theta_size, forecast_size, bias=False)

    @property
    def theta_size(self) -> int:
        return self._theta_size

    def forward(self, theta_b, theta_f):
        return self.linear_b(theta_b), self.linear_f(theta_f)

class NBeatsBlock(nn.Module):
    def __init__(self, basis: nn.Module, backcast_size: int,
                 hidden: int = 256, layers: int = 4):
        super().__init__()
        self.basis = basis
        units = [backcast_size] + [hidden] * layers
        fcs = []
        for in_dim, out_dim in zip(units[:-1], units[1:]):
            fcs.append(nn.Linear(in_dim, out_dim))
            fcs.append(nn.ReLU())
        self.fc = nn.Sequential(*fcs)
        self.head_b = nn.Linear(hidden, basis.theta_size)
        self.head_f = nn.Linear(hidden, basis.theta_size)

    def forward(self, x):
        h = self.fc(x)
        theta_b = self.head_b(h)
        theta_f = self.head_f(h)
        return self.basis(theta_b, theta_f)  # (backcast, forecast)

class NBeats(nn.Module):
    def __init__(self, blocks: list[nn.Module]):
        super().__init__()
        self.blocks = nn.ModuleList(blocks)

    def forward(self, x):
        residual = x
        forecast = 0.0
        for blk in self.blocks:
            backcast, fc = blk(residual)
            residual = residual - backcast
            forecast = forecast + fc
        return forecast

def make_interpretable(history: int, horizon: int,
                       trend_blocks: int = 3, seasonal_blocks: int = 3,
                       trend_degree: int = 3,
                       hidden: int = 256, layers: int = 4) -> NBeats:
    blocks = []
    trend_basis = TrendBasis(trend_degree, history, horizon)
    for _ in range(trend_blocks):
        blocks.append(NBeatsBlock(trend_basis, history, hidden, layers))
    seas_basis = SeasonalityBasis(history, horizon)
    for _ in range(seasonal_blocks):
        blocks.append(NBeatsBlock(seas_basis, history, hidden, layers))
    return NBeats(blocks)

def make_generic(history: int, horizon: int,
                 num_blocks: int = 30, theta_size: int = 32,
                 hidden: int = 512, layers: int = 4) -> NBeats:
    blocks = []
    for _ in range(num_blocks):
        basis = GenericBasis(theta_size, history, horizon)
        blocks.append(NBeatsBlock(basis, history, hidden, layers))
    return NBeats(blocks)

一个关键细节:在可解释变体中,TrendBasisSeasonalityBasis 实例在同一栈内的所有块间共享。各块拥有独立 MLP 与系数头,但共用同一固定基矩阵,既维持归纳偏置,又节省参数。


训练方法#

Oreshkin 等人的训练方案极为简洁:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from torch.optim import Adam
from torch.optim.lr_scheduler import CosineAnnealingLR

def train_nbeats(model, train_loader, val_loader, epochs=100,
                 lr=1e-3, device="cuda"):
    model = model.to(device)
    opt = Adam(model.parameters(), lr=lr)
    sched = CosineAnnealingLR(opt, T_max=epochs)
    crit = nn.L1Loss()  # MAE;论文根据数据集用 sMAPE/MASE/MAPE,
                        # L1 是个稳妥的默认选择
    best = float("inf")
    for ep in range(epochs):
        model.train()
        train_loss = 0.0
        for xb, yb in train_loader:
            xb, yb = xb.to(device), yb.to(device)
            opt.zero_grad()
            loss = crit(model(xb), yb)
            loss.backward()
            opt.step()
            train_loss += loss.item() * xb.size(0)
        sched.step()
        train_loss /= len(train_loader.dataset)

        model.eval()
        val_loss = 0.0
        with torch.no_grad():
            for xb, yb in val_loader:
                xb, yb = xb.to(device), yb.to(device)
                val_loss += crit(model(xb), yb).item() * xb.size(0)
        val_loss /= len(val_loader.dataset)
        if val_loss < best:
            best = val_loss
            torch.save(model.state_dict(), "nbeats.pt")
        if (ep + 1) % 10 == 0:
            print(f"epoch {ep+1}: train {train_loss:.4f} val {val_loss:.4f}")

几点实践建议:

  • 损失函数:M4 使用 sMAPE;实际场景中 MAE(L1)通常比 MSE 更稳定,因其不对离群点过度惩罚。选择与评估指标匹配的损失。
  • 逐窗口标准化:对每个输入窗口,先减均值、除标准差,再送入网络,最后对预测结果做逆变换。此步远比损失选择重要——若不做,网络需额外学习每条序列的尺度,浪费容量。
  • 早停策略:N-BEATS 需较长训练,但在多数数据集上约 50 轮后验证损失趋于平稳。监控验证损失,停止改善时即可终止。

N-BEATS 在 M4 竞赛中的表现#

M4 包含统计方法(ARIMA、ETS、Theta)、冠军 Smyl 的 ES-RNN 混合模型,以及亚军 FFORMA(基于特征元学习)。N-BEATS 无任何统计预处理,却在整体 sMAPE 及六个频率分组中的五个上全面胜出。

N-BEATS 在 M4 竞赛中的表现

论文数据如下:

  • N-BEATS(可解释 + 通用集成):sMAPE 11.135
  • N-BEATS(仅通用):sMAPE 11.168
  • Smyl 的 ES-RNN(M4 冠军):sMAPE 11.374
  • FFORMA:sMAPE 11.720
  • 最佳经典方法(Theta):sMAPE 12.309

绝对 sMAPE 差距虽小,但在年、季、月、周、日频段均稳定领先。唯独小时频段,Smyl 的 ES-RNN 以 0.4 sMAPE 微弱优势胜出。

更深层启示:一个表达力足够强且具备合适归纳偏置的深度模型,能从零复现统计学家数十年手工构建的知识。


集成:隐藏在配方中的另一半#

细读 M4 论文会发现一个脚注:头条 N-BEATS 结果实为 180 个模型实例预测的中位数。各实例在回看长度(2H 至 7H)、训练损失(sMAPE/MASE/MAPE)或随机种子上有所不同。单模型性能明显弱于集成。

为什么 N-BEATS 使用集成:原理与实证 sMAPE 提升

右侧实证曲线显示收益递减:大部分增益来自前约 30 个成员。生产中几乎无需训练 180 个模型——混合 10 至 30 个不同回看长度与种子的模型,即可捕获绝大部分提升。

一个简易集成辅助工具:

1
2
3
4
5
6
7
class EnsembleNBeats:
    def __init__(self, models: list[NBeats]):
        self.models = models

    def predict(self, x: torch.Tensor, aggregator="median") -> torch.Tensor:
        outs = torch.stack([m(x) for m in self.models], dim=0)
        return outs.median(dim=0).values if aggregator == "median" else outs.mean(0)

对 sMAPE 类损失,中位数聚合优于均值,因其对单窗口异常预测更具鲁棒性。


案例 1:月度零售销量预测#

设定:基于过去 36 个月销量,预测某多门店零售连锁未来 12 个月销量。数据具强假日季节性(12 月高峰)、整体上升趋势及偶发促销,涵盖约 200 条产品-门店序列。

架构选择:采用可解释变体。业务团队需明确区分“趋势贡献”“12 月季节性提升”与“残差”。[3 个趋势块] + [3 个季节性块] 的设计直接满足此需求。

1
2
3
4
5
6
model = make_interpretable(
    history=36, horizon=12,
    trend_blocks=3, seasonal_blocks=3,
    trend_degree=2,                     # 平滑的多年趋势
    hidden=256, layers=4,
)

训练后可提取各栈贡献,观察其学习内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def stack_contributions(model: NBeats, x: torch.Tensor) -> dict:
    """针对单个窗口,按栈拆分预测贡献。"""
    residual = x.clone()
    out = {}
    cur_stack = "trend"
    cumulative = torch.zeros(x.size(0), model.blocks[0].basis.V_f.shape[1])
    for i, blk in enumerate(model.blocks):
        backcast, fc = blk(residual)
        residual = residual - backcast
        cumulative = cumulative + fc
        next_stack = "seasonality" if isinstance(blk.basis, SeasonalityBasis) else "trend"
        if i + 1 == len(model.blocks) or not isinstance(model.blocks[i + 1].basis, type(blk.basis)):
            out[cur_stack] = cumulative.clone()
            cumulative = torch.zeros_like(cumulative)
            cur_stack = next_stack
    return out

contribs = stack_contributions(model, x_val[:1])

典型结果:在拥有 5 年历史的真实零售数据上,可解释 N-BEATS 在 12 月预测 horizon 上 MAPE 达 7%–12%,媲美最佳梯度提升+特征工程流水线,却省去繁重特征工程。真正的优势在于可解释性:一个允许业务方用已知促销事件覆盖 12 月季节性的模型,远比回测精度高 0.5% 的黑盒实用。


案例 2:小时级电力需求预测#

设定:基于过去 168 小时(一周)电网需求,预测未来 24 小时。数据具强日/周周期、天气敏感性,热浪期间出现尖峰。

架构选择:选用通用(Generic)变体。模式复杂(多分辨率日/周周期 + 天气驱动),且业务方首要关注精度——每兆瓦预测误差均增加运营备用成本。故采用更深通用栈与更宽隐状态。

1
2
3
4
5
model = make_generic(
    history=168, horizon=24,
    num_blocks=30, theta_size=32,
    hidden=512, layers=4,
)

逐窗口标准化在此尤为关键:需求水平随季节大幅波动;若不做标准化,模型会浪费容量学习“冬季高于夏季”这类常识。标准化后,网络只需专注形状学习。

1
2
3
4
5
6
7
8
def normalise_window(x: torch.Tensor) -> tuple:
    mu = x.mean(dim=-1, keepdim=True)
    sd = x.std(dim=-1, keepdim=True) + 1e-6
    return (x - mu) / sd, mu, sd

x_norm, mu, sd = normalise_window(x)
y_norm = model(x_norm)
y_hat = y_norm * sd + mu  # 广播到 (B, H)

典型结果:在公开 ETT(Electricity Transformer Temperature)小时级数据集上,10 个通用 N-BEATS 模型集成在 24 小时 horizon 上 MSE 约 0.31,显著优于 LSTM(~0.42),且实现成本远低于 Informer,精度却相当。


超参速查表#

新数据集的实用起点:

超参数默认(可解释)默认(通用)调参提示
回看 history预测长度 × 4 到 7预测长度 × 4 到 7越大覆盖越多季节;按 2 倍步长调整
隐藏宽度256512若验证损失高位停滞则增大
每块 MLP 层数44极少需改动
趋势阶数2 或 3趋势有曲率用 3,否则用 2
趋势块数3若趋势 RMSE 主导误差则增加
季节性块数3多分辨率季节性时增加
通用块数20 到 30增加通常仍有帮助,但收益递减
通用 $\theta$ 尺寸3216 易欠拟合,64+ 很少带来增益
损失函数MAE / sMAPEMAE / sMAPE与评估指标一致
优化器Adam, lr 1e-3Adam, lr 1e-3用余弦退火;无需 warmup
集成规模10–3010–30用中位数聚合

何时不该用 N-BEATS#

  • 超长预测 horizon(>1000 步):输出投影 $\theta \to \hat{y}$ 成为参数瓶颈。改用 N-HiTS(N-BEATS 后继者)或 PatchTST,二者通过多速率下采样扩展能力。
  • 强交互多变量时序:N-BEATS 本质为单变量模型。可为每变量单独建模,或尝试 TFT / Informer / DeepAR。
  • 极短序列(~50 观测点):MLP 主干含数万参数,在小数据上难敌 Theta 或 ETS。
  • 在线流式预测:N-BEATS 处理固定窗口,非流式隐藏状态。LSTM 或 TCN 更自然。
  • 预测需满足硬约束(非负、整数):N-BEATS 输出无约束实数;可后处理或用带裁剪的分位损失,但原生支持分布输出的模型(如 DeepAR)可能更简单。

常见问题#

为何选多项式与傅里叶基?#

多项式在紧区间上稠密于连续函数(Stone-Weierstrass 定理),傅里叶基则稠密于周期函数。二者结合构成“平滑趋势 + 周期成分”的强先验,契合多数真实序列。

三阶多项式趋势块能否学习高阶行为?#

局部可以,全局不行。每个块处理的是前块残差,故三层趋势块可组合三个三次函数,足以拟合合理预测范围内的任意趋势。

为何用 Adam 而非 SGD?#

实验表明 N-BEATS 用 Adam 收敛快得多。论文指出 SGD 需精细调参才能匹配 Adam 默认效果。

N-BEATS 需位置编码吗?#

不需要。MLP 将窗口视为固定向量,“位置”隐含于输入列索引,输出基矩阵已编码每步时间索引。

如何扩展以支持外生变量(天气、节假日)?#

官方扩展 N-BEATS-X:将外生协变量拼接至输入窗口,并为每块添加辅助输入头。实践中,直接拼接外生序列并略增首层 MLP 宽度通常已足够。

N-HiTS 是什么?是否应直接使用?#

N-HiTS(2023)出自同团队,通过输出端多速率下采样与插值,支持更长 horizon(720+ 步)且运行更快。但对短中期 horizon(<100 步),原版 N-BEATS 仍具竞争力且更简单。

为何必须集成?我的单模型效果很好。

你可能在此数据集上运气不错。论文显示,在 10 万条 M4 序列上单模型方差很大——即使仅集成 5 个模型,也能降低 1–2 sMAPE 点。若只需单点估计且数据量小,可跳过集成;但若需报告结果,强烈建议集成。


总结#

N-BEATS 是个“架构平淡无奇”的模型,却靠将合适模块按正确顺序堆叠而取胜。双重残差流赋予其 boosting 式行为;基函数输出头提供强归纳偏置,并在可解释变体中实现免费分解;M4 排行榜验证了该组合对经典方法与循环深度模型的双重优势。

对大多数采样规则、趋势/季节结构清晰的单变量预测问题,N-BEATS 是最强开箱即用基线之一。建议从可解释变体入手以争取业务认可,若需榨取极限精度,可切换至(或集成)通用变体,并记得在回看长度与随机种子上做集成。

下一章我们将以 Informer 收尾,它解决的是另一难题:如何让 Transformer 在千步级 horizon 预测中,避开 $\mathcal{O}(L^2)$ 注意力计算成本的拖累。


下一步#

N-BEATS 给我们的核心 lesson 是:在时间序列领域,架构设计比模型容量更重要。它没有用 RNN、没有用 attention、参数也不算特别多,仅凭"双重残差堆叠 + 基函数展开"这一个结构性想法,就在 M4 上击败了几十年精心调过的统计集成。这件事也告诉你,盲目堆 Transformer 不一定是对的——很多时序任务,对的归纳偏置(inductive bias)比模型容量更值钱。

不过 N-BEATS 也有它的边界:它本质是 MLP,对特别长的输入序列会有点吃力(参数量跟着 lookback 长度线性涨)。如果你的任务是"基于过去 720 步预测未来 168 步"——能源、气象、长程 IoT 这一类——N-BEATS 就开始力不从心了,这时候你需要专门为长序列设计的架构。

下一篇 Informer 正是为这个场景设计的。它用三个独立的技巧联合发力:ProbSparse 注意力把 O(n²) 降到 O(n log n)、编码器蒸馏让序列每层减半、生成式解码器一次预测整个 horizon。综合下来比朴素 Transformer 快 6-10 倍、精度还更好。这是本系列的收尾篇,我会同时给一份"该选哪个模型"的总览决策表,帮你在新项目上从这八种架构里挑出最合适的那个。

参考文献#

  • Oreshkin, B. N., Carpov, D., Chapados, N., & Bengio, Y. (2020). N-BEATS: Neural Basis Expansion Analysis for Interpretable Time Series Forecasting. ICLR.
  • Makridakis, S., Spiliotis, E., & Assimakopoulos, V. (2020). The M4 Competition: 100,000 Time Series and 61 Forecasting Methods. International Journal of Forecasting, 36(1).
  • Smyl, S. (2020). A Hybrid Method of Exponential Smoothing and Recurrent Neural Networks for Time Series Forecasting. International Journal of Forecasting, 36(1).
  • Challu, C. et al. (2023). N-HiTS: Neural Hierarchical Interpolation for Time Series Forecasting. AAAI.
  • Olivares, K. et al. (2023). Neural Basis Expansion Analysis with Exogenous Variables: Forecasting Electricity Prices with N-BEATS-X. International Journal of Forecasting.

本文是时间序列模型系列的第 7 篇,共 8 篇。

本系列

时间序列预测 8 篇

  1. 01 时间序列模型(一):传统统计模型
  2. 02 时间序列模型(二):LSTM——门控机制与长期依赖
  3. 03 时间序列模型(三):GRU——轻量门控与效率权衡
  4. 04 时间序列模型(四):Attention 机制——直接的长程依赖
  5. 05 时间序列模型(五):时间序列的 Transformer 架构
  6. 06 时间序列模型(六):时序卷积网络 (TCN)
  7. 07 时间序列模型(七):N-BEATS——可解释的深度架构 当前
  8. 08 时间序列模型(八):Informer——高效长序列预测

读有所得?

GitHub 关注我 → 新文周更

GitHub