图数据表示与邻接矩阵
做 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输入的核心流程。你品品这个转换关系:
什么时候需要重新建图?
你可能会问:建一次图够不够?我建议每次做批次校正或聚类后,都重新检查图的质量。因为 GNN 的性能高度依赖图的真实性。如果批次效应严重,原本的相似性边可能全是“假邻居”。这时候我会先做一次快速粗略校正,再基于校正后的表达建新图,第二轮训练效果能提升 10~15%。
核心总结:邻接矩阵就是 GNN 的骨架。骨架歪了,后面怎么学都救不回来。多花 10 分钟检查图结构,能省 2 小时调参。