备注
点击此处下载完整示例代码
DCGAN 教程 ¶
创建于:2025 年 4 月 1 日 | 最后更新:2025 年 4 月 1 日 | 最后验证:2024 年 11 月 5 日
作者:Nathan Inkawhich
简介
本教程将通过示例介绍 DCGANs。我们将训练一个生成对抗网络(GAN),在展示许多真实名人的图片后,生成新的名人图片。这里的大部分代码来自 pytorch/examples 中的 DCGAN 实现,本文档将对实现进行详细解释,并阐明该模型是如何以及为什么能工作的。但别担心,不需要有 GAN 的先验知识,但初学者可能需要花时间思考底层实际发生了什么。此外,为了节省时间,拥有 GPU 或两个将有所帮助。让我们从开始讲起。
生成对抗网络
什么是 GAN?
生成对抗网络(GANs)是一种框架,用于教会深度学习模型捕捉训练数据分布,以便我们可以从这个相同的分布中生成新的数据。GANs 由 Ian Goodfellow 于 2014 年发明,并在论文《生成对抗网络》中首次描述。它们由两个不同的模型组成,一个生成器和一个人工智能。生成器的任务是生成看起来像训练图像的“假”图像。判别器的任务是观察图像并输出它是否是真实的训练图像或来自生成器的假图像。在训练过程中,生成器不断尝试通过生成越来越好的假图像来欺骗判别器,而判别器则努力成为一个更好的侦探,正确地分类真实和假图像。这个游戏的平衡点在于生成器生成完美的假图像,看起来就像直接来自训练数据,而判别器则被迫始终以 50%的置信度猜测生成器的输出是真实还是假。
现在,让我们定义在整个教程中将要使用的符号,从判别器开始。设 \(x\) 为表示图像的数据。\(D(x)\) 是判别器网络,它输出 \(x\) 来自训练数据而不是生成器的(标量)概率。在这里,由于我们处理的是图像,\(D(x)\) 的输入是一个 CHW 大小为 3x64x64 的图像。直观上,当 \(x\) 来自训练数据时,\(D(x)\) 应该是 HIGH,当 \(x\) 来自生成器时,\(D(x)\) 应该是 LOW。\(D(x)\) 也可以被视为一个传统的二分类器。
对于生成器的符号,设 \(z\) 为从标准正态分布采样的潜在空间向量。\(G(z)\) 表示生成器函数,它将潜在向量 \(z\) 映射到数据空间。\(G\) 的目标是估计训练数据来自的分布 (\(p_{data}\)),以便它可以生成来自该估计分布的假样本 (\(p_g\))。
因此,\(D(G(z))\) 是生成器 \(G\) 输出为真实图像的概率(标量)。正如 Goodfellow 的论文中所述,\(D\) 和 \(G\) 在一个最小-最大游戏中扮演着角色,其中 \(D\) 尝试最大化其正确分类真实和伪造图像的概率(\(logD(x)\)),而 \(G\) 尝试最小化 \(D\) 预测其输出为伪造图像的概率(\(log(1-D(G(z)))\))。从论文中可以看出,GAN 的损失函数是
理论上,这个最小-最大游戏的解是 \(p_g = p_{data}\),此时判别器随机猜测输入是真实还是伪造。然而,GAN 的收敛理论仍在积极研究中,现实中模型并不总是训练到这一点。
什么是 DCGAN?
DCGAN 是上述 GAN 的直接扩展,不同之处在于它明确地在判别器和生成器中分别使用了卷积和转置卷积层。它首先由 Radford 等人在其论文《使用深度卷积生成对抗网络的无监督表示学习》中描述。判别器由步长卷积层、批量归一化层和 LeakyReLU 激活组成。输入是一个 3x64x64 的输入图像,输出是一个表示输入来自真实数据分布的标量概率。生成器由转置卷积层、批量归一化层和 ReLU 激活组成。输入是一个从标准正态分布中抽取的潜在向量\(z\),输出是一个 3x64x64 的 RGB 图像。步长转置卷积层允许将潜在向量转换为与图像相同形状的体积。在论文中,作者还提供了一些关于如何设置优化器、如何计算损失函数以及如何初始化模型权重的建议,这些内容将在接下来的章节中解释。
#%matplotlib inline
import argparse
import os
import random
import torch
import torch.nn as nn
import torch.nn.parallel
import torch.optim as optim
import torch.utils.data
import torchvision.datasets as dset
import torchvision.transforms as transforms
import torchvision.utils as vutils
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from IPython.display import HTML
# Set random seed for reproducibility
manualSeed = 999
#manualSeed = random.randint(1, 10000) # use if you want new results
print("Random Seed: ", manualSeed)
random.seed(manualSeed)
torch.manual_seed(manualSeed)
torch.use_deterministic_algorithms(True) # Needed for reproducible results
输入
让我们定义一些运行所需的输入:
dataroot
- 数据集根目录的路径。我们将在下一节中详细介绍数据集。workers
- 使用DataLoader
加载数据的工作线程数量。batch_size
- 训练中使用的批量大小。DCGAN 论文使用 128 的批量大小。image_size
- 用于训练的图像的空间大小。此实现默认为 64x64。如果需要其他大小,D 和 G 的结构必须更改。更多详情请参阅此处。输入图像中的颜色通道数。对于彩色图像,这是 3。
潜在向量的长度。
与生成器中携带的特征图深度相关。
设置通过判别器传播的特征图深度。
num_epochs
- 运行的训练轮数。训练时间更长可能会带来更好的结果,但也会花费更多时间。lr
- 训练的学习率。如 DCGAN 论文所述,这个数字应该是 0.0002。beta1
- Adam 优化器的 beta1 超参数。如论文所述,这个数字应该是 0.5。ngpu
- 可用的 GPU 数量。如果这个数字是 0,代码将在 CPU 模式下运行。如果这个数字大于 0,它将在那么多 GPU 上运行。
# Root directory for dataset
dataroot = "data/celeba"
# Number of workers for dataloader
workers = 2
# Batch size during training
batch_size = 128
# Spatial size of training images. All images will be resized to this
# size using a transformer.
image_size = 64
# Number of channels in the training images. For color images this is 3
nc = 3
# Size of z latent vector (i.e. size of generator input)
nz = 100
# Size of feature maps in generator
ngf = 64
# Size of feature maps in discriminator
ndf = 64
# Number of training epochs
num_epochs = 5
# Learning rate for optimizers
lr = 0.0002
# Beta1 hyperparameter for Adam optimizers
beta1 = 0.5
# Number of GPUs available. Use 0 for CPU mode.
ngpu = 1
数据 ¶
在本教程中,我们将使用 Celeb-A 人脸数据集,该数据集可以从链接网站或 Google Drive 下载。数据集将下载为名为 img_align_celeba.zip
的文件。下载完成后,创建一个名为 celeba
的目录,并将 zip 文件提取到该目录中。然后,将此笔记本的 dataroot
输入设置为刚刚创建的 celeba
目录。生成的目录结构应如下所示:
/path/to/celeba
-> img_align_celeba
-> 188242.jpg
-> 173822.jpg
-> 284702.jpg
-> 537394.jpg
...
这是一个重要的步骤,因为我们将使用 ImageFolder
数据集类,该类要求在数据集根目录中存在子目录。现在,我们可以创建数据集,创建数据加载器,设置运行设备,并最终可视化一些训练数据。
# We can use an image folder dataset the way we have it setup.
# Create the dataset
dataset = dset.ImageFolder(root=dataroot,
transform=transforms.Compose([
transforms.Resize(image_size),
transforms.CenterCrop(image_size),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),
]))
# Create the dataloader
dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size,
shuffle=True, num_workers=workers)
# Decide which device we want to run on
device = torch.device("cuda:0" if (torch.cuda.is_available() and ngpu > 0) else "cpu")
# Plot some training images
real_batch = next(iter(dataloader))
plt.figure(figsize=(8,8))
plt.axis("off")
plt.title("Training Images")
plt.imshow(np.transpose(vutils.make_grid(real_batch[0].to(device)[:64], padding=2, normalize=True).cpu(),(1,2,0)))
plt.show()
实现 ¶
我们的输入参数设置完毕,数据集准备就绪后,我们现在可以进入实现阶段。我们将从权重初始化策略开始,然后详细讨论生成器、判别器、损失函数和训练循环。
权重初始化
根据 DCGAN 论文,作者指定所有模型权重应从正态分布中随机初始化为 mean=0
, stdev=0.02
。 weights_init
函数接受一个初始化的模型作为输入,并将所有卷积、转置卷积和批归一化层重新初始化以满足此标准。此函数在初始化模型后立即应用。
# custom weights initialization called on ``netG`` and ``netD``
def weights_init(m):
classname = m.__class__.__name__
if classname.find('Conv') != -1:
nn.init.normal_(m.weight.data, 0.0, 0.02)
elif classname.find('BatchNorm') != -1:
nn.init.normal_(m.weight.data, 1.0, 0.02)
nn.init.constant_(m.bias.data, 0)
生成器
生成器 \(G\) 被设计用来将潜在空间向量 \((z)\) 映射到数据空间。由于我们的数据是图像,将 \(z\) 转换为数据空间意味着最终创建一个与训练图像大小相同的 RGB 图像(即 3x64x64)。在实践中,这是通过一系列步长二维转置卷积层来实现的,每个层都配对了一个 2d 批量归一化层和 relu 激活函数。生成器的输出通过 tanh 函数传递,以返回到输入数据的范围 \([-1,1]\)。值得注意的是,在卷积转置层之后存在批量归一化函数,这是 DCGAN 论文的一个关键贡献。这些层有助于训练过程中的梯度流动。下面是 DCGAN 论文中生成器图像的示例。

注意,我们在输入部分设置( nz
、 ngf
和 nc
)的输入如何影响生成器架构的代码。 nz
是 z 输入向量的长度, ngf
与通过生成器传播的特征图大小相关, nc
是输出图像中的通道数(对于 RGB 图像设置为 3)。以下是生成器的代码。
# Generator Code
class Generator(nn.Module):
def __init__(self, ngpu):
super(Generator, self).__init__()
self.ngpu = ngpu
self.main = nn.Sequential(
# input is Z, going into a convolution
nn.ConvTranspose2d( nz, ngf * 8, 4, 1, 0, bias=False),
nn.BatchNorm2d(ngf * 8),
nn.ReLU(True),
# state size. ``(ngf*8) x 4 x 4``
nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 4),
nn.ReLU(True),
# state size. ``(ngf*4) x 8 x 8``
nn.ConvTranspose2d( ngf * 4, ngf * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 2),
nn.ReLU(True),
# state size. ``(ngf*2) x 16 x 16``
nn.ConvTranspose2d( ngf * 2, ngf, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf),
nn.ReLU(True),
# state size. ``(ngf) x 32 x 32``
nn.ConvTranspose2d( ngf, nc, 4, 2, 1, bias=False),
nn.Tanh()
# state size. ``(nc) x 64 x 64``
)
def forward(self, input):
return self.main(input)
现在,我们可以实例化生成器并应用 weights_init
函数。查看打印出的模型,以了解生成器对象的构成。
# Create the generator
netG = Generator(ngpu).to(device)
# Handle multi-GPU if desired
if (device.type == 'cuda') and (ngpu > 1):
netG = nn.DataParallel(netG, list(range(ngpu)))
# Apply the ``weights_init`` function to randomly initialize all weights
# to ``mean=0``, ``stdev=0.02``.
netG.apply(weights_init)
# Print the model
print(netG)
判别器
如前所述,判别器\(D\)是一个二分类网络,它接受一个图像作为输入,并输出一个标量概率,表示输入图像是真实的(而不是伪造的)。在这里,\(D\)接受一个 3x64x64 的输入图像,通过一系列的 Conv2d、BatchNorm2d 和 LeakyReLU 层进行处理,并通过 Sigmoid 激活函数输出最终的概率。如果需要,这种架构可以通过添加更多层来扩展,但使用步长卷积、批归一化和 LeakyReLUs 有其重要性。DCGAN 论文提到,使用步长卷积而不是池化是一种好的实践,因为它允许网络学习自己的池化函数。此外,批归一化和 LeakyReLU 函数促进了健康的梯度流动,这对于\(G\)和\(D\)的学习过程至关重要。
判别器代码
class Discriminator(nn.Module):
def __init__(self, ngpu):
super(Discriminator, self).__init__()
self.ngpu = ngpu
self.main = nn.Sequential(
# input is ``(nc) x 64 x 64``
nn.Conv2d(nc, ndf, 4, 2, 1, bias=False),
nn.LeakyReLU(0.2, inplace=True),
# state size. ``(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),
# state size. ``(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),
# state size. ``(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),
# state size. ``(ndf*8) x 4 x 4``
nn.Conv2d(ndf * 8, 1, 4, 1, 0, bias=False),
nn.Sigmoid()
)
def forward(self, input):
return self.main(input)
现在,就像生成器一样,我们可以创建判别器,应用 weights_init
函数,并打印模型的架构。
# Create the Discriminator
netD = Discriminator(ngpu).to(device)
# Handle multi-GPU if desired
if (device.type == 'cuda') and (ngpu > 1):
netD = nn.DataParallel(netD, list(range(ngpu)))
# Apply the ``weights_init`` function to randomly initialize all weights
# like this: ``to mean=0, stdev=0.2``.
netD.apply(weights_init)
# Print the model
print(netD)
损失函数和优化器
在设置好 D 和 G 之后,我们可以指定它们通过损失函数和优化器如何学习。我们将使用二进制交叉熵损失函数(BCELoss),它在 PyTorch 中定义为:
注意这个函数提供了目标函数中两个对数成分的计算(即 \(log(D(x))\) 和 \(log(1-D(G(z)))\))。我们可以通过 \(y\) 输入来指定使用 BCE 方程的哪一部分。这是在接下来的训练循环中完成的,但了解我们如何仅通过更改 \(y\)(即 GT 标签)来选择要计算的成分是很重要的。
接下来,我们将真实标签定义为 1,将伪造标签定义为 0。这些标签将在计算 \(D\) 和 \(G\) 的损失时使用,这也是原始 GAN 论文中使用的约定。最后,我们为 \(D\) 和 \(G\) 设置了两个单独的优化器。根据 DCGAN 论文的指定,这两个都是学习率为 0.0002,Beta1 = 0.5 的 Adam 优化器。为了跟踪生成器的学习进度,我们将生成一个固定批次的从高斯分布中抽取的潜在向量(即固定噪声)。在训练循环中,我们将定期将这个固定噪声输入到 \(G\) 中,并在迭代过程中看到图像从噪声中形成。
# Initialize the ``BCELoss`` function
criterion = nn.BCELoss()
# Create batch of latent vectors that we will use to visualize
# the progression of the generator
fixed_noise = torch.randn(64, nz, 1, 1, device=device)
# Establish convention for real and fake labels during training
real_label = 1.
fake_label = 0.
# Setup Adam optimizers for both G and D
optimizerD = optim.Adam(netD.parameters(), lr=lr, betas=(beta1, 0.999))
optimizerG = optim.Adam(netG.parameters(), lr=lr, betas=(beta1, 0.999))
训练 ¶
终于,现在我们已经定义了 GAN 框架的所有部分,我们可以开始训练它了。请注意,训练 GAN 是一种艺术形式,因为错误的超参数设置会导致模式崩溃,而错误的原因却难以解释。在这里,我们将紧密遵循 Goodfellow 论文中的算法 1,同时遵循 ganhacks 中展示的一些最佳实践。具体来说,我们将“为真实和伪造图像构建不同的迷你批次”,并且调整 G 的目标函数以最大化\(log(D(G(z)))\)。训练分为两个主要部分。第一部分更新判别器,第二部分更新生成器。
第一部分 - 训练判别器
回顾,训练判别器的目标是最大化正确分类给定输入为真实或虚假的概率。在 Goodfellow 的术语中,我们希望“通过上升判别器的随机梯度来更新判别器”。实际上,我们希望最大化 \(log(D(x)) + log(1-D(G(z)))\)。由于 ganhacks 提出的单独的 mini-batch 建议,我们将分两步计算。首先,我们将从训练集中构造一个真实样本批次,通过 \(D\) 前向传递,计算损失(\(log(D(x))\)),然后在反向传递中计算梯度。其次,我们将使用当前生成器构造一个假样本批次,通过 \(D\) 前向传递这个批次,计算损失(\(log(1-D(G(z)))\)),并在反向传递中累积梯度。现在,有了从所有真实和所有假批次累积的梯度,我们调用判别器优化器的一步。
第二部分 - 训练生成器
如原文所述,我们希望通过最小化 \(log(1-D(G(z)))\) 来训练生成器,以生成更好的伪造品。正如所提到的,Goodfellow 已经证明这不能提供足够的梯度,尤其是在学习过程的早期。作为修复方案,我们希望最大化 \(log(D(G(z)))\)。在代码中,我们通过以下方式实现:将第一部分的生成器输出分类为判别器的输出,使用真实标签作为 GT 计算 G 的损失,在反向传播中计算 G 的梯度,并最终使用优化器步骤更新 G 的参数。使用真实标签作为损失函数的 GT 标签可能看起来有些反直觉,但这允许我们使用 \(log(x)\) 部分(而不是 \(log(1-x)\) 部分),这正是我们想要的。
最后,我们将进行一些统计报告,并在每个 epoch 结束时将我们的固定噪声批次通过生成器,以视觉跟踪 G 的训练进度。报告的训练统计信息包括:
Loss_D - 刨除器损失,计算为所有真实和所有伪造批次损失的总和(\(log(D(x)) + log(1 - D(G(z)))\))。
Loss_G - 生成器损失,计算为 \(log(D(G(z)))\)。
D(x) - 刨除器对所有真实批次输出的平均值(跨批次)。这应该接近 1,然后理论上收敛到 0.5,当 G 变得更好时。想想为什么是这样。
D(G(z)) - 对所有伪造批次平均的刨除器输出。第一个数字是在 D 更新之前,第二个数字是在 D 更新之后。这些数字应该接近 0,当 G 变得更好时收敛到 0.5。想想为什么是这样。
注意:此步骤可能需要一段时间,具体取决于您运行了多少个 epoch 以及是否从数据集中删除了一些数据。
# Training Loop
# Lists to keep track of progress
img_list = []
G_losses = []
D_losses = []
iters = 0
print("Starting Training Loop...")
# For each epoch
for epoch in range(num_epochs):
# For each batch in the dataloader
for i, data in enumerate(dataloader, 0):
############################
# (1) Update D network: maximize log(D(x)) + log(1 - D(G(z)))
###########################
## Train with all-real batch
netD.zero_grad()
# Format batch
real_cpu = data[0].to(device)
b_size = real_cpu.size(0)
label = torch.full((b_size,), real_label, dtype=torch.float, device=device)
# Forward pass real batch through D
output = netD(real_cpu).view(-1)
# Calculate loss on all-real batch
errD_real = criterion(output, label)
# Calculate gradients for D in backward pass
errD_real.backward()
D_x = output.mean().item()
## Train with all-fake batch
# Generate batch of latent vectors
noise = torch.randn(b_size, nz, 1, 1, device=device)
# Generate fake image batch with G
fake = netG(noise)
label.fill_(fake_label)
# Classify all fake batch with D
output = netD(fake.detach()).view(-1)
# Calculate D's loss on the all-fake batch
errD_fake = criterion(output, label)
# Calculate the gradients for this batch, accumulated (summed) with previous gradients
errD_fake.backward()
D_G_z1 = output.mean().item()
# Compute error of D as sum over the fake and the real batches
errD = errD_real + errD_fake
# Update D
optimizerD.step()
############################
# (2) Update G network: maximize log(D(G(z)))
###########################
netG.zero_grad()
label.fill_(real_label) # fake labels are real for generator cost
# Since we just updated D, perform another forward pass of all-fake batch through D
output = netD(fake).view(-1)
# Calculate G's loss based on this output
errG = criterion(output, label)
# Calculate gradients for G
errG.backward()
D_G_z2 = output.mean().item()
# Update G
optimizerG.step()
# Output training stats
if i % 50 == 0:
print('[%d/%d][%d/%d]\tLoss_D: %.4f\tLoss_G: %.4f\tD(x): %.4f\tD(G(z)): %.4f / %.4f'
% (epoch, num_epochs, i, len(dataloader),
errD.item(), errG.item(), D_x, D_G_z1, D_G_z2))
# Save Losses for plotting later
G_losses.append(errG.item())
D_losses.append(errD.item())
# Check how the generator is doing by saving G's output on fixed_noise
if (iters % 500 == 0) or ((epoch == num_epochs-1) and (i == len(dataloader)-1)):
with torch.no_grad():
fake = netG(fixed_noise).detach().cpu()
img_list.append(vutils.make_grid(fake, padding=2, normalize=True))
iters += 1
结果 ¶
最后,让我们看看我们的表现如何。在这里,我们将查看三个不同的结果。首先,我们将看到 D 和 G 的损失在训练过程中的变化。其次,我们将可视化 G 在固定噪声批次上的输出,每个 epoch 一次。第三,我们将查看一个真实数据批次与 G 生成的假数据批次并排。
损失与训练迭代的关系
下面是 D & G 的损失与训练迭代次数的图表。
plt.figure(figsize=(10,5))
plt.title("Generator and Discriminator Loss During Training")
plt.plot(G_losses,label="G")
plt.plot(D_losses,label="D")
plt.xlabel("iterations")
plt.ylabel("Loss")
plt.legend()
plt.show()
G 的进展可视化
记得我们在每个训练周期结束后,将生成器的输出保存到 fixed_noise 批次中。现在,我们可以通过动画来可视化 G 的训练进展。点击播放按钮开始动画。
fig = plt.figure(figsize=(8,8))
plt.axis("off")
ims = [[plt.imshow(np.transpose(i,(1,2,0)), animated=True)] for i in img_list]
ani = animation.ArtistAnimation(fig, ims, interval=1000, repeat_delay=1000, blit=True)
HTML(ani.to_jshtml())
真实图像与伪造图像对比
最后,让我们看看一些真实图像和伪造图像并排的情况。
# Grab a batch of real images from the dataloader
real_batch = next(iter(dataloader))
# Plot the real images
plt.figure(figsize=(15,15))
plt.subplot(1,2,1)
plt.axis("off")
plt.title("Real Images")
plt.imshow(np.transpose(vutils.make_grid(real_batch[0].to(device)[:64], padding=5, normalize=True).cpu(),(1,2,0)))
# Plot the fake images from the last epoch
plt.subplot(1,2,2)
plt.axis("off")
plt.title("Fake Images")
plt.imshow(np.transpose(img_list[-1],(1,2,0)))
plt.show()
接下来去哪里
我们的旅程已经结束,但您可以从这里去几个地方。您可以:
增加训练时间,看看结果能有多好
修改此模型以使用不同的数据集,并可能更改图像大小和模型架构
在这里查看一些其他酷炫的 GAN 项目
创建生成音乐的 GAN
脚本总运行时间:(0 分钟 0.000 秒)