【深度学习精通】第16章 | 自编码器与VAE - 隐空间学习与生成
环境声明
- Python 版本:
Python 3.10+(建议使用 3.10 以上版本) - 深度学习框架:
PyTorch 2.0+ - GPU 支持:CUDA 11.8+ (推荐,用于加速训练)
- 操作系统:
Windows/macOS/Linux(通用) - 依赖库:
torch,torchvision,numpy,matplotlib,scikit-learn
学习目标和摘要
学习目标
本章将带领读者深入理解自编码器及其变体,掌握变分自编码器(VAE)的核心原理与实现。通过本章学习,你将能够:
- 理解自编码器的编码器-解码器架构及其工作原理
- 掌握降维与特征学习的核心概念
- 深入理解变分自编码器的概率图模型视角
- 掌握重参数化技巧(Reparameterization Trick)的数学原理
- 能够独立实现VAE模型并进行图像生成
- 了解条件VAE、beta-VAE等高级变体的应用场景
文章摘要
自编码器(Autoencoder)是一类重要的无监督学习模型,通过编码器-解码器架构学习数据的有效表示。本章从基础自编码器出发,逐步深入到变分自编码器(VAE),详细讲解其概率图模型基础、ELBO推导、重参数化技巧等核心内容。同时涵盖去噪自编码器、稀疏自编码器、条件VAE、beta-VAE等变体,并提供完整的PyTorch实现代码。
1. 自编码器结构与原理
1.1 什么是自编码器
自编码器(Autoencoder, AE)是一种神经网络架构,旨在学习输入数据的有效编码(表示)。它由两部分组成:
- 编码器(Encoder):将高维输入数据压缩为低维隐向量
- 解码器(Decoder):从隐向量重构原始输入数据
用一个形象的比喻:想象你是一位画家,编码器就像是你观察风景后用寥寥数笔记录下关键特征的过程,而解码器则是你根据这些简笔画重新绘制出完整风景的能力。
1.2 编码器-解码器架构
自编码器的数学表达如下:
编码过程:
z=fθ(x)=σ(Wex+be)z = f_{\theta}(x) = \sigma(W_e x + b_e)z=fθ(x)=σ(Wex+be)
其中,x∈Rdx \in \mathbb{R}^dx∈Rd 是输入数据,z∈Rkz \in \mathbb{R}^kz∈Rk 是隐向量(通常 k≪dk \ll dk≪d),fθf_{\theta}fθ 是编码器函数。
解码过程:
x^=gϕ(z)=σ(Wdz+bd)\hat{x} = g_{\phi}(z) = \sigma(W_d z + b_d)x^=gϕ(z)=σ(Wdz+bd)
其中,x^\hat{x}x^ 是重构输出,gϕg_{\phi}gϕ 是解码器函数。
目标函数:
L(θ,ϕ)=1N∑i=1N∥xi−x^i∥2\mathcal{L}(\theta, \phi) = \frac{1}{N} \sum_{i=1}^{N} \|x_i - \hat{x}_i\|^2L(θ,ϕ)=N1i=1∑N∥xi−x^i∥2
这是一个均方误差(MSE)损失,衡量重构质量。
1.3 基础自编码器实现
import torch
import torch.nn as nn
import torch.nn.functional as F
class Autoencoder(nn.Module):
"""基础自编码器实现"""
def __init__(self, input_dim=784, hidden_dim=256, latent_dim=64):
super(Autoencoder, self).__init__()
# 编码器
self.encoder = nn.Sequential(
nn.Linear(input_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, latent_dim),
nn.ReLU()
)
# 解码器
self.decoder = nn.Sequential(
nn.Linear(latent_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, input_dim),
nn.Sigmoid() # 输出归一化到[0,1]
)
def forward(self, x):
# 展平输入
x = x.view(x.size(0), -1)
# 编码
z = self.encoder(x)
# 解码
x_recon = self.decoder(z)
return x_recon, z
# 训练函数
def train_autoencoder(model, dataloader, epochs=10, lr=1e-3, device='cuda'):
model = model.to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
criterion = nn.MSELoss()
for epoch in range(epochs):
total_loss = 0
for batch_idx, (data, _) in enumerate(dataloader):
data = data.to(device)
optimizer.zero_grad()
recon, z = model(data)
loss = criterion(recon, data.view(data.size(0), -1))
loss.backward()
optimizer.step()
total_loss += loss.item()
avg_loss = total_loss / len(dataloader)
print(f'Epoch [{epoch+1}/{epochs}], Loss: {avg_loss:.4f}')
2. 降维与特征学习
2.1 PCA与自编码器对比
主成分分析(PCA)是经典的线性降维方法。自编码器可以看作是非线性的PCA推广。
| 特性 | PCA | 自编码器 |
|---|---|---|
| 线性/非线性 | 线性变换 | 非线性变换(激活函数) |
| 激活函数 | 无 | ReLU, Sigmoid等 |
| 可学习参数 | 解析解(特征分解) | 梯度下降优化 |
| 表达能力 | 受限(线性子空间) | 强大(非线性流形) |
| 计算复杂度 | O(d^3) | O(N * d * h) |
| 适用场景 | 线性相关数据 | 复杂非线性数据 |
2.2 特征学习的本质
自编码器学习到的隐向量 zzz 捕获了输入数据的核心特征。这些特征具有以下性质:
- 压缩性:维度远低于原始数据
- 代表性:包含重构所需的关键信息
- 连续性:相似输入映射到相近的隐向量
2.3 隐空间可视化
import matplotlib.pyplot as plt
from sklearn.manifold import TSNE
def visualize_latent_space(model, dataloader, device='cuda'):
"""使用t-SNE可视化隐空间"""
model.eval()
latent_vectors = []
labels_list = []
with torch.no_grad():
for data, labels in dataloader:
data = data.to(device)
_, z = model(data)
latent_vectors.append(z.cpu().numpy())
labels_list.append(labels.numpy())
latent_vectors = np.concatenate(latent_vectors, axis=0)
labels_list = np.concatenate(labels_list, axis=0)
# t-SNE降维到2D
tsne = TSNE(n_components=2, random_state=42)
latent_2d = tsne.fit_transform(latent_vectors)
# 绘制散点图
plt.figure(figsize=(10, 8))
scatter = plt.scatter(latent_2d[:, 0], latent_2d[:, 1],
c=labels_list, cmap='tab10', alpha=0.6)
plt.colorbar(scatter)
plt.title('Latent Space Visualization (t-SNE)')
plt.xlabel('Dimension 1')
plt.ylabel('Dimension 2')
plt.show()
3. 去噪自编码器与稀疏自编码器
3.1 去噪自编码器(Denoising Autoencoder)
去噪自编码器的核心思想:训练模型从被污染的输入中恢复干净数据。
训练过程:
- 对输入 xxx 添加噪声得到 x~\tilde{x}x~
- 编码器处理 x~\tilde{x}x~ 得到 zzz
- 解码器从 zzz 重构原始 xxx
数学表达:
L=∥x−gϕ(fθ(x~))∥2\mathcal{L} = \|x - g_{\phi}(f_{\theta}(\tilde{x}))\|^2L=∥x−gϕ(fθ(x~))∥2
噪声类型:
- 高斯噪声:x~=x+ϵ,ϵ∼N(0,σ2)\tilde{x} = x + \epsilon, \epsilon \sim \mathcal{N}(0, \sigma^2)x~=x+ϵ,ϵ∼N(0,σ2)
- 掩码噪声:随机将部分像素置零
class DenoisingAutoencoder(nn.Module):
"""去噪自编码器"""
def __init__(self, input_dim=784, hidden_dim=256, latent_dim=64, noise_factor=0.3):
super(DenoisingAutoencoder, self).__init__()
self.noise_factor = noise_factor
self.encoder = nn.Sequential(
nn.Linear(input_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, latent_dim),
nn.ReLU()
)
self.decoder = nn.Sequential(
nn.Linear(latent_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, input_dim),
nn.Sigmoid()
)
def add_noise(self, x):
"""添加高斯噪声"""
noise = torch.randn_like(x) * self.noise_factor
return torch.clamp(x + noise, 0., 1.)
def forward(self, x):
x_flat = x.view(x.size(0), -1)
# 添加噪声(仅在训练时)
if self.training:
x_noisy = self.add_noise(x_flat)
else:
x_noisy = x_flat
z = self.encoder(x_noisy)
x_recon = self.decoder(z)
return x_recon, z
3.2 稀疏自编码器(Sparse Autoencoder)
稀疏自编码器通过引入稀疏性约束,使隐层神经元只在特定输入下激活。
稀疏性约束:
L=∥x−x^∥2+β⋅KL(ρ∥ρ^)\mathcal{L} = \|x - \hat{x}\|^2 + \beta \cdot \text{KL}(\rho \| \hat{\rho})L=∥x−x^∥2+β⋅KL(ρ∥ρ^)
其中:
- ρ\rhoρ:目标稀疏度(如0.05)
- ρ^j=1N∑i=1Nzj(i)\hat{\rho}_j = \frac{1}{N} \sum_{i=1}^{N} z_j^{(i)}ρ^j=N1∑i=1Nzj(i):神经元 jjj 的平均激活
- KL散度:KL(ρ∥ρ^)=ρlogρρ^+(1−ρ)log1−ρ1−ρ^\text{KL}(\rho \| \hat{\rho}) = \rho \log\frac{\rho}{\hat{\rho}} + (1-\rho)\log\frac{1-\rho}{1-\hat{\rho}}KL(ρ∥ρ^)=ρlogρ^ρ+(1−ρ)log1−ρ^1−ρ
class SparseAutoencoder(nn.Module):
"""稀疏自编码器"""
def __init__(self, input_dim=784, hidden_dim=256, latent_dim=64,
sparsity_param=0.05, beta=3):
super(SparseAutoencoder, self).__init__()
self.sparsity_param = sparsity_param
self.beta = beta
self.encoder = nn.Sequential(
nn.Linear(input_dim, latent_dim),
nn.Sigmoid() # 使用Sigmoid确保输出在[0,1]
)
self.decoder = nn.Sequential(
nn.Linear(latent_dim, input_dim),
nn.Sigmoid()
)
def kl_divergence(self, rho, rho_hat):
"""计算KL散度"""
rho_hat = torch.clamp(rho_hat, 1e-10, 1-1e-10)
return rho * torch.log(rho / rho_hat) + (1 - rho) * torch.log((1 - rho) / (1 - rho_hat))
def forward(self, x):
x_flat = x.view(x.size(0), -1)
z = self.encoder(x_flat)
x_recon = self.decoder(z)
return x_recon, z
def sparse_loss(self, z):
"""计算稀疏性损失"""
rho_hat = torch.mean(z, dim=0)
kl_div = self.kl_divergence(self.sparsity_param, rho_hat)
return self.beta * torch.sum(kl_div)
4. 变分自编码器原理
4.1 从确定性到概率性
传统自编码器的隐向量 zzz 是确定性的。变分自编码器(VAE)将 zzz 视为随机变量,学习其后验分布 qϕ(z∣x)q_{\phi}(z|x)qϕ(z∣x)。
核心思想:
- 编码器输出隐变量的分布参数(均值 μ\muμ 和方差 σ2\sigma^2σ2)
- 从该分布采样得到 zzz
- 解码器从 zzz 重构输入
4.2 概率图模型视角
VAE的生成过程可以描述为:
- 从先验分布采样隐变量:z∼p(z)z \sim p(z)z∼p(z)
- 从似然分布生成数据:x∼pθ(x∣z)x \sim p_{\theta}(x|z)x∼pθ(x∣z)
我们的目标是学习模型参数 θ\thetaθ,使得边缘似然 pθ(x)=∫pθ(x∣z)p(z)dzp_{\theta}(x) = \int p_{\theta}(x|z)p(z)dzpθ(x)=∫pθ(x∣z)p(z)dz 最大化。
问题:直接计算边缘似然需要积分,计算困难。
解决方案:引入变分推断,使用近似后验 qϕ(z∣x)q_{\phi}(z|x)qϕ(z∣x) 逼近真实后验 pθ(z∣x)p_{\theta}(z|x)pθ(z∣x)。
4.3 ELBO推导
证据下界(Evidence Lower BOund, ELBO):
L(θ,ϕ;x)=Eqϕ(z∣x)[logpθ(x∣z)]−KL(qϕ(z∣x)∥p(z))\mathcal{L}(\theta, \phi; x) = \mathbb{E}_{q_{\phi}(z|x)}[\log p_{\theta}(x|z)] - \text{KL}(q_{\phi}(z|x) \| p(z))L(θ,ϕ;x)=Eqϕ(z∣x)[logpθ(x∣z)]−KL(qϕ(z∣x)∥p(z))
推导过程:
logpθ(x)=log∫pθ(x∣z)p(z)dz\log p_{\theta}(x) = \log \int p_{\theta}(x|z)p(z)dzlogpθ(x)=log∫pθ(x∣z)p(z)dz
引入变分分布 qϕ(z∣x)q_{\phi}(z|x)qϕ(z∣x):
=log∫qϕ(z∣x)pθ(x∣z)p(z)qϕ(z∣x)dz= \log \int q_{\phi}(z|x) \frac{p_{\theta}(x|z)p(z)}{q_{\phi}(z|x)} dz=log∫qϕ(z∣x)qϕ(z∣x)pθ(x∣z)p(z)dz
=logEqϕ(z∣x)[pθ(x∣z)p(z)qϕ(z∣x)]= \log \mathbb{E}_{q_{\phi}(z|x)}\left[\frac{p_{\theta}(x|z)p(z)}{q_{\phi}(z|x)}\right]=logEqϕ(z∣x)[qϕ(z∣x)pθ(x∣z)p(z)]
应用Jensen不等式:
≥Eqϕ(z∣x)[logpθ(x∣z)p(z)qϕ(z∣x)]\geq \mathbb{E}_{q_{\phi}(z|x)}\left[\log \frac{p_{\theta}(x|z)p(z)}{q_{\phi}(z|x)}\right]≥Eqϕ(z∣x)[logqϕ(z∣x)pθ(x∣z)p(z)]
=Eqϕ(z∣x)[logpθ(x∣z)]+Eqϕ(z∣x)[logp(z)qϕ(z∣x)]= \mathbb{E}_{q_{\phi}(z|x)}[\log p_{\theta}(x|z)] + \mathbb{E}_{q_{\phi}(z|x)}\left[\log \frac{p(z)}{q_{\phi}(z|x)}\right]=Eqϕ(z∣x)[logpθ(x∣z)]+Eqϕ(z∣x)[logqϕ(z∣x)p(z)]
=Eqϕ(z∣x)[logpθ(x∣z)]−KL(qϕ(z∣x)∥p(z))= \mathbb{E}_{q_{\phi}(z|x)}[\log p_{\theta}(x|z)] - \text{KL}(q_{\phi}(z|x) \| p(z))=Eqϕ(z∣x)[logpθ(x∣z)]−KL(qϕ(z∣x)∥p(z))
ELBO的两项含义:
- 重构项:Eqϕ(z∣x)[logpθ(x∣z)]\mathbb{E}_{q_{\phi}(z|x)}[\log p_{\theta}(x|z)]Eqϕ(z∣x)[logpθ(x∣z)] - 衡量重构质量
- KL散度项:KL(qϕ(z∣x)∥p(z))\text{KL}(q_{\phi}(z|x) \| p(z))KL(qϕ(z∣x)∥p(z)) - 使近似后验接近先验
5. 重参数化技巧
5.1 梯度传播问题
在VAE中,我们需要从 qϕ(z∣x)q_{\phi}(z|x)qϕ(z∣x) 采样 zzz。但采样操作是不可导的,这阻碍了梯度传播。
解决方案:重参数化技巧(Reparameterization Trick)
5.2 重参数化技巧原理
假设 qϕ(z∣x)=N(z;μ,σ2)q_{\phi}(z|x) = \mathcal{N}(z; \mu, \sigma^2)qϕ(z∣x)=N(z;μ,σ2),我们可以将 zzz 表示为:
z=μ+σ⊙ϵ,ϵ∼N(0,I)z = \mu + \sigma \odot \epsilon, \quad \epsilon \sim \mathcal{N}(0, I)z=μ+σ⊙ϵ,ϵ∼N(0,I)
这样,随机性来自与参数无关的噪声 ϵ\epsilonϵ,而 μ\muμ 和 σ\sigmaσ 成为确定性变换的参数,可以正常计算梯度。
5.3 数学证明
原始采样:
z∼N(μ,σ2)z \sim \mathcal{N}(\mu, \sigma^2)z∼N(μ,σ2)
重参数化后:
z=g(μ,σ,ϵ)=μ+σϵ,ϵ∼N(0,I)z = g(\mu, \sigma, \epsilon) = \mu + \sigma \epsilon, \quad \epsilon \sim \mathcal{N}(0, I)z=g(μ,σ,ϵ)=μ+σϵ,ϵ∼N(0,I)
验证分布等价性:
E[z]=E[μ+σϵ]=μ+σE[ϵ]=μ\mathbb{E}[z] = \mathbb{E}[\mu + \sigma \epsilon] = \mu + \sigma \mathbb{E}[\epsilon] = \muE[z]=E[μ+σϵ]=μ+σE[ϵ]=μ
Var[z]=Var[μ+σϵ]=σ2Var[ϵ]=σ2\text{Var}[z] = \text{Var}[\mu + \sigma \epsilon] = \sigma^2 \text{Var}[\epsilon] = \sigma^2Var[z]=Var[μ+σϵ]=σ2Var[ϵ]=σ2
因此,zzz 服从 N(μ,σ2)\mathcal{N}(\mu, \sigma^2)N(μ,σ2)。
5.4 代码实现
def reparameterize(mu, log_var):
"""
重参数化技巧
mu: 均值 [batch_size, latent_dim]
log_var: 对数方差 [batch_size, latent_dim]
"""
std = torch.exp(0.5 * log_var) # 标准差
eps = torch.randn_like(std) # 从标准正态分布采样
return mu + std * eps
6. VAE的损失函数与训练
6.1 KL散度计算
假设先验 p(z)=N(0,I)p(z) = \mathcal{N}(0, I)p(z)=N(0,I),近似后验 qϕ(z∣x)=N(μ,σ2)q_{\phi}(z|x) = \mathcal{N}(\mu, \sigma^2)qϕ(z∣x)=N(μ,σ2)。
两个高斯分布的KL散度有解析解:
KL(qϕ(z∣x)∥p(z))=−12∑j=1J(1+log(σj2)−μj2−σj2)\text{KL}(q_{\phi}(z|x) \| p(z)) = -\frac{1}{2} \sum_{j=1}^{J} \left(1 + \log(\sigma_j^2) - \mu_j^2 - \sigma_j^2\right)KL(qϕ(z∣x)∥p(z))=−21j=1∑J(1+log(σj2)−μj2−σj2)
推导:
对于一维高斯分布:
KL(N(μ,σ2)∥N(0,1))=∫q(z)logq(z)p(z)dz\text{KL}(\mathcal{N}(\mu, \sigma^2) \| \mathcal{N}(0, 1)) = \int q(z) \log \frac{q(z)}{p(z)} dzKL(N(μ,σ2)∥N(0,1))=∫q(z)logp(z)q(z)dz
=∫q(z)logq(z)dz−∫q(z)logp(z)dz= \int q(z) \log q(z) dz - \int q(z) \log p(z) dz=∫q(z)logq(z)dz−∫q(z)logp(z)dz
=−12(1+log(2πσ2))−∫q(z)(−12log(2π)−z22)dz= -\frac{1}{2}(1 + \log(2\pi\sigma^2)) - \int q(z) \left(-\frac{1}{2}\log(2\pi) - \frac{z^2}{2}\right) dz=−21(1+log(2πσ2))−∫q(z)(−21log(2π)−2z2)dz
=−12(1+log(2πσ2))+12log(2π)+12E[z2]= -\frac{1}{2}(1 + \log(2\pi\sigma^2)) + \frac{1}{2}\log(2\pi) + \frac{1}{2}\mathbb{E}[z^2]=−21(1+log(2πσ2))+21log(2π)+21E[z2]
=−12(1+log(σ2))+12(μ2+σ2)= -\frac{1}{2}(1 + \log(\sigma^2)) + \frac{1}{2}(\mu^2 + \sigma^2)=−21(1+log(σ2))+21(μ2+σ2)
=−12(1+log(σ2)−μ2−σ2)= -\frac{1}{2}(1 + \log(\sigma^2) - \mu^2 - \sigma^2)=−21(1+log(σ2)−μ2−σ2)
6.2 完整VAE实现
import torch
import torch.nn as nn
import torch.nn.functional as F
class VAE(nn.Module):
"""变分自编码器完整实现"""
def __init__(self, input_dim=784, hidden_dim=400, latent_dim=20):
super(VAE, self).__init__()
self.latent_dim = latent_dim
# 编码器
self.fc1 = nn.Linear(input_dim, hidden_dim)
self.fc_mu = nn.Linear(hidden_dim, latent_dim)
self.fc_logvar = nn.Linear(hidden_dim, latent_dim)
# 解码器
self.fc3 = nn.Linear(latent_dim, hidden_dim)
self.fc4 = nn.Linear(hidden_dim, input_dim)
def encode(self, x):
"""编码:输出分布参数"""
h = F.relu(self.fc1(x))
mu = self.fc_mu(h)
log_var = self.fc_logvar(h)
return mu, log_var
def reparameterize(self, mu, log_var):
"""重参数化采样"""
std = torch.exp(0.5 * log_var)
eps = torch.randn_like(std)
return mu + std * eps
def decode(self, z):
"""解码:从隐变量重构"""
h = F.relu(self.fc3(z))
return torch.sigmoid(self.fc4(h))
def forward(self, x):
"""前向传播"""
x_flat = x.view(x.size(0), -1)
mu, log_var = self.encode(x_flat)
z = self.reparameterize(mu, log_var)
x_recon = self.decode(z)
return x_recon, mu, log_var
def loss_function(self, x_recon, x, mu, log_var):
"""
VAE损失函数 = 重构损失 + KL散度
"""
# 重构损失(二元交叉熵)
x_flat = x.view(x.size(0), -1)
BCE = F.binary_cross_entropy(x_recon, x_flat, reduction='sum')
# KL散度
KLD = -0.5 * torch.sum(1 + log_var - mu.pow(2) - log_var.exp())
return BCE + KLD
# 训练函数
def train_vae(model, dataloader, epochs=20, lr=1e-3, device='cuda'):
model = model.to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
for epoch in range(epochs):
model.train()
train_loss = 0
for batch_idx, (data, _) in enumerate(dataloader):
data = data.to(device)
optimizer.zero_grad()
recon_batch, mu, log_var = model(data)
loss = model.loss_function(recon_batch, data, mu, log_var)
loss.backward()
train_loss += loss.item()
optimizer.step()
avg_loss = train_loss / len(dataloader.dataset)
print(f'Epoch [{epoch+1}/{epochs}], Average Loss: {avg_loss:.4f}')
6.3 图像生成
def generate_images(model, num_images=16, device='cuda'):
"""从先验分布采样生成新图像"""
model.eval()
with torch.no_grad():
# 从标准正态分布采样隐向量
z = torch.randn(num_images, model.latent_dim).to(device)
# 解码生成图像
samples = model.decode(z).cpu()
# 重塑为图像格式
samples = samples.view(num_images, 1, 28, 28)
return samples
def interpolate(model, x1, x2, num_steps=10, device='cuda'):
"""在隐空间中进行插值"""
model.eval()
with torch.no_grad():
# 编码得到隐向量
mu1, _ = model.encode(x1.view(1, -1).to(device))
mu2, _ = model.encode(x2.view(1, -1).to(device))
# 线性插值
alphas = torch.linspace(0, 1, num_steps).to(device)
interpolated = []
for alpha in alphas:
z = (1 - alpha) * mu1 + alpha * mu2
recon = model.decode(z).cpu()
interpolated.append(recon.view(1, 28, 28))
return torch.stack(interpolated)
7. 条件VAE与beta-VAE
7.1 条件VAE(Conditional VAE)
条件VAE在生成过程中引入条件信息 yyy(如类别标签),实现有控制的生成。
模型结构:
- 编码器:qϕ(z∣x,y)q_{\phi}(z|x, y)qϕ(z∣x,y)
- 解码器:pθ(x∣z,y)p_{\theta}(x|z, y)pθ(x∣z,y)
条件ELBO:
L=Eqϕ(z∣x,y)[logpθ(x∣z,y)]−KL(qϕ(z∣x,y)∥p(z∣y))\mathcal{L} = \mathbb{E}_{q_{\phi}(z|x,y)}[\log p_{\theta}(x|z,y)] - \text{KL}(q_{\phi}(z|x,y) \| p(z|y))L=Eqϕ(z∣x,y)[logpθ(x∣z,y)]−KL(qϕ(z∣x,y)∥p(z∣y))
class ConditionalVAE(nn.Module):
"""条件变分自编码器"""
def __init__(self, input_dim=784, hidden_dim=400, latent_dim=20, num_classes=10):
super(ConditionalVAE, self).__init__()
self.latent_dim = latent_dim
self.num_classes = num_classes
# 编码器(输入拼接类别one-hot)
self.fc1 = nn.Linear(input_dim + num_classes, hidden_dim)
self.fc_mu = nn.Linear(hidden_dim, latent_dim)
self.fc_logvar = nn.Linear(hidden_dim, latent_dim)
# 解码器(输入拼接类别one-hot)
self.fc3 = nn.Linear(latent_dim + num_classes, hidden_dim)
self.fc4 = nn.Linear(hidden_dim, input_dim)
def encode(self, x, y):
"""编码"""
y_onehot = F.one_hot(y, self.num_classes).float()
x_cond = torch.cat([x, y_onehot], dim=1)
h = F.relu(self.fc1(x_cond))
mu = self.fc_mu(h)
log_var = self.fc_logvar(h)
return mu, log_var
def decode(self, z, y):
"""解码"""
y_onehot = F.one_hot(y, self.num_classes).float()
z_cond = torch.cat([z, y_onehot], dim=1)
h = F.relu(self.fc3(z_cond))
return torch.sigmoid(self.fc4(h))
def forward(self, x, y):
x_flat = x.view(x.size(0), -1)
mu, log_var = self.encode(x_flat, y)
z = self.reparameterize(mu, log_var)
x_recon = self.decode(z, y)
return x_recon, mu, log_var
def reparameterize(self, mu, log_var):
std = torch.exp(0.5 * log_var)
eps = torch.randn_like(std)
return mu + std * eps
def generate(self, y, num_samples=1):
"""根据类别生成图像"""
z = torch.randn(num_samples, self.latent_dim).to(y.device)
y = y.repeat(num_samples)
return self.decode(z, y)
7.2 beta-VAE
beta-VAE通过引入超参数 β\betaβ 控制KL散度项的权重,学习更加解耦(disentangled)的表示。
beta-VAE损失函数:
Lβ=Eqϕ(z∣x)[logpθ(x∣z)]−β⋅KL(qϕ(z∣x)∥p(z))\mathcal{L}_{\beta} = \mathbb{E}_{q_{\phi}(z|x)}[\log p_{\theta}(x|z)] - \beta \cdot \text{KL}(q_{\phi}(z|x) \| p(z))Lβ=Eqϕ(z∣x)[logpθ(x∣z)]−β⋅KL(qϕ(z∣x)∥p(z))
当 β>1\beta > 1β>1 时,模型更倾向于学习独立的隐变量,每个维度控制数据的一个独立变化因素。
class BetaVAE(nn.Module):
"""Beta-VAE实现"""
def __init__(self, input_dim=784, hidden_dim=400, latent_dim=20, beta=4.0):
super(BetaVAE, self).__init__()
self.latent_dim = latent_dim
self.beta = beta # 控制解耦程度
# 编码器
self.encoder = nn.Sequential(
nn.Linear(input_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU()
)
self.fc_mu = nn.Linear(hidden_dim, latent_dim)
self.fc_logvar = nn.Linear(hidden_dim, latent_dim)
# 解码器
self.decoder = nn.Sequential(
nn.Linear(latent_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, input_dim),
nn.Sigmoid()
)
def encode(self, x):
h = self.encoder(x)
return self.fc_mu(h), self.fc_logvar(h)
def reparameterize(self, mu, log_var):
std = torch.exp(0.5 * log_var)
eps = torch.randn_like(std)
return mu + std * eps
def decode(self, z):
return self.decoder(z)
def forward(self, x):
x_flat = x.view(x.size(0), -1)
mu, log_var = self.encode(x_flat)
z = self.reparameterize(mu, log_var)
x_recon = self.decode(z)
return x_recon, mu, log_var
def loss_function(self, x_recon, x, mu, log_var):
"""beta-VAE损失"""
x_flat = x.view(x.size(0), -1)
# 重构损失
recon_loss = F.binary_cross_entropy(x_recon, x_flat, reduction='sum')
# KL散度(带beta权重)
kld = -0.5 * torch.sum(1 + log_var - mu.pow(2) - log_var.exp())
return recon_loss + self.beta * kld
8. VAE变体对比
| 变体 | 核心特点 | 损失函数 | 应用场景 |
|---|---|---|---|
| 基础VAE | 概率隐变量,重参数化 | ELBO = 重构 + KL | 图像生成,降维 |
| 条件VAE | 引入条件信息 | 条件ELBO | 可控生成,类别指定 |
| beta-VAE | 可调节KL权重 | ELBO + beta*KL | 解耦表示学习 |
| VQ-VAE | 离散隐空间,向量量化 | 重构 + 码本损失 | 高分辨率图像生成 |
| VAE-GAN | 结合GAN判别器 | ELBO + 对抗损失 | 高质量图像生成 |
| 去噪VAE | 鲁棒性训练 | 去噪ELBO | 噪声数据,特征学习 |
9. VAE与VQ-VAE、VAE-GAN
9.1 VQ-VAE简介
VQ-VAE(Vector Quantized VAE)使用离散的隐空间表示,通过向量量化将连续编码映射到最近的离散码本向量。
核心区别:
- VAE:隐变量是连续的实数向量
- VQ-VAE:隐变量是从固定码本中选取的离散编码
VQ-VAE优势:
- 离散表示更适合序列生成任务
- 生成图像更清晰锐利
- 为后续自回归模型(如PixelCNN)提供良好基础
9.2 VAE-GAN简介
VAE-GAN结合了VAE的隐空间结构和GAN的判别能力。
架构特点:
- 编码器:学习数据到隐空间的映射
- 生成器(解码器):从隐空间生成图像
- 判别器:区分真实图像和生成图像
损失函数:
L=LVAE+λ⋅LGAN\mathcal{L} = \mathcal{L}_{\text{VAE}} + \lambda \cdot \mathcal{L}_{\text{GAN}}L=LVAE+λ⋅LGAN
这种结合既保留了VAE的隐空间插值能力,又提升了生成图像的质量。
10. 避坑小贴士
10.1 数值稳定性问题
问题:KL散度计算中出现NaN
原因:log_var.exp()可能溢出
解决方案:
# 使用log_var时添加限制
log_var = torch.clamp(log_var, min=-10, max=10)
# 或使用softplus代替exp
std = F.softplus(log_var) + 1e-4
10.2 重构质量差
可能原因:
- KL散度权重过大,隐空间信息丢失
- 隐维度设置过小
- 网络容量不足
解决方案:
- 调整beta参数(beta-VAE)
- 增加latent_dim
- 增加网络深度或宽度
- 使用卷积层代替全连接层
10.3 后验坍塌(Posterior Collapse)
现象:解码器忽略隐变量,KL散度趋近于0
解决方案:
- 使用KL退火:逐渐增加KL权重
- 使用Free Bits技术:设置KL下限
- 减少解码器容量
# KL退火实现
def get_kl_weight(epoch, warmup_epochs=10):
if epoch < warmup_epochs:
return epoch / warmup_epochs
return 1.0
10.4 采样质量不高
改进建议:
- 使用更好的先验(如流模型)
- 增加采样时的温度参数调节
- 使用重要性采样改进ELBO估计
11. 本章小结和知识点回顾
核心概念总结
一句话总结:自编码器通过编码器-解码器架构学习数据压缩表示,而VAE通过概率建模和重参数化技巧实现了隐空间的连续采样和高质量生成。
知识点回顾
-
自编码器基础
- 编码器-解码器架构
- 重构损失优化
- 隐空间学习
-
自编码器变体
- 去噪自编码器:鲁棒特征学习
- 稀疏自编码器:稀疏表示
- 卷积自编码器:图像数据
-
变分自编码器核心
- 概率图模型视角
- ELBO推导与优化
- 重参数化技巧
- KL散度解析解
-
高级VAE变体
- 条件VAE:可控生成
- beta-VAE:解耦表示
- VQ-VAE:离散隐空间
- VAE-GAN:高质量生成
数学公式速查
- ELBO:L=E[logp(x∣z)]−KL(q(z∣x)∥p(z))\mathcal{L} = \mathbb{E}[\log p(x|z)] - \text{KL}(q(z|x) \| p(z))L=E[logp(x∣z)]−KL(q(z∣x)∥p(z))
- 重参数化:z=μ+σ⊙ϵz = \mu + \sigma \odot \epsilonz=μ+σ⊙ϵ
- KL散度:KL=−12∑(1+logσ2−μ2−σ2)\text{KL} = -\frac{1}{2}\sum(1 + \log\sigma^2 - \mu^2 - \sigma^2)KL=−21∑(1+logσ2−μ2−σ2)
- beta-VAE损失:L=重构+β⋅KL\mathcal{L} = \text{重构} + \beta \cdot \text{KL}L=重构+β⋅KL
参考文献与扩展阅读
- Kingma, D. P., & Welling, M. (2013). Auto-Encoding Variational Bayes. ICLR 2014.
- Doersch, C. (2016). Tutorial on Variational Autoencoders. arXiv:1606.05908.
- Higgins, I., et al. (2017). beta-VAE: Learning Basic Visual Concepts with a Constrained Variational Framework. ICLR 2017.
- Van Den Oord, A., & Vinyals, O. (2017). Neural Discrete Representation Learning. NeurIPS 2017.
- Larsen, A. B. L., et al. (2016). Autoencoding beyond pixels using a learned similarity metric. ICML 2016.
本文档是《深度学习精通》系列教程的一部分,转载请注明出处。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)