3. 图数据表示:邻接表、邻接矩阵、特征矩阵、标签矩阵
在团伙识别任务中,图数据是核心载体。与图像或文本数据不同,图数据需要同时存储结构信息(谁与谁相连)和属性信息(节点/边有什么特征)。本章系统梳理四种最基础的图数据表示形式,并给出它们在 Python 中的对应实现。
3.1 四种表示形式概览
| 表示形式 | 存储内容 | 典型形状 | 团伙识别中的作用 |
|---|---|---|---|
| 邻接表 | 每个节点的邻居列表 | list of list | 快速遍历邻居、采样子图 |
| 邻接矩阵 | 节点间连接关系(0/1 或权重) | N × N | 矩阵运算、谱分析、GCN 传播 |
| 特征矩阵 | 每个节点的属性向量 | N × F | 输入到 GNN 的节点特征 |
| 标签矩阵 | 节点/边的类别标注 | N × C 或 N × 1 | 监督训练、团伙分类 |
实战提示:在 PyG (PyTorch Geometric) 中,图数据通常以
Data(x, edge_index, y) 形式组织,其中 edge_index 是邻接表的紧凑变体(COO 格式),x 是特征矩阵,y 是标签矩阵。
3.2 邻接表:最直观的邻居存储
邻接表为每个节点维护一个列表,记录其直接相连的邻居节点编号。对于团伙网络,邻接表可以快速回答“某成员与哪些人直接联系”。
# 示例:6个节点的团伙网络邻接表
adj_list = [
[1, 2], # 节点0的邻居:1, 2
[0, 2, 3], # 节点1的邻居:0, 2, 3
[0, 1, 4], # 节点2的邻居:0, 1, 4
[1, 4, 5], # 节点3的邻居:1, 4, 5
[2, 3, 5], # 节点4的邻居:2, 3, 5
[3, 4] # 节点5的邻居:3, 4
]
团伙识别场景:当需要从某个嫌疑人出发进行 k 跳邻居采样时,邻接表是最高效的数据结构,时间复杂度 O(degree)。
3.3 邻接矩阵:代数视角的图结构
邻接矩阵 A 是一个 N×N 的方阵,A[i][j] = 1 表示节点 i 与节点 j 相连(无向图对称,有向图不对称)。在 GraphSAGE 中,邻接矩阵用于消息传递的归一化聚合。
import numpy as np
# 对应上述邻接表的邻接矩阵(无向图)
A = np.array([
[0, 1, 1, 0, 0, 0],
[1, 0, 1, 1, 0, 0],
[1, 1, 0, 0, 1, 0],
[0, 1, 0, 0, 1, 1],
[0, 0, 1, 1, 0, 1],
[0, 0, 0, 1, 1, 0]
], dtype=np.float32)
print("邻接矩阵形状:", A.shape) # (6, 6)
print("度矩阵 D = diag(sum(A, axis=1)):", np.diag(A.sum(axis=1)))
内存警告:对于百万节点级别的团伙网络,邻接矩阵需要 N² 存储空间,通常不可行。此时必须使用稀疏格式(如 COO、CSR)或邻接表。
3.4 特征矩阵:节点属性数字化
特征矩阵 X 的每一行对应一个节点的特征向量。在团伙识别中,特征可以是交易金额、通话频率、地理位置编码、历史行为统计等。
# 假设每个节点有4个特征:[交易次数, 平均金额, 联系人数, 活跃天数]
X = np.array([
[12, 3500, 8, 45], # 节点0
[8, 2800, 6, 30], # 节点1
[15, 4200, 10, 60], # 节点2
[5, 1500, 4, 20], # 节点3
[20, 5000, 12, 90], # 节点4
[10, 3000, 7, 40] # 节点5
], dtype=np.float32)
print("特征矩阵形状:", X.shape) # (6, 4)
print("节点2的特征向量:", X[2]) # [15, 4200, 10, 60]
归一化建议:特征矩阵在输入 GraphSAGE 前通常需要标准化(Z-score 或 Min-Max),避免数值大的特征主导聚合权重。
3.5 标签矩阵:监督信号
标签矩阵 Y 存储节点或边的类别标注。对于团伙识别,通常每个节点被标记为“团伙成员”或“正常用户”(二分类),也可能有多个团伙类别(多分类)。
# 二分类标签:0=正常,1=团伙成员
y_binary = np.array([0, 0, 1, 0, 1, 1], dtype=np.int64)
# 多分类标签(one-hot 编码):3个团伙 + 1个正常
# 类别映射:0=正常, 1=团伙A, 2=团伙B, 3=团伙C
y_multiclass = np.array([
[1, 0, 0, 0], # 节点0:正常
[1, 0, 0, 0], # 节点1:正常
[0, 1, 0, 0], # 节点2:团伙A
[1, 0, 0, 0], # 节点3:正常
[0, 0, 1, 0], # 节点4:团伙B
[0, 0, 0, 1] # 节点5:团伙C
], dtype=np.float32)
print("二分类标签形状:", y_binary.shape) # (6,)
print("多分类标签形状:", y_multiclass.shape) # (6, 4)
标签不平衡陷阱:团伙识别中正样本(团伙成员)通常远少于负样本(正常用户)。训练时需使用加权损失或采样策略,否则模型会偏向预测为正常。
3.6 四种表示在 GraphSAGE 中的协同
GraphSAGE 的前向传播同时依赖这四种表示:
- 邻接表(或 edge_index)控制消息传递的拓扑路径。
- 邻接矩阵(或其归一化形式)用于聚合时的权重计算。
- 特征矩阵作为每一层聚合的输入。
- 标签矩阵在训练阶段计算损失,驱动反向传播。
3.7 实战:构建团伙图的完整数据对象
以下代码将上述四种表示整合为 PyG 的 Data 对象,这是后续 GraphSAGE 训练的标准输入格式。
import torch
from torch_geometric.data import Data
# 1. 特征矩阵 (6节点, 4特征)
x = torch.tensor([
[12, 3500, 8, 45],
[8, 2800, 6, 30],
[15, 4200, 10, 60],
[5, 1500, 4, 20],
[20, 5000, 12, 90],
[10, 3000, 7, 40]
], dtype=torch.float)
# 2. 边索引 (COO格式:2 × E)
# 无向边:每条边出现两次
edge_index = torch.tensor([
[0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5],
[1, 2, 0, 2, 3, 0, 1, 4, 1, 4, 5, 2, 3, 5, 3, 4]
], dtype=torch.long)
# 3. 标签 (二分类)
y = torch.tensor([0, 0, 1, 0, 1, 1], dtype=torch.long)
# 4. 构建 Data 对象
data = Data(x=x, edge_index=edge_index, y=y)
print("=== 团伙图数据对象 ===")
print(data)
print("节点数:", data.num_nodes)
print("边数:", data.num_edges)
print("特征维度:", data.num_node_features)
print("类别数:", len(torch.unique(y)))
关键理解:在 PyG 中,
edge_index 是邻接表的紧凑替代,x 是特征矩阵,y 是标签矩阵。邻接矩阵通常不直接存储,而是在消息传递时由 edge_index 动态构建稀疏矩阵参与运算。
3.8 四种表示的适用场景对比
| 场景 | 推荐表示 | 原因 |
|---|---|---|
| 小图(N < 10⁴) | 邻接矩阵 | 方便矩阵运算和谱分解 |
| 大图(N > 10⁵) | 邻接表 / COO | 节省内存,支持邻居采样 |
| 节点属性丰富 | 特征矩阵 | 直接作为 GNN 输入 |
| 半监督学习 | 标签矩阵 + 掩码 | 只对部分节点标注,其余用于推理 |
| 动态图 / 流式图 | 邻接表 | 支持增量添加边和节点 |