65. DCGAN 动漫人物图像生成#

65.1. 介绍#

上一个实验中,我们对生成对抗神经网络有了一定的认识,也搭建了一个 GAN 网络用于生成手写字符。本次挑战中,你将了解的 GAN 的一种常见的结构 DCGAN,并使用它来搭建一个可以自动生成动漫头像的神经网络。

65.2. 知识点#

  • PyTorch 实践运用

  • DCGAN 网络搭建

DCGAN 是 GAN 的一种十分实用的网络结构,它由 Alec Radford 等人于 2015 年提出。DCGAN 的全称为:Deep Convolutional Generative Adversarial Networks,翻译成中文也就是:深度卷积生成对抗网络。DCGAN 将卷积网络引入到生成式模型当中来做无监督的训练。这种结构很好地利用了卷积网络强大的特征提取能力,从而有效提高了生成网络的学习效果。

首先,我们需要下载挑战所用到的数据集,并完成数据解压。数据集中包含 3000 张从网络上爬取的动漫人物头像。

Note

wget -nc "https://cdn.huhuhang.com/hands-on-ai/files/avatar.zip" # 下载数据集
unzip -o "avatar.zip" # 解压数据集

得益于 PyTorch 在图像数据预处理方面的优势,以及可以很方便地制作数据加载器。所以,本次挑战依旧使用 PyTorch 来完成。

torchvision 中实现了许多经常用到的模块,例如我们之前用过的 datasets 可以直接加载 MNIST 手写体数据集,CIFAR 数据集等。今天的挑战中,会使用到 torchvision.datasets.ImageFolder 🔗,该方法可以直接读取自定义的图片文件夹。

除此之外,这里还会使用 transforms 中定义好的许多数据处理函数,包括反转、裁剪等,而 transforms.Compose 的功能是一系列的变换函数组合在一起,然后依次作用在数据上,更具体的函数功能和模块可以查阅 官方文档

接下来,我们读取图片,对图片大小进行统一修剪,处理成张量并完成归一化。

import torch
from torchvision import datasets, transforms

# 定义图片处理方法
transforms = transforms.Compose(
    [
        transforms.Resize(64),  # 调整图片大小到 64*64
        transforms.CenterCrop(64),  # 中心裁剪
        # 将 PIL Image 或者 numpy.ndarray 转化为 PyTorch 中的 Tensor,并转化像素范围从 [0, 255] 到 [0.0, 1.0]
        transforms.ToTensor(),
        transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),  # 将图片归一化到(-1,1)
    ]
)
# 读取自定义图片数据集
dataset = datasets.ImageFolder("avatar/", transform=transforms)  # 数据路径,一个类别的图片在一个文件夹中
# 制作数据加载器
dataloader = torch.utils.data.DataLoader(
    dataset, batch_size=16, shuffle=True, num_workers=2  # 批量大小  # 乱序  # 多进程
)
dataloader
<torch.utils.data.dataloader.DataLoader at 0x106376e90>

65.3. DCGAN 网络搭建#

接下来,我们搭建 DCGAN 网络。DCGAN 的生成器结构如下所示:

接下来,我们首先定义生成器模型。根据 DCGAN 的结构,生成器网络将使用转置卷积层。这里使用到 PyTorch 提供的 torch.nn.ConvTranspose2d 🔗

Exercise 65.1

挑战:按照规定,定义生成器网络。

规定:训练时,生成器的输入噪声通道为 100,依次经过 5 个转置卷积层后,最终输出 3 通道张量。具体结构如下:

image

  • 转置卷积层中,除第一个转置卷积层 stridepadding 使用默认参数外,其余层 stride=2padding=1。另外,全部转置卷积层 kernel_size=4,且不添加偏差项。其他全部使用默认参数。

  • 批归一化可以使用 BatchNorm2d() 类,输入张量形状与前一层一致,且其他采用默认参数。官方文档

## 补充代码 ###

运行测试

Generator()

期望输出:

Generator(
  (main): Sequential(
    (0): ConvTranspose2d(100, 512, kernel_size=(4, 4), stride=(1, 1), bias=False)
    (1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (2): ReLU(inplace)
    (3): ConvTranspose2d(512, 256, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (4): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (5): ReLU(inplace)
    (6): ConvTranspose2d(256, 128, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (7): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (8): ReLU(inplace)
    (9): ConvTranspose2d(128, 64, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (10): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (11): ReLU(inplace)
    (12): ConvTranspose2d(64, 3, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (13): Tanh()
  )
)

接下来,我们定义判别器模型。

Exercise 65.2

挑战:按照规定,定义判别器网络。

规定:训练时,判别器输入图片张量为 3 通道,依次经过 5 个卷积层后,最终输出 1 维的张量用于判定是否符合要求。具体结构如下:

image

  • 卷积层中,除最后卷积层 stridepadding 使用默认参数外,其余层 stride=2padding=1。另外,全部卷积层 kernel_size=4,且不添加偏差项。其他全部使用默认参数。

  • 批归一化可以使用 BatchNorm2d() 类,输入张量形状与前一层一致,且其他采用默认参数。官方文档

  • 激活函数使用 ReLU 的变体 LeakyReLU,参数 negative_slope=0.2inplace=True官方文档

## 补充代码 ###

运行测试

Discriminator()

期望输出:

Discriminator(
  (main): Sequential(
    (0): Conv2d(3, 64, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (2): LeakyReLU(negative_slope=0.2, inplace)
    (3): Conv2d(64, 128, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (4): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (5): LeakyReLU(negative_slope=0.2, inplace)
    (6): Conv2d(128, 256, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (7): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (8): LeakyReLU(negative_slope=0.2, inplace)
    (9): Conv2d(256, 512, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (10): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (11): LeakyReLU(negative_slope=0.2, inplace)
    (12): Conv2d(512, 1, kernel_size=(4, 4), stride=(1, 1), bias=False)
    (13): Sigmoid()
  )
)

网络搭建完成后,接下来需要定义训练所需的损失函数,优化器等。

# 如果 GPU 可用则使用 CUDA 加速,否则使用 CPU 设备计算
dev = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
dev

本次实验可以在高配置 CPU 云主机完成,无需使用 GPU。

netD = Discriminator().to(dev)
netG = Generator().to(dev)
criterion = nn.BCELoss().to(dev)

lr = 0.0002  # 学习率
optimizerD = torch.optim.Adam(netD.parameters(), lr=lr, betas=(0.5, 0.999))  # Adam 优化器
optimizerG = torch.optim.Adam(netG.parameters(), lr=lr, betas=(0.5, 0.999))

一切准备就绪,就可以开始训练模型了。训练模型的代码挑战已经完整提供。

from torchvision.utils import make_grid
from matplotlib import pyplot as plt
from IPython import display

%matplotlib inline

epochs = 100
for epoch in range(epochs):
    for n, (images, _) in enumerate(dataloader):
        real_labels = torch.ones(images.size(0)).to(dev)  # 真实数据的标签为 1
        fake_labels = torch.zeros(images.size(0)).to(dev)  # 伪造数据的标签为 0

        # 使用真实图片训练判别器网络
        netD.zero_grad()  # 梯度置零
        output = netD(images.to(dev))  # 输入真实数据
        lossD_real = criterion(output.squeeze(), real_labels)  # 计算损失

        # 使用伪造图片训练判别器网络
        noise = torch.randn(images.size(0), 100, 1, 1).to(dev)  # 随机噪声,生成器输入
        fake_images = netG(noise)  # 通过生成器得到输出
        output2 = netD(fake_images.detach())  # 输入伪造数据
        lossD_fake = criterion(output2.squeeze(), fake_labels)  # 计算损失
        lossD = lossD_real + lossD_fake
        lossD.backward()
        optimizerD.step()

        # 训练生成器网络
        netG.zero_grad()
        output3 = netD(fake_images)
        lossG = criterion(output3.squeeze(), real_labels)
        lossG.backward()
        optimizerG.step()

        # 生成 64 组测试噪声样本,最终绘制 8x8 测试网格图像
        fixed_noise = torch.randn(64, 100, 1, 1).to(dev)
        fixed_images = netG(fixed_noise)
        fixed_images = make_grid(fixed_images.data, nrow=8, normalize=True).cpu()
        plt.figure(figsize=(6, 6))
        plt.title(
            "Epoch[{}/{}], Batch[{}/{}]".format(
                epoch + 1, epochs, n + 1, len(dataloader)
            )
        )
        plt.imshow(fixed_images.permute(1, 2, 0).numpy())
        display.display(plt.gcf())
        display.clear_output(wait=True)

DCGAN 的训练过程会比较慢,随着迭代次数的上升,效果也越来越好。经过我们测试,数 10 个 Epoch 已经能达到看起来还不错的效果。

image

如果你想得到更加精细的图片,可能需要依赖于更高分辨率的数据集。关于使用 GAN 生成动漫头像的实践内容,也可以参考更多的开源项目:jayleicn/animeGAN