3. SpaGCN算法原理与图卷积网络基础
说实话,第一次看SpaGCN论文时,我也被“图卷积”“自监督”“空间邻接”这些词搞得有点晕。但当我真正在自己项目里跑通一次后,才发现它背后的逻辑其实很清晰。说白了,SpaGCN就是把空间转录组数据看作一张“图”,然后用图神经网络去捕捉细胞之间的位置关系和基因表达相似性。
这一节我会带你拆解三个核心问题:为什么用图?图卷积怎么工作?SpaGCN具体改了哪些地方?
3.1 从空间数据到图结构
空间转录组数据长什么样?每个spot有坐标(x,y),每个spot有基因表达向量。但我个人习惯的第一步,不是直接跑模型,而是先问自己:这些点之间是什么关系?传统的非空间方法(比如Louvain聚类)把每个spot当作独立样本,完全不管它们挨不挨着。
SpaGCN的做法很直接:把每个spot看作图中的一个节点,两个spot如果物理距离足够近,就连一条边。这样一来,空间信息就被编码成了图的拓扑结构。
radius参数控制距离阈值。太小会丢失邻居,太大会引入噪声。我一般先画个距离分布直方图,选一个能覆盖90%最近邻居的阈值。
这张图的节点数量就是spot数(通常1000~5000),边数取决于空间分辨率。举个例子,10X Visium芯片上,相邻spot中心距离约100μm,我通常设radius=200μm,每个spot能连到6~12个邻居。
# 伪代码:从坐标构建邻接表
from scipy.spatial import KDTree
tree = KDTree(spatial_coords) # shape: (n_spots, 2)
adjacency = tree.query_ball_tree(tree, r=radius)
edges = []
for i, neighbors in enumerate(adjacency):
for j in neighbors:
if i < j:
edges.append((i, j))
3.2 图卷积网络的核心思想
图卷积网络(GCN)不是SpaGCN独创的,但它是SpaGCN的主干。要理解SpaGCN,先得弄明白GCN到底在干什么。
传统卷积在图像上用一个固定大小的滑动窗口(比如3×3)聚合邻域像素。但在图上面,每个节点的邻居数量不固定,没法用固定尺寸的卷积核。GCN的做法是:每个节点从自己的邻居那里“收集”特征,然后跟自己的特征融合,再经过一个神经网络层。
我记得第一次手动实现GCN时,写过这样的代码:
import torch
import torch.nn.functional as F
class SimpleGCNLayer(torch.nn.Module):
def __init__(self, in_dim, out_dim):
super().__init__()
self.W = torch.nn.Linear(in_dim, out_dim)
def forward(self, x, adj):
# x: (N, in_dim), adj: (N, N)
support = self.W(x) # (N, out_dim)
output = torch.mm(adj, support) # 邻居聚合
return F.relu(output)
注意这个adj矩阵——它包含了归一化的邻接关系。我早期踩过一个坑:直接用原始邻接矩阵会导致节点度数高的spot特征被过度平滑。SpaGCN用了对称归一化,公式是 :
adj_norm = D^(-1/2) * adj * D^(-1/2)
其中D是度矩阵。这一下就把不同规模的邻居影响拉到了一个量级上。
3.3 SpaGCN的独特设计
基础GCN只能聚合同一层空间邻域,但SpaGCN做了两件关键的事:位置编码与像素融合。
第一,位置编码。 它没有直接用简单的欧氏距离作为边权重。而是先计算spot之间的物理距离矩阵,再用一个可学习的核函数(Gaussian RBF)把距离转换成权重:
weight = exp(-distance^2 / sigma^2)
sigma是自动学习的参数。这样一来,距离近的spot权重更大,距离远的权重迅速衰减。我项目里用的sigma初值一般设为坐标标准差的1/2,模型会自动调整。
第二,像素融合。 传统GCN只用spot本身的基因表达向量,但SpaGCN还额外把两个spot的空间坐标拼接起来作为边特征,和基因表达一起输入到GCN中。为什么这么做?因为光靠表达量,两个远距离但基因相似的spot可能被误判为紧密关联;加上坐标信息后,网络就知道它们的实际距离很远,从而降低边的重要性。
这种“边特征+MHA”(多头注意力)的设计,让SpaGCN能同时利用表达相似性和位置临近性。
3.4 图构建与聚类全流程
下面这张SVG图展示了SpaGCN的完整处理流程。你会看到从原始坐标+表达矩阵,到图构建,再到GCN编码,最后用自监督聚类输出空间域,每一步都有明确的输入输出。
SpaGCN的损失函数包含两部分:一部分是自监督的KL散度(让软分配接近目标分布),另一部分是图正则项(鼓励相邻spot有相似标签)。我调试时发现,这个正则项系数lamb很敏感,太大则所有spot都粘成一个域,太小则退化为普通K-means。
| 组件 | 作用 | 我常用的经验值 |
|---|---|---|
| 空间图半径 | 决定每个spot的邻居数量 | 200~300 μm(10X Visium) |
| GCN层数 | 消息传递深度 | 2层(过深会过度平滑) |
| 嵌入维度 | 压缩后的特征维数 | 32~64 |
| sigma初值 | RBF核宽度 | 坐标标准差的1/2 |
| lamb正则系数 | 空间一致性权重 | 0.1~1.0 |
核心记忆点:SpaGCN = 空间图构建 + GCN编码 + 自监督聚类。图卷积让每个spot“看到”邻居的信息,自监督让聚类结果不依赖外部标注。这两者结合起来,就能在空间转录组数据上自动发现组织域。
如果你对GCN的数学推导感兴趣,我建议先理解邻居聚合 + 特征变换这个基本单元。实际编码时,绝大多数坑都出在图构建这一步——比如坐标单位不统一(有的用像素,有的用微米),或者邻接矩阵没有归一化。我有个项目就因为忘了归一化,导致大cluster一直收敛不了,调了好久才发现。
networkx把图可视化一下。看看邻居数分布是否均匀,有没有孤立点。如果出现度数极低的spot,要么是边界点,要么是数据质量问题。我一般会设置一个最小邻居数(比如3),低于这个值的spot直接移除或手动补边。