从‘鬼画符’到‘以假乱真’:手把手用PyTorch复现一个生成手写数字的GAN

在深度学习领域,生成对抗网络(GAN)无疑是最令人着迷的技术之一。想象一下,计算机能够凭空创造出逼真的图像、音乐甚至视频,这听起来像是科幻小说中的情节。但通过GAN,这一切正在成为现实。本文将带你从零开始,用PyTorch框架构建一个能够生成手写数字的GAN模型,让你亲身体验这种神奇技术的魅力。

对于有一定Python和深度学习基础的开发者来说,这是一个绝佳的实践机会。我们将使用经典的MNIST数据集,一步步搭建并训练一个简单的生成对抗网络。通过这个过程,你不仅能理解GAN的核心原理,还能亲眼见证模型从最初的"鬼画符"逐渐进步到生成足以"以假乱真"的手写数字的全过程。

1. GAN基础与准备工作

1.1 GAN的核心思想

生成对抗网络由两个相互对抗的神经网络组成:生成器(Generator)和判别器(Discriminator)。它们就像艺术伪造者与鉴定专家之间的猫鼠游戏:

  • 生成器:接收随机噪声作为输入,试图生成与真实数据相似的假样本
  • 判别器:接收真实样本和生成器产生的假样本,试图区分它们的真伪

这种对抗训练的过程可以用以下伪代码表示:

for epoch in range(num_epochs):
    # 训练判别器
    for real_data in dataloader:
        noise = torch.randn(batch_size, noise_dim)
        fake_data = generator(noise)
        
        # 计算判别器对真实和假数据的判断
        d_real = discriminator(real_data)
        d_fake = discriminator(fake_data.detach())
        
        # 更新判别器
        
    # 训练生成器
    noise = torch.randn(batch_size, noise_dim)
    fake_data = generator(noise)
    d_fake = discriminator(fake_data)
    
    # 更新生成器

1.2 环境配置与数据准备

在开始编码前,我们需要准备好开发环境。建议使用Python 3.7+和PyTorch 1.8+版本。以下是必要的库安装命令:

pip install torch torchvision matplotlib numpy

MNIST数据集是手写数字识别的基准数据集,包含60,000张训练图像和10,000张测试图像。PyTorch提供了方便的接口来加载和处理这些数据:

from torchvision import datasets, transforms

transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5,), (0.5,))
])

train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=64, shuffle=True)

2. 构建生成器网络

2.1 生成器架构设计

我们的生成器将采用全连接神经网络结构,它接收一个随机噪声向量(通常维度为100)作为输入,输出一个28×28的手写数字图像。以下是生成器的PyTorch实现:

import torch.nn as nn

class Generator(nn.Module):
    def __init__(self, noise_dim=100, img_size=28):
        super(Generator, self).__init__()
        self.fc = nn.Sequential(
            nn.Linear(noise_dim, 256),
            nn.LeakyReLU(0.2),
            nn.Linear(256, 512),
            nn.LeakyReLU(0.2),
            nn.Linear(512, 1024),
            nn.LeakyReLU(0.2),
            nn.Linear(1024, img_size * img_size),
            nn.Tanh()  # 输出在[-1,1]之间
        )
        self.img_size = img_size
        
    def forward(self, z):
        img = self.fc(z)
        return img.view(-1, 1, self.img_size, self.img_size)

提示:使用LeakyReLU而不是普通的ReLU可以缓解梯度消失问题,这在GAN训练中尤为重要。

2.2 生成器的工作原理

生成器的目标是学习训练数据的分布,将随机噪声映射到数据空间。我们可以通过以下步骤测试生成器的初始输出:

import matplotlib.pyplot as plt

noise_dim = 100
generator = Generator(noise_dim)

# 生成随机噪声
test_noise = torch.randn(1, noise_dim)

# 生成图像
with torch.no_grad():
    test_img = generator(test_noise).squeeze().numpy()

plt.imshow(test_img, cmap='gray')
plt.show()

初始阶段,生成器输出的只是随机噪声,看起来就像"鬼画符"。但随着训练的进行,这些噪声将逐渐形成可识别的手写数字。

3. 构建判别器网络

3.1 判别器架构设计

判别器是一个二分类网络,它接收一张图像(真实或生成的),输出一个0到1之间的值,表示图像为真实数据的概率。以下是判别器的实现:

class Discriminator(nn.Module):
    def __init__(self, img_size=28):
        super(Discriminator, self).__init__()
        self.fc = nn.Sequential(
            nn.Linear(img_size * img_size, 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, img):
        flattened = img.view(img.size(0), -1)
        validity = self.fc(flattened)
        return validity

3.2 判别器的训练策略

判别器的训练需要同时考虑真实数据和生成数据:

# 定义损失函数和优化器
criterion = nn.BCELoss()
d_optimizer = torch.optim.Adam(discriminator.parameters(), lr=0.0002)
g_optimizer = torch.optim.Adam(generator.parameters(), lr=0.0002)

# 真实数据标签为1,假数据标签为0
real_labels = torch.ones(batch_size, 1)
fake_labels = torch.zeros(batch_size, 1)

# 判别器训练步骤
d_real_loss = criterion(discriminator(real_data), real_labels)
d_fake_loss = criterion(discriminator(fake_data.detach()), fake_labels)
d_loss = d_real_loss + d_fake_loss

d_optimizer.zero_grad()
d_loss.backward()
d_optimizer.step()

4. GAN的训练过程与技巧

4.1 完整的训练循环

GAN的训练需要交替优化生成器和判别器。以下是完整的训练循环框架:

for epoch in range(num_epochs):
    for i, (real_imgs, _) in enumerate(train_loader):
        # 准备真实数据和噪声
        real_imgs = real_imgs.to(device)
        noise = torch.randn(batch_size, noise_dim).to(device)
        
        # 生成假图像
        fake_imgs = generator(noise)
        
        # 训练判别器
        d_optimizer.zero_grad()
        
        real_loss = criterion(discriminator(real_imgs), real_labels)
        fake_loss = criterion(discriminator(fake_imgs.detach()), fake_labels)
        d_loss = real_loss + fake_loss
        
        d_loss.backward()
        d_optimizer.step()
        
        # 训练生成器
        g_optimizer.zero_grad()
        
        g_loss = criterion(discriminator(fake_imgs), real_labels)
        
        g_loss.backward()
        g_optimizer.step()

4.2 训练可视化与监控

为了监控训练进度,我们可以定期保存生成器输出的样本图像:

def save_sample_images(epoch, generator, noise):
    with torch.no_grad():
        fake_imgs = generator(noise).cpu()
    
    fig, axes = plt.subplots(1, 10, figsize=(20, 2))
    for i, ax in enumerate(axes):
        ax.imshow(fake_imgs[i].squeeze(), cmap='gray')
        ax.axis('off')
    plt.savefig(f"gan_samples/epoch_{epoch}.png")
    plt.close()

典型的训练过程中,生成器的输出会经历以下几个阶段:

  1. 初始阶段(epoch 0-10):输出完全是随机噪声
  2. 中期阶段(epoch 10-50):开始出现数字的模糊轮廓
  3. 后期阶段(epoch 50-100):生成清晰可辨的手写数字
  4. 优化阶段(epoch 100+):数字更加多样化和逼真

4.3 常见问题与解决方案

GAN训练中常会遇到以下问题:

问题表现解决方案
模式崩溃生成器只产生少量模式样本增加噪声维度、使用Mini-batch判别
判别器过强生成器无法学习减少判别器更新频率、调整学习率
梯度消失训练停滞使用LeakyReLU、Wasserstein GAN
训练不稳定损失剧烈波动使用标签平滑、梯度裁剪

5. 模型优化与进阶技巧

5.1 超参数调优

GAN对超参数非常敏感。以下是几个关键参数的建议范围:

  • 噪声维度(z_dim): 50-200
  • 学习率: 0.0001-0.0005
  • 批量大小: 64-256
  • 优化器: Adam(β1=0.5, β2=0.999)

可以通过网格搜索或随机搜索找到最佳组合:

from itertools import product

z_dims = [50, 100, 200]
learning_rates = [0.0001, 0.0002, 0.0005]
batch_sizes = [64, 128, 256]

for z_dim, lr, batch_size in product(z_dims, learning_rates, batch_sizes):
    print(f"Testing z_dim={z_dim}, lr={lr}, batch_size={batch_size}")
    # 初始化模型和训练...

5.2 架构改进

基础GAN可以进一步改进:

  1. DCGAN:使用卷积神经网络提升图像质量
  2. WGAN:使用Wasserstein损失改善训练稳定性
  3. Conditional GAN:添加标签信息控制生成内容

例如,DCGAN的生成器可以这样实现:

class DCGenerator(nn.Module):
    def __init__(self, noise_dim=100):
        super(DCGenerator, self).__init__()
        self.main = nn.Sequential(
            # 输入是Z, 进入全连接
            nn.ConvTranspose2d(noise_dim, 512, 4, 1, 0, bias=False),
            nn.BatchNorm2d(512),
            nn.ReLU(True),
            # 上采样到8x8
            nn.ConvTranspose2d(512, 256, 4, 2, 1, bias=False),
            nn.BatchNorm2d(256),
            nn.ReLU(True),
            # 上采样到16x16
            nn.ConvTranspose2d(256, 128, 4, 2, 1, bias=False),
            nn.BatchNorm2d(128),
            nn.ReLU(True),
            # 上采样到32x32
            nn.ConvTranspose2d(128, 1, 4, 2, 1, bias=False),
            nn.Tanh()
        )
    
    def forward(self, input):
        return self.main(input)

5.3 评估生成质量

评估GAN生成质量的方法包括:

  • 人工评估:直观检查生成样本
  • Inception Score:衡量生成图像的多样性和质量
  • FID(Fréchet Inception Distance):比较真实和生成图像分布的相似度

计算FID得分的示例代码:

from torchmetrics.image.fid import FrechetInceptionDistance

fid = FrechetInceptionDistance(feature=64)
# 真实图像
fid.update(real_imgs, real=True)
# 生成图像
fid.update(fake_imgs, real=False)
fid_score = fid.compute()
print(f"FID score: {fid_score}")

在实际项目中,我发现调整噪声维度和学习率的组合对生成质量影响最大。经过多次实验,噪声维度设为100,生成器和判别器的学习率都设为0.0002时,模型收敛最快且生成效果最好。另一个实用技巧是在训练初期适当降低判别器的学习能力,给生成器更多学习机会,这可以通过减少判别器的层数或增加dropout率来实现。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