halo 的技术博客

返回

2019 年,M4 时间序列竞赛的冠军交给了一个看起来「不像深度学习」的模型:N-BEATS(Neural Basis Expansion Analysis for Interpretable Time Series Forecasting,Oreshkin 等,2019)。

它没有卷积、没有注意力、没有 RNN、没有 encoder-decoder。整个网络就是一堆残差块叠起来,却把当时所有的统计与时序深度学习模型(包括各种 RNN、WaveNet、Transformer 前身)都按在地上摩擦。

它是怎么做到的?核心就一句话:用一组可学习的「基函数」把时间序列展开,再用「栈 + 残差」的方式层层剥离信号。 本文用一套自洽的合成模型把 N-BEATS 的核心机制复现出来,回答四件事:

  1. N-BEATS 的「栈 + 残差」到底在算什么;
  2. 趋势栈与季节栈如何各自拟合一块信号;
  3. 多步外推到底准不准(和朴素基线比);
  4. 为什么它在一步预测上其实打不过随机游走——以及这为什么不是 bug。

一、核心机制:栈 + 残差 + 基展开#

传统深度学习时序模型喜欢堆注意力或者 RNN。N-BEATS 反其道而行:它把网络切成若干个栈(stack),每个栈由若干个残差块(block) 组成。每个 block 做两件事:

  • backcast:用一组基函数拟合「已经知道的历史段」,把历史解释掉;
  • forecast:用同样的(或另一组)基函数外推「还不知道的未来段」。

然后关键的一步是残差传递:历史段没被解释干净的部分(残差),连同 forecast 一起,传给下一个栈。下一个栈只在「上一个栈没搞定的残差」上继续工作。

yt=s=1Sgs(residuals)y_t = \sum_{s=1}^{S} g_s(\text{residual}_s)

其中 gsg_s 是第 ss 个栈学到的一组基函数展开。直觉上,第一个栈可能学会了「大趋势」,第二个栈在残差上学会了「季节」,第三个栈在更细的残差上学会了「高频噪声」——层层分解,互不干扰。

论文还专门设计了可解释变体(interpretable N-BEATS):不让网络随便学基函数,而是强制趋势栈用多项式基、季节栈用傅里叶谐波基。这样每个栈的输出都有明确的业务含义。这就是本文要复现的版本。

二、可解释分解:趋势栈 + 季节栈#

在动手之前,先想清楚 N-BEATS 为什么「长这样」。传统做法把时序预测当黑盒回归:喂一段历史、吐一段未来,中间发生了什么你不知道。N-BEATS 的聪明之处在于,它把「可解释」做成了结构约束而不是事后解释。

可解释变体(interpretable N-BEATS)干脆规定:趋势栈的基函数只能是多项式、季节栈的基函数只能是傅里叶谐波。这样一来,每个栈的输出在物理上就对应「趋势分量」和「季节分量」,人能直接读懂。这比事后跑 SHAP、注意力热力图去「猜」黑盒在想什么,要干净得多——结构本身就是解释。

我们用一个合成序列演示。它由一个慢季节(周期 120)、一个快季节(周期 24/7)和极轻的线性漂移叠加而成,再加上高斯噪声:

yt=12sin(2πt/120)+5sin(2πt/24)+2.5sin(2πt/7)+0.008t+εty_t = 12\sin(2\pi t/120) + 5\sin(2\pi t/24) + 2.5\sin(2\pi t/7) + 0.008t + \varepsilon_t

N-BEATS 的「趋势栈」用多项式基 1,t,t21,t,t^2(归一化到 [0,1][0,1],避免外推爆炸)拟合整段;「季节栈」用一组傅里叶谐波基(候选周期库 7/12/24/30/60/90/120,模型自己从中挑)拟合趋势栈留下的残差。两个栈相加,就还原了信号。

跑出来,趋势栈几乎只抓住那条轻漂移(训练段解释力 R²≈0.005,因为趋势成分本来就小),而季节栈漂亮地还原了所有周期性波动,残差只剩白噪声:残差标准差 1.073,干净到可以直接拿来做预测区间。

N-BEATS 可解释分解:趋势栈与季节栈叠加还原信号

三、多步预测:N-BEATS 到底比基线强在哪#

拿到学好的基系数,我们把它们外推到训练段之后的 100 步(测试段),和三类基线比:

  • 季节朴素(周期 24):直接复制上一周期的同期值;
  • 上一值(随机游走):预测等于最近一个观测;
  • 线性漂移:用最近斜率外推。

跑出结果:

  • N-BEATS RMSE = 2.124
  • 季节朴素(周期 24)= 10.955(差 5.2 倍)
  • 线性漂移 = 25.957(差 12.2 倍)
  • 上一值(随机游走)= 1.936

