\[ \begin{align}\begin{aligned}\newcommand{\ba}{\boldsymbol{a}} \newcommand{\bb}{\boldsymbol{b}} \newcommand{\be}{\boldsymbol{e}} \newcommand{\bq}{\boldsymbol{q}} \newcommand{\bk}{\boldsymbol{k}} \newcommand{\bw}{\boldsymbol{w}} \newcommand{\bx}{\boldsymbol{x}} \newcommand{\by}{\boldsymbol{y}} \newcommand{\bz}{\boldsymbol{z}} \newcommand{\bd}{\boldsymbol{d}} \newcommand{\bv}{\boldsymbol{v}} \newcommand{\bs}{\boldsymbol{s}}\\\newcommand{\btheta}{\boldsymbol{\theta}} \newcommand{\bbeta}{\boldsymbol{\beta}} \newcommand{\bgamma}{\boldsymbol{\gamma}} \newcommand{\bsigma}{\boldsymbol{\sigma}} \newcommand{\md}{\mbox{d}} \newcommand{\bmu}{\boldsymbol{\mu}} \newcommand{\bone}{\boldsymbol{1}} \newcommand{\bzero}{\boldsymbol{0}} \newcommand{\bepsilon}{\boldsymbol{\epsilon}} \newcommand{\bphi}{\boldsymbol{\phi}} \newcommand{\bh}{\boldsymbol{h}} \newcommand{\bc}{\boldsymbol{c}} \newcommand{\br}{\boldsymbol{r}} \newcommand{\bQ}{\boldsymbol{Q}} \newcommand{\bK}{\boldsymbol{K}} \newcommand{\bV}{\boldsymbol{V}} \newcommand{\bSigma}{\boldsymbol{\Sigma}} \newcommand{\bg}{\boldsymbol{g}} \newcommand{\bxi}{\boldsymbol{\xi}} \newcommand{\bvarepsilon}{\boldsymbol{\varepsilon}} \newcommand{\bdelta}{\boldsymbol{\delta}} \newcommand{\bq}{\boldsymbol{q}} \newcommand{\bk}{\boldsymbol{k}} \newcommand{\bJ}{\boldsymbol{J}} \newcommand{\bp}{\boldsymbol{p}} \newcommand{\bi}{\boldsymbol{i}} \newcommand{\bo}{\boldsymbol{o}} \newcommand{\bE}{\boldsymbol{E}} \newcommand{\bH}{\boldsymbol{H}} \newcommand{\bL}{\boldsymbol{L}} \newcommand{\bu}{\boldsymbol{u}} \newcommand{\bLambda}{\boldsymbol{\Lambda}} \newcommand{\trans}{^{\rm\scriptsize T}} \newcommand{\var}{\mathrm{var}}\\\newcommand{\bA}{\boldsymbol{A}} \newcommand{\bB}{\boldsymbol{B}} \newcommand{\bC}{\boldsymbol{C}} \newcommand{\bD}{\boldsymbol{D}} \newcommand{\bG}{\boldsymbol{G}} \newcommand{\bI}{\boldsymbol{I}} \newcommand{\bM}{\boldsymbol{M}} \newcommand{\bP}{\boldsymbol{P}} \newcommand{\bS}{\boldsymbol{S}} \newcommand{\bU}{\boldsymbol{U}} \newcommand{\bW}{\boldsymbol{W}} \newcommand{\bX}{\boldsymbol{X}} \newcommand{\bY}{\boldsymbol{Y}} \newcommand{\bZ}{\boldsymbol{Z}} \newcommand{\cotp}{\textcolor[RGB]{48,209,88}{TP}} \newcommand{\cotn}{\textcolor[RGB]{100,210,255}{TN}} \newcommand{\cofp}{\textcolor[RGB]{94,92,230}{FP}} \newcommand{\cofn}{\textcolor[RGB]{191,90,242}{FN}}\\\newcommand{\numcotp}{\textcolor[RGB]{48,209,88}{50}} \newcommand{\numcotn}{\textcolor[RGB]{100,210,255}{30}} \newcommand{\numcofp}{\textcolor[RGB]{94,92,230}{10}} \newcommand{\numcofn}{\textcolor[RGB]{191,90,242}{10}} \DeclareMathOperator*{\argmin}{arg\,min}\end{aligned}\end{align} \]

图注意力网络#

