
图论这块我接触得比较早但真正让我觉得图不是一个抽象数学概念而是在实际工程里能直接落地的还是近两年做图神经网络项目的时候。你去看任何一篇GNN的论文不管是GCN、GAT还是GraphSAGE开头引言部分必然会出现两个东西邻接矩阵和度矩阵。很多人卡在入门这一步不是因为神经网络不懂而是因为图本身的表示方式没吃透。这篇就把最基础的部分一次性讲清楚图是什么邻居怎么定义邻接矩阵和度矩阵到底怎么构建、怎么用以及这些概念在图计算和图神经网络里是怎么发挥作用的。1. 图的基本概念先搞清楚我们在说什么1.1 图不是什么高深的东西图Graph在计算机科学里的定义其实非常朴素它由一组顶点Vertex也叫节点Node和一组连接顶点的边Edge也叫链接Link组成。你不用把它想得多复杂它就是一种描述事物之间关系的数据结构。人话版本你在微信里的好友关系就是一个图。每个人是一个节点两个人互加好友就是一条边。你在一个社交网络里所有人和所有好友关系组合起来就是一个完整的图。类似地电商里的用户和商品可以构成图交通路网可以构成图分子结构里的原子和化学键也可以构成图。图的严谨记法是 G (V, E)。V是顶点集合E是边集合。每条边连接两个顶点比如一条边 e (u, v) 表示顶点u和顶点v之间存在某种关联。这里有个很关键的点图不关心节点的具体内容也不关心边究竟代表什么业务含义。它只关心结构。这个只关心结构的特性恰恰是图之所以强大的原因。因为结构是通用的你可以把社交网络、蛋白质分子、电路设计全都抽象成同一种数学对象来处理。1.2 从数据结构角度理解图的分类图的主要分类方式有这么几种我直接列一个对比表方便你快速建立全局认知分类维度类型说明典型场景边是否有方向无向图边没有方向u到v和v到u是同一条边好友关系、分子结构边是否有方向有向图边有方向u到v和v到u是两条不同边关注关系、网页超链接边是否带权重无权图边只有存在/不存在两种状态最简单的结构分析边是否带权重加权图边带有数值表示关系强度或代价交通距离、相似度矩阵边的存在形式简单图没有自环自己连自己没有重边大多数算法默认假设边的存在形式多重图两个节点之间可能有多条边通信网络、交易网络实际工程里你大概率遇到的是简单图或者带权重的简单图这个前提在后面的矩阵构建里非常重要。如果你忽视了图的类型后面所有矩阵操作都可能出错。1.3 为什么要用矩阵来表示图图本身只是一种抽象概念但计算机要处理它必须有具体的存储结构。最常见的两种直观方式是邻接表和邻接矩阵。邻接表很好理解每个节点后面跟着一个列表列表里存的是它直接相连的节点。比如节点1连着节点2和节点3那就记录 1: [2, 3]。邻接矩阵则是一个方阵行和列都对应图里的节点矩阵第i行第j列的值表示节点i和节点j是否有边连接。你可能会问既然有邻接表这么直观的东西为什么还要搞邻接矩阵原因在于矩阵是数学工具可以进行线性代数运算。图神经网络的核心操作——消息传递、特征聚合、拉普拉斯变换——全都建立在矩阵运算之上。你用邻接表没法直接做矩阵乘法但用邻接矩阵可以。这就是为什么所有图神经网络框架的底层数据装载都离不开邻接矩阵和度矩阵。2. 邻居与邻接矩阵图的局部结构2.1 邻居到底是怎么定义的说到邻居Neighbor这是图论和图算法里被反复提到的高频词。定义很直接节点 v 的邻居是所有与 v 直接相连的节点集合。用 N(v) 来表示。举个例子三个人 A、B、C其中 A 跟 B 是好友A 跟 C 也是好友但 B 和 C 不是好友。那么 N(A) {B, C}N(B) {A}N(C) {A}。邻居这个概念看上去简单但它承载了图论里一个极其深刻的思想在图里一个节点的性质和行为主要由它周围的邻居决定。你可以孤立地知道A的年龄、职业、爱好但如果你想知道A会不会被某条信息影响你必须看A的邻居都在干什么。这个思想就是图神经网络消息传递机制的哲学基础。邻居还分一阶邻居和多阶邻居。一阶邻居就是直接相连的节点二阶邻居就是邻居的邻居也就是距离为2的节点。很多图算法会利用多阶邻居信息比如衡量两个节点是否相似就看它们的一阶邻居重合度有多大。2.2 邻接矩阵的构建规则现在来正经构建邻接矩阵。假设图里有 n 个节点那么邻接矩阵就是一个 n × n 的方阵 A。A[i][j] 的定义是对于无权图如果节点 i 和节点 j 之间有边A[i][j] 1否则 A[i][j] 0。对于有权图如果节点 i 和节点 j 之间有边A[i][j] ww 就是这条边的权重否则 A[i][j] 0。对角线上的 A[i][i] 默认是 0因为我们讨论的是没有自环的简单图。举一个具体的小例子。假设有一个无向图4个节点边集合为 {(1,2), (1,3), (2,4), (3,4)}。那么邻接矩阵就是import numpy as np n 4 A np.zeros((n, n)) edges [(0, 1), (0, 2), (1, 3), (2, 3)] # 0-indexed 对应1、2、3、4号节点 for u, v in edges: A[u][v] 1 A[v][u] 1 # 无向图必须对称赋值 print(A)输出结果是[[0. 1. 1. 0.] [1. 0. 0. 1.] [1. 0. 0. 1.] [0. 1. 1. 0.]]注意这里有个关键细节无向图的邻接矩阵一定是对称矩阵。因为边 (u,v) 和 (v,u) 是同一条边所以 A[u][v] 和 A[v][u] 必须相等。如果你在构建矩阵的时候只赋值了一次后面做矩阵运算的时候会发现矩阵不对称进而导致一系列诡异的结果。这个问题我在实际项目里遇到不下三次每次都是排查了半天才发现是构建阶段漏了对称赋值。2.3 邻接矩阵的维度爆炸与稀疏化邻接矩阵在概念上非常清晰但它有一个致命的问题空间复杂度是 O(n²)。如果你处理的图有一百万个节点邻接矩阵就有 10¹² 个元素光存储就需要 8TB 的内存按 float64 计算。这是不可能接受的。实际图数据中的边数量通常远小于 n²。社交网络里一个人平均好友数是几百但总用户数是几千万甚至几亿。这种边数远小于理论上限的图叫做稀疏图反过来就是稠密图。工程上用邻接矩阵处理稀疏图必须用稀疏矩阵格式比如CSRCompressed Sparse Row或者COOCoordinate List。好消息是PyTorch、TensorFlow、NumPy 这些主流框架都内置了稀疏矩阵支持。你以为你存了一个 n×n 的矩阵实际上底层只存了非零元素的行索引、列索引和值内存占用直接降到 O(边数)。PyGPyTorch Geometric和 DGLDeep Graph Library这两个主流图神经网络框架默认的数据结构就是稀疏格式。PyG 里的edge_index本质上就是邻接矩阵的COO表示两个数组一个存边的起点一个存边的终点。3. 度矩阵从邻居数量到归一化操作3.1 度的定义和度矩阵的结构在图论里节点 v 的度Degree定义为与 v 关联的边的数量也就是 v 的邻居个数简单图情况下。记作 d(v) 或者 D[v][v]。有向图要稍微注意出度out-degree是从该节点出发的边数入度in-degree是进入该节点的边数。无向图则只有一个度。度矩阵 D 是一个对角矩阵对角线上第 i 个元素是第 i 个节点的度其他位置全部是零。它和邻接矩阵的维度相同都是 n × n。所以度矩阵和邻接矩阵是同一个图的两种不同视角。邻接矩阵记录的是谁和谁相连度矩阵记录的是每个节点连了多少条边。两个矩阵合在一起你就能同时知道图的局部连接信息和全局分布信息了。3.2 度矩阵的Python实现继续用上面那个4节点的无向图来演示。图的边是 {(1,2), (1,3), (2,4), (3,4)}每个节点的度分别是节点1连了2条边节点2连了2条边节点3连了2条边节点4连了2条边。所以度矩阵是 diag(2, 2, 2, 2)。用代码来算# 继续使用上面构建的A矩阵 degree A.sum(axis1) # 每行求和得到每个节点的度 D np.diag(degree) print(degree) # [2. 2. 2. 2.] print(D)输出 D 就是[[2. 0. 0. 0.] [0. 2. 0. 0.] [0. 0. 2. 0.] [0. 0. 0. 2.]]无向图里邻接矩阵每行求和就是该节点的度因为每行非零元素的个数就是邻居个数。但如果你处理的是加权图A.sum(axis1) 得到的是带权度也就是所有边权之和这在图神经网络里也有对应用途比如带权聚合。你需要根据业务场景决定到底用度还是带权度。3.3 度矩阵和邻接矩阵联合使用的关键场景这两兄弟最大的一次联合作战就是图拉普拉斯矩阵Graph Laplacian的构造。标准拉普拉斯矩阵定义为 L D - A。它有几个非常有用的性质L 是对称半正定矩阵它的每一行之和都是0它的特征值可以反映图的一些全局结构性质比如连通分量的数量等于特征值为0的重数。这里也顺便解释一下为什么图卷积网络GCN里要用到度矩阵做归一化。GCN 的核心公式是H^(l1) σ( D̂^(-1/2) Â D̂^(-1/2) H^(l) W^(l) )其中 Â A I加自环的邻接矩阵D̂ 是 Â 的度矩阵。为什么非要加自环因为如果不加每个节点在聚合时不会考虑自身特征只聚合邻居特征这会导致自身信息丢失。加上自环后每个节点在聚合时天然地包含了自身的上一层特征。为什么用 D̂^(-1/2) 做对称归一化这是为了平衡度数的差异防止高度数节点在聚合中占据过大权重。如果你直接用 D̂^(-1) A随机游走归一化那权重分配就不对称了矩阵不再是相似变换的关系数值稳定性也会差一些。这个公式里的细节非常多很多初学者一上来就调GCN但并不知道 D^(-1/2) 是怎么来的更不知道为什么矩阵要用对称归一化而不是行归一化。理解了度矩阵的数学含义之后这些就都是顺理成章的事情了。4. 图的性质与邻居结构分析4.1 从邻接矩阵里读出图的结构特征邻接矩阵和度矩阵的价值不只是用来做图神经网络输入。它们也是图分析的起点两者结合可以快速获得一系列图的特征。图的总边数无向图等于邻接矩阵所有非零元素之和除以2有向图等于非零元素之和。从度矩阵的角度看所有节点的度之和再除以2就是边数。平均度所有节点度数的平均值反映图整体连接的密集程度。最大度/最小度反映图中是否存在超级枢纽节点比如社交网络里的大V或者孤立点。节点的重要程度度中心的定义就是直接看度的大小。度越大的节点在局部结构里越重要。图的连通性如果邻接矩阵没法由行交换和列交换变成块对角形式图大概率是全连通的。我实际做社交网络分析时拿到一份新的图数据第一步永远是用度分布来感受数据质量。正常社交网络是无标度网络度分布服从幂律分布少数节点度数极高大量节点度数很低。如果你画出来的度分布是均匀分布那数据大概率不是社交网络可能是均匀随机图这时候你就要重新审视数据的来源和采样方式了。4.2 邻居重叠与图聚类的直觉邻居这个概念还能引出一个很实用的相似度指标——共同邻居数。两个节点的共同邻居越多它们在结构上就越相似。举个例子用户A和用户B有20个共同好友用户B和用户C只有2个共同好友。直观上A和B的关系比B和C更紧密这就是社交推荐系统里你可能认识的人功能背后的基础逻辑。计算共同邻居数在邻接矩阵里非常方便两个节点的共同邻居就是它们对应行的逐元素乘积之和。代码是np.dot(A[i], A[j])因为只有当节点k同时和i、j相连时A[i][k] 和 A[j][k] 才同时为1乘积才为1。从邻接矩阵到共同邻居从共同邻居到节点相似度从相似度到社区发现和链路预测这是一条完整的分析链路。图的很多经典算法比如CNCommon Neighbors、AAAdamic-Adar指数本质上都是在玩邻居集合的运算。4.3 度分布与图数据质量检查做图数据处理时我建议你接手任何一份图数据先画出度分布直方图。这一步能快速暴露数据问题如果出现大量度为0的节点说明图可能包含很多孤立节点后续训练时这些节点的消息传递根本收不到任何邻居信息。如果最大度异常高可能说明数据里混入了错误的边或者确实存在超级枢纽节点。如果图的平均度特别低比如小于1那这个图基本相当于一堆碎片任何图神经网络都很难学习到有用的结构信息。遇到这些问题处理方案要分情况。孤立节点要么直接丢弃要么加自环让模型可以单独学习自身特征要么设计特殊的处理策略。在实际GCN训练中如果训练集包含大量孤立节点模型对这部分节点的预测效果会非常差因为没有任何邻居特征可以被聚合过来。5. 从邻接矩阵到度矩阵代码实现与工程化封装5.1 三种图存储格式对比做工程实现时你到底应该用哪种方式存储图这里整理我自己实践下来的结论存储格式空间复杂度随机访问节点邻居矩阵运算适用场景邻接表O(VE)快直接查列表不方便图遍历、路径搜索稠密邻接矩阵numpy二维数组O(V²)非常快O(1)方便小规模图节点1万稀疏邻接矩阵scipy.sparse / PyG EdgeIndexO(E)需要额外索引非常方便大规模图、图神经网络训练这里给一个真实的经验值如果节点数少于5000用稠密numpy矩阵完全没问题代码好写调试方便。如果节点数超过10万老老实实走稀疏路线不然内存会直接爆炸。1万到10万之间看边的密度边很少继续用稀疏边很多则两者皆可。5.2 使用NetworkX快速验证图论概念NetworkX是Python生态里最经典的图分析库非常适合做实验验证。你可以先在NetworkX里手工构建一个小图然后直接获取它的邻接矩阵和度矩阵import networkx as nx import numpy as np G nx.Graph() G.add_edges_from([(1, 2), (1, 3), (2, 4), (3, 4)]) # 获取邻接矩阵numpy格式 A nx.to_numpy_array(G, nodelist[1, 2, 3, 4]) # 获取度字典 deg dict(G.degree()) # 构建度矩阵 D np.diag([deg[n] for n in [1, 2, 3, 4]]) print(邻接矩阵:) print(A) print(度矩阵:) print(D) print(拉普拉斯矩阵 D-A:) print(D - A)这段代码输出里你会注意到一个问题nx.to_numpy_array返回的矩阵形状遵循 nodelist 的顺序如果你不指定 nodelist顺序可能和你的直觉不一致。这也是我在多节点构图中踩过的一个坑你以为矩阵第0行是节点1其实第0行对应的是节点4因为NetworkX内部节点存储顺序是按照插入顺序或排序规则排列的。所以任何时候做矩阵计算都要明确记录节点和矩阵行索引的映射关系这个映射关系是图数据处理中的基础中的基础。5.3 PyTorch Geometric里的邻接矩阵和度矩阵如果你走向图神经网络方向你会发现在PyG里邻接矩阵直接被封装成了edge_index格式。它不需要显式存储一个完整的邻接矩阵而只存储边的起点列表和终点列表本质上就是邻接矩阵的COO稀疏格式。from torch_geometric.data import Data import torch edge_index torch.tensor([[0, 1, 2, 3], [1, 2, 3, 0]], dtypetorch.long) # 表示四条边: (0,1), (1,2), (2,3), (3,0) x torch.randn(4, 16) # 4个节点每个节点16维特征 data Data(xx, edge_indexedge_index)PyG内部提供degree()方法可以根据 edge_index 快速计算度矩阵的对角线元素。这个度向量其实就是归一化操作里的 D^(-1/2) 的来源。你用一行代码deg degree(edge_index[0], num_nodes4)就能拿到每个节点的度而不需要先构建完整的 n×n 邻接矩阵。5.4 工程里如何处理带权图的邻接矩阵带权图的邻接矩阵每个非零元素不再固定为1而是边的权重。在PyG里权重可以单独用一个edge_attr张量存储和edge_index一一对应。处理带权图时有两个细节容易出问题。第一权重的尺度问题。如果边权范围跨度很大比如从0.001到10000直接把原始权重拿去做聚合操作数值很容易溢出或者被大数主导小权重边的作用会被忽略。建议做归一化或标准化把权重缩放到合理区间。我一般用对数变换或者最大最小值归一化。第二度矩阵要相应变为带权度。当你想计算图上每个节点的总权重时对邻接矩阵按行求和得到的就不再是邻居个数而是该节点所有出边权重的总和。这在很多加权图算法里比如PageRank的变体有直接应用。6. 实操中容易踩的坑与排查技巧6.1 邻接矩阵索引从0开始还是从1开始这是新手问得最多的问题。理论上图的顶点编号从1开始是数学家的习惯但所有主流编程语言里数组索引都是从0开始。这导致一个常见的bug你用NetworkX导出一个图节点id是1到1000但你把邻接矩阵转成numpy数组后矩阵的第0行对应的是节点1吗答案是不一定取决于你传给to_numpy_array的nodelist参数。我建议的做法是在处理任何图数据时第一步统一做一个重映射把原始节点ID映射为从0开始的连续整数并保存一份 id_to_idx 和 idx_to_id 的字典。这一步成本极低但能在后面省下大量排查时间。# 节点ID重映射 node_ids list(G.nodes()) id_to_idx {node_id: i for i, node_id in enumerate(node_ids)} idx_to_id {i: node_id for i, node_id in enumerate(node_ids)}6.2 有向图和无向图的矩阵差异这个问题我在多个项目里吃过亏。有向图的邻接矩阵不一定对称A[i][j] 表示存在从节点i指向节点j的边但这不代表存在从j指向i的边。做矩阵构建时如果忘了区分图类型代码可能会写出错误结果而毫无察觉。比如在社交场景的关注关系里A关注了B但B不一定关注A。如果你还在套用无向图的对称赋值方法那数据从源头上就坏了。判断方法是看你的业务语义如果边代表的是单向行为比如关注、转账、引用、跳转一律用有向图构建矩阵时只给对应位置赋值一次。如果边代表的是双向关系比如好友、同事、共现用无向图矩阵必须对称赋值。6.3 度矩阵对角线出现0的问题度数出现0意味着有孤立节点。在图神经网络训练里孤立节点会让消息聚合时出现除零或空聚合问题具体表现是这些节点的输出特征全变成0或者直接报NaN错误。处理这个问题有几种办法最简单的方式是给邻接矩阵加自环也就是 Â A I。加了自环后每个节点至少有一条连向自己的边度至少为1消息传递时不会出现空聚合。如果加了自环后仍然有节点度数为0那说明这个节点在矩阵里的索引对不上很有可能是构建矩阵时漏掉了这个节点的某条边这种数据质量问题是加自环救不了的。加自环这个操作在GCN里是标配。你仔细看GCN的公式里面用的就是 A I 而不是A本身。另外从度矩阵的角度看加自环会让每个节点的度增加1这对度的归一化操作影响很大。实际计算时需要注意使用的是 D̂含自环的度矩阵而不是 D原始度矩阵。6.4 稀疏矩阵运算中的隐式类型转换用scipy或者PyTorch的稀疏矩阵时矩离乘法和稠密矩阵乘法在行为上有差异。有些稀疏矩阵不支持某些原生操作比如直接对稀疏矩阵做元素级乘法时需要先调用to_dense()但这会导致内存暴涨。一个实际的优化建议在图神经网络训练中如果邻接矩阵在迭代过程中不变提前预计算归一化矩阵。不要每次训练迭代都重新做 D^(-1/2) A D^(-1/2) 的计算而是把归一化结果缓存下来。反正邻接矩阵又不变这么做纯粹是浪费算力。我第一次写GCN代码时就是没注意到这个每轮迭代都重新算归一化矩阵500轮训练多跑了20分钟优化之后速度大幅提升。6.5 图数据质量检查清单最后给一个我自己每次拿到新图数据都会走的检查流程你直接照着用就可以确认图的节点数和边数是否和文档描述一致。检查是否有重复边重复边会让邻接矩阵的构建产生歧义。检查图里是否有自环处理方式要和业务对齐。统计孤立节点的数量判断是否需要过滤。确认有向图还是无向图检查矩阵是否满足对应性质有向图检查汇入/汇出无向图检查对称性。查看度分布排除数据污染或构建错误。这套流程跑完大部分数据问题都能在进入模型训练之前暴露出来省得训练到一半才发现结果是垃圾。图的基础概念看似简单但它是所有图算法和图神经网络的地基。邻接矩阵描述了图的结构度矩阵描述了节点的连接数量两者结合又能衍生出拉普拉斯矩阵、归一化操作等更高级的工具。把这些概念吃透你再去看GCN论文或者PyG源码会发现整体逻辑通顺很多。我当年就是在这几个概念上反复折腾才搞明白图神经网络为什么这样设计希望这篇经验能帮你少走一些弯路。