在下面的教程中,我们将通过示例代码说明DCGAN网络如何设置网络、优化器、如何计算损失函数以及如何初始化模型权重。在本教程中,使用的动漫头像数据集共有70,171张动漫头像图片,图片大小均为96*96。
1、GAN基础原理
1.1 模型简介
生成式对抗网络(Generative Adversarial Networks,GAN)是一种生成式机器学习模型,是近年来复杂分布上无监督学习最具前景的方法之一。
最初,GAN由Ian J. Goodfellow于2014年发明,并在论文Generative Adversarial Nets中首次进行了描述,其主要由两个不同的模型共同组成——生成器(Generative Model)和判别器(Discriminative Model):
-
生成器的任务是生成看起来像训练图像的“假”图像;
-
判别器需要判断从生成器输出的图像是真实的训练图像还是虚假的图像。
GAN通过设计生成模型和判别模型这两个模块,使其互相博弈学习产生了相当好的输出。
GAN模型的核心在于提出了通过对抗过程来估计生成模型这一全新框架。在这个框架中,将会同时训练两个模型——捕捉数据分布的生成模型 𝐺 和估计样本是否来自训练数据的判别模型 𝐷 。
在训练过程中,生成器会不断尝试通过生成更好的假图像来骗过判别器,而判别器在这过程中也会逐步提升判别能力。这种博弈的平衡点是,当生成器生成的假图像和训练数据图像的分布完全一致时,判别器拥有50%的真假判断置信度。
用 𝑥 代表图像数据,用 𝐷(𝑥)表示判别器网络给出图像判定为真实图像的概率。在判别过程中,𝐷(𝑥) 需要处理作为二进制文件的大小为 1×28×28 的图像数据。当 𝑥 来自训练数据时,𝐷(𝑥) 数值应该趋近于 1 ;而当 𝑥 来自生成器时,𝐷(𝑥) 数值应该趋近于 0 。因此 𝐷(𝑥) 也可以被认为是传统的二分类器。
用 𝑧 代表标准正态分布中提取出的隐码(隐向量),用 𝐺(𝑧):表示将隐码(隐向量) 𝑧 映射到数据空间的生成器函数。函数 𝐺(𝑧) 的目标是将服从高斯分布的随机噪声 𝑧 通过生成网络变换为近似于真实分布 𝑝𝑑𝑎𝑡𝑎(𝑥) 的数据分布,我们希望找到 𝜃 使得 𝑝𝐺(𝑥;𝜃) 和 𝑝𝑑𝑎𝑡𝑎(𝑥) 尽可能的接近,其中 𝜃 代表网络参数。
𝐷(𝐺(𝑧)) 表示生成器 𝐺 生成的假图像被判定为真实图像的概率,如Generative Adversarial Nets中所述,𝐷 和 𝐺 在进行一场博弈,𝐷 想要最大程度的正确分类真图像与假图像,也就是参数 log𝐷(𝑥);而 𝐺 试图欺骗 𝐷 来最小化假图像被识别到的概率,也就是参数 log(1−𝐷(𝐺(𝑧)))。因此GAN的损失函数为:
从理论上讲,此博弈游戏的平衡点是𝑝𝐺(𝑥;𝜃)=𝑝𝑑𝑎𝑡𝑎(𝑥),此时判别器会随机猜测输入是真图像还是假图像。下面我们简要说明生成器和判别器的博弈过程:
-
在训练刚开始的时候,生成器和判别器的质量都比较差,生成器会随机生成一个数据分布。
-
判别器通过求取梯度和损失函数对网络进行优化,将靠近真实数据分布的数据判定为1,将靠近生成器生成出来数据分布的数据判定为0。
-
生成器通过优化,生成出更加贴近真实数据分布的数据。
-
生成器所生成的数据和真实数据达到相同的分布,此时判别器的输出为1/2。
在上图中,蓝色虚线表示判别器,黑色虚线表示真实数据分布,绿色实线表示生成器生成的虚假数据分布,𝑧 表示隐码,𝑥 表示生成的虚假图像 𝐺(𝑧)。该图片来源于Generative Adversarial Nets。详细的训练方法介绍见原论文。
1.2 DCGAN原理
DCGAN(深度卷积对抗生成网络,Deep Convolutional Generative Adversarial Networks)是GAN的直接扩展。不同之处在于,DCGAN会分别在判别器和生成器中使用卷积和转置卷积层。
它最早由Radford等人在论文Unsupervised Representation Learning With Deep Convolutional Generative Adversarial Networks中进行描述。判别器由分层的卷积层、BatchNorm层和LeakyReLU激活层组成。输入是3x64x64的图像,输出是该图像为真图像的概率。生成器则是由转置卷积层、BatchNorm层和ReLU激活层组成。输入是标准正态分布中提取出的隐向量𝑧𝑧,输出是3x64x64的RGB图像。
本教程将使用动漫头像数据集来训练一个生成式对抗网络,接着使用该网络生成动漫头像图片。
2、数据准备与处理
2.1 数据下载
首先我们将数据集下载到指定目录下并解压。示例代码如下:
from download import download
url = "https://download.mindspore.cn/dataset/Faces/faces.zip"
path = download(url, "./faces", kind="zip", replace=True)
2.2 数据处理
首先为执行过程定义一些输入:
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超参数
定义create_dataset_imagenet
函数对数据进行处理和增强操作。
import numpy as np
import mindspore.dataset as ds
import mindspore.dataset.vision as vision
def create_dataset_imagenet(dataset_path):
"""数据加载"""
# 加载ImageNet格式的数据集
dataset = ds.ImageFolderDataset(dataset_path,
num_parallel_workers=4, # 设置并行处理的线程数为4
shuffle=True, # 对数据进行随机打乱
decode=True) # 解码图像数据
# 数据增强操作
transforms = [
vision.Resize(image_size), # 调整图像大小
vision.CenterCrop(image_size), # 中心裁剪图像
vision.HWC2CHW(), # 将图像从HWC格式转换为CHW格式
lambda x: ((x / 255).astype("float32")) # 将像素值归一化到[0, 1]范围并转换为float32类型
]
# 数据映射操作
dataset = dataset.project('image') # 选择要处理的数据列(此处为'image'列)
dataset = dataset.map(transforms, 'image') # 对'image'列应用数据增强操作
# 批量操作
dataset = dataset.batch(batch_size) # 将数据分批次处理
return dataset
# 创建数据集,路径为'./faces'
dataset = create_dataset_imagenet('./faces')
通过create_dict_iterator
函数将数据转换成字典迭代器,然后使用matplotlib
模块可视化部分训练数据。
import matplotlib.pyplot as plt
def plot_data(data):
# 可视化部分训练数据
plt.figure(figsize=(10, 3), dpi=140) # 创建一个图形对象,设置大小为10x3英寸,分辨率为140 DPI
for i, image in enumerate(data[0][:30], 1): # 遍历数据的前30个图像
plt.subplot(3, 10, i) # 创建一个3行10列的子图布局,并选择第i个子图
plt.axis("off") # 关闭坐标轴
plt.imshow(image.transpose(1, 2, 0)) # 显示图像,并将图像从CHW格式转换为HWC格式
plt.show() # 显示图形
# 从数据集中获取一个批次的数据,dataset.create_tuple_iterator(output_numpy=True):创建一个元组迭代器,并将输出转换为NumPy数组。
sample_data = next(dataset.create_tuple_iterator(output_numpy=True))
#调用plot_data函数,传入从数据集中获取的样本数据进行可视化。
plot_data(sample_data)
3、构建网络
当处理完数据后,就可以来进行网络的搭建了。按照DCGAN论文中的描述,所有模型权重均应从mean
为0,sigma
为0.02的正态分布中随机初始化。
3.1 生成器
生成器G
的功能是将隐向量z
映射到数据空间。由于数据是图像,这一过程也会创建与真实图像大小相同的 RGB 图像。在实践场景中,该功能是通过一系列Conv2dTranspose
转置卷积层来完成的,每个层都与BatchNorm2d
层和ReLu
激活层配对,输出数据会经过tanh
函数,使其返回[-1,1]
的数据范围内。
DCGAN论文生成图像如下所示:图片来源:Unsupervised Representation Learning With Deep Convolutional Generative Adversarial Networks
我们通过输入部分中设置的nz
、ngf
和nc
来影响代码中的生成器结构。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(
# 反卷积层,输入通道数为 nz,输出通道数为 ngf * 8,卷积核大小为 4,步幅为 1,无填充
nn.Conv2dTranspose(nz, ngf * 8, 4, 1, 'valid', weight_init=weight_init),
nn.BatchNorm2d(ngf * 8, gamma_init=gamma_init), # 批归一化
nn.ReLU(), # 激活函数
# 反卷积层,输入通道数为 ngf * 8,输出通道数为 ngf * 4,卷积核大小为 4,步幅为 2,填充 1
nn.Conv2dTranspose(ngf * 8, ngf * 4, 4, 2, 'pad', 1, weight_init=weight_init),
nn.BatchNorm2d(ngf * 4, gamma_init=gamma_init), # 批归一化
nn.ReLU(), # 激活函数
# 反卷积层,输入通道数为 ngf * 4,输出通道数为 ngf * 2,卷积核大小为 4,步幅为 2,填充 1
nn.Conv2dTranspose(ngf * 4, ngf * 2, 4, 2, 'pad', 1, weight_init=weight_init),
nn.BatchNorm2d(ngf * 2, gamma_init=gamma_init), # 批归一化
nn.ReLU(), # 激活函数
# 反卷积层,输入通道数为 ngf * 2,输出通道数为 ngf,卷积核大小为 4,步幅为 2,填充 1
nn.Conv2dTranspose(ngf * 2, ngf, 4, 2, 'pad', 1, weight_init=weight_init),
nn.BatchNorm2d(ngf, gamma_init=gamma_init), # 批归一化
nn.ReLU(), # 激活函数
# 反卷积层,输入通道数为 ngf,输出通道数为 nc,卷积核大小为 4,步幅为 2,填充 1
nn.Conv2dTranspose(ngf, nc, 4, 2, 'pad', 1, weight_init=weight_init),
nn.Tanh() # Tanh 激活函数,输出在 [-1, 1] 之间
)
def construct(self, x):
# 前向传播
return self.generator(x)
# 创建生成器实例
generator = Generator()
3.2 判别器
如前所述,判别器D
是一个二分类网络模型,输出判定该图像为真实图的概率。通过一系列的Conv2d
、BatchNorm2d
和LeakyReLU
层对其进行处理,最后通过Sigmoid
激活函数得到最终概率。
DCGAN论文提到,使用卷积而不是通过池化来进行下采样是一个好方法,因为它可以让网络学习自己的池化特征。
判别器的代码实现如下:
class Discriminator(nn.Cell):
"""DCGAN网络判别器"""
def __init__(self):
super(Discriminator, self).__init__()
self.discriminator = nn.SequentialCell(
# 卷积层,输入通道数为 nc,输出通道数为 ndf,卷积核大小为 4,步幅为 2,填充 1
nn.Conv2d(nc, ndf, 4, 2, 'pad', 1, weight_init=weight_init),
nn.LeakyReLU(0.2), # LeakyReLU 激活函数,负斜率为 0.2
# 卷积层,输入通道数为 ndf,输出通道数为 ndf * 2,卷积核大小为 4,步幅为 2,填充 1
nn.Conv2d(ndf, ndf * 2, 4, 2, 'pad', 1, weight_init=weight_init),
nn.BatchNorm2d(ndf * 2, gamma_init=gamma_init), # 批归一化
nn.LeakyReLU(0.2), # LeakyReLU 激活函数,负斜率为 0.2
# 卷积层,输入通道数为 ndf * 2,输出通道数为 ndf * 4,卷积核大小为 4,步幅为 2,填充 1
nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 'pad', 1, weight_init=weight_init),
nn.BatchNorm2d(ndf * 4, gamma_init=gamma_init), # 批归一化
nn.LeakyReLU(0.2), # LeakyReLU 激活函数,负斜率为 0.2
# 卷积层,输入通道数为 ndf * 4,输出通道数为 ndf * 8,卷积核大小为 4,步幅为 2,填充 1
nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 'pad', 1, weight_init=weight_init),
nn.BatchNorm2d(ndf * 8, gamma_init=gamma_init), # 批归一化
nn.LeakyReLU(0.2), # LeakyReLU 激活函数,负斜率为 0.2
# 卷积层,输入通道数为 ndf * 8,输出通道数为 1,卷积核大小为 4,步幅为 1,无填充
nn.Conv2d(ndf * 8, 1, 4, 1, 'valid', weight_init=weight_init)
)
self.adv_layer = nn.Sigmoid() # Sigmoid 激活函数,用于将输出限制在 [0, 1] 之间
def construct(self, x):
out = self.discriminator(x)
out = out.reshape(out.shape[0], -1) # 将输出展平成二维张量,形状为 (batch_size, -1)
return self.adv_layer(out) # 返回经过 Sigmoid 激活函数后的输出
# 创建判别器实例
discriminator = Discriminator()
4、模型训练
4.1 损失函数
当定义了D
和G
后,接下来将使用MindSpore中定义的二进制交叉熵损失函数BCELoss。
mindspore.nn.BCELoss(weight=None, reduction='mean')
计算目标值和预测值之间的二值交叉熵损失值。
将预测值设置为 𝑥 ,目标值设置为 𝑦 ,输出损失设置为 ℓ(𝑥,𝑦) 。
则公式如下:
其中N是批次大小。公式如下:
【参数】
-
weight (Tensor, 可选) - 指定每个批次二值交叉熵的权重。与输入数据的shape和数据类型相同。默认值:
None
。 -
reduction (str,可选) - 指定应用于输出结果的规约计算方式,可选
'none'
、'mean'
、'sum'
,默认值:'mean'
。-
"none"
:不应用规约方法。 -
"mean"
:计算输出元素的加权平均值。 -
"sum"
:计算输出元素的总和。
-
# 定义损失函数
adversarial_loss = nn.BCELoss(reduction='mean')
# 创建一个二元交叉熵损失函数 (Binary Cross Entropy Loss)
# reduction='mean' 表示对所有样本的损失求平均值
4.2 优化器
这里设置了两个单独的优化器,一个用于D
,另一个用于G
。这两个都是lr = 0.0002
和beta1 = 0.5
的Adam优化器。
# 为生成器和判别器设置优化器
optimizer_D = nn.Adam(discriminator.trainable_params(), learning_rate=lr, beta1=beta1)
# 使用 Adam 优化器为判别器的可训练参数设置优化器
# learning_rate=lr 表示学习率
# beta1=beta1 表示 Adam 优化器的 beta1 参数(用于一阶矩估计的指数衰减率)
optimizer_G = nn.Adam(generator.trainable_params(), learning_rate=lr, beta1=beta1)
# 使用 Adam 优化器为生成器的可训练参数设置优化器
# learning_rate=lr 表示学习率
# beta1=beta1 表示 Adam 优化器的 beta1 参数(用于一阶矩估计的指数衰减率)
optimizer_G.update_parameters_name('optim_g.')
# 更新生成器优化器的参数名称前缀为 'optim_g.'
optimizer_D.update_parameters_name('optim_d.')
# 更新判别器优化器的参数名称前缀为 'optim_d.'
4.3 训练模型
训练分为两个主要部分:训练判别器和训练生成器。
-
训练判别器
训练判别器的目的是最大程度地提高判别图像真伪的概率。按照Goodfellow的方法,是希望通过提高其随机梯度来更新判别器,所以我们要最大化𝑙𝑜𝑔𝐷(𝑥)+𝑙𝑜𝑔(1−𝐷(𝐺(𝑧))𝑙𝑜𝑔𝐷(𝑥)+𝑙𝑜𝑔(1−𝐷(𝐺(𝑧))的值。
-
训练生成器
如DCGAN论文所述,我们希望通过最小化𝑙𝑜𝑔(1−𝐷(𝐺(𝑧)))𝑙𝑜𝑔(1−𝐷(𝐺(𝑧)))来训练生成器,以产生更好的虚假图像。
在这两个部分中,分别获取训练过程中的损失,并在每个周期结束时进行统计,将fixed_noise
批量推送到生成器中,以直观地跟踪G
的训练进度。
下面实现模型训练正向逻辑:
def generator_forward(real_imgs, valid):
# 将噪声采样为发生器的输入
z = ops.standard_normal((real_imgs.shape[0], nz, 1, 1)) # 生成标准正态分布的随机噪声z,形状与输入图片数量相关
# 生成一批图像
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_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)
# 使用mindspore计算生成器损失和梯度,并保留附加信息
grad_discriminator_fn = ms.value_and_grad(discriminator_forward, None, optimizer_D.parameters)
# 使用mindspore计算鉴别器损失和梯度
@ms.jit # 使用JIT编译提高函数执行效率
def train_step(imgs):
valid = ops.ones((imgs.shape[0], 1), mindspore.float32) # 生成全1的张量,表示真实标签
fake = ops.zeros((imgs.shape[0], 1), mindspore.float32) # 生成全0的张量,表示虚假标签
(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次迭代,就收集生成器和判别器的损失,以便于后面绘制训练过程中损失函数的图像。
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) # 执行一个训练步骤,返回生成器和鉴别器的损失以及生成的图像
# 每100次迭代或最后一次迭代时输出训练记录
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文件保存生成器和鉴别器的模型参数到ckpt文件。
mindspore.save_checkpoint(generator, "./generator.ckpt")
mindspore.save_checkpoint(discriminator, "./discriminator.ckpt")
运行结果较长————不沾了
5、结果展示
运行下面代码,描绘D
和G
损失与训练迭代的关系图:
import matplotlib.pyplot as plt
# 创建一个大小为10x5的图像
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') # 绘制鉴别器损失,颜色为橙色
# 设置x轴标签
plt.xlabel("iterations")
# 设置y轴标签
plt.ylabel("Loss")
# 显示图例
plt.legend()
# 显示图像
plt.show()
可视化训练过程中通过隐向量fixed_noise
生成的图像。
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np # 添加缺失的numpy导入
def showGif(image_list):
show_list = [] # 初始化用于保存动画帧的列表
fig = plt.figure(figsize=(8, 3), dpi=120) # 创建一个图像对象,大小为8x3英寸,分辨率为120 DPI
for epoch in range(len(image_list)):
images = [] # 用于保存当前epoch的图像行
for i in range(3):
# 将当前epoch的图像分成3行,每行包含8个图像
row = np.concatenate((image_list[epoch][i * 8:(i + 1) * 8]), axis=1)
images.append(row)
# 将图像行拼接成一个大图像,并将值裁剪到0到1之间
img = np.clip(np.concatenate((images[:]), axis=0), 0, 1)
plt.axis("off") # 关闭坐标轴
show_list.append([plt.imshow(img)]) # 将图像添加到show_list中,作为动画帧
# 创建动画对象,interval参数设置帧间隔时间,repeat_delay设置动画重复前的延迟时间
ani = animation.ArtistAnimation(fig, show_list, interval=1000, repeat_delay=1000, blit=True)
# 将动画保存为GIF文件,使用'pillow'作为写入器,帧率为1帧/秒
ani.save('./dcgan.gif', writer='pillow', fps=1)
showGif(image_list) # 调用函数生成并保存GIF动画
从上面的图像可以看出,随着训练次数的增多,图像质量也越来越好。如果增大训练周期数,当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()
# 创建一个图像对象,大小为8x3英寸,分辨率为120 DPI
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))
# 将所有行的图像在垂直方向上拼接在一起,并将像素值裁剪到0到1之间
img = np.clip(np.concatenate((images[:]), axis=0), 0, 1)
# 关闭坐标轴
plt.axis("off")
# 显示图像
plt.imshow(img)
plt.show()
打卡