StemGNN:在频域里同时建模"序列间"和"序列内"的多变量时序预测

  • 关联论文:2103.07719
  • 作者:flyP
  • 更新:2026-10-24(Jay 第二读者精修:事实核查 + 工程落地节)

一句话结论

StemGNN 把多变量时序预测的整条链路搬进频谱域——先用 Graph Fourier Transform(GFT)把"哪些序列相关"这件事变成可学习的邻接矩阵,再用 Discrete Fourier Transform(DFT)把每条序列的时间模式变成频域里的卷积问题,两个正交变换串在同一个 block 里端到端训练;代价是每个 block 都要做一次 Laplacian 特征分解,$O(N^3)$ 的复杂度把"大节点数"直接挡在门外。

【Jay 修正】 最后半句是论文口径,不是官方代码口径。论文附录 F.1 自述 GFT 走特征分解、整体复杂度 $O(N^3)$;但 microsoft/StemGNN 官方实现(master HEAD dc7dea6,2022-11-28)全仓没有任何 eigendecomposition:graph_fft / get_laplacian 定义了但从未被调用,torch.rfft / torch.irfft 只出现在 models/base_model.py:49 与 :58。代码走的是 4 阶 Chebyshev 多项式滤波,O(N^3) 的来源变成每次 forward 里两次 N×N 稠密矩阵乘(算 $L^2$、$L^3$)。详见 ## 工程落地与核查(Jay)。

解决什么真问题

多变量时序预测难在哪?难在要同时处理两种完全不同的相关性:

  • intra-series(序列内):一条序列自己跟自己的时间相关性,比如早晚高峰、日周期、季节性;
  • inter-series(序列间):不同序列之间的横向关联,比如相邻路段的车流、下游电站的负载、两国之间的疫情传播。

论文对 2020 年前后的主流做法做了一个很锋利的归类:大多数方法只在时域建模时间相关性,而序列间关系靠"预先定义"(pre-defined prior)塞进去。DCRNN、ST-GCN、GraphWaveNet 这类 GNN 方法是典型——它们把路网拓扑当输入喂给 GCN,然后接一个 LSTM/GRU/TCN 处理时间维度。问题在于:

  1. 时域和图域是拼起来的,不是统一建模的。堆叠式结构(stacking)意味着"图卷积看到的"和"时间模块看到的"是两套表征,中间靠层与层的接口对齐,而不是数学上的统一;
  2. 必须有先验图。没有路网拓扑的场景——电力、太阳能、心电、疫情跨国传播——直接套不上;而有路网的场景,人工定义的边权又未必是任务相关的(一条路在空间上相邻,不代表它的流量在预测任务上强相关);
  3. GFT 的输出本身仍然是多变量时序。这是论文洞察里最锋利的一句:GFT 把结构信息编码进去了,但它没有显式学到序列内的时间关系,所以"时间"这一半还得另开一套模块来补。

StemGNN 的回答是:既然 GFT 和 DFT 各自有一半本事,为什么不在频域里把两者合起来?GFT 处理序列间(把 N 条相互纠缠的序列正交分解成 N 条线性无关的成分),DFT 处理序列内(把每条成分在时间上分解成不同频率),两者串联后,谱域表征的"模式更清晰",卷积和序列学习模块就能直接吃。

论文原文对这一点的措辞是:"To the best of our knowledge, StemGNN is the first work that represents both intra-series and inter-series correlations jointly in the spectral domain."

核心方法

1. 问题形式化:多变量时序图

论文把任务写成"多变量时序图" $\mathcal{G}=(X,W)$,$X\in\mathbb{R}^{N\times T}$(N 条序列,T 个时间戳),$W\in\mathbb{R}^{N\times N}$ 是邻接矩阵。给定前 K 步 ${X_{t-K},\dots,X_{t-1}}$,预测未来 H 步:

$$\hat{X}t,\dots,\hat{X}{t+H-1} = F(X_{t-K},\dots,X_{t-1}; \mathcal{G}; \Phi)$$

关键在于 $\mathcal{G}$ 既可以作为先验输入,也可以从数据里自动推断——后面这个自由度是 StemGNN 相对 DCRNN/ST-GCN 的核心差异。

2. Latent Correlation Layer:让图自己长出来

这是把"不要先验"落地的部件。做法很直接:

# 伪代码:Latent Correlation Layer
H = GRU(X)              # X: N×T,逐时刻算隐状态
R = H[-1]               # 取最后一个隐状态作为整段序列的表示,维度 N×d
Q = R @ W_Q             # 可学习投影
K = R @ W_K
W = softmax(Q @ K.T / sqrt(d))   # N×N,直接当邻接矩阵用

注意 R 是 N×d(每条序列一个表示),W 是 N×N(序列两两之间的关联强度)。也就是说,注意力矩阵在这里不是"做聚合",而是直接就是图的邻接矩阵。自注意力整体复杂度 $O(N^2d)$。

这带来一个很反直觉但论文用实验证了的结论:学出来的图比人画的图更好。在 PEMS-BAY 的案例分析里,探测器 $s_1$ 位于主干道交叉口,其列的相关性明显偏高;$s_{25}$ 在单条路上,相关性就弱。COVID-19 上更明显:US/Canada/Mexico 高度相关,China/Japan/Korea 高度相关,地理邻接被模型自己学出来了。

【Jay 修正】 官方代码里的 Latent Correlation Layer 与上面这段伪代码不是同一个东西,落地前必须知道: - 不是点积注意力:代码里 weight_key / weight_query 形状是 (unit, 1),logits = leakyrelu(key_i + query_j),是加性形式、无点积、无 $1/\sqrt{d}$,等价于 d=1; - GRU 沿节点轴迭代:nn.GRU(self.time_step, self.unit),unit 被设成 node_cnt,输入经 x.permute(2,0,1) 后序列维是节点,不是时间;代码用的是 GRU 的全部输出,不是论文所述的最后隐状态 $R$; - 一个 batch 共享一张图:attention = torch.mean(attention, dim=0),对 batch 求均值; - softmax 之后还有 dropout:self.dropout(attention),默认 p=0.5(训练时),意味着"学出来的邻接矩阵"每次 forward 有一半边被随机置零; - 论文的 "attention dimension 32" 在代码里没有对应超参。

另外按代码形状推断(未在论文中说明、属【Jay 推测】):注意力矩阵的行/列下标来自 GRU 的隐状态通道(unit = node_cnt),而 Laplacian 作用在节点轴上;两者维度恰好都等于节点数所以能对上,但语义并不同源。

3. StemGNN Block:GFT → DFT → 频域卷积 → IGFT

Block 内部的算子顺序是全文的技术核心:

输入 X (N×T)
  │
  ├─► GFT:  X̂ = Uᵀ X          # U 是归一化 Laplacian 的特征向量矩阵
  │         → N 条线性无关的单变量序列
  │
  ├─► DFT:  每个分量转到频域
  │         → X̂_u = X̂_u^r + i·X̂_u^i
  │
  ├─► Spe-Seq Cell(频域里的 1D Conv + GLU)
  │         M*(X̂_u*) = θ*_τ(X̂_u*) ⊙ σ*(θ*_τ(X̂_u*)),  * ∈ {r, i}
  │
  ├─► IDFT: 回到时域
  │
  ├─► 图卷积:  g_Θ(Λ) 作用在谱域
  │
  └─► IGFT:  X̂ → U X̂  回到原空间

几点值得抠一下:

(a)Spe-Seq Cell 的 GLU 门控意义。 公式里 GLU 写成 $\theta^_\tau(\hat{X}_u^) \odot \sigma^(\theta^_\tau(\hat{X}_u^))$,注意两个分支用的是同一组卷积核参数、只靠 sigmoid 门控区分。论文的说法是"非线性 sigmoid 门控 $\sigma^$ 决定当前输入有多少信息与序列模式紧密相关"。在频域里做门控的直觉是:不同频率成分对预测的贡献差异极大(低频管趋势、高频管噪声),门控让网络自己学会"这条频率我信多少"。

