2. scVI 的数学原理:变分自编码器基础

这一章,我们来聊聊 scVI 背后的数学引擎——变分自编码器(VAE)。

你可能听说过 VAE 是生成模型的一种,专门用来“创造”新数据。但在单细胞数据里,它的角色其实更像个“翻译官”——把混乱的高维表达数据,解码成一条条清晰的生物信号。

别被“变分”两个字唬住。说白了,它就是一套用神经网络做贝叶斯推断的框架。我在项目中第一次推导 VAE 损失时,也觉得这个公式很绕。但理解它,是掌握 scVI 抗批次干扰能力的关键。

核心一句话:VAE 教会模型在潜空间中,把每个细胞的“真实身份”和“批次噪声”分离开。

2.1 从自编码器到变分自编码器

传统的自编码器长什么样?输入 -> 编码器 -> 压缩成低维向量 -> 解码器 -> 重构出原始数据。这个过程很直观,就像把一个 10000 维的基因表达向量压扁成 10 维,再把它撑回 10000 维。

但普通自编码器有个致命问题:它得到的潜变量是确定性的点。什么意思?每个细胞只能映射到一个具体的 10 维点上。一旦你换了一个有点差异的输入,那个点就会飘走。这种“过拟合”式的压缩,对批次效应特别敏感——不同批次的同类型细胞,会跑到潜空间中不同的角落里。

VAE 的改进很聪明:它不输出单一的潜变量点,而是输出一个概率分布。具体来说,编码器输出的是潜变量的均值和方差。这样,每个细胞在潜空间中不再是一个点,而是一团“云”。

我早期做跨批次整合时,用过普通自编码器做降维对齐,效果非常差。换用 VAE 后,不同批次的同类型细胞终于能聚到同一团“云”里了。这里的概率化,就是抗干扰的起点。

2.2 VAE 的三大组成部分

咱们把 VAE 拆开来看。三部分,不多不少。

组件 角色 在 scVI 中的体现
编码器(Encoder) 将输入 x 映射为潜变量 z 的后验分布 q(z|x) 输入基因表达量,输出潜变量均值和方差
解码器(Decoder) 从潜变量 z 重构出数据分布 p(x|z) 生成零膨胀负二项分布(ZINB)的参数
潜变量空间(Latent Space) 低维隐变量 z 的分布区域,通常假设为标准正态 10-20 维,用于细胞聚类和批次校正

为什么编码器要输出方差?因为我们要对那个“不确定性”建模。想一想,一个测序深度很低、基因捕获不全的细胞,它的表达模式本来就模糊。VAE 的方差能捕捉这种模糊性——不确定性越大的细胞,它在潜空间里的“云”就越散。这在后续整合时,能有效避免将低质量细胞强行拉到错误的位置。

经验之谈:我习惯把编码器输出的方差视为“置信度”。方差大的细胞,在后续分析中应被适当降权。scVI 的框架天然支持这一点。

2.3 重参数化技巧(Reparameterization Trick)

嗯,这里有个经典问题:VAE 编码器输出的是分布参数(均值 μ 和标准差 σ),然后我们要从分布 N(μ, σ²) 中采样得到具体的潜变量 z。但采样操作是不可导的,梯度传不回去,神经网络就没法训练了。

重参数化技巧是怎么破的?它把采样过程分成两步:

  1. 从标准正态分布 N(0, 1) 中采样一个随机噪声 ε
  2. 通过线性变换得到 z = μ + σ ⊙ ε

这样一来,μ 和 σ 的梯度就能直接传递到编码器,整个模型便可端到端训练。

我在调试自己的单细胞模型时,重参数化这一步经常写错。最常见的是标准差预热(annealing),即在训练初期把 σ 标准差的初始值设大一些,让模型先探索潜空间,再逐渐收紧。这个技巧在 scVI 的官方实现中也是可选的。

2.4 VAE 的损失函数

VAE 的损失由两部分构成,这个公式我建议你记在笔记本上:

L = 重构损失 + KL散度

重构损失 = -E_{z~q(z|x)}[log p(x|z)]
          = 衡量解码器重构数据的质量

KL散度 = D_KL(q(z|x) || p(z))
        = 衡量后验分布与先验分布(标准正态)的差异

这个损失有两个目标,其实是相互制衡的:

  • 重构损失:让潜变量尽可能保留原始表达信息,避免信息丢失
  • KL 散度:让潜变量分布不要偏离标准正态太远,起到正则化作用

你想啊,如果没有 KL 项,模型就会退化成普通自编码器,潜空间毫无结构可言。如果 KL 项权重过大,重构又会变得很差,生成的数据都是“大路货”。这个平衡,在 scVI 里是通过一个超参数 β(kl_weight)来调节的。

避坑指南:我曾经在一个高噪声的批次整合任务中,把 kl_weight 设得过大,结果不同批次的细胞被强行拉到一个位置,混在一起分不开。后来我把 kl_weight 降低到 0.1,模型才开始正常工作。这个参数是整个 scVI 训练的“甜点”。

2.5 从 VAE 到 scVI:批次感知编码

scVI 在标准 VAE 基础上加了一个关键设计:把批次标签(batch label)作为条件信息输入到模型中。

具体做法是:

  1. 编码器输入:基因表达量 x + 批次标签 b → 输出潜变量分布参数
  2. 潜变量 z:只携带生物变异信息,批次变异被剥离
  3. 解码器输入:潜变量 z + 批次标签 b → 输出基因表达分布

为什么这样做能抗批次干扰?因为解码器在重构时,知道每个细胞的批次标签。它会“学会”把由批次引起的表达差异,归因于标签变量,而不是潜变量。这样,潜变量 z 就只包含纯粹的生物学信号。

举个例子:两个不同实验室测的 T 细胞,表达谱差异可能很大。scVI 会让潜变量 z 只学习“T 细胞”的共性模式,而让解码器根据批次标签 b 去调整不同实验室特有的技术噪声。

2.6 VAE 在 scVI 中的整体数据流

下面这张图总结了 scVI 从输入到输出的完整流程:

scVI 变分自编码器数据流 输入层 基因表达量 x + 批次标签 b 编码器 q(z|x, b) 输出 μ 和 σ 潜变量 z ~ N(μ, σ²) 生物信号,无批次 解码器 p(x|z, b) 输出 ZINB 参数 输出层 重构基因表达 损失 = 重构 + KL 端到端反向传播 关键 批次标签 b 只参与编码和解码 不进入潜变量 z

整张图展示了 scVI 训练时的数据流向。注意批次标签 b 既输入到编码器,也输入到解码器——这就是它“记住”批次差异,从而解放潜变量 z 的关键。

2.7 小结:VAE 之于 scVI 的意义

咱们快速回顾一下:

  • VAE 输出概率分布,而不是确定性点,这对于建模单细胞数据的不确定性非常关键
  • 重参数化技巧让模型可训练,这一步的实现细节直接影响模型收敛
  • KL 散度和重构损失的平衡,是调参的核心
  • scVI 将批次标签作为条件变量加入 VAE,这是它抗批次干扰的底层机制

我个人觉得,理解 VAE 的这些数学细节,对于后续调整 scVI 的架构、甚至自己写一个定制化模型,都有很大帮助。至少你不会对着那几个损失项发呆,知道“哦,我调 kl_weight 其实是在调潜空间的紧致度”。

想动手试试?可以在 PyTorch 里搭一个最小化的 VAE,用 MNIST 数据跑一跑。替换成 scRNA-seq 数据后,你就理解 scVI 为什么要把损失函数从高斯分布换成 ZINB 分布了。这个点到第 4 章会详细展开。