4.6 图神经网络:在邻接结构上传播


文档摘要

4.6 图神经网络:在邻接结构上传播 本节摘要:CNN 与 RNN 的输入形状是规则网格与定长序列,但社交网络、分子结构、知识图谱的形状是任意的拓扑——节点数量不定、邻居数量不定。图神经网络(GNN)的解法是消息传递:每个节点收集邻居的消息、聚合、更新自身表示,重复若干轮后,表示里就揉进了多跳邻域的信息。本节讲消息传递三步与聚合函数的约束,用稀疏张量在 TensorFlow 里手写一层最小图卷积,并跑一个最小图分类例程。 学习目标 阅读完本节,你应当能够: 解释消息传递三步与"轮数即感受半径"的关系; 说明聚合函数为什么必须对邻居数量置换不变; 用邻接表加 gather 手写一层最小图卷积; 完成一个最小图分类任务,理解读出层的角色。

4.6 图神经网络:在邻接结构上传播

本节摘要:CNN 与 RNN 的输入形状是规则网格与定长序列,但社交网络、分子结构、知识图谱的形状是任意的拓扑——节点数量不定、邻居数量不定。图神经网络(GNN)的解法是消息传递:每个节点收集邻居的消息、聚合、更新自身表示,重复若干轮后,表示里就揉进了多跳邻域的信息。本节讲消息传递三步与聚合函数的约束,用稀疏张量在 TensorFlow 里手写一层最小图卷积,并跑一个最小图分类例程。

学习目标

阅读完本节,你应当能够:

  1. 解释消息传递三步与"轮数即感受半径"的关系;
  2. 说明聚合函数为什么必须对邻居数量置换不变;
  3. 用邻接表加 gather 手写一层最小图卷积;
  4. 完成一个最小图分类任务,理解读出层的角色。

消息传递:把卷积推广到任意拓扑

卷积核扫网格,图卷积"扫"邻接表。每一轮消息传递三步:收集(每个节点向邻居要消息)、聚合(把收到的消息合成一条,常用求和、取平均、取最大)、更新(把聚合结果与自身表示融合出新表示)。关键性质是聚合函数必须置换不变——邻居 A、B、C 按什么顺序聚合,结果必须一样,因为图的邻居本来就没有顺序。求和、平均、最大都满足;拼接再接线性层则不满足。轮数决定信息传播半径:一轮只看直接邻居,两轮看到邻居的邻居——与卷积核堆叠扩感受野是同构的思想。

图 21 消息传递一轮的三步

图 21 消息传递一轮的三步

手写一层最小图卷积

TensorFlow 主包不含 GNN 模块,生产中通常用社区库(如 spektral 或 tensorflow_gnn),但理解机理最好的方式是亲手写一层——数据用邻接表(边的两端编号对),聚合用 gather:

import tensorflow as tf import numpy as np # 6 个节点的图:节点特征 4 维,8 条边(双向记 16 个方向) node_feat = tf.random.normal([6, 4]) edge_index = tf.constant([[0, 1], [0, 2], [1, 3], [2, 3], [3, 4], [3, 5], [4, 5], [0, 3]], dtype=tf.int32) edge_both = tf.concat([edge_index, tf.reverse(edge_index, axis=[1])], axis=0) # 16 行:每行 源节点 到 目标节点 class MiniGraphConv(tf.keras.layers.Layer): """最小图卷积:邻居聚合求平均,再与自身线性变换融合""" def __init__(self, units): super().__init__() self.self_w = tf.keras.layers.Dense(units) # 自身变换 self.neigh_w = tf.keras.layers.Dense(units) # 邻居消息变换 def call(self, feats, edge_src, edge_dst): msg = self.neigh_w(tf.gather(feats, edge_src)) # 收集:沿边取源节点表示 # 聚合:按目标节点分组求和,tf.math.unsorted_segment_sum 置换不变 agg = tf.math.unsorted_segment_sum(msg, edge_dst, num_segments=tf.shape(feats)[0]) deg = tf.math.unsorted_segment_sum(tf.ones_like(msg[:, :1]), edge_dst, num_segments=tf.shape(feats)[0]) agg = agg / tf.maximum(deg, 1.0) # 平均化,度归一 return tf.nn.relu(self.self_w(feats) + agg) # 更新:自身加邻域 layer = MiniGraphConv(8) out = layer(node_feat, edge_both[:, 0], edge_both[:, 1]) print(out.shape) # 输出:(6, 8) # 每个节点的新表示揉进了一跳邻居的信息,节点数不变、维度换成 8

