跳过正文
  1. Posts/

VAE 为什么要让 Latent Variable 变成一个概率分布

·6201 字·13 分钟

起因
#

上一篇文章理清了 latent variable 和 latent space 的概念,最后停在一个地方:

不要直接在复杂的数据空间中生成数据。先在简单的 latent space 中采样一个 \(z\),再通过学到的映射把 \(z\) 变成 \(x\)。

这个思路很漂亮,但当时只讲了"这个方向可以做",没有讲具体怎么做。

接着看 6.S191 的内容,第一个出场的具体模型就是 VAE(Variational Autoencoder,变分自编码器)。我最初以为 VAE 就是"一个 Autoencoder 加上一些概率约束",看下去才发现它做了一个非常关键的改变——这个改变本身就能回答"为什么 Autoencoder 不能直接拿来生成"。

这篇就围绕这一个改变展开。

从 Autoencoder 开始
#

VAE 名字里有 Autoencoder,所以先回顾一下普通的 Autoencoder(自编码器)。

结构非常直接:

$$ x \xrightarrow{\text{Encoder}} z \xrightarrow{\text{Decoder}} x' $$

Encoder 把输入 \(x\) 压缩成一个 latent representation \(z\),Decoder 再把 \(z\) 展开成重建结果 \(x'\)。训练目标是让 \(x'\) 尽量接近原始的 \(x\)。

Autoencoder 结构

这里有一个对后面讨论很重要的性质:普通 Autoencoder 里,\(z\) 是一个确定性的映射结果。

$$ z = f_\phi(x) $$

同一个 \(x\) 送进 Encoder,永远得到同一个 \(z\)。这和查表一样——输入确定,输出就确定。

拿 Autoencoder 做生成,卡在哪里
#

Autoencoder 本来就不是为生成设计的。它的训练目标是 reconstruction——给一张图,压缩再重建,尽量不丢信息。

但既然我们已经有了 Decoder,一个自然的想法是:

如果我随便在 latent space 里选一个 \(z\),丢给 Decoder,能不能生成一个合理的新样本?

这个想法听起来很自然,但做起来会碰壁。

原因是:普通 Autoencoder 的训练目标并没有对 latent space 的全局结构做任何要求。Encoder 只关心一件事——怎么编码才能让 Decoder 重建得更好。它没有义务把 latent space 整理成一个"到处都能生成合理输出"的空间。

具体来说:

  • 训练数据经过 Encoder 后,只会在 latent space 中落到某些特定区域。
  • 这些区域的分布可能非常不规则——某些地方密集,某些地方空旷,相邻区域之间不一定平滑过渡。
  • 随便选一个 \(z\),很可能落在 Decoder 训练时几乎没有见过的区域。

这里要注意措辞:我不是说"Autoencoder 的 latent space 一定不连续"。而是说,它的训练目标并没有明确要求 latent space 满足某个方便采样的全局概率结构,因此没有理由保证从某个简单分布随机采样出的 \(z\) 会落到 Decoder 熟悉的区域。

于是问题变成:怎么让 latent space 变成一个"我们知道该怎么采样"的空间?

VAE 做了什么改变
#

VAE 的核心改变可以用一句话概括:

把 Encoder 的输出从"一个确定的点"变成了"一个概率分布"。

普通 Autoencoder:

$$ x \to z $$

VAE:

$$ x \to q(z|x) $$

对于一个输入 \(x\),Encoder 不再简单地回答"它对应 latent space 中的 \(z = 1.37\)"。它回答的是:“对于这个 \(x\),我认为 \(z\) 大概分布在这个范围里。”

更具体地说,经典 VAE 中这个分布被定义为一个高斯分布:

$$ q_\phi(z|x) = \mathcal{N}(\mu(x),\, \operatorname{diag}(\sigma^2(x))) $$

Encoder 实际输出的是两组参数:均值 \(\mu(x)\) 和方差 \(\sigma^2(x)\),它们共同描述一个高斯分布。

我后来觉得有一个视觉上很好用的理解方式:

  • 普通 Autoencoder:每个输入对应 latent space 中的一个点。
  • VAE:每个输入对应一团概率云。
