图数据表示与邻接矩阵

做 GNN 批次质控,第一步就是把你的组学数据变成图。我刚开始接触这个时也觉得挺绕——好好的表达矩阵,凭什么要拧成一张图?但你想想看,单细胞数据里样本和基因的关系,本质上就是节点和边的结构。批次效应本质上是图上的社区偏差,用 GNN 去学,天然合适。

这一节我带你理清两个核心问题:图上节点怎么定义、边怎么建、邻接矩阵怎么搞定。

节点与边:定义那些“生物对象”

在我处理的实际项目中,节点通常是两种东西:

  • 细胞(或细胞簇) —— 批次质控最常见的主角。
  • 基因 —— 当作特征维度,一般不作为交互节点。

边代表什么?边表示两个节点之间存在生物或统计上的“关系”。比如:

  • 基于表达谱的相似性(cosine、欧式距离)
  • 空间相邻(成像数据)
  • 共享一个批次标签(注意!不能直接用批次标签构建邻接矩阵,否则信息泄露)

我曾经踩过一个坑:直接用批次编号做 one-hot 节点特征,结果模型只学到了批次 ID,完全没学到校正。后来我把批次信息当作边构建时的“约束条件”而不是节点特征,效果才正常。

邻接矩阵:图的“身份证”

邻接矩阵 A 的规则很简单:如果节点 i 和 j 有边,A[i][j] = 1,否则为 0。批次质控场景下,我几乎只用无向图,所以矩阵是对称的。

但现实数据动辄几万个细胞,全连接 O(n²) 的矩阵存不下。所以我习惯用稀疏格式:scipy.sparse.csr_matrix 或 dgl.graph 的 COO 形式。

构建方法阈值大小边数适用场景
k-近邻 (k=10~50)每个节点连 k 个最近邻居约 n×k细胞量 1 万以上,推荐
距离阈值 (e.g. cosine>0.8)自行设定依赖阈值,可能稀疏不均数据噪声低,已知相似性范围
全连接无n²小数据集(<500 节点)测试用
共享批次标签不推荐结构泄露❌ 禁止在边构建中使用

个人经验:我一般先用 k=15 的 kNN 建图,然后检查孤立节点比例。如果超过 5% 的节点没连上任何边,就增大 k 或改用余弦距离 + 自适应阈值。

代码实战:从表达矩阵到邻接矩阵

直接给一段我常用的小工具函数。假设你有一个 n_cells × n_genes 的归一化表达矩阵 X。

import numpy as np
from sklearn.neighbors import kneighbors_graph
from scipy.sparse import csr_matrix

def build_adjacency(X, k=15, metric='cosine'):
    """
    X: ndarray, shape (n_cells, n_features)
    返回: sparse csr_matrix (n_cells×n_cells)
    """
    A = kneighbors_graph(X, n_neighbors=k, metric=metric,
                         mode='connectivity', include_self=False)
    # 对称化:无向图
    A = (A + A.T) / 2
    A.data = np.ones_like(A.data)  # 修约浮点对称误差
    return A.tocsr()

# 示例用法
# adj = build_adjacency(expr_norm_matrix, k=15)
# print(adj.shape, adj.nnz)

注意:sklearn 返回的矩阵默认是双向k近邻,我习惯再对称化一次确保无向。不怕多这一步,我曾经因为不对称导致 GNN 消息传递方向不一致,结果收敛速度慢了一半。

邻接矩阵的“健康检查”

建好图后我通常会跑几个检查:

  • 是否有孤立节点? 用 adj.sum(axis=1) == 0 查出来。
  • 是否对称? (adj != adj.T).nnz == 0
  • 稀疏程度如何? 一般建议边数在 10n ~ 50n 之间,太密会变成无意义全连接。

避坑:我在一个 5 万细胞的数据集上用了 k=50,结果邻接矩阵有 250 万条边。GNN 训练时显存直接爆掉。后来我改成 k=20,再配合边采样才跑动。记住:边数 = n×k(无向约一半),对照显存量,别贪心。

图数据结构总览

下面这张图总结了从原始表达矩阵到邻接矩阵,再到GNN输入的核心流程。你品品这个转换关系:

图数据表示:从表达矩阵到邻接矩阵 表达矩阵 细胞 × 基因 (n × d) kNN 图结构 节点=细胞,边=相似性 邻接 稀疏邻接矩阵 CSR / COO 形状: n×n 非零元: 边数×2 说明: 1. 表达矩阵每行是一个节点的原始特征。 2. 通过kNN或距离阈值得到节点间的边,生成图结构。 3. 邻接矩阵存储节点间的连接信息,作为GNN消息传递的“路线图”。 图数据表示核心流程:表达矩阵 → 图结构(节点+边) → 稀疏邻接矩阵

什么时候需要重新建图?

你可能会问:建一次图够不够?我建议每次做批次校正或聚类后,都重新检查图的质量。因为 GNN 的性能高度依赖图的真实性。如果批次效应严重,原本的相似性边可能全是“假邻居”。这时候我会先做一次快速粗略校正,再基于校正后的表达建新图,第二轮训练效果能提升 10~15%。

核心总结:邻接矩阵就是 GNN 的骨架。骨架歪了,后面怎么学都救不回来。多花 10 分钟检查图结构,能省 2 小时调参。