计算机视觉CV-Pytorch DCGAN实战:深度卷积生成对抗网络

动手学CV-Pytorch计算机视觉 DCGAN实战:深度卷积生成对抗网络

生成对抗网络是指一类采用对抗训练方式来进行学习的深度生成模型,其包含的判别网络和生成网络都可以根据不同的生成任务使用不同的网络结构。

本节介绍一个生成对抗网络的具体模型:深度卷积生成对抗网络(Deep Convolutional Generative Adversarial Network,DCGAN)[Radford et al., 2016]。在 DCGAN 中,判别网络是一个传统的深度卷积网络,但使用了带步长的卷积来实现下采样操作,不用最大汇聚(pooling)操作;生成网络使用一个特殊的深度卷积网络来实现,使用微步卷积来生成64 × 64大小的图像。

5.4.1 生成器模型

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

代码示例

n z nz nz z z z输入向量的长度, n g f ngf ngf与通过生成器传播的特征图的大小有关, n c nc 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)

5.4.2 判别器模型

判别器网络是一个二进制分类网络,该二进制分类网络将图像(3 * 64 * 64)作为输入并输出输入图像是真实的(与假的相对)的标量概率。D 可以看成是 G 结构反过来的样子,简而言之通过一系列的Conv2d,BatchNorm2d和LeakyReLU层对其进行处理,然后通过Sigmoid激活函数输出最终概率,最终得到一个 1024 * 4 * 4 的结果,再通过view(-1)进行展开成一维tensor。

如果需要解决此问题,则可以用更多层扩展此体系结构,但是使用跨步卷积BatchNormLeakyReLU仍然具有重要意义。 DCGAN论文提到,使用跨步卷积而不是通过池化来进行下采样是一个好习惯,因为它可以让网络学习自己的池化功能。 BatchNormLeakyReLU函数还有利于梯度的传递,这对于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)

5.4.3 训练流程

最后,既然我们已经定义了GAN框架的所有部分,我们就可以对其进行训练。请注意,训练GAN某种程度上是一种艺术形式,因为不正确的超参数设置会导致模式崩溃,而对失败的原因几乎没有解释。在这里,我们将严格遵循Goodfellow论文中的算法,同时遵守ganhacks(https://github.com/soumith/ganhacks)中显示的一些最佳做法。即,我们将为真实和伪造构建不同的小批量图像,并调整G的目标函数以最大化$log(1 − D(G(z)))$

模型的训练主要分为两个部分。第1部分更新了判别器,第2部分更新了生成器

第1部分-训练判别器

回想一下,训练判别器的目的是最大程度地提高将给定图片正确分类的可能性。实际上,我们要$\max \log(D(x))+\log(1-D(G(z)))$。由于ganhacks提出了单独的小批量建议,因此我们将分两步进行计算以至可以“通过提升随机梯度来更新鉴别器”

  • 首先,我们将从训练集中构造一批真实样本,通过D,计算损失$log(D(x))$,然后再反向传播计算梯度。
  • 其次,我们通过生成器构造一批假样本,将该批样本通过D,计算损失$log(1-D(G(z)))$,并通过反向传播来累积梯度。
  • 现在,利用从所有真实批次和所有伪批次累积的渐变,我们将这个过程其称为“判别器”一次更新。
第2部分-训练生成器

如原始论文所述,我们希望通过最小化$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的训练进度。

  • $Loss_D$-判别器损失,计算为所有真实批次和所有假批次的损失总和$log(D(x))+ log(1-D(G(z)))$
  • $Loss_G$-生成器损失计算为$log(D(G(z)))$
  • $D(x)$-所有真实批次的判别器的平均输出。这应该从接近1开始,然后在G变得更好时理论上收敛到0.5
  • $D(G(z))$-所有假批次的判别器的平均输出。第一个数字在D更新之前,第二个数字在D更新之后。这些数字应从0开始,并随着G变好而收敛至0.5

注意:此步骤可能需要一段时间,具体取决于您运行了多少个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

5.4.4 可视化结果

最后,让我们来看看我们做得怎么样。在这里,我们将查看三个不同的结果。首先,我们来看看D和G在训练中的损失是如何变化的。其次,我们将在每个时期的Fixed noise batch上可视化G的输出。第三,我们将看一批真实数据和一批来自G的生成的数据。

1. 损失与训练迭代

下面是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()
2. 图片生成过程的可视化

我们可以用动画来可视化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())
3. 真实图片和虚假图片
# 从数据加载器中获取一批真实图像
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()

5.4.5 小结

在本节内容中,我们从原理和代码的角度直观清晰的了解了DCGAN模型。而DCGAN是基于原始GAN模型(详见动手学CV-Pytorch计算机视觉 GAN实战 手写数字生成),在网络层方面进行改进,引入卷积层进行实现,形成了新的模型。

DCGAN 的主要优点是通过一些经验性的网络结构设计使得对抗训练更加稳定。比如:

  • 使用带步长的卷积(在判别网络中)和微步卷积(在生成网络中)来代替汇聚操作,以免损失信息;
  • 使用批量归一化
  • 去除卷积层之后的全连接层
  • 在生成网络中,除了最后一层使用 Tanh 激活函数外,其余层都使用ReLU函数;
  • 在判别网络中,都使用LeakyReLU激活函数。
  • 25
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值