从‘鬼画符’到‘以假乱真’:手把手用PyTorch复现一个生成手写数字的GAN
从‘鬼画符’到‘以假乱真’:手把手用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()
典型的训练过程中,生成器的输出会经历以下几个阶段:
- 初始阶段(epoch 0-10):输出完全是随机噪声
- 中期阶段(epoch 10-50):开始出现数字的模糊轮廓
- 后期阶段(epoch 50-100):生成清晰可辨的手写数字
- 优化阶段(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可以进一步改进:
- DCGAN:使用卷积神经网络提升图像质量
- WGAN:使用Wasserstein损失改善训练稳定性
- 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率来实现。
更多推荐



所有评论(0)