自编码器与VAE 本节摘要:一个普通自编码器(Autoencoder)只做「压缩再还原」,它会过拟合记忆,但它不会生成。加一个把编码强行推向高斯分布的小技巧,它就变成了一个采样器——这个技巧,就是把 重参数化(Reparameterization)。正是这一个等式,决定了你在 2026 年用到的每一个潜在扩散(Latent Diffusion)、流匹配图像模型,输入端都挂着一个 VAE。本节从数学上讲透:为什么编码器要输出分布而不是点、为什么 ELBO 有两项、KL 惩罚凭什么能把编码空间「塑形」成可采样的高斯、重参数化如何让梯度穿过随机节点。读完本节,你就理解了所有图像流水线那层「隐形的第一层」。 对应原课程:Phase 08 · Lesson 02 · (原英文 )。
本节摘要:一个普通自编码器(Autoencoder)只做「压缩再还原」,它会过拟合记忆,但它不会生成。加一个把编码强行推向高斯分布的小技巧,它就变成了一个采样器——这个技巧,就是把
z = μ + σ·ε重参数化(Reparameterization)。正是这一个等式,决定了你在 2026 年用到的每一个潜在扩散(Latent Diffusion)、流匹配图像模型,输入端都挂着一个 VAE。本节从数学上讲透:为什么编码器要输出分布而不是点、为什么 ELBO 有两项、KL 惩罚凭什么能把编码空间「塑形」成可采样的高斯、重参数化如何让梯度穿过随机节点。读完本节,你就理解了所有图像流水线那层「隐形的第一层」。
对应原课程:Phase 08 · Lesson 02 ·
autoencoders-vae(原英文phases/08-generative-ai/02-autoencoders-vae/docs/en.md)。
阅读完本节,你应当能够:
q(z|x)、KL 惩罚把它拉向先验 N(0, I)、重参数化让采样可微。把一个 784 像素的 MNIST 数字压缩成 16 个数的编码,再还原回去——一个普通自编码器能把重建 MSE 做得很漂亮,但它的编码空间是一团无结构的乱麻。你随便在这个空间里挑一个点,解码出来,得到的是噪声。它没有采样器,它只是个穿了生成模型外衣的压缩模型。
你真正想要的是三件事:
N(0, I))。三个目标、一个架构、一个损失。
Kingma 2013 的 VAE 这样解决:让编码器输出的不是一个点,而是一个分布 q(z|x) = N(μ(x), σ(x)²);用 KL 惩罚把这个分布拉向先验 N(0, I);再从 q(z|x) 里采样出 z 送给解码器。推理时丢掉编码器,直接 z ~ N(0, I) 再解码。KL 惩罚是把编码空间「塑形」成可采样的那只手。
💡 在 2026 年,VAE 很少独立交付——原始图像质量上它已被扩散超越——但它是每一个潜在扩散模型(SD 1/2/XL/3、Flux、AudioCraft)的输入编码器。学会 VAE,你就学会了你正在用的每一条图像流水线那层「隐形的第一层」。
为什么编码器要输出分布? 普通自编码器只给一个点,这个点周围长什么样你一无所知,所以你不敢采样。让编码器输出 μ(x) 和 σ(x),你就有了「这个样本附近,编码空间长什么样」的局部地图。把所有局部地图用 KL 拼到同一张全局地图(N(0, I))上,整个空间就变得平滑、可外推。
为什么要重参数化? 直接从 N(μ, σ²) 采样,采样这个操作本身不可微,梯度断在「采样」这一步。把采样改写成 z = μ + σ·ε,ε ~ N(0, I) 是与参数无关的纯噪声——于是 z 是 (μ, σ) 的确定性函数加上一个外部噪声,梯度可以顺畅地流过 μ 和 σ。这个改写叫重参数化技巧(Reparameterization Trick),是 VAE 整套训练能跑起来的命脉。
为什么是 ELBO? 我们真正想最大化的是 log p(x),但它含一个对 z 的不可处理积分:log p(x) = log ∫ p(x|z)p(z) dz。把后验 p(z|x) 用一个可学的近似 q(z|x) 代入,用 Jensen 不等式,得到证据下界(ELBO):
log p(x) ≥ ELBO = E_q[log p(x|z)] − KL[q(z|x) || p(z)] = −重建误差 − KL 惩罚
右端两项都可计算、可微分、可用随机梯度下降优化。最大化 ELBO 同时做了两件事:让 x̂ 逼近 x(重建),让 q(z|x) 逼近先验(塑形)。这就是 VAE 的全部数学。
code/main.py 在不用 numpy、不用 torch 的前提下实现了一个迷你 VAE。输入是 8 维合成数据,来自一个 8 维的两分量高斯混合。编码器与解码器都是单隐层 MLP,我们手写 tanh 激活、前向传播、损失函数,以及一个手写的反向传播。这不是生产代码,这是教学。
def encode(x, enc): h = tanh(add(matmul(enc["W1"], x), enc["b1"])) mu = add(matmul(enc["W_mu"], h), enc["b_mu"]) log_sigma2 = add(matmul(enc["W_sig"], h), enc["b_sig"]) return mu, log_sigma2
为什么输出 log σ² 而不是 σ?因为网络的输出应当无约束(可正可负),而 σ 必须为正。对 σ 直接套 softplus 是个陷阱——在 σ ≈ 0 处梯度会死。log σ² 让网络输出任意实数,需要时 σ = exp(0.5·log σ²) 即可还原。
def reparameterize(mu, log_sigma2, rng): eps = [rng.gauss(0, 1) for _ in mu] # 纯噪声 ε ~ N(0,I) sigma = [math.exp(0.5 * lv) for lv in log_sigma2] # σ = exp(0.5·log σ²) return [m + s * e for m, s, e in zip(mu, sigma, eps)] def decode(z, dec): h = tanh(add(matmul(dec["W1"], z), dec["b1"])) return add(matmul(dec["W_out"], h), dec["b_out"])
注意 reparameterize 里,梯度只走 μ 和 σ 这条确定性路径;ε 当作常量(它的梯度我们不关心)。反向传播时,∂z/∂μ = 1、∂z/∂σ = ε,干净利落。
def elbo(x, x_hat, mu, log_sigma2, beta=1.0): recon = sum((a - b) ** 2 for a, b in zip(x, x_hat)) kl = 0.5 * sum(math.exp(lv) + m * m - lv - 1 for m, lv in zip(mu, log_sigma2)) return recon + beta * kl, recon, kl
KL 项用了 N(μ, diag(σ²)) 与 N(0, I) 之间的闭式解:
KL = 0.5 · Σ_i ( σ_i² + μ_i² − log σ_i² − 1 )
⚠️ 永远用闭式 KL,不要数值积分,也不要蒙特卡洛估计 KL。两个分布都是高斯,闭式公式精确且快 3 倍。但 2026 年依然有人在线上代码里跑蒙特卡洛 KL,纯属浪费算力。
def sample(dec, z_dim, rng): z = [rng.gauss(0, 1) for _ in range(z_dim)] # 从先验 N(0,I) 采样 return decode(z, dec)
这就是生成模型本身——五行代码。一次前向,无需迭代采样(这与扩散的「几十步」形成鲜明对比)。
后验崩塌(Posterior Collapse):KL 项太猛,把 q(z|x) 一把推成 N(0, I),于是 z 不再携带任何关于 x 的信息,解码器只能瞎编。修复:β 退火(β 从 0 起步,慢慢爬到 1)、free bits(给每个维度保留一点 KL 配额)、或在「沉默」的维度上跳过 KL。
样本模糊(Blurry Samples):高斯解码器似然隐含 MSE 重建,而 MSE 对 L2 是贝叶斯最优的——最优解是一堆合理数字的均值,而均值天生就是一张模糊图。修复:改用离散解码器(VQ-VAE、NVAE),或者干脆只把 VAE 当编码器用,在潜变量上再叠一个扩散模型(这正是 Stable Diffusion 的做法)。
β 太大太早:等同于后验崩塌。从 β≈0.01 起步,慢慢退火到 1。
潜变量维度太小:MNIST 用 16 维够,ImageNet 256² 要 256 维,ImageNet 1024² 要 2048 维。Stable Diffusion 的 VAE 把 512×512×3 压到 64×64×4(空间面积压缩 64 倍,通道也压缩)。
💡 β-VAE(Higgins 2017)把 β 做成可调旋钮而一举成名:β < 1 → 样本更锐、编码空间更不规整;β > 1 → 编码空间更干净、样本更模糊。它还意外开启了「解耦表示学习」(disentanglement)的整个研究方向——大的 β 会强迫每个潜变量维度对应一个语义因子。
| 场景 | 选什么 |
|---|---|
| 扩散的图像潜变量编码器 | Stable Diffusion VAE(sd-vae-ft-ema)或 Flux VAE |
| 音频潜变量编码器 | Encodec(Meta)、SoundStream、DAC(Descript) |
| 视频潜变量 | Sora 的时空 patch、Latte VAE、WAN VAE |
| 解耦表示学习 | β-VAE、FactorVAE、TCVAE |
| 离散潜变量(供 Transformer 建模) | VQ-VAE、RVQ(ResidualVQ) |
| 用于生成的连续潜变量 | 纯 VAE,然后在该潜空间里条件一个流 / 扩散模型 |
关键洞察:一个潜在扩散模型,本质上是一个 VAE,中间塞了一个扩散模型。VAE 做「粗压缩」,扩散模型做「重活」。视频(VAE + 视频-扩散 DiT)和音频(Encodec + MusicGen Transformer)都是同一个套路。
VAE 负责把高维像素压到低维潜变量(让扩散不必在像素空间里算,大幅省算力);扩散负责在潜空间里建模分布。这种分工,是潜在扩散能在消费级 GPU 上跑起来的根本原因。
本节产出一个训练技能文件(位于原课程 outputs/skill-vae-trainer.md)。
skill-vae-trainer.md:输入「数据集画像 + 目标潜变量维度 + 下游用途(重建 / 采样 / 作为潜在扩散输入)」,输出:架构选择(纯 / β / VQ / RVQ)、β 调度、潜变量维度、解码器似然(高斯 vs 类别型)、评估方案(重建 MSE、每维 KL、q(z|x) 与 N(0, I) 之间的 Fréchet 距离)。简单。 把 code/main.py 里的 β 依次改为 0.01、0.1、1.0、5.0,记录最终的重建 MSE 与 KL。对你的合成数据,哪个 β 在「重建 / KL」帕累托前沿上最优?
中等。 把高斯解码器似然换成伯努利似然(交叉熵损失)。在同一合成数据的二值化版本上,比较样本质量。
困难。 把 code/main.py 扩展成一个迷你 VQ-VAE:用「在 K=32 的码本里做最近邻查找」替换连续的 z。比较重建 MSE,并报告码本里有几项真正被用到(码本崩塌(codebook collapse)是真实存在的问题)。
q(z|x)=N(μ,σ²)、KL 把它拉向先验 N(0,I)、重参数化 z=μ+σ·ε 让采样可微。x̂ 拉向 x,KL 把 q(z|x) 拉向 N(0,I);两者此消彼长,β 是权衡旋钮。下一节,我们进入桶 3 的代表——GAN(生成对抗网络):用一个判别器逼着生成器产出锐利样本,代价是训练不稳定与无似然。我们会讲透博弈损失、模式崩塌,以及为什么 StyleGAN 至今在窄领域照片级人脸仍是标杆。