halo 的技术博客

返回

Transformer 在 NLP 里封神之后,所有人都想把它搬到时间序列预测上。逻辑听着无懈可击:注意力机制能捕捉任意距离的依赖,价格序列不正是充满长程依赖吗?

结果尴尬了。2022 年一篇名字很欠揍的论文《Are Transformers Effective for Time Series Forecasting?》直接证明:一个只有一层的线性模型(DLinear),在多个主流预测基准上把当时最花哨的 Transformer 全都吊打了。 这盆冷水浇得整个领域很难看——难道注意力对时序根本没用?

问题不在 Transformer,而在怎么用。结论先放这:PatchTST(Nie et al., ICLR 2023)用两个极简的改动,让 Transformer 重新在时序预测上站稳脚跟——第一招把序列切成一段段「patch(补丁/分块)」当作 token,第二招是多变量之间「通道独立」。 本文用纯 numpy 复现”分块 vs 逐点”的核心机制,在趋势+双周期+噪声的自洽合成序列上,两种方法都把朴素基线的 MSE 从 51.6 压到 2.7,而分块相对逐点还有 2.3% 的额外增益。数字不大,但背后的道理很重要——下面讲清楚为什么。

PatchTST 核心:把长回看窗口切成一串重叠 patch,每块当一个 token 送进 Transformer

一、逐点建模:Transformer 用错地方了#

早期把 Transformer 用于时序,最自然的做法是把每个时间步当一个 token。回看 336 个交易日,就是 336 个 token,扔进自注意力算两两关系。

这有两个致命问题:

问题一:算力爆炸。 自注意力是 O(L2)O(L^2) 复杂度。L=336L=336 还行,但金融里想看更长的历史(一年 250 天、三年 750 天),L2L^2 直接把显存吃穿。

问题二,也是更根本的——单个时间步没有语义。 在 NLP 里,一个 token(一个词)本身携带完整语义,“苹果”就是”苹果”。但时序里单独一个价格点几乎不含信息:今天收盘 101 块,这个”101”孤立看毫无意义,有意义的是”过去两周从 95 涨到 101 的这段形态”。逐点建模强行让注意力去关联单个价格点和单个价格点,就像让你不看单词、只看一个个字母去理解句子——注意力被迫在毫无语义的原子上打转,自然学不出东西。

这就是为什么简单线性模型能赢:DLinear 直接把整段历史线性映射到未来,反而”整体地”用到了序列形态,绕过了逐点注意力的这个坑。

二、PatchTST 的两招#

招式一:Patching(分块)——给 token 注入局部语义#

PatchTST 的核心洞察:别把单个时间步当 token,把一段连续的子序列(patch)当 token。

具体做法:用一个长度为 PP 的窗口,以步长 SS 在序列上滑动,切出一串 patch。比如 L=336L=336P=16P=16S=8S=8,就切出 (33616)/8+1=41(336-16)/8+1 = 41 个 patch,每个 patch 是一段长 16 的子序列。

这一招同时解决了两个问题:

  • 语义有了。 每个 patch 是一段完整的局部形态(一个小趋势、一次回调),注意力现在关联的是”形态 vs 形态”,而不是”点 vs 点”。这正好对应技术分析里”看 K 线形态”而非”看单根收盘价”的直觉。
  • 算力降了。 token 数从 L=336L=336 降到 4141,自注意力的 O(L2)O(L^2) 直接缩了近 7070 倍。想看更长历史,成本也可控了。

招式二:Channel Independence(通道独立)#

金融预测常是多变量的:你想同时预测 100 只股票,或者用”开高低收量”5 个变量。传统做法是把多变量在每个时间步拼成一个大向量一起建模(channel-mixing)。

PatchTST 反其道而行:每个变量(通道)用完全相同的一套模型权重,各自独立预测,互不干扰。 100 只股票,就是同一个模型跑 100 遍。

这听着反直觉——不是应该让模型学变量间的相互作用吗?但实证发现通道独立更强,原因有二:一是共享权重 = 天然的数据增强,100 只股票的样本全用来训练同一套参数,等于样本量翻 100 倍,极大缓解过拟合;二是channel-mixing 容易学到虚假的跨变量相关,这些相关在样本外经常翻车。通道独立牺牲了显式的跨资产建模,换来了强得多的泛化。

三、纯 numpy 复现核心机制#

