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)或邻接表。
邻接表 vs 邻接矩阵(6节点团伙网络) 邻接表(稀疏存储) 节点0 → [1, 2] 节点1 → [0, 2, 3] 节点2 → [0, 1, 4] 节点3 → [1, 4, 5] 节点4 → [2, 3, 5] 节点5 → [3, 4] 存储空间:O(N + E) ≈ 6 + 7 = 13 邻居查询:O(degree) 邻接矩阵(稠密存储) 存储空间:O(N²) = 36 邻居查询:O(N) 对角线(自环) 有边连接 无边

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 的前向传播同时依赖这四种表示:

  1. 邻接表(或 edge_index)控制消息传递的拓扑路径。
  2. 邻接矩阵(或其归一化形式)用于聚合时的权重计算。
  3. 特征矩阵作为每一层聚合的输入。
  4. 标签矩阵在训练阶段计算损失,驱动反向传播。
GraphSAGE 数据流:四种表示的协同 邻接表 / edge_index 邻接矩阵 A 特征矩阵 X GraphSAGE 聚合 邻居采样 消息传递 聚合函数 (Mean / LSTM / Pool) 更新节点嵌入 节点嵌入 Z 分类器 / 预测 标签矩阵 Y(损失) 反向传播更新权重

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 输入
半监督学习 标签矩阵 + 掩码 只对部分节点标注,其余用于推理
动态图 / 流式图 邻接表 支持增量添加边和节点