从‘满月到弦月’:用PyTorch Lightning轻松复现李宏毅VAE课堂Demo
从‘满月到弦月’:用PyTorch Lightning轻松复现李宏毅VAE课堂Demo
在机器学习领域,变分自编码器(VAE)一直以其优雅的数学理论和强大的生成能力吸引着研究者的目光。但对于初学者来说,那些复杂的概率公式和抽象概念往往让人望而生畏。今天,我们将通过一个生动有趣的"月亮相位变化"案例,用PyTorch Lightning框架带你轻松入门VAE的世界。
1. 为什么选择月亮作为教学案例?
月亮相位变化是一个绝佳的VAE可视化案例。想象一下,从满月到弦月的渐变过程,就像在隐空间(latent space)中平滑移动。这种直观的变化能帮助我们理解VAE最核心的两个特性:
- 连续隐空间 :不同于传统自编码器的离散编码,VAE的隐变量服从高斯分布,允许我们在编码之间平滑插值
- 生成新数据 :VAE能够产生训练集中不存在但合理的样本,比如介于满月和弦月之间的"四分之三月"
李宏毅教授在他的经典课程中使用这个例子时,特别强调了VAE与普通自编码器的关键区别:当你在普通AE的隐空间中间点采样时,可能会得到毫无意义的噪声;而VAE经过特殊设计,能确保隐空间的每个点都对应有意义的输出。
2. 环境准备与数据构建
2.1 安装必要依赖
pip install torch pytorch-lightning matplotlib numpy
2.2 创建简易月亮数据集
虽然可以使用MNIST等标准数据集,但为了更直观地理解VAE,我们将创建一个自定义的月亮图像数据集:
import numpy as np
import matplotlib.pyplot as plt
def generate_moon_phase(phase, size=28):
"""生成指定月相的图像"""
img = np.zeros((size, size))
radius = size // 3
center = (size//2, size//2)
# 根据月相决定显示部分
for y in range(size):
for x in range(size):
distance = np.sqrt((x-center[0])**2 + (y-center[1])**2)
if distance <= radius:
if phase == 'full': # 满月
img[y,x] = 1.0
elif phase == 'crescent': # 弦月
if x >= center[0]:
img[y,x] = 1.0
return img
# 生成示例图像
full_moon = generate_moon_phase('full')
crescent_moon = generate_moon_phase('crescent')
plt.subplot(1,2,1)
plt.imshow(full_moon, cmap='gray')
plt.title('满月')
plt.subplot(1,2,2)
plt.imshow(crescent_moon, cmap='gray')
plt.title('弦月')
plt.show()
3. VAE模型架构设计
PyTorch Lightning让模型实现变得异常简洁。我们将构建一个包含编码器、解码器和重参数化技巧的完整VAE:
import torch
import torch.nn as nn
import pytorch_lightning as pl
class MoonVAE(pl.LightningModule):
def __init__(self, latent_dim=2):
super().__init__()
# 编码器网络
self.encoder = nn.Sequential(
nn.Linear(28*28, 128),
nn.ReLU(),
nn.Linear(128, 64),
nn.ReLU(),
nn.Linear(64, latent_dim*2) # 输出均值和对数方差
)
# 解码器网络
self.decoder = nn.Sequential(
nn.Linear(latent_dim, 64),
nn.ReLU(),
nn.Linear(64, 128),
nn.ReLU(),
nn.Linear(128, 28*28),
nn.Sigmoid() # 输出在0-1之间
)
self.latent_dim = latent_dim
def reparameterize(self, mu, logvar):
"""重参数化技巧"""
std = torch.exp(0.5*logvar)
eps = torch.randn_like(std)
return mu + eps*std
def forward(self, x):
# 编码过程
h = self.encoder(x.view(-1, 28*28))
mu, logvar = torch.chunk(h, 2, dim=1)
# 重参数化
z = self.reparameterize(mu, logvar)
# 解码过程
return self.decoder(z), mu, logvar
def training_step(self, batch, batch_idx):
x, _ = batch
recon_x, mu, logvar = self(x)
# 重构损失
recon_loss = nn.functional.binary_cross_entropy(
recon_x, x.view(-1, 28*28), reduction='sum')
# KL散度
kl_div = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
# 总损失
loss = recon_loss + kl_div
self.log('train_loss', loss)
return loss
def configure_optimizers(self):
return torch.optim.Adam(self.parameters(), lr=1e-3)
4. 训练与可视化
4.1 数据加载与训练
from torch.utils.data import Dataset, DataLoader
class MoonDataset(Dataset):
def __init__(self, num_samples=1000):
self.data = []
for _ in range(num_samples):
# 随机生成满月或弦月
phase = 'full' if torch.rand(1) > 0.5 else 'crescent'
img = generate_moon_phase(phase)
self.data.append((torch.FloatTensor(img), 0)) # 标签不重要
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
return self.data[idx]
# 准备数据
dataset = MoonDataset()
dataloader = DataLoader(dataset, batch_size=32, shuffle=True)
# 训练模型
model = MoonVAE(latent_dim=2)
trainer = pl.Trainer(max_epochs=50)
trainer.fit(model, dataloader)
4.2 隐空间可视化
训练完成后,我们可以可视化隐空间的分布:
def plot_latent_space(model, dataloader):
mus = []
labels = []
for x, _ in dataloader:
h = model.encoder(x.view(-1, 28*28))
mu, _ = torch.chunk(h, 2, dim=1)
mus.append(mu)
labels.append(x[:,14,14] > 0.5) # 简单判断是满月还是弦月
mus = torch.cat(mus).detach().numpy()
labels = torch.cat(labels).detach().numpy()
plt.figure(figsize=(10,8))
plt.scatter(mus[:,0], mus[:,1], c=labels, cmap='viridis')
plt.colorbar()
plt.xlabel('z1')
plt.ylabel('z2')
plt.title('隐空间分布')
plt.show()
plot_latent_space(model, dataloader)
4.3 生成月相渐变
最激动人心的部分是观察VAE如何在隐空间中生成月相渐变:
def generate_moon_interpolation(model, z1, z2, steps=10):
"""在两个隐变量之间生成渐变序列"""
interpolations = []
for alpha in np.linspace(0, 1, steps):
z = alpha * z1 + (1-alpha) * z2
with torch.no_grad():
moon = model.decoder(torch.FloatTensor(z)).view(28,28).numpy()
interpolations.append(moon)
plt.figure(figsize=(15,3))
for i, img in enumerate(interpolations):
plt.subplot(1, len(interpolations), i+1)
plt.imshow(img, cmap='gray')
plt.axis('off')
plt.show()
# 选择两个隐变量点
z_full = torch.FloatTensor([1.5, 0.5]) # 满月区域
z_crescent = torch.FloatTensor([-1.0, -0.5]) # 弦月区域
generate_moon_interpolation(model, z_full, z_crescent)
5. 关键概念深入解析
5.1 重参数化技巧为什么重要?
VAE面临的一个核心问题是:如何通过随机采样操作进行反向传播?重参数化技巧提供了优雅的解决方案:
- 原始问题 :直接从N(μ,σ²)采样不可导
- 解决方案 :改为从N(0,1)采样,然后通过可导变换得到相同分布
z = μ + σ ⊙ ε, 其中ε ~ N(0,1)
这种方法既保持了随机性,又允许梯度通过μ和σ传播。
5.2 KL散度的作用
VAE损失函数中的KL散度项促使编码器产生两个效果:
- 紧凑性 :隐变量分布接近标准正态分布
- 解耦 :不同维度之间尽可能独立
我们可以通过调整KL项的权重(β-VAE)来控制这些特性的强度:
# 在training_step中修改
beta = 0.5 # 小于1减轻KL约束,大于1加强约束
loss = recon_loss + beta * kl_div
5.3 为什么选择高斯分布?
VAE通常假设隐变量服从高斯分布,这主要基于以下考虑:
- 数学便利 :高斯分布有良好的解析性质
- 通用性 :根据中心极限定理,许多复杂分布可视为高斯混合
- 连续性 :便于在隐空间中进行插值
不过近年来也有研究探索其他分布形式,如von Mises-Fisher分布等。
6. 扩展应用与进阶技巧
6.1 应用到真实月亮图像
虽然我们使用了简化的月亮图像,但相同方法可以应用于真实月相照片:
- 收集不同月相的真实照片
- 使用卷积网络改进编码器/解码器
- 增加隐空间维度捕捉更多细节
class ConvVAE(pl.LightningModule):
def __init__(self, latent_dim=32):
super().__init__()
# 卷积编码器
self.encoder = nn.Sequential(
nn.Conv2d(3, 16, 3, stride=2, padding=1),
nn.ReLU(),
nn.Conv2d(16, 32, 3, stride=2, padding=1),
nn.ReLU(),
nn.Flatten(),
nn.Linear(32*7*7, latent_dim*2)
)
# 转置卷积解码器
self.decoder = nn.Sequential(
nn.Linear(latent_dim, 32*7*7),
nn.Unflatten(1, (32,7,7)),
nn.ConvTranspose2d(32, 16, 3, stride=2, padding=1, output_padding=1),
nn.ReLU(),
nn.ConvTranspose2d(16, 3, 3, stride=2, padding=1, output_padding=1),
nn.Sigmoid()
)
6.2 条件VAE实现
如果想控制生成的月相类型,可以引入条件变量:
class ConditionalVAE(MoonVAE):
def __init__(self, latent_dim=2, num_classes=2):
super().__init__(latent_dim)
# 条件标签嵌入
self.label_embedding = nn.Embedding(num_classes, latent_dim)
def forward(self, x, labels=None):
h = self.encoder(x.view(-1, 28*28))
mu, logvar = torch.chunk(h, 2, dim=1)
z = self.reparameterize(mu, logvar)
# 加入条件信息
if labels is not None:
z = z + self.label_embedding(labels)
return self.decoder(z), mu, logvar
6.3 隐空间探索技巧
-
均匀采样 :在隐空间单位球面上均匀采样
def sample_uniform_sphere(num_samples, latent_dim): z = torch.randn(num_samples, latent_dim) z = z / torch.norm(z, dim=1, keepdim=True) return z -
属性编辑 :通过方向向量控制特定属性
# 找到从满月指向弦月的方向向量 direction = z_crescent - z_full # 沿此方向生成新样本 new_z = z_full + 0.5 * direction
7. 常见问题与调试技巧
7.1 生成的图像模糊怎么办?
VAE常被批评生成的图像比较模糊,可以尝试以下改进:
-
调整损失函数 :使用感知损失替代像素级MSE
perceptual_loss = nn.L1Loss()(vgg_features(recon_x), vgg_features(x)) -
修改模型架构 :增加网络容量或使用残差连接
-
调整KL权重 :降低β值减轻对重构质量的惩罚
7.2 隐空间没有良好解耦?
如果隐变量维度没有对应有意义的独立特征:
- 增加KL项的β值 :促进更严格的正则化
- 使用解耦正则项 :如TCVAE中的总相关项
- 延长训练时间 :解耦可能需要更长时间训练
7.3 如何处理更复杂的分布?
对于多模态分布(如同时包含太阳和月亮):
- 增加隐空间维度 :提供更多表达能力
- 使用更复杂的先验 :如混合高斯分布
- 尝试VQ-VAE :使用离散隐变量
在实际项目中,我发现2D隐空间已经足够捕捉月相变化的基本模式,但增加维度可以带来更精细的控制。训练过程中,保持KL项和重构损失的平衡是关键——过早强调KL项会导致"后验坍缩",而忽视它又会使隐空间失去良好结构。
更多推荐




所有评论(0)