谱图神经网络:把关联矩阵当成拉普拉斯信号
GNN 在金融里最被滥用的说法是「我能从图结构学到别的方法学不到的东西」。但它的数学本质其实很朴素:把 N 个资产之间的相关性当成一个图,相关矩阵 C 截成非负加权邻接 A,算归一化拉普拉斯 L = I − D^{−1/2} A D^{−1/2},对 L 特征分解得到 U 和特征值 λ;λ 就是「频率轴」——小 λ=低频/跨图平滑的板块信号,大 λ=高频/噪声。图傅里叶变换 x̂ = Uᵀx 把节点信号搬进谱域,再套一个谱滤波器 g(λ) 就能做全局低通去噪。本文用纯 numpy 从零实现拉普拉斯特征分解 + 谱低通 + Kipf-Welling 局部 GCN + 谱聚类,在 50 资产两板块合成图上实测:局部 1 跳 GCN 去噪 corr=0.973(远超观测 0.430、谱低通 0.633)、但谱聚类恢复准确率仅 0.720,并诚实拆穿「图比表强 / 谱低通最优 / 深层才分得好 / 邻接怎么造都行 / 金融可直接落地」五类真实陷阱(中阶)。
你有一篮子资产,想知道「哪几只其实属于同一个板块」。最直觉的做法是看相关性矩阵:同板块的票一起涨一起跌,相关性高;跨板块的低。但相关性矩阵是个 N×N 的密矩阵,直接拿去做聚类、去噪,既没有「结构」的归纳偏置,也扛不住噪声——你观测到的相关里混了大量随机波动。
图神经网络(GNN)给了一个更聪明的框架:把相关性当成一张图的边,把去噪/聚类变成在图上传播信号。 但 GNN 的数学本质其实没那么玄——它的核心就是把节点信号从「节点域」搬到「谱域」,在谱域按频率加权,再搬回来。
一句话结论:谱图卷积 = 对图拉普拉斯矩阵做特征分解,把节点信号 x 用特征向量 U 旋转到谱域,再套一个谱滤波器 g(λ) 对「频率」加权,最后 U 转回来。 这里的「频率」由拉普拉斯特征值 λ 定义:小 λ 对应跨全图平滑的低频信号(板块共同趋势),大 λ 对应剧烈震荡的高频信号(噪声)。
在我们的 50 资产 / 两板块合成实验上,这套机制可复现地给出:
- 局部 1 跳 GCN(Kipf-Welling 重归一化)去噪:恢复出的板块标签与真值 corr=0.973、分类准确率 1.000,远超原始观测(corr=0.430、acc=0.640)和全局谱低通(corr=0.633、acc=0.740)。
- 谱聚类:取拉普拉斯前两个非平凡特征向量做 2D 嵌入,k-means 恢复板块准确率 0.720——比去噪差一截,因为聚类只看几何结构、没用板块标签做监督。
后文把每个数字讲透,并诚实拆穿五类把 GNN 吹上天的误区。

1. 从相关矩阵到拉普拉斯:图是怎么来的#
谱 GNN 第一步是把「资产之间的关系」变成一个图。我们有 N 个资产,先用 2D 隐因子载荷构造一个**保证半正定(PSD)**的相关矩阵——这比随手编一个相关矩阵重要,因为 PSD 才 genuine 是一个相关矩阵:
import numpy as np
from scipy.linalg import eigh
rng = np.random.default_rng(20260723)
N, HALF = 50, 25
# 两个板块:载荷各自聚在 (1,0) 和 (0,1)
z = np.zeros((N, 2))
z[:HALF] = rng.normal([1.0, 0.0], 0.28, size=(HALF, 2))
z[HALF:] = rng.normal([0.0, 1.0], 0.28, size=(HALF, 2))
G = z @ z.T # 线性核 → 半正定
d = np.sqrt(np.diag(G))
C = G / np.outer(d, d) # 相关矩阵(对角=1, PSD)python然后把它截成非负加权邻接 A(去掉自环)——这是构造图最关键也最容易被忽视的一步:
A = np.clip(C, 0, None) # 只保留正相关边,去掉负相关
np.fill_diagonal(A, 0.0)
A = (A + A.T) / 2 # 对称化python为什么 clip 到非负?因为图的「边」默认是「相似/连通」的语义,负相关边会破坏谱聚类的几何直觉。这一步本身就是一种先验注入——你假设同板块正相关。如果市场里存在「对冲关系」(负相关也代表强联动),单纯 clip 会丢掉信息,这是陷阱 4 要讲的。
有了 A,算归一化(对称)拉普拉斯:
deg = A.sum(axis=1)
Dinv_sqrt = np.diag(1.0 / np.sqrt(deg))
L = np.eye(N) - Dinv_sqrt @ A @ Dinv_sqrt
L = (L + L.T) / 2
lam, U = eigh(L) # 升序:λ0≈0,无负特征值
lam = np.clip(lam, 0, None)
lam_max = lam[-1]pythoneigh 返回升序特征值:λ₀≈0 对应「全 1 常向量」(图上最平滑的信号),λ 越大对应震荡越剧烈的模态。拉普拉斯特征分解是整篇的基石——U 是「图傅里叶基」,λ 是频率轴。
2. 图傅里叶变换与谱滤波器:在频率上动手脚#
普通傅里叶变换用 cos/sin 做基;图傅里叶变换用拉普拉斯特征向量 U 做基。节点信号 x 搬进谱域:
还原回来就是 。关键在于:谱域里每个坐标对应一个频率 λₖ。我们想「去噪」,就去衰减高频、保留低频——套一个谱低通滤波器 g(λ):
def spectral_lowpass(x, alpha=8.0):
g = 1.0 / (1.0 + np.exp(-alpha * (1.0 - lam / lam_max))) # sigmoid 低通
return U @ (g * (U.T @ x))python这个 sigmoid 的形状就是文章封面第二根曲线:小 λ 处 g≈1(保留),大 λ 处 g≈0(滤掉)。α 控制「截止频率」陡不陡——α 越大,越像理想阶跃低通。

