2. 图数据表示:节点、边、邻接矩阵、特征矩阵、图的基本属性

好,我们直接进入正题。图数据怎么表示?你别看网上那些公式一堆,其实核心就那几样东西。

我刚开始接触图神经网络时,也懵过一阵。那时候觉得“图”无非就是点和线,但真正上手做特征提取才发现,怎么把图“喂”给计算机,这里面门道不少。你想想看,计算机只认矩阵,不认画在纸上的圆圈和箭头。

这一章,我们就彻底把这层窗户纸捅破。

2.1 图的基本构成:节点和边

最简单的图,就两个要素:节点(Node)和边(Edge)。

节点就是实体。比如社交网络里,每个用户就是一个节点。分子结构里,每个原子就是一个节点。

边就是关系。用户之间的“关注”是边,原子之间的“化学键”也是边。

我个人习惯把图看作一个集合:

  • 节点集合 V = {v₁, v₂, …, vₙ}
  • 边集合 E ⊆ V × V

边上还能带方向。有方向的叫有向图,没方向的叫无向图。这在后面讲消息传递时很关键。

实战中需要注意:

有向图中,边的方向决定了信息的流动方向。我在做推荐系统时,用户点击商品是单向的,用户之间的好友关系却是双向的。建图时搞混方向,模型性能直接腰斩。

2.2 核心数据结构:邻接矩阵

好了,实体和关系都有了。怎么让神经网络理解?靠邻接矩阵 (Adjacency Matrix)。

说白了,邻接矩阵就是一个表格。表格的行和列都代表节点。如果节点 i 和节点 j 之间有边,那表格第 i 行第 j 列就填 1,否则填 0。

对于有 n 个节点的图,邻接矩阵 A 就是一个 n×n 的方阵:

A[i][j] = 1, 如果 (vᵢ, vⱼ) ∈ E
A[i][j] = 0, 其他情况

无向图的邻接矩阵是对称的,因为边是双向的。有向图则不一定。

一个小技巧:

我刚开始学的时候,总觉得邻接矩阵这玩意不就是个二维数组吗?有什么特别的?后来做图采样才发现,现实中的图往往是稀疏的,大部分节点之间没有连接。如果用稠密矩阵存,内存瞬间爆炸。这时候就需要用稀疏矩阵(比如 COO 格式)来存储了。

2.3 节点的“身份证”:特征矩阵

光有连接关系还不够。每个节点本身还有自己的属性。比如一个用户节点的年龄、性别、历史点击记录。

这些属性构成了特征矩阵 (Feature Matrix),通常用 X 表示。

假设每个节点有 d 个特征,那么 X 就是一个 n×d 的矩阵。第 i 行就是节点 vᵢ 的 d 维特征向量。

你可以理解为:邻接矩阵 A 描述的是结构,特征矩阵 X 描述的是个体。两者缺一不可。

避坑指南:

我遇到过几次特征矩阵和邻接矩阵维度对不齐的情况。节点编号从 0 开始还是从 1 开始?矩阵的行索引和节点 ID 是否一一对应?索引错位是个非常隐蔽的bug。我的习惯是:在建图时,先做一个节点 ID 到矩阵行号的映射表,后续所有操作都通过这个表来查。

2.4 图的“体检报告”:核心属性

建好图之后,我们得看看这个图长什么样。有几个关键属性,每次做特征提取之前我都习惯先算一下:

属性定义实战意义
度 (Degree)节点连接的边的数量度高的节点往往是“枢纽”,信息传播能力强
连通分量图中能够相互到达的最大节点子集图是否被分成多个孤立的“小团体”
最短路径两个节点之间经过的最少边数衡量节点之间的“距离”,影响消息传递范围

度 (Degree) 是最直观的。无向图中,一个节点的度等于邻接矩阵中该行(或该列)非零元素的数量。有向图里,还得分入度和出度。

连通分量 这概念有意思。一个图可能由好几个互不相连的子图组成。每个子图就是一个连通分量。如果你要做节点分类,而你的图有多个连通分量,模型很可能只能学习到分量内部的结构,跨分量的信息是无法传递的。