三个实现要点。其一,边要双向化:消息传递默认无向,存图时把每条边正反各记一次。其二,unsorted_segment_sum 是图聚合的核心原语——按目标节点编号分组求和,天然置换不变,度归一(除以邻居数)防止高度节点表示被放大。其三,gather 加 segment 组合让整个过程都是张量运算,GPU 并行友好——这就是社区库内部的基本手法。

最小图分类例程

图分类的目标是给整张图打标(分子是否有毒、社区是否异常),需要在节点表示之上加读出层:把所有节点表示池化成一个图级表示。堆两层图卷积扩感受野,全局平均池化读出,接全连接分类:

def graph_batch(node_feats, edges_src, edges_dst, batch_ids, n_nodes): """把多个小图拼成一个大批次:batch_ids 标记每个节点属于哪张图""" x = node_feats for gcn in gnn_layers: x = gcn(x, edges_src, edges_dst) graph_vec = tf.math.unsorted_segment_mean(x, batch_ids, n_nodes) # 读出 return graph_vec gnn_layers = [MiniGraphConv(16), MiniGraphConv(8)] # 合成两张 5 节点图做演示 feats = tf.random.normal([10, 4]) src = tf.constant([0,1,1,2,3, 5,6,6,7,8], dtype=tf.int32) dst = tf.constant([1,2,0,1,4, 6,7,5,8,9], dtype=tf.int32) batch_ids = tf.constant([0,0,0,0,0, 1,1,1,1,1], dtype=tf.int32) graph_vec = graph_batch(feats, src, dst, batch_ids, 2) print(graph_vec.shape) # 输出:(2, 8) —— 两张图各浓缩成一个 8 维图级向量 logits = tf.keras.layers.Dense(2)(graph_vec) print(logits.shape) # 输出:(2, 2) # 图级向量接全连接即分类头,之后与普通模型一样 compile、fit

多图批量拼图(把小图们拼成一个大图、用 batch_ids 区分归属)是 GNN 工程的标准技巧——它让不定形的图批次能装进统一的张量管道,与第 2 章的批次约定接上了轨。

谱系与选型

图卷积按聚合设计分三个流派:谱方法(GCN 为代表,把度归一的邻接矩阵直接当变换矩阵,简单高效)、消息传递流派(GraphSAGE、MPNN,聚合函数可学习、支持采样邻居应对超大图)、注意力流派(GAT,邻居重要性用注意力打分——4.5 的机制直接移植过来)。选型按图规模:小图全批训练用 GCN 最省心;百万节点级大图必须选带邻居采样的流派;需要强调特定邻居重要性时上 GAT。

⚠️ 常见坑:堆太多轮消息传递。轮数多了所有节点的表示趋于雷同(过平滑),分类性能不升反降——节点分类一般两到三轮封顶,靠加深宽度与残差而非堆轮数扩感受野。

💡 关键直觉:GNN 就是"把卷积的邻域概念从网格搬到图上"——收集、聚合、更新对应卷积的窗口采样、加权求和、非线性变换。看懂这一层对应,整个家族的变体都只是聚合函数的花样。

本节要点回顾

  • 消息传递三步:收集、聚合、更新;轮数即跳数即感受半径。
  • 置换不变:聚合函数对邻居顺序不敏感,segment 求和是核心原语。
  • 最小图卷积:gather 拉消息、segment 聚合、度归一、自身融合。
  • 图分类读出:节点表示池化成图级向量,多图用 batch_ids 拼批。
  • 三流派选型:小图 GCN、大图采样式、重要性敏感用 GAT。

第 4 章到站。第 5 章接管排程室本身:自定义、加速与部署。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U