Skip to content

필사 모드: 生成式AI完全指南:精通 GAN、VAE 与扩散模型

中文
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

引言

生成式AI(Generative AI)是当今技术领域最炙手可热的方向。DALL-E 3 仅凭一行文字就能生成照片般逼真的图像,Stable Diffusion 能创作艺术作品,Sora 则能生成视频。

这一切创新的背后,是数十年间不断演进的生成模型。从 VAE(变分自编码器)、GAN(生成对抗网络),到如今占据主导地位的扩散模型 — 让我们彻底掌握它们各自的思路、数学原理,以及具体的实现方法。


1. 生成模型(Generative Model)概述

1.1 生成模型 vs 判别模型

深度学习模型大致可分为两类。

判别模型(Discriminative Model):在给定输入数据 x 的条件下,学习标签 y 的条件概率 P(y|x)。图像分类、目标检测等任务属于此类。

生成模型(Generative Model):学习数据的联合概率分布 P(x) 或 P(x, y)。训练完成后可以生成新的数据样本。

生成模型的核心问题是:"这份数据是如何生成的?如何从同一分布中生成新的样本?"

1.2 潜在空间(Latent Space)的概念

大多数生成模型都会利用潜在空间(Latent Space)这一低维表示空间。

例如,28x28 的 MNIST 图像(784 维)可以压缩为 2 到 100 维的潜在向量。在这个潜在空间中:

  • 数字"3"和数字"8"彼此相邻
  • 对中间点的向量解码,会得到介于"3"和"8"之间的形态
  • 在潜在空间中游走(插值)可以实现连续的变换

1.3 生成模型的应用领域

  • 图像生成:生成逼真的人脸、风景、艺术作品
  • 图像到图像转换:白天照片 → 夜晚照片,草图 → 照片
  • 数据增强:补充不足的训练数据
  • 药物发现:生成新的分子结构
  • 文本生成:GPT 系列语言模型
  • 音乐/语音合成:创作新音乐、TTS

2. 自编码器(Autoencoder)

我们从理解 VAE 所需的基础 — 自编码器开始讲起。

2.1 编码器-解码器结构

自编码器由两部分组成。

  • 编码器(Encoder):高维输入 x → 低维潜在向量 z
  • 解码器(Decoder):低维潜在向量 z → 重构输出 x'

目标:训练使 x' 尽可能接近 x(即最小化重构损失)。

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

class Autoencoder(nn.Module):
    """基础自编码器(用于 MNIST)"""
    def __init__(self, input_dim=784, latent_dim=32):
        super().__init__()

        # 编码器
        self.encoder = nn.Sequential(
            nn.Linear(input_dim, 256),
            nn.ReLU(),
            nn.Linear(256, 128),
            nn.ReLU(),
            nn.Linear(128, latent_dim),
            nn.ReLU()
        )

        # 解码器
        self.decoder = nn.Sequential(
            nn.Linear(latent_dim, 128),
            nn.ReLU(),
            nn.Linear(128, 256),
            nn.ReLU(),
            nn.Linear(256, input_dim),
            nn.Sigmoid()  # 将像素值限制在 [0, 1] 范围内
        )

    def forward(self, x):
        # 展平
        x = x.view(x.size(0), -1)
        z = self.encoder(x)
        x_reconstructed = self.decoder(z)
        return x_reconstructed, z

    def encode(self, x):
        x = x.view(x.size(0), -1)
        return self.encoder(x)

    def decode(self, z):
        return self.decoder(z).view(-1, 1, 28, 28)


def train_autoencoder(epochs=20):
    """训练自编码器"""
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

    # MNIST 数据集
    transform = transforms.Compose([
        transforms.ToTensor(),
    ])
    train_dataset = datasets.MNIST('./data', train=True, download=True, transform=transform)
    train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True)

    model = Autoencoder().to(device)
    optimizer = optim.Adam(model.parameters(), lr=1e-3)
    criterion = nn.BCELoss()  # 二元交叉熵

    for epoch in range(epochs):
        total_loss = 0
        for batch_idx, (data, _) in enumerate(train_loader):
            data = data.to(device)

            optimizer.zero_grad()
            reconstructed, z = model(data)
            target = data.view(data.size(0), -1)
            loss = criterion(reconstructed, target)
            loss.backward()
            optimizer.step()

            total_loss += loss.item()

        avg_loss = total_loss / len(train_loader)
        print(f"Epoch {epoch+1}/{epochs}, Loss: {avg_loss:.4f}")

    return model

2.2 自编码器的局限

基础自编码器能很好地压缩数据,但生成新样本却很困难。潜在空间并不规则,从任意潜在向量解码,往往会得到毫无意义的图像。

VAE 正是为了解决这个问题而诞生的。


3. 变分自编码器(VAE)

VAE(Variational Autoencoder)由 Kingma 与 Welling 在 2013 年发表的开创性论文(arXiv:1312.6114)中提出。

3.1 VAE 的核心思路

VAE 的核心,是在潜在空间中学习一个概率分布

  • 基础自编码器:z = encoder(x)(确定性的点)
  • VAE:z ~ N(μ, σ²)(从高斯分布中采样)

编码器现在输出的不再是潜在向量 z 本身,而是分布的参数(均值 μ、方差 σ²)。

生成新图像时,从标准正态分布 N(0, I)中采样 z,输入解码器即可。

3.2 ELBO(证据下界)

VAE 的训练目标是最大化数据的对数似然 log P(x)。由于这一目标难以直接优化,因此转而采用最大化 ELBO 的方式。

log P(x) >= E_q[log P(x|z)] - KL[q(z|x) || P(z)]
                    ↑                    ↑
              重构损失              KL 散度

