「百战GAN」适合所有人的第一个GAN项目:DCGAN图像生成代码实战

大家好,欢迎来到专栏《百战GAN》,在这个专栏里,我们会进行GAN相关项目的核心思想讲解,代码的详解,模型的训练和测试等内容。

作者&编辑 | 言有三

00:00

本文篇幅:4000字

背景要求:会使用Python和Pytorch

附带资料:参考论文和项目,视频讲解

1 项目背景

GAN无疑是这几年深度学习领域里最酷的技术,不管是理论的研究,还是GAN在图像生成,图像翻译,语音图像等基础领域的应用,都超级的丰富。我们公众号从很早以前开始就输出过超级多的GAN相关资源,包括免费与付费的视频课,知识星球中的GAN模型原理解读专题,公众号的GAN付费专栏,大家可以阅读下面的文章了解详情。

「百战GAN」适合所有人的第一个GAN项目:DCGAN图像生成代码实战

【CV秋季划】生成对抗网络GAN有哪些研究和应用,如何循序渐进地学习好(2022年言有三一对一辅导)?

2 项目解读

为了让大家能够快速上手,本次我们给大家介绍一个超级适合新手入门的项目,使用DCGAN来进行图片生成,项目效果如文章开头的图片。

2.1 数据获取

本次我们完成一个人脸表情图像生成的任务,使用的数据集也是多次在咱们的项目中出现过的数据集,我们选择了其中一类表情的数据,如下:

「百战GAN」适合所有人的第一个GAN项目:DCGAN图像生成代码实战

数据的读取超级简单,直接使用torchvision的ImageFolder接口即可,与我们以前介绍过的图像分类任务一样,核心代码如下,不再赘述。

## 读取数据
dataroot = "mouth/"
dataset = datasets.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)),
                           ]))
dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size,
                                         shuffle=True, num_workers=workers)

不熟悉的朋友可以阅读:【CV实战】年轻人的第一个深度学习CV项目应该是什么样的?(支持13大深度学习开源框架)

2.2 判别器定义

接下来我们再看判别器的定义,它就是一个图像分类模型,与原始DCGAN论文中的参数配置略有差异。

class Discriminator(nn.Module):
    def __init__(self, ndf=64, nc=3):
        super(Discriminator, self).__init__()
        self.ndf = ndf
        self.nc = nc
        self.main = nn.Sequential(
            # 输入图片大小 (nc) x 64 x 64,输出 (ndf) x 32 x 32
            nn.Conv2d(nc, ndf, 4, 2, 1, bias=False),
            nn.LeakyReLU(0.2, inplace=True),

            # 输入(ndf) x 32 x 32,输出(ndf*2) x 16 x 16
            nn.Conv2d(ndf, ndf * 2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 2),
            nn.LeakyReLU(0.2, inplace=True),

            # 输入(ndf*2) x 16 x 16,输出 (ndf*4) x 8 x 8
            nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 4),
            nn.LeakyReLU(0.2, inplace=True),

            # 输入(ndf*4) x 8 x 8,输出(ndf*8) x 4 x 4
            nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 8),
            nn.LeakyReLU(0.2, inplace=True),

            # 输入(ndf*8) x 4 x 4,输出1 x 1 x 1
            nn.Conv2d(ndf * 8, 1, 4, 1, 0, bias=False),
            nn.Sigmoid()
        )

    def forward(self, input):
        return self.main(input)

以上代码定义了一个包含5层卷积,其中前面4个卷积层的卷积核大小为4×4,宽和高的步长等于2,使用了padding技术,padding大小为1,每经过一次卷积,图像长宽都降低为原来的1/2。每一个卷积层后都跟随一个batch normalization层和lrelu层。

输出层也是一个卷积层,其特征图大小空间尺寸为4×4,卷积核大小也是4×4,所以输出空间层维度为1,使用sigmoid激活函数,输出就是一个0到1之间的概率值。

2.3 生成器定义

接下来我们再看生成器的定义,它输入一维的噪声向量,输出二维的图像。