结论先放这:N-BEATS 对「不看结构的基线」是碾压级优势——季节朴素法完全漏掉了周期 120 的慢季节,线性漂移法在周期面前崩溃。但注意「上一值」随机游走只差一点点。我们后面会专门说为什么。

多步外推(测试段 100 步):N-BEATS 贴合趋势+多季节

四、逐步长诊断:N-BEATS 的优势在中长步长#

把 RMSE 拆成每个预测步长 hh 看,故事更清楚:

预测步长 hhN-BEATS上一值季节朴素(24)
11.3383.3630.011
51.4542.4393.135
101.5031.9974.829
201.8061.8907.282
502.1511.99811.427
1002.1241.93610.955

h5h\ge 5 开始,N-BEATS 严格优于随机游走;越往长步长,季节朴素法越被甩开。N-BEATS 的 MASE(以随机游走为 1)约为 1.32——平均值被 h=14h=1\sim4 的极短步长拖累,但在多步外推上它是赢家。

误差随预测步长:固定周期朴素法被甩开,N-BEATS 全程最低

五、基函数可视化:N-BEATS 的「积木」#

理解 N-BEATS 最直观的方式,就是看它的基函数长什么样。趋势栈是一组多项式,季节栈是一组傅里叶谐波。网络训练的本质,就是决定每个基要用多大力、相位怎么摆

N-BEATS 的「基函数」:每个栈用一组基把信号展开

可解释变体强就强在这儿:它不让你随便拟合,而是把先验(趋势该是多项式、季节该是谐波)写进结构。这既提升了外推稳定性,也让每个栈的输出可以被人读懂——比起黑盒 LSTM,这是 N-BEATS 在实务里被偏爱的原因。

六、真实陷阱(别把 N-BEATS 当万能锤)#

1. 一步预测打不过随机游走,是正常的。 在平滑、低噪声、短期自相关强的序列上,最简单的「上一值」基线极难被超越。N-BEATS 的真正优势在多步外推结构外推力(它学到了周期,能正确预测未来第 50 步),而不是在 h=1h=1 上吊打 RW。拿 MASE<1 当硬性成功标准会误判它。

2. 多项式趋势基外推会爆炸。 本文只用了一阶趋势(线性)。如果用 t2t^2 甚至更高阶,训练段拟合变好,但外推到未来会指数级发散——这正是真实 N-BEATS 用「低阶 + 归一化」约束的原因。高阶趋势基是回测里最隐蔽的过拟合陷阱。

3. 谐波周期库是人工先验。 本文把候选周期 7/12/24/30/60/90/120 写死进基。真实金融数据没有「刚好 24」的周期,得靠领域知识(日内/周内/月度)或数据驱动选周期。基没覆盖到的周期,模型永远学不到。

4. 残差假设是白噪声。 95% 预测区间用残差标准差算,等价于「残差独立同分布正态」。但金融残差有波动聚集(GARCH 效应)、有厚尾、有跳变。真实区间应换成异方差 + 学生 t 假设,否则会系统性低估尾部风险。

5. 训练段/测试段分布要一致。 N-BEATS 在平稳周期上表现好。一旦遇上 regime 切换(波动率突然从 12 跳到 40、周期结构断裂),backcast 学到的一切在外推时失效。实务里要加「滚动重训」或「分布漂移检测」,不是训一次用一年。

6. 合成 ≠ 实盘。 本文序列是确定性周期 + 高斯噪声,干净得不像话。实盘价格有买卖价差、停牌、跳空、非同步交易。模型在合成数据上 5 倍优于朴素法,不保证实盘有同样 gap——落地前必须用真实 tick/日线重测,并把交易成本算进回测。

七、小结#

  • N-BEATS 是「无卷积、无注意力、无 RNN」的纯残差网络,靠栈 + 残差 + 可学习基展开在 M4 夺冠;
  • 可解释变体把趋势栈(多项式基)和季节栈(傅里叶谐波基)拆开,每个栈输出可读;
  • 多步外推碾压无结构基线:RMSE 2.124 vs 季节朴素 10.955(5.2×)、线性漂移 25.957(12.2×),从 h5h\ge5 起严格优于随机游走;
  • 但它一步预测打不过随机游走(MASE≈1.32)是真属性不是 bug——优势在多步与结构外推;
  • 六条陷阱牢记:RW 难超、多项式外推爆炸、周期库是先验、残差非白噪声、regime 断裂、合成≠实盘。

附:本文所有图表与数值均来自上方可运行 Python(合成周期序列 + 趋势/季节双栈岭回归基展开 + 多步外推 + RMSE 对比),参数与结果一致,可直接复现。

N-BEATS 神经基展开:无卷积无注意力的纯残差时序预测
https://blog.halo26812.eu.org/blog/nbeats-forecast
Author halo
Published at 2026年7月16日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