4. SPOTlight 算法原理:非负最小二乘与主题建模

这一章我们来啃 SPOTlight 最核心的部分——它是怎么把空间位置信息和单细胞数据“拼”在一起的。坦白讲,我刚接触这个算法时也绕了好一阵,但搞清楚 NNLS 和主题建模后,后面的内容就顺畅多了。

4.1 核心思想:从混合信号到空间解卷积

空间转录组测出来的每个 spot,其实是一个“大杂烩”。它包含好几种细胞的 RNA 混合物。我们的目标很明确:从这堆混合信号里,反向推算出每种细胞各占多少。

这像什么?就像给你一杯混合果汁,你要尝出里面橙汁、苹果汁、西瓜汁各加了多少毫升。只不过 SPOTlight 尝的是转录组数据,用的是数学方法。

整个流程可以拆成两步:

  • 先学“配方”——从单细胞参考数据里学出每种细胞类型的表达特征
  • 再算“比例”——对每个 spot,用学到的特征去解卷积,算出细胞组成

我个人习惯把第一步叫“训练”,第二步叫“预测”。嗯,这里要注意,两个步骤用的是同一套数学框架——非负最小二乘。

4.2 非负最小二乘(NNLS):为什么它比普通回归更合适?

普通的最小二乘回归大家都很熟:给一堆自变量,求因变量,允许系数为正或为负。但问题来了——细胞比例怎么可能是负的?

关键约束:细胞比例必须是 [0, 1] 之间的非负数,而且总和必须为 1(或近似 1)。NNLS 正好满足这个需求,它强制所有系数 ≥ 0。

NNLS 的数学表达式其实很简洁:

目标:最小化 ||Ax - b||²
约束:x ≥ 0

其中:
A —— 参考矩阵(基因 × 细胞类型)
x —— 系数向量(待求解的细胞比例)
b —— 目标向量(某个 spot 的表达谱)

我第一次用 NNLS 做空间映射时,差点踩坑——忘了对参考矩阵做“标记基因筛选”。如果你直接把所有基因扔进去,运算慢不说,细胞类型之间的区分度也不够。后来我养成了一个习惯:先用 wilcoxon 或 t-test 选出 top 50-100 个标记基因,再构建参考矩阵 A。

我的经验:NNLS 对参考矩阵的“质量”很敏感。细胞类型之间的基因表达模式不要有太高相关性,否则系数估计会抖动。我一般会跑个相关性热图检查一下。

4.3 主题建模:让参考矩阵更“聪明”

如果你直接拿纯的细胞类型表达谱去做 NNLS,会有一个问题:同一个细胞类型在不同样本、不同状态下,表达谱其实在漂移。主题建模就是来解决这个问题的。

说白了,主题建模是一种降维+软聚类的方法。它把“同一个细胞类型”的表达谱,分解成几个“主题”(topics)。每个主题是一组共表达的基因模块。这样,参考矩阵就不是简单的细胞类型×基因,而是主题×基因。

为什么这样做更鲁棒?

  • 主题能捕捉细胞状态的连续性(比如从 resting 到 activated 的过渡)
  • 主题之间共享基因,但不完全重叠,提高了表达的冗余度
  • 空间 spot 里的混合信号,可以用几个主题的线性组合来拟合,比用纯细胞类型更精准

我记得有人问过我:“这不就是换个方式做非负矩阵分解(NMF)吗?” 对,主题建模的数学本质和 NMF 是一样的。只是它在单细胞领域被重新包装了一下,强调“主题”代表基因的共表达模块。

4.4 SPOTlight 的完整工作流

把 NNLS 和主题建模串起来,就是 SPOTlight 的完整流程。我画个图方便理解:

SPOTlight 核心工作流:NNLS + 主题建模 单细胞参考数据 细胞类型 + 表达矩阵 主题建模(NMF) 主题 × 基因矩阵 参考矩阵 A 主题 × 基因 空间转录组数据 每个 spot 的表达谱 非负最小二乘(NNLS)求解 对每个 spot:最小化 ||A · x - b||²,约束 x ≥ 0 输出:每 spot 细胞类型比例 NMF权重 → 映射回原始细胞类型 输入:标记基因 + 表达谱 输出:NMF 主题 输入:主题模型 + 空间数据 约束:非负

流程图展示得很清楚:从单细胞数据出发,经过主题建模生成参考矩阵 A,然后结合空间数据的每个 spot 表达谱 b,用 NNLS 求解细胞比例 x。最后再把主题的权重映射回原始的细胞类型名称。

4.5 关键细节:主题数量怎么选?

