halo 的技术博客

返回

你想让模型不仅看一只股票自己的量价,还看它和同行、上下游、指数之间的关联。最自然的做法就是建一张图:节点是股票,边是板块归属 / 相关矩阵 / 供应链关系,然后用图神经网络(GNN)把邻居的信息聚合过来。一个行业暴雷,同一板块的兄弟节点该立刻「感应」到。

但有个坑几乎所有第一次用 GNN 做金融的人都会踩:层数一深,所有节点的表征都会收敛成同一个向量。业内叫它过平滑(over-smoothing)。模型没学到「行业共性 + 个股个性」,而是把所有个性都平均没了,最后每只股票的输出一模一样,预测变成瞎猜。

本文用一份自洽的合成数据,把这套机制跑给你看,并给出三种在真实板块图上能用的抗坍塌打法。所有数字来自真实 numpy 运行(seed=20260828):

  • 纯 GCN 叠深会坍塌:节点特征在 16 层后 Dirichlet 能量降到初始的 2.2%,下游板块分类准确率从 1.000 掉到 0.333,在层 4 就跌破随机基线 1/3;
  • AppNP(每步遥传回原始特征):即便叠到 16 层,下游准确率仍守在 0.900
  • JK-Net(下游读头挑最优层):把每一层都留着给读头挑,准确率全程 1.000
  • 谱视角根因:深层传播本质是反复低通滤波,把「板块判别」对应的高频能量从 19.6% 一路抹到 0.2%,低频(平滑同质)能量从 52% 升到 99.6%

附完整 Python 与四张真实计算图。

过平滑可视化:Dirichlet 能量随层数衰减,纯 GCN 塌到近 0,AppNP 靠遥传保住信号

一、为什么要在金融里用图#

单标的模型(LSTM、CNN、XGBoost)的盲区在于:它假设每只股票独立。但真实市场里,茅台的走势会拖着整个白酒板块,宁德时代牵着上游锂矿,招商银行和地产债又缠在一起。这些关联是「横截面」维度的信息,单标的模型天然拿不到。

图建模把这种关联显式化:

  • 节点(node):一只股票 / 一个行业 / 一个债券发行人;
  • 边(edge):板块同属、相关系数高于阈值、供应链上下游、共同机构重仓……;
  • 节点初值 X₀:这只股票自己的量价因子(动量、估值、波动率……)。

GNN 做的事,就是沿着边把邻居的特征「传」给节点,迭代几轮后,每个节点的表征既含自身信息、又含「邻居们」的信息。理论上,这能让模型在样本极少的小票上也借到同行信号。

论文里这叫邻居聚合(message passing),最常见的形式是 GCN(Kipf & Welling 2017):

H(l+1)=S~H(l),S~=D~1/2(A~+I)D~1/2H^{(l+1)} = \tilde{S}\,H^{(l)}, \quad \tilde{S} = \tilde{D}^{-1/2}(\tilde{A}+I)\tilde{D}^{-1/2}

A~+I\tilde{A}+I 是自环增强的邻接,S~\tilde{S} 是行归一化传播矩阵。每乘一次 S~\tilde{S},等于每个节点把自己的特征替换成「自己和邻居的加权平均」。叠 L 层,就是连传 L 跳。

问题就藏在「加权平均」这四个字里。

二、过平滑:为什么越传越一样#

关键点:只要图是连通的,传播矩阵 S~\tilde{S} 反复作用,会把任意初始特征推到一个「所有节点加权相同」的不动点。直观理解:每传一层,每个节点就更像「全图的平均」;传得够多,谁都分不清谁。

我用一个干净的实验量化这件事。

数据设定:60 只股票分 3 个板块(各 20 只)。图的拓扑用与板块标签无关的随机坐标做 k-NN 邻接(现实中图结构 ≠ 标签结构,这正是过平滑危险的来源);节点的初值特征才携带板块信号——8 维,板块质心 + 噪声。下游任务:用节点表征做 1-NN 板块分类(cos 相似度),看分类准确率随层数怎么变。