ELBO = 重构损失 + KL 散度

  • 重构损失:编码后再解码得到的结果,与原始输入有多相似
  • KL 散度:学习到的潜在分布 q(z|x),与先验分布 P(z) = N(0, I)有多接近

3.3 重参数化技巧(Reparameterization Trick)

若直接从 z ~ N(μ, σ²)中采样,反向传播将无法进行。重参数化技巧正是为了解决这一问题。

z = μ + σ * ε,  ε ~ N(0, I)

这样一来,随机性(ε)就与网络参数分离开来,从而可以计算关于 μ 和 σ 的梯度。

3.4 完整的 VAE 实现(MNIST)

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
import numpy as np

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

        # 编码器:输入 -> μ, log(σ²)
        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)  # log 方差

        # 解码器:潜在向量 -> 重构
        self.fc3 = nn.Linear(latent_dim, hidden_dim)
        self.fc4 = nn.Linear(hidden_dim, input_dim)

    def encode(self, x):
        """编码器:x -> (μ, log_var)"""
        h = F.relu(self.fc1(x))
        return self.fc_mu(h), self.fc_logvar(h)

    def reparameterize(self, mu, logvar):
        """重参数化技巧:z = μ + ε * σ"""
        if self.training:
            std = torch.exp(0.5 * logvar)  # σ = exp(0.5 * log σ²)
            eps = torch.randn_like(std)     # ε ~ N(0, I)
            return mu + eps * std
        else:
            return mu  # 推理时只使用均值

    def decode(self, z):
        """解码器:z -> x'"""
        h = F.relu(self.fc3(z))
        return torch.sigmoid(self.fc4(h))

    def forward(self, x):
        x_flat = x.view(-1, 784)
        mu, logvar = self.encode(x_flat)
        z = self.reparameterize(mu, logvar)
        x_recon = self.decode(z)
        return x_recon, mu, logvar

    def generate(self, num_samples, device):
        """从标准正态分布采样以生成图像"""
        with torch.no_grad():
            z = torch.randn(num_samples, self.latent_dim).to(device)
            samples = self.decode(z)
            return samples.view(num_samples, 1, 28, 28)


def vae_loss(x_recon, x, mu, logvar, beta=1.0):
    """
    VAE 损失 = 重构损失 + β * KL 散度
    beta=1:标准 VAE
    beta>1:β-VAE(潜在表示的解耦程度更高)
    """
    # 重构损失(BCE)
    recon_loss = F.binary_cross_entropy(
        x_recon, x.view(-1, 784),
        reduction='sum'
    )

    # KL 散度:KL[N(μ, σ²) || N(0, 1)]
    # = -0.5 * Σ(1 + log σ² - μ² - σ²)
    kl_loss = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())

    return recon_loss + beta * kl_loss


def train_vae(epochs=50, latent_dim=20, beta=1.0):
    """训练 VAE"""
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

    transform = transforms.Compose([transforms.ToTensor()])
    train_dataset = datasets.MNIST('./data', train=True, download=True, transform=transform)
    train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True)
    test_dataset = datasets.MNIST('./data', train=False, transform=transform)
    test_loader = DataLoader(test_dataset, batch_size=128)

    model = VAE(latent_dim=latent_dim).to(device)
    optimizer = optim.Adam(model.parameters(), lr=1e-3)

    train_losses = []

    for epoch in range(epochs):
        model.train()
        total_loss = 0

        for data, _ in train_loader:
            data = data.to(device)
            optimizer.zero_grad()

            x_recon, mu, logvar = model(data)
            loss = vae_loss(x_recon, data, mu, logvar, beta)

            loss.backward()
            optimizer.step()
            total_loss += loss.item()

        avg_loss = total_loss / len(train_dataset)
        train_losses.append(avg_loss)

        if (epoch + 1) % 10 == 0:
            print(f"Epoch {epoch+1}/{epochs}, Loss: {avg_loss:.4f}")

    return model, train_losses


def interpolate_latent_space(model, img1, img2, steps=10, device='cpu'):
    """在潜在空间中对两张图像进行插值"""
    model.eval()
    with torch.no_grad():
        # 编码潜在向量
        z1_flat = img1.view(-1, 784).to(device)
        z2_flat = img2.view(-1, 784).to(device)

        mu1, _ = model.encode(z1_flat)
        mu2, _ = model.encode(z2_flat)

        # 线性插值
        interpolated_images = []
        for alpha in np.linspace(0, 1, steps):
            z_interp = (1 - alpha) * mu1 + alpha * mu2
            img_recon = model.decode(z_interp)
            interpolated_images.append(img_recon.view(1, 28, 28))

    return interpolated_images

3.5 卷积 VAE(用于 CIFAR-10)

class ConvVAE(nn.Module):
    """卷积 VAE(用于彩色图像)"""
    def __init__(self, latent_dim=128):
        super().__init__()
        self.latent_dim = latent_dim

        # 卷积编码器
        self.encoder_conv = nn.Sequential(
            nn.Conv2d(3, 32, 4, stride=2, padding=1),  # 16x16
            nn.ReLU(),
            nn.Conv2d(32, 64, 4, stride=2, padding=1), # 8x8
            nn.ReLU(),
            nn.Conv2d(64, 128, 4, stride=2, padding=1),# 4x4
            nn.ReLU(),
        )
        self.fc_mu = nn.Linear(128 * 4 * 4, latent_dim)
        self.fc_logvar = nn.Linear(128 * 4 * 4, latent_dim)

        # 转置卷积解码器
        self.decoder_fc = nn.Linear(latent_dim, 128 * 4 * 4)
        self.decoder_conv = nn.Sequential(
            nn.ConvTranspose2d(128, 64, 4, stride=2, padding=1),  # 8x8
            nn.ReLU(),
            nn.ConvTranspose2d(64, 32, 4, stride=2, padding=1),   # 16x16
            nn.ReLU(),
            nn.ConvTranspose2d(32, 3, 4, stride=2, padding=1),    # 32x32
            nn.Sigmoid()
        )

    def encode(self, x):
        h = self.encoder_conv(x).view(x.size(0), -1)
        return self.fc_mu(h), self.fc_logvar(h)

    def reparameterize(self, mu, logvar):
        std = torch.exp(0.5 * logvar)
        eps = torch.randn_like(std)
        return mu + eps * std

    def decode(self, z):
        h = F.relu(self.decoder_fc(z)).view(-1, 128, 4, 4)
        return self.decoder_conv(h)

    def forward(self, x):
        mu, logvar = self.encode(x)
        z = self.reparameterize(mu, logvar)
        return self.decode(z), mu, logvar