最短路径 我常用来分析图的“直径”。图直径就是所有节点对中最短路径的最大值。这个值大了,说明信息在图上传播慢,设计的 GNN 层数也要相应增加。

实战经验:

我记得有一次做交通流量预测。原始图包含了城市所有道路,结果发现图直径非常大。我尝试用最短路径算法计算节点间的“实际距离”,然后只用距离小于某个阈值的边来构建“局部邻接矩阵”。这么做下来,模型收敛速度快了一倍。

2.5 案例:用 Python 构建一个迷你图

光说不练假把式。我们手写一个最简单的图结构,把上面这些概念串起来。

下面这段代码,你用 Python 就能跑。我会用 networkx 来演示,但实际训练 GNN 时,我建议用 dgl 或 pyg,它们对 GPU 更友好。

import numpy as np

# 1. 定义节点和边
# 假设有 4 个节点:A, B, C, D
# 边:A-B, A-C, B-D, C-D (无向图)
num_nodes = 4
edges = [(0, 1), (0, 2), (1, 3), (2, 3)]

# 2. 构建邻接矩阵
adj_matrix = np.zeros((num_nodes, num_nodes), dtype=int)
for u, v in edges:
    adj_matrix[u, v] = 1
    adj_matrix[v, u] = 1  # 无向图对称

print("邻接矩阵 A:")
print(adj_matrix)

# 3. 计算每个节点的度
degrees = np.sum(adj_matrix, axis=1)
print("\n每个节点的度:", degrees)

# 4. 构建特征矩阵 (假设每个节点有2个特征)
# 节点A: [0.5, 1.2], B: [0.1, 0.9], C: [0.8, 0.3], D: [0.2, 0.7]
feature_matrix = np.array([
    [0.5, 1.2],
    [0.1, 0.9],
    [0.8, 0.3],
    [0.2, 0.7]
])
print("\n特征矩阵 X:")
print(feature_matrix)

# 5. 检查连通分量 (简单方法: BFS)
visited = [False] * num_nodes
components = []
for node in range(num_nodes):
    if not visited[node]:
        # BFS 遍历
        queue = [node]
        comp = []
        while queue:
            cur = queue.pop(0)
            if not visited[cur]:
                visited[cur] = True
                comp.append(cur)
                for neighbor in range(num_nodes):
                    if adj_matrix[cur, neighbor] == 1 and not visited[neighbor]:
                        queue.append(neighbor)
        components.append(comp)
print("\n连通分量:", components)

# 6. 计算最短路径 (Floyd-Warshall, 仅演示)
# 对于小图可以直接用
dist = np.full((num_nodes, num_nodes), np.inf)
for i in range(num_nodes):
    dist[i, i] = 0
    for j in range(num_nodes):
        if adj_matrix[i, j] == 1:
            dist[i, j] = 1
# Floyd
for k in range(num_nodes):
    for i in range(num_nodes):
        for j in range(num_nodes):
            if dist[i, j] > dist[i, k] + dist[k, j]:
                dist[i, j] = dist[i, k] + dist[k, j]
print("\n最短路径矩阵 (任意两点间距离):")
print(dist)

你运行这段代码,会看到邻接矩阵、度、连通分量和最短路径这些概念全部落地了。嗯,这才是我们做 GNN 特征提取的起点。

2.6 本章知识点结构图

最后,我把这一章的核心知识点画成一张图,方便你对照理解它们之间的关系。

图数据表示核心结构 图 G = (V, E) 节点集合 V 边集合 E 图基本属性 特征矩阵 X ∈ ℝⁿˣᵈ 邻接矩阵 A ∈ {0,1}ⁿˣⁿ 度 (Degree) 入度/出度 连通分量 子图结构 最短 路径 理解要点 • 邻接矩阵 A 和特征矩阵 X 是 GNN 的两个核心输入 • 度、连通分量、最短路径是分析图结构质量的三个关键指标 • 建图时务必检查索引对齐,这是最容易被忽略的 bug

到这,图数据表示的底子就打牢了。邻接矩阵、特征矩阵、图的基本属性——这三样东西,在后面几十章的案例里会反复出现。你只要有这几个概念在脑子里,跑代码时就不会发懵。