学习目标与记号#

  1. 推导 GAT 的边打分、邻域 Softmax 和加权聚合公式;

  2. 正确处理邻接掩码、注意力归一化轴及多头拼接或平均;

  3. 实现数值稳定的邻域 Softmax,并审慎解释注意力权重;

  本节沿用统一记号:普通小写字母表示标量,粗体小写字母表示向量,粗体大写字母表示矩阵或高阶张量;层编号写作上标 \([l]\)⁠,节点、样本或边编号写作下标;转置写作 \(\trans\)⁠。除非另有说明,节点特征按行存放,邻接约定为 \(A_{ij}>0\) 表示节点 \(j\) 向节点 \(i\) 发送消息。正文与练习中的程序都应同时检查数值结果和数组维度。本节练习的参考答案见 图注意力网络(GAT)答案⁠。

  GAT 将 消息传递 的聚合权重改为数据依赖形式。它与 Transformer 多头注意力 共享部分思想,但只在图邻域内归一化;固定归一化权重的对照模型见 GCN⁠。

1Veličković 等(2018)⁠,Graph Attention Networks⁠。

提出背景与意义

  在 GAT 出现以前,GCN 等模型通常根据图的连接关系和节点度数,为各个邻居分配预先确定的权重。这种做法简单有效,但无法根据当前节点和邻居的特征判断“哪个邻居提供的信息更加重要”。

  Veličković 等在 2017 年提出 GAT,并将其发表于 ICLR 2018 1Veličković 等(2018)⁠,Graph Attention Networks⁠。⁠。GAT 把注意力机制引入图的局部邻域,让模型自动学习不同邻居的重要程度,再按照这些权重汇总信息。它使图神经网络能够更灵活地利用邻居信息,也便于把同一组参数用于新节点或新图。不过,较大的注意力权重只表示模型在当前计算中更加重视相应邻居,不能直接理解为因果关系。

  GCN 的边权由图结构与节点度固定决定。图注意力网络(graph attention network,GAT)让模型根据节点表示为不同邻居学习不同权重。经典 GAT 不是 Transformer 的完整查询—键—值公式,而是对边两端线性投影后的特征拼接打分,再在每个接收节点的邻域内归一化。

单头图注意力#

GAT 为目标节点的不同邻居分配不同注意力权重

图 38 GAT 的边权由节点特征学习,并只在目标节点的真实邻域内做 Softmax#

  先做共享线性投影,得到投影后的节点特征向量 \(\bz_i\)⁠:

\[\bz_i=\bh_i\cdot\bW, \qquad \bW\in\mathbb{R}^{d_{\mathrm{in}}\times d_{\mathrm{out}}}.\]

其中 \(\bW\) 对所有节点共享。对每条允许的边 \(j\to i\) 计算未归一化得分

\[e_{ij} =\operatorname{LeakyReLU}\!\left( [\bz_i\Vert\bz_j]\cdot\ba \right),\]

其中 \(\ba\in\mathbb{R}^{2d_{\mathrm{out}}\times 1}\)⁠,\(\Vert\) 表示沿特征轴拼接。只在 \(j\in\mathcal{N}(i)\cup\{i\}\) 上做 Softmax:

\[\alpha_{ij} =\frac{\exp(e_{ij})} {\sum_{k\in\mathcal{N}(i)\cup\{i\}}\exp(e_{ik})}.\]

  最后加权聚合

