学习 GNN 时,最容易被公式掩盖的错误之一,是把边的方向理解反了。先用一个能手算的小图检查消息,再去实现更复杂的网络。

从数据含义开始

一张图至少需要说明三件事:节点代表什么、边代表什么、每个节点有哪些在预测时可用的特征。边可以表示依赖、连接或交互,不必然表示两个节点属于相同类别。

常见任务也不相同:节点分类为每个节点预测类别;链接预测判断一对节点之间的关系;图分类为整张图给出结果。不能把这三种任务的标签、数据划分和指标直接混用。

把一次消息传递拆开检查

  • 生成消息:从来源节点提取需要传递的信息。
  • 聚合消息:把流入目标节点的多条消息进行汇总。
  • 更新状态:结合原状态与聚合结果,得到新的节点表示。

下面约定 (source, target) 表示从前者流向后者。实际实现中必须核对库的约定,不能只凭矩阵的行列位置猜测。PyG 的 MessagePassing 文档给出了 flow、消息构造和聚合的接口定义。

一个独立编写的标量例子

def mean_incoming(features, edges):
    inbox = {node: [] for node in features}
    for source, target in edges:
        if source not in features or target not in features:
            raise ValueError("unknown node")
        inbox[target].append(features[source])
    return {
        node: sum(values) / len(values) if values else features[node]
        for node, values in inbox.items()
    }

features = {"a": 1.0, "b": 3.0, "c": 5.0, "isolated": 7.0}
edges = [("a", "c"), ("b", "c"), ("c", "b")]
result = mean_incoming(features, edges)
assert result == {"a": 1.0, "b": 5.0, "c": 2.0, "isolated": 7.0}
assert mean_incoming(features, list(reversed(edges))) == result
print(result)

本例刻意不加入自环;没有流入消息时保留原值。这只是为了便于检查的一种约定,不是所有 GNN 都如此处理。反转边列表的排列不会改变均值,但把每条边的方向反转,通常会改变结果。

这个函数没有可学习权重、损失函数和训练过程,因此不是“训练好了一个 GNN”。它只验证了消息方向、汇总和孤立节点处理。

真正训练前的检查单

  1. 有向图是否真的需要增加反向边?无向图在当前库里如何表示?
  2. 自环由数据预处理添加,还是由卷积层内部添加?避免无意重复。
  3. 重复边代表多次交互,还是应当去重的数据错误?
  4. 训练、验证和测试的边或图是否按任务协议划分?测试标签不能用于调参。
  5. 增加层数是否确有收益?更远的邻居未必带来更有用的信息。

课程对应与整理边界

对应疏锦行《26图神经网络专题》的《01_为什么需要GNN》《02_消息传递》讲义及消息传递视频目录。本文围绕实现检查整理,不复刻讲义图示或原代码;视频口头补充尚未完整核验。

原课程