Point vs Distribution

这个从"点"到"云"的变化,就是理解 VAE 最重要的一步。后面所有的设计——prior、KL divergence、采样——都是围绕这个变化展开的。

高斯分布速查
#

在继续之前,先做一个最小的概率复习。后面会反复出现高斯分布,如果这部分已经熟悉可以跳过。

\(X \sim \mathcal{N}(\mu, \sigma^2)\) 表示随机变量 \(X\) 服从高斯分布(Gaussian distribution,也叫正态分布)。

其中:

  • \(\mu\)(mu)是 mean(均值),决定分布的中心位置——钟形曲线的峰在哪里。
  • \(\sigma\)(sigma)是 standard deviation(标准差),控制分布有多宽——\(\sigma\) 越大,曲线越矮胖。
  • \(\sigma^2\) 是 variance(方差)。

一个容易混淆的地方:\(\mathcal{N}(\mu, \sigma^2)\) 的第二个参数写的是方差,不是标准差。比如 \(\mathcal{N}(0, 4)\) 意味着 \(\sigma^2 = 4\),因此 \(\sigma = 2\)。

高斯曲线表示的是概率密度(probability density),不是某个单点的概率——之前写过一篇专门讨论这件事。这里只需要知道:曲线越高的位置,从附近采样到的可能性越大。

高斯分布参数的直观理解

左图:三条曲线均值不同(\(\mu = -2, 0, 2\)),方差相同——\(\mu\) 控制"分布在哪里"。

右图:三条曲线均值相同,标准差不同(\(\sigma = 0.5, 1, 2\))——\(\sigma\) 控制"分布有多宽"。

Encoder 为什么输出"恰好"是高斯
#

这是我看 VAE 时产生的第一个真实困惑。

看到"Encoder 输出一个高斯分布"这个说法,我的第一反应是:

训练居然能让 Encoder 的输出恰好是高斯分布?模型怎么控制输出一定是高斯?

后来发现这个理解的方向完全反了。

不是 Encoder 自己训练着训练着发现了一个高斯分布。而是模型设计者事先决定:\(q(z|x)\) 使用高斯这个分布族。

Encoder 真正输出的不是一个分布,而是分布的参数。具体来说,Encoder 网络的最后一层会输出两组向量:

  • \(\mu(x)\):均值向量
  • \(\log \sigma^2(x)\):对数方差向量(实际实现中用 log variance 而不是直接输出 \(\sigma\),因为 log 可以取任意实数值,数值上更稳定)

然后我们人为定义:

$$ q_\phi(z|x) = \mathcal{N}(\mu(x),\, \operatorname{diag}(\sigma^2(x))) $$

所以逻辑是这样的:

  • 高斯这个分布族(distribution family)——是设计者选择的。
  • \(\mu(x)\) 和 \(\sigma(x)\) 的具体数值——是训练学出来的。

分布族是选择的,分布参数才是学习的。

用分类模型做个类比
#

这个逻辑其实和其他神经网络设计完全一样。

比如一个 1000 类的图像分类模型,最后一层输出 1000 个 logit:

$$ \text{hidden state} \to \text{Linear}(d, 1000) \to \text{1000 logits} $$

它为什么"恰好"输出 1000 个数?不是训练过程中"发现世界上有 1000 类"。而是模型结构在一开始就硬写了 output_dim = 1000。训练负责的是:这 1000 个 logit 应该各自取什么值。

VAE 的 Encoder 一样。网络结构规定了输出层有 \(2d\) 个神经元(\(d\) 个给 \(\mu\),\(d\) 个给 \(\log \sigma^2\)),然后由设计者定义"这些输出参数化一个对角高斯"。训练负责学习这些参数应该是什么。

为什么偏偏选 Gaussian
#

既然分布族是人选的,为什么选高斯而不是别的?

我本来以为答案是"因为自然界很多东西服从正态分布"。但这不是 VAE 这里最核心的原因。真正的原因更偏向工程和数学上的便利。

参数简单。 一维高斯只需要 \(\mu\) 和 \(\sigma\) 两个参数。对于 \(d\) 维的 latent space,对角高斯也只需要 \(2d\) 个参数(\(d\) 个均值 + \(d\) 个方差),而不是 \(d^2\) 量级。Encoder 的输出维度是可控的。