## 定义生成器
class Generator(nn.Module):
    def __init__(self, nz=100, ngf=64, nc=3):
        super(Generator, self).__init__()
        self.ngf = ngf
        self.nz = nz
        self.nc = nc
        self.main = nn.Sequential(
            # 输入噪声向量Z,(ngf*8) x 4 x 4特征图
            nn.ConvTranspose2d( nz, ngf * 8, 4, 1, 0, bias=False),
            nn.BatchNorm2d(ngf * 8),
            nn.ReLU(True),
            # 输入(ngf*8) x 4 x 4特征图,输出(ngf*8) x 4 x 4
            nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf * 4),
            nn.ReLU(True),
            # 输入(ngf*4) x 8 x 8,输出(ngf*2) x 16 x 16
            nn.ConvTranspose2d( ngf * 4, ngf * 2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf * 2),
            nn.ReLU(True),
            # 输入(ngf*2) x 16 x 16,输出(ngf) x 32 x 32
            nn.ConvTranspose2d( ngf * 2, ngf, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf),
            nn.ReLU(True),
            # 输入(ngf) x 32 x 32,输出(nc) x 64 x 64
            nn.ConvTranspose2d( ngf, nc, 4, 2, 1, bias=False),
            nn.Tanh()
        )

    def forward(self, input):
        return self.main(input)

可以看出,总共包含5个上采样层。其中前4个上采样层后接有BN层,和ReLU层。第一个上采样层将输入的一维噪声向量经过一个上采样层生成4×4大小的图,然后经过后面的4个上采样层得到输出。前面4层的激活函数为ReLU,最后一层的激活函数为Tanh。

2.4 优化目标与方法定义

损失函数使用了BCE交叉熵损失,真样本和假样本标签分别为1和0。

# 损失函数
criterion = nn.BCELoss()
# 真假标签
real_label = 1.0
fake_label = 0.0

判别器和生成器都采用了Adam方法作为优化器,且使用了同样的配置,定义如下:

lr = 0.0003
beta1 = 0.5
optimizerD = optim.Adam(netD.parameters(), lr=lr, betas=(beta1, 0.999))
optimizerG = optim.Adam(netG.parameters(), lr=lr, betas=(beta1, 0.999))

3 模型训练

接下来进行模型训练,添加可视化部分,缓存中间结果,核心的迭代代码如下:

for epoch in range(num_epochs):
    lossG = 0.0
    lossD = 0.0
    for i, data in enumerate(dataloader, 0):
        ############################
        # (1) Update D network: maximize log(D(x)) + log(1 - D(G(z)))
        ###########################
        ## 训练真实图片
        netD.zero_grad()
        real_data = data[0].to(device)
        b_size = real_data.size(0)
        label = torch.full((b_size,), real_label, device=device)
        output = netD(real_data).view(-1)
        # 计算真实图片损失,梯度反向传播
        errD_real = criterion(output, label)
        errD_real.backward()
        D_x = output.mean().item()

        ## 训练生成图片
        # 产生latent vectors
        noise = torch.randn(b_size, nz, 1, 1, device=device)
        # 使用G生成图片
        fake = netG(noise)
        label.fill_(fake_label)
        output = netD(fake.detach()).view(-1)
        # 计算生成图片损失,梯度反向传播
        errD_fake = criterion(output, label)
        errD_fake.backward()
        D_G_z1 = output.mean().item()

        # 累加误差,参数更新
        errD = errD_real + errD_fake
        optimizerD.step()

        ############################
        # (2) Update G network: maximize log(D(G(z)))
        ###########################
        netG.zero_grad()
        label.fill_(real_label)  # 给生成图赋标签
        # 对生成图再进行一次判别
        output = netD(fake).view(-1)
        # 计算生成图片损失,梯度反向传播
        errG = criterion(output, label)
        errG.backward()
        D_G_z2 = output.mean().item()
        optimizerG.step()

        # Output training stats
        if i % 50 == 0:
            print('[%d/%d][%d/%d]	Loss_D: %.4f	Loss_G: %.4f	D(x): %.4f	D(G(z)): %.4f / %.4f'
                  % (epoch, num_epochs, i, len(dataloader),
                     errD.item(), errG.item(), D_x, D_G_z1, D_G_z2))

        # 存储损失
        nbatch = nbatch + 1
        lossG = lossG + errG.item()
        lossD = lossD + errD.item()

        # 对固定的噪声向量,存储生成的结果
        if (iters % 20 == 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))
            i = vutils.make_grid(fake, padding=2, normalize=True)
            fig = plt.figure(figsize=(8, 8))
            plt.imshow(np.transpose(i, (1, 2, 0)))
            plt.axis('off')  # 关闭坐标轴
            plt.savefig("out/%d_%d.png" % (epoch, iters))
            plt.close(fig)
        iters += 1

    writer.add_scalar('data/lossG', lossG, epoch)
    writer.add_scalar('data/lossD', lossD, epoch)