这里有个现实问题:每个细胞类型应该分多少个主题?

SPOTlight 的默认做法是为每个细胞类型设置 k 个主题(k 一般在 3-10 之间)。但我在项目中发现,这个参数对结果挺敏感。

避坑指南:我曾经在一个脑组织的映射任务中,k 设为 5 时神经元比例估计偏高,换成 3 就正常了。后来我总结了一个经验——先用 silhouette 分数评估主题分离度,再选 k。如果 silhouette > 0.5,说明主题是合理的。

实际代码里是这样调的:

# R 伪代码,SPOTlight 中的主题建模参数
library(SPOTlight)

# 为每个细胞类型设置主题数
# 神经元:5个主题,少突胶质细胞:3个主题
ct_topic <- c("Neuron" = 5, "Oligo" = 3, "Astro" = 4)

# 构建参考矩阵
spotlight_ls <- spotlight(
  reference = sce_reference,      # 单细胞参考
  clusters = "cell_type",         # 标签列名
  group = ct_topic,              # 每个类型的主题数
  verbose = TRUE
)

这一行就完成了主题建模+参考矩阵构建。输出结果是一个 spotlight 对象,里面包含了主题×基因的参考矩阵 A、以及每个细胞类型映射到主题的权重。

4.6 NNLS 在 SPOTlight 中的实现细节

NNLS 的求解器在 R 里有现成的包 nnls 和 quadprog。SPOTlight 用的是 R 自带 nnls 包的实现,做了两件事:

步骤 操作 目的
1 对每个 spot,取参考矩阵 A 中与 spot 共有的基因 对齐特征空间
2 调用 nnls(A, b),求解非负系数 x 得到主题权重
3 将主题权重按细胞类型汇总 映射回细胞比例
4 归一化,使比例之和为 1 获得概率解释

这里有个微妙之处:NNLS 是不保证系数之和为 1 的,它只保证非负。所以 SPOTlight 在最后一步做了 softmax 或简单的除以总和,把系数转为比例。

重要检查点:如果你发现某个 spot 的细胞比例总和远小于 1(比如只有 0.3),说明参考矩阵 A 没有很好地解释这个 spot 的表达谱。这时候要考虑:是不是少了某个细胞类型?还是这个 spot 处于组织的边缘、RNA 捕获效率低?

我在肠道数据分析时就遇到过这种情况——隐含了“未知细胞类型”。后来我在参考数据里多加了成纤维细胞,拟合度才上去。所以,参考数据的覆盖度直接影响映射质量。

4.7 主题建模 vs. 纯 NNLS:哪个更靠谱?

你可能想问:为什么不直接用纯细胞类型表达谱做 NNLS?偏要加一层主题建模?

我拿自己的项目经验说:

  • 纯 NNLS:在细胞类型区分度高的时候(比如神经元 vs. 胶质细胞),效果还行。但如果细胞类型间有连续状态(比如 T 细胞的不同激活阶段),它容易把同一种细胞的不同状态割裂开。
  • 主题建模 + NNLS:主题建模先做了“软聚类”,把相似的细胞状态 merge 成主题。NNLS 求解的是主题比例,而不是直接解细胞类型。最后一步才把主题映射回细胞类型。这个“缓冲层”让结果更平滑。

说白了,主题建模就像给细胞类型加了一层“容错垫”。它在状态连续的情况下更有优势。

4.8 运行时的注意事项

最后,我整理几个实战中遇到的坑:

  • 基因数量不能太少:我试过只用 top 10 标记基因,NNLS 求解不稳定,各个 spot 的系数震荡很大。建议至少 30-50 个基因。
  • 避免高度相关的细胞类型:比如 B 细胞和 plasma 细胞,基因表达谱高度相似。NNLS 会在这两个类型之间分配权重不稳定。我一般会先合并,映射完再拆分。
  • 内存问题:如果参考数据有 50 万细胞、3 万基因,跑主题建模会非常吃内存。我通常先做 downsampling,每个细胞类型取 200-500 个细胞,够用就行。
注意:NNLS 不适用于零膨胀数据。如果你的空间转录组数据有很多 dropout(某些基因在很多 spot 完全检测不到),建议先做 imputation 或直接在原始 count 上跑,不要用 log-normalized 数据。我踩过这个坑——归一化后的零值会误导 NNLS 把系数推成 0。

嗯,关于 NNLS 和主题建模的原理,就讲到这里。你把这一章消化掉,后面的“自定义参考矩阵”和“结果评估”就很好理解。说白了,SPOTlight 的核心就是一个带约束的优化问题,而主题建模是为它提供高质量参考矩阵的前处理步骤。