STAGE 2 生成模型

VAE 变分自编码器

让潜在空间学会"规律" — 从压缩到生成

📊 Excel 手推 ⏱️ 20 分钟 🎯 生成模型
1
📋 本章要点
  • VAE 在编码器输出均值和方差,用重参数化技巧实现可微采样
  • KL 散度约束潜在分布接近标准正态,实现正则化
  • 损失函数 = 重构损失 + KL 散度(ELBO)
  • 潜在空间连续且平滑,可以插值生成新样本

为什么需要 VAE?

普通自编码器(AE)学到了数据的压缩表示,但它的潜在空间是"混乱"的 — 不同数据的编码可能散落在潜在空间的任意位置,彼此之间没有规律。

问题:如果你想生成新数据,你需要从潜在空间中随机采样一个点,然后解码。但 AE 的潜在空间不连续、不规则,随机采样很可能落在"空白区域",解码出来的东西毫无意义。

VAE 的核心思想:强制让潜在空间服从正态分布 N(0,1)。这样潜在空间是连续、平滑的,任意采样一个点都能解码出有意义的结果。

🗜️

普通自编码器 (AE)

编码器输出一个固定的向量 z

潜在空间不连续,无法生成新数据

🎲

变分自编码器 (VAE)

编码器输出分布参数 μ 和 σ,从中采样得到 z

潜在空间连续平滑,可以自由采样生成

生活类比

想象你在学习画人脸。普通 AE 就像死记硬背每张脸的压缩编码,但不理解人脸的"规律" — 眼睛间距、鼻子大小、脸型等。

VAE 则像学会了人脸的"配方" — 每个维度代表一个有意义的属性(如"眼睛大小"、"脸型"),且这些属性服从正态分布。你可以自由调整配方参数,画出无穷多张不同但合理的人脸。

2

📊 编码器输出 μ 和 σ

VAE 的编码器不像 AE 直接输出一个向量 z,而是输出两个向量:均值 μ 和标准差 σ。这两个参数定义了潜在空间中的一个正态分布。 点击单元格可以编辑输入值!

输入 x
原始数据
编码器
神经网络
μ, σ
分布参数
采样 z
z ~ N(μ, σ²)
解码器
重建 x̂

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(μ, σ²) 中采样得到的随机变量。

3

📊 重参数化技巧 (Reparameterization Trick)

直接从 N(μ, σ²) 中采样 z 是一个随机操作,无法反向传播梯度。 重参数化技巧把随机性"转移"到一个外部噪声 ε 上:

z = μ + σ × ε   其中   ε ~ N(0, 1)

把采样的随机性"外包"给 ε,让 μ 和 σ 的梯度可以正常计算

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 对 σ 的偏导 = ε。梯度链路打通了!

4

📊 KL 散度损失

VAE 的损失函数有两部分:重建损失(让解码结果接近输入)和 KL 散度损失(让潜在分布接近标准正态分布)。

Ltotal = Lrecon + LKL

重建损失 + KL 散度损失

KL 散度公式

KL 散度衡量"编码器输出的分布 q(z|x)"与"标准正态分布 p(z) = N(0,1)"之间的差异。 对于对角高斯分布,KL 散度有解析解:

DKL = -0.5 × Σ(1 + log(σ²) - μ² - σ²)

对所有维度求和。当 μ=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) 的分布中。

5

🎮 互动实验:潜在空间采样

拖动滑块调整 μ 和 σ,观察采样点在 2D 潜在空间中的分布变化。蓝色区域代表标准正态分布 N(0,1) 的密度。

0.0
0.0
1.0
50

观察:

  • 当 μ=0, σ=1 时,采样点与标准正态分布完美重合
  • 改变 μ 会让采样中心偏移(KL 损失增大)
  • 改变 σ 会让采样范围变大或变小(KL 损失增大)
  • 这就是 KL 散度在惩罚什么 — 让分布回归 N(0,1)
KL 散度
0.000
重建质量指示
良好
6

💻 PyTorch 代码实现

vae.py
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
loss.py
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
train_vae.py
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)       # 解码生成新图片
7

🧠 小测验

🔗 相关章节推荐
  • AutoencoderVAE 的基础——确定性自编码器
  • GANGAN 是另一种生成模型,对比学习
  • Loss & 反向传播重参数化技巧让采样可微分

1. VAE 与普通自编码器 (AE) 最核心的区别是什么?

2. 为什么 VAE 需要重参数化技巧 (Reparameterization Trick)?

3. KL 散度损失在 VAE 中的作用是什么?

Easy Deep Learning · 用 Excel 手推理解每一个公式