4. 生成对抗网络(GAN)

GAN(Generative Adversarial Network)是 Ian Goodfellow 在 2014 年发表的论文(arXiv:1406.2661)中提出的革命性构想。

4.1 Generator 与 Discriminator 的博弈

GAN 由两个相互竞争的神经网络共同训练而成。

生成器(Generator, G):从随机噪声 z 生成伪造数据。

  • 目标:生成足以骗过 Discriminator 的逼真数据

判别器(Discriminator, D):区分真实数据与生成的伪造数据。

  • 目标:将真实数据判为 1,伪造数据判为 0

在这场博弈中,两个网络不断竞争学习,直至达到纳什均衡(Nash Equilibrium)。

4.2 极小极大损失(Minimax Loss)

min_G max_D [E_x[log D(x)] + E_z[log(1 - D(G(z)))]]
  • D 最大化:在真实数据上最大化 log D(x),在伪造数据上最大化 log(1 - D(G(z)))
  • G 最小化:使 G(z)足以骗过 D,最小化 log(1 - D(G(z)))

实际训练中,G 的损失通常使用 -log(D(G(z)))(非饱和损失,non-saturating loss)。

4.3 基础 GAN 实现

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
import numpy as np

class Generator(nn.Module):
    """GAN 生成器"""
    def __init__(self, noise_dim=100, output_dim=784):
        super().__init__()
        self.model = nn.Sequential(
            nn.Linear(noise_dim, 256),
            nn.LeakyReLU(0.2),
            nn.BatchNorm1d(256),
            nn.Linear(256, 512),
            nn.LeakyReLU(0.2),
            nn.BatchNorm1d(512),
            nn.Linear(512, 1024),
            nn.LeakyReLU(0.2),
            nn.BatchNorm1d(1024),
            nn.Linear(1024, output_dim),
            nn.Tanh()  # 范围为 [-1, 1]
        )

    def forward(self, z):
        return self.model(z).view(-1, 1, 28, 28)


class Discriminator(nn.Module):
    """GAN 判别器"""
    def __init__(self, input_dim=784):
        super().__init__()
        self.model = nn.Sequential(
            nn.Linear(input_dim, 1024),
            nn.LeakyReLU(0.2),
            nn.Dropout(0.3),
            nn.Linear(1024, 512),
            nn.LeakyReLU(0.2),
            nn.Dropout(0.3),
            nn.Linear(512, 256),
            nn.LeakyReLU(0.2),
            nn.Dropout(0.3),
            nn.Linear(256, 1),
            nn.Sigmoid()
        )

    def forward(self, x):
        return self.model(x.view(x.size(0), -1))


def train_gan(epochs=200, noise_dim=100, lr=2e-4):
    """训练 GAN"""
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

    transform = transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize([0.5], [0.5])  # 归一化到 [-1, 1] 范围
    ])
    train_dataset = datasets.MNIST('./data', train=True, download=True, transform=transform)
    dataloader = DataLoader(train_dataset, batch_size=64, shuffle=True)

    G = Generator(noise_dim).to(device)
    D = Discriminator().to(device)

    # 分别使用独立的优化器
    optimizer_G = optim.Adam(G.parameters(), lr=lr, betas=(0.5, 0.999))
    optimizer_D = optim.Adam(D.parameters(), lr=lr, betas=(0.5, 0.999))

    criterion = nn.BCELoss()

    for epoch in range(epochs):
        for i, (real_imgs, _) in enumerate(dataloader):
            batch_size = real_imgs.size(0)
            real_imgs = real_imgs.to(device)

            real_labels = torch.ones(batch_size, 1).to(device)
            fake_labels = torch.zeros(batch_size, 1).to(device)

            # === 更新 Discriminator ===
            optimizer_D.zero_grad()

            # 真实图像损失
            d_real = D(real_imgs)
            d_loss_real = criterion(d_real, real_labels)

            # 伪造图像损失
            z = torch.randn(batch_size, noise_dim).to(device)
            fake_imgs = G(z).detach()  # 阻断 G 的梯度
            d_fake = D(fake_imgs)
            d_loss_fake = criterion(d_fake, fake_labels)

            d_loss = d_loss_real + d_loss_fake
            d_loss.backward()
            optimizer_D.step()

            # === 更新 Generator ===
            optimizer_G.zero_grad()

            z = torch.randn(batch_size, noise_dim).to(device)
            fake_imgs = G(z)
            g_pred = D(fake_imgs)
            # G 试图骗过 D:让伪造样本被判为真实
            g_loss = criterion(g_pred, real_labels)

            g_loss.backward()
            optimizer_G.step()

        if (epoch + 1) % 20 == 0:
            print(f"Epoch {epoch+1}/{epochs} | D Loss: {d_loss.item():.4f} | G Loss: {g_loss.item():.4f}")

    return G, D

4.4 模式崩溃(Mode Collapse)问题

这是 GAN 最大的问题之一。Generator 不再生成多样化的图像,而是反复生成少数几种足以骗过 Discriminator 的模式,这一现象就叫模式崩溃。