完整的 PatchTST 需要训练一个多层 Transformer,代码量大且训练慢。我们抓住它的灵魂——分块嵌入——用一个可复现的简化版来对比”分块 vs 逐点”:分块方法把序列切 patch、用共享的线性嵌入(这里用 patch 主成分作为固定嵌入基,保证稳定可复现)把每个 patch 压成低维向量,再拼接起来做预测;逐点方法则直接把整段展平做线性预测。两者都用岭回归收尾,公平对比。

先造自洽合成序列——趋势 + 两个周期(50 和 12)+ 噪声:

import numpy as np
rng = np.random.default_rng(20260722)

T = 2600
t = np.arange(T)
trend   = 0.02 * t
season1 = 6.0 * np.sin(2*np.pi*t/50)          # 中周期 50
season2 = 3.0 * np.sin(2*np.pi*t/12 + 0.7)    # 短周期 12
noise   = rng.normal(0, 1.5, T)
series  = 50 + trend + season1 + season2 + noise

L, H, P, S = 336, 48, 16, 8                    # 回看 / 预测 / patch长 / 步长
n_patch = (L - P) // S + 1                     # = 41
python

滑窗构造样本,并做实例归一化(RevIN 的核心思想:每个窗口减去自己的均值,预测完再加回来,专治分布漂移):

def make_windows(x, L, H):
    X, Y = [], []
    for i in range(L, len(x) - H + 1):
        X.append(x[i-L:i]); Y.append(x[i:i+H])
    return np.array(X), np.array(Y)

X, Y = make_windows(series, L, H)
ntr = int(len(X) * 0.7)
Xtr, Ytr, Xte, Yte = X[:ntr], Y[:ntr], X[ntr:], Y[ntr:]

mu_tr = Xtr.mean(1, keepdims=True); mu_te = Xte.mean(1, keepdims=True)
Xtr_n, Ytr_n = Xtr - mu_tr, Ytr - mu_tr        # 实例归一化
Xte_n, Yte_n = Xte - mu_te, Yte - mu_te
python

方法 A——逐点线性,把整段 LL 展平直接映射到 HH

def ridge_fit(A, B, lam):
    d = A.shape[1]
    return np.linalg.solve(A.T @ A + lam*np.eye(d), A.T @ B)

Wp = ridge_fit(Xtr_n, Ytr_n, lam=5.0)
pred_point = Xte_n @ Wp + mu_te
python

方法 B——PatchTST 思想:切 patch → 每个 patch 过共享嵌入(主成分基)→ 拼接 → 预测:

四、结果:都碾压朴素,分块小幅领先#

序列长度 2600 | 回看 L=336 | 预测 H=48 | patch长 P=16 步长 S=8 | patch数 41
[MSE] 朴素持平=51.595 | 逐点线性=2.701 | PatchTST分块=2.639
[MAE] 朴素持平=5.869  | 逐点线性=1.322 | PatchTST分块=1.312
分块相对逐点 MSE 降幅: 2.3%
plaintext

诚实地读这三行:

两种正经方法都把朴素基线打成了渣。 朴素持平(用最后一个值当未来 48 步的预测)MSE 51.6,逐点线性和分块都在 2.7 左右——降了近 20 倍。这说明这份合成序列的趋势和周期结构非常”可学”,任何用了整段历史的方法都能吃到。

分块相对逐点只领先 2.3%。 这个数字我必须如实说:在这么干净、这么规整的合成数据上,patching 的优势很小。为什么?因为逐点线性在如此低噪、结构如此清晰的序列上已经接近上限,没给分块留多少提升空间。别被”降幅 2.3%“这种个位数吓到就否定 PatchTST——它的真正价值不在这种玩具数据上,见第五节。

单样本 48 步预测:分块(红)和逐点(蓝)都紧贴真实(黑),朴素持平(灰)早已跑飞

从单样本预测看,分块和逐点几乎重叠,都很好地还原了周期波动;而朴素持平从第一步就成了一条平线,完全接不住结构。

RMSE 随预测步长增长:分块在长步长段衰减更慢,优势主要体现在远端

更有意思的是误差随步长的曲线:分块和逐点在短步长(1-10 步)上几乎没差别,分块的优势主要出现在长步长(30-48 步)的远端——这恰好印证了 PatchTST 的设计初衷:它是为长程预测设计的,短程预测本来就不是它的主战场。