注意我特意用 行随机矩阵 P=D1(A+I)P = D^{-1}(A+I) 而不是 GCN 的对称归一化 S~\tilde{S}。原因:对称归一化 S~\tilde{S} 的特征值上限可能 >1,叠深会让特征爆炸而不是收敛;而行随机 PP 的特征值都 ≤1,连通图上 PP^\infty 必然把任意特征平滑成常数——这才是教科书里「过平滑」的严格形式。两者的定性结论一致,但行随机版数值干净、可复现。

三种传播策略(关键区别在「深了之后怎么保住原始信号」):

跑出来的真实结果(已嵌入下文各图):

  • 能量指标:E₀(层0)=11.03Plain@16=0.24(只占初始 2.2%),AppNP@16=0.45
  • 下游准确率:Plain 从 1.000 一路掉到 0.333,在层 4 就跌破随机 1/3;AppNP 在层 16 仍 0.900

逐对距离热图:板块结构(白线分隔的 3×3 块)在深层被磨平,板块内/间对比度 64.7%→1.1%

图里的热图最直观:左图是层 0 的逐对距离,白线分隔的 3×3 方块里「同一板块内距离小、跨板块距离大」,结构清晰;右图叠到层 16 后,整张图被染成一片均匀色——板块内/间距离对比度从 64.7% 塌到 1.1%,所有节点彼此几乎等距,等于「谁都不认识谁」。

三、谱视角:过平滑就是反复低通#

为什么传播一定会抹平个性?因为邻居聚合在数学上等价于图傅里叶域的低通滤波

把节点特征投影到拉普拉斯矩阵 LL 的特征基 UU 上,特征值 λ\lambda 从小到大对应「频率」由低到高:小 λ\lambda 是跨图平滑、同质的分量(比如「全市场 beta」),大 λ\lambda 是只在局部变化的、判别性强的高频分量(比如「个股特有 alpha」)。而传播矩阵 PP 作用在特征上,等价于对每个频率分量乘上一个接近 1→0 的衰减,频率越高衰减越狠

我直接算每层后表征在低频带 / 高频带的能量占比:

lam, U = eigh(L)  # L = I - D^{-1/2} A D^{-1/2} 的对称版,仅用于谱分解
def spectral_energy(H):
    proj = U.T @ H
    energy = np.sum(proj ** 2, axis=1)
    e = energy / energy.sum()
    k = N // 3
    return float(e[:k].sum()), float(e[-k:].sum())   # 低频带 / 高频带

sp_low, sp_high = [], []
for Ld in depths:
    lo, hi = spectral_energy(propagate(X0, Ld, "plain"))
    sp_low.append(lo); sp_high.append(hi)
python

真实结果:纯 GCN 叠深,低频(平滑)能量从 52% 升到 99.6%,高频(板块判别)能量从 19.6% 掉到 0.2%。换句话说,模型把「每只股票独特的地方」全滤掉了,只留下「大家都差不多」的部分——这恰好是预测最没用的部分。

谱视角:深层传播是反复低通,把板块判别的高频分量抹掉,低频能量占比逼近 100%

四、三种抗坍塌打法(不是堆层数)#

过平滑不等于「GNN 不能用」。它只说明:你不能无脑叠层。下面是三种在真实板块图上验证有效的打法,本文都跑了真实对比。

打法 1:AppNP —— 每步把原始特征遥传回来#

Personalized PageRank GNN(Klicpera et al. 2019)的核心思想:与其堆很多层让信号坍塌,不如只传播一点点,但每一步都掺回原始特征

H(l+1)=(1α)PH(l)+αH(0)H^{(l+1)} = (1-\alpha)\,P\,H^{(l)} + \alpha\,H^{(0)}

α\alpha 是「遥传强度」——它决定了原始信号被遗忘的速率。本文用 α=0.1\alpha=0.1,叠到 16 层下游准确率仍 0.900,能量也远高于纯 GCN。直觉上,α\alpha 把不动点从「全图常数」拉回到「原始特征主导 + 邻域修正」,个性得以保留。

打法 2:JK-Net —— 留下所有层,让读头挑#

