跳至内容
生成模型入门:VAE、GAN 与扩散模型

生成模型入门: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̂ ≈ x

VAE 的改进:隐向量不是固定点,而是一个高斯分布的均值和方差,从中采样再解码。这样隐空间连续且有规律,可以从隐空间任意采样生成新图像。

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()

三大生成模型对比

特性VAEGAN扩散模型
生成质量一般(偏模糊)好(锐利)最好
训练稳定性不稳定(模式崩塌)
生成速度快(一步)快(一步)慢(多步去噪)
可控性中(隐空间插值)强(文本引导)
学习难度中高
代表应用异常检测、压缩图像风格转换文生图、视频生成

一句话小结

VAE 学隐空间的概率分布;GAN 用对抗博弈逼近真实分布;扩散模型学习逐步去噪。三者复杂度和效果递增。入门先理解 VAE 的重参数技巧,再看 GAN 的对抗训练,最后理解扩散模型的「加噪 → 学去噪」框架,就掌握了生成式 AI 的演进脉络。

最后更新于