容易采样。 想从 \(\mathcal{N}(\mu, \sigma^2)\) 中采一个样本,只需要先从标准正态 \(\mathcal{N}(0, 1)\) 采一个 \(\epsilon\),然后做线性变换:

$$ z = \mu + \sigma \cdot \epsilon, \qquad \epsilon \sim \mathcal{N}(0, I) $$

这个写法同时解决了 VAE 训练中"随机采样如何参与反向传播"的问题——因为随机性被隔离到了 \(\epsilon\) 上,而 \(\mu\) 和 \(\sigma\) 仍然是网络的确定性输出,梯度可以正常流过。这被称为 reparameterization trick,本文不展开。

与标准高斯 prior 的 KL divergence 有闭式解。 两个高斯之间的 KL 散度可以用解析公式直接算出来,不需要数值近似。这让训练计算变得高效。

Prior:从哪里采样 z
#

现在回到生成的问题。如果训练完成后,我们想生成新样本——不给具体的 \(x\),直接从 latent space 中采一个 \(z\)——我们需要知道"从哪里采"。

这就是 prior(先验分布)的角色。

Prior \(p(z)\) 回答的问题是:在没有看到任何具体输入 \(x\) 之前,latent variable \(z\) 应该大致遵循什么分布?

经典 VAE 的答案很简单:

$$ p(z) = \mathcal{N}(0, I) $$

也就是一个标准多维高斯。

因为 latent \(z\) 通常是一个 \(d\) 维向量 \(z = (z_1, z_2, \ldots, z_d)\),所以这里的 \(\mathcal{N}(0, I)\) 是多维的。\(I\) 是单位矩阵(identity matrix),直观理解就是:每个 latent 维度独立、均值都是 0、方差都是 1。

这个 prior 提供了一个简单、明确、可以采样的全局规则。生成时只需要从这个分布里抽一个 \(z\),交给 Decoder 就行。

q(z|x) 与 p(z) 的冲突
#

到这里有一个矛盾出现了。

一方面,对于每个训练输入 \(x\),Encoder 输出一个 conditional distribution \(q(z|x)\)。

另一方面,我们事先规定了 prior \(p(z) = \mathcal{N}(0, I)\),希望将来生成时能从这个分布采样。

问题是:如果 Encoder 想输出什么分布就输出什么分布,那 prior 还有什么意义?

比如 Encoder 完全可以学出:

$$ q(z|x_1) = \mathcal{N}(100,\, 0.01^2) $$$$ q(z|x_2) = \mathcal{N}(-500,\, 20^2) $$

一个 \(z\) 集中在 100 附近,另一个跑到 -500 去了。训练时 Decoder 确实能适应这些位置。但将来生成时,我们从 \(\mathcal{N}(0, 1)\) 采样,采到的 \(z\) 根本不在 100 或 -500 附近——训练区域和采样区域完全对不上。

所以需要一种机制来衡量:每个 \(q(z|x)\) 到底和 prior \(p(z)\) 偏离了多少。

这就引出了 KL divergence。

KL Divergence 在比较什么
#

KL divergence(Kullback–Leibler divergence,KL 散度)是一种衡量两个概率分布有多不一致的方法。

公式是:

$$ D_{\text{KL}}(q \| p) = \int q(z) \log \frac{q(z)}{p(z)} \, dz $$

不需要通过积分来理解它。在 VAE 的语境里,它回答一个非常具体的问题:

对于这个输入 \(x\),Encoder 给出的 \(q(z|x)\) 与我们希望的 prior \(p(z)\) 之间,差距有多大?

  • 如果 \(q(z|x)\) 和 \(p(z)\) 完全一样,KL = 0。
  • \(q(z|x)\) 偏离 \(p(z)\) 越远,KL 越大。

下面的图直观展示了三种情况:

KL Divergence 直观理解

左图:两个分布完全重合,KL = 0。中图:稍有偏移,KL 较小。右图:严重偏离,KL 很大。

