\[ \begin{align}\begin{aligned}\newcommand{\ba}{\boldsymbol{a}} \newcommand{\bb}{\boldsymbol{b}} \newcommand{\be}{\boldsymbol{e}} \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{\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} \]

图神经网络(尚在完善)#

学习目标与记号#

  社交网络、分子和知识图谱等数据由对象及其关系共同构成,节点数量、邻域大小和存储顺序通常不固定。图神经网络通过置换相容的邻域聚合学习结构化表示;本章将学习图数据记号、消息传递、GCN、GAT、GraphSAGE 与 GIN,目标是能够分析节点、边和图级任务,核对稀疏结构与特征维度,并建立没有结构泄漏的实验流程。

  1. 准确区分图、节点、边及其特征,并识别节点级、边级和图级任务;

  2. 使用邻接矩阵、度矩阵和特征矩阵表达图数据的维度与置换性质;

  3. 设计不会泄漏图结构信息的数据划分,并为后续图神经网络实验建立可靠的对照模型;

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

图数据的节点、边、特征和三类预测任务

图 35 图由结构和特征共同组成;监督信号可以位于节点、边或整张图上#

图、矩阵与特征#

  一个有限图写为 \(\mathcal{G}=(\mathcal{V},\mathcal{E})\)⁠,其中 \(\mathcal{V}\) 为节点(顶点)集,\(\mathcal{E}\) 为边集。令节点数为 \(n=|\mathcal{V}|\)⁠,边数为 \(m=|\mathcal{E}|\)⁠。在给节点临时编号后,邻接矩阵 \(\bA\in\mathbb{R}^{n\times n}\) 定义为

\[\begin{split}A_{ij}=\begin{cases} w_{ij}, & (v_j,v_i)\in\mathcal{E},\\ 0, & \text{otherwise}, \end{cases},\end{split}\]

其中无权图取 \(w_{ij}=1\)⁠。无向图的邻接矩阵对称;有向图一般不对称,必须明确 \(A_{ij}\) 表示从 \(i\)\(j\)⁠,还是从 \(j\)\(i\) 发送消息。本文约定 \(A_{ij}>0\) 表示节点 \(j\) 的信息传给节点 \(i\)⁠,并参与节点 \(i\) 的信息聚合。

什么是自环?

  自环(self-loop)是从一个节点出发、又返回该节点自身的边。例如,节点 \(i\) 的自环连接 \(i\)\(i\)⁠,在邻接矩阵中对应对角线元素 \(A_{ii}\)⁠;在无权图中,通常用 \(A_{ii}=1\) 表示存在自环。消息传递时加入自环,可以让节点在汇总邻居信息的同时继续使用自己的特征。后文的 GCN 重归一化 会通过给邻接矩阵加上单位矩阵,为每个节点加入一条自环。

不同教材中的邻接矩阵约定

  邻接矩阵下标所表示的边方向并没有统一规定。在传统图论教材和通用图计算软件中,更常见的约定是用 \(A_{ij}>0\) 表示存在从节点 \(i\) 指向节点 \(j\) 的边。例如,NetworkX 的邻接矩阵说明DGL 的邻接矩阵说明 都采用行对应发送节点、列对应接收节点的方式。

  在图神经网络的消息传递公式中,也经常采用相反的约定,即 \(A_{ij}>0\) 表示节点 \(j\) 的信息传给节点 \(i\)⁠。这样,在节点特征按行存放时,矩阵乘法 \(\boldsymbol{H}'=\bA\cdot\boldsymbol{H}\) 的第 \(i\) 行便直接表示节点 \(i\) 从其他节点接收并聚合的信息;PyTorch Geometric 的消息传递说明 也使用从节点 \(j\) 向节点 \(i\) 传递消息的公式。为了使本章后续的消息聚合公式更直观,本文采用这一约定。

  两种约定都可以使用,但在阅读公式、调用软件或编写程序前,应先确认所采用的约定,并在后续推导和实现中保持一致。对于无向图,邻接矩阵通常满足 \(A_{ij}=A_{ji}\)⁠,因此两种约定得到的邻接矩阵相同;对于有向图,两种约定对应的矩阵互为转置,不能混用。

  度矩阵记为 \(\bD=\operatorname{diag}(d_1,\ldots,d_n)\)⁠,其中 \(d_i=\sum_j A_{ij}\)⁠。若第 \(i\) 个节点的输入特征列向量为 \(\bx_i\in\mathbb{R}^{d_x}\)⁠,则节点特征矩阵统一写为 \(\bX=[\bx_1\trans;\ldots;\bx_n\trans]\in\mathbb{R}^{n\times d_x}\)⁠。边特征可写为 \(\boldsymbol{E}\in\mathbb{R}^{m_s\times d_e}\)⁠,其中 \(m_s\) 是实际存储的有向边数。

  大图的邻接矩阵中通常只有少量元素不为 0,因此实现时一般不保存完整的 \(n\times n\) 邻接矩阵,而只记录实际存在的边。常见方式包括边索引矩阵和稀疏张量。边索引矩阵的维度为 \(2\times m_s\)⁠:第一行记录每条边的发送节点,第二行记录相应的接收节点。稀疏张量则只保存邻接矩阵中非零元素的位置及其权重。若 \(m\) 条无向边都分别记录为两个相反方向的有向边,则 \(m_s=2m\)⁠。不能为了书写方便就把稀疏图转换成 \(n\times n\) 的稠密矩阵。

例 7.1 用边索引矩阵记录图中的边

  假设图中实际存储了两条有向边 \(1\to2\)\(3\to2\)⁠,则 \(m_s=2\)⁠,相应的边索引矩阵可以写为

\[\begin{split}\begin{bmatrix} 1 & 3\\ 2 & 2 \end{bmatrix},\end{split}\]

其中,每一列表示一条边:第一列中的 \((1,2)\trans\) 表示节点 \(1\) 向节点 \(2\) 发送信息,第二列中的 \((3,2)\trans\) 表示节点 \(3\) 向节点 \(2\) 发送信息。这样只需保存两条实际存在的边,而不必保存包含大量 0 的完整邻接矩阵。

预测任务与监督位置#

  1. 节点级任务⁠:为每个节点预测类别或连续值,例如论文主题分类;

  2. 边级任务⁠:预测两个节点是否应相连,或判断边的类型;

  3. 图级任务⁠:将整张图看作一个样本。首先汇总图中所有节点的信息,得到整张图的表示;然后根据该表示预测整张图的属性,例如预测一个分子的溶解度、毒性或所属类别。

  节点分类常见“单图、多节点样本”的设置:训练、验证和测试节点共享同一张图,但只有训练节点标签进入损失。图分类则通常以多张互相独立的图为样本。两种设置的批处理、划分和泛化问题不同,不能只看输入张量都是“图”就混为一谈。

节点置换与模型应满足的性质#

  节点编号只是存储约定。令 \(\boldsymbol{P}\) 为置换矩阵,重新编号后

\[\bA'=\boldsymbol{P}\cdot\bA\cdot\boldsymbol{P}\trans, \qquad \bX'=\boldsymbol{P}\cdot\bX.\]

  节点级模型用于为图中的每个节点分别预测类别或连续值。例如,在论文引用网络中,可以预测每篇论文所属的研究主题;在道路网络中,可以预测每个路口未来的交通流量。模型通常会同时利用节点自身的特征、相邻节点的信息以及节点之间的连接关系。这里的“分别预测”并不表示要为每个节点训练一个独立模型;所有节点共用同一个模型及其参数,模型只是为不同节点分别生成预测结果。用 \(f_{\mathrm{node}}\) 表示这样的节点级模型,它应满足置换等变性

\[f_{\mathrm{node}}(\bA',\bX') =\boldsymbol{P}\cdot f_{\mathrm{node}}(\bA,\bX),\]

其中,\(f_{\mathrm{node}}\) 以邻接矩阵 \(\bA\) 和节点特征矩阵 \(\bX\) 为输入。若图中有 \(n\) 个节点,每个节点的输出包含 \(d_o\) 个数,则 \(f_{\mathrm{node}}(\bA,\bX)\in\mathbb{R}^{n\times d_o}\)⁠,其中第 \(i\) 行是节点 \(i\) 的预测结果。上式表示:给节点重新编号并相应地调整输入矩阵后,模型对每个实际节点给出的预测不应改变,只有预测结果的排列顺序会随节点编号一起调整。

  图级模型把整张图看作一个样本,并为整张图输出一个预测结果。它通常先为各个节点计算表示,再汇总所有节点的信息,得到整张图的表示,最后预测图的类别或某个连续值。例如,可以把一个分子看作一张图,预测该分子的类别、溶解度或毒性。与节点级模型为每个节点分别输出结果不同,图级模型通常为每张图输出一组结果。

  图级模型应满足置换不变性,即重新编号不改变整张图的预测结果。邻居求和、均值、最大值和对集合定义的注意力聚合都可以构成置换不变的局部聚合器;直接按邻居存储顺序拼接通常不具备这一性质。

例 7.2 给三名学生重新编号

  假设一张关系图中有小林、小周和小陈三名学生,小林与小周相连,小周与小陈相连。最初依次把三人编号为节点 \(1\)⁠、节点 \(2\) 和节点 \(3\)⁠,邻接矩阵为

\[\begin{split}\bA= \begin{bmatrix} 0&1&0\\ 1&0&1\\ 0&1&0 \end{bmatrix}.\end{split}\]

  现在只改变编号顺序:把小陈、小林和小周依次编号为节点 \(1\)⁠、节点 \(2\) 和节点 \(3\)⁠。三个人及其关系都没有发生变化,但邻接矩阵变为

\[\begin{split}\bA'= \begin{bmatrix} 0&0&1\\ 0&0&1\\ 1&1&0 \end{bmatrix}.\end{split}\]

  节点特征也必须按照新的编号顺序同步排列。假设节点模型原来为小林、小周和小陈给出的预测分别为 \(0.2\)⁠、\(0.7\)\(0.4\)⁠,那么重新编号后的节点预测应依次变为

\[\begin{bmatrix}0.4&0.2&0.7\end{bmatrix}\trans.\]

  每个人对应的预测值并没有改变,改变的只是这些预测值的排列顺序。这就是 置换等变性⁠。如果模型预测的是这三名学生组成的整张关系图的某个类别,那么重新编号前后的图级预测应完全相同,这就是 置换不变性⁠。因此,节点编号只是数据的排列方式,不应改变模型对实际对象和关系的判断。

数据划分与泄漏#

  图数据的泄漏不仅来自标签。链接预测若在划分前用完整邻接矩阵生成节点表示,测试边已经进入输入结构;时间图若随机打乱边,训练阶段可能看到未来关系;图分类若同一实体的近重复图同时出现在训练和测试集,指标也会偏乐观。因此应先定义部署时可见的信息,再划分边、节点、时间或实体,最后只用训练范围内的数据拟合预处理统计量。

  在划分数据之前,还应检查图的结构和节点特征在维度上能否对应。下面的 validate_graph 函数用于完成这项基本检查。参数 adjacency 是邻接矩阵,参数 features 是节点特征矩阵。函数不会训练模型或划分数据,而是在后续计算开始前检查两个矩阵的维度和数值是否合法;检查通过后,它会返回转换为浮点数组的邻接矩阵和节点特征矩阵。该示例接收完整的邻接矩阵,只适合小图检查;对于使用边索引或稀疏张量存储的大图,应针对相应的存储方式编写检查函数,不能先把大图转换成完整的邻接矩阵。

import numpy as np

def validate_graph(adjacency, features):
    adjacency = np.asarray(adjacency, dtype=float)
    features = np.asarray(features, dtype=float)
    if adjacency.ndim != 2 or adjacency.shape[0] != adjacency.shape[1]:
        raise ValueError("邻接矩阵必须是方阵")
    if features.ndim != 2 or features.shape[0] != adjacency.shape[0]:
        raise ValueError("特征矩阵的节点轴必须与邻接矩阵一致")
    if not np.isfinite(adjacency).all() or not np.isfinite(features).all():
        raise ValueError("图数据包含非有限值")
    return adjacency, features

  代码首先把两个输入转换为 NumPy 浮点数组。第一个判断要求邻接矩阵是方阵,因为一个含有 \(n\) 个节点的图需要用 \(n\) 行和 \(n\) 列表示节点之间的连接关系。第二个判断要求节点特征矩阵是二维数组,而且其行数必须等于节点数,从而保证特征矩阵的行数与节点数一致。第三个判断检查数组中是否出现 NaN正无穷大或负无穷大等不能直接参与正常计算的数值。若某项检查失败,函数会给出相应的错误提示,而不会带着有问题的数据继续运行。需要注意,这些判断只能检查矩阵维度,不能确认某一行特征在实际含义上是否确实属于相应的节点;节点编号与特征行的对应关系仍需在读取和整理数据时保证。

这段代码不能检查什么

  validate_graph 只能检查维度和数值是否合法,不能判断数据划分是否发生泄漏。例如,它无法知道测试边是否提前放入了训练阶段使用的邻接矩阵,也无法判断训练集和测试集是否包含同一实体的近重复图。这些问题仍需根据具体任务,另外检查边、时间、节点或实体的划分过程。

核心推导与实现核验#

核心关系

\[\bA'=\boldsymbol{P}\cdot\bA\cdot\boldsymbol{P}\trans, \qquad \bX'=\boldsymbol{P}\cdot\bX,\]
\[f_{\mathrm{node}}(\bA',\bX') =\boldsymbol{P}\cdot f_{\mathrm{node}}(\bA,\bX), \qquad f_{\mathrm{graph}}(\bA',\bX') =f_{\mathrm{graph}}(\bA,\bX).\]

其中,\(\boldsymbol{P}\) 是表示节点重新编号的置换矩阵,\(f_{\mathrm{node}}\)\(f_{\mathrm{graph}}\) 分别表示节点级模型和图级模型。

  推导路径。 给节点编号后用邻接矩阵编码结构、用特征矩阵逐行编码节点属性;节点编号改变时,结构和特征必须由同一个置换矩阵同步变换。节点级预测随后按相同方式改变排列顺序,而整张图的预测保持不变。

关键条件

  对任意置换矩阵,邻居集合只被重新编号而没有改变。若所有节点共用相同的消息函数和更新函数,并且局部聚合器不受邻居排列顺序影响,则每个旧节点的输出在新编号下恰好移动到对应行,因此节点输出置换等变;再使用求和或均值汇总所有节点的表示,即可得到图级置换不变性。邻接矩阵和节点特征矩阵必须按照同一个置换同步调整。

数据规模

  若图中有 \(n\) 个节点、每个节点有 \(d_x\) 个特征,并实际存储了 \(m_s\) 条有向边,则邻接矩阵大小为 \(n\times n\)⁠,节点特征矩阵大小为 \(n\times d_x\)⁠,边索引大小通常为 \(2\times m_s\)⁠。节点分类标签可以是长度为 \(n\) 的类别编号,也可以写成 \(n\times C\) 的类别指示矩阵。

常见误区

  节点编号只是为了区分节点,本身不代表节点的实际含义。如果需要重新排列节点,必须按照相同的顺序同时调整邻接矩阵和节点特征矩阵,否则节点及其特征会发生错误对应。进行链接预测时,应先划分训练边、验证边和测试边;训练过程不能使用需要在验证或测试阶段预测的边,否则模型就相当于提前看到了部分答案。对于节点很多的图,不宜建立完整的 \(n\times n\) 邻接矩阵,而应使用边列表或稀疏矩阵,只保存实际存在的边,从而减少内存占用。

动手检查

  可以随机改变节点的排列顺序,并用 np.allclose 比较变换前后的结果:节点输出应按照相同顺序重新排列,图输出应保持不变。在 Python 采用从 0 开始的节点编号时,边索引中的所有发送节点和接收节点编号都应位于 \(0\)\(n-1\) 之间,而且边索引的列数应等于实际存储的有向边数 \(m_s\)⁠。在单图节点分类任务中,训练、验证和测试掩码都应是长度为 \(n\) 的布尔数组,而且任意两组都不能包含同一个节点。对于大图,可以直接使用节点排列索引完成检查,不必构造完整的置换矩阵。

数值稳定性与规模

  度为 0 的节点需要自环或显式约定;度归一化不能直接除以 0。稀疏图应使用边索引或稀疏矩阵,避免创建 \(n\times n\) 的临时数组。

本章小结#

  1. 图数据同时包含关系结构、节点或边特征以及特定层级的监督信号。

  2. 节点级模型应置换等变,图级读出应置换不变。

  3. 数据划分必须匹配部署信息边界,并特别防止结构泄漏。