【Jay 修正】 官方代码的 GLU 是 nn.Linear 实现的两组独立权重:GLU.forward = linear_left(x) * sigmoid(linear_right(x))。也就是"值"和"门"用不同参数,与论文公式里"同一组 $\theta^$ 用两次"的写法不同;而且代码里根本没有 nn.Conv1d*,论文的 "1D convolution, kernel size 3" 在实现层不存在。

(b)实虚部并行处理。 DFT 的输出是复数,论文把实部 $\hat{X}_u^r$ 和虚部 $\hat{X}_u^i$ 用相同结构但不同参数的两条并行分支处理,最后合成 $M^r(\hat{x}_u^r) + i M^i(\hat{x}_u^i)$ 再做 IDFT。(代码侧一致:GLUs[i*2] 给实部、GLUs[2i+1] 给虚部,参数独立。)

(c)GFT 用满 N 个特征向量,不做降维。 原文明确写"the number of eigenvectors used in GFT is equivalent to the multivariate dimension (N) without dimension reduction"。这是后面 $O(N^3)$ 瓶颈的直接来源——你没法用"取前 k 个低频分量"来省,因为省了就丢掉信息。

(d)双输出:forecasting + backcasting。 借鉴 N-BEATS 的 basis expansion($Y = V\theta$,V 是可学习的基向量,$\theta$ 由 FC 层从 $Z$ 算出),block 有两个分支:forecasting 分支出未来预测,backcasting 分支出历史重构。损失函数:

$$\mathcal{L}(\hat{X},X;\Delta_\theta) = \sum_{t=0}^{T}|\hat{X}t - X_t|_2^2 + \sum{t=K}^{T}\sum_{i=1}^{K}|B_{t-i}(X) - X_{t-i}|_2^2$$

backcasting 项的作用,论文说是"regulate the functional space for the block to represent time-series data"——用自编码的方式逼编码器学出有表达力的表征。

