240929-DCGAN生成漫画头像
DCGAN是GAN的直接扩展,简单从命名来理解,DCGAN(Deep Convolutional Generative Adversarial Networks)就是比GAN(Generative Adversarial Networks)多了DC(Deep Convolutional),也就是加入了深度卷积,把CNN和GAN相融合了起来,在生成器和判别器的网络结构中加入了CNN架构。
GAN前些天记录过,详见240925-GAN生成对抗网络-CSDN博客
DCGAN与GAN相对比,主要改进之处有以下几个方面:
在融入CNN架构的基础上,生成器和判别器都舍弃了池化层,判别器保留了整体架构,生成器将卷积替换成了反卷积层。反卷积详见反卷积(Transposed conv deconv)实现原理(通俗易懂)-CSDN博客
在判别器和生成器中使用BN(batch normalization)层。
移除全连接层。使用1*1卷积层替换。
生成器输出层的激活函数使用Tanh,其余层使用ReLU激活函数。
鉴别器激活函数均使用LeakyReLU。
下面我们通过一个实战案例来介绍DCGAN
实战案例
案例中,我们使用的动漫头像数据集共有70,171张动漫头像图片,图片大小均为96*96。
首先自行下载数据集放在根目录,下载后的数据集目录结构如下:
数据预处理
首先定义一些参数
batch_size = 128 # 批量大小
image_size = 64 # 训练图像空间大小
nc = 3 # 图像彩色通道数
nz = 100 # 隐向量的长度
ngf = 64 # 特征图在生成器中的大小
ndf = 64 # 特征图在判别器中的大小
num_epochs = 3 # 训练周期数
lr = 0.0002 # 学习率
beta1 = 0.5 # Adam优化器的beta1超参数
编写一个数据预处理方法对数据进行处理和增强
import numpy as np
import mindspore.dataset as ds
import mindspore.dataset.vision as vision
def create_dataset_imagenet(dataset_path):
"""数据加载"""
dataset = ds.ImageFolderDataset(dataset_path,
num_parallel_workers=4,
shuffle=True,
decode=True)
# 数据增强操作
transforms = [
vision.Resize(image_size),
vision.CenterCrop(image_size),
vision.HWC2CHW(),
# 图像数据归一化
lambda x: ((x / 255).astype("float32"))
]
# 数据映射操作
# 仅保留名为 'image' 的这一列数据。其他任何列或字段都会被移除,只留下图像数据
dataset = dataset.project('image')
dataset = dataset.map(transforms, 'image')
# 批量操作
dataset = dataset.batch(batch_size)
return dataset
dataset = create_dataset_imagenet('./faces')
数据载入后拿出来看看长什么样子,可视化操作
import matplotlib.pyplot as plt
def plot_data(data):
# 可视化部分训练数据
plt.figure(figsize=(10, 3), dpi=140)
for i, image in enumerate(data[0][:30], 1):
plt.subplot(3, 10, i)
plt.axis("off")
plt.imshow(image.transpose(1, 2, 0))
plt.show()
sample_data = next(dataset.create_tuple_iterator(output_numpy=True))
plot_data(sample_data)
构建网络
加载完数据就到了DCGAN和GAN主要差别的地方,网络构建了
生成器
下图是DCGAN的生成器网络架构图
前面也说了,在DCGAN网络中,主要采用反卷积、Batch Normal层、以及ReLU激活函数来组成生成器(输出层使用Tahn)
结合网络架构图,我们可以简单的写出其网络结构代码,在该案例中未采用和上图完全一致的输入输出channel,而是采用二分之一的channel,经过查阅,pytorch官方代码也是采用二分之一channel。
下述代码中nz
是隐向量z
的长度,ngf
与通过生成器传播的特征图的大小有关,nc
是输出图像中的通道数,在之前都进行了初始化
import mindspore as ms
from mindspore import nn, ops
from mindspore.common.initializer import Normal
weight_init = Normal(mean=0, sigma=0.02)
gamma_init = Normal(mean=1, sigma=0.02)
class Generator(nn.Cell):
"""DCGAN网络生成器"""
def __init__(self):
super(Generator, self).__init__()
self.generator = nn.SequentialCell(
nn.Conv2dTranspose(nz, ngf * 8, 4, 1, 'valid', weight_init=weight_init),
nn.BatchNorm2d(ngf * 8, gamma_init=gamma_init),
nn.ReLU(),
nn.Conv2dTranspose(ngf * 8, ngf * 4, 4, 2, 'pad', 1, weight_init=weight_init),
nn.BatchNorm2d(ngf * 4, gamma_init=gamma_init),
nn.ReLU(),
nn.Conv2dTranspose(ngf * 4, ngf * 2, 4, 2, 'pad', 1, weight_init=weight_init),
nn.BatchNorm2d(ngf * 2, gamma_init=gamma_init),
nn.ReLU(),
nn.Conv2dTranspose(ngf * 2, ngf, 4, 2, 'pad', 1, weight_init=weight_init),
nn.BatchNorm2d(ngf, gamma_init=gamma_init),
nn.ReLU(),
nn.Conv2dTranspose(ngf, nc, 4, 2, 'pad', 1, weight_init=weight_init),
nn.Tanh()
)
def construct(self, x):
return self.generator(x)
generator = Generator()
可以看到生成器我们主要采用反卷积构建,每个反卷积后跟一个BN层以及ReLU激活函数(输出层除外),一共有五个反卷积层,其
- 输入通道数:依次为
nz
,ngf * 8
,ngf * 4
,ngf * 2
,ngf
。 - 输出通道数:依次为
ngf * 8
,ngf * 4
,ngf * 2
,ngf
,nc
。
判别器
判别器是一个二分类网络模型,和生成器的网络结构正好相反,其输入输出维度也正好相反。
因为判别器的网络结构比较简单常规,我们可以理解为先有的判别器,然后我们把判别器反过来,卷积变成反卷积,ReLU激活变成LeakyReLU,就有了生成器。在维度上的变换可以类比U-Net网络的左右对称结构。
以下是判别器的代码实现:
class Discriminator(nn.Cell):
"""DCGAN网络判别器"""
def __init__(self):
super(Discriminator, self).__init__()
self.discriminator = nn.SequentialCell(
nn.Conv2d(nc, ndf, 4, 2, 'pad', 1, weight_init=weight_init),
nn.LeakyReLU(0.2),
nn.Conv2d(ndf, ndf * 2, 4, 2, 'pad', 1, weight_init=weight_init),
nn.BatchNorm2d(ngf * 2, gamma_init=gamma_init),
nn.LeakyReLU(0.2),
nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 'pad', 1, weight_init=weight_init),
nn.BatchNorm2d(ngf * 4, gamma_init=gamma_init),
nn.LeakyReLU(0.2),
nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 'pad', 1, weight_init=weight_init),
nn.BatchNorm2d(ngf * 8, gamma_init=gamma_init),
nn.LeakyReLU(0.2),
nn.Conv2d(ndf * 8, 1, 4, 1, 'valid', weight_init=weight_init),
)
self.adv_layer = nn.Sigmoid()
def construct(self, x):
out = self.discriminator(x)
out = out.reshape(out.shape[0], -1)
return self.adv_layer(out)
discriminator = Discriminator()
损失函数
损失函数方面,还是使用和GAN相同的交叉熵损失函数,两个网络不同点主要就是在网络构建,后面就简单讲解。
# 定义损失函数
adversarial_loss = nn.BCELoss(reduction='mean')
优化器
与GAN一致,需要两个优化器,都是lr = 0.0002
和beta1 = 0.5
的Adam优化器。
# 为生成器和判别器设置优化器
optimizer_D = nn.Adam(discriminator.trainable_params(), learning_rate=lr, beta1=beta1)
optimizer_G = nn.Adam(generator.trainable_params(), learning_rate=lr, beta1=beta1)
optimizer_G.update_parameters_name('optim_g.')
optimizer_D.update_parameters_name('optim_d.')
训练模型
训练主要分为两个部分,训练判别器和训练生成器。
-
训练判别器
训练判别器的目的是最大程度地提高判别图像真伪的概率。按照Goodfellow的方法,是希望通过提高其随机梯度来更新判别器,所以我们要最大化𝑙𝑜𝑔𝐷(𝑥)+𝑙𝑜𝑔(1−𝐷(𝐺(𝑧))logD(x)+log(1−D(G(z))的值。
-
训练生成器
如DCGAN论文所述,我们希望通过最小化𝑙𝑜𝑔(1−𝐷(𝐺(𝑧)))log(1−D(G(z)))来训练生成器,以产生更好的虚假图像。
在这两个部分中,分别获取训练过程中的损失,并在每个周期结束时进行统计,将fixed_noise
批量推送到生成器中,以直观地跟踪G
的训练进度。
def generator_forward(real_imgs, valid):
"""
训练生成器。
参数:
real_imgs: 真实的图像样本。
valid: 表示真实标签的张量。
返回:
g_loss: 生成器的损失。
gen_imgs: 生成的图像样本。
"""
# 将噪声采样为发生器的输入
z = ops.standard_normal((real_imgs.shape[0], nz, 1, 1))
# 生成一批图像
gen_imgs = generator(z)
# 损失衡量发生器绕过判别器的能力
g_loss = adversarial_loss(discriminator(gen_imgs), valid)
return g_loss, gen_imgs
def discriminator_forward(real_imgs, gen_imgs, valid, fake):
"""
训练判别器。
参数:
real_imgs: 真实的图像样本。
gen_imgs: 生成的图像样本。
valid: 表示真实标签的张量。
fake: 表示伪造标签的张量。
返回:
d_loss: 判别器的损失。
"""
# 衡量鉴别器从生成的样本中对真实样本进行分类的能力
real_loss = adversarial_loss(discriminator(real_imgs), valid)
fake_loss = adversarial_loss(discriminator(gen_imgs), fake)
d_loss = (real_loss + fake_loss) / 2
return d_loss
# 创建计算生成器和判别器梯度的函数
grad_generator_fn = ms.value_and_grad(generator_forward, None,
optimizer_G.parameters,
has_aux=True)
grad_discriminator_fn = ms.value_and_grad(discriminator_forward, None,
optimizer_D.parameters)
@ms.jit
def train_step(imgs):
"""
执行一次训练步骤。
参数:
imgs: 输入的图像样本。
返回:
g_loss: 生成器的损失。
d_loss: 判别器的损失。
gen_imgs: 生成的图像样本。
"""
# 准备真实和伪造标签
valid = ops.ones((imgs.shape[0], 1), mindspore.float32)
fake = ops.zeros((imgs.shape[0], 1), mindspore.float32)
# 计算生成器损失和梯度,并更新生成器参数
(g_loss, gen_imgs), g_grads = grad_generator_fn(imgs, valid)
optimizer_G(g_grads)
# 计算判别器损失和梯度,并更新判别器参数
d_loss, d_grads = grad_discriminator_fn(imgs, gen_imgs, valid, fake)
optimizer_D(d_grads)
return g_loss, d_loss, gen_imgs
循环训练网络,每经过50次迭代,就收集生成器和判别器的损失,以便于后面绘制训练过程中损失函数的图像。
%%time
import mindspore
G_losses = []
D_losses = []
image_list = []
total = dataset.get_dataset_size()
for epoch in range(num_epochs):
generator.set_train()
discriminator.set_train()
# 为每轮训练读入数据
for i, (imgs, ) in enumerate(dataset.create_tuple_iterator()):
g_loss, d_loss, gen_imgs = train_step(imgs)
if i % 100 == 0 or i == total - 1:
# 输出训练记录
print('[%2d/%d][%3d/%d] Loss_D:%7.4f Loss_G:%7.4f' % (
epoch + 1, num_epochs, i + 1, total, d_loss.asnumpy(), g_loss.asnumpy()))
D_losses.append(d_loss.asnumpy())
G_losses.append(g_loss.asnumpy())
# 每个epoch结束后,使用生成器生成一组图片
generator.set_train(False)
fixed_noise = ops.standard_normal((batch_size, nz, 1, 1))
img = generator(fixed_noise)
image_list.append(img.transpose(0, 2, 3, 1).asnumpy())
# 保存网络模型参数为ckpt文件
mindspore.save_checkpoint(generator, "./generator.ckpt")
mindspore.save_checkpoint(discriminator, "./discriminator.ckpt")
结果可视化
plt.figure(figsize=(10, 5))
plt.title("Generator and Discriminator Loss During Training")
plt.plot(G_losses, label="G", color='blue')
plt.plot(D_losses, label="D", color='orange')
plt.xlabel("iterations")
plt.ylabel("Loss")
plt.legend()
plt.show()
可视化训练过程中通过隐向量fixed_noise
生成的图像。
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np # 假设这个代码块之前已经有 import numpy as np
def showGif(image_list):
"""
将图像列表生成并显示为GIF动画。
参数:
image_list: 图像列表,每个元素是一个图像数组列表,用于在不同的时间点显示。
返回值:
无返回值,但生成一个GIF动画文件。
"""
# 初始化图像显示列表
show_list = []
# 创建绘图窗口,设置尺寸
fig = plt.figure(figsize=(8, 3), dpi=120)
# 遍历图像列表的每个时间点
for epoch in range(len(image_list)):
# 初始化当前时间点的图像块列表
images = []
# 每个时间点内,处理3行图像
for i in range(3):
# 按照一定步长拼接图像块为一行
row = np.concatenate((image_list[epoch][i * 8:(i + 1) * 8]), axis=1)
# 将拼接好的一行图像添加到列表
images.append(row)
# 将所有行图像垂直拼接成最终的图像,并确保图像值在有效范围内
img = np.clip(np.concatenate((images[:]), axis=0), 0, 1)
# 不显示坐标轴
plt.axis("off")
# 将图像添加到显示列表
show_list.append([plt.imshow(img)])
# 创建动画对象,设置间隔时间和重复延迟时间
ani = animation.ArtistAnimation(fig, show_list, interval=1000, repeat_delay=1000, blit=True)
# 使用pillow后端保存为GIF文件
ani.save('./dcgan.gif', writer='pillow', fps=1)
# 调用函数,传入图像列表
showGif(image_list)
这里运行完我们可以看到一个不停跳动的git图,随着训练次数的增多,图像质量也越来越好。如果增大训练周期数,当num_epochs
达到50以上时,生成的动漫头像图片与数据集中的较为相似,下面我们通过加载生成器网络模型参数文件来生成图像,代码如下:
# 从文件中获取模型参数并加载到网络中
mindspore.load_checkpoint("./generator.ckpt", generator)
fixed_noise = ops.standard_normal((batch_size, nz, 1, 1))
img64 = generator(fixed_noise).transpose(0, 2, 3, 1).asnumpy()
fig = plt.figure(figsize=(8, 3), dpi=120)
images = []
for i in range(3):
images.append(np.concatenate((img64[i * 8:(i + 1) * 8]), axis=1))
img = np.clip(np.concatenate((images[:]), axis=0), 0, 1)
plt.axis("off")
plt.imshow(img)
plt.show()
原论文:1511.06434 (arxiv.org)
参考代码:DCGAN生成漫画头像.… - JupyterLab (mindspore.cn)
参考资料:
【GAN】三、DCGAN论文详解 - 知乎 (zhihu.com)
反卷积(Transposed conv deconv)实现原理(通俗易懂)-CSDN博客