CGAN损失函数详解:从理论到PyTorch实战

1次阅读
没有评论

共计 3781 个字符,预计需要花费 10 分钟才能阅读完成。

image.webp

为什么需要 CGAN?原始 GAN 的局限性

原始 GAN(Generative Adversarial Network)虽然能生成逼真数据,但存在两个明显问题:

CGAN 损失函数详解:从理论到 PyTorch 实战

  1. 缺乏生成方向控制:无法指定生成特定类别(如生成数字 ”7″ 而非随机数字)
  2. 模式崩溃(Mode Collapse):生成器倾向于生成有限几种样本,缺乏多样性

CGAN(Conditional GAN)通过添加条件信息 y(如图像类别标签)解决这些问题。其核心思想是将条件信息同时输入生成器 (Generator) 和判别器(Discriminator),使生成过程可控。

CGAN 损失函数数学原理

CGAN 的目标函数是对原始 GAN 的扩展:

$$\min_G \max_D V(D,G) = \mathbb{E}{x\sim p[\log(1 – D(G(z|y)))]$$}(x)}[\log D(x|y)] + \mathbb{E}_{z\sim p_z(z)

其中:

  • $G(z|y)$:给定噪声 z 和条件 y 时生成器的输出
  • $D(x|y)$:判别器对真实数据 x(带条件 y)的判别概率

判别器 (Discriminator) 损失

判别器需要最大化正确分类真实 / 生成样本的能力:

$$L_D = -\mathbb{E}{x\sim p[\log(1 – D(G(z|y)))]$$}}[\log D(x|y)] – \mathbb{E}_{z\sim p_z

生成器 (Generator) 损失

生成器需要最小化被判别器识破的概率:

$$L_G = -\mathbb{E}_{z\sim p_z}[\log D(G(z|y))]$$

实际训练中更常用的是非饱和 (Non-saturating) 形式:

$$L_G = \mathbb{E}_{z\sim p_z}[\log(1 – D(G(z|y)))]$$

PyTorch 完整实现

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

# 定义生成器
class Generator(nn.Module):
    def __init__(self, latent_dim, num_classes):
        super().__init__()
        self.label_emb = nn.Embedding(num_classes, num_classes)
        self.model = nn.Sequential(nn.Linear(latent_dim + num_classes, 256),
            nn.LeakyReLU(0.2),
            nn.Linear(256, 512),
            nn.LeakyReLU(0.2),
            nn.Linear(512, 1024),
            nn.LeakyReLU(0.2),
            nn.Linear(1024, 784),
            nn.Tanh())

    def forward(self, z, labels):
        c = self.label_emb(labels)
        x = torch.cat([z, c], dim=1)
        return self.model(x)

# 定义判别器
class Discriminator(nn.Module):
    def __init__(self, num_classes):
        super().__init__()
        self.label_emb = nn.Embedding(num_classes, num_classes)
        self.model = nn.Sequential(nn.Linear(784 + num_classes, 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, labels):
        x = x.view(x.size(0), -1)
        c = self.label_emb(labels)
        x = torch.cat([x, c], dim=1)
        return self.model(x)

# 训练参数
batch_size = 64
lr = 0.0002
epochs = 100
latent_dim = 100

# 初始化网络
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
generator = Generator(latent_dim, 10).to(device)
discriminator = Discriminator(10).to(device)

# 损失函数和优化器
adversarial_loss = nn.BCELoss()
optimizer_G = optim.Adam(generator.parameters(), lr=lr)
optimizer_D = optim.Adam(discriminator.parameters(), lr=lr)

# 数据加载
transform = transforms.Compose([transforms.ToTensor(),
    transforms.Normalize((0.5,), (0.5,))
])
dataset = datasets.MNIST('data', train=True, download=True, transform=transform)
dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)

# 训练循环
for epoch in range(epochs):
    for i, (imgs, labels) in enumerate(dataloader):

        # 真实数据和噪声
        real_imgs = imgs.to(device)
        labels = labels.to(device)
        z = torch.randn(imgs.size(0), latent_dim).to(device)
        gen_labels = torch.randint(0, 10, (imgs.size(0),)).to(device)

        # 训练生成器
        optimizer_G.zero_grad()
        gen_imgs = generator(z, gen_labels)
        validity = discriminator(gen_imgs, gen_labels)
        g_loss = adversarial_loss(validity, torch.ones_like(validity))
        g_loss.backward()
        optimizer_G.step()

        # 训练判别器
        optimizer_D.zero_grad()
        real_loss = adversarial_loss(discriminator(real_imgs, labels), torch.ones_like(validity))
        fake_loss = adversarial_loss(discriminator(gen_imgs.detach(), gen_labels), torch.zeros_like(validity))
        d_loss = (real_loss + fake_loss) / 2
        d_loss.backward()
        optimizer_D.step()

    print(f"[Epoch {epoch}/{epochs}] D loss: {d_loss.item():.4f}, G loss: {g_loss.item():.4f}")

关键调参技巧

  1. 学习率选择:
  2. 推荐范围:0.0001~0.0005
  3. 判别器和生成器可以采用不同学习率(如 D: 0.0004, G: 0.0001)

  4. Batch Size 影响:

  5. 太小(<32)易导致模式崩溃
  6. 太大(>256)可能降低生成质量
  7. MNIST/CIFAR 推荐值:64~128

  8. 噪声维度:

  9. latent_dim 通常取 50~200
  10. 复杂数据集需要更高维度

常见问题与解决方案

1. 模式崩溃(Mode Collapse)

现象:生成器只产生几种相似样本
解决方法:
– 增加 mini-batch discrimination
– 使用 Wasserstein GAN(WGAN)损失
– 尝试不同的噪声分布

2. 梯度消失

现象:判别器过早收敛,生成器无法学习
解决方法:
– 使用 LeakyReLU 替代 ReLU
– 训练时先更新多次判别器
– 添加梯度惩罚(Gradient Penalty)

3. 生成质量不稳定

现象:部分 epoch 生成效果好但无法保持
解决方法:
– 使用指数移动平均 (EMA) 保存模型
– 添加谱归一化(Spectral Normalization)
– 适当降低学习率

延伸练习

  1. 尝试修改损失函数权重,观察生成效果变化
  2. 在 CIFAR-10 数据集上测试不同条件信息的编码方式
  3. 实现 WGAN-GP 版本并比较训练稳定性

总结

通过本文我们深入理解了 CGAN 损失函数的设计原理和实现细节。关键收获包括:
– 条件信息的引入方式(嵌入层 + 拼接)
– 判别器与生成器的博弈平衡
– 实际训练中的调参经验

建议初学者先从 MNIST 等简单数据集开始实验,逐步掌握损失函数对生成效果的影响规律。

正文完
 0
评论(没有评论)