图注意力金融网络:用注意力权重重新分配板块间的信息流
板块不是孤岛——半导体一动,消费电子、新能源车、光伏跟着抖。传统 GCN 用固定归一化权重平均所有邻居,把真信号和噪声边一视同仁地糊在一起。图注意力网络 GAT 换思路:给每条边学一个注意力分数 α,让模型自己决定『该多听谁的』。本文用纯 numpy 从零实现单层 GAT(含通过 masked-softmax 的完整反向传播),构造 10 个板块的有向信息流图,故意混入 9 条『市场惯性误认』的噪声边(图上有连接但真实传导=0)。500 个时间点训练后样本外对比:GAT 的 IC 0.670,甩开被噪声边拖累的 GCN(0.625)和完全无图的 MLP(0.577)。注意力热图显示 GAT 学到的 α 与真实传导 β 高度吻合、对噪声边自动降权;hub 分析揭示 GAT 给『消费电子→半导体』分配 0.495 权重(GCN 固定只给 0.316),因为它读得懂『这条边真的重要』。拆穿注意力=因果、单层感受野局限、注意力可解释性幻觉、图结构错了 GAT 也救不了四类陷阱(中阶)。
先说结论:板块之间的信息流是非对称、稀疏、且强弱悬殊的——半导体对下游的传导远强于白酒对银行,而传统图卷积(GCN)用固定权重平均所有邻居,等于把这种差异一笔抹平。图注意力网络(GAT)的价值就在这里:它给每条边学一个注意力分数,让模型自己决定”该多听谁的”。本文用纯 numpy 从零实现单层 GAT,在一个故意混入噪声边的板块图上,样本外 IC(0.670)明显高于被噪声拖累的 GCN(0.625)和完全无视图结构的 MLP(0.577)。
一、为什么”平均邻居”是个坏主意#
图神经网络的核心操作是聚合邻居信息。GCN 的做法简单粗暴:把每个节点的邻居特征做归一化平均。用矩阵写就是:
其中 是对称归一化的邻接矩阵。问题在于 是固定的——它只取决于图的连接结构(谁连谁),不取决于连接的强弱。
这在金融图上是致命的。想象一个板块图:
- 半导体 → 消费电子(强传导,真实系数 0.9)
- 半导体 → 新能源车(强传导,0.8)
- 白酒 → 银行(几乎无关,市场惯性误认的弱边,0.15)
GCN 会用几乎一样的权重去平均这些邻居,把 0.9 的强信号和 0.15 的噪声搅在一起。更糟的是——真实的板块图里充满了”看起来相关但实际无关”的噪声边:某两个板块历史上凑巧齐涨齐跌,就被相关性矩阵连成了边,但根本没有因果传导。GCN 对这些噪声边照单全收。
GAT 的回答:让权重可学习。
二、GAT 的核心:给每条边打一个注意力分数#
GAT 把固定的 换成可学习的注意力矩阵 。对每条边 ,先算一个原始分数:
这里 把节点特征投影到隐藏空间, 是可学习的注意力向量。然后对每个节点 的所有邻居做 masked softmax(只在图上真实存在的边之间归一化):
最后加权聚合:
对比 GCN,唯一但关键的差别是: 依赖节点特征、随数据学习,而 GCN 的权重只依赖图结构、固定不变。这让 GAT 能对同一个节点的不同邻居分配天差地别的权重。
三、构造一个”带噪声边”的板块图#
要公平地展示 GAT 的价值,得让图里既有真信号边、又有噪声边——否则如果所有边都同等重要,GCN 的均匀平均反而是最优的(这也是很多”GAT 打平 GCN”实验的真相)。
import numpy as np
SECTORS = ["半导体", "消费电子", "新能源车", "光伏", "锂电材料",
"医药", "白酒", "银行", "地产", "券商"]
N = len(SECTORS)
F = 6 # 每个板块的因子特征维度
# 真实信息流:半导体是 hub,对下游强传导
true_flow = {(0,1):0.9, (0,2):0.8, (0,3):0.7, (2,4):0.5, (3,4):0.4,
(5,6):0.15, (7,8):0.5, (7,9):0.4, (8,9):0.35, (1,2):0.3, (4,2):0.25}
# 邻接矩阵:真实边 + 故意混入的噪声边(图上有连接但真实传导=0)
edges = list(true_flow.keys()) + [
(6,0),(8,0),(9,0), # 白酒/地产/券商 误连到半导体
(4,7),(2,5),(1,8),(3,9),(5,0),(6,7) # 跨链噪声边
]
for i in range(N):
edges.append((i, i)) # 自环
A = np.zeros((N, N))
for (j, i) in edges:
A[i, j] = 1.0 # A[i,j]=1 表示 i 能看到 jpython节点特征和目标的生成——每个板块下一期收益 = 自身动量 + 邻居传导 + 噪声:
T = 500
rng = np.random.default_rng(7)
X_seq = rng.standard_normal((T, N, F))
own = X_seq[:, :, 0] # 每个板块自身动量因子
BETA = np.zeros((N, N))
for (j, i), b in true_flow.items():
BETA[i, j] = b
Y = np.zeros((T, N))
for t in range(T):
spill = BETA @ own[t] # 只有真实边传导,噪声边贡献=0
Y[t] = 0.5 * own[t] + spill + 0.5 * rng.standard_normal(N)python注意 Y 的生成只用了 BETA(真实传导),噪声边虽然在邻接矩阵 A 里,但对 Y 零贡献。这就是考验:GCN 会被迫平均噪声边,GAT 有机会学会忽略它们。
下图是这个板块图的结构,箭头粗细代表真实传导强度:

半导体(橙色 hub)向下游射出几条粗边,其余是背景细边和噪声边。
四、GAT 的关键:masked-softmax 的完整反向传播#
很多”从零实现”到这里就偷懒用固定注意力了。但 GAT 的灵魂恰恰是注意力能被训练,所以必须实现穿过 softmax 的反向传播。核心是 softmax 的雅可比:
def masked_softmax(e, mask):
e = np.where(mask > 0, e, -1e9)
e = e - e.max(axis=1, keepdims=True)
ex = np.exp(e) * mask
return ex / (ex.sum(axis=1, keepdims=True) + 1e-12)
def backward_gat(err, cache, W, a_src, a_dst, v):
H, Xt, alpha, z_pre = cache["H"], cache["Xt"], cache["alpha"], cache["z_pre"]
N_ = len(err)
dpred = (2.0 * err / N_).reshape(-1, 1)
dv = cache["Hagg"].T @ dpred
dHagg = dpred @ v.T
dH_value = alpha.T @ dHagg # 走 value 路径的梯度
dalpha = dHagg @ H.T # 走注意力权重的梯度
# softmax backward:de = alpha * (dalpha - sum_row(dalpha*alpha))
gvec = (dalpha * alpha).sum(1, keepdims=True)
de = alpha * (dalpha - gvec)
dz = de * np.where(z_pre > 0, 1.0, 0.2) # LeakyReLU 导数
rowsum, colsum = dz.sum(1), dz.sum(0)
da_dst = H.T @ rowsum
da_src = H.T @ colsum
dH = dH_value + rowsum[:,None]*a_dst[None,:] + colsum[:,None]*a_src[None,:]
dW = Xt.T @ dH
return dW, da_src, da_dst, dv, float(dpred.sum())python那行 de = alpha * (dalpha - gvec) 就是 softmax backward 的精华——它保证了梯度在归一化约束(每行注意力和为 1)下正确流动。没有这一步,注意力就学不动,GAT 退化成随机加权的 GCN。
五、训练结果:GAT 甩开被噪声拖累的 GCN#
用前 400 个时间点训练、后 100 个样本外评估,对比 GAT / GCN / MLP(无图基线):

样本外表现:
| 模型 | IC | RMSE | 说明 |
|---|---|---|---|
| GAT | 0.670 | 0.671 | 学会给噪声边降权 |
| GCN | 0.625 | 0.718 | 被迫平均所有邻居(含噪声) |
| MLP(无图) | 0.577 | 0.769 | 完全忽略板块间传导 |
三个层次很清楚:
- MLP 最差(0.577):它只看每个板块自己的因子,完全无视”半导体一动下游跟着抖”这个真实结构。这证明图信息确实有价值。
- GCN 提升有限(0.625):它用上了图,但被噪声边拖累——那 9 条真实传导为 0 的边,GCN 照样平均进去,稀释了真信号。
- GAT 最优(0.670):比 GCN 高 4.5 个 IC 点。差距全部来自”对噪声边降权”的能力。
六、注意力学到了什么:α 对齐真实传导 β#
GAT 的可解释性体现在注意力矩阵上。下图左边是真实传导强度 ,右边是 GAT 学到的注意力权重 :

两张图的高亮区高度重合——GAT 学到的注意力集中在半导体→消费电子/新能源车/光伏这些真实强传导的边上,而对噪声边(比如白酒→半导体)分配了很低的权重。这不是我们手工设定的,是模型从”预测下一期收益”这个目标里自己学出来的。
更直接的对比是 hub 节点的权重分配。下游板块”该给半导体多少注意力”:

| 下游板块 | 真实传导 β | GAT 注意力 α | GCN 固定权重 |
|---|---|---|---|
| 消费电子 | 0.9 | 0.495 | 0.316 |
| 新能源车 | 0.8 | 0.249 | 0.224 |
| 光伏 | 0.7 | 0.493 | 0.316 |
关键观察:GAT 的 α 随真实 β 起伏(消费电子传导最强,GAT 给的注意力 0.495 也最高),而 GCN 的权重几乎一视同仁(0.316/0.224/0.316,只反映图的度数结构,不反映传导强弱)。这就是”重新分配信息流”的字面含义。
七、四个必须拆穿的陷阱#
陷阱一:把注意力权重读成因果关系。 GAT 学到”消费电子对半导体注意力高”,只能说明这条边对预测有用,不能说明”半导体因果驱动消费电子”。注意力是相关性驱动的预测权重,不是因果推断。本文里我们知道 β 是真实因果(因为是自己造的),但在真实数据上,高注意力可能来自共同因子、反向因果或纯粹的样本内巧合。别拿注意力热图当因果图讲故事。
陷阱二:单层 GAT 的感受野只有一跳。 单层 GAT 只能聚合直接邻居。如果信息是”半导体 → 新能源车 → 锂电材料”这样两跳传导的,单层 GAT 看不到半导体对锂电材料的间接影响。堆叠多层能扩大感受野,但会引入过平滑(层数一多,所有节点表征趋同)。层数是个需要谨慎调的超参,不是越深越好。
陷阱三:注意力可解释性是有条件的幻觉。 “GAT 可解释”这个卖点被过度神话了。注意力权重是否对齐真实结构,取决于信号足够强、噪声边足够可分。本文里噪声边的真实传导是干净的 0,所以 GAT 学得出来;真实数据里噪声边往往有微弱的伪相关,GAT 可能给它们分配不小的注意力。注意力可解释 ≠ 注意力正确,还得用样本外表现和结构稳定性交叉验证。
陷阱四:图结构错了,GAT 也救不了。 GAT 只能在给定的邻接矩阵上重新分配权重——它能给噪声边降权,但无法凭空补上一条图里根本不存在的真实边。如果你的板块图漏掉了”医药 → 半导体”这条真实传导边,GAT 永远学不到它,因为 masked-softmax 只在已有边上归一化。图的构建(谁连谁)比注意力机制更底层、更重要。garbage graph in, garbage attention out。
八、落地路径#
本文用合成数据把机制讲透,真实落地时的关键改动:
- 图的构建:板块邻接可以用供应链上下游、资金流向、或滚动相关性阈值来定义。建议宁可多连边(让 GAT 去降权),也别漏掉真实边(GAT 补不回来)。
- 特征工程:节点特征换成真实因子(动量、估值、波动率、资金流),边特征也可以引入(比如供应链敞口比例)。
- 多头注意力:真实 GAT 用多头(multi-head)注意力提升稳定性——多个独立注意力头并行、结果拼接或平均,能缓解单头的方差。
- 验证方式:严格 walk-forward,训练期在测试期之前。注意力的可解释性要在多个时间窗上检查是否稳定——如果注意力权重每个季度都翻天覆地,那它学到的多半是噪声。
图注意力的本质是”让模型自己决定该听谁的”。它在图里既有真信号边又有噪声边时价值最大——如果你的图很干净、所有边都同等重要,老实用 GCN 就好,GAT 的额外复杂度换不来收益。判断标准很简单:你的板块图里,有多少条边是”市场惯性误认的伪相关”?越多,GAT 越值得。