4. scVI的核心思想:变分自编码器

好,聊完了单细胞数据那些让人头疼的特性,我们该切入正题了——scVI到底是怎么工作的?

我刚开始接触scVI时也一头雾水。什么变分自编码器,听起来像物理名词。后来我把它拆开看:变分、自编码器。说白了,就是一个能自动学习数据内部规律,还能对抗噪声的神经网络。

我个人习惯把VAE看作是“数据压缩-还原游戏”。你有一幅高分辨率的画,你先把它压缩成一个简短描述,再根据描述把画还原出来。如果还原得很像原画,说明你的压缩描述抓住了精髓。scVI就是这么个思路。

4.1 自编码器基础回顾

先不急着讲scVI。我们从最原始的自编码器开始。你想想看,它其实就三部分:

  • 编码器:把高维输入(比如一个细胞的基因表达向量)映射到一个低维隐藏空间
  • 隐藏层:低维的“中间表示”,也就是我们的隐变量
  • 解码器:从隐藏空间再映射回原始高维空间

训练目标很简单:让输出尽可能接近输入。这样一来,隐藏层就不得不学习数据中最核心的结构。

我记得到2016年左右,普通自编码器开始被用到单细胞数据里去降维。效果还行,但有个致命问题——过度自信。它对每个输入只输出一个点,完全不考虑不确定性。单细胞数据噪声那么大,你硬说某个细胞的潜在位置就是那么精确,不出问题才怪。

核心缺陷:普通自编码器无法量化不确定性,在稀疏高噪声的单细胞数据上很容易过拟合。

4.2 变分自编码器的改进

变分自编码器(VAE)在自编码器基础上引入了一个关键变化:输出分布,而不是点。

具体怎么做的?编码器不再输出一个确定的向量,而是输出两个东西:均值和方差。然后把隐变量采样为:

z = μ + σ * ε, 其中 ε ~ N(0, I)

这个技巧叫重参数化。没有它,采样操作不可导,整个网络就训练不了。

再配合一个KL散度项,强制隐藏空间的分布靠近标准正态分布。这样做的好处是:隐空间变得光滑有拓扑结构,你在两个真实细胞之间插值,解码出来的基因表达也是有意义的。

我当年第一次跑通VAE时,试了试隐空间插值。从一个T细胞平滑过渡到一个B细胞,中间状态的基因表达居然真的变了——这件事让我对VAE建模单细胞数据的潜力深信不疑。

4.3 scVI对标准VAE的改造

scVI没有直接用标准VAE。它针对单细胞数据做了几个关键改造。我个人觉得这些改造才是它真正厉害的地方。

第一,零膨胀负二项输出分布。

标准VAE假设数据服从高斯分布,这在单细胞数据上行不通。UMI计数是离散的、过离散的,而且有大量零值。scVI使用零膨胀负二项分布(ZINB)来建模每个基因的表达量:

p(x_g | z, s) = ZINB(π_g, μ_g, θ_g)

其中:
π_g ——  dropout概率(技术零的概率)
μ_g ——  负二项分布的均值(真实表达水平)
θ_g ——  负二项分布的离散度

这样就能同时处理“基因没表达”和“基因表达了但没捕获到”两种情况。我在处理10X数据时发现,这个假设对零值的拟合效果比高斯假设好太多了。

第二,显式建模批次效应。

scVI把批次信息作为条件输入。编码器和解码器都加入批次标签作为条件变量。用公式表示就是:

编码器: q(z | x, batch)
解码器: p(x | z, batch)

训练时,模型能看到同一基因在不同批次中的不同分布。推理时,你可以把所有批次都统一到一个参考批次,从而实现批次校正。

组件 标准VAE scVI
输出分布 高斯分布 零膨胀负二项分布
批次处理 无显式支持 条件编码/解码
库大小归一化 需预处理 内置于解码器
隐变量 连续向量 连续向量 + 可扩展

第三,内建库大小校正。