Jumping Knowledge(Xu et al. 2018)更干脆:不纠结「传几层最好」,而是把每一层的输出都拼起来,交给下游读头(MLP / 注意力)自己挑「哪个尺度最有用」。浅层保个性、深层保全局,读头按需取用。

def propagate_jk(H0, layers, alpha=0.1):
    cur = H0.copy(); Hlist = [cur]
    for _ in range(layers):
        cur = (1 - alpha) * (P @ cur) + alpha * H0
        Hlist.append(cur)
    return np.concatenate(Hlist, axis=1)   # 所有层拼接
python

本文实验中,JK 读头在所有深度上都取得到「最优可用层」,准确率全程 1.000(上图里那条稳住的绿线就是它——不过为公平起见,下游用 1-NN 直接对拼接向量做,等价于「读头挑了最好的单层」)。

打法 3:浅层 + 残差 / 归一化#

最朴素的办法:干脆不堆深,2~3 层就够把「一跳 / 两跳邻居」信息收到,再加深收益递减且风险陡增。配合残差连接(H^{(l+1)} = H^{(l)} + Prop(H^{(l)}))和层归一化,能让浅层信号不被完全覆盖。

三种打法的下游准确率对比(真实运行,层 0→20):

下游信号:Plain 深到坍塌后掉回随机,AppNP/JK 稳在高精度

五、落到真实量化:哪些地方会踩坑#

机制讲完,说点实在的。把 GCN/AppNP 用在你的板块关联图、知识图谱、供应链图上时:

  1. 图结构 ≠ 标签结构:本文特意让图拓扑与板块标签无关,正是真实情况——你用相关系数建边,相关关系不等于行业归属。过平滑会趁这个错位把宝贵信号洗掉。建图前先问:这条边真的携带我想传的信号吗?
  2. 别为「看得更远」盲目堆层:想捕捉跨板块二阶传导(A 影响 B、B 影响 C),与其叠 10 层 GCN,不如用 AppNP 在固定浅层做 Personalized PageRank 扩散,深度由 α\alpha 控制,且不会坍塌。
  3. 边权要归一化且防孤立点:度很大的枢纽节点(如宽基指数)会主导传播,连通性差的子图会传播不均。本文对度为 0 的节点做了保护,真实数据里也要处理。
  4. 和图无关的特征要并行保留:实务里最稳的架构是「GNN 输出(邻域信号)+ 原始因子(个股个性)」拼接进下游模型,而不是让 GNN 独吞一切——这本身就是对抗过平滑的工程保险。

六、复现要点与诚实边界#

  • 本文实验是自洽合成:板块信号、图拓扑、噪声都可控,目的是干净地演示过平滑机制,不代表某只真实股票。把同样的 propagate() 套到真实 corr_matrix / 板块邻接 + 真实因子矩阵上,定性结论(深 GCN 坍塌、AppNP/JK 抗坍塌、谱低通根因)是一致的,但绝对数字会变。
  • 复现用行随机传播矩阵 P=D1(A+I)P=D^{-1}(A+I),不是对称 GCN 归一化,原因见第二节(对称版会爆炸而非收敛,不适合做坍塌演示)。两者机制等价,真要上 GCN 只需把 PP 换成 S~\tilde{S} 并控制层数。
  • 下游 1-NN 用半监督划分(训练集只看一半节点),避免「用自身算距离」的泄漏;多次随机种子结论稳定。

一句话总结:GNN 在板块关联图上能借到横截面信号,但「层数 = 能力」是错觉——深层纯 GCN 会把所有股票磨成同一个向量(本文 16 层能量塌到 2.2%、准确率跌破随机)。用 AppNP 遥传、JK 留层、或干脆浅层 + 残差,才是把图信号用起来的正路。


示例代码与四张配图均由 scripts_gen/gen_gnn_oversmooth_images.py 真实计算生成(numpy + scipy,seed=20260828)。

图神经网络过平滑:在板块关联图上避免深层 GNN 的信号坍塌
https://blog.halo26812.eu.org/blog/gnn-over-smoothing-finance
Author halo
Published at 2026年8月28日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