【Jay 修正】 官方代码只优化 forecasting 项:models/handler.py 里训练损失只有 forecast_loss(forecast, target)(MSE),backcast 分支没有进入损失(GitHub issue #18 就是在问"Where is the backcast loss?")。且代码里第二个 block(stack_cnt==1)没有 backcast 分支,两个 block 的 forecasting 输出是相加(forecast = result[0] + result[1])而不是论文写的"拼接后送入 GLU + FC";输出头 self.fc 是 Linear → LeakyReLU → Linear,无 GLU。

(e)多通道扩展。 论文给出通用形式:

$$Z_j = \mathcal{GF}^{-1}\left(\sum_i g_{\Theta_{ij}}(\Lambda_i)\,\mathcal{S}(\mathcal{GF}(X_i))\right)$$

对每个输入通道 $X_i$ 做 GFT + Spe-Seq Cell,用核 $\Theta_{\cdot j}$ 加权求和,最后 IGFT 得到输出通道 $Z_j$,再把各 $Z_j$ 拼接成 $Z$。

(f)推理用 rolling 策略。 不是一次性输出 H 步,而是把预测值 $\hat{X}_t$ 塞回输入窗口,滚动推出后续 H 步。这意味着长程预测会有误差累积。

【Jay 修正】 这是论文口径;官方代码等价于 direct 多步:输出头 self.fc 的最后一层直接出 horizon 步,handler.inference 的滚动循环体对任意 horizon 只执行一轮(len_model_output == horizon,step += min(horizon-step, len_model_output) 一次跳满),并没有把预测值回灌窗口。落地时如果按"论文=rolling"预留误差累积预算,会与代码行为对不上。

4. 整体结构

X ──► Latent Correlation Layer ──► W (N×N)
                                    │
                                    ▼
        StemGNN Block ×2 (残差连接) ──► 输出层 (GLU + FC)
                                    │
                    ┌───────────────┴───────────────┐
                    ▼                               ▼
            Forecasting (Y)                   Backcasting (X̂)

两个 block 用残差连接堆叠,第二个 block 逼近的是"真值 − 第一个 block 重构值"的残差,最终两个 block 的输出拼接后送进 GLU + FC。

【Jay 修正】 代码里第二个 block 的输入是第一个 block 的 backcast_source(重构值本身),没有减去真值再喂回去;"残差"语义只靠 backcast 承担,且该分支不参与损失。

关键实验与数据

数据集

论文说 "ten real-world datasets"(abstract)而正文实验部分是 "nine public datasets",加 COVID-19 案例共 10 个(Table 1 列了 10 个):

【Jay 修正】 这句归属写错了。逐句核对原文:abstract 写 "nine public benchmarks",§5.1 写 "nine public datasets",Appendix B.1 才写 "ten public datasets"(明确含 COVID-19)。所以口径差是 "9 个 benchmark(§5.1/abstract) vs 10 个数据集(附录 B.1 含 COVID-19 案例)",而不是"abstract 说 ten"。

数据集 节点数 时间步 粒度 起始时间
METR-LA 207 34,272 5min 2018-09-01
PEMS-BAY 325 52,116 5min 2018-01-01
PEMS07 228 12,672 5min 2016-07-01
PEMS03 358 26,209 5min 2012-05-01
PEMS04 307 16,992 5min 2017-07-01
PEMS08 170 17,856 5min 2012-03-01
Solar 137 52,560 10min 2006-01-01
Electricity 321 26,304 1hour 2012-01-01
ECG5000 140 5,000 — —
COVID-19 25 110 1day 2020-01-22

只有交通数据集带先验拓扑,这是 StemGNN 宣称"不需要先验图"能被验证的前提(Appendix B.1 原话:"Among all the datasets, only the datasets from traffic domain provide apriori topology.")。

主结果(Table 2,摘录 MAE)

数据集 GraphWaveNet DeepGLO StemGNN 最优基线 MAE
METR-LA 2.69 2.91 2.56 2.69 (GraphWaveNet)
PEMS-BAY 1.30 1.39 1.23 1.30
PEMS07 2.25 (ST-GCN) 3.01 2.14 2.25
PEMS03 15.59 (DeepState) 17.25 14.32 15.59
PEMS04 20.21 (DeepState) 23.25 21.64 20.21
PEMS08 15.12 (DeepGLO) 15.12 15.83 15.12 ⚠️
Solar 0.05 (SFM/GraphWaveNet) 0.09 0.03 0.05
Electricity 0.071 (GraphWaveNet) 0.08 0.04 0.071
ECG5000 0.08 (N-BEATS) 0.09 0.05 0.08

【Jay 修正】 上表的 GraphWaveNet 列与"最优基线"列整表错位,且 PEMS04 行是拿 PEMS03 的 RMSE 数字填的。逐格回查论文 Table 2 后,正确取值如下(原表保留在上一行对照,正表以此为准):

数据集 GraphWaveNet MAE DeepGLO MAE StemGNN (MAE/RMSE/MAPE) 最优基线 MAE(谁)
METR-LA 2.69 2.91 2.56 / 5.06 / 6.46 2.69 (GraphWaveNet)
PEMS-BAY 1.30 1.39 1.23 / 2.48 / 2.63 1.30 (GraphWaveNet)
PEMS07 –(表中未报) 3.01 2.14 / 4.01 / 5.01 2.25 (DCRNN & ST-GCN)
PEMS03 19.85 17.25 14.32 / 21.64 / 16.24 15.59 (DeepState)
PEMS04 26.85 25.45 20.24 / 32.15 / 10.03 22.70 (ST-GCN)
PEMS08 19.13 15.12 15.83 / 24.93 / 9.26 15.12 (DeepGLO)
Solar 0.05 0.09 0.03 / 0.07 / 11.55 0.05 (SFM & GraphWaveNet)
Electricity 0.071 0.08 0.04 / 0.06 / 14.77 0.06 (LSTNet)
ECG5000 0.19 0.09 0.05 / 0.07 / 10.58 0.08 (N-BEATS & LSTNet)

关键连带修正: - PEMS04 上 StemGNN 的 MAE 其实是最优(20.24 < 最优基线 22.70),原表写的 "21.64 vs 20.21" 是把 PEMS03 的 RMSE 列错搬过来了; - Electricity 的最优基线是 LSTNet 0.06,不是 GraphWaveNet 0.071; - ECG5000 的 GraphWaveNet MAE 是 0.19(不是 0.08),最优基线 0.08 由 N-BEATS 与 LSTNet 并列; - PEMS08 的 GraphWaveNet 是 19.13,"15.12" 是 DeepGLO 的值;PEMS07 的 GraphWaveNet 一栏论文原表就是 "-"。

论文的汇总口径是:"on all datasets, StemGNN improves an average of 8.1% on MAE and 13.3% on RMSE compared to the best baseline for each dataset"(引用时保持这个措辞,原文是 "an 13.3%" 的笔误)。

【Jay 修正】 这个 8.1% / 13.3% 按常见口径复算不出来,引用时请标注"论文自述、聚合口径未公开"。用上表逐数据集取"每数据集最优基线"复算: - 逐数据集百分比取均值:MAE 15.58%、RMSE 5.32%、MAPE 7.65%; - 总和比(各数据集求和后相比):MAE 5.68%、RMSE 0.15%; - 若把 COVID-19 28 天一并计入(该表只给 MAPE%,且与其他数据集不同量纲),逐数据集均值变成 MAE 16.52% / RMSE 7.21% / MAPE 7.42%。

三种口径都对不上 8.1% / 13.3%。最接近的是 MAPE 逐数据集均值 7.65%,但论文原文把 8.1% 标为 MAE、13.3% 标为 RMSE。结论:论文没有公开聚合口径,无法复现(这是【Jay 待核】项,非"数字错误")。

⚠️ 必须点出的负面事实:PEMS08 上 StemGNN 的 MAE 是 15.83,而 DeepGLO 是 15.12、DeepState 是 19.34——StemGNN 在 PEMS08 上不是最优。论文正文对此的解释是 DeepGLO "shows competitive performances on some datasets like solar and PEMS08"(事实上 solar 上 DeepGLO 也不是最优,StemGNN 0.03 vs DeepGLO 0.09)。这个数据集上的失利在 abstract 里被抹平了。

【Jay 修正·补充负面事实】 StemGNN 不止 PEMS08 一处不占优。逐指标回查 Table 2,落后单元格至少还有: - PEMS03 RMSE:StemGNN 21.64 vs DeepState 20.21(落后 7.1%); - Solar RMSE:StemGNN 0.07 vs TCN 0.06(落后 16.7%); - PEMS08 MAE:15.83 vs DeepGLO 15.12(落后 4.7%)。

即"9 个数据集 × 3 个指标"共 27 个单元格里,StemGNN 有 3 个不是最优;abstract 里的 "state-of-the-art performances on nine public benchmarks" 掩盖了这 3 格。

COVID-19 案例(Table 4,MAPE%)

25 个国家,2020-01-22 至 2020-05-10 共 110 天,前 60 天训练、后 50 天测试:

预测跨度 FC-LSTM N-BEATS DeepGLO GraphWaveNet StemGNN
7 Day 20.3 16.5 17.1 18.9 15.5
14 Day 22.9 18.5 18.9 24.4 17.1
28 Day 27.4 20.4 23.1 25.2 19.3

值得注意的是预测跨度越长,StemGNN 相对次优的领先幅度在缩小(7 天领先 N-BEATS 1.0 个点,28 天只领先 0.9 个点),这和 rolling 策略的误差累积直觉一致。

【Jay 修正】 "28 天只领先 0.9 个点"和"幅度在缩小"两处都错。Table 4 逐行回查:各行次优都是 N-BEATS,差距为 - 7 Day:16.5 − 15.5 = 1.0 点(相对 6.06%) - 14 Day:18.5 − 17.1 = 1.4 点(相对 7.57%) - 28 Day:20.4 − 19.3 = 1.1 点(相对 5.39%)

即幅度是先扩后收、不是单调缩小,且 28 天的差距是 1.1 而非 0.9。

消融(PEMS07,Table 3)

变体 MAE RMSE MAPE(%)
完整 StemGNN 2.144 4.010 5.010
w/o LC(改用先验拓扑) 2.158 4.017 5.113
w/o Spe-Seq Cell 2.612 4.692 6.180
w/o DFT 2.299 4.170 5.336
w/o GFT 2.237 4.068 5.222
w/o Residual 2.256 4.155 5.230
w/o Backcasting 2.203 4.077 5.130

最刺眼的一点:w/o LC(把学出来的图换成人工先验拓扑)只从 2.144 掉到 2.158,MAE 差 0.014,几乎无损。而 w/o Spe-Seq Cell 掉到 2.612(+22%)。也就是说:时间维度的频域建模是主要收益来源,"自动学图"在有先验拓扑的交通数据上带来的增益相当有限。论文自己的叙述重点放在"学图比人画的好"上,但消融数据说的是另一回事。

【Jay 修正】 "自动学图增益有限"不能泛化到所有数据集。附录 E.2 Table 7 给出了 METR-LA 的消融: - METR-LA 15min:w/o Latent Correlations 2.79 vs 完整 2.56 → +8.98%(比 PEMS07 的 +0.65% 大一个量级); - METR-LA 30min:3.122 vs 3.011 → +3.69%; - METR-LA 1hour:3.568 vs 3.43 → +4.02%。

正确表述是:"在 PEMS07 这类先验拓扑质量高的数据集上,学图几乎不带来增量;但在 METR-LA 上自动学图贡献可达 9% MAE"。同时注意论文 Table 3 的 w/o Spe-Seq Cell MAPE 是 6.180,附录表同项是 6.189,论文自身存在微差。

效率(Table 8)

单张 NVIDIA GeForce GTX 1080:

训练时间(秒) StemGNN SFM N-BEATS STGCN
PEMS07 459 1013 1251 352
METR-LA 1137 2284 2561 1035

StemGNN 训练速度比 SFM/N-BEATS 快约一倍,和一阶近似的 STGCN 相当,但精度明显更高。

亮点与局限

亮点

  1. 范式统一而非堆叠。"GFT 和 DFT 串成一个 block,在频域里联合建模 intra/inter 相关性"是一个真正有解释力的命题,而不是又一个 attention 变体。
  2. "不要先验图"是实打实的能力扩展。Solar / Electricity / ECG / COVID 这四类数据上,DCRNN 和 ST-GCN 根本用不了(论文明确标注 "not applicable in scenarios without a priori topology"),StemGNN 能跑且拿了 SOTA。
  3. 可解释性有实证支撑。学出来的邻接矩阵有地理/经济语义(US-Canada-Mexico、China-Japan-Korea),这是很多 GNN 论文拿不出的东西。
  4. GFT 的平滑性论证扎实。Figure 4/6 展示了归一化 Laplacian 的前几个特征向量对应的时序确实捕获了"全球趋势""亚洲趋势""南美趋势"——作者对 $u_0, u_1, u_2$ 的解释是自洽且可核对的。(附录 F.2 原文:$u_0$ 捕捉全球总体趋势,$u_1$ 是亚洲主要趋势,$u_2$ 是南美共同趋势;对应 Figure 6(c)。)
  5. 工程成本低。GTX 1080 单卡,METR-LA 训练 1137 秒;权重、依赖都很轻。
  6. 代码开源且给了数据链接:https://github.com/microsoft/StemGNN/ ,README 明确列了 10 个数据集的下载地址、ECG5000 的清洗后版本(5000×140)、以及 python main.py 的入口。

局限(论文自述 + 读出来的)

  1. $O(N^3)$ 特征分解是硬天花板。论文自己在 Conclusion 里承认:"we will investigate approximation method to reduce the time complexity of StemGNN, because directly applying eigenvalue decomposition is prohibitive for very large graphs of high-dimensional time-series." 每个 block 都要对 N×N 的 Laplacian 做一次特征分解。测试集最大 N=358 已经接近这个架构的舒适区上限;上到几千节点基本没戏。
  2. GFT 不做降维是主动选择。用满 N 个特征向量意味着模型完整吃下了 $O(N^3)$ 的成本,一点余量都没留。
  3. PEMS08 上不是最优(15.83 vs DeepGLO 15.12),而 abstract 和 intro 的汇总口径把这一失利模糊掉了。
  4. "学图 > 先验图"的证据强度被高估。消融里 w/o LC 只掉 0.014 MAE;COVID-19 的关联性分析是定性可视化,没有给出"学出来的图 vs 真实地理邻接"的量化指标(比如边权重与真实距离的相关性)。
  5. rolling 推理导致长程误差累积,论文没有对这一衰减做定量归因分析。
  6. 部分评测口径不统一。作者自己写明"We report the performances of baseline models in their original publications unless otherwise stated"——基线数字来自各自原论文,评测代码/切分/归一化细节的差异没有被完全控制,这是 TS 领域 benchmark 的通病,但读表时要留个心。Solar / Electricity / ECG 的评测还额外做了"先 min-max 到 [0,1] 再预测、再反变换回原尺度算指标",与其他数据集的口径不同。
  7. 代码复现环境陈旧。README 指定 Ubuntu 18.04.2 + Python 3.7 + pip==20.1.1 + setuptools==41.2.0,这是 2021 年前后的组合,在现代环境里装依赖大概率要折腾。
  8. 数据清洗负担被转嫁给用户。README 明确说"Since complex data cleansing is needed on the above datasets provided in the urls before fed into the StemGNN model",只对 ECG 提供了清洗后的版本,其余 9 个数据集的清洗流程都需要自己复现。输入格式要求是 T×N 的无表头 CSV,T 是时间戳、N 是节点数——这个方向(时间在前)反直觉,容易写反。
  9. 论文自身的表述瑕疵:intro 写 "8.1% on MAE an 13.3% on RMSE"("an" 应为 "and");abstract 说 "ten real-world datasets" 而 §5.1 说 "nine public datasets",实际是 9 个 benchmark + 1 个 COVID 案例。

【Jay 修正·局限节】 - 第 1 条须区分口径:$O(N^3)$ 是论文的特征分解成本;官方代码的同阶成本来自 Chebyshev 多项式的稠密 $N\times N$ 矩阵乘,且每次 forward 只算一次(两个 block 共享同一份 mul_L),不是"每 block 一次"。 - 第 3 条应扩写为 3 个落后单元格(PEMS08 MAE、PEMS03 RMSE、Solar RMSE,见上)。 - 第 4 条须限定数据集(PEMS07 几乎无损,METR-LA 可达 +9% MAE,见上)。 - 第 5 条须标注为论文口径(代码是 direct 多步,见上)。 - 第 6 条关于 min-max:需区分两件事——训练输入归一化是 Z-Score,但 §5.1 与附录 D 都写了 "The inputs of ECG are normalized by min-max normalization",即 ECG 的训练输入本身用 min-max;而 "把 Solar/Electricity/ECG 缩放到 [0,1] 再预测、再还原到原尺度算指标" 是评测口径。原文这两句并存,不是同一件事,复现时两边都要对上。 - 第 7 条可补一条更硬的:官方 requirements.txt 固定 torch==1.7.1 / numpy==1.19.5 / pandas==1.1.5 / scipy==1.5.4,而 torch.rfft / torch.irfft 在 torch 1.8+ 已被移除,现代环境直接跑不起来。 - 第 9 条前半段要改:abstract 原文是 "nine public benchmarks","ten public datasets" 出自 Appendix B.1(见正文【Jay 修正】)。 - 补充局限(官方仓库层面):master 分支最后提交为 2022-11-28(HEAD dc7dea6,提交信息 "Auto merge mandatory file pr"),GitHub API 记录 pushed_at=2024-05-31(非 master 活动);仓库当前 618 stars / 123 forks / 27 open issues,且仓库根目录没有 LICENSE 文件(https://raw.githubusercontent.com/microsoft/StemGNN/master/LICENSE 返回 404;issue #27 就是为此开的)。开源可读,但无许可证 = 默认保留全部权利,商用前必须自己确认授权。

对工程落地的启发

坑 1:节点数直接决定你能不能用这套架构。 - 现象:每个 StemGNN block 都要做 N×N 归一化 Laplacian 的特征分解,复杂度 $O(N^3)$。 - 影响:N=358(PEMS03)时单块尚可;N 上到几千,训练时间会失控,且每个 forward 都要重算特征向量。 - 修复:特征向量 U 和特征值 Λ 可以预先算好缓存(Laplacian 只依赖 W,而 W 由 latent correlation layer 产生——注意这里有个鸡生蛋问题:W 是学出来的,训练中每步都在变,所以 U 也在变,没法完全预计算)。实践中的折中:如果你的 N 确实大,考虑改用 Chebyshev 近似 / 邻居采样,放弃精确 GFT;或者退回 spatial domain 的 GraphWaveNet 风格 adaptive adjacency + dilated TCN。

【Jay 修正】 坑 1 的"每个 block 一次特征分解"是论文口径。官方代码里没有特征分解,真实成本结构见 ## 工程落地与核查(Jay) 的坑 1':$O(N^3)$ 来自每次 forward 的两次 $N\times N$ 稠密矩阵乘($L^2$、$L^3$),内存瓶颈其实是 $[B,N,N]$ 的注意力张量而不是特征分解。

坑 2:learned graph 在有真实拓扑的场景未必比先验拓扑值钱。 - 现象:PEMS07 消融里 w/o LC 只掉 0.014 MAE(2.144 → 2.158),几乎无损。 - 影响:如果你手里有高质量先验拓扑(比如路网距离矩阵),StemGNN 的"自动学图"卖点对你几乎不产生增量价值,却让你多付了 $O(N^3)$ 的代价。 - 修复:有先验就用先验。StemGNN 的 w/o LC 变体本身就支持这个配置,工程上应该把"是否启用 latent correlation layer"做成开关而不是硬编码。

【Jay 修正】 两处要改:(1)"增益有限"只在 PEMS07 成立,METR-LA 上学图贡献达 +9% MAE,不能一概而论;(2)官方代码根本没有 w/o LC 这条路径——get_laplacian 从未被调用,也没有"注入先验拓扑"的接口,要用先验拓扑得自己改 latent_correlation_layer。

坑 3:图相关矩阵的方向/归一化容易搞错。 - 现象:W = softmax(QKᵀ/√d) 的 softmax 是在最后一个维度(K 方向)上做的,得到的 W 行和不为 1(列和为 1)。紧接着要算 $L = I_N - D^{-1/2} W D^{-1/2}$,其中 $D_{ii} = \sum_j W_{ij}$ 是按行和定义的度数。 - 影响:如果你按列和归一化,或者把 W 当成对称矩阵处理,谱分解出来的 U 会和论文不一致,实验复现不出来。 - 修复:严格按原文实现——softmax 沿 K 维,度数用行和;不要强制对称化。

【Jay 修正】 坑 3 的三句话有两句反了: - softmax 沿最后一个维度(K 方向)归一化,得到的是行和为 1(每个 query 对 keys 的分布),不是"列和为 1"; - 代码里 degree = torch.sum(attention, dim=1) 在 2D 矩阵上就是行和,与论文 $D_{ii}=\sum_j W_{ij}$ 一致; - 代码强制对称化了:attention = 0.5 * (attention + attention.T),而且是在算完 degree 之后才做,导致 $D$ 与 $L$ 里实际用的 $W$ 不自洽($D$ 取自未对称化矩阵,$W$ 取自对称化后的)。另外 softmax 之后还有 p=0.5 的 dropout,训练态下行和并不严格等于 1。

坑 4:多步预测是 rolling 而非 direct。 - 现象:推理时把 $\hat{X}_t$ 塞回输入窗口,循环 H 次。 - 影响:长程预测误差累积,而且训练时的 loss 并不能反映多步滚动的实际退化(训练是逐点预测 + backcasting)。 - 修复:上线前必须自己测"滚动 H 步"和"直接出 H 步"两种模式下的指标差;H 越大差距越明显。

【Jay 修正】 这是论文口径;官方代码等价 direct 多步(fc 一次出满 horizon 步,滚动循环只跑一轮)。要按 rolling 预算误差累积,得自己实现回灌逻辑。

坑 5:数据清洗与输入方向。 - 现象:README 明确要求 CSV 无表头、形状 T×N(T 时间戳在前,N 节点在后),且 10 个数据集中 9 个需要用户自己按链接下载并清洗。 - 影响:输入方向写反是最常见的低级错误,会得到"能跑但结果离谱"的结果而不是报错。 - 修复:写一个 loader 层的 shape assert(assert df.shape[0] > df.shape[1],N=140 的 ECG 是 5000×140);数据清洗照 README 里的逐条描述做(Solar 合并 Alabama 5 分钟 CSV 共 137 条;Electricity 删表头和索引;COVID 用 time_series_covid19_confirmed_global.csv 算日新增)。

坑 6:归一化与反归一化的口径在数据集之间不统一。 - 现象:训练用 Z-Score($X_{in}=(X_{in}-\mu)/\sigma$,$\mu,\sigma$ 取自训练集);但 Solar/Electricity/ECG 的评测额外先 min-max 到 [0,1],预测后反变换回原尺度再算 MAE/RMSE/MAPE。 - 影响:自己复现时如果只在 Z-Score 空间算指标,得到的数字和论文 Table 2 对不上,会误判为"复现失败"。 - 修复:严格区分"训练归一化"和"评测反归一化"两套操作;MAPE 尤其敏感,反变换误差会直接放大。

【Jay 补充】 这条要再拆成两件事:ECG 的训练输入本身就是 min-max(§5.1 + 附录 D);Solar/Electricity/ECG 的 [0,1] 缩放 + 还原是评测口径。代码侧 normalized() 的 min_max 分支用 +1e-5 做 scale、de_normalized() 用 +1e-8,正反变换不严格互逆;且 forward 会 np.clip(data, 0, 1),把训练区间外的值截断。

坑 7:复现环境老旧。 - 现象:README 指定 Ubuntu 18.04.2 / Python 3.7 / pip==20.1.1 / setuptools==41.2.0。 - 影响:在 Python 3.10+ 上老依赖大概率装不上或行为变化。 - 修复:用 python3.7 -m venv 单独建环境(README 原话是 "virtualenv is recommended"),别污染主环境;依赖装不上时优先考虑用现代 PyTorch 重写 Spe-Seq Cell(它只有 Conv1D + GLU,逻辑不复杂)。

【Jay 修正】 "Spe-Seq Cell 只有 Conv1D + GLU"不成立——官方代码里没有 nn.Conv1d,GLU 是 nn.Linear 实现的。现代 PyTorch 重写要动的点是 torch.rfft/irfft(1.8+ 已移除)→ torch.fft.rfft/irfft,以及 torch.load 的 weights_only 行为变化。

坑 8:超参搜索空间与默认配置要写进自己的实验记录。 - 现象:原文给的搜索空间——self-attention 维度 [16,32,64,128] 选中 32;图卷积 channel [16,32,64,128] 选中 64;1D conv kernel [3,6,9,12] 选中 3;batch size 50;lr 0.001,每 5 epoch 衰减 0.7 倍;总共 50 epoch;优化器 RMSprop。 - 影响:RMSprop + 50 epoch 这种配置在现代 practice 里偏保守,容易欠拟合。 - 修复:把这套配置当 baseline 起点而不是最优解;至少加一版 AdamW + 更长训练的对照。

【Jay 修正】 论文超参与代码默认值对不上,且代码里根本没有 attention 维度 / 图卷积 channel / conv kernel 这三个超参(见工程节参数核查表)。multi_layer 常被误读为"层数",实际是 Spe-Seq cell 的通道倍数(output_channel = 4 * multi),block 数固定为 2。

与同方向工作的关系

  • 对 DCRNN / ST-GCN / GraphWaveNet:这是最直接的对照系。StemGNN 的差异在于把"图 + 时间"从堆叠改成频域内联合,并且把图从先验换成可学。GraphWaveNet 也有 adaptive adjacency(同样是数据驱动建图),但它不引入 DFT 频域处理,这正是 StemGNN 消融里 w/o DFT 掉 0.155 MAE 所对应的差距来源。
  • 对 N-BEATS:StemGNN 直接借了它的 basis expansion($Y=V\theta$)和 backcasting 机制,block 里的 forecasting / backcasting 双分支就是 N-BEATS 那套"通用基 + 可解释系数"的结构。StemGNN 的贡献是把这个结构塞进频域图卷积的骨架里。
  • 对 SFM(State Frequency Memory):SFM 用 DFT 分解 LSTM 的 cell state 成多个频率分量,处理的是序列内;StemGNN 的 Spe-Seq Cell 是它的谱域版本,但作用对象换成 GFT 之后的正交分量,且加上了图结构。效率表里 SFM 比 StemGNN 慢一倍还更差,说明这个"频域 + 图"的组合确实不是简单叠加。
  • 对 DeepGLO:DeepGLO 用 TCN-regularized Matrix Factorization 把每条序列表示成 k=128 个基序列的线性组合,是"全局-局部"混合思路。StemGNN 与它是同一问题域的两种解法:DeepGLO 靠矩阵分解做全局,StemGNN 靠 GFT 特征分解做全局。DeepGLO 在 PEMS08 上赢了 StemGNN,说明"矩阵分解式全局建模"在某些交通数据上并不弱于谱分解。
  • 对 Fourier 系列(Joint Fourier Transform、VARMA on graphs、ST-vertex signal processing):这些是"图上的傅里叶分析"理论工作,StemGNN 相当于把它们的思想变成了可训练的端到端网络,是"理论 → 工程"的一次漂亮转化。
  • 对后来的时序基础模型方向(TimesNet / PatchTST / iTransformer 这条线):StemGNN 属于"领域模型"路线,依赖图结构与频域归纳偏置。今天的通用 SOTA 常常靠 patch 化 + attention 走另一条路,绕开了 $O(N^3)$ 的瓶颈。StemGNN 在 2020–2021 是顶点,但它的"可学图"思想在后来的 iTransformer(把 variate 当 token 做 attention)里又活了过来。

适合谁读

适合读:

  • 做多变量时序预测、需要同时处理"多条序列相关"和"单条序列周期"的研究者;
  • 交通 / 电力 / 能源 / 医疗监测这类没有现成图拓扑、但变量之间明显相关的落地场景的工程师——StemGNN 的 latent correlation layer 给了你一条不依赖专家知识的路;
  • 想理解"图傅里叶变换到底在时序预测里能干什么"的人——这篇的 GFT 章节和 Figure 4/6 的可视化是很好的教学材料;
  • 做 AIOps / 预测性维护的人,论文自己就把这个列为 future work。

不太适合:

  • 节点数上千、要求秒级更新的在线场景——$O(N^3)$ 直接劝退;
  • 追求开箱即用 SOTA 且不愿做数据清洗的人——9 个数据集的清洗流程要自己复现;
  • 想找"最新 SOTA"的人——这是 2021 年的工作(被引 854 次,Semantic Scholar 口径),baseline 是 GraphWaveNet 那一代,横向对比 2024+ 的模型已不成立;把它当方法论参考读,而不是当性能标杆读。

一句话建议:把 StemGNN 当成"频域 + 图"这个组合的思想原型来读——它的价值在"为什么这样排算子"和"自动学图的可解释性展示",而不是那 8.1% / 13.3% 的数字。


附:本篇的来源与不确定项

来源: - arXiv abs 页 https://arxiv.org/abs/2103.07719(标题、作者列表、abstract、Comments 字段 "Accepted by NeurIPS 2020. 20 pages, 7 figures"、Subjects、提交历史 v1 2021-03-13)— 已 fetch,2026-10-24 - arXiv HTML 全文 https://arxiv.org/html/2103.07719v1(§1–§7、Appendix A–F)— 已 fetch,2026-10-24 - GitHub https://github.com/microsoft/StemGNN/(README、数据集链接、环境要求、输入格式)— 已 fetch,2026-10-24 - 知识库论文卡 organized/paper_cards/1814-2103-07719.md(TLDR、被引 854 / OpenAlex 388 / 影响力被引 57)

不确定 / 原文未明确之处: 1. Table 2 原文对部分 baseline 只标了引用编号(如 METR-LA 的 GraphWaveNet 那一行标 [15]/[4]),我按表中数值与 model 名的对应关系推断归属,逐格 baseline 归属建议回查 PDF 原表。 2. 交通/能源/电力数据集的预测跨度 H 与输入窗口 K,论文只在 Appendix D 用自然语言描述(如"one-hour historical data to predict the next 15 minutes"),未给出统一的 H/K 表格。 3. 论文 abstract 写 "ten real-world datasets",§5.1 写 "nine public datasets",Table 1 列 10 个;我按"9 个 benchmark + 1 个 COVID 案例"理解,原文未明确口径。 4. PEMS08 的最优 MAE 我取表中 DeepGLO 的 15.12(与 SFM 16.01 / DeepState 19.34 比较),需注意 GraphWaveNet 在 PEMS08 行为 19.13。 5. 论文未报告参数量、推理延迟、显存占用,只有训练时间(Table 8,GTX 1080)——"inference speed shows the same conclusion"是原文一句话,未给具体数字。 6. GitHub 仓库的 star 数、最新 commit、license 状态本次未核(只 fetch 了 README 正文),「代码活跃度」属未核项。 7. 论文提到 "electrical grid Solar ... 137 stations in Alabama State"(Solar 的具体 137 条序列构成)与 Electricity "370 customers"(Table 1 节点数写 321)存在口径差异,原文未解释,我按 Table 1 的 321 引用。 8. 我未查 GitHub Issues / PR 区是否有已知复现问题,复现踩坑清单可能不完整。

【Jay 核查结果 · 逐条回填】 1. 已核:Table 2 逐格归属已按 HTML 原表重排,见正文修正后的主结果表;原表 GraphWaveNet 列与最优基线列整表错位、PEMS04 行误填 PEMS03 的 RMSE 值。 2. 已核:Appendix D 给出各数据集的自然语言 H/K 描述(交通 speed 1h→15min、flow 1h→1h、Solar 4h→0.5h、Electricity 24h→3h、ECG5000 window 12 / horizon 3、COVID max 28 天),仍无统一表格,此条保留。 3. 已修正:abstract 原文为 "nine public benchmarks",§5.1 为 "nine public datasets",Appendix B.1 为 "ten public datasets"(含 COVID-19)。 4. 已核:PEMS08 最优 MAE = DeepGLO 15.12;GraphWaveNet 该行为 19.13;SFM 16.01;DeepState 19.34。 5. 仍缺:论文确实没有参数量、推理延迟、显存数据;官方代码侧 handler.py 会打印 "Total Trainable Params",但论文未报。 6. 已核:618 stars / 123 forks / 27 open issues;master HEAD dc7dea6,最后提交 2022-11-28;仓库无 LICENSE 文件(raw URL 404,issue #27)。GitHub API pushed_at 为 2024-05-31(非 master 分支活动,未逐分支核对)。 7. 仍存疑:Solar 137 stations、Electricity 370 customers vs Table 1 的 321,论文未解释,保留【待核】。 8. 已核:已查 open issues(#9、#10、#12、#14、#18、#19、#21、#22、#27、#28、#29、#32、#36 等),复现坑清单已补入工程节。

【Jay 新增待核项】 - 论文 "8.1% MAE / 13.3% RMSE" 的聚合口径不可复现(三种口径均对不上),引用时须标"口径未公开"。 - 官方代码中注意力矩阵的行/列下标来自 GRU 隐状态通道(unit = node_cnt),与 Laplacian 作用的节点轴语义不同源(维度恰同为节点数才得以对齐)——属【Jay 推测】,论文与代码注释均未说明。 - multi_layer 的真实语义(Spe-Seq cell 通道倍数,非层数)由代码推断,README 的参数说明("controls the parameter number of hidden layers")有误导。


工程落地与核查(Jay)

本节是第二读者视角的工程复核,不是论文摘要的复述。核查对象:microsoft/StemGNN master HEAD dc7dea6(2022-11-28,克隆于 /tmp/stemrepo)+ arXiv:2103.07719v1 全文。 核查方法:git log/grep 全仓符号检索(graph_fft、get_laplacian、torch.rfft/torch.irfft、eigh/eig)、逐行读 models/base_model.py、models/handler.py、data_loader/forecast_dataloader.py、utils/math_utils.py、main.py、requirements.txt、README 命令行与参数表;GitHub REST API 查仓库元数据与 open issues。

0. 四维度评分

维度 评分 依据
机制(原解读是否讲对) 3.5 / 5 主结果表整表错位、COVID 领先幅度错、消融泛化过度、8.1%/13.3% 不可复算、rolling 与 O(N³) 口径未区分论文/代码
工程(可落地度) 2.5 / 5 环境三连过期、CLI 参数名与 README 不符、COVID 配置开箱即崩、无 LICENSE
可复现 2 / 5 官方 issue #32 直接标题即 "The result can not be reproduced";#9/#10/#14/#18/#21/#22/#29 并列
可验证 4 / 5 论文、官方代码、GitHub API 三方均可核;本节所有数字均给出文件:行号或 API 字段

综合结论:原解读的方法论叙述(为什么这么排算子、范式统一的价值)质量很高,但所有量化断言都需要逐格回表——主结果表错位、COVID 领先幅度、消融泛化、聚合口径四处硬伤,叠加官方代码与论文在 8 处以上结构性差异,直接照原解读写工程方案会踩坑。本节列出 10 个坑,前 8 个给出量化阈值与修复动作。

1. 组件拆解:论文部件 ↔ 官方代码实现

论文部件 官方代码实现 文件:行号 差异等级
GFT(特征分解 $U^\top X$) 无特征分解;cheb_polynomial 生成 4 阶多项式基,mul_L @ x 做节点维多项式传播 base_model.py:120-136(cheb_polynomial)、:66-68(gfted) 结构性
归一化 Laplacian $L=I-D^{-1/2}WD^{-1/2}$ 手工构造:degree_l - attention,degree 取对称化之前的行和 base_model.py:143-149 结构性
GFT 用满 N 个特征向量 4 阶基实为 $[0,\ L,\ 2L^2,\ 4L^3-L]$(first_laplacian 是全零,不是单位阵;$T_2$ 缺 $-I$、$T_3$ 缺 $-3L$) base_model.py:127-135 实现 bug 级
IGFT($U\hat X$) 可学习线性映射 self.weight,形状 (1, 4, 1, T·multi, multi·T),作用在节点域而非特征向量域 base_model.py:22-24、:69-71 结构性
Latent Correlation Layer 的 $Softmax(QK^\top/\sqrt d)$ 加性:leakyrelu(key_i + query_j),weight_key/weight_query 形状 (unit, 1)(标量投影),无点积、无 $1/\sqrt d$ base_model.py:160-171 结构性
$R$ = GRU 最后隐状态 GRU 沿节点轴迭代(输入 x.permute(2,0,1),unit=node_cnt),取全部输出 base_model.py:87、:140 结构性
每个样本一张图 torch.mean(attention, dim=0):整批共享一张图 base_model.py:142 结构性
Spe-Seq Cell:DFT → 1D Conv → GLU → IDFT 无 nn.Conv1d;GLU 用 nn.Linear;实/虚部参数独立 base_model.py:6-14、:47-61 结构性
DFT torch.rfft(input, 1, onesided=False),作用在时间轴 T base_model.py:49 一致(但 API 已移除)
IDFT torch.irfft(time_step_as_inner, 1, onesided=False),作用在长度 T·multi 的轴上,与前向 FFT 不是一对逆变换 base_model.py:58 结构性
两 block 输出拼接 → GLU + FC forecast = result[0] + result[1](相加);输出头 Linear → LeakyReLU → Linear(无 GLU) base_model.py:176-180 结构性
损失含 backcasting 项 训练只有 forecast_loss(forecast, target)(MSE) handler.py:151-153 结构性
rolling 多步推理 输出头直接出满 horizon 步,inference 滚动循环只跑一轮 → 等价 direct handler.py:52-70、base_model.py:182-186 结构性

2. 十个坑(现象 / 影响 / 修复)

坑 1':$O(N^3)$ 的真实来源不是特征分解,是每次 forward 的两次稠密 N×N 矩阵乘。 - 现象:latent_correlation_layer 每次 forward 都重建 Laplacian 并调 cheb_polynomial;其中 third = 2 * matmul(L, L)、forth = 2 * matmul(L, third),两次 N×N×N 稠密乘。两个 block 复用同一份 mul_L,所以是每 forward 一次而非"每 block 一次"。 - 影响:N=358 时两次矩阵乘约 $2\times358^3\approx9.2\times10^7$ FLOPs,可忽略;N=3000 时约 $5.4\times10^{10}$ FLOPs/forward,成为训练主瓶颈之一。真正的显存墙在别处(见坑 2')。 - 修复:要复用计算就把 mul_L 提出来缓存——但注意 W 每步都在变,实际只能在同一 batch 内复用(代码已经这么做了);要降本只有两条路:换 Chebyshev 阶数(当前 4 阶,降到 2 阶可省一半矩阵乘)或退回 spatial 域(GraphWaveNet 式 adaptive adjacency)。不要照论文去实现 torch.linalg.eigh——那是 $O(N^3)$ 特征分解,成本比现在贵一个量级。

坑 2':内存瓶颈是 $[B,N,N]$ 的注意力张量,不是特征分解。 - 现象:self_graph_attention 构造 [B,N,N] 张量并做 softmax + dropout,latent_correlation_layer 再对 batch 求均值;cheb_polynomial 另存 [4,N,N]。 - 影响:显存按 $O(B\cdot N^2)$ 增长。B=32 时:N=358 约 16 MB/张,N=2000 约 512 MB/张(且 softmax/dropout 会同时持有多份中间量),N=5000 约 3.2 GB/张——先于 FLOPs 撞墙。 - 修复:给硬阈值——单卡 24 GB 上 N ≤ 2000 且 batch ≤ 32(把注意力张量控制在 512 MB 内);N > 2000 时把 latent_correlation_layer 的图计算与 batch 无关化(如每 N 步重算一次图,batch 内复用),或用 chunked attention 把峰值内存降到 $O(B\cdot N\cdot C)$。

坑 3':注意力的形态与论文完全不同,加性 + 标量投影 + 全 batch 共享 + 0.5 dropout。 - 现象:weight_key/weight_query 是 (unit, 1) 标量向量,logits = leakyrelu(key_i + query_j);torch.mean(attention, dim=0) 让整个 batch 共用一张图;softmax 之后 self.dropout(attention)(p 默认 0.5)随机置零一半边。 - 影响:三个后果叠加——(i)"每个样本一张动态图"实际上退化成"每个 batch 一张图";(ii)学到的边权每次 forward 一半被随机改掉,训练态与推理态的图分布不一致;(iii)标量投影使注意力表达能力远低于论文描述的 $O(N^2d)$、d 可调(论文 attention dim=32 在代码中无对应超参)。 - 修复:如果要复现论文行为,改三处:weight_key/weight_query 改成 (unit, d) 并用 key @ query.T / sqrt(d);去掉 torch.mean(attention, dim=0),改成逐样本构图再 batch 维拼接(或保留共享图但显式承认这是改动);dropout_rate 设 0 或改到 0.1 并重训。监控时不要直接读 attention 矩阵当"学到的图"——推理态是 batch 均值后的单张图。

坑 4':softmax 归一化方向与 degree 定义,代码里跟论文对不上,而且 symmetrization 顺序有坑。 - 现象:F.softmax(data, dim=2) 沿最后一维(keys 方向)归一化 → 行和为 1(每行是 query 对 keys 的分布),不是列和;degree = torch.sum(attention, dim=1) 在 2D 矩阵上取的是行和,与论文 $D_{ii}=\sum_j W_{ij}$ 一致;但 attention = 0.5 * (attention + attention.T) 的对称化发生在算完 degree 之后,$D$ 与实际代入 $L$ 的 $W$ 不同源。 - 影响:想自己实现"论文版 GFT"时,如果按列和建 $D$ 或在对称化后再算 $D$,得到的 $U$ 与论文不一致,指标对不上;这正是 issue #12(21 条评论)与 #21(2 条评论)在争的问题。 - 修复:自己写实现时明确三步顺序:softmax → 对称化 → 再从对称化后的矩阵算 degree → 再建 $L$,并把 degree 与 $W$ 绑定到同一个矩阵;训练态还要考虑 dropout 对行和的扰动(可用 attention / attention.sum(dim=1, keepdim=True) 重归一化后再算 degree)。

坑 5':没有先验拓扑注入路径,w/o LC 变体在官方代码里不存在。 - 现象:get_laplacian(graph, normalize) 定义了但从未被调用;latent_correlation_layer 无任何"用给定邻接矩阵替换学到的 W"的入参。 - 影响:想跑"有先验图 vs 学图"的对照实验,必须自己改代码;在 PEMS07 上先验图几乎无损(+0.65% MAE),在 METR-LA 上学图贡献 +8.98%——这个对照恰恰是最该做的实验,却需要自己动手。 - 修复:给 Model.forward 加一个可选 prior_W 参数,在 latent_correlation_layer 里若非空则跳过学图、直接用它构造 $L$;注意此时不要再叠 softmax/dropout。

坑 6':训练只优化 forecast MSE,backcast 分支是"半残废"的。 - 现象:handler.train 里 loss = forecast_loss(forecast, target),没有 backcasting 项(论文 Eq. 的第二项在代码里不存在,issue #18 就是问这个);第二个 block(stack_cnt==1)干脆没有 backcast 分支;两个 block 的 forecasting 输出是相加。 - 影响:backcast 只通过"作为第二个 block 的输入"间接获得梯度;论文里"用自编码逼出有表达力的表征"这一设计意图在代码中基本没有兑现,因此复现出的模型是论文的简化版,指标可能低于论文值。 - 修复:要复现论文行为需自己补:loss = mse(forecast, target) + λ * mse(backcast_source, input_window),并把两 block 输出改成拼接后过 GLU + FC(与论文一致);λ 可从 0.1 起扫。在补上之前,不要把论文数字当作该代码的可达基线。

坑 7':Spe-Seq Cell 的 FFT/IDFT 不是一对逆变换,且用的是已从 torch 移除的 API。 - 现象:前向 torch.rfft(input, 1, onesided=False) 作用在长度 T 的时间轴;反向 torch.irfft(time_step_as_inner, 1, onesided=False) 作用在长度 T·multi 的轴(GLU 把通道从 4T 扩到 4T·multi 后再 reshape)。torch.rfft/torch.irfft 在 torch 1.8 起被移除(requirements.txt 固定 torch==1.7.1)。 - 影响:升级 torch 到 ≥1.8 直接报 TypeError: fft_rfft() got an unexpected keyword argument 'onesided'(issue #14 的原文报错);即使降级安装成功,前后轴长不匹配意味着这个"频域模块"在数学上并不是对同一个信号的变换对。 - 修复:迁移时把 torch.rfft(x, 1, onesided=False) 换成 torch.fft.rfft(x, dim=-1) + 手工拼实虚部,或直接 torch.fft.fft(x, dim=-1);并把 IDFT 的轴长改回与前向一致(或明确承认代码是"频域增强"而非严格逆变换)。同时别忘了 numpy>=1.24 已删除 np.float,handler.py:50 的 dtype=np.float 会直接 AttributeError。

坑 8':默认配置开箱即错三连——切分比例、CLI 参数名、COVID 窗口。 - 现象:(a)main.py 默认 --train_length 7 --valid_length 2 --test_length 1 = 7:2:1,而论文 PEMS03/04/08 用 6:2:2;(b)README 给的命令用了 --output_dir、--n_route、--validate_length,argparse 里三个都没有(正确名是 --valid_length),照抄必然 unrecognized arguments(issue #28、#36);(c)README 表给 COVID-19 配 window_size 28 / horizon 28,而 110 天按 7:2:1 切出来 valid 只有 22 行,$window+horizon=56>22$ → Cannot organize enough validation data(issue #9)。 - 影响:三个问题分别对应"复现出来的指标对不上论文"、"命令直接报错"、"COVID 案例跑不起来"——是 issue #32 "The result can not be reproduced" 最常见的三个来源。 - 修复:可用命令(ECG 已随仓库提供):

bash # ECG5000(仓库自带 dataset/ECG_data.csv,5000×140) python main.py --dataset ECG_data --window_size 12 --horizon 3 \ --train_length 7 --valid_length 2 --test_length 1 --norm_method z_score # PEMS03/04/08 复现论文口径必须改成 6:2:2 python main.py --dataset PEMS04 --window_size 12 --horizon 3 \ --train_length 6 --valid_length 2 --test_length 2 --norm_method z_score

切分比例是比例(train_length/valid_length/test_length 三者之和归一),不是行数——写 6/2/2 而非 60/20/20。COVID 若要跑,需要改 loader 支持"前 60 天训练 / 后 50 天测试"(论文口径)或把切分改成 7:2:1 且把 window 降到 7。

坑 9':MAPE 口径会污染结论——零真值按 500% 计,且用的是非 masked 版本。 - 现象:utils/math_utils.py 里 MAPE 做 (|v_-v|/|v| + 1e-5) 并在 5 处截断 → 单点上限 500%;evaluate() 调用的是 MAPE 而不是同文件里已存在的 masked_MAPE(后者会把真值为 0 的点置为 NaN 剔除)。真值为 0 时 |v_-v|/0 → inf → 被截断成 5.0。 - 影响:只要数据集里有零值节点(Electricity/Solar/COVID 的早期零新增日都可能),这些点每个都贡献 500% 的误差,直接抬高整体 MAPE 并把模型之间的差异压扁;论文的 MAPE 口径与代码口径不一定一致。 - 修复:自建评测时改用 masked_MAPE,并同时报告零真值占比(>1% 就说明 MAPE 不可直接比较);对 MAPE 的结论只在同口径内比较。

坑 10':模型保存/加载在现代 torch 上直接失败;且无 LICENSE。 - 现象:save_model 用 torch.save(model, f) 存整个 module 对象(pickle),load_model 用 torch.load(f) 不带参数;torch ≥ 2.6 起 weights_only 默认改为 True,加载这种全量 pickle 会直接失败。另:仓库根目录无 LICENSE(raw URL 404,issue #27)。 - 影响:checkpoint 跨版本不可用,线上热更新/迁移会卡住;无许可证意味着默认保留全部权利,任何商用都要先取得授权。 - 修复:加载时显式 torch.load(f, weights_only=False)(并把该文件视为不可信输入,只加载自己产出的);更稳的做法是改成 state_dict 保存 + 重新构造模型。商用前需向 Microsoft 确认授权口径——本条为【Jay 提示】的合规风险,不构成法律意见。

3. 参数核查状态表

项 论文(Appendix D) 代码默认值 状态
batch size 50 32(--batch_size) 不一致,需显式传参
learning rate 0.001,每 5 epoch ×0.7 1e-4,每 5 epoch ×0.5(--decay_rate) 衰减率差 0.2/0.7,直接决定 50 epoch 后 lr 量级
epochs 50 50 一致
optimizer RMSprop RMSProp(默认)/ Adam 一致
attention dim 32(搜 [16,32,64,128]) 无此超参(投影为 (unit,1) 标量) 代码无对应实现
graph conv channel 64(搜 [16,32,64,128]) 无此超参;通道由 multi_layer 决定(output_channel = 4·multi,默认 20) 代码无对应实现
1D conv kernel 3(搜 [3,6,9,12]) 代码无 nn.Conv1d 代码无对应实现
multi_layer 论文未提 README 称"hidden layers",实为 Spe-Seq cell 通道倍数 语义误导
block 数 2 2(Model(node_cnt, 2, ...)) 一致
GRU hidden 论文未明示 d unit = node_cnt(随 N 线性增长) 论文未覆盖
dropout 论文未提 0.5(作用于注意力矩阵) 论文未覆盖
切分 PEMS03/04/08 = 6:2:2,其余 7:2:1 默认 7:2:1 需显式传参
归一化 ECG 训练输入 min-max;Solar/Electricity/ECG 评测走 [0,1] 缩放 + 还原;其余 z-score --norm_method z_score(默认)/ min_max 口径需自行对齐;min_max 正反不严格互逆(+1e-5 vs +1e-8)
COVID 窗口/跨度 28 天 README 表 28/28,但 7:2:1 下 valid 样本不足 开箱报错(issue #9)
设备 单张 GTX 1080 --device 默认 cpu;main.py 在 import torch 之后硬编码 CUDA_VISIBLE_DEVICES='0,1' 单卡/多卡环境需自行改(issue #29)
随机性 未提 torch.manual_seed(0),numpy 无种子 部分可控

4. 复现路径建议(按投入产出排序)

  1. 先跑通 ECG5000(唯一自带干净数据的版本):--dataset ECG_data --window_size 12 --horizon 3,CPU 可跑通全流程,用来验证你的 loader 与指标口径。
  2. 再补 6:2:2 切分的交通数据:PEMS03/04/08 必须显式 --train_length 6 --valid_length 2 --test_length 2,否则与论文 Table 2 不可比。
  3. 最后做"学图 vs 先验图"对照:这需要改代码加 prior_W 注入(坑 5'),但这是决定"要不要用 StemGNN"的关键实验——PEMS07 上先验图几乎无损(+0.65%),METR-LA 上学图 +8.98%。
  4. COVID 案例要么改 loader 支持 60/50 天切分,要么放弃(issue #9 未解)。

5. 诚实声明与本节局限

  • 未实测:本次核查没有实际运行训练/推理(环境为 arm64 容器、无 torch、无数据集),所有代码结论均来自静态阅读(git log + 全仓 grep + 逐行读源码)与论文原文核对,不是运行结果。GPU 显存数字为按张量形状推算的估算值,非实测峰值。
  • 已实测的是外部事实:GitHub API 仓库元数据(618 stars / 123 forks / 27 open issues / pushed_at=2024-05-31)、master HEAD 与提交日期、LICENSE 404、issues 列表与标题,均为 2026-10-24 的 API 返回。
  • 口径未公开:论文 "8.1% MAE / 13.3% RMSE" 的聚合方式无法复算(三种口径分别为 15.58%/5.32%/7.65%、5.68%/0.15%、含 COVID 的 16.52%/7.21%/7.42%),引用须标注"论文自述、口径未公开"。
  • 仍待核:ECG5000 的 N-BEATS 完整行(Table 2 中 Electricity 的 N-BEATS 为 "-");Electricity "370 customers" 与 Table 1 的 321 节点差异;README Table 3 的 COVID 662.24 / 1023.19 / 19.3 与 Table 4/6 的逐行对应(19.3 可与 Table 4 的 28 Day MAPE 对上,662.24 / 1023.19 属 MAE/RMSE,未在 Table 4 中出现,来源未完全确认)。
  • 属推测而非事实:注意力矩阵下标与节点轴"维度恰同、语义不同源"这一判断,论文与代码注释均未说明,是【Jay 依据张量形状】的推断。
  • 合规提示:仓库无 LICENSE,商用授权需自行确认;本节不构成法律意见。

Jay · 2026-10-24 · 核查基线:microsoft/StemGNN @ dc7dea6(2022-11-28)+ arXiv:2103.07719v1 · 本节只读核查,未执行 git 写操作、未修改认领状态