不同细胞捕获到的UMI总数差异很大。普通方法会在预处理时做CPM、TPM之类的归一化。scVI则把每个细胞的总UMI数作为输入传给解码器,让模型自己去学这个规模因子和基因表达之间的关系。

这招比固定公式的归一化灵活多了。举个例子,在细胞周期相关基因上,总UMI数高不代表这些基因的相对比例高,模型能学到这个细微差别。

4.4 scVI的完整工作流

好了,我们把上面这些零件组合起来,看看scVI一次训练是怎么跑的:

  1. 输入:每个细胞的基因表达向量x,加上批次标签b,加上总UMI数s
  2. 编码:用神经网络把(x, b)映射到隐变量分布的参数(μ, σ)
  3. 采样:用重参数化从N(μ, σ²)采样出隐变量z
  4. 解码:用另一个神经网络把(z, b, s)映射到ZINB分布参数(π, μ, θ)
  5. 损失:最大化数据的对数似然 + 最小化KL散度

下面是scVI训练的流程图。我建议你把这个图跟上面的步骤对照着看,能理解得更透。

scVI 变分自编码器结构 输入 x 基因表达 + 批次 + 库大小 编码器网络 NN encoder μ, σ (隐参数) 采样 z = μ + σε 解码器网络 NN decoder 输出 ZINB π, μ, θ 损失 = 重构损失 + β·KL散度 最大化ELBO 重参数化让采样可导 z = μ + σε, ε ~ N(0,I)

训练完成后,我们就可以取出隐变量z用于下游分析。z是低维的、经过批次校正的、去噪后的细胞表示。

实用技巧:我第一次用scVI做整合时,发现训练后的隐变量比PCA的聚类效果明显更好。尤其是我把不同实验室的10X数据丢进去,原先batch clustering的问题消失了。

4.5 训练过程与调参经验

scVI是用PyTorch实现的,训练过程跟一般深度网络差别不大。核心超参数有这么几个:

参数 默认值 我的建议
n_latent 10 数据量大时可调大到20-30,保留更多信息
n_layers 2 3层够用,再多容易过拟合
dropout_rate 0.1 稀疏数据建议降到0.05
max_epochs 400 加early_stopping,100epoch无改善就停
batch_size 128 细胞量>5万时用256,稳定很多

我曾经踩过的坑:在早停机制里只监控了训练损失。训练损失一路下降,但验证集上的聚类指标反而越跑越差。后来我改成监控验证集的ELBO和KL项的平衡,才把问题解决。所以调参时一定要看验证集的指标,不能只看训练损失。

4.6 推理与下游使用

模型训练好了,具体怎么用?我总结了几种常见场景:

  • 降维可视化:直接取隐变量z,交给UMAP或者t-SNE画图。如果需要可视化批次校正效果,可以在推理时统一所有细胞的批次标签。
  • 数据去噪:用解码器重构出原始的基因表达。重构值不再有dropout噪声,用做差异表达分析背景更干净。
  • 缺失值填补:把原始零值输入模型,重构出一个非零值。不过要小心,不要过度解读填补值。
  • 新细胞类型检测:训练好模型后,把新数据输入,看它落在隐空间的哪个区域。如果落在现有簇之外,可能就是新的细胞亚群。

我个人在实际场景中经常遇到一个问题:用户问“scVI能直接输出聚类结果吗?”答案是不能。scVI在底层只是一个生成模型,它的输出是潜在表示,不是类别标签。聚类还需要你自己用Leiden或k-means去完成。但有了好的潜在表示,聚类准确率会高出不少。

4.7 小结:scVI的变分自编码器核心

把上面所有内容串起来,scVI的核心就四句话:

  • 用变分自编码器框架,学习隐变量的后验分布
  • 零膨胀负二项分布建模基因表达,贴合真实数据生成过程
  • 条件变分自编码器思想,内建批次校正功能
  • 库大小作为输入,自动处理技术差异,无须预处理归一化

这四点就是我们后续对比分析scVI和BBKNN的技术基础。你把VAE的原理吃透了,后面理解模型之间的差异就轻松多了。