图神经网络入门:GCN、GAT、GraphSAGE核心思想

我们先从最基础的问题说起。图神经网络要解决的核心问题,说白了就是——怎么让计算机像理解图像或文本那样,去理解一个图结构的数据?

我刚开始接触图神经网络时,第一个困惑就是:图像有固定的像素网格,文本有顺序的词序列。但图呢?它的邻居数量不一样,顺序也不固定。这种不规则的拓扑结构,才是真正的难点。

我个人习惯把图神经网络想象成一场「信息聚会」。每个节点是一个参会者,边就是他们之间的对话关系。我们需要设计一套规则,让每个人听完邻居的发言后,更新自己的观点。这个观点,就是节点的特征表示。

下面这张图,展示了今天要讲的三种经典图神经网络,它们各自不同的聚合方式:

图神经网络三大经典架构对比 输入图结构 A B C D E GCN 均值聚合 邻居 → 求和/平均 等权处理 A' B' C' 归一化后加权 GAT 注意力聚合 邻居 → 加权求和 权重学习得到 A' B' C' 可区分注意力 GraphSAGE 采样+聚合 固定邻居采样 拼接/均值/LSTM A' B' C' 可扩展性强 左图 → 右图: GCN 对邻居进行归一化均值聚合,所有邻居同等重要。 GAT 学习每个邻居的注意力权重,能区分不同邻居的重要性。 GraphSAGE 先采样固定数量邻居,再选择聚合函数,适合大规模图。

GCN:最简单的消息传递方式

GCN 的全称是 Graph Convolutional Network,中文叫图卷积网络。说实话,这个名字有点吓人。但它的核心思想非常简单——对邻居节点的特征做归一化平均,然后用一个共享的权重矩阵做线性变换。

我刚开始学 GCN 时,觉得这不就是加权求和嘛。但真正在项目中跑起来才发现,关键在于那个「归一化」操作。

GCN 的更新公式可以写成:

h_i^(l+1) = σ( W * ( Σ_j∈N(i) h_j^(l) / √(d_i * d_j) ) )

其中 h_i 是节点 i 的特征向量,N(i) 是它的邻居集合,d_i 是节点度数。这个除以 √(d_i*d_j) 就是归一化操作。为什么要做归一化?

你想想看,如果一个节点有 100 个邻居,另一个节点只有 1 个邻居。如果不做归一化,前者聚合的信息量是后者的 100 倍。这会导致节点特征被度数的数量级主导,而不是真正有用的信息。

关键点:GCN 的核心假设是「我告诉你的邻居,和你自己,共同决定你的新状态」。这个假设虽然简单,但非常有效。

我记得第一次用 GCN 做分子性质预测时,模型收敛速度非常快。但有个问题——GCN 会把所有邻居一视同仁。这在分子图里可能不太合理。比如一个原子和碳原子连接,和与氧原子连接,影响显然不同。这就是 GCN 的局限性。

用代码实现一个简单的 GCN 卷积层,大概长这样:

import torch
import torch.nn as nn
import torch.nn.functional as F

class GCNLayer(nn.Module):
    def __init__(self, in_features, out_features):
        super().__init__()
        self.W = nn.Linear(in_features, out_features)

    def forward(self, x, adj):
        # x: [N, in_features], adj: [N, N] 归一化的邻接矩阵
        support = self.W(x)            # 线性变换
        output = torch.mm(adj, support) # 消息传递
        return F.relu(output)

个人经验:实际项目中,GCN 的归一化方式有几种变体。我建议先用对称归一化(即上面公式里的 √(d_i*d_j)),它比行归一化更稳定。如果遇到梯度爆炸,可以试试加上自环。

GAT:让邻居「说话」有轻重

GCN 的等权聚合,说白了就是不分青红皂白。GAT 解决了这个问题——它引入了一个注意力机制,让模型自己学习每个邻居的重要性。

GAT 的思路特别直观:对于中心节点 i 和它的每个邻居 j,先计算一个注意力系数:

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

这里 || 表示向量拼接。然后对 e_ij 做 softmax 得到权重 α_ij,最后用这些权重对邻居做加权求和。

我在项目里用过 GAT 做分子图里的官能团识别。效果比 GCN 好不少,因为注意力机制能自动关注到那些重要的化学键。比如判断一个碳原子是不是羰基碳时,模型学会了对氧原子邻居给予更高的注意力权重。

GAT 还有一个很实用的特性——多头注意力。就像 Transformer 里那样,多个注意力头各自学习不同的关系模式,然后拼接或平均:

# 伪代码示例:多头GAT
heads = 4
head_outputs = []
for i in range(heads):
    head_outputs.append( gat_attention(x, adj) )
output = torch.cat(head_outputs, dim=-1)  # 拼接
# 如果最后一层,用平均

为什么会这样设计?单一注意力头可能只关注一种关系,比如距离。多个头就能同时关注距离、角度、电负性等多种因素。我在分子生成任务里常用 8 个头,效果比较稳定。

避坑指南:曾经我在一个含噪声的数据集上直接跑 GAT,模型完全不收敛。后来发现是因为注意力系数的分布过于极端——某些邻居权重趋近于 0,某些趋近于 1。解决办法是加一个 dropout 在注意力权重上,强制模型不依赖少数邻居。嗯,这一点很容易被忽略。

GraphSAGE:搞定大规模图

GCN 和 GAT 都有一个共同的问题——它们需要整张图的邻接矩阵参与计算。这意味着,图有多大,计算开销就有多大。对于分子生成这种场景,虽然单个分子很小,但一次要处理上百万个分子时,全图计算就吃不消了。

GraphSAGE 的思路完全不同。它不做全图批量更新,而是对每个节点采样出固定数量的邻居,然后只用这些采样到的邻居来聚合。

具体来说就是三步:

  1. 采样:为每个节点采样 K 层邻居,每层固定采样 S 个
  2. 聚合:用聚合函数(均值、LSTM、池化)处理采样到的邻居特征
  3. 更新:将聚合结果与当前节点特征拼接,过一层 MLP

其中聚合函数有多种选择:

聚合函数 特点 适用场景
Mean 聚合 对邻居取平均,最简单 特征分布均匀时
LSTM 聚合 考虑邻居顺序,但有偏 邻居有序列关系时
Pool 聚合 对邻居特征做 Max/Mean 池化 需要提取显著特征时

我建议新手从 Mean 聚合开始,效果已经足够好。GraphSAGE 的实际实现代码,以 NeighborLoader 的方式采样:

from torch_geometric.loader import NeighborLoader

# 对一个分子图进行2层采样
loader = NeighborLoader(
    data,
    num_neighbors=[10, 5],  # 第一层采样10个,第二层5个
    batch_size=32,
    shuffle=True
)

for batch in loader:
    out = model(batch.x, batch.edge_index)
    # 只取 batch 中的目标节点做 loss
    loss = criterion(out[batch.n_id], batch.y)

GraphSAGE 的优势:训练时只使用采样得到的子图,内存开销从 O(|V|) 降到 O(S^K)。这让我们可以处理百万甚至上亿节点的图。更重要的是,聚合函数可以灵活替换,这使得 GraphSAGE 成了一个框架而不是单一模型。

我记得有一个项目要把 1000 万个分子的图特征提取出来。用 GCN 直接算,显存直接爆了。换成 GraphSAGE 后,采样邻居数为 5,深度为 2,单个 batch 只处理几百个节点,妥妥跑下来了。这就是工程中的实际选择。

三种模型的选型指南

说了这么多,到底该用哪个?我直接给一个对比表,大家根据场景选:

维度 GCN GAT GraphSAGE
聚合方式 归一化均值 学习注意力权重 采样 + 自定义聚合
邻居范围 全部邻居 全部邻居 采样邻居
计算复杂度 O(|E|) O(|E| * d) O(S^K * B)
小图性能 ★★★★ ★★★★★ ★★★
大规模图性能 ★ ★★ ★★★★★
分子生成适用度 基础分子性质 官能团识别 大规模预训练

拿分子生成来说:

  • 如果你只是做个基础的性质预测,比如 logP、溶解度,GCN 就够了
  • 如果需要模型感知不同化学键的重要性,用 GAT 效果更好
  • 如果要做图级的大规模预训练,GraphSAGE 是目前最实用的选择

工程建议:我个人习惯是先跑一个 GCN 基线,验证数据流程没问题。然后换成 GAT 看提升幅度。如果数据量很大,直接上 GraphSAGE。这样迭代的成本最低,问题暴露得也最早。

三种方法其实都是在解决同一个问题——如何用邻居信息更新节点特征。区别只在于「如何选择邻居」和「如何聚合它们的特征」。深层的东西,都是信息传递框架下的不同实现。理解了这个本质,后面学更复杂的模型,比如 Graph Isomorphism Network 或者消息传递神经网络,就顺理成章了。