3. 消息传递框架与图卷积网络
图神经网络的核心问题就一个:怎么把邻居的信息传给中心节点? 你想想看,图像有固定的网格,序列有前后顺序,但图呢?每个节点的邻居数量都不一样,邻居也没有顺序。传统的卷积操作直接搬过来,根本行不通。那怎么办?学术界给出的答案就是 消息传递框架。
我个人习惯把消息传递拆成三步来看:发消息、收消息、更新自己。听起来很简单,对吧?但就是这个简单的框架,统一了绝大多数图神经网络的变体。从 GCN 到 GAT,从 GraphSAGE 到 GIN,背后都是这套逻辑。
3.1 为什么图学习需要消息传递?
我们先想想卷积在图像上是怎么做的。一个 3x3 的卷积核,每次固定看 9 个像素,位置关系是确定的。但分子图呢?一个碳原子可能有 3 个邻居,也可能有 4 个,邻居的排列顺序也不固定。没法用一个固定尺寸的 "卷积核" 去扫。
消息传递的做法是:每个节点主动向邻居发送自己的特征,然后每个节点把收到的所有消息汇总,再结合自己的特征做更新。 这样一来,不管你有几个邻居,不管邻居的顺序如何,处理流程都是一样的。我最早接触这个思路时,觉得它特别像 "小区业主群"——每家发一条消息,群主把消息汇总后再发个公告。嗯,差不多就是这个意思。
在 QSAR 场景里,消息传递尤其重要。分子中的每个原子看它的化学环境(邻居原子、键类型),通过消息传递才能知道整个分子的性质。比如一个苯环上的碳,如果不知道它连了哪个官能团,你根本判断不了它的化学位移。
3.2 消息传递框架的数学形式
消息传递框架(Message Passing Neural Network, MPNN)由 Gilmer 等人在 2017 年正式提出。它定义了三个核心函数:消息函数 φ、聚合函数 □、更新函数 ψ。整个过程分两步:
- 消息生成与聚合:每个节点 i 收集所有邻居 j 的消息,经过聚合得到 m_i
- 节点更新:节点 i 用自身特征和聚合消息,更新为新特征
写成公式就是:
m_i = □_{j ∈ N(i)} φ(x_i, x_j, e_ij)
x_i' = ψ(x_i, m_i)
其中 x_i 是节点 i 的特征,e_ij 是边特征,N(i) 是 i 的邻居集合。φ 生成消息,□ 做聚合(求和、平均、取最大等),ψ 做更新。不同的 GNN 模型,本质上就是 φ、□、ψ 的不同实现。
下面这张图展示了消息传递的完整流程:
你看,这个流程是典型的三段式。我在做 QSAR 项目时,最初就把消息传递理解成 "邻居特征取平均",后来发现不对——不同化学键类型(单键、双键、芳香键)对原子的影响权重完全不同,所以 φ 函数必须把键类型也编码进去。这个细节后面讲 GAT 时会再提到。
3.3 图卷积网络(GCN)
图卷积网络(Graph Convolutional Network, GCN)是消息传递框架最经典的实例。它由 Kipf & Welling 在 2017 年提出。GCN 的核心思想是:用归一化的邻接矩阵做消息聚合,再加一个线性变换和激活函数。
GCN 的层间更新公式是这样的:
H^(l+1) = σ( D̃^(-1/2) Ã D̃^(-1/2) H^(l) W^(l) )
其中 Ã = A + I(加上自环),D̃ 是 Ã 的度矩阵。这个公式说白了就干了三件事:加自环、对称归一化、线性变换 + 激活。
为什么加自环?因为在消息传递时,节点自己的特征也需要保留。我刚开始学的时候想:这不废话吗?但在实现中忘了加自环,结果模型收敛特别慢,因为节点在更新时「丢掉了自己」。后来我养成了习惯,每次写 GCN 第一件事就是检查 A_norm 有没有加 I。
对称归一化 D̃^(-1/2) Ã D̃^(-1/2) 的作用是防止高层节点的特征值过大,低度节点的特征被淹没。你可以把它理解成「每个人发言时间与 ta 的朋友数成反比」——朋友多的少说点,朋友少的多说点,这样讨论才公平。
3.4 消息传递的变体对比
GCN 只是消息传递框架的一种实现。不同的模型在 φ、□、ψ 上各有变化。我用一个表格来对比几个主流模型:
| 模型 | 消息函数 φ | 聚合函数 □ | 更新函数 ψ | 核心特点 |
|---|---|---|---|---|
| GCN | φ(xⱼ) = W·xⱼ | 对称归一化求和 | σ(·) | 简单高效,适合同配图 |
| GAT | φ(xᵢ, xⱼ) = α_ij·W·xⱼ | 注意力加权求和 | σ(·) | 可学习邻居权重 |
| GraphSAGE | φ(xⱼ) = W·xⱼ | 均值 / LSTM / 池化 | σ(Concat(·)) | 支持归纳学习,可采样邻居 |
| GIN | φ(xⱼ) = (1+ε)·xᵢ + xⱼ | 求和 | MLP(·) | 理论上表达能力最强 |
注意看,GAT 的 φ 函数引入了注意力系数 α_ij,这意味着每个邻居的贡献权重是学习出来的,不是固定的。我在处理蛋白质-配体相互作用时,GAT 的效果通常比 GCN 好 5%-10%,因为不同原子对结合自由能的贡献差异很大,注意力机制能自动找到关键原子。
GraphSAGE 的亮点在于「采样邻居」。对于超大规模的图(比如上亿节点),GCN 要聚合所有邻居,计算量吃不消。GraphSAGE 每层只采样固定数量的邻居,把复杂度降下来了。嗯,这个在工业界用得很多。
GIN(图同构网络)在理论上最强——它证明了求和聚合器加 MLP 更新函数能达到与 WL 测试同等的区分能力。不过在实际的 QSAR 任务中,GIN 并不总是碾压 GCN,因为分子图本身没有那么多「同构结构需要区分」的场景。我做过对比,在 Tox21 数据集上 GIN 比 GCN 高了不到 2%,但训练时间翻了一倍。
3.5 动手实现一个消息传递层
光说不练假把式。我用 PyTorch 实现一个最简的 GCN 消息传递层,让你感受一下代码的骨架:
import torch
import torch.nn as nn
class GCNMessagePassing(nn.Module):
def __init__(self, in_dim, out_dim):
super().__init__()
self.W = nn.Linear(in_dim, out_dim)
self.act = nn.ReLU()
def forward(self, X, A_norm):
# X : 节点特征, shape (N, in_dim)
# A_norm : 对称归一化邻接矩阵, shape (N, N)
# 消息聚合: 邻居特征加权求和
messages = A_norm @ X # (N, in_dim)
# 线性变换 + 激活
out = self.act(self.W(messages)) # (N, out_dim)
return out
就这么几行。你看,消息聚合就是 A_norm @ X,一行矩阵乘法搞定。但实际项目中,我不会直接用这种密集矩阵乘法,因为分子图大多是稀疏的。我会用 torch.sparse.mm 或者 PyG 的 MessagePassing 接口,把邻接矩阵存成稀疏格式,能省下一大块显存。
MessagePassing 基类,只需要实现 message()、aggregate()、update() 三个方法。框架帮你处理了邻居遍历和稀疏矩阵乘法。如果你手撸框架,记得用 scatter 操作做聚合,别写 for 循环——我在早期项目中用 for 循环聚合 100 万个原子,训练一次要 3 小时,换成 scatter 后缩短到 20 分钟。
3.6 实践中的坑与经验
最后聊几个我在实际项目中踩过的坑。你遇到了可以绕过。
还有一个容易被忽略的细节:消息函数的输入顺序。在 QSAR 中,化学键是有方向的(从原子 A 到原子 B),但分子图本身是无向图。如果你在 φ 中用了 x_i 和 x_j 的拼接,那么 (i→j) 和 (j→i) 的消息应该分开处理。我曾经在这里出过 bug,导致模型对分子图的对称性产生了错误假设,好在验证时发现了。
嗯,总结一下这章的核心:消息传递框架用「发消息→聚合→更新」三步骤统一了 GNN 的设计范式。GCN 是其中最简单有效的实现,而 GAT、GraphSAGE、GIN 各自在不同维度上做了扩展。理解了这个框架,你再去看任何 GNN 模型的论文,都能快速抓住它的本质——无非就是 φ、□、ψ 怎么设计的问题。