5. GAN 发展史

5.1 DCGAN(Deep Convolutional GAN)

2015 年提出的 DCGAN,成功地将卷积神经网络应用到了 GAN 中。

class DCGANGenerator(nn.Module):
    """DCGAN 生成器(用于 64x64 图像)"""
    def __init__(self, noise_dim=100, ngf=64, nc=3):
        super().__init__()
        self.main = nn.Sequential(
            # 输入:noise_dim x 1 x 1
            nn.ConvTranspose2d(noise_dim, ngf * 8, 4, 1, 0, bias=False),
            nn.BatchNorm2d(ngf * 8),
            nn.ReLU(True),
            # 状态:(ngf*8) x 4 x 4
            nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf * 4),
            nn.ReLU(True),
            # 状态:(ngf*4) x 8 x 8
            nn.ConvTranspose2d(ngf * 4, ngf * 2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf * 2),
            nn.ReLU(True),
            # 状态:(ngf*2) x 16 x 16
            nn.ConvTranspose2d(ngf * 2, ngf, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf),
            nn.ReLU(True),
            # 状态:(ngf) x 32 x 32
            nn.ConvTranspose2d(ngf, nc, 4, 2, 1, bias=False),
            nn.Tanh()
            # 输出:nc x 64 x 64
        )

    def forward(self, z):
        z = z.view(z.size(0), -1, 1, 1)
        return self.main(z)


class DCGANDiscriminator(nn.Module):
    """DCGAN 判别器(用于 64x64 图像)"""
    def __init__(self, ndf=64, nc=3):
        super().__init__()
        self.main = nn.Sequential(
            # 输入:nc x 64 x 64
            nn.Conv2d(nc, ndf, 4, 2, 1, bias=False),
            nn.LeakyReLU(0.2, inplace=True),
            # 状态:(ndf) x 32 x 32
            nn.Conv2d(ndf, ndf * 2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 2),
            nn.LeakyReLU(0.2, inplace=True),
            # 状态:(ndf*2) x 16 x 16
            nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 4),
            nn.LeakyReLU(0.2, inplace=True),
            # 状态:(ndf*4) x 8 x 8
            nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 8),
            nn.LeakyReLU(0.2, inplace=True),
            # 状态:(ndf*8) x 4 x 4
            nn.Conv2d(ndf * 8, 1, 4, 1, 0, bias=False),
            nn.Sigmoid()
        )

    def forward(self, x):
        return self.main(x).view(-1, 1)

5.2 WGAN(Wasserstein GAN)

WGAN(arXiv:1701.07875)使用 Wasserstein 距离取代 Jensen-Shannon 散度,大幅提升了训练的稳定性。

class WGANDiscriminator(nn.Module):
    """WGAN Critic(判别器):不使用 Sigmoid"""
    def __init__(self, input_dim=784):
        super().__init__()
        self.model = nn.Sequential(
            nn.Linear(input_dim, 512),
            nn.LeakyReLU(0.2),
            nn.Linear(512, 256),
            nn.LeakyReLU(0.2),
            nn.Linear(256, 1)
            # 不使用 Sigmoid!为了计算 Wasserstein 距离
        )

    def forward(self, x):
        return self.model(x.view(x.size(0), -1))


def train_wgan(G, D, dataloader, device, epochs=100,
               n_critic=5, clip_value=0.01, lr=5e-5):
    """训练 WGAN"""
    optimizer_G = optim.RMSprop(G.parameters(), lr=lr)
    optimizer_D = optim.RMSprop(D.parameters(), lr=lr)

    for epoch in range(epochs):
        for i, (real_imgs, _) in enumerate(dataloader):
            real_imgs = real_imgs.to(device)
            batch_size = real_imgs.size(0)

            # 将 Critic 更新 n_critic 次
            for _ in range(n_critic):
                optimizer_D.zero_grad()

                z = torch.randn(batch_size, 100).to(device)
                fake_imgs = G(z).detach()

                # Wasserstein 损失:E[D(real)] - E[D(fake)]
                d_loss = -torch.mean(D(real_imgs)) + torch.mean(D(fake_imgs))
                d_loss.backward()
                optimizer_D.step()

                # 权重裁剪(Lipschitz 条件)
                for p in D.parameters():
                    p.data.clamp_(-clip_value, clip_value)

            # 更新 Generator
            optimizer_G.zero_grad()
            z = torch.randn(batch_size, 100).to(device)
            fake_imgs = G(z)
            g_loss = -torch.mean(D(fake_imgs))
            g_loss.backward()
            optimizer_G.step()

5.3 WGAN-GP(Gradient Penalty)

WGAN 中的权重裁剪,被梯度惩罚(Gradient Penalty)取代,用来更好地强制满足 Lipschitz 条件。

def gradient_penalty(D, real_imgs, fake_imgs, device):
    """计算梯度惩罚"""
    batch_size = real_imgs.size(0)
    # 在真实图像与伪造图像之间随机插值
    alpha = torch.rand(batch_size, 1, 1, 1).to(device)
    interpolated = alpha * real_imgs + (1 - alpha) * fake_imgs
    interpolated.requires_grad_(True)

    d_interpolated = D(interpolated)

    gradients = torch.autograd.grad(
        outputs=d_interpolated,
        inputs=interpolated,
        grad_outputs=torch.ones_like(d_interpolated),
        create_graph=True,
        retain_graph=True
    )[0]

    gradients = gradients.view(batch_size, -1)
    gradient_norm = gradients.norm(2, dim=1)
    penalty = ((gradient_norm - 1) ** 2).mean()
    return penalty


