halo 的技术博客

返回

你有一篮子资产,想知道「哪几只其实属于同一个板块」。最直觉的做法是看相关性矩阵:同板块的票一起涨一起跌,相关性高;跨板块的低。但相关性矩阵是个 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,算归一化(对称)拉普拉斯

L=ID1/2AD1/2,D=diag(A1)L = I - D^{-1/2} A\, D^{-1/2},\qquad D = \mathrm{diag}(A\mathbf 1)

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]
python

eigh 返回升序特征值:λ₀≈0 对应「全 1 常向量」(图上最平滑的信号),λ 越大对应震荡越剧烈的模态。拉普拉斯特征分解是整篇的基石——U 是「图傅里叶基」,λ 是频率轴。


2. 图傅里叶变换与谱滤波器:在频率上动手脚#

普通傅里叶变换用 cos/sin 做基;图傅里叶变换用拉普拉斯特征向量 U 做基。节点信号 x 搬进谱域:

x^=Ux\hat x = U^\top x

还原回来就是 x=Ux^x = U \hat x。关键在于:谱域里每个坐标对应一个频率 λₖ。我们想「去噪」,就去衰减高频、保留低频——套一个谱低通滤波器 g(λ):

y=Udiag(g(λ))Uxy = U\,\mathrm{diag}(g(\lambda))\,U^\top x

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(滤掉)。α 控制「截止频率」陡不陡——α 越大,越像理想阶跃低通。

去噪恢复能力:观测 / 局部 GCN / 谱低通 / 神谕 的相关系数与分类准确率


3. 局部 1 跳 GCN:为什么它比全局谱低通更狠#

谱低通是「全局最优平滑」——它把每个节点的信号按整张图的频率结构平滑。但金融去噪真正想要的是「相邻板块内平均、跨板块不平均」。这正是 Kipf-Welling GCN 的 1 跳传播做的事:

A^=A+I,S^=D^1/2A^D^1/2,y=S^x\hat A = A + I,\qquad \hat S = \hat D^{-1/2}\hat A\,\hat D^{-1/2},\qquad y = \hat S\,x

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 @ x
python

加自环(Â = A + I)保证每个节点也保留自己的一份;重归一化 D^1/2A^D^1/2\hat D^{-1/2}\hat A\hat D^{-1/2} 防止度大的节点主导。一次传播 = 每个节点取「邻居(含自己)的加权平均」。

我们造一个低 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.073
plaintext

三个诚实结论:

  • 局部 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.720
python

谱聚类嵌入:两板块在图傅里叶空间自然分离

注意一个反差:去噪准确率能到 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. 落地路径#

  1. 板块/行业发现:用个股收益相关矩阵造 A,谱聚类自动分出未被标签覆盖的「事实板块」。
  2. 因子去噪:把原始因子值当节点信号 x,GCN 1 跳传播做板块内平滑,滤掉个股特异噪声。
  3. 风险传染图:危机时用时变相关造 A,看谱聚类怎么重排——传染链会浮出来。
  4. 真要端到端:上 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)。把图当「先验放大器」,它才好用——把它当「免调参魔盒」,必翻车。

谱图神经网络:把关联矩阵当成拉普拉斯信号
https://blog.halo26812.eu.org/blog/spectral-gnn-financial
Author halo
Published at 2026年7月23日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