\[\bh'_i =\sigma\!\left( \sum_{j\in\mathcal{N}(i)\cup\{i\}} \alpha_{ij}\bz_j \right).\]

对固定 \(i\)⁠,权重非负且和为 1。注意力不是对全图所有节点做归一化,而是对该接收节点允许看到的邻居集合做归一化。若先对全图 Softmax 再把非边位置清零,剩余权重和会小于 1,模型也可能在归一化阶段利用不存在的边。

  下面的动画展示目标节点如何分别计算各邻居的注意力得分,再在该节点的邻域内完成 Softmax 归一化,并用不同权重汇总邻居信息。

多头注意力#

  第 \(r\) 个头有独立的 \(\bW_r,\ba_r\)⁠。隐藏层常把 \(K\) 个头拼接:

\[\bh'_i =\mathop{\Vert}_{r=1}^{K} \sigma\!\left(\sum_j\alpha_{ij}^{(r)}\bh_j\cdot\bW_r\right),\]

  输出宽度为 \(Kd_{\mathrm{head}}\)⁠。分类输出层常对各头取平均,从而保持类别维度不随头数变化。实现时必须明确拼接轴、每头宽度与最终输出宽度,不能只把头数当作一个无需重新核对维度的超参数。

稀疏实现、复杂度与解释边界#

  GAT 只需为实际边和自环计算得分,边打分与聚合的结构成本随 \(m\) 增长;若构造 \(n\times n\) 得分矩阵,则会失去稀疏优势。多个头还会按头数增加边得分、显存和聚合成本。

  较大的 \(\alpha_{ij}\) 表示在当前层、当前头和当前输入下,节点 \(i\) 的更新对 \(j\) 的线性变换赋予较高混合系数。它不是自动可靠的因果解释:权重会受其他邻居、尺度、头、层和后续非线性影响,不同参数化也可能产生相似输出。

稳定的邻域 Softmax#

  下面用 NumPy 实现只在真实邻域内计算的 Softmax。输入 scoresmask 是大小相同的二维数组:scores[i, j] 表示节点 \(j\) 向节点 \(i\) 发送信息时尚未归一化的注意力得分,mask[i, j] 表示这条边是否允许参与计算。函数返回大小相同的注意力权重矩阵,每一行只在相应接收节点的有效邻居之间进行归一化。

import numpy as np

def masked_softmax(scores, mask):
    scores = np.asarray(scores, dtype=float)
    mask = np.asarray(mask, dtype=bool)
    if scores.shape != mask.shape or scores.ndim != 2:
        raise ValueError("scores 与 mask 必须是同形二维数组")
    if np.any(mask.sum(axis=1) == 0):
        raise ValueError("每个接收节点至少要有一个允许的邻居")
    masked = np.where(mask, scores, -np.inf)
    shifted = masked - np.max(masked, axis=1, keepdims=True)
    weights = np.where(mask, np.exp(shifted), 0.0)
    return weights / weights.sum(axis=1, keepdims=True)

  代码先检查 scoresmask 是否为大小相同的二维数组,并要求每个接收节点至少有一个有效邻居。随后,它把不存在的边对应的得分设为负无穷大,再从每行的有效得分中减去该行最大值,以降低指数运算产生过大数值的风险。非边位置的权重被设为 0,最后对每一行归一化。因此,返回结果中不存在的边权重为 0,每个接收节点的有效邻居权重之和为 1。

  这个实现用每一行表示一个接收节点、每一列表示一个候选发送节点,因而需要建立完整的二维得分矩阵,只适合小图检查。处理大图时,应只保存实际存在的边,并将指向同一接收节点的边分为一组,在各组内部完成 Softmax,而不应构造完整的 \(n\times n\) 得分矩阵。

什么是分段 Softmax

  上述“先把边分组,再分别计算 Softmax”的方法称为分段 Softmax(segment softmax)。这里的“分段”不是把一个函数写成几个分段表达式,而是把已经保存的边按照接收节点分成若干组:指向同一个接收节点的边属于同一组,不同组之间互不参与归一化。

  设一共保存了 \(M\) 条边,第 \(r\) 条边的注意力得分为 \(s_r\)⁠,接收节点编号为 \(g_r\)⁠。先在每一组内找到最大得分,再只使用同组边计算权重:

\[u_i=\max_{q:g_q=i}s_q, \qquad \alpha_r =\frac{\exp(s_r-u_{g_r})} {\displaystyle\sum_{q:g_q=g_r}\exp(s_q-u_{g_r})}.\]

其中,\(u_i\) 是所有指向节点 \(i\) 的边得分中的最大值。减去它可以降低指数运算产生过大数值的风险;对于每个接收节点,指向它的全部边权重之和等于 1。例如,若前三条边指向节点 \(i\)⁠,后两条边指向节点 \(j\)⁠,就分别进行一次包含三项的 Softmax 和一次包含两项的 Softmax,不能把五条边放在一起归一化。

  分段 Softmax 与上面的二维掩码 Softmax 完成相同的数学运算,但它只保存并处理实际存在的边,因而适合稀疏图,也避免了建立 \(n\times n\) 得分矩阵。

Shiny 交互演示:GAT 邻域注意力

  在交互页面中选择 GAT 后,可以改变 Leaky ReLU 的负半轴斜率,并逐步查看边得分、只在真实邻域与自环内计算的 Softmax 权重、逐邻居贡献和节点更新结果。注意力权重描述当前计算中的分配比例,不等同于因果影响。

点击打开“图神经网络逐步实验室”

核心推导与实现核验#

核心关系

\[e_{ij}=\operatorname{LeakyReLU}\!\left([\bh_i\cdot\bW\Vert\bh_j\cdot\bW]\cdot\ba\right),\quad \alpha_{ij}=\operatorname{softmax}_{j\in\mathcal{N}(i)\cup\{i\}}(e_{ij}).\]

  推导路径。 每个节点先共享线性投影;每条允许边根据接收和发送表示产生标量得分;在同一接收节点的邻域内做稳定 Softmax,再以这些权重加权发送节点表示。

关键条件

  对固定接收节点,邻居置换只重排得分向量。Softmax 与同一置换可交换,加权和也不依赖枚举顺序,因此一层 GAT 对节点编号置换等变。

数据规模

  若图中有 \(n\) 个节点,每个节点有 \(d_{\mathrm{in}}\) 个输入特征,单头输出宽度为 \(d_{\mathrm{out}}\)⁠,则输入矩阵写为 \(\bH=[\bh_1\trans;\ldots;\bh_n\trans]\in\mathbb{R}^{n\times d_{\mathrm{in}}}\)⁠,权重 \(\bW\) 和投影结果 \(\bZ\) 的大小依次为 \(d_{\mathrm{in}}\times d_{\mathrm{out}}\)\(n\times d_{\mathrm{out}}\)⁠。若原图有 \(m\) 条有向边并加入 \(n\) 个自环,就需要计算 \(m+n\) 个注意力权重;\(K\) 个头拼接后每个节点有 \(Kd_{\mathrm{head}}\) 个输出特征。

常见误区

  Softmax 必须沿发送邻居轴且在掩码后计算;全被屏蔽的行会产生 NaN;把无向图只保存单向边会让两个方向的邻域不同。

动手检查

  检查每个接收节点的权重和为 1、非边权重为 0;将全部得分加同一常数后权重不变;邻居表示相同时,加权和应等于该共同表示。

数值稳定性与规模

  邻域 Softmax 先减该邻域最大得分;每个节点至少保留自环或一个有效邻居。稀疏图使用 分段 Softmax⁠,避免构造 \(n\times n\) 得分矩阵。

本节小结#

  1. GAT 通过特征相关的边得分学习邻居权重。

  2. 注意力必须在每个接收节点的真实邻域内稳定归一化。

  3. 多头提高表示灵活性,也增加维度、计算与解释上的复杂度。

综合练习#

  程序题应写出维度断言并报告运行环境。全部参考答案见 图注意力网络(GAT)答案⁠。

  1. 邻域注意力计算。 某接收节点只有两个允许的发送节点,边得分分别为 \(0\)\(\log3\)⁠,对应投影后值向量为 \((2,0)\)\((0,4)\)⁠。计算两个注意力权重和聚合结果;再说明若先对包含一个非邻居的全图得分做 Softmax、之后把非邻居权重清零,为什么结果一般不同。

  2. 归一化与置换证明。 证明固定接收节点的注意力权重非负且和为 1;再证明改变该节点邻居的枚举顺序只会同步重排得分与权重,不改变加权和。由此说明共享参数的 GAT 层为什么满足节点置换等变性。

  3. 稳定 Softmax 计算。 对得分 \((1000,999,-5)\) 和掩码 \((1,1,0)\)⁠,使用“先掩码、再减有效最大值”的方法计算权重。证明给同一邻域全部有效得分加任意常数不改变权重,并解释直接计算指数可能出现的问题。

  4. 多头维度与计算量。 某有向图有 \(n=100\) 个节点、\(m=500\) 条已存边,输入维度为 16;GAT 使用 4 个头,每头输出维度为 8,并为每个节点加入一个自环。写出每头投影矩阵、注意力向量、投影结果和拼接输出的维度;计算忽略偏置时的投影与注意力参数量,以及需要计算的边得分数。

  5. 分段 Softmax 实现。 根据 分段 Softmax 的定义,基于一维边得分和每条边的接收端点实现稳定 segment_softmax不得构造 \(n\times n\) 矩阵;应按接收节点减最大值、归一化,并拒绝没有自环或入边的节点。用第 1、3 题和含重复边的小图测试。

  6. 单头与多头 GAT 实现。 基于 edge_index第 5 题的 分段 Softmax 和共享线性投影实现单头 GAT,再扩展到多头拼接与平均。返回边得分和权重;与逐节点循环版本比较前向结果以及输入、投影矩阵和注意力向量的梯度。

  7. 性质与边界测试。 为 GAT 编写节点置换等变性、邻居顺序不变性、得分平移不变性、权重分段和为 1、非边权重为 0、相同值向量聚合不变等测试。加入全空邻域、极大绝对值得分、无向边只存一个方向和错误 Softmax 分组的失败用例。

  8. GCN 与 GAT。 在同一节点分类数据上比较两层 GCN 与 GAT。两种模型使用相同的训练集、验证集和测试集划分、随机种子、输入特征、近似参数量和训练更新次数,并使用同一个验证集和相同标准选择模型;报告准确率或 macro-F1⁠、训练时间、固定图批量预测时间、峰值内存、参数量、边得分数和图层运行次数。

  9. 注意力头数。 在保持总输出宽度近似不变的前提下比较 \(K\in\{1,2,4,8\}\) 个头,并另列“每头宽度固定”设置以揭示计算扩张。固定数据划分、随机种子集合、层数、训练更新次数和评价代码;报告预测质量、训练时间、固定批量预测时间、峰值内存、参数量、边得分数和注意力层调用次数。

  10. 学习权重与均匀权重。 保持其他设置不变,把 GAT 学到的邻域注意力权重换成均匀权重,再与原 GAT 和 GCN 比较。各组使用相同的数据划分、随机种子、隐藏宽度、训练更新次数、优化器和评价程序;报告预测质量、训练时间、固定批量预测时间、峰值内存、边得分数与图层运行次数,并比较稀疏实现和仅用于小图检查的稠密实现。根据结果分析学习邻居权重可能带来的影响。