生成模型入门:VAE、GAN 与扩散模型
判别模型回答「这张图是猫还是狗」,生成模型回答「生成一张猫的图片」。从 VAE 到 GAN,再到扩散模型(Stable Diffusion 背后的技术),生成式 AI 的每一步飞跃背后都有清晰的数学思想。这篇由浅入深地把三大生成模型讲清楚。
生成模型的核心问题
我们有一批真实数据(如人脸图片),想让模型学会「从噪声生成和真实数据分布一致的新样本」。
目标:学习数据的真实分布 p_data(x)
做法:训练一个生成器 G,使 G(z) 的分布尽可能接近 p_data(x)
z 是从简单分布(如高斯)采样的随机向量(隐变量)VAE:变分自编码器
VAE(Variational Autoencoder)是生成模型入门的最佳起点,思路直观:
普通 Autoencoder(用于压缩):
输入 x → Encoder → 隐向量 z → Decoder → 重建 x̂
目标:让 x̂ ≈ xVAE 的改进:隐向量不是固定点,而是一个高斯分布的均值和方差,从中采样再解码。这样隐空间连续且有规律,可以从隐空间任意采样生成新图像。
import torch
import torch.nn as nn
import torch.nn.functional as F
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
class VAE(nn.Module):
def __init__(self, input_dim=784, hidden_dim=400, latent_dim=20):
super().__init__()
# 编码器:输出均值和对数方差
self.fc1 = nn.Linear(input_dim, hidden_dim)
self.fc_mu = nn.Linear(hidden_dim, latent_dim) # 均值
self.fc_log_var = nn.Linear(hidden_dim, latent_dim) # log 方差
# 解码器
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))
return self.fc_mu(h), self.fc_log_var(h)
def reparameterize(self, mu, log_var):
"""重参数技巧:从 N(mu, sigma) 采样 = mu + sigma * N(0,1)
这样梯度可以通过采样操作反传"""
std = torch.exp(0.5 * log_var)
eps = torch.randn_like(std) # 从 N(0,1) 采样
return mu + eps * std
def decode(self, z):
h = F.relu(self.fc3(z))
return torch.sigmoid(self.fc4(h))
def forward(self, x):
mu, log_var = self.encode(x.view(-1, 784))
z = self.reparameterize(mu, log_var)
return self.decode(z), mu, log_var
def vae_loss(recon_x, x, mu, log_var):
# 重建损失:让解码结果尽量接近原图
recon_loss = F.binary_cross_entropy(recon_x, x.view(-1, 784), reduction="sum")
# KL 散度:让隐空间分布接近标准正态 N(0, 1)
kl_loss = -0.5 * torch.sum(1 + log_var - mu.pow(2) - log_var.exp())
return recon_loss + kl_loss
# 训练
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = VAE().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
train_loader = DataLoader(
datasets.MNIST("./data", train=True, download=True,
transform=transforms.ToTensor()),
batch_size=128, shuffle=True
)
for epoch in range(20):
model.train()
total_loss = 0
for X, _ in train_loader:
X = X.to(device)
optimizer.zero_grad()
recon, mu, log_var = model(X)
loss = vae_loss(recon, X, mu, log_var)
loss.backward()
optimizer.step()
total_loss += loss.item()
print(f"Epoch {epoch+1:3d} | Loss: {total_loss/len(train_loader.dataset):.4f}")
# 生成新样本
model.eval()
with torch.no_grad():
z = torch.randn(64, 20).to(device) # 从 N(0,1) 随机采样
generated = model.decode(z).view(64, 1, 28, 28)
# 保存为图片...GAN:生成对抗网络
GAN(2014 年 Ian Goodfellow 提出)用一个更直接的对抗框架:
生成器 G:接收随机噪声 z,生成「假」样本 G(z)
判别器 D:区分「真」样本 x 和「假」样本 G(z),输出真实概率
博弈过程:
D 尽力辨别真假(D(x) → 1,D(G(z)) → 0)
G 尽力欺骗 D(让 D(G(z)) → 1)
最终博弈均衡:D(G(z)) = 0.5(分不清真假)# DCGAN(深度卷积 GAN),在 MNIST 上生成手写数字
class Generator(nn.Module):
def __init__(self, latent_dim=100):
super().__init__()
self.model = nn.Sequential(
# 从隐向量扩展到特征图
nn.ConvTranspose2d(latent_dim, 256, 7, 1, 0, bias=False),
nn.BatchNorm2d(256), nn.ReLU(True),
nn.ConvTranspose2d(256, 128, 4, 2, 1, bias=False),
nn.BatchNorm2d(128), nn.ReLU(True),
nn.ConvTranspose2d(128, 1, 4, 2, 1, bias=False),
nn.Tanh(), # 输出范围 [-1, 1]
)
def forward(self, z):
return self.model(z.view(-1, 100, 1, 1))
class Discriminator(nn.Module):
def __init__(self):
super().__init__()
self.model = nn.Sequential(
nn.Conv2d(1, 128, 4, 2, 1, bias=False),
nn.LeakyReLU(0.2, inplace=True), # GAN 判别器用 LeakyReLU,不用 ReLU
nn.Conv2d(128, 256, 4, 2, 1, bias=False),
nn.BatchNorm2d(256),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(256, 1, 7, 1, 0, bias=False),
nn.Sigmoid(),
)
def forward(self, x):
return self.model(x).view(-1)
G = Generator().to(device)
D = Discriminator().to(device)
optim_G = torch.optim.Adam(G.parameters(), lr=2e-4, betas=(0.5, 0.999))
optim_D = torch.optim.Adam(D.parameters(), lr=2e-4, betas=(0.5, 0.999))
criterion = nn.BCELoss()
for epoch in range(50):
for real_imgs, _ in train_loader:
batch = real_imgs.size(0)
real_imgs = real_imgs.to(device)
# —— 训练判别器 ——
optim_D.zero_grad()
real_labels = torch.ones(batch).to(device)
fake_labels = torch.zeros(batch).to(device)
d_loss_real = criterion(D(real_imgs), real_labels)
z = torch.randn(batch, 100).to(device)
d_loss_fake = criterion(D(G(z).detach()), fake_labels) # .detach() 不更新 G
d_loss = (d_loss_real + d_loss_fake) / 2
d_loss.backward(); optim_D.step()
# —— 训练生成器 ——
optim_G.zero_grad()
z = torch.randn(batch, 100).to(device)
g_loss = criterion(D(G(z)), real_labels) # 让 D 以为生成的是真实样本
g_loss.backward(); optim_G.step()
print(f"Epoch {epoch+1} | D_loss: {d_loss.item():.4f} | G_loss: {g_loss.item():.4f}")GAN 的训练难题
GAN 很难训练,常见问题:
| 问题 | 表现 | 应对 |
|---|---|---|
| 模式崩塌 | G 只生成几种相似样本 | 用 minibatch discrimination 或 WGAN |
| 训练不稳定 | 损失震荡,不收敛 | 用 Spectral Normalization,降低学习率 |
| D 太强 / 太弱 | G 得不到有效梯度 | 平衡 G/D 的训练次数和能力 |
扩散模型:当前最强生成模型
扩散模型(2020 年 DDPM 论文正式奠基)是 Stable Diffusion、DALL-E 3、Sora 等的技术基础。
核心思想:
前向过程(加噪):
原始图像 x₀ → 逐步加高斯噪声 → x₁ → x₂ → ... → xₜ(纯噪声)
每步:xₜ = √αₜ · xₜ₋₁ + √(1-αₜ) · ε,ε ~ N(0, I)
反向过程(去噪,用神经网络学习):
纯噪声 xₜ → 神经网络预测 εₜ(当前噪声) → xₜ₋₁ → ... → x₀(干净图像)
训练目标:最小化 ||ε - εθ(xₜ, t)||²(让网络准确预测每步加的噪声)神经网络只需学会「给定当前噪声图 xₜ 和时间步 t,预测加进去的噪声 ε」,就能在推理时反向去噪生成图像。
用 diffusers 库快速体验(不用从头训练):
from diffusers import StableDiffusionPipeline, DiffusionPipeline
import torch
# 文生图
pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype=torch.float16
).to("cuda")
image = pipe(
prompt="a photorealistic cat sitting on a mountain, golden hour lighting",
negative_prompt="blurry, low quality, cartoon",
num_inference_steps=30, # 去噪步数,越多越清晰(越慢)
guidance_scale=7.5, # 提示词引导强度,7-12 通常合适
height=512, width=512,
).images[0]
image.save("generated_cat.png")训练自己的简单扩散模型(MNIST):
import torch
import torch.nn as nn
class SimpleUNet(nn.Module):
"""极简 UNet:用于预测噪声"""
def __init__(self, dim=64):
super().__init__()
self.time_emb = nn.Embedding(1000, dim)
self.down1 = nn.Conv2d(1, dim, 3, padding=1)
self.down2 = nn.Conv2d(dim, dim*2, 3, stride=2, padding=1)
self.mid = nn.Conv2d(dim*2, dim*2, 3, padding=1)
self.up1 = nn.ConvTranspose2d(dim*2, dim, 4, stride=2, padding=1)
self.out = nn.Conv2d(dim*2, 1, 3, padding=1) # 残差连接
self.relu = nn.ReLU()
def forward(self, x, t):
# 时间嵌入(让网络知道当前是第几步去噪)
t_emb = self.time_emb(t)[:, :, None, None] # [B, dim, 1, 1]
h1 = self.relu(self.down1(x) + t_emb[:, :self.down1.out_channels])
h2 = self.relu(self.down2(h1))
h2 = self.relu(self.mid(h2))
h = self.relu(self.up1(h2))
return self.out(torch.cat([h, h1], dim=1)) # 跳跃连接
def linear_beta_schedule(timesteps=1000):
"""线性噪声调度:β 从小到大,控制每步加多少噪声"""
return torch.linspace(1e-4, 0.02, timesteps)
betas = linear_beta_schedule()
alphas = 1 - betas
alphas_cumprod = torch.cumprod(alphas, dim=0)
def add_noise(x0, t):
"""前向过程:直接从 x₀ 算出 xₜ(利用封闭形式)"""
sqrt_alpha = alphas_cumprod[t].sqrt().view(-1, 1, 1, 1)
sqrt_one_minus_alpha = (1 - alphas_cumprod[t]).sqrt().view(-1, 1, 1, 1)
noise = torch.randn_like(x0)
return sqrt_alpha * x0 + sqrt_one_minus_alpha * noise, noise
# 训练循环
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = SimpleUNet().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=2e-4)
for epoch in range(50):
for x0, _ in train_loader:
x0 = x0.to(device) * 2 - 1 # 归一化到 [-1, 1]
t = torch.randint(0, 1000, (x0.size(0),), device=device)
xt, noise = add_noise(x0, t.cpu())
xt = xt.to(device)
# 预测噪声,和真实噪声做 MSE
pred_noise = model(xt, t)
loss = nn.MSELoss()(pred_noise, noise.to(device))
optimizer.zero_grad()
loss.backward()
optimizer.step()三大生成模型对比
| 特性 | VAE | GAN | 扩散模型 |
|---|---|---|---|
| 生成质量 | 一般(偏模糊) | 好(锐利) | 最好 |
| 训练稳定性 | 好 | 不稳定(模式崩塌) | 好 |
| 生成速度 | 快(一步) | 快(一步) | 慢(多步去噪) |
| 可控性 | 中(隐空间插值) | 中 | 强(文本引导) |
| 学习难度 | 低 | 中 | 中高 |
| 代表应用 | 异常检测、压缩 | 图像风格转换 | 文生图、视频生成 |
一句话小结
VAE 学隐空间的概率分布;GAN 用对抗博弈逼近真实分布;扩散模型学习逐步去噪。三者复杂度和效果递增。入门先理解 VAE 的重参数技巧,再看 GAN 的对抗训练,最后理解扩散模型的「加噪 → 学去噪」框架,就掌握了生成式 AI 的演进脉络。
最后更新于