有一点需要额外说明:虽然直觉上可以把 KL 理解成衡量两个分布"距离多远",但数学上它不是 distance metric,因为一般来说 \(D_{\text{KL}}(q \| p) \neq D_{\text{KL}}(p \| q)\)——它不对称。这里不需要深入信息论,只需要知道 KL 是一种有方向的 divergence。

Reconstruction 与 KL 的拉扯
#

这是我后来觉得理解 VAE 最重要的一个直觉。

VAE 的训练可以概念性地理解成同时满足两个目标:

$$ \text{Loss} \approx \text{Reconstruction} + \text{KL} $$

这里我只是在建立训练目标的直观理解,不是给严格的 ELBO 定义。

Reconstruction 想做什么? 它希望从 \(z\) 能恢复出原始输入。这意味着不同输入的 \(q(z|x_1)\)、\(q(z|x_2)\)、\(q(z|x_3)\) 需要保持区别——如果所有输入都被编码成完全一样的分布,Decoder 就无法区分它们,重建质量会崩溃。

KL 想做什么? 它希望每个 \(q(z|x)\) 不要严重偏离 prior \(p(z) = \mathcal{N}(0, I)\)。如果某个输入的 \(q(z|x)\) 跑到了 \(\mathcal{N}(100, 0.01^2)\),KL 会很大,loss 会被惩罚。

这两个目标存在天然的张力。Reconstruction 想让每个输入的概率云尽量有特色、彼此可区分。KL 想把所有概率云往标准高斯覆盖的区域拉拢。

Reconstruction 与 KL 的拉扯

VAE 最终学到的 latent representation,是信息表达能力和全局规则性之间的折中。

两个容易产生的误解
#

理解了 Reconstruction 与 KL 的拉扯之后,有两个容易出现的错误理解需要纠正。

第一个:KL 并不是要求所有 \(q(z|x)\) 都变成 \(\mathcal{N}(0, I)\)。如果所有输入都对应完全相同的标准高斯,那 latent \(z\) 就不携带任何关于输入的信息了,Decoder 没法重建任何东西。实际上不同输入的 \(q(z|x)\) 是不同的——比如一个输入可能对应 \(\mathcal{N}(-0.7, 0.8^2)\),另一个对应 \(\mathcal{N}(0.9, 0.6^2)\)——它们有区别,但都没有离 prior 特别远。

第二个:每个 \(q(z|x)\) 是高斯,不等于整个 latent space 上的分布就是一个高斯。 每个 conditional distribution \(q(z|x_i)\) 确实是高斯(因为我们这么定义的)。但如果把所有训练数据的 \(x\) 混在一起,得到的 aggregated distribution \(q(z) = \mathbb{E}_{x \sim p_{\text{data}}}[q(z|x)]\) 是很多高斯的混合,可能具有复杂的多峰形状。KL regularization 做的是推动每个 conditional distribution 不要严重偏离 prior,但不是说整体一定就是一个漂亮的标准高斯。

为什么 VAE 因此可以生成
#

现在终于可以回到最初的问题:为什么 VAE 能做生成。

训练完成后,我们不需要一个现成的输入 \(x\)。直接从 prior 采样:

$$ z \sim p(z) = \mathcal{N}(0, I) $$

然后把 \(z\) 交给 Decoder:

$$ z \xrightarrow{\text{Decoder}} \text{generated } x $$

这就是 VAE 的生成过程。

这里最关键的一点:普通 Autoencoder 也有 Decoder,但它不能这样用,因为它的 latent space 没有被整理成一个我们知道如何采样的概率空间。 VAE 通过 KL regularization,让训练数据在 latent space 中的分布不至于离 prior 太远——这样从 prior 采样的 \(z\),有更大的概率落在 Decoder 见过、能产出合理输出的区域。

从 AE 到 VAE:真正改变了什么
#

把前面的内容拉到一起看。

AE 与 VAE 的完整对比

左边是普通 Autoencoder:输入经过 Encoder 变成一个确定性的 latent point,再由 Decoder 重建。训练目标是 reconstruction。Latent space 没有全局概率约束,不保证随机采样能得到合理输出。