torch.save(netG.state_dict(),'models/netG.pt')

训练结果曲线如下:

「百战GAN」适合所有人的第一个GAN项目:DCGAN图像生成代码实战

由于标准GAN的损失与模型的生成结果之间的关系不像以前介绍的任务那么直观,我们应该以实际生成的图片结果为准,下面从左到右分别是第0,10,100个epoch的生成结果。

「百战GAN」适合所有人的第一个GAN项目:DCGAN图像生成代码实战

从图结果来看,随着训练的进行,逐渐生成了许多有意义且超级逼真的样本,在10个epoch的时候生成图片都有明显的瑕疵,到100个epoch时已经开始生成一些逼真的样本。不过最终生成的图像依旧有一部分效果很差,这是由于DCGAN本身模型性能所限,后续可以使用更好的模型进行改善。

4 模型测试

上面已经训练好了模型,我们接下来的目标,就是要用它来做推理,真正把模型用起来。

import torch
import torch.nn as nn
import torchvision.utils as vutils
import matplotlib.pyplot as plt

from net import Generator
device = torch.device("cuda:0" if (torch.cuda.is_available() and ngpu > 0) else "cpu")
netG = Generator().to(device)

## 载入模型权重
modelpath = sys.argv[1] 
savepath = sys.argv[2] 
netG.load_state_dict(torch.load(modelpath,map_location=lambda storage,loc: storage))
netG.eval() ## 设置推理模式,使得dropout和batchnorm等网络层在train和val模式间切换
torch.no_grad() ## 停止autograd模块的工作,以起到加速和节省显存
nz = 100

for i in range(0,100):
    noise = torch.randn(64, nz, 1, 1, device=device)
    fake = netG(noise).detach().cpu()
    rows = vutils.make_grid(fake, padding=2, normalize=True)
    fig = plt.figure(figsize=(8, 8))
    plt.imshow(np.transpose(rows, (1, 2, 0)))
    plt.axis('off')  # 关闭坐标轴
    plt.savefig(os.path.join(savepath,"%d.png" % (i)))
    plt.close(fig)

推理的核心代码就是使用torch.load函数载入生成器模型,然后输入随机的噪声向量,得到生成的结果。

下面展示了一些结果。

「百战GAN」适合所有人的第一个GAN项目:DCGAN图像生成代码实战

从图中我们可以看到,总体的生成结果还是不错的,不过本次的任务还有许多可以提升的空间,包括但不限于:(1) 做更多的数据增强。(2) 改善模型。这些就留给读者去进行实验。

本文参考的文献如下:

[1] Radford A, Metz L, Chintala S. Unsupervised representation learning with deep convolutional generative adversarial networks[J]. arXiv preprint arXiv:1511.06434, 2015.

本文视频讲解和代码,请大家移步:【项目实战课】基于Pytorch的DCGAN人脸嘴部表情图像生成实战

「百战GAN」适合所有人的第一个GAN项目:DCGAN图像生成代码实战

「百战GAN」适合所有人的第一个GAN项目:DCGAN图像生成代码实战

总结

本次我们使用DCGAN完成了人脸嘴唇图像生成任务,DCGAN是最基础的图像生成框架,是所有从事GAN相关领域工作的朋友必须掌握的内容,欢迎大家后来持续关注《百战GAN专栏》。

如何系统性地学习生成对抗网络GAN

欢迎大家关注有三AI-CV秋季划GAN小组,可以系统性学习GAN相关的内容,包括GAN的基础理论,《深度学习之图像生成GAN:理论与实践篇》,《深度学习之图像翻译GAN:理论与实践篇》以及各类GAN任务的实战。

介绍如下:【CV秋季划】生成对抗网络GAN有哪些研究和应用,如何循序渐进地学习好(2022年言有三一对一辅导)?

「百战GAN」适合所有人的第一个GAN项目:DCGAN图像生成代码实战

© 版权声明

相关文章

1 条评论

  • 头像
    空空两对半 投稿者

    收藏了,感谢分享

    无记录
    回复