VAE 变分自编码器
让潜在空间学会"规律" — 从压缩到生成
- VAE 在编码器输出均值和方差,用重参数化技巧实现可微采样
- KL 散度约束潜在分布接近标准正态,实现正则化
- 损失函数 = 重构损失 + KL 散度(ELBO)
- 潜在空间连续且平滑,可以插值生成新样本
为什么需要 VAE?
普通自编码器(AE)学到了数据的压缩表示,但它的潜在空间是"混乱"的 — 不同数据的编码可能散落在潜在空间的任意位置,彼此之间没有规律。
问题:如果你想生成新数据,你需要从潜在空间中随机采样一个点,然后解码。但 AE 的潜在空间不连续、不规则,随机采样很可能落在"空白区域",解码出来的东西毫无意义。
VAE 的核心思想:强制让潜在空间服从正态分布 N(0,1)。这样潜在空间是连续、平滑的,任意采样一个点都能解码出有意义的结果。
普通自编码器 (AE)
编码器输出一个固定的向量 z
潜在空间不连续,无法生成新数据
变分自编码器 (VAE)
编码器输出分布参数 μ 和 σ,从中采样得到 z
潜在空间连续平滑,可以自由采样生成
生活类比
想象你在学习画人脸。普通 AE 就像死记硬背每张脸的压缩编码,但不理解人脸的"规律" — 眼睛间距、鼻子大小、脸型等。
VAE 则像学会了人脸的"配方" — 每个维度代表一个有意义的属性(如"眼睛大小"、"脸型"),且这些属性服从正态分布。你可以自由调整配方参数,画出无穷多张不同但合理的人脸。
📊 编码器输出 μ 和 σ
VAE 的编码器不像 AE 直接输出一个向量 z,而是输出两个向量:均值 μ 和标准差 σ。这两个参数定义了潜在空间中的一个正态分布。 点击单元格可以编辑输入值!
Excel 手推:编码器计算 μ 和 σ
假设输入 x = [0.5, 0.8, 0.3],经过编码器网络(简化为线性变换)
| A x₁ |
B x₂ |
C x₃ |
|
|---|---|---|---|
| 输入 x | 0.5 | 0.8 | 0.3 |
| 权重 wμ | 1.2 | 0.8 | -0.5 |
| μ = w·x + bμ | 0.75 | 1.14 | 0.05 |
| 权重 wσ | 0.6 | -0.4 | 0.9 |
| log(σ²) = w·x + bσ | 0.10 | -0.22 | 0.37 |
| σ = exp(0.5 × log(σ²)) | 1.051 | 0.896 | 1.204 |
💡 实际中编码器输出 log(σ²) 而非 σ,因为 log 空间更稳定,数值范围更大
关键区别:AE 编码器直接输出 z,而 VAE 编码器输出 μ 和 σ(分布参数)。z 不再是一个固定值,而是从 N(μ, σ²) 中采样得到的随机变量。
📊 重参数化技巧 (Reparameterization Trick)
直接从 N(μ, σ²) 中采样 z 是一个随机操作,无法反向传播梯度。 重参数化技巧把随机性"转移"到一个外部噪声 ε 上:
把采样的随机性"外包"给 ε,让 μ 和 σ 的梯度可以正常计算
Excel 手推:计算 z
用上一步得到的 μ、σ 和随机采样的 ε 来计算 z
| A 维度 1 |
B 维度 2 |
C 维度 3 |
|
|---|---|---|---|
| μ (均值) | 0.75 | 1.14 | 0.05 |
| σ (标准差) | 1.051 | 0.896 | 1.204 |
| ε ~ N(0,1) | 0.52 | -1.07 | 0.86 |
| σ × ε | 0.547 | -0.959 | 1.035 |
| z = μ + σ × ε | 1.297 | 0.181 | 1.085 |
没有重参数化
z = sample(N(μ, σ²))
采样操作不可微分,梯度无法从 z 传回 μ 和 σ,编码器无法训练
有重参数化
z = μ + σ × ε, ε ~ N(0,1)
随机性在 ε 上,μ 和 σ 参与确定性计算,梯度可以正常反向传播
为什么有效?ε 来自标准正态分布,不依赖模型参数。所以 z 对 μ 的偏导 = 1,z 对 σ 的偏导 = ε。梯度链路打通了!
📊 KL 散度损失
VAE 的损失函数有两部分:重建损失(让解码结果接近输入)和 KL 散度损失(让潜在分布接近标准正态分布)。
重建损失 + KL 散度损失
KL 散度公式
KL 散度衡量"编码器输出的分布 q(z|x)"与"标准正态分布 p(z) = N(0,1)"之间的差异。 对于对角高斯分布,KL 散度有解析解:
对所有维度求和。当 μ=0, σ=1 时,KL=0(完美匹配标准正态)
Excel 手推:计算 KL 散度
用前面得到的 μ 和 σ,逐维度计算 KL 散度
| A 维度 1 |
B 维度 2 |
C 维度 3 |
|
|---|---|---|---|
| μ | 0.75 | 1.14 | 0.05 |
| σ² | 1.105 | 0.803 | 1.450 |
| log(σ²) | 0.100 | -0.220 | 0.370 |
| 1 + log(σ²) | 1.100 | 0.780 | 1.370 |
| - μ² | -0.563 | -1.300 | -0.003 |
| - σ² | -1.105 | -0.803 | -1.450 |
| 每维度: 1+log(σ²)-μ²-σ² | -0.568 | -1.323 | -0.083 |
| KL = -0.5 × Σ | 0.987 | ||
直觉理解:KL 散度惩罚编码器"偏离"标准正态分布的行为。如果 μ 远离 0 或 σ 远离 1,KL 损失就会增大,迫使编码器把数据"压缩"到接近 N(0,1) 的分布中。
🎮 互动实验:潜在空间采样
拖动滑块调整 μ 和 σ,观察采样点在 2D 潜在空间中的分布变化。蓝色区域代表标准正态分布 N(0,1) 的密度。
观察:
- 当 μ=0, σ=1 时,采样点与标准正态分布完美重合
- 改变 μ 会让采样中心偏移(KL 损失增大)
- 改变 σ 会让采样范围变大或变小(KL 损失增大)
- 这就是 KL 散度在惩罚什么 — 让分布回归 N(0,1)
💻 PyTorch 代码实现
import torch
import torch.nn as nn
class VAE(nn.Module):
def __init__(self, input_dim=784, hidden_dim=256, latent_dim=2):
super().__init__()
# 编码器:输入 → 隐藏层 → μ 和 log(σ²)
self.encoder = nn.Sequential(
nn.Linear(input_dim, hidden_dim),
nn.ReLU(),
)
self.fc_mu = nn.Linear(hidden_dim, latent_dim) # 输出 μ
self.fc_logvar = nn.Linear(hidden_dim, latent_dim) # 输出 log(σ²)
# 解码器:z → 隐藏层 → 重建
self.decoder = nn.Sequential(
nn.Linear(latent_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, input_dim),
nn.Sigmoid(),
)
def reparameterize(self, mu, logvar):
"""重参数化技巧:z = μ + σ × ε"""
std = torch.exp(0.5 * logvar) # σ = exp(0.5 * log(σ²))
eps = torch.randn_like(std) # ε ~ N(0, 1)
return mu + std * eps # z = μ + σ × ε
def forward(self, x):
h = self.encoder(x)
mu = self.fc_mu(h)
logvar = self.fc_logvar(h)
z = self.reparameterize(mu, logvar)
x_recon = self.decoder(z)
return x_recon, mu, logvar
def vae_loss(x_recon, x, mu, logvar):
# 重建损失:二元交叉熵(或 MSE)
recon_loss = nn.functional.binary_cross_entropy(
x_recon, x, reduction='sum'
)
# KL 散度损失:-0.5 × Σ(1 + log(σ²) - μ² - σ²)
kl_loss = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
return recon_loss + kl_loss
model = VAE(input_dim=784, latent_dim=2)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
for epoch in range(50):
for batch_x in dataloader:
x_recon, mu, logvar = model(batch_x)
loss = vae_loss(x_recon, batch_x, mu, logvar)
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 生成新数据:从标准正态采样 z,直接解码
z = torch.randn(16, 2) # 从 N(0,1) 采样
generated = model.decoder(z) # 解码生成新图片
🧠 小测验
- Autoencoder — VAE 的基础——确定性自编码器
- GAN — GAN 是另一种生成模型,对比学习
- Loss & 反向传播 — 重参数化技巧让采样可微分
1. VAE 与普通自编码器 (AE) 最核心的区别是什么?
2. 为什么 VAE 需要重参数化技巧 (Reparameterization Trick)?
3. KL 散度损失在 VAE 中的作用是什么?