1. 图神经网络基础:图数据表示与邻接矩阵
说实话,我刚接触图神经网络那会儿,第一反应是:这不就是关系数据库嘛?
后来栽了个跟头才明白——图数据最大的坑,恰恰在于它没有固定的"表结构"。每个节点可以带不同数量的邻居,边可以带权也可以不带,甚至节点特征维度都可能不一样。今天咱们就从最底层的图表示聊起,把邻接矩阵这东西彻底吃透。
1.1 图数据的核心三要素
一个图,说白了就三样东西:节点集合、边集合、特征矩阵。
我在做推荐系统项目时吃过一个闷亏:客户把用户行为数据导成了一张 Excel 表,每行是一条边。我拿到手以为可以直接当图训练,结果发现用户换设备后 ID 对不上,孤立节点一堆。后来我养成了习惯——先问清楚三个问题:节点是什么?边怎么定义的?特征从哪来?
| 元素 | 符号 | 典型含义 | 我踩过的坑 |
|---|---|---|---|
| 节点集 | V | 用户、物品、分子原子 | 节点 ID 不连续,存成 dict 更安全 |
| 边集 | E | 交互、链接、化学键 | 千万注意无向边别存成两条 |
| 特征矩阵 | X | 节点属性(年龄、词向量) | 维度不统一时先补齐或做掩码 |
你想想看,如果特征矩阵 X 的形状是 [N, d],N 是节点数,d 是特征维数。这玩意儿在 GraphSAGE 里就是采样的起点。
1.2 邻接矩阵:图的"身份证"
邻接矩阵 A 是图最直接的数学表示。A[i][j] = 1 表示节点 i 和节点 j 之间有边,否则为 0。无向图的话,A 是对称的。有向图就反着来。
核心要点:GraphSAGE 的邻居采样,本质上就是在 A 的某一行里挑非零的位置。
但这里有个大问题——真实图往往太稀疏了。我记得有个社交网络数据,100 万用户,平均每人只有 50 个好友。邻接矩阵存储的话,得开 100 万 × 100 万的矩阵,99.99% 是 0。那内存直接爆炸。
所以实践中几乎没人用全量邻接矩阵,都改用稀疏格式。常见的两种:
- COO(坐标列表):存三列 (i, j, value)。直观但做矩阵乘法慢。
- CSR(压缩行存储):存行偏移指针 + 列索引 + 值。GraphSAGE 的 DGL 和 PyG 底层都是 CSR。
我刚开始手撸 GNN 时,傻乎乎用 numpy 的稠密矩阵存一个 10 万节点图,结果还没跑采样内存就炸了。后来改用 scipy.sparse.csr_matrix,才把 demo 跑通。嗯,教训深刻。
1.3 邻接矩阵的变体:归一化与自环
直接用 0/1 邻接矩阵做消息传递有问题——度数大的节点会收到海量信息,特征值容易爆炸。所以 GraphSAGE 里一般会做对称归一化:
# 伪代码:对称归一化邻接矩阵
A_norm = D^{-0.5} @ A @ D^{-0.5}
# 其中 D 是度对角矩阵,D[i][i] = 节点 i 的度数
这样一来,每个邻居的贡献会被除以 sqrt(deg(i)) * sqrt(deg(j)),高 degree 节点不会过度主导。
另一个常见操作是加自环:A_hat = A + I。为啥要做这个?因为 GraphSAGE 聚合邻居时,如果不加自环,节点自己的特征就丢了。我刚开始实验时忘了加,结果节点表示越更新越像邻居的均值,自己的身份特征全没了——这就好比开圆桌会议,每个人只讲别人的观点,最后谁是谁都分不清了。
个人经验:在 DGL 或 PyG 里,加自环可以用 dgl.add_self_loop(g),但要注意图如果太大,自环会增加度数,影响归一化计算。我习惯先加自环再归一化。
1.4 图表示的结构总览
下面这张图把上面讲的核心要素串在一起。你可以把它当作 GraphSAGE 数据准备的"体检清单":
1.5 邻接矩阵的归纳总结
我用一个表格把你最需要记住的要点列出来,方便后续实验查漏:
| 概念 | 关键点 | 我踩过的坑 |
|---|---|---|
| 图的三要素 | V、E、X 缺一不可 | 特征 X 和节点顺序不一致 |
| 邻接矩阵 A | N×N 稀疏矩阵,CSR 存储 | 直接用稠密阵存 10 万节点,内存不足 |
| 对称归一化 | D^{-0.5} A D^{-0.5} | 忘了算度矩阵,特征值爆炸 |
| 加自环 | A + I,保留自身特征 | 没加自环,节点表示漂移 |
| 稀疏格式 | COO 适合构建,CSR 适合运算 | PyG 默认 CSR,DGL 也用 CSR |
1.6 实操检查清单
我自己每次搭建 GraphSAGE 数据管道时,都会过一遍下面这几点:
- 节点 ID 是否连续?如果不连续,建邻接矩阵时容易数组越界。我用 dict 映射一下。
- 边是否有重复?有些数据源会存双向边,对无向图来说就是两条。去重前先确认意图。
- 特征是否对齐?特征矩阵的行序号必须和节点 ID 映射后的索引一致。错位了模型白训。
- 自环和归一化顺序?我习惯先加自环再归一化,因为自环改变了度数矩阵。
- 稀疏矩阵形状?确保 (N, N),别写成 (N, M),M 是边数。
避坑提醒:我曾经在生产环境忘了对邻接矩阵做.astype(np.float32),结果所有边权重都是 True/False,矩阵乘法直接报类型错。养成好习惯:A = A.astype(np.float32)。
好了,图数据表示和邻接矩阵这块,底子就算打好了。你看,整个图神经网络的数据流动,其实就围绕这几个矩阵在转。后面我们在讲 GraphSAGE 的采样和聚合时,每一步都绕不开今天说的这些东西。