3. 图的注意力机制原理:自注意力、图注意力、softmax归一化

注意力机制,说白了就是让模型学会「该看哪」。我在做分子性质预测时,最开始用GCN对所有邻居一视同仁,结果发现某个官能团其实对活性贡献更大——GCN根本抓不住。换成GAT之后,模型自动把注意力集中在关键原子上,效果提升很明显。

这一节咱们把注意力机制的三个核心概念拆开讲:自注意力、图注意力、softmax归一化。理解这些,你才算真正懂了GAT。

3.1 自注意力机制:先学会「自己看自己」

自注意力(Self-Attention)是Transformer的基石。它让每个节点自己决定「我该多关注谁」。

回忆一下经典公式:
Attention(Q,K,V) = softmax(QK^T/√d)V

其中Q、K、V分别代表查询、键、值。每个节点都算自己的Q、K、V,然后计算它与所有其他节点之间的注意力分数。

我个人习惯把Q想象成「我想问的问题」,K想象成「我能回答什么」,V就是「我实际提供的信息」。分数越高,说明这个邻居的回答越匹配我的问题。

举个具体例子:假设只有一个batch,3个节点,特征维度d=4。

import torch
import torch.nn.functional as F

# 假设3个节点,每个节点4维特征
X = torch.randn(3, 4)          # [3, 4]
W_q = torch.randn(4, 4)        # 可学习矩阵
W_k = torch.randn(4, 4)
W_v = torch.randn(4, 4)

Q = X @ W_q                     # [3, 4]
K = X @ W_k                     # [3, 4]
V = X @ W_v                     # [3, 4]

# 计算相似度分数  [3, 3]
scores = Q @ K.T / (4 ** 0.5)   # 缩放因子√d=2
attn_weights = F.softmax(scores, dim=-1)  # 对每一行归一化
output = attn_weights @ V       # [3, 4]
print(output.shape)

这个输出就是自注意力后的表示。每个节点的表示现在融合了全部其他节点的信息,权重由注意力分数决定。

自注意力的关键:所有节点之间全连接计算。这在分子图里有个问题——分子中原子只和相邻原子有化学键,全连接会引入大量无关信息。

所以,图注意力机制应运而生。

3.2 图注意力机制:只看「邻居」的注意力

图注意力(Graph Attention)跟自注意力最大的区别就是:只在邻居节点之间计算注意力。这样既保留了注意力动态分配权重的能力,又遵循了图的拓扑结构。

GAT的论文里用了一个单层前馈网络来计算注意力系数:

e_ij = LeakyReLU( a^T [ W h_i || W h_j ] )

其中||表示向量拼接。这个e_ij就是节点i和邻居j之间的原始注意力分数。然后对每个节点的所有邻居做softmax归一化,得到α_ij。

我曾在一个配体-蛋白对接项目里用GAT,一开始没处理好自环(self-loop)。每个节点应该关注自身的信息,我忘了在邻居集合里包含节点自己,结果模型收敛很慢。嗯,这里要注意:GAT实现时通常会给每个节点加一条自环边。

下面给出带掩码的图注意力计算伪代码:

def graph_attention(H, adj, W, a):
    # H: [N, d] 节点特征, adj: [N, N] 邻接矩阵(含自环)
    Wh = H @ W                     # [N, d_out]
    # 拼接所有节点对的特征
    Wh_i = Wh.unsqueeze(1).expand(-1, N, -1)  # [N, N, d_out]
    Wh_j = Wh.unsqueeze(0).expand(N, -1, -1)  # [N, N, d_out]
    concat = torch.cat([Wh_i, Wh_j], dim=-1)  # [N, N, 2*d_out]
    e = F.leaky_relu(concat @ a.squeeze(-1), negative_slope=0.2)  # [N, N, 1]
    # 掩码:只保留邻居(包括自身)
    e = e.squeeze(-1).masked_fill(adj == 0, -1e9)
    alpha = F.softmax(e, dim=-1)    # [N, N]
    out = alpha @ Wh                # [N, d_out]
    return out
掩码时一定要用很大的负数(比如-1e9)而不是0,否则softmax后非邻居位置仍有非零权重。我曾经踩过这个坑,导致注意力流到了不相干的原子上,分子性质预测直接崩了。

自注意力和图注意力的核心差异,我整理成下表:

维度自注意力 (Transformer)图注意力 (GAT)
注意范围全部节点(全局)邻居节点(局部)
掩码方式序列掩码(因果)邻接矩阵掩码
计算复杂度O(N²)O(E) 边数
分子图适用性不直接适用,无视化学键天然适合,利用拓扑
可学参数W_q, W_k, W_vW, a(单层网络)

3.3 Softmax归一化:把「原始分数」变成「权重分布」

softmax是注意力机制里的最后一步,也是最容易被忽略的一步。它把原始分数转换成一个概率分布,所有邻居的注意力权重之和为1。

公式很简单:

α_ij = exp(e_ij) / Σ_{k∈N(i)} exp(e_ik)

为什么不用普通的归一化(比如除以和)?你想想看,原始分数可能是负的,而且数值范围很大。softmax通过指数函数把任何实数映射到正数,再归一化,既避免负数权重,又放大差异——分数高一点,权重就会明显高出一截。

我处理过一个分子数据集,有些原子度数很高(比如金属中心),邻居数多达10几个。如果不做softmax,模型会把注意力均分给每个邻居,等于没学到重点。加入softmax后,模型学会了把大部分注意力集中在关键配位原子上。

实际使用时,注意数值稳定性。PyTorch的F.softmax内部会自动减去最大值,所以一般不需要手动处理。但如果自己手动实现,记得做一步 e = e - e.max(dim=-1, keepdim=True)[0] 防止exp溢出。

这里还有一个小细节:GAT论文里在计算e_ij时用了LeakyReLU,而标准的Transformer用的是无激活的线性层。为什么呢?我个人猜测是为了引入非线性,让注意力分数更灵活——毕竟图结构复杂,线性打分可能不够用。

下图总结了从自注意力到图注意力的完整流程,以及softmax的角色:

自注意力 vs 图注意力 核心流程 输入特征 X [N, d] Q / K / V 线性变换 全连接 Q·Kᵀ/√d Softmax 归一化 加权求和 (α·V) 自注意力 输入特征 H [N, d] W 线性变换 -> Wh 邻居拼接 Wh_i || Wh_j LeakyReLU(a·[||]) 邻接掩码 + Softmax 只对邻居归一化 加权求和 (α·Wh) 图注意力 关键区别:全连接 vs 邻居掩码

从SVG图可以清楚看到:左边自注意力全连接所有节点,右边图注意力先用邻接矩阵「剪枝」,只让信息沿着边流动。而softmax归一化是两者最后的共同环节,只不过图注意力的归一化分母只加和邻居。

理解这些后,你就能看懂GAT的每一行代码了。下一期咱们直接动手实现一个分子图上的GAT模型,到时候你会更深刻体会这些机制的妙处。