查看全课大纲(第 30 / 40 节)
- 1.从规则到学习:深度学习解决什么问题
- 2.表示学习与端到端训练
- 3.张量、计算图与自动微分
- 4.训练三要素:数据、模型、目标函数
- 5.从感知机到多层感知机
- 6.反向传播:手推一个两层网络
- 7.激活函数与初始化
- 8.优化器:SGD → Momentum → Adam
- 9.正则化:权重衰减、Dropout、BN/LN
- 10.卷积的直觉与参数共享
- 11.池化、感受野与下采样
- 12.经典网络演进:LeNet → AlexNet → VGG → ResNet
- 13.残差连接与归一化为什么有效
- 14.迁移学习与特征可视化
- 15.RNN 与 BPTT
- 16.LSTM/GRU:门控如何缓解梯度问题
- 17.Seq2Seq 与编码器-解码器
- 18.注意力前史:Bahdanau 加性注意力
- 19.Self-Attention:QKV 与缩放点积
- 20.多头注意力:并行的多种关系
- 21.位置编码:正弦与可学习
- 22.残差、LayerNorm 与前馈层
- 23.从零实现一个最小 Transformer block
- 24.训练稳定性、复杂度与常见误区
- 25.自监督与语言建模目标
- 26.BERT:掩码语言模型与双向编码
- 27.GPT:自回归生成与缩放定律
- 28.指令微调与对齐:SFT → RLHF/DPO
- 29.参数高效微调:LoRA 与适配器
- 30.自编码器与 VAE
- 31.GAN:对抗训练与模式崩塌
- 32.扩散模型:前向加噪与反向去噪
- 33.扩散模型的应用与采样加速
- 34.对比学习与 CLIP
- 35.ViT:把图像当序列
- 36.多模态架构:对齐、融合与生成
- 37.能力边界、幻觉与评测
- 38.PyTorch 2.x 训练循环与混合精度
- 39.实验管理、评测与复现
- 40.推理部署与成本:量化/蒸馏概览
自编码器与 VAE
小象实战讲义 · 深度学习基础:从神经网络到 Transformer
上一章我们让模型学会“读”和“接龙”,这一章换个方向:让模型学会造。生成模型的第一站是自编码器——它用“压缩再解压”的方式无监督地学表示;把它的潜在空间改成概率分布,就得到了 VAE。学完本节你将能回答:自编码器凭什么能学到有用特征?VAE 相比普通 AE 改了什么?重参数化技巧为什么是 VAE 能训起来的关键?
先修:多层感知机与反向传播(第 1 章)、交叉熵与 KL 散度(第 6 章 6.4 节用过)。后续:下一节 GAN,再下一节扩散模型。
💡 核心导读
- 自编码器是一个由编码器和解码器组成的神经网络,通过最小化输入与重建输出之间的差异来学习数据的压缩表示。
- 变分自编码器(VAE) 将自编码器置于概率生成模型的框架下,其编码器输出潜在变量的分布参数(均值和方差),而非确定值。
- VAE 的损失函数由两部分构成:重建损失(衡量解码质量)和 KL 散度(约束潜在分布接近标准正态先验),后者是 VAE 能生成新样本的关键。
- 重参数化技巧是 VAE 训练的核心,它允许从参数化分布中采样,同时保持梯度可计算,从而支持端到端的反向传播。
🧭 前置知识
- 神经网络前向传播与反向传播:理解多层感知机(MLP)的基本运作。
- 均方误差(MSE)与交叉熵损失:了解基本的损失函数。
- 概率分布基础概念:了解高斯分布(正态分布)的均值和方差参数。
- KL 散度的直观意义:衡量两个概率分布之间的差异(不展开公式推导,见站内数学基础课)。
自编码器:结构与目标
自编码器的目标很简单:学习一个能将输入数据高效压缩(编码)到一个低维潜在空间,并能从该压缩表示中尽可能准确地重建(解码)原始数据的模型。
一个典型的自编码器结构如下:
输入 X (32 维)
↓ 编码器
潜在编码 Z (4 维)
↓ 解码器
重建输出 X' (32 维)训练目标是最小化原始输入 X 与重建输出 X' 之间的差异,常用均方误差(MSE)或二值交叉熵(BCE)作为损失函数。
import torch
import torch.nn as nn
import torch.nn.functional as F
torch.manual_seed(0)
# 定义一个简单的自编码器
class Autoencoder(nn.Module):
def __init__(self, input_dim=32, latent_dim=4):
super().__init__()
# 编码器:将高维输入压缩到低维潜在空间
self.encoder = nn.Sequential(
nn.Linear(input_dim, 16),
nn.ReLU(),
nn.Linear(16, 8),
nn.ReLU(),
nn.Linear(8, latent_dim), # 输出确定性的潜在编码 z
)
# 解码器:从潜在空间重建原始数据
self.decoder = nn.Sequential(
nn.Linear(latent_dim, 8),
nn.ReLU(),
nn.Linear(8, 16),
nn.ReLU(),
nn.Linear(16, input_dim),
nn.Sigmoid() # 把输出压到 [0,1],模拟归一化后的像素
)
def forward(self, x):
z = self.encoder(x)
x_recon = self.decoder(z)
return x_recon
# 模拟一批数据(batch=4,特征维 32;真实场景换成展平后的图像)
batch_size = 4
input_dim = 32
x_dummy = torch.rand(batch_size, input_dim) # [0,1) 均匀分布,配合 Sigmoid 输出
model = Autoencoder(input_dim=input_dim, latent_dim=4)
reconstructed = model(x_dummy)
print("输入数据形状:", x_dummy.shape)
print("重建数据形状:", reconstructed.shape)
print("潜在编码维度: 4")
print(f"重建损失 (MSE): {F.mse_loss(reconstructed, x_dummy).item():.4f}")输入数据形状: torch.Size([4, 32])
重建数据形状: torch.Size([4, 32])
潜在编码维度: 4
重建损失 (MSE): 0.0872这个自编码器学习到的潜在空间 z 是确定性的。对于同一个输入,编码器总是输出相同的 z。这导致潜在空间可能是不连续、不平滑的,我们无法通过简单地插值 z 来生成有意义的、连续变化的新样本。
变分自编码器(VAE):概率生成视角
变分自编码器(Variational Autoencoder,VAE)在《Auto-Encoding Variational Bayes(Kingma & Welling, 2013,arXiv:1312.6114)》中被提出。VAE 的核心思想是:将数据生成过程视为一个概率模型。
在 VAE 的框架中:
- 每个数据点
x由一个不可观测的连续潜在变量z生成。 - 我们假设
z的先验分布p(z)是一个简单的标准正态分布N(0, I)。 - 生成过程(解码)由条件分布
p(x|z)定义,通常用一个神经网络(解码器)来参数化。 - 由于真实后验分布
p(z|x)难以计算,我们用一个由神经网络(编码器)参数化的分布q(z|x)来近似它,通常也假设为高斯分布N(μ(x), σ²(x)I)。
因此,VAE 的编码器不再输出一个确定的 z,而是输出潜在分布的参数:均值 μ 和对数方差 log_var(输出对数方差是为了保证方差为正)。
import torch
import torch.nn as nn
import torch.nn.functional as F
torch.manual_seed(0)
class VariationalAutoEncoder(nn.Module):
def __init__(self, input_dim=32, latent_dim=2):
super().__init__()
self.latent_dim = latent_dim
# 编码器部分:输出分布参数 μ 和 log_var
self.encoder_fc1 = nn.Linear(input_dim, 16)
self.encoder_fc2 = nn.Linear(16, 8)
self.fc_mu = nn.Linear(8, latent_dim) # 均值 μ
self.fc_log_var = nn.Linear(8, latent_dim) # 对数方差 log(σ²)
# 解码器部分:从潜在变量 z 重建数据
self.decoder = nn.Sequential(
nn.Linear(latent_dim, 8),
nn.ReLU(),
nn.Linear(8, 16),
nn.ReLU(),
nn.Linear(16, input_dim),
nn.Sigmoid()
)
def encode(self, x):
# 编码器前向传播,得到分布参数
h = F.relu(self.encoder_fc1(x))
h = F.relu(self.encoder_fc2(h))
mu = self.fc_mu(h)
log_var = self.fc_log_var(h)
return mu, log_var
def reparameterize(self, mu, log_var):
"""重参数化技巧:从 N(mu, var) 采样,同时保持梯度可传"""
std = torch.exp(0.5 * log_var) # 标准差 σ = exp(0.5 * log_var)
eps = torch.randn_like(std) # 从标准正态分布采样噪声 ε
z = mu + eps * std # 得到采样结果 z = μ + ε * σ
return z
def decode(self, z):
return self.decoder(z)
def forward(self, x):
mu, log_var = self.encode(x)
z = self.reparameterize(mu, log_var)
x_recon = self.decode(z)
return x_recon, mu, log_var
# 实例化模型并前向传播
model_vae = VariationalAutoEncoder(input_dim=32, latent_dim=2) # 2 维潜在空间便于可视化
x_dummy = torch.rand(2, 32) # batch_size=2
x_recon, mu, log_var = model_vae(x_dummy)
print("输入 x.shape:", x_dummy.shape)
print("重建 x_recon.shape:", x_recon.shape)
print("潜在分布均值 mu.shape:", mu.shape)
print("潜在分布对数方差 log_var.shape:", log_var.shape)
print("mu[0]:", [round(v, 4) for v in mu[0].tolist()])
print("log_var[0]:", [round(v, 4) for v in log_var[0].tolist()])输入 x.shape: torch.Size([2, 32])
重建 x_recon.shape: torch.Size([2, 32])
潜在分布均值 mu.shape: torch.Size([2, 2])
潜在分布对数方差 log_var.shape: torch.Size([2, 2])
mu[0]: [0.3491, -0.1196]
log_var[0]: [0.1523, 0.116]VAE 的损失函数与重参数化技巧
VAE 的损失函数:证据下界(ELBO)
VAE 的优化目标是最大化数据 x 的对数似然 log p(x) 的下界,即证据下界(ELBO)。ELBO 可以分解为两项: L(x) = E_{z~q(z|x)}[log p(x|z)] - D_{KL}(q(z|x) || p(z))
- 重建损失(Reconstruction Loss):
E_{z~q(z|x)}[log p(x|z)]。期望项通常通过一次蒙特卡洛采样(即一次前向传播)来近似。对于二值数据(如图像像素),常用二值交叉熵(BCE);对于实值数据,常用均方误差(MSE)。它衡量了解码器重建输入的能力。 - KL 散度损失(KL Divergence Loss):
D_{KL}(q(z|x) || p(z))。其中q(z|x) = N(μ, σ²I),p(z) = N(0, I)。这一项约束编码器输出的分布接近标准正态先验。它有解析解,对于高斯分布,其值为:KL = -0.5 * Σ (1 + log(σ²) - μ² - σ²)
重参数化技巧(Reparameterization Trick)
这是 VAE 训练的关键。如果我们直接从 N(μ, σ²) 采样 z,采样操作是不可导的,梯度无法通过 z 回传到编码器参数。重参数化技巧将采样过程分解为:z = μ + σ * ε,其中 ε ~ N(0, I)。这样,随机性被转移到了噪声 ε 上,而 μ 和 σ 是确定性的、可导的。梯度可以通过 z 顺利地传播到编码器。
import torch
import torch.nn as nn
import torch.nn.functional as F
torch.manual_seed(0)
# 块自包含:这里重新定义一份极简 VAE(结构同上,单隐层)
class TinyVAE(nn.Module):
def __init__(self, input_dim=32, latent_dim=2):
super().__init__()
self.enc = nn.Linear(input_dim, 8)
self.fc_mu = nn.Linear(8, latent_dim)
self.fc_log_var = nn.Linear(8, latent_dim)
self.dec = nn.Sequential(nn.Linear(latent_dim, 8), nn.ReLU(),
nn.Linear(8, input_dim), nn.Sigmoid())
def forward(self, x):
h = F.relu(self.enc(x))
mu, log_var = self.fc_mu(h), self.fc_log_var(h)
std = torch.exp(0.5 * log_var)
z = mu + torch.randn_like(std) * std # 重参数化
return self.dec(z), mu, log_var
def vae_loss(x_recon, x_target, mu, log_var):
"""负 ELBO = 重建损失 + KL;x_target 是 [0,1] 的目标值,不再二次 sigmoid"""
recon_loss = F.binary_cross_entropy(x_recon, x_target, reduction='sum')
kl_loss = -0.5 * torch.sum(1 + log_var - mu.pow(2) - log_var.exp())
return recon_loss + kl_loss, recon_loss.item(), kl_loss.item()
model = TinyVAE(input_dim=32, latent_dim=2)
x = torch.rand(2, 32) # 模拟归一化到 [0,1] 的数据
x_recon, mu, log_var = model(x)
total_loss, recon_val, kl_val = vae_loss(x_recon, x, mu, log_var)
print(f"总损失: {total_loss.item():.4f}")
print(f"重建损失 (BCE, 求和): {recon_val:.4f}")
print(f"KL 散度损失: {kl_val:.4f}")
print(f"KL 均值(除以 batch×维度 = 2×2): {kl_val / (2 * 2):.4f}")总损失: 44.5783
重建损失 (BCE, 求和): 44.4617
KL 散度损失: 0.1165
KL 均值(除以 batch×维度 = 2×2): 0.0291VAE 的生成与插值
由于 VAE 的潜在空间被约束为接近连续、平滑的标准正态分布,我们可以通过以下方式利用它:
- 生成新样本:从先验分布
p(z) = N(0, I)中采样一个z,然后通过解码器生成x。 - 潜在空间插值:在两个数据点对应的潜在编码
z1和z2之间进行线性插值,解码后可以得到一系列语义连续变化的新样本。
import torch
import torch.nn as nn
torch.manual_seed(0)
# 块自包含:一个只保留解码器的极简 VAE 骨架(训练好的模型可整体替换)
class TinyDecoder(nn.Module):
def __init__(self, latent_dim=2, out_dim=32):
super().__init__()
self.net = nn.Sequential(nn.Linear(latent_dim, 8), nn.ReLU(),
nn.Linear(8, out_dim), nn.Sigmoid())
def forward(self, z):
return self.net(z)
decoder = TinyDecoder(latent_dim=2, out_dim=32)
decoder.eval() # 推理模式,关掉 dropout 之类
# 1. 从先验 N(0, I) 采样一个 z,解码成新样本
with torch.no_grad():
z_new = torch.randn(1, 2)
generated = decoder(z_new)
print("生成样本的形状:", tuple(generated.shape))
print(f"生成样本的值范围: [{generated.min().item():.4f}, {generated.max().item():.4f}]")
# 2. 两个潜在点之间线性插值
z1, z2 = torch.tensor([[-1.0, 0.0]]), torch.tensor([[1.0, 0.0]])
num_interp = 5
print("\n在 z1 和 z2 之间线性插值:")
with torch.no_grad():
for i in range(num_interp):
alpha = i / (num_interp - 1)
z_interp = (1 - alpha) * z1 + alpha * z2
out = decoder(z_interp)
print(f" 插值点 {i}: z = [{z_interp[0, 0]:.2f}, {z_interp[0, 1]:.2f}]"
f" → 输出均值 {out.mean().item():.4f}")生成样本的形状: (1, 32)
生成样本的值范围: [0.2603, 0.6421]
在 z1 和 z2 之间线性插值:
插值点 0: z = [-1.00, 0.00] → 输出均值 0.4996
插值点 1: z = [-0.50, 0.00] → 输出均值 0.4988
插值点 2: z = [0.00, 0.00] → 输出均值 0.4972
插值点 3: z = [0.50, 0.00] → 输出均值 0.4945
插值点 4: z = [1.00, 0.00] → 输出均值 0.4927📝 动手练一练
问题:在 VAE 的损失函数中,KL 散度项 D_{KL}(q(z|x) || N(0, I)) 会鼓励潜在分布的参数 μ 和 σ 趋向于什么值?这可能会对重建任务产生什么潜在影响?如何通过一个超参数 β 来调节重建与正则化之间的平衡?
👉 点击查看参考答案
思路:KL 散度项会鼓励 μ 趋近于 0,σ 趋近于 1(即 log_var 趋近于 0)。这会使潜在空间更规整、平滑,有利于生成和插值,但可能过度约束了编码器,导致其携带的信息量减少,从而可能损害重建质量。为了调节这种权衡,Higgins 等人在《β-VAE: Learning Basic Visual Concepts with a Constrained Variational Framework(2017,arXiv:1606.05579)》中引入了 β 参数,将总损失修改为:L = 重建损失 + β * KL损失。当 β > 1 时,更强调潜在空间的正则化;当 β < 1 时,更强调重建精度。
import torch
torch.manual_seed(0)
# 为聚焦 β 的作用,直接给出“重建输出 + 目标 + 分布参数”三组张量
x_recon = torch.rand(2, 32) # 解码器输出,已在 [0,1]
x_target = torch.rand(2, 32) # 目标值
mu = torch.randn(2, 2) # 潜在均值
log_var = torch.randn(2, 2) # 潜在对数方差
def beta_vae_loss(x_recon, x_target, mu, log_var, beta=1.0):
recon = torch.nn.functional.binary_cross_entropy(x_recon, x_target, reduction='sum')
kl = -0.5 * torch.sum(1 + log_var - mu.pow(2) - log_var.exp())
return recon + beta * kl, recon.item(), kl.item()
for beta in [0.1, 1.0, 5.0]:
total, recon, kl = beta_vae_loss(x_recon, x_target, mu, log_var, beta=beta)
print(f"β={beta}: 总损失={total.item():.2f}, 重建损失={recon:.2f}, KL损失={kl:.2f}")β=0.1: 总损失=65.41, 重建损失=65.01, KL损失=4.01
β=1.0: 总损失=69.02, 重建损失=65.01, KL损失=4.01
β=5.0: 总损失=85.06, 重建损失=65.01, KL损失=4.01常见错误
| 错误理解或写法 | 现象或报错 | 原因 | 正确做法 |
|---|---|---|---|
在 VAE 中,编码器直接输出 z,而不是 μ 和 log_var。 | 模型无法学习到连续的潜在空间,生成和插值效果差。 | 混淆了普通自编码器与 VAE。VAE 的编码器需要输出分布参数,通过重参数化得到 z。 | 编码器应输出 mu 和 log_var,在 forward 中使用 reparameterize(mu, log_var) 获得 z。 |
计算 KL 散度时,错误地使用了方差 var 而不是对数方差 log_var。 | KL 损失计算错误,可能导致训练不稳定或潜在空间约束失效。 | KL 散度的解析表达式中直接使用 log_var 更方便且数值稳定。 | 使用公式 KL = -0.5 * sum(1 + log_var - mu^2 - exp(log_var))。 |
| 在训练循环中,忘记对损失取平均(或求和后除以 batch size)。 | 损失值非常大,梯度爆炸,训练无法收敛。 | recon_loss 和 kl_loss 通常对批次和维度求和,总损失值随 batch size 和维度线性增长。 | 在返回总损失前,除以 batch size:total_loss = (recon_loss + kl_loss) / x.size(0),或者优化器使用较小的学习率。 |
| 对实值数据使用 BCE 损失,或对二值数据使用 MSE 损失。 | 重建质量差,损失下降缓慢。 | 损失函数与数据分布不匹配。 | 二值数据(如二值图像)用 BCE;实值数据(如标准化后的图像)用 MSE。 |
本章小结
- 自编码器通过编码-解码结构和重建损失,实现无监督的降维和特征学习。
- 变分自编码器(VAE)是一种概率生成模型,其编码器输出潜在变量的高斯分布参数(
μ,log_var)。 - VAE 的损失函数是证据下界(ELBO),包含重建损失和 KL 散度损失,后者约束潜在分布接近标准正态先验。
- 重参数化技巧(
z = μ + σ * ε)是 VAE 能够端到端训练的关键,它使采样操作可导。 - VAE 的连续潜在空间支持从先验分布生成新样本和在潜在向量间进行有意义的插值。
β-VAE通过引入超参数β来调节重建精度与潜在空间正则化强度之间的平衡。
📋 行动清单
- 尝试修改代码中
latent_dim的值(例如改为 16 或 64),重新运行损失计算,观察 KL 损失项的变化。 - 在
beta_vae_loss函数中,将beta设置为一个很大的数(如 100),观察 KL 损失如何主导总损失,思考这对mu和log_var会产生什么影响。 - (进阶)假设输入数据是标准化到
[-1, 1]的实值图像,将解码器最后的Sigmoid激活函数移除,并将重建损失改为 MSE,重新实现并运行前向传播。
延伸阅读
- Auto-Encoding Variational Bayes(Kingma & Welling, 2013,arXiv:1312.6114)—— VAE 的奠基论文。
- β-VAE: Learning Basic Visual Concepts with a Constrained Variational Framework(Higgins et al., 2017,arXiv:1606.05579)—— 引入
β参数平衡 VAE 目标。
—— 小象教研组