3. 局部 1 跳 GCN:为什么它比全局谱低通更狠#
谱低通是「全局最优平滑」——它把每个节点的信号按整张图的频率结构平滑。但金融去噪真正想要的是「相邻板块内平均、跨板块不平均」。这正是 Kipf-Welling GCN 的 1 跳传播做的事:
def gcn_one_hop(x):
Ahat = A + np.eye(N)
dhat = np.sqrt(Ahat.sum(axis=1))
Dhat = np.diag(1.0 / dhat)
Sh = Dhat @ Ahat @ Dhat
return Sh @ xpython加自环(Â = A + I)保证每个节点也保留自己的一份;重归一化 防止度大的节点主导。一次传播 = 每个节点取「邻居(含自己)的加权平均」。
我们造一个低 SNR 去噪任务验证:真值 m = [+1×25, −1×25](板块标签),观测 x = m + 噪声(σ=1.5):
m = np.concatenate([np.ones(HALF), -np.ones(HALF)]).astype(float)
x = m + rng.normal(0, 1.5, N)
def corr(a, b):
a = a - a.mean(); b = b - b.mean()
return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-12))
y_lp = spectral_lowpass(x)
y_loc = gcn_one_hop(x)python结果(数字来自脚本真实输出):
[去噪] 观测 x 与真值 m 相关: corr=0.430 acc=0.640
[去噪] 局部1跳(GCN) 恢复: corr=0.973 acc=1.000
[去噪] 谱低通(全局最优平滑): corr=0.633 acc=0.740
[谱] 拉普拉斯特征值: λ0=0.000 λ1=0.211 λ2=0.913 λmax=1.073plaintext三个诚实结论:
- 局部 GCN 碾压全局谱低通:corr 0.973 vs 0.633。原因干净——GCN 只在同一板块的局部边上平均,跨板块边被图结构天然挡住;谱低通对全图统一平滑,会隔着板块「糊」过去一点。
- 原始观测只有 0.430:σ=1.5 的噪声把板块信号淹了一半,分类准确率才 64%——不做图处理基本分不清两板块。
- 谱低通不是「最优化」:它是个固定 sigmoid 滤波器,没有利用「板块内强相关」的局部结构,所以输给 GCN。所谓「谱滤波最优雅」不等于「谱滤波最有用」(陷阱 2)。
4. 谱聚类:用特征向量做嵌入,把板块在 2D 里分开#
谱聚类的套路是:取拉普拉斯前 k 个非平凡特征向量(跳过 λ₀≈0 的常向量)当新坐标,再做普通 k-means。因为 L 的特征向量按「图的连接结构」排列节点,同板块的节点在新坐标里会聚成一团。
emb = U[:, 1:3] # 跳过平凡 λ0,取第 2、3 特征向量
pred = kmeans2(emb, 2, seed=1) # 文中自写 k-means
acc_clust = max(
float(np.mean(pred == (m > 0).astype(int))),
float(np.mean(pred == (m <= 0).astype(int))),
)
# 实测:acc_clust = 0.720python
注意一个反差:去噪准确率能到 1.000,谱聚类只有 0.720。 根因是分工不同——GCN 去噪用了真值标签 m 的相对符号做对齐(align_sign),本质是「半监督」;谱聚类是纯无监督,只用图的几何,对板块间那 25 只边界模糊的票会判错。这恰好说明:谱方法的能力上限取决于你喂的图结构有多准,而不是方法本身多神。
5. 五个真实陷阱(必须说清)#
陷阱 1:图结构 ≠ 比表格强,图只是换了归纳偏置。 很多人以为「上 GNN 就能学到表格学不到的东西」。事实是:GNN 的增益 100% 来自你构造的图 A 编码了正确的先验(同板块相连)。如果你的邻接矩阵造错了(比如用全相关、不 clip 负相关),GNN 会把对冲关系当连通、把噪声边当信号,性能还不如直接对 C 做 PCA。图是放大器,不是魔法。
陷阱 2:谱低通「最优平滑」是错觉。 谱低通对所有频率套同一个 sigmoid,不会区分「这块噪声」和「那块信号」。它在「全局去噪」任务上不如「局部」GCN,因为金融信号的空间结构是非均匀的。别被「在图傅里叶域操作」的优雅骗了——优雅 ≠ 有用。
陷阱 3:深层谱卷积不一定分得更开。 本文只用 1 跳(一次传播)。理论上可以叠多层、甚至学一组可微谱滤波器参数(ChebNet / GCN / GAT 都是它的特例)。但金融图通常稀疏、板块结构浅,叠多层只会把信号平滑成 λ₀ 常向量(过平滑,over-smoothing)——深度对图不一定友好。
陷阱 4:邻接矩阵怎么造,决定了一切。
我们用 A = clip(C, 0) 注入「同板块正相关」先验。但:
- 用全相关(含负)会让对冲对也被连边,谱聚类把「反向联动」当成「同簇」;
- 用 k-NN(只连最相关的 k 个邻居)能去噪但可能切断弱相连板块;
- 阈值截断的阈值选择本身就要调。 真实落地第一步永远是「邻接矩阵怎么来」,不是「选哪个 GNN 层」。
陷阱 5:金融不能直接当图拿来用。 我们用了 50 资产两板块的合成图,板块标签已知、边干净。真实市场:
- 相关性随时间变(危机时全相关发散),静态 A 会过时;
- 幸存者偏差——退市的票不在图里,回测会虚高;
- 中国 A 股 T+1、涨跌停、卖空不可得,图信号落不成可交易组合。 谱 GNN 是特征/聚类工具,不是印钞机。
6. 它和 PCA / 相关矩阵聚类怎么选#
| 方法 | 用到的结构 | 优点 | 缺点 |
|---|---|---|---|
| PCA on 收益 | 协方差特征向量 | 快、稳定、全样本 | 只抓线性主成分,无「邻接」语义 |
| 相关矩阵阈值聚类 | 边的连通分量 | 直观、可解释 | 阈值敏感、不平滑噪声 |
| 谱聚类 | 拉普拉斯特征向量 | 几何结构清晰、抗噪 | 要先有好邻接 A |
| 谱 GNN 去噪 | 邻接 + 信号传播 | 局部平均、最准 | 依赖 A 质量、易过平滑 |
一句话:谱 GNN 把「相关性 → 图 → 在图上传播信号」这条链做成了可微分、可堆叠的框架;它的威力不在「神经网络」四个字,而在你构造的图 A 有没有编码正确的金融先验。
7. 落地路径#
- 板块/行业发现:用个股收益相关矩阵造 A,谱聚类自动分出未被标签覆盖的「事实板块」。
- 因子去噪:把原始因子值当节点信号 x,GCN 1 跳传播做板块内平滑,滤掉个股特异噪声。
- 风险传染图:危机时用时变相关造 A,看谱聚类怎么重排——传染链会浮出来。
- 真要端到端:上 PyG / DGL,把可微谱滤波器(ChebNet)当层,邻接随相关矩阵滚动更新,别用静态 A。
8. 结论#
谱图神经网络的核心一句话:把 N 个资产的相关矩阵 C 截成邻接 A,算归一化拉普拉斯 L,对 L 特征分解得到图傅里叶基 U 和频率轴 λ;节点信号经 Uᵀ 搬进谱域,套谱滤波器 g(λ) 对频率加权再搬回来,就是图卷积。
我们纯 numpy 从零实现证明:在 50 资产两板块合成图上,局部 1 跳 GCN 去噪 corr=0.973、acc=1.000,远超原始观测(0.430/0.640)和全局谱低通(0.633/0.740);谱聚类恢复准确率 0.720——比去噪低,因它是无监督、只靠几何。
但图的威力 100% 来自你造的邻接 A,不是 GNN 层本身(陷阱 1、4);谱低通优雅却不最优(陷阱 2);深层会过平滑(陷阱 3);真实市场相关性时变 + 幸存者偏差,不能直接拿来用(陷阱 5)。把图当「先验放大器」,它才好用——把它当「免调参魔盒」,必翻车。