def wgan_gp_d_loss(D, real_imgs, fake_imgs, device, lambda_gp=10):
    """WGAN-GP 判别器损失"""
    d_real = D(real_imgs).mean()
    d_fake = D(fake_imgs).mean()
    gp = gradient_penalty(D, real_imgs, fake_imgs, device)
    return -d_real + d_fake + lambda_gp * gp

5.4 条件 GAN(cGAN)

通过添加标签条件,来生成特定类别的图像。

class ConditionalGenerator(nn.Module):
    """条件 GAN 生成器"""
    def __init__(self, noise_dim=100, num_classes=10, embed_dim=50):
        super().__init__()
        self.label_embedding = nn.Embedding(num_classes, embed_dim)
        self.model = nn.Sequential(
            nn.Linear(noise_dim + embed_dim, 256),
            nn.LeakyReLU(0.2),
            nn.Linear(256, 512),
            nn.LeakyReLU(0.2),
            nn.Linear(512, 784),
            nn.Tanh()
        )

    def forward(self, z, labels):
        label_embed = self.label_embedding(labels)
        x = torch.cat([z, label_embed], dim=1)
        return self.model(x).view(-1, 1, 28, 28)

6. 扩散模型(Diffusion Models)

扩散模型是当前图像生成领域的标准做法,也是最新的生成模型。DDPM(Denoising Diffusion Probabilistic Models, Ho et al., 2020, arXiv:2006.11239)开创了这一领域。

6.1 扩散模型的直觉

扩散模型的核心思路在于两个过程。

Forward Process(添加噪声):向真实图像逐步添加高斯噪声,最终变为纯噪声。经过 T 步之后,就会变成标准正态分布。

Reverse Process(去除噪声):从纯噪声出发,逐步去除噪声,还原出真实图像。神经网络学习的正是这一逆向过程。

6.2 Forward Process 公式

每一步都会添加噪声。

q(x_t | x_{t-1}) = N(x_t; sqrt(1-β_t) * x_{t-1}, β_t * I)

这里的 β_t 就是噪声调度(noise schedule)。

一次性跳跃 t 步(重要特性)

q(x_t | x_0) = N(x_t; sqrt(ā_t) * x_0, (1-ā_t) * I)

这里的 ā_t,是 s 从 1 到 t 变化时 (1 - β_s)的连乘积。

因此,可以直接计算出任意时间步 t 上的噪声图像。

import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np

class NoiseSchedule:
    """噪声调度管理"""
    def __init__(self, timesteps=1000, beta_start=1e-4, beta_end=0.02):
        self.timesteps = timesteps

        # 线性噪声调度
        self.betas = torch.linspace(beta_start, beta_end, timesteps)
        self.alphas = 1.0 - self.betas
        self.alphas_cumprod = torch.cumprod(self.alphas, dim=0)
        self.alphas_cumprod_prev = F.pad(self.alphas_cumprod[:-1], (1, 0), value=1.0)

        # Forward process 系数
        self.sqrt_alphas_cumprod = torch.sqrt(self.alphas_cumprod)
        self.sqrt_one_minus_alphas_cumprod = torch.sqrt(1.0 - self.alphas_cumprod)

        # Reverse process 系数
        self.posterior_variance = (
            self.betas * (1.0 - self.alphas_cumprod_prev) /
            (1.0 - self.alphas_cumprod)
        )

    def q_sample(self, x_start, t, noise=None):
        """Forward process:向 x_0 添加 t 步噪声"""
        if noise is None:
            noise = torch.randn_like(x_start)

        # 提取对应 ā_t 的系数
        sqrt_alphas_cumprod_t = self.sqrt_alphas_cumprod[t].view(-1, 1, 1, 1)
        sqrt_one_minus_alphas_cumprod_t = self.sqrt_one_minus_alphas_cumprod[t].view(-1, 1, 1, 1)

        return sqrt_alphas_cumprod_t * x_start + sqrt_one_minus_alphas_cumprod_t * noise

    def predict_start_from_noise(self, x_t, t, noise):
        """从预测的噪声中还原 x_0"""
        sqrt_recip_alphas_cumprod = 1.0 / self.sqrt_alphas_cumprod[t].view(-1, 1, 1, 1)
        sqrt_recipm1_alphas_cumprod = (
            torch.sqrt(1.0 / self.alphas_cumprod[t] - 1.0).view(-1, 1, 1, 1)
        )
        return sqrt_recip_alphas_cumprod * x_t - sqrt_recipm1_alphas_cumprod * noise

6.3 U-Net 架构

扩散模型的噪声预测网络使用 U-Net 结构,并通过正余弦嵌入对时间步 t 进行条件化。

class SinusoidalPositionEmbeddings(nn.Module):
    """用于时间步的正弦位置嵌入"""
    def __init__(self, dim):
        super().__init__()
        self.dim = dim

    def forward(self, time):
        device = time.device
        half_dim = self.dim // 2
        embeddings = np.log(10000) / (half_dim - 1)
        embeddings = torch.exp(torch.arange(half_dim, device=device) * -embeddings)
        embeddings = time[:, None] * embeddings[None, :]
        embeddings = torch.cat((embeddings.sin(), embeddings.cos()), dim=-1)
        return embeddings


class ResidualBlock(nn.Module):
    """带有时间步条件化的残差块"""
    def __init__(self, in_channels, out_channels, time_emb_dim):
        super().__init__()
        self.time_mlp = nn.Sequential(
            nn.SiLU(),
            nn.Linear(time_emb_dim, out_channels)
        )
        self.block1 = nn.Sequential(
            nn.GroupNorm(8, in_channels),
            nn.SiLU(),
            nn.Conv2d(in_channels, out_channels, 3, padding=1)
        )
        self.block2 = nn.Sequential(
            nn.GroupNorm(8, out_channels),
            nn.SiLU(),
            nn.Conv2d(out_channels, out_channels, 3, padding=1)
        )
        self.residual_conv = (
            nn.Conv2d(in_channels, out_channels, 1)
            if in_channels != out_channels else nn.Identity()
        )

    def forward(self, x, time_emb):
        h = self.block1(x)
        time_emb = self.time_mlp(time_emb)[:, :, None, None]
        h = h + time_emb
        h = self.block2(h)
        return h + self.residual_conv(x)


class SimpleUNet(nn.Module):
    """简化版 U-Net(用于 DDPM)"""
    def __init__(self, in_channels=1, model_channels=64, time_emb_dim=256):
        super().__init__()

        # 时间步嵌入
        self.time_mlp = nn.Sequential(
            SinusoidalPositionEmbeddings(model_channels),
            nn.Linear(model_channels, time_emb_dim),
            nn.SiLU(),
            nn.Linear(time_emb_dim, time_emb_dim)
        )

        # 编码器
        self.down1 = ResidualBlock(in_channels, model_channels, time_emb_dim)
        self.down2 = ResidualBlock(model_channels, model_channels * 2, time_emb_dim)
        self.pool = nn.MaxPool2d(2)

        # 瓶颈层
        self.bottleneck = ResidualBlock(model_channels * 2, model_channels * 2, time_emb_dim)

        # 解码器
        self.up1 = ResidualBlock(model_channels * 4, model_channels, time_emb_dim)
        self.up2 = ResidualBlock(model_channels * 2, model_channels, time_emb_dim)
        self.upsample = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)

        # 输出
        self.final = nn.Conv2d(model_channels, in_channels, 1)

    def forward(self, x, t):
        time_emb = self.time_mlp(t)

        # 编码器路径
        x1 = self.down1(x, time_emb)
        x2 = self.down2(self.pool(x1), time_emb)

        # 瓶颈层
        x_mid = self.bottleneck(x2, time_emb)

        # 解码器路径(跳跃连接)
        x = self.up1(torch.cat([self.upsample(x_mid), x2], dim=1), time_emb)
        x = self.up2(torch.cat([self.upsample(x), x1], dim=1), time_emb)

        return self.final(x)

6.4 DDPM 训练与采样

class DDPM:
    """DDPM 训练与采样"""
    def __init__(self, model, noise_schedule, device):
        self.model = model
        self.schedule = noise_schedule
        self.device = device

    def get_loss(self, x_start, t):
        """训练损失:噪声预测误差"""
        noise = torch.randn_like(x_start)
        x_noisy = self.schedule.q_sample(x_start, t, noise)

        # 神经网络预测所添加的噪声
        predicted_noise = self.model(x_noisy, t)

        # 简单的 MSE 损失
        return F.mse_loss(noise, predicted_noise)

    @torch.no_grad()
    def p_sample(self, x_t, t):
        """Reverse process:从 x_t 一步去噪到 x_{t-1}"""
        betas_t = self.schedule.betas[t].view(-1, 1, 1, 1).to(self.device)
        sqrt_one_minus_alphas_cumprod_t = (
            self.schedule.sqrt_one_minus_alphas_cumprod[t].view(-1, 1, 1, 1).to(self.device)
        )
        sqrt_recip_alphas_t = torch.sqrt(
            1.0 / self.schedule.alphas[t]
        ).view(-1, 1, 1, 1).to(self.device)

        # 预测的噪声
        predicted_noise = self.model(x_t, t)

        # 计算均值
        model_mean = sqrt_recip_alphas_t * (
            x_t - betas_t * predicted_noise / sqrt_one_minus_alphas_cumprod_t
        )

        if t[0] == 0:
            return model_mean
        else:
            # 加上方差
            posterior_variance_t = self.schedule.posterior_variance[t].view(-1, 1, 1, 1).to(self.device)
            noise = torch.randn_like(x_t)
            return model_mean + torch.sqrt(posterior_variance_t) * noise

    @torch.no_grad()
    def sample(self, batch_size, image_shape):
        """从纯噪声出发生成图像"""
        img = torch.randn(batch_size, *image_shape).to(self.device)

        for i in reversed(range(self.schedule.timesteps)):
            t = torch.full((batch_size,), i, dtype=torch.long, device=self.device)
            img = self.p_sample(img, t)

        return img


def train_ddpm(epochs=100):
    """训练 DDPM"""
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

    transform = transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize([0.5], [0.5])  # 归一化到 [-1, 1] 范围
    ])
    dataset = datasets.MNIST('./data', train=True, download=True, transform=transform)
    dataloader = DataLoader(dataset, batch_size=128, shuffle=True)

    model = SimpleUNet(in_channels=1).to(device)
    schedule = NoiseSchedule(timesteps=1000)
    ddpm = DDPM(model, schedule, device)

    optimizer = optim.Adam(model.parameters(), lr=2e-4)

    for epoch in range(epochs):
        total_loss = 0
        for batch, (x, _) in enumerate(dataloader):
            x = x.to(device)
            # 随机采样时间步
            t = torch.randint(0, schedule.timesteps, (x.size(0),), device=device)

            loss = ddpm.get_loss(x, t)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            total_loss += loss.item()

        if (epoch + 1) % 10 == 0:
            avg_loss = total_loss / len(dataloader)
            print(f"Epoch {epoch+1}/{epochs}, Loss: {avg_loss:.4f}")
            # 生成样本
            samples = ddpm.sample(16, (1, 28, 28))
            print(f"Generated samples: {samples.shape}")

    return model, ddpm

7. DDIM — 快速采样

DDPM 需要 1000 步采样,速度很慢。DDIM(Denoising Diffusion Implicit Models, Song et al., 2020)提供了更快的确定性采样方式。

7.1 DDIM 的思路

DDIM 将采样过程重新定义为一个非马尔可夫(non-Markovian)过程。核心在于沿用同一个训练好的模型,同时大幅减少采样步数(1000 → 50 步)。

@torch.no_grad()
def ddim_sample(model, schedule, batch_size, image_shape,
                ddim_timesteps=50, eta=0.0, device='cpu'):
    """
    DDIM 采样
    eta=0.0:完全确定性
    eta=1.0:等同于 DDPM
    """
    # 以均匀间隔选取时间步
    c = schedule.timesteps // ddim_timesteps
    timestep_seq = list(range(0, schedule.timesteps, c))[::-1]

    img = torch.randn(batch_size, *image_shape).to(device)

    for i, t in enumerate(timestep_seq):
        t_tensor = torch.full((batch_size,), t, dtype=torch.long, device=device)
        t_prev = timestep_seq[i + 1] if i + 1 < len(timestep_seq) else -1

        # 当前时间步的系数
        alpha_bar = schedule.alphas_cumprod[t].to(device)
        alpha_bar_prev = (
            schedule.alphas_cumprod[t_prev].to(device) if t_prev >= 0
            else torch.tensor(1.0, device=device)
        )

        # 预测噪声
        pred_noise = model(img, t_tensor)

        # 预测 x_0
        pred_x0 = (img - torch.sqrt(1 - alpha_bar) * pred_noise) / torch.sqrt(alpha_bar)
        pred_x0 = torch.clamp(pred_x0, -1, 1)

        # 计算 sigma
        sigma = eta * torch.sqrt(
            (1 - alpha_bar_prev) / (1 - alpha_bar) * (1 - alpha_bar / alpha_bar_prev)
        )

        # 计算方向
        pred_dir = torch.sqrt(1 - alpha_bar_prev - sigma**2) * pred_noise

        # 下一步的图像
        noise = torch.randn_like(img) if t_prev >= 0 else 0
        img = torch.sqrt(alpha_bar_prev) * pred_x0 + pred_dir + sigma * noise

    return img

8. Stable Diffusion 解析

Stable Diffusion(arXiv:2112.10752)是一种创新方法:不在像素空间,而是在潜在空间(Latent Space)中执行扩散过程。

8.1 潜在扩散模型(LDM)

若直接对高分辨率图像(512x512)应用扩散,计算成本会非常高。LDM 的做法是:

  1. 用 VAE 编码器将图像压缩到潜在空间(512x512x3 → 64x64x4)
  2. 在潜在空间中执行扩散
  3. 用 VAE 解码器还原出原始分辨率

由此,计算成本可以节省 8 倍以上。

8.2 Stable Diffusion 的组成部分

文本提示词 → CLIP 文本编码器 → 文本嵌入
纯噪声 z_T → [U-Net with Cross-Attention] → 潜在向量 z_0
                                    VAE 解码器 → 最终图像

CLIP 文本编码器:将文本转换为有意义的向量表示。

U-Net with Cross-Attention:通过交叉注意力(cross-attention),将文本嵌入用作条件。

Classifier-Free Guidance(CFG):将无文本条件下的预测与有条件时的预测相结合,以提升文本还原度。

guided_noise = uncond_noise + guidance_scale * (cond_noise - uncond_noise)

8.3 使用 diffusers 库调用 Stable Diffusion

from diffusers import StableDiffusionPipeline, DPMSolverMultistepScheduler
import torch

# 加载模型
model_id = "stabilityai/stable-diffusion-2-1"
pipe = StableDiffusionPipeline.from_pretrained(
    model_id,
    torch_dtype=torch.float16,
    use_safetensors=True,
)

# 使用更快的调度器
pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config)
pipe = pipe.to("cuda")

# 生成图像
prompt = "a photorealistic landscape of mountains at sunset, 8k, highly detailed"
negative_prompt = "blurry, low quality, distorted"

image = pipe(
    prompt,
    negative_prompt=negative_prompt,
    num_inference_steps=25,    # 将 DDPM 的 1000 步缩减为 25 步
    guidance_scale=7.5,        # CFG 尺度
    height=512,
    width=512,
    generator=torch.Generator("cuda").manual_seed(42)
).images[0]

image.save("generated_image.png")


# 图像到图像转换
from diffusers import StableDiffusionImg2ImgPipeline
from PIL import Image

img2img_pipe = StableDiffusionImg2ImgPipeline.from_pretrained(
    model_id,
    torch_dtype=torch.float16
).to("cuda")

init_image = Image.open("input.jpg").resize((512, 512))
result = img2img_pipe(
    prompt="a painting in Van Gogh style",
    image=init_image,
    strength=0.75,  # 保留原图的程度(0-1)
    guidance_scale=7.5,
    num_inference_steps=50
).images[0]

8.4 Inpainting(图像修复)

from diffusers import StableDiffusionInpaintPipeline

inpaint_pipe = StableDiffusionInpaintPipeline.from_pretrained(
    "runwayml/stable-diffusion-inpainting",
    torch_dtype=torch.float16
).to("cuda")

# 原始图像与蒙版
image = Image.open("photo.jpg").resize((512, 512))
mask = Image.open("mask.jpg").resize((512, 512))  # 白色 = 需要修复的区域

result = inpaint_pipe(
    prompt="a beautiful garden with flowers",
    image=image,
    mask_image=mask,
    num_inference_steps=50
).images[0]

9. ControlNet — 精细的图像控制

9.1 ControlNet 架构

ControlNet(Zhang et al., 2023)让 Stable Diffusion 能够以额外的控制信号(Canny 边缘、深度图、姿态等)作为条件。

它的做法是:固定原有的 U-Net 权重,另外添加一个独立的控制网络。在约 3.6 亿参数的 Stable Diffusion 之上,再额外训练一份约 3.6 亿参数的副本。

from diffusers import StableDiffusionControlNetPipeline, ControlNetModel
from diffusers.utils import load_image
import cv2
import numpy as np

# 加载 Canny ControlNet
controlnet = ControlNetModel.from_pretrained(
    "lllyasviel/sd-controlnet-canny",
    torch_dtype=torch.float16
)
pipe = StableDiffusionControlNetPipeline.from_pretrained(
    "runwayml/stable-diffusion-v1-5",
    controlnet=controlnet,
    torch_dtype=torch.float16
).to("cuda")

# 提取 Canny 边缘
image = load_image("input.jpg")
image_array = np.array(image)
canny = cv2.Canny(image_array, 100, 200)
canny_image = Image.fromarray(canny)

# 使用 ControlNet 生成图像
result = pipe(
    prompt="a beautiful oil painting",
    image=canny_image,       # 将 Canny 边缘用作控制信号
    controlnet_conditioning_scale=1.0,
    num_inference_steps=50,
    guidance_scale=7.5
).images[0]

10. 最新的生成模型趋势

10.1 DiT(Diffusion Transformers)

Peebles 与 Xie(2022)提出的 DiT,用 Transformer 架构取代 U-Net,作为扩散模型的骨干网络。Sora、Flux、SD3 等最新模型都基于 DiT。

class DiTBlock(nn.Module):
    """Diffusion Transformer Block"""
    def __init__(self, hidden_dim, num_heads, mlp_ratio=4.0):
        super().__init__()
        self.norm1 = nn.LayerNorm(hidden_dim)
        self.attn = nn.MultiheadAttention(hidden_dim, num_heads, batch_first=True)
        self.norm2 = nn.LayerNorm(hidden_dim)

        mlp_dim = int(hidden_dim * mlp_ratio)
        self.mlp = nn.Sequential(
            nn.Linear(hidden_dim, mlp_dim),
            nn.GELU(),
            nn.Linear(mlp_dim, hidden_dim)
        )

        # AdaLN(Adaptive Layer Normalization):时间步条件化
        self.adaLN_modulation = nn.Sequential(
            nn.SiLU(),
            nn.Linear(hidden_dim, 6 * hidden_dim)
        )

    def forward(self, x, c):
        # 从时间步/条件嵌入中计算调制参数
        shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (
            self.adaLN_modulation(c).chunk(6, dim=-1)
        )

        # 调制后的归一化
        x_norm = (1 + scale_msa.unsqueeze(1)) * self.norm1(x) + shift_msa.unsqueeze(1)
        attn_out, _ = self.attn(x_norm, x_norm, x_norm)
        x = x + gate_msa.unsqueeze(1) * attn_out

        x_norm = (1 + scale_mlp.unsqueeze(1)) * self.norm2(x) + shift_mlp.unsqueeze(1)
        x = x + gate_mlp.unsqueeze(1) * self.mlp(x_norm)

        return x

10.2 SDXL(Stable Diffusion XL)

生成更高分辨率(1024x1024)图像的 Stable Diffusion 升级版本。

from diffusers import StableDiffusionXLPipeline

pipe = StableDiffusionXLPipeline.from_pretrained(
    "stabilityai/stable-diffusion-xl-base-1.0",
    torch_dtype=torch.float16,
    use_safetensors=True,
    variant="fp16"
).to("cuda")

image = pipe(
    prompt="a majestic lion in photorealistic style, 4k",
    negative_prompt="cartoon, blurry",
    height=1024,
    width=1024,
    num_inference_steps=50,
    guidance_scale=5.0
).images[0]

10.3 生成模型比较

模型年份核心思路优点缺点
VAE2013学习潜在分布稳定,潜在空间可解释图像较模糊
GAN2014对抗训练图像清晰锐利训练不稳定,模式崩溃
DDPM2020迭代式去噪高质量,多样性好采样速度慢
LDM2022潜在空间扩散高效,支持文本条件结构较复杂
DiT2022Transformer 骨干扩展效率高计算成本高

结语

我们完整地探索了生成式AI的旅程:从 VAE 的潜在空间理论开始,走过 GAN 的对抗博弈、DDPM 的迭代去噪,直至 Stable Diffusion 的潜在空间扩散。

这一领域发展得极快。今天的最新技术,明天就会成为基础。牢固地理解基础知识,并持续跟进最新论文,至关重要。

以下是几份推荐的持续学习资料。

  • Hugging Face Diffusers:https://huggingface.co/docs/diffusers/
  • VAE 原始论文:arXiv:1312.6114
  • GAN 原始论文:arXiv:1406.2661
  • DDPM 原始论文:arXiv:2006.11239
  • LDM/Stable Diffusion:arXiv:2112.10752

参考资料

  • Kingma, D. P., & Welling, M. (2013). Auto-Encoding Variational Bayes. arXiv:1312.6114
  • Goodfellow, I., et al. (2014). Generative Adversarial Networks. arXiv:1406.2661
  • Ho, J., et al. (2020). Denoising Diffusion Probabilistic Models. arXiv:2006.11239
  • Rombach, R., et al. (2022). High-Resolution Image Synthesis with Latent Diffusion Models. arXiv:2112.10752
  • Arjovsky, M., et al. (2017). Wasserstein GAN. arXiv:1701.07875
  • Peebles, W., & Xie, S. (2022). Scalable Diffusion Models with Transformers. arXiv:2212.09748
  • Hugging Face Diffusers: https://huggingface.co/docs/diffusers/

현재 단락 (1/805)

生成式AI(Generative AI)是当今技术领域最炙手可热的方向。DALL-E 3 仅凭一行文字就能生成照片般逼真的图像,Stable Diffusion 能创作艺术作品,Sora 则能生成视频...

작성 글자: 0원문 글자: 27,323작성 단락: 0/805