五、这份数据的 2.3% 之外,PatchTST 真正的价值#

玩具数据低估了 PatchTST。它在真实长序列预测里的价值体现在三处,是本文简化版没法完全展现的:

  • 算力可扩展。 真实场景想看一年、三年的历史,逐点注意力的 O(L2)O(L^2) 直接崩溃,而 patching 把 token 数砍到 L/SL/S,才让”超长回看窗口”在计算上可行。序列越长,分块的相对优势越大。
  • 通道独立带来的泛化。 本文只做了单变量,没体现第二招。在几百个变量的真实多资产预测里,通道独立的共享权重 = 数百倍数据增强,这才是 PatchTST 拉开身位的地方。
  • 对噪声更鲁棒。 patch 内部相当于做了一次局部平滑,比逐点建模更抗单点噪声。合成数据噪声低,这个优势没显出来;真实金融数据噪声大,差距会拉开。

六、六类真实陷阱#

把 PatchTST 用到真实交易上,这六个坑一个都躲不开:

陷阱一:金融时序的信噪比远低于本文合成数据。 这是最根本的落差。合成序列的趋势和周期是”真的”,模型学到就能预测。真实价格接近随机游走,可预测的成分极其微弱——别指望在真实收益率上复现 20 倍于朴素基线的 MSE 降幅,能稳定跑赢朴素持平就已经很了不起。

陷阱二:非平稳与 regime 切换。 合成序列的周期是恒定的 50 和 12,真实市场的”周期”随时会变、会消失。实例归一化(RevIN)能缓解均值漂移,但对结构性 regime 切换(牛熊转换、波动率跳变)无能为力。训练集学到的模式,测试集可能整个失效。

陷阱三:patch 长度和步长是敏感超参。 PP 太短,patch 退化回逐点、失去语义;PP 太长,token 太少、丢失时间分辨率。SS 决定重叠程度。这几个参数要在验证集上调,而金融数据上”调参”极易过拟合——你很容易调出一组只在历史上好看的 (P,S)(P, S)

陷阱四:预测价格 ≠ 能赚钱。 本文预测的是价格水平(含强趋势),MSE 好看是因为趋势好预测。但趋势的收益已经被 buy-and-hold 拿走了,真正难的是预测去趋势后的收益率——那才是 alpha 所在,也是信噪比最低的部分。别用”预测价格 MSE 低”来论证策略能赚钱,这是深度学习选股里最常见的自欺。

陷阱五:前视偏差藏在归一化里。 实例归一化只用回看窗口内的均值,这是安全的。但如果你图省事用了全序列的均值/方差做标准化,就偷看了未来——这是时序深度学习里最隐蔽的 look-ahead,回测会莫名其妙地好。

陷阱六:本文是简化版,不是完整 PatchTST。 我用主成分嵌入 + 岭回归替代了 Transformer 编码器 + 多头自注意力,为的是可复现、讲清分块的思想。真实 PatchTST 的自注意力能建模 patch 之间的长程依赖,表达力远强于此。别拿本文的 2.3% 当 PatchTST 的真实上限——那只是”分块这个动作本身”在玩具数据上的边际贡献。

七、总结#

PatchTST 的故事,本质是一个”用对工具”的故事。Transformer 不是对时序无效,而是早期被用错了粒度——让注意力去关联毫无语义的单个价格点。PatchTST 只做了一件真正重要的事:把 token 的粒度从”点”提升到”段”,让注意力终于有了值得关注的东西。

在本文的合成数据上,分块相对逐点只领先 2.3%,因为数据太干净、逐点已接近上限。但那个”误差在长步长上衰减更慢”的曲线,那个”token 数砍到 1/8”的算力账,才是 PatchTST 在真实长程、多变量预测里真正值钱的地方。

给量化实践者的建议:如果你在做多资产、长回看窗口的预测,PatchTST 是当前性价比很高的基线,务必开启通道独立。但请把陷阱四刻在脑子里——预测价格水平的漂亮 MSE,和策略能不能赚钱,是两码事。 先在去趋势收益率上验证它有没有真 alpha,再谈上不上仓位。

PatchTST 时序分块预测:把价格切成一块块「补丁」,Transformer 才真正用得上
https://blog.halo26812.eu.org/blog/patchtst-time-series
Author halo
Published at 2026年7月22日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