从零构建DCGAN:用PyTorch实战揭秘对抗训练的损失博弈

在咖啡厅里第一次看到GAN生成的逼真人脸时,我的手抖得差点打翻杯子——这些像素组成的笑容居然完全出自算法之手。作为最早接触生成对抗网络的实践者之一,我至今记得调试第一个DCGAN时,盯着损失曲线那种又爱又恨的心情。本文将带你用PyTorch亲手搭建一个在MNIST数据集上生成手写数字的DCGAN,重点不是复现论文公式,而是通过代码和可视化,让你真正感受到判别器和生成器之间那场惊心动魄的"猫鼠游戏"。

1. 环境准备与数据加载

工欲善其事,必先利其器。我们先准备好实验所需的工具包和数据管道。建议使用Python 3.8+和PyTorch 1.10+环境,以下是我的常用配置组合:

import torch
import torch.nn as nn
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

print(f"PyTorch版本: {torch.__version__}")
print(f"CUDA可用: {torch.cuda.is_available()}")

MNIST数据集的加载需要特别注意预处理步骤。不同于普通分类任务,GAN对输入数据的尺度非常敏感。以下是经过实战验证的参数配置:

transform = transforms.Compose([
    transforms.Resize(64),  # DCGAN通常需要更大的输入尺寸
    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, num_workers=4)

提示:将像素值归一化到[-1,1]区间而非[0,1]是GAN训练的常见技巧,这能让模型在训练初期获得更好的梯度信号。

2. 构建DCGAN网络结构

2.1 生成器设计

我们的生成器采用典型的转置卷积结构,将100维的随机噪声逐步上采样为28x28的手写数字图像。注意每个转置卷积层后都跟着BatchNorm和ReLU激活(最后一层使用Tanh):

class Generator(nn.Module):
    def __init__(self, latent_dim=100):
        super().__init__()
        self.main = nn.Sequential(
            # 输入: latent_dim维噪声
            nn.ConvTranspose2d(latent_dim, 512, 4, 1, 0, bias=False),
            nn.BatchNorm2d(512),
            nn.ReLU(True),
            # 当前尺寸: (512,4,4)
            nn.ConvTranspose2d(512, 256, 4, 2, 1, bias=False),
            nn.BatchNorm2d(256),
            nn.ReLU(True),
            # 当前尺寸: (256,8,8)
            nn.ConvTranspose2d(256, 128, 4, 2, 1, bias=False),
            nn.BatchNorm2d(128),
            nn.ReLU(True),
            # 当前尺寸: (128,16,16)
            nn.ConvTranspose2d(128, 1, 4, 2, 1, bias=False),
            nn.Tanh()
            # 输出尺寸: (1,32,32)
        )

    def forward(self, input):
        return self.main(input)

关键参数说明:

参数名称 取值 作用说明
latent_dim 100 噪声向量的维度
nn.ConvTranspose2d 多层 实现上采样操作
nn.BatchNorm2d 除输出层外 稳定训练过程
nn.Tanh() 输出层 将像素值约束到[-1,1]区间

2.2 判别器设计

判别器是标准的卷积神经网络,但需要注意以下几点:

  1. 不使用池化层,改用带步长的卷积进行下采样
  2. 除最后一层外都使用LeakyReLU激活
  3. 输出层使用Sigmoid产生0-1的概率值
class Discriminator(nn.Module):
    def __init__(self):
        super().__init__()
        self.main = nn.Sequential(
            # 输入: (1,32,32)
            nn.Conv2d(1, 64, 4, 2, 1, bias=False),
            nn.LeakyReLU(0.2, inplace=True),
            # 当前尺寸: (64,16,16)
            nn.Conv2d(64, 128, 4, 2, 1, bias=False),
            nn.BatchNorm2d(128),
            nn.LeakyReLU(0.2, inplace=True),
            # 当前尺寸: (128,8,8)
            nn.Conv2d(128, 256, 4, 2, 1, bias=False),
            nn.BatchNorm2d(256),
            nn.LeakyReLU(0.2, inplace=True),
            # 当前尺寸: (256,4,4)
            nn.Conv2d(256, 1, 4, 1, 0, bias=False),
            nn.Sigmoid()
        )

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

3. 训练过程的对抗博弈

3.1 初始化与损失函数

我们先初始化模型和优化器。关键点在于:

  • 使用Adam优化器(GAN训练的金标准)
  • 为两个网络设置不同的学习率
  • 使用BCELoss作为损失函数
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

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

criterion = nn.BCELoss()

# 使用不同的学习率
d_optimizer = optim.Adam(D.parameters(), lr=0.0002, betas=(0.5, 0.999))
g_optimizer = optim.Adam(G.parameters(), lr=0.0001, betas=(0.5, 0.999))

3.2 训练循环中的动态平衡

下面是训练过程的核心代码,我们逐段分析对抗训练的精妙之处:

fixed_noise = torch.randn(64, 100, 1, 1, device=device)
d_losses, g_losses = [], []

for epoch in range(50):
    for i, (real_images, _) in enumerate(dataloader):
        batch_size = real_images.size(0)
        real_images = real_images.to(device)
        
        # 训练判别器
        d_optimizer.zero_grad()
        
        # 真实图像的损失
        real_labels = torch.ones(batch_size, device=device)
        real_output = D(real_images)
        d_loss_real = criterion(real_output, real_labels)
        
        # 生成图像的损失
        noise = torch.randn(batch_size, 100, 1, 1, device=device)
        fake_images = G(noise)
        fake_labels = torch.zeros(batch_size, device=device)
        fake_output = D(fake_images.detach())
        d_loss_fake = criterion(fake_output, fake_labels)
        
        # 总判别器损失
        d_loss = d_loss_real + d_loss_fake
        d_loss.backward()
        d_optimizer.step()
        
        # 训练生成器
        g_optimizer.zero_grad()
        output = D(fake_images)
        g_loss = criterion(output, real_labels)  # 骗过判别器
        g_loss.backward()
        g_optimizer.step()
        
        # 记录损失
        d_losses.append(d_loss.item())
        g_losses.append(g_loss.item())

对抗训练的关键观察点:

  1. 判别器损失组成

    • d_loss_real:判断真实图像的能力
    • d_loss_fake:识别生成图像的能力
    • 两者相加才是完整的判别器损失
  2. 生成器训练技巧

    • 使用fake_images而非fake_images.detach()
    • 目标标签是real_labels(试图骗过判别器)
  3. 梯度更新顺序

    • 先更新判别器,再更新生成器
    • 每次迭代中两者各更新一次

3.3 损失曲线的秘密语言

将训练过程中的损失变化可视化,你会看到GAN特有的"对抗舞蹈":

plt.figure(figsize=(10,5))
plt.plot(d_losses, label='Discriminator Loss')
plt.plot(g_losses, label='Generator Loss')
plt.xlabel('Iterations')
plt.ylabel('Loss')
plt.legend()
plt.show()

典型训练过程中可能观察到的现象:

  • 初期阶段

    • 判别器损失快速下降(轻松区分真假)
    • 生成器损失较高(生成的图像很假)
  • 中期阶段

    • 判别器损失开始回升(生成器进步)
    • 生成器损失震荡下降(尝试各种策略)
  • 后期阶段

    • 两者损失达到动态平衡
    • 剧烈震荡可能意味着模式崩溃

注意:理想的训练状态是判别器损失维持在0.5-0.7之间,这表明判别器对真假图像的判断存在合理的不确定性。

4. 实战调试技巧与可视化

4.1 训练稳定性技巧

经过数十次实验,我总结了这些提升DCGAN稳定性的技巧:

  • 学习率调整

    • 判别器学习率通常是生成器的2倍
    • 可以使用学习率衰减策略
  • 标签平滑

    real_labels = torch.FloatTensor(batch_size).uniform_(0.9, 1.0)  # 代替1.0
    fake_labels = torch.FloatTensor(batch_size).uniform_(0.0, 0.1)  # 代替0.0
    
  • 噪声添加

    # 在判别器输入中加入噪声
    real_images += 0.01 * torch.randn_like(real_images)
    

4.2 生成效果可视化

定期查看生成样本能直观评估训练效果:

def visualize_generator(G, fixed_noise, epoch):
    with torch.no_grad():
        fake_images = G(fixed_noise).detach().cpu()
    
    fig = plt.figure(figsize=(8,8))
    for i in range(64):
        plt.subplot(8,8,i+1)
        plt.imshow(fake_images[i,0], cmap='gray')
        plt.axis('off')
    plt.suptitle(f'Epoch {epoch}')
    plt.show()

# 每5个epoch可视化一次
if epoch % 5 == 0:
    visualize_generator(G, fixed_noise, epoch)

4.3 常见问题诊断

当训练出现异常时,可以通过这些信号快速定位问题:

现象 可能原因 解决方案
生成器损失降为0 判别器太弱 增强判别器容量或减少生成器学习率
判别器损失接近0 判别器过强 添加噪声或减弱判别器
生成图像模糊 模式崩溃 尝试Wasserstein GAN
损失剧烈震荡 学习率过高 降低学习率或使用Adam的beta参数

在调试过程中,保持耐心至关重要。记得我最早训练DCGAN时,花了整整两周时间才让生成的手写数字看起来像模像样。有次因为忘记zero_grad(),白白浪费了三天时间——这个错误现在想来可笑,但当时确实让我抓狂不已。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