64. DCGAN 动漫人物图像生成#
64.1. 介绍#
上一个实验中,我们对生成对抗神经网络有了一定的认识,也搭建了一个 GAN 网络用于生成手写字符。本次挑战中,你将了解的 GAN 的一种常见的结构 DCGAN,并使用它来搭建一个可以自动生成动漫头像的神经网络。
64.2. 知识点#
PyTorch 实践运用
DCGAN 网络搭建
DCGAN 是 GAN 的一种十分实用的网络结构,它由 Alec Radford 等人于 2015 年提出。DCGAN 的全称为:Deep Convolutional Generative Adversarial Networks,翻译成中文也就是:深度卷积生成对抗网络。DCGAN 将卷积网络引入到生成式模型当中来做无监督的训练。这种结构很好地利用了卷积网络强大的特征提取能力,从而有效提高了生成网络的学习效果。
首先,我们需要下载挑战所用到的数据集,并完成数据解压。数据集中包含 3000 张从网络上爬取的动漫人物头像。
Note
wget -nc "https://cdn.aibydoing.com/aibydoing/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>
64.3. DCGAN 网络搭建#
接下来,我们搭建 DCGAN 网络。DCGAN 的生成器结构如下所示:
接下来,我们首先定义生成器模型。根据 DCGAN
的结构,生成器网络将使用转置卷积层。这里使用到 PyTorch
提供的
torch.nn.ConvTranspose2d
🔗。
挑战:按照规定,定义生成器网络。
规定:训练时,生成器的输入噪声通道为 100,依次经过 5 个转置卷积层后,最终输出 3 通道张量。具体结构如下:
-
转置卷积层中,除第一个转置卷积层
stride
和padding
使用默认参数外,其余层stride=2
和padding=1
。另外,全部转置卷积层kernel_size=4
,且不添加偏差项。其他全部使用默认参数。 -
批归一化可以使用
BatchNorm2d()
类,输入张量形状与前一层一致,且其他采用默认参数。官方文档
## 补充代码 ###
参考答案 Exercise 64.1
from torch import nn
class Generator(nn.Module):
def __init__(self):
super(Generator, self).__init__()
self.main = nn.Sequential(
# input is Z, going into a convolution
nn.ConvTranspose2d( 100, 64 * 8, 4, 1, 0, bias=False),
nn.BatchNorm2d(64 * 8),
nn.ReLU(True),
# state size. (64*8) x 4 x 4
nn.ConvTranspose2d(64 * 8, 64 * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(64 * 4),
nn.ReLU(True),
# state size. (64*4) x 8 x 8
nn.ConvTranspose2d( 64 * 4, 64 * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(64 * 2),
nn.ReLU(True),
# state size. (64*2) x 16 x 16
nn.ConvTranspose2d( 64 * 2, 64, 4, 2, 1, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(True),
# state size. (64) x 32 x 32
nn.ConvTranspose2d( 64, 3, 4, 2, 1, bias=False),
nn.Tanh()
# state size. (3) x 64 x 64
)
def forward(self, input):
return self.main(input)
运行测试
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() ) )
接下来,我们定义判别器模型。
挑战:按照规定,定义判别器网络。
规定:训练时,判别器输入图片张量为 3 通道,依次经过 5 个卷积层后,最终输出 1 维的张量用于判定是否符合要求。具体结构如下:
-
卷积层中,除最后卷积层
stride
和padding
使用默认参数外,其余层stride=2
和padding=1
。另外,全部卷积层kernel_size=4
,且不添加偏差项。其他全部使用默认参数。 -
批归一化可以使用
BatchNorm2d()
类,输入张量形状与前一层一致,且其他采用默认参数。官方文档 -
激活函数使用 ReLU 的变体 LeakyReLU,参数
negative_slope=0.2
,inplace=True
。官方文档
## 补充代码 ###
参考答案 Exercise 64.2
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
self.main = nn.Sequential(
# input is (3) x 64 x 64
nn.Conv2d(3, 64, 4, 2, 1, bias=False),
nn.BatchNorm2d(64),
nn.LeakyReLU(0.2, inplace=True),
# state size. (64) x 32 x 32
nn.Conv2d(64, 64 * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(64 * 2),
nn.LeakyReLU(0.2, inplace=True),
# state size. (64*2) x 16 x 16
nn.Conv2d(64 * 2, 64 * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(64 * 4),
nn.LeakyReLU(0.2, inplace=True),
# state size. (64*4) x 8 x 8
nn.Conv2d(64 * 4, 64 * 8, 4, 2, 1, bias=False),
nn.BatchNorm2d(64 * 8),
nn.LeakyReLU(0.2, inplace=True),
# state size. (64*8) x 4 x 4
nn.Conv2d(64 * 8, 1, 4, 1, 0, bias=False),
nn.Sigmoid()
)
def forward(self, input):
return self.main(input)
运行测试
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 已经能达到看起来还不错的效果。
如果你想得到更加精细的图片,可能需要依赖于更高分辨率的数据集。关于使用 GAN 生成动漫头像的实践内容,也可以参考更多的开源项目:jayleicn/animeGAN
○ 欢迎分享本文链接到你的社交账号、博客、论坛等。更多的外链会增加搜索引擎对本站收录的权重,从而让更多人看到这些内容。