图注意力网络(GAT)原理
Graph Attention Network,简称 GAT。说实话,我第一次看到这个论文标题的时候,第一反应是:又给 GCN 加了个 attention 壳子?但真正看完代码和推导,我发现这事儿没那么简单。
GAT 的核心思想很直白:让模型学会自己判断哪些邻居更重要。而不是像 GCN 那样,所有邻居一视同仁,权值全靠度矩阵算死。你想想看,在分子图里,某个原子旁边的官能团,有的对活性贡献极大,有的就是个旁观者。GCN 分不清,GAT 就能。
注意力机制在图上怎么玩?
标准 attention 在 NLP 里是 query 和 key 做点积,然后 softmax 得到权重。图上的 attention 也类似,但需要把节点特征和邻居特征对齐到同一个空间。
我个人习惯把 GAT 的注意力计算拆成三步:
- 线性变换:每个节点特征乘一个共享的权重矩阵 W,映射到高维空间。
- 注意力打分:把目标节点和邻居节点的变换后特征拼接,再过一个单层 MLP,得到标量分数。
- 归一化:对每个节点的所有邻居做 softmax,得到最终注意力系数。
公式其实就两行,但很多初学者容易在维度上搞混。我这里贴一个我常用的 PyTorch 风格实现片段:
class GATLayer(nn.Module):
def __init__(self, in_dim, out_dim):
super().__init__()
self.W = nn.Linear(in_dim, out_dim, bias=False)
self.a = nn.Linear(2 * out_dim, 1, bias=False)
self.leaky_relu = nn.LeakyReLU(0.2)
def forward(self, h, adj):
# h: [N, in_dim]
Wh = self.W(h) # [N, out_dim]
N = Wh.size(0)
# 扩展成 [N, N, out_dim] 方便广播
Wh_i = Wh.unsqueeze(1).expand(-1, N, -1)
Wh_j = Wh.unsqueeze(0).expand(N, -1, -1)
# 拼接后打分
e = self.leaky_relu(self.a(torch.cat([Wh_i, Wh_j], dim=-1)))
e = e.squeeze(-1) # [N, N]
# 用邻接矩阵 mask 掉非邻居
e = e.masked_fill(adj == 0, float('-inf'))
attn = torch.softmax(e, dim=-1)
h_out = torch.mm(attn, Wh)
return h_out
嗯,这里要注意维度扩展那两步。我之前在项目里写的版本没做 expand,直接循环,慢了不止一个数量级。后来被同事提醒才改过来的。
多头注意力:为什么需要多个头?
单头注意力有个问题:容易过拟合到某些特定的邻居模式。尤其是在图很小、结构又对称的情况下,注意力系数可能退化成几乎均匀分布,这就失去了 attention 的意义。
多头注意力的做法是:
- 独立训练 K 个注意力头,每个头有自己的 W 和 a 参数。
- 中间层:每个头分别计算注意力,得到独立的节点表示,然后拼接在一起。
- 输出层:所有头的表示取平均(或者求和),再经过一个非线性激活。
我在分子性质预测项目里用过 8 个头,效果确实比 4 个头好,但再增加到 16 个头就没什么提升了,反而参数量翻倍。这个经验数字你可以参考。
多头注意力的核心好处是:不同头能关注到不同的化学环境。比如一个头可能对电负性敏感,另一个头对空间位阻敏感。最后拼接起来,相当于模型拥有了多视角的理解能力。
GAT 与 GCN 的本质差异
很多人问我:GAT 不就是把 GCN 的固定邻接权重换成了可学习的注意力权重吗?
表面上看是的。但深层次看,这两者的区别在于归纳偏置:
| 对比维度 | GCN | GAT |
|---|---|---|
| 聚合权重 | 由度矩阵静态决定 | 由特征动态学习 |
| 对异质图的适应 | 差,同质假设 | 好,可自适应 |
| 对噪声邻居的鲁棒性 | 低,噪声会平等污染 | 高,可以给低权重 |
| 计算复杂度 | 较低,O(E) | 稍高,O(E * d) |
| 可解释性 | 几乎没有 | 注意力系数可视为边重要性 |
说白了,GCN 适合那种“邻居都很重要”的场景,比如社交网络里的朋友关系。但分子图完全不是这么回事——同一种官能团,在不同位置可能起截然不同的作用。GAT 在这里明显更合适。
GAT 的变体与进阶
原版 GAT 发表之后,陆续出了几个改进版本,我这里挑了跟 QSAR 关系最密切的两个:
- GATv2:把注意力打分从拼接后的线性层改成了先加再激活,表达能力更强。我发现对环状结构的识别有明显提升。
- GaAN(门控注意力网络):在多头注意力之上加了一个门控机制,让不同头的贡献权重可以调节。适合处理多尺度化学环境。
避坑指南
另外,GAT 的注意力系数虽然可以解释,但不能直接当“特征重要性”来用。它是在图结构约束下的局部重要性,不是全局打分。我见过不少人把注意力系数抽出来当 SHAP 值用,结果得出完全错误的化学结论。
最后一点,数值稳定性。上面代码里我用了 masked_fill 把非邻居位置填成 -inf,这招在 CPU 上没问题。但是在 GPU 上,float32 的 -inf 导致 softmax 梯度出 NaN 的情况偶有发生。我现在的做法是在 masked_fill 之后手动加一个很小的 eps:
e = e.masked_fill(adj == 0, -1e9)
attn = torch.softmax(e, dim=-1)
小结一下
GAT 给图神经网络带来的,不是简单的“加 attention”,而是让图学习从结构驱动走向了特征驱动。在 QSAR 这样的领域,分子的结构和性质之间存在着非常复杂的映射关系。GAT 通过注意力机制,为每个分子动态构建了最适合当前任务的邻居权重,这正是它比 GCN 更适用的根本原因。
当然,GAT 不是万能药。如果你的分子图里所有邻居都差不多重要,GAT 的 attention 反而增加不必要的方差。但这种情况在真实化学数据集里很少见。