右边是 VAE:Encoder 输出的不是一个点,而是一个分布 \(q(z|x)\) 的参数 \(\mu(x)\) 和 \(\sigma(x)\)。从这个分布中采样一个 \(z\),再交给 Decoder 重建。KL divergence 约束 \(q(z|x)\) 不要严重偏离 prior \(p(z) = \mathcal{N}(0, I)\)。

而生成路径完全绕过了 Encoder:直接从 \(p(z) = \mathcal{N}(0, I)\) 采样 \(z\),交给 Decoder 生成新样本。

我一开始理解错在哪里
#

写到这里,回头看我最初的理解演化。

第一阶段,我把 VAE 理解成:

普通 Autoencoder 学一个确定性的 \(z\),VAE 学一个"符合高斯分布的 \(z\)"。

方向接近,但不准确。更准确的说法是:普通 Autoencoder 对每个输入得到一个确定性的 latent point;VAE 则对每个输入参数化一个 latent probability distribution \(q(z|x)\),经典 VAE 选择高斯 family,然后学习其参数 \(\mu\) 和 \(\sigma\)。

第二个卡点是"为什么 Encoder 出来的就一定是高斯"。修正后的理解:高斯不是训练发现的,而是建模假设。Encoder 网络的结构决定了输出是 \(2d\) 个数,设计者定义这些数参数化一个对角高斯。训练学习的是参数取什么值。

第三个容易滑过去的错误是"KL 就是让所有 latent distribution 都变成 \(\mathcal{N}(0, I)\)"。但如果真的全变成标准高斯,所有输入的信息就丢了。KL 是 regularization,它和 reconstruction objective 之间有张力;VAE 要在表达输入信息和遵守 prior 之间找到平衡。

本文没有展开的训练数学
#

VAE 的训练还有两个值得继续追的问题,本文刻意没有展开:

  1. 从概率分布中随机采样,梯度如何传播? 采样操作本身不可微。VAE 通过 reparameterization trick(\(z = \mu + \sigma \cdot \epsilon\),把随机性隔离到 \(\epsilon\) 上)解决了这个问题。
  2. 为什么训练目标恰好是 Reconstruction + KL? 更完整的回答需要从 variational inference 出发,引出 evidence lower bound(ELBO)的概念和推导。

对于只希望建立 VAE 基本心智模型的读者,到本文为止已经足够理解它的核心设计。如果后续想深入训练数学,这两个问题是接下来的入口。

写在最后
#

把全文压缩成一条因果链:

Autoencoder 对每个输入得到一个确定性的 latent point → 重建没有问题 → 但 latent space 没有被要求服从一个方便采样的全局分布 → 如果想主动生成新样本,不知道从哪里采样 \(z\) → VAE 把 \(x \to z\) 改成 \(x \to q(z|x)\) → 选择高斯 family,Encoder 学习 \(\mu(x)\) 和 \(\sigma(x)\) → 规定 prior \(p(z) = \mathcal{N}(0, I)\) → KL divergence 约束 \(q(z|x)\) 不要严重偏离 prior → Reconstruction 保证 \(z\) 仍然携带输入信息 → 最终得到一个既能表达数据,又可以从已知 prior 中采样的 latent space → 从 prior 采样 \(z\),交给 Decoder,生成新样本。

VAE 最值得记住的,不是某个 KL 公式或者 ELBO 推导,而是它对 latent representation 的重新定义:一个输入不再对应 latent space 中的一个确定点,而是对应一个概率分布。 一旦做出这个改变,prior、KL divergence、采样这些设计就不再是孤立的数学技巧,而是沿着同一条逻辑自然出现。

参考资料
#

相关文章

9 年后重温 CNN:剥掉算子细节后,真正留下了什么

最近在重新过 MIT 6.S191 Lecture 3(卷积神经网络)。2017 年刚接触 CV 那会儿,CNN 算是吃饭的家伙,每天都在调。后来精力逐渐转到 ML Infra,成天跟 GPU 显存、通信拓扑和算子优化打交道,卷积网络的很多具体细节就慢慢生疏了——写个 nn.Conv2d 时 weight 的四维形状到底怎么排、Kaiming 初始化的方差怎么推、感受野怎么算,冷不丁被问到,还得在脑子里卡壳一下。