图神经网络基础:从 GCN 到 GraphSAGE

社交网络、分子结构、知识图谱、引文网络,这些数据的本质都是「图」:由节点(实体)和边(关系)组成。传统的深度学习模型擅长处理图像(网格)和文本(序列)这类规整结构,却很难直接啃下图这种不规则、非欧式的数据。图神经网络(Graph Neural Network,GNN)正是为此而生的一类模型,它让消息沿着边在节点之间流动,从而学到同时包含「节点自身特征」与「图结构信息」的向量表示。

本篇面向已经掌握深度学习基础、想入门图表示学习的读者,依次讲清三件事:为什么图需要专用模型、GCN/GraphSAGE/GAT 三种经典架构的核心思想,以及用 PyTorch Geometric 在 Cora 引文网络上跑通一个节点分类的最小示例。

为什么图结构数据需要专用模型

普通的卷积网络(CNN)之所以能处理图像,是因为图像是「欧式网格」:每个像素都有固定的上下左右邻居,卷积核可以平移复用。但图不满足这个前提,它有三个让传统模型头疼的特点:

  • 非欧式结构:节点数量可变,邻居数量不等,没有天然的「空间顺序」。你无法像滑动卷积核那样在图上平移一个固定窗口,因为第 i 个节点的邻居可能是 3 个,第 j 个节点的邻居可能是 30 个。
  • 节点与边都带特征:节点可能是「一篇论文」(标题、摘要的词向量),边可能是「引用关系」且自带权重。这些属性需要被一起编码进表示,而不是只靠拓扑结构。
  • 任务的传递性依赖结构:节点分类、链接预测、图分类等任务的结果,高度依赖邻居信息。孤立地看一个节点往往信息不足,必须聚合它的局部邻域。

GNN 的统一思路是「消息传递」:每一层里,节点先收集邻居发来的信息,再和自己的旧表示融合,得到更「懂全局」的新表示。堆叠多层后,一个节点的表示就能感知到多跳之外的远方邻居。

图卷积网络 GCN

GCN(Graph Convolutional Network)由 Kipf 与 Welling 在 2017 年提出,是对谱图卷积的一阶局部近似,也是后续大量 GNN 的基石。它的核心就一句话:把邻居的特征做「对称归一化」后的加权平均,再过一个线性变换和非线性激活。

消息传递与对称归一化

设图的邻接矩阵为 A,节点特征矩阵为 H。GCN 先加上自环得到 Ã = A + I,再计算度矩阵 D̃(D̃_ii 等于节点 i 在 Ã 中的度数)。关键的归一化矩阵是:

D̃^(-1/2) · Ã · D̃^(-1/2)

这个对称归一化的作用,是让度数大的节点不会因为连接多而被放大、度数小的节点也不会被忽略,从而保持特征尺度稳定。

一层的公式

单层 GCN 的更新公式如下:

H^(l+1) = σ( D̃^(-1/2) · Ã · D̃^(-1/2) · H^(l) · W^(l) )

其中 H^(l) 是第 l 层的节点表示,W^(l) 是可学习的权重矩阵,σ 是非线性激活(常用 ReLU)。注意乘法链里,邻接部分只负责「按结构聚合邻居」,W^(l) 负责「非线性变换」,二者顺序固定。

GCN 是「直推式」(transductive)的:训练时所有节点(包括测试节点)必须都在图中,它学到的是每个节点的具体嵌入,而不是一个能套到新节点上的函数。这一点正是 GraphSAGE 想要突破的。

来源核验:Kipf, T. N. 与 Welling, M.《Semi-Supervised Classification with Graph Convolutional Networks》,arXiv:1609.02907,ICLR 2017。标题、作者、方法描述已通过 arXiv 摘要页核验(已核验)。

GraphSAGE:采样与聚合,走向归纳式学习

GraphSAGE(SAmple and aggreGatE)由 Hamilton、Ying、Leskovec 在 2017 年提出,解决的核心问题是:当图很大、且不断有新节点加入时,GCN 那种「训练时看全图」的方式就跑不动了。

采样与聚合

GraphSAGE 不再为每个节点单独学一个嵌入,而是学一个「聚合函数」:给定任意节点的局部邻居,这个函数就能生成它的嵌入。具体分三步:

  1. 采样(Sample):对每个节点,从其邻居中均匀采样固定数量(比如 5 个、10 个)的邻居,控制每层计算的规模,避免大度节点拖垮训练;
  2. 聚合(Aggregate):把采样到的邻居特征聚合成一个向量,聚合函数要满足「对邻居排列顺序不敏感」,常见选择有均值(Mean)、最大池化(MaxPool)、LSTM 聚合;
  3. 拼接与更新:把聚合结果和节点自身上一层的表示拼接,过一层带非线性变换的权重。

归纳式学习

因为学的是「聚合函数」而非「节点嵌入」,GraphSAGE 是「归纳式」(inductive)的:训练完的模型可以直接套用在训练时没见过的新节点、甚至一张全新的图上。这对推荐系统、不断增长的社交网络等场景至关重要。

其第 l 层的更新表达为:

h_v^(l) = σ( W^(l) · CONCAT( h_v^(l-1), AGGREGATE( { h_u^(l-1), ∀u ∈ N(v) } ) ) )

其中 N(v) 是节点 v 采样得到的邻居集合。直观理解:节点 v 的新表示,由「自己旧表示」和「邻居旧表示的聚合」拼接后再变换得到。

来源核验:Hamilton, W. L.、Ying, R. 与 Leskovec, J.《Inductive Representation Learning on Large Graphs》,arXiv:1706.02216,NeurIPS 2017。标题、作者、采样聚合与归纳式方法描述已通过 arXiv 摘要页核验(已核验)。

图注意力 GAT

GCN 对所有邻居「一视同仁」地平均,GraphSAGE 也只是均匀采样聚合。但现实里,有些邻居明显比另一些更重要。GAT(Graph Attention Network)由 Veličković 等人在 2018 年提出,把注意力机制引入图卷积,让节点自己学习「该多看重哪些邻居」。

注意力系数

对节点 i 和它的每个邻居 j,GAT 用一个共享的注意力向量 a 和权重矩阵 W 计算「未归一化的注意力分数」:

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

这里 [· CONCAT ·] 表示把节点 i 和邻居 j 变换后的特征拼接起来。随后在节点 i 的所有邻居上做 softmax,得到归一化的注意力系数:

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

这个 α_ij 就是邻居 j 对节点 i 的「重要程度权重」。节点 i 的新表示,就是邻居(含自己)按 α_ij 加权求和后再过非线性激活。GAT 还可以叠加多组互相独立的注意力(多头注意力),让模型从不同子空间捕捉结构信息,进一步提升稳定性。

GAT 同时适用于直推式与归纳式任务,且不需要事先知道整张图的结构(只需邻居列表),因此非常灵活。

来源核验:Veličković, P. 等《Graph Attention Networks》,arXiv:1710.10903,ICLR 2018。标题、作者、注意力系数与多头机制描述已通过 arXiv 摘要页核验(已核验)。

代码示例:用 PyTorch Geometric 在 Cora 上做节点分类

下面用一个最小可运行示例,演示如何借助 PyTorch Geometric(PyG)在 Cora 引文网络上训练一个两层 GCN,对论文节点做七分类(对应七个学科主题)。Cora 中每个节点是一篇论文,特征是词袋向量,边是引用关系,标签是论文所属领域。

import torch
from torch_geometric.datasets import Planetoid
from torch_geometric.nn import GCNConv

  # 载入 Cora 引文网络数据集,首次运行会自动下载到本地缓存
dataset = Planetoid(root="/tmp/Cora", name="Cora")
data = dataset[0]

  # 定义两层 GCN 模型
class GCN(torch.nn.Module):
    def __init__(self, in_channels, hidden_channels, out_channels):
        super().__init__()
        self.conv1 = GCNConv(in_channels, hidden_channels)
        self.conv2 = GCNConv(hidden_channels, out_channels)

    def forward(self, x, edge_index):
        # 第一层卷积后接 ReLU 激活与 dropout
        x = self.conv1(x, edge_index).relu()
        x = torch.dropout(x, p=0.5, train=True)
        # 第二层卷积直接输出分类 logits
        x = self.conv2(x, edge_index)
        return x

  # 输入维度为特征数,输出维度为类别数
model = GCN(dataset.num_features, 16, dataset.num_classes)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)

  # 只在带标签的训练节点上计算交叉熵损失
def train():
    model.train()
    optimizer.zero_grad()
    out = model(data.x, data.edge_index)
    loss = torch.nn.functional.cross_entropy(
        out[data.train_mask], data.y[data.train_mask])
    loss.backward()
    optimizer.step()
    return loss.item()

  # 在测试掩码上评估分类准确率
def test():
    model.eval()
    out = model(data.x, data.edge_index)
    pred = out.argmax(dim=1)
    acc = (pred[data.test_mask] == data.y[data.test_mask]).float().mean()
    return acc.item()

  # 简单训练 200 轮后打印测试集准确率
for epoch in range(200):
    train()

print("测试集准确率:", test())

这段代码展示了 GNN 训练的惯用套路:数据来自 PyG 的 Planetoid 接口,模型用 GCNConv 堆叠,训练时只对有标签的 train_mask 节点算损失,而测试时利用整张图的消息传递得到所有节点嵌入。把 GCNConv 换成 SAGEConvGATConv,就能快速改写成 GraphSAGE 或 GAT 版本。

来源核验:PyTorch Geometric 官方文档(pytorch-geometric.readthedocs.io)确认其为「构建于 PyTorch 之上的图神经网络库」,提供数据集与 torch_geometric.nn 中的各类图卷积层(已核验)。示例中 Planetoid 数据集与 GCNConv 的具体调用参数为常见用法,建议以你安装版本的官方文档为准(待核实)。

小结

我们从图数据的三个难点(非欧结构、节点/边特征、结构依赖)出发,理清了 GNN「消息传递」的统一范式,并对比了三条主线:

  • GCN 用对称归一化邻接矩阵做邻居加权平均,简洁高效,但是直推式的,新节点得重新训练;
  • GraphSAGE 通过采样与聚合学习一个可复用的聚合函数,支持归纳式学习,能处理超大规模、不断生长的图;
  • GAT 引入注意力系数,让节点按重要程度给邻居加权,无需预先固定结构,灵活且表现力强。

三者并非互斥:它们的核心都是「聚合邻居信息」,区别仅在「怎么聚合」。理解了这一层,你就能在阅读更现代的 GNN(如图采样、图 Transformer、超图网络)时快速抓住主干。

参考与延伸阅读

本文累计阅读