5.4 DCGAN实战:深度卷积生成对抗网络 生成对抗网络是指一类采用对抗训练方式来进行学习的深度生成模型,其包含的判别网络和生成网络都可以根据不同的生成任务使用不同的网络结构。 本节介绍一个生成对抗网络的具体模型:深度卷积生成对抗网络(Deep Convolutional Generative Adversarial Network,DCGAN)[Radford et al., 2016]。在 DCGAN 中,判别网络是一个传统的深度卷积网络,但使用了带步长的卷积来实现下采样操作,不用最大汇聚(pooling)操作;生成网络使用一个特殊的深度卷积网络来实现,使用微步卷积来生成64 × 64大小的图像。 5.4.
生成对抗网络是指一类采用对抗训练方式来进行学习的深度生成模型,其包含的判别网络和生成网络都可以根据不同的生成任务使用不同的网络结构。
本节介绍一个生成对抗网络的具体模型:深度卷积生成对抗网络(Deep Convolutional Generative Adversarial Network,DCGAN)[Radford et al., 2016]。在 DCGAN 中,判别网络是一个传统的深度卷积网络,但使用了带步长的卷积来实现下采样操作,不用最大汇聚(pooling)操作;生成网络使用一个特殊的深度卷积网络来实现,使用微步卷积来生成64 × 64大小的图像。
DCGAN在GAN的基础上优化了网络结构,加入了卷积层(Conv)、转置卷积 (ConvTranspose)、**批量正则(Batch_norm)**等层,使得网络更容易训练,下图为使用卷积层的DCGAN的生成器网络结构示意图。

可以看出,生成器的输入是一个 100 维的噪声,中间会通过 4 层卷积层,每通过一个卷积层通道数减半,长宽扩大一倍 ,最终产生一个 64*64*3 大小的图片输出。值得说明的是,在很多引用 DCGAN 的paper中,误以为这 4 个卷积层是Wide Convolution(宽卷积)层,但其实在DCGAN 的介绍中这 4 个卷积层是 Fractionally Strided Convolution(微步幅度卷积)层,二者的差别如下图所示:
上图左边是宽卷积,用 3*3 的卷积核把 2*2 的矩阵反卷积成 4*4 的矩阵;而右边是微步幅度卷积,用 3*3 的卷积核把 3*3 的矩阵卷积成 5*5 的矩阵,二者的差别在于,宽卷积是在整个输入矩阵周围添 0,而微步幅度卷积会把输入矩阵拆开,在每一个像素点的周围添 0。
上述的两种从低维特征映射到高维特征的卷积操作称为转置卷积(Transposed Convolution)[Dumoulin et al., 2016],也称为反卷积(Deconvolution)[Zeiler et al., 2011]。
转置卷积的动图见https://nndl.github.io/v/cnn-conv-more
nz是z输入向量的长度,ngf与通过生成器传播的特征图的大小有关,nc是输出图像中的通道数(对于RGB图像设置为3)。
以下是生成器的代码:
# 生成器代码 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)
判别器网络是一个二进制分类网络,该二进制分类网络将图像(3 * 64 * 64)作为输入并输出输入图像是真实的(与假的相对)的标量概率。D 可以看成是 G 结构反过来的样子,简而言之通过一系列的Conv2d,BatchNorm2d和LeakyReLU层对其进行处理,然后通过Sigmoid激活函数输出最终概率,最终得到一个 1024 * 4 * 4 的结果,再通过view(-1)进行展开成一维tensor。
如果需要解决此问题,则可以用更多层扩展此体系结构,但是使用跨步卷积,BatchNorm和LeakyReLU仍然具有重要意义。 DCGAN论文提到,使用跨步卷积而不是通过池化来进行下采样是一个好习惯,因为它可以让网络学习自己的池化功能。 BatchNorm和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)
最后,既然我们已经定义了GAN框架的所有部分,我们就可以对其进行训练。请注意,训练GAN某种程度上是一种艺术形式,因为不正确的超参数设置会导致模式崩溃,而对失败的原因几乎没有解释。在这里,我们将严格遵循Goodfellow论文中的算法,同时遵守ganhacks(https://github.com/soumith/ganhacks)中显示的一些最佳做法。即,我们将为真实和伪造构建不同的小批量图像,并调整G的目标函数以最大化log(1 − D(G(z)))。
模型的训练主要分为两个部分。第1部分更新了判别器,第2部分更新了生成器。
回想一下,训练判别器的目的是最大程度地提高将给定图片正确分类的可能性。实际上,我们要\max \log(D(x))+\log(1-D(G(z)))。由于ganhacks提出了单独的小批量建议,因此我们将分两步进行计算以至可以“通过提升随机梯度来更新鉴别器”
如原始论文所述,我们希望通过最小化log(1-D(G(z)))来训练生成器,以产生更好的fake image。如前所述,Goodfellow证明这不能提供较好的梯度,尤其是在学习过程的早期。作为解决方法,我们希望最大化log(D(G(z))。
在代码中,我们通过以下方式实现此目的:使用判别器对第1部分的Generator输出进行分类,使用真实标签作为GT计算G的损失,再反向传播计算G的梯度,最后使用优化器步骤更新G的参数。
使用真实标签作为损失函数的GT标签似乎有悖常理,但这允许我们使用BCELoss的log(X)部分(而不是log(1-x)部分),这正是我们想要的。
最后,我们将进行一些统计报告,并在每个Epoch结束时,将我们的fixed_noise batch输入到到生成器中,来直观地跟踪G的训练进度。
注意:此步骤可能需要一段时间,具体取决于您运行了多少个Epoch以及是否从数据集中删除了一些数据。
# 训练过程 # 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) batch_size = real_cpu.size(0) label = torch.full((batch_size,), 1, 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(batch_size, nz, 1, 1, device=device) # Generate fake image batch with G fake = netG(noise) label.fill_(0) # 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_(1) # 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在训练中的损失是如何变化的。其次,我们将在每个时期的Fixed noise batch上可视化G的输出。第三,我们将看一批真实数据和一批来自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的训练过程。按播放按钮开始播放动画。
# 图片生成过程的可视化 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())

# 从数据加载器中获取一批真实图像 real_batch = next(iter(dataloader)) # 绘制真实图像 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))) # 绘制最后一个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()

在本节内容中,我们从原理和代码的角度直观清晰的了解了DCGAN模型。而DCGAN是基于原始GAN模型(详见5.2 GAN实战: 手写数字生成),在网络层方面进行改进,引入卷积层进行实现,形成了新的模型。
DCGAN 的主要优点是通过一些经验性的网络结构设计使得对抗训练更加稳定。比如: