1、GAN 算法的设计思想
此处,我们用一个漫画家的成长轨迹来形象介绍生成对抗网络的思想。考虑一对双胞胎兄弟,分别称为老二 G 和老大 D,G 学习如何绘制漫画,D 学习如何鉴赏画作。还在娃娃时代的两兄弟,尚且只学会了如何使用画笔和纸张,G 绘制了一张不明所以的画作,如下图 (a) 所示,由于此时 D 鉴别能力不高,觉得 G 的作品还行,但是人物主体不够鲜明。在 D 的指引和鼓励下,G 开始尝试学习如何绘制主体轮廓和使用简单的色彩搭配。一年后,G 提升了绘画的基本功,D 也通过分析名作和初学者 G 的作品,初步掌握了鉴别作品的能力。此时 D 觉得 G 的作品人物主体有了,如图 (b),但是色彩的运用还不够成熟。数年后,G 的绘画基本功已经很扎实了,可以轻松绘制出主体鲜明、颜色搭配合适和逼真度较高的画作,如图 (
c
c
c),但是 D 同样通过观察 G 和其它名作的差别,提升了画作鉴别能力,觉得 G 的画作技艺已经趋于成熟,但是对生活的观察尚且不够,作品没有传达神情且部分细节不够完美。又过了数年,G 的绘画功力达到了炉火纯青的地步,绘制的作品细节完美、风格迥异、惟妙惟肖,宛如大师级水准,如图 (d),即便此时的D 鉴别功力也相当出色,亦很难将 G 和其他大师级的作品区分开来。
上述画家的成长历程其实是一个生活中普遍存在的学习过程,通过双方的博弈学习,相互提高,最终达到一个平衡点。GAN 网络借鉴了博弈学习的思想,分别设立了两个子网络:负责生成样本的生成器 G 和负责鉴别真伪的鉴别器 D。类比到画家的例子,生成器 G 就是老二,鉴别器 D 就是老大。鉴别器 D 通过观察真实的样本和生成器 G 产生的样本之间的区别,学会如何鉴别真假,其中真实的样本为真,生成器 G 产生的样本为假。而生成器 G 同样也在学习,它希望产生的样本能够获得鉴别器 D 的认可,即在鉴别器 D 中鉴别为真,因此生成器 G 通过优化自身的参数,尝试使得自己产生的样本在鉴别器 D 中判别为真。生成器 G 和鉴别器 D 相互博弈,共同提升,直至达到平衡点。此时生成器 G 生成的样本非常逼真,使得鉴别器 D 真假难分。
2、GAN 原理
2.1 网络结构
生成对抗网络包含了两个子网络:生成网络(Generator,简称 G)和判别网络(Discriminator,简称 D),其中生成网络 G 负责学习样本的真实分布,判别网络 D 负责将生成网络采样的样本与真实样本区分开来。
生成网络G(𝒛):生成网络 G 和自编码器的 Decoder 功能类似,从先验分布 𝑝𝒛(∙) 中采样隐藏变量 𝒛~ 𝑝𝒛(∙),通过生成网络 G 参数化的 𝑝𝑔(𝒙|𝒛) 分布,获得生成样本 𝒙~ 𝑝𝑔(𝒙|𝒛),如下图所示。
其中隐藏变量 𝒛 的先验分布 𝑝𝒛(∙) 可以假设为某中已知的分布,比如多元均匀分布 𝑧~ Uniform(−1,1)。 𝑝𝑔(𝒙|𝒛) 可以用深度神经网络来参数化,如下图所示,从均匀分布 𝑝𝒛(∙) 中采样出隐藏变量 𝒛,经过多层转置卷积层网络参数化的 𝑝𝑔(𝒙|𝒛) 分布中采样出样本 𝒙𝑓。从输入输出层面来看,生成器 G 的功能是将隐向量 𝒛 通过神经网络转换为样本向量𝒙𝑓,下标 𝑓 代表假样本(Fake samples)。
判别网络D(𝒙) : 判别网络和普通的二分类网络功能类似,它接受输入样本 𝒙 的数据集,包含了采样自真实数据分布 𝑝𝑟(∙) 的样本𝒙𝑟 ~ 𝑝𝑟(∙),也包含了采样自生成网络的假样本 𝒙𝑓~ 𝑝𝑔(𝒙|𝒛),𝒙𝑟 和 𝒙𝑓 共同组成了判别网络的训练数据集。判别网络输出为 𝒙 属于真实样本的概率 𝑃(𝒙为真|𝒙),我们把所有真实样本 𝒙𝑟 的标签标注为真(1),所有生成网络产生的样本𝒙𝑓标注为假(0),通过最小化判别网络 D 的预测值与标签之间的误差来优化判别网络参数,如图所示。
2.2 网络训练
GAN 博弈学习的思想体现在在它的训练方式上,由于生成器 G 和判别器 D 的优化目标不一样,不能和之前的网络模型的训练一样,只采用一个损失函数。
对于判别网络 D,它的目标是能够很好地分辨出真样本 𝒙𝑟 与假样本 𝒙𝑓。以图片生成为例,它的目标是最小化图片的预测值和真实值之间的交叉熵损失函数:
其中 𝐷𝜃(𝒙𝑟) 代表真实样本 𝒙𝑟 在判别网络 𝐷𝜃 的输出,𝜃 为判别网络的参数集,𝐷𝜃(𝒙𝑓) 为生成样本 𝒙𝑓 在判别网络的输出,𝑦𝑟 为 𝒙𝑟 的标签,由于真实样本标注为真,故 𝑦𝑟 = 1,𝑦𝑓 为生成样本的 𝒙𝑓 的标签,由于生成样本标注为假,故 𝑦𝑓 = 0。CE 函数代表交叉熵损失函数 CrossEntropy。二分类问题的交叉熵损失函数定义为:
因此判别网络 D 的优化目标是:
把
min
x
→
θ
L
\min\limits_{x\rightarrow\theta}L
x→θminL 问题转换为
max
x
→
θ
−
L
\max\limits_{x\rightarrow\theta}-L
x→θmax−L ,并写成期望形式:
.
对于生成网络 G(𝒛),我们希望 𝒙𝑓 = 𝐺(𝒛) 能够很好地骗过判别网络 D,假样本 𝒙𝑓 在判别网络的输出越接近真实的标签越好。也就是说,在训练生成网络时,希望判别网络的输出 𝐷(𝐺(𝒛)) 越逼近 1 越好,最小化 𝐷(𝐺(𝒛)) 与 1 之间的交叉熵损失函数:
把
min
ϕ
L
\min\limits_{\phi}L
ϕminL 问题转换为
max
ϕ
−
L
\max\limits_{\phi}-L
ϕmax−L ,并写成期望形式:
再次等价转化为:
其中 ϕ \phi ϕ 为生成网络 G 的参数集,可以利用梯度下降算法来优化参数 ϕ \phi ϕ。
.
2.3 统一目标函数
我们把判别网络的目标和生成网络的目标合并,写成min − max博弈形式:
GAN算法流程 |
---|
随机初始化参数𝜽和𝝓 repeat for k 次 do 随机采样隐向量𝒛~ 𝒑𝒛(∙) 随机采样真实样本𝒙r ~ 𝒑r(∙) 根据梯度上升算法更新 D 网络: 𝛁𝜽 𝔼 𝒙 r - 𝒑 r(∙) 𝐥𝐨𝐠𝑫𝜽 (𝒙 ) + 𝔼 𝒙 f - 𝒑 g(∙) 𝐥𝐨𝐠(1-𝑫𝜽(𝒙 ) 随机采样隐向量𝒛~ 𝒑𝒛 (∙) 根据梯度下降算法更新 G 网络: 𝛁𝝓𝔼𝒛 - 𝒑𝒛(∙) 𝐥𝐨𝐠 (𝟏 − 𝑫𝜽(𝑮𝝓(𝒛))) end for until 训练回合数达到要求 输出:训练好的生成器𝑮𝝓 |
3、DCGAN 实战
DCGAN 网络结构的判别器 D 利用普通卷积层实现,生成器 G 利用转置卷积层实现。此次实战为利用DCGAN来完成一个二次元动漫头像图片生成。
3.1 动漫图片数据集
这里使用的是一组二次元动漫头像的数据集,共 51223 张图片,无标注信息,图片主体已裁剪、对齐并统一缩放到 96×96大小。这里直接通过预编 写好的 make_anime_dataset 函数返回已经处理好的数据集对象。代码如下:
# 数据集路径,从 https://pan.baidu.com/s/1eSifHcA 提取码:g5qa 下载解压
img_path = glob.glob(r'C:\Users\faces\*.jpg')
# 构建数据集对象,返回数据集 Dataset 类和图片大小
dataset, img_shape, _ = make_anime_dataset(img_path, batch_size, resize=64)
3.2 生成器
生成网络 G 由 5 个转置卷积层单元堆叠而成,实现特征图高宽的层层放大,特征图通道数的层层减少。首先将长度为 100 的隐藏向量 𝒛 通过 Reshape 操作调整为 [𝑏, 1,1,100] 的 4 维张量,并依序通过转置卷积层,放大高宽维度,减少通道数维度,最后得到高宽为 64,通道数为 3 的彩色图片。每个卷积层中间插入 BN 层来提高训练稳定性,卷积层选择不使用偏置向量。生成器的类代码实现如下:
import keras
class Generator(keras.Model):
# 生成器网络类
def __init__(self):
super(Generator, self).__init__()
filter = 64
# 转置卷积层 1,输出 channel 为 filter*8,核大小 4,步长 1,不使用 padding,不使用偏置
self.conv1 = layers.Conv2DTranspose(filter*8,4,1, 'valid', use_bias=False)
self.bn1 = layers.BatchNormalization()
# 转置卷积层 2
self.conv2 = layers.Conv2DTranspose(filter*4,4,2, 'same', use_bias=False)
self.bn2 = layers.BatchNormalization()
# 转置卷积层 3
self.conv3 = layers.Conv2DTranspose(filter*2,4,2, 'same', use_bias=False)
self.bn3 = layers.BatchNormalization()
# 转置卷积层 4
self.conv4 = layers.Conv2DTranspose(filter*1,4,2, 'same', use_bias=False)
self.bn4 = layers.BatchNormalization()
# 转置卷积层 5
self.conv5 = layers.Conv2DTranspose(3,4,2, 'same', use_bias=False)
# 生成网络 G 的前向传播过程
def call(self, inputs, training=None):
x = inputs # [z, 100]
# Reshape 乘 4D 张量,方便后续转置卷积运算:(b, 1, 1, 100)
x = tf.reshape(x, (x.shape[0], 1, 1, x.shape[1]))
x = tf.nn.relu(x) # 激活函数
# 转置卷积-BN-激活函数:(b, 4, 4, 512)
x = tf.nn.relu(self.bn1(self.conv1(x), training=training))
# 转置卷积-BN-激活函数:(b, 8, 8, 256)
x = tf.nn.relu(self.bn2(self.conv2(x), training=training))
# 转置卷积-BN-激活函数:(b, 16, 16, 128)
x = tf.nn.relu(self.bn3(self.conv3(x), training=training))
# 转置卷积-BN-激活函数:(b, 32, 32, 64)
x = tf.nn.relu(self.bn4(self.conv4(x), training=training))
# 转置卷积-激活函数:(b, 64, 64, 3)
x = self.conv5(x)
x = tf.tanh(x) # 输出 x 范围-1~1,与预处理一致
return x
3.3 判别器
判别网络 D 与普通的分类网络相同,接受大小为 [𝑏, 64,64,3] 的图片张量,连续通过 5 个卷积层实现特征的层层提取,卷积层最终输出大小为 [𝑏, 2,2,1024],再通过池化层 GlobalAveragePooling2D 将特征大小转换为[𝑏, 1024],最后通过一个全连接层获得二分类任务的概率。判别网络 D 类的代码实现如下:
class Discriminator(keras.Model):
# 判别器类
def __init__(self):
super(Discriminator, self).__init__()
filter = 64
# 卷积层 1
self.conv1 = layers.Conv2D(filter, 4, 2, 'valid', use_bias=False)
self.bn1 = layers.BatchNormalization()
# 卷积层 2
self.conv2 = layers.Conv2D(filter*2, 4, 2, 'valid', use_bias=False)
self.bn2 = layers.BatchNormalization()
# 卷积层 3
self.conv3 = layers.Conv2D(filter*4, 4, 2, 'valid', use_bias=False)
self.bn3 = layers.BatchNormalization()
# 卷积层 4
self.conv4 = layers.Conv2D(filter*8, 3, 1, 'valid', use_bias=False)
self.bn4 = layers.BatchNormalization()
# 卷积层 5
self.conv5 = layers.Conv2D(filter*16, 3, 1, 'valid', use_bias=False)
self.bn5 = layers.BatchNormalization()
# 全局池化层
self.pool = layers.GlobalAveragePooling2D()
# 特征打平层
self.flatten = layers.Flatten()
# 2 分类全连接层
self.fc = layers.Dense(1)
# 判别器 D 的前向计算过程
def call(self, inputs, training=None):
# 卷积-BN-激活函数:(4, 31, 31, 64)
x = tf.nn.leaky_relu(self.bn1(self.conv1(inputs), training=training))
# 卷积-BN-激活函数:(4, 14, 14, 128)
x = tf.nn.leaky_relu(self.bn2(self.conv2(x), training=training))
# 卷积-BN-激活函数:(4, 6, 6, 256)
x = tf.nn.leaky_relu(self.bn3(self.conv3(x), training=training))
# 卷积-BN-激活函数:(4, 4, 4, 512)
x = tf.nn.leaky_relu(self.bn4(self.conv4(x), training=training))
# 卷积-BN-激活函数:(4, 2, 2, 1024)
x = tf.nn.leaky_relu(self.bn5(self.conv5(x), training=training))
# 卷积-BN-激活函数:(4, 1024)
x = self.pool(x)
# 打平
x = self.flatten(x)
# 输出,[b, 1024] => [b, 1]
logits = self.fc(x)
# 判别器的输出大小为[𝑏, 1],类内部没有使用 Sigmoid 激活函数,通过 Sigmoid 激活函数后可获得𝑏个样本属于真实样本的概率。
return logits
3.4 训练与可视化
判别网络 判别网络的训练目标是最大化ℒ(𝐷, 𝐺)函数,使得真实样本预测为真的概率接近于 1,生成样本预测为真的概率接近于 0。我们将判断器的误差函数实现在 d_loss_fn 函数中,将所有真实样本标注为 1,所有生成样本标注为 0,并通过最小化对应的交叉熵损失函数来实现最大化ℒ(𝐷,𝐺)函数。d_loss_fn 函数实现如下:
def d_loss_fn(generator, discriminator, batch_z, batch_x, is_training):
# 计算判别器的误差函数
# 采样生成图片
fake_image = generator(batch_z, is_training)
# 判定生成图片
d_fake_logits = discriminator(fake_image, is_training)
# 判定真实图片
d_real_logits = discriminator(batch_x, is_training)
# 真实图片与 1 之间的误差
d_loss_real = celoss_ones(d_real_logits)
# 生成图片与 0 之间的误差
d_loss_fake = celoss_zeros(d_fake_logits)
# 合并误差
loss = d_loss_fake + d_loss_real
return loss
def celoss_ones(logits):
# 计算属于与标签为 1 的交叉熵
y = tf.ones_like(logits)
loss = keras.losses.binary_crossentropy(y, logits, from_logits=True)
return tf.reduce_mean(loss)
def celoss_zeros(logits):
# 计算属于与便签为 0 的交叉熵
y = tf.zeros_like(logits)
loss = keras.losses.binary_crossentropy(y, logits, from_logits=True)
return tf.reduce_mean(loss)
生成网络的训练目标是最小化ℒ(𝐷, 𝐺)目标函数,由于真实样本与生成器无关,因此误差函数只需要考虑最小化 𝔼𝒛 ~ 𝑝𝑧(∙) log (1 − 𝐷𝜃(𝐺𝜙(𝒛)))项即可。可以通过将生成的样本标注为 1,最小化此时的交叉熵误差。需要注意的是,在反向传播误差的过程中,判别器也参与了计算图的构建,但是此阶段只需要更新生成器网络参数,而不更新判别器的网络参数。生成器的误差函数代码如下:
def g_loss_fn(generator, discriminator, batch_z, is_training):
# 采样生成图片
fake_image = generator(batch_z, is_training)
# 在训练生成网络时,需要迫使生成图片判定为真
d_fake_logits = discriminator(fake_image, is_training)
# 计算生成图片与 1 之间的误差
loss = celoss_ones(d_fake_logits)
return loss
网络训练 在每个 Epoch,首先从先验分布 𝑝(∙) 中随机采样隐藏向量,从真实数据集中随机采样真实图片,通过生成器和判别器计算判别器网络的损失,并优化判别器网络参数 𝜃。在训练生成器时,需要借助于判别器来计算误差,但是只计算生成器的梯度信息并更新 𝜙。这里设定判别器训练𝑘 = 5次后,生成器训练一次。
完整代码
import keras
class Generator(keras.Model):
# 生成器网络类
def __init__(self):
super(Generator, self).__init__()
filter = 64
# 转置卷积层 1,输出 channel 为 filter*8,核大小 4,步长 1,不使用 padding,不使用偏置
self.conv1 = layers.Conv2DTranspose(filter*8,4,1, 'valid', use_bias=False)
self.bn1 = layers.BatchNormalization()
# 转置卷积层 2
self.conv2 = layers.Conv2DTranspose(filter*4,4,2, 'same', use_bias=False)
self.bn2 = layers.BatchNormalization()
# 转置卷积层 3
self.conv3 = layers.Conv2DTranspose(filter*2,4,2, 'same', use_bias=False)
self.bn3 = layers.BatchNormalization()
# 转置卷积层 4
self.conv4 = layers.Conv2DTranspose(filter*1,4,2, 'same', use_bias=False)
self.bn4 = layers.BatchNormalization()
# 转置卷积层 5
self.conv5 = layers.Conv2DTranspose(3,4,2, 'same', use_bias=False)
def call(self, inputs, training=None):
x = inputs # [z, 100]
# Reshape 乘 4D 张量,方便后续转置卷积运算:(b, 1, 1, 100)
x = tf.reshape(x, (x.shape[0], 1, 1, x.shape[1]))
x = tf.nn.relu(x) # 激活函数
# 转置卷积-BN-激活函数:(b, 4, 4, 512)
x = tf.nn.relu(self.bn1(self.conv1(x), training=training))
# 转置卷积-BN-激活函数:(b, 8, 8, 256)
x = tf.nn.relu(self.bn2(self.conv2(x), training=training))
# 转置卷积-BN-激活函数:(b, 16, 16, 128)
x = tf.nn.relu(self.bn3(self.conv3(x), training=training))
# 转置卷积-BN-激活函数:(b, 32, 32, 64)
x = tf.nn.relu(self.bn4(self.conv4(x), training=training))
# 转置卷积-激活函数:(b, 64, 64, 3)
x = self.conv5(x)
x = tf.tanh(x) # 输出 x 范围-1~1,与预处理一致
return x
class Discriminator(keras.Model):
# 判别器类
def __init__(self):
super(Discriminator, self).__init__()
filter = 64
# 卷积层 1
self.conv1 = layers.Conv2D(filter, 4, 2, 'valid', use_bias=False)
self.bn1 = layers.BatchNormalization()
# 卷积层 2
self.conv2 = layers.Conv2D(filter*2, 4, 2, 'valid', use_bias=False)
self.bn2 = layers.BatchNormalization()
# 卷积层 3
self.conv3 = layers.Conv2D(filter*4, 4, 2, 'valid', use_bias=False)
self.bn3 = layers.BatchNormalization()
# 卷积层 4
self.conv4 = layers.Conv2D(filter*8, 3, 1, 'valid', use_bias=False)
self.bn4 = layers.BatchNormalization()
# 卷积层 5
self.conv5 = layers.Conv2D(filter*16, 3, 1, 'valid', use_bias=False)
self.bn5 = layers.BatchNormalization()
# 全局池化层
self.pool = layers.GlobalAveragePooling2D()
# 特征打平层
self.flatten = layers.Flatten()
# 2 分类全连接层
self.fc = layers.Dense(1)
# 判别器 D 的前向计算过程
def call(self, inputs, training=None):
# 卷积-BN-激活函数:(4, 31, 31, 64)
x = tf.nn.leaky_relu(self.bn1(self.conv1(inputs), training=training))
# 卷积-BN-激活函数:(4, 14, 14, 128)
x = tf.nn.leaky_relu(self.bn2(self.conv2(x), training=training))
# 卷积-BN-激活函数:(4, 6, 6, 256)
x = tf.nn.leaky_relu(self.bn3(self.conv3(x), training=training))
# 卷积-BN-激活函数:(4, 4, 4, 512)
x = tf.nn.leaky_relu(self.bn4(self.conv4(x), training=training))
# 卷积-BN-激活函数:(4, 2, 2, 1024)
x = tf.nn.leaky_relu(self.bn5(self.conv5(x), training=training))
# 卷积-BN-激活函数:(4, 1024)
x = self.pool(x)
# 打平
x = self.flatten(x)
# 输出,[b, 1024] => [b, 1]
logits = self.fc(x)
# 判别器的输出大小为[𝑏, 1],类内部没有使用 Sigmoid 激活函数,通过 Sigmoid 激活函数后可获得𝑏个样本属于真实样本的概率。
return logits
def d_loss_fn(generator, discriminator, batch_z, batch_x, is_training):
# 计算判别器的误差函数
# 采样生成图片
fake_image = generator(batch_z, is_training)
# 判定生成图片
d_fake_logits = discriminator(fake_image, is_training)
# 判定真实图片
d_real_logits = discriminator(batch_x, is_training)
# 真实图片与 1 之间的误差
d_loss_real = celoss_ones(d_real_logits)
# 生成图片与 0 之间的误差
d_loss_fake = celoss_zeros(d_fake_logits)
# 合并误差
loss = d_loss_fake + d_loss_real
return loss
import os
import glob
import numpy as np
import tensorflow as tf
from tensorflow import keras
from GAN import Generator, Discriminator
from Dataset import make_anime_dataset
from PIL import Image
import scipy.misc
import matplotlib.pyplot as plt
def d_loss_fn(generator, discriminator, batch_z, batch_x, is_training):
# 计算判别器的误差函数
# 采样生成图片
fake_image = generator(batch_z, is_training)
# 判定生成图片
d_fake_logits = discriminator(fake_image, is_training)
# 判定真实图片
d_real_logits = discriminator(batch_x, is_training)
# 真实图片与1 之间的误差
d_loss_real = celoss_ones(d_real_logits)
# 生成图片与0 之间的误差
d_loss_fake = celoss_zeros(d_fake_logits)
# 合并误差
loss = d_loss_fake + d_loss_real
return loss
def celoss_ones(logits):
# 计算属于与标签为1 的交叉熵
y = tf.ones_like(logits)
loss = keras.losses.binary_crossentropy(y, logits, from_logits=True)
return tf.reduce_mean(loss)
def celoss_zeros(logits):
# 计算属于与便签为0 的交叉熵
y = tf.zeros_like(logits)
loss = keras.losses.binary_crossentropy(y, logits, from_logits=True)
return tf.reduce_mean(loss)
def g_loss_fn(generator, discriminator, batch_z, is_training):
# 采样生成图片
fake_image = generator(batch_z, is_training)
# 在训练生成网络时,需要迫使生成图片判定为真
d_fake_logits = discriminator(fake_image, is_training)
# 计算生成图片与1 之间的误差
loss = celoss_ones(d_fake_logits)
return loss
def save_result(val_out, val_block_size, image_path, color_mode):
def preprocess(img):
img = ((img + 1.0) * 127.5).astype(np.uint8)
# img = img.astype(np.uint8)
return img
preprocesed = preprocess(val_out)
final_image = np.array([])
single_row = np.array([])
for b in range(val_out.shape[0]):
# concat image into a row
if single_row.size == 0:
single_row = preprocesed[b, :, :, :]
else:
single_row = np.concatenate((single_row, preprocesed[b, :, :, :]), axis=1)
# concat image row to final_image
if (b + 1) % val_block_size == 0:
if final_image.size == 0:
final_image = single_row
else:
final_image = np.concatenate((final_image, single_row), axis=0)
# reset single row
single_row = np.array([])
if final_image.shape[2] == 1:
final_image = np.squeeze(final_image, axis=2)
im = Image.fromarray(final_image)
im.save('exam11_final_image.png')
# Image.save(final_image)
# Image(final_image).save(image_path)
d_losses, g_losses = [], []
def draw():
plt.figure()
plt.plot(d_losses, 'b', label='generator')
plt.plot(g_losses, 'r', label='discriminator')
plt.xlabel('Epoch')
plt.ylabel('ACC')
plt.legend()
plt.savefig('exam11.1_train_test_VAE.png')
plt.show()
def main():
batch_size = 64
learning_rate = 0.0002
z_dim = 100
is_training = True
epochs = 300
img_path = glob.glob(r'G:\2020\python\faces\faces\*.jpg')
print('images num:', len(img_path))
# 构建数据集对象,返回数据集Dataset 类和图片大小
dataset, img_shape, _ = make_anime_dataset(img_path, batch_size, resize=64) # (64, 64, 64, 3) (64, 64, 3)
sample = next(iter(dataset)) # 采样 (64, 64, 64, 3)
print(sample.shape, tf.reduce_max(sample).numpy(), tf.reduce_min(sample).numpy()) # (64, 64, 64, 3) 1.0 -1.0
dataset = dataset.repeat(100) # 重复循环
db_iter = iter(dataset)
generator = Generator() # 创建生成器
generator.build(input_shape=(4, z_dim))
discriminator = Discriminator() # 创建判别器
discriminator.build(input_shape=(4, 64, 64, 3))
# 分别为生成器和判别器创建优化器
g_optimizer = keras.optimizers.Adam(learning_rate=learning_rate, beta_1=0.5)
d_optimizer = keras.optimizers.Adam(learning_rate=learning_rate, beta_1=0.5)
# generator.load_weights('exam11.1_generator.ckpt')
# discriminator.load_weights('exam11.1_discriminator.ckpt')
# print('Loaded chpt!!')
for epoch in range(epochs): # 训练epochs 次
# 1. 训练判别器
for _ in range(5):
# 采样隐藏向量
batch_z = tf.random.normal([batch_size, z_dim])
batch_x = next(db_iter) # 采样真实图片
# 判别器前向计算
with tf.GradientTape() as tape:
d_loss = d_loss_fn(generator, discriminator, batch_z, batch_x, is_training)
grads = tape.gradient(d_loss, discriminator.trainable_variables)
d_optimizer.apply_gradients(zip(grads, discriminator.trainable_variables))
# 2. 训练生成器
# 采样隐藏向量
batch_z = tf.random.normal([batch_size, z_dim])
batch_x = next(db_iter) # 采样真实图片
# 生成器前向计算
with tf.GradientTape() as tape:
g_loss = g_loss_fn(generator, discriminator, batch_z, is_training)
grads = tape.gradient(g_loss, generator.trainable_variables)
g_optimizer.apply_gradients(zip(grads, generator.trainable_variables))
if epoch % 100 == 0:
print(epoch, 'd-loss:', float(d_loss), 'g-loss:', float(g_loss)) # 可视化
z = tf.random.normal([100, z_dim])
fake_image = generator(z, training=False)
img_path = os.path.join('gan_images', 'gan-%d.png' % epoch)
save_result(fake_image.numpy(), 10, img_path, color_mode='P')
d_losses.append(float(d_loss))
g_losses.append(float(g_loss))
if epoch % 10000 == 1:
# print(d_losses)
# print(g_losses)
generator.save_weights('exam11.1_generator.ckpt')
discriminator.save_weights('exam11.1_discriminator.ckpt')
if __name__ == '__main__':
main()
draw()
4、纳什均衡
现在我们从理论层面进行分析,通过博弈学习的训练方式,生成器 G 和判别器 D 分别会达到什么平衡状态。具体地,我们将探索以下两个问题:
❑ 固定 G,D 会收敛到什么最优状态𝐷∗?
❑ 在 D 达到最优状态𝐷∗后,G 会收敛到什么状态?
首先我们通过 𝒙𝑟~ 𝑝𝑟(∙) 一维正态分布的例子给出一个直观的解释。如下图所示
黑色虚线曲线代表了真实数据的分布 𝑝𝑟(∙),为某正态分布 𝒩(𝜇, 𝜎2),绿色实线代表了生成网络学习到的分布 𝒙𝑓 ~ 𝑝𝑔(∙),蓝色虚线代表了判别器的决策边界曲线,图 (a)、(b)、 ( c ) (c) (c)、(d) 分别代表了生成网络的学习轨迹。在初始状态,如图 (a) 所示,𝑝𝑔(∙) 分布与 𝑝𝑟(∙) 差异较大,判别器可以很轻松地学习到明确的决策边界,即图 (a) 中的蓝色虚线,将来自 𝑝𝑔(∙) 的采样点判定为 0,𝑝𝑟(∙) 中的采样点判定为 1。随着生成网络的分布 𝑝𝑔(∙) 越来越逼近真实分布 𝑝𝑟(∙),判别器越来越困难将真假样本区分开,如图 (b), ( c ) (c) (c) 所示。最后,生成网络学习到的分布 𝑝𝑔(∙) = 𝑝𝑟(∙) 时,此时从生成网络中采样的样本非常逼真,判别器无法区分,即判定为真假样本的概率均等,如图 (d) 所示。这个例子直观地解释了 GAN 网络的训练过程。
4.1 判别器状态
现在来推导第一个问题:固定 G,D 会收敛到什么最优状态𝐷∗?
回顾 GAN 的损失函数:
对于判别器 D,优化的目标是最大化ℒ(𝐺,𝐷)函数,需要找出函数 f𝜃 的最大值,其中 𝜃 为判别器 𝐷 的网络参数。
.
考虑 𝑓𝜃 更通用的函数的最大值情况:
要求得函数 𝑓(𝑥) 的最大值。考虑 𝑓(𝑥) 的导数:
令导数等于0,可以求得 𝑓(𝑥) 函数的极值点:
x
=
A
A
+
B
x=\frac{A}{A+B}
x=A+BA
因此,可以得知,𝑓𝜃 函数的极值点同样为:
也就是说,判别器网络 𝐷𝜃 处于 𝐷𝜃∗ 状态时,𝑓𝜃 函数取得最大值,ℒ(𝐺, 𝐷)函数也取得最大值。ℒ(𝐺,𝐷)的最大值点在:
时取得,此时也是𝐷𝜃的最优状态𝐷∗。
4.2 生成器状态
在推导第二个问题之前,先介绍一下与 KL 散度类似的另一个分布距离度量标准:JS 散度,它定义为 KL 散度的组合:
JS 散度克服了 KL 散度不对称的缺陷。
当 D 达到最优状态 𝐷∗ 时,我们来考虑此时 𝑝r 和 𝑝g 的 JS 散度:
合并常数项可得:
即:
考虑在判别网络到达 𝐷∗ 时,此时的损失函数为:
因此在判别网络到达 𝐷∗ 时,𝐷𝐽𝑆( 𝑝𝑟|| 𝑝𝑔)与 ℒ(𝐺,𝐷∗) 满足关系:
对于生成网络 G 而言,训练目标是
min
G
L
(
𝐺
,
𝐷
)
\min\limits_{G}L(𝐺,𝐷)
GminL(G,D) ,考虑到 JS 散度具有大于等于 0 的性质,因此 ℒ(𝐺,𝐷*) 取得最小值仅在 𝐷𝐽𝑆 (𝑝𝑟 ||𝑝𝑔) = 0 时(此时 𝑝𝑔 = 𝑝𝑟),ℒ(𝐺,𝐷* )取得最小值:
值:
L
(
𝐺
∗
,
𝐷
∗
)
=
−
2
𝑙
𝑜
𝑔
2
ℒ(𝐺^∗,𝐷^∗) = −2 𝑙𝑜𝑔 2
L(G∗,D∗)=−2log2
此时生成网络𝐺∗的状态是:𝑝𝑔 = 𝑝𝑟
即 𝐺∗ 的学到的分布 𝑝𝑔 与真实分布 𝑝𝑟 一致,网络达到平衡点,此时:
4.3 纳什均衡点
通过上面的推导,可以总结出生成网络 G 最终将收敛到真实分布,即:𝑝𝑔 = 𝑝𝑟
此时生成的样本与真实样本来自同一分布,真假难辨,在判别器中均有相同的概率判定为真或假,即 𝐷(∙) = 0.5, 此时损失函数为
L
(
𝐺
∗
,
𝐷
∗
)
=
−
2
𝑙
𝑜
𝑔
2
ℒ(𝐺^∗,𝐷^∗) = −2 𝑙𝑜𝑔 2
L(G∗,D∗)=−2log2
5、WGAN 原理
WGAN 算法从理论层面分析了 GAN 训练不稳定的原因,并提出了有效的解决方法。那么是什么原因导致了 GAN 训练如此不稳定呢?WGAN 提出是因为 JS 散度在不重叠的分布 𝑝 和 𝑞 上的梯度曲面是恒定为 0 的。当分布 𝑝 和 𝑞 不重叠时,JS 散度的梯度值始终为 0,从而导致此时 GAN 的训练出现梯度弥散现象,参数长时间得不到更新,网络无法收敛。
5.1 JS 散度的缺陷
经推导,可以得到 𝐷𝐽𝑆(𝑝||𝑞) 随 𝜃 的变化趋势:
也就是说,当两个分布完全不重叠时,无论分布之间的距离远近,JS 散度为恒定值log 2,此时 JS 散度将无法产生有效的梯度信息;当两个分布出现重叠时,JS 散度才会平滑变动,产生有效梯度信息;当完全重合后,JS 散度取得最小值 0。如图中所示,红色的曲线分割两个正态分布,由于两个分布没有重叠,生成样本位置处的梯度值始终为 0,无法更新生成网络的参数,从而出现网络训练困难的现象。
因此,JS 散度在分布 𝑝 和 𝑞 不重叠时是无法平滑地衡量分布之间的距离,从而导致此位置上无法产生有效梯度信息,出现 GAN 训练不稳定的情况。要解决此问题,需要使用一种更好的分布距离衡量标准,使得它即使在分布𝑝和𝑞不重叠时,也能平滑反映分布之间的真实距离变化。
5.2 EM 距离
WGAN 论文发现了 JS 散度导致 GAN 训练不稳定的问题,并引入了一种新的分布距离度量方法:Wasserstein 距离,也叫推土机距离(Earth-Mover Distance,简称 EM 距离),它表示了从一个分布变换到另一个分布的最小代价,定义为:
其中
∏
\prod
∏ (𝑝, 𝑞)是分布 𝑝 和 𝑞 组合起来的所有可能的联合分布的集合,对于每个可能的联合分布 𝛾 ∼
∏
\prod
∏(𝑝, 𝑞),计算距离 ‖𝑥 − 𝑦‖ 的期望 𝔼(𝑥,𝑦)∼𝛾 [‖𝑥 − 𝑦‖],其中 (𝑥, 𝑦) 采样自联合分布 𝛾。不同的联合分布𝛾有不同的期望𝔼(𝑥,𝑦)∼𝛾[‖𝑥 − 𝑦‖],这些期望中的下确界即定义为分布 𝑝 和 𝑞 的 Wasserstein 距离。其中 inf{∙} 表示集合的下确界,例如{𝑥|1 < 𝑥 < 3, 𝑥 ∈ 𝑅}的下确界为 1。
以下图为例
可以直接给出分布 𝑝 和 𝑞 之间的 EM 距离的表达式:𝑊(𝑝, 𝑞) = |𝜃|
绘制出 JS 散度和 EM 距离的曲线,如下图所示,可以看到,JS 散度在𝜃 = 0处不连续,其他位置导数均为 0,而 EM 距离总能够产生有效的导数信息,因此 EM 距离相对于JS 散度更适合指导 GAN 网络的训练。
6、WGAN-GP实战
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
class Generator(keras.Model):
def __init__(self):
super(Generator, self).__init__()
# z: [b, 100] => [b, 3*3*512] => [b, 3, 3, 512] => [b, 64, 64, 3]
self.fc = layers.Dense(3*3*512)
self.conv1 = layers.Conv2DTranspose(256, 3, 3, 'valid')
self.bn1 = layers.BatchNormalization()
self.conv2 = layers.Conv2DTranspose(128, 5, 2, 'valid')
self.bn2 = layers.BatchNormalization()
self.conv3 = layers.Conv2DTranspose(3, 4, 3, 'valid')
def call(self, inputs, training=None):
# [z, 100] => [z, 3*3*512]
x = self.fc(inputs)
x = tf.reshape(x, [-1, 3, 3, 512])
x = tf.nn.leaky_relu(x)
#
x = tf.nn.leaky_relu(self.bn1(self.conv1(x), training=training))
x = tf.nn.leaky_relu(self.bn2(self.conv2(x), training=training))
x = self.conv3(x)
x = tf.tanh(x)
return x
class Discriminator(keras.Model):
def __init__(self):
super(Discriminator, self).__init__()
# [b, 64, 64, 3] => [b, 1]
self.conv1 = layers.Conv2D(64, 5, 3, 'valid')
self.conv2 = layers.Conv2D(128, 5, 3, 'valid')
self.bn2 = layers.BatchNormalization()
self.conv3 = layers.Conv2D(256, 5, 3, 'valid')
self.bn3 = layers.BatchNormalization()
# [b, h, w ,c] => [b, -1]
self.flatten = layers.Flatten()
self.fc = layers.Dense(1)
def call(self, inputs, training=None):
x = tf.nn.leaky_relu(self.conv1(inputs))
x = tf.nn.leaky_relu(self.bn2(self.conv2(x), training=training))
x = tf.nn.leaky_relu(self.bn3(self.conv3(x), training=training))
# [b, h, w, c] => [b, -1]
x = self.flatten(x)
# [b, -1] => [b, 1]
logits = self.fc(x)
return logits
def main():
d = Discriminator()
g = Generator()
x = tf.random.normal([2, 64, 64, 3])
z = tf.random.normal([2, 100])
prob = d(x)
print(prob)
x_hat = g(z)
print(x_hat.shape)
if __name__ == '__main__':
main()
WGAN_train
import os
import numpy as np
import tensorflow as tf
from tensorflow import keras
from PIL import Image
import glob
from Chapter13.GAN import Generator, Discriminator
from Chapter13.dataset import make_anime_dataset
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
def save_result(val_out, val_block_size, image_path, color_mode):
def preprocess(img):
img = ((img + 1.0) * 127.5).astype(np.uint8)
# img = img.astype(np.uint8)
return img
preprocesed = preprocess(val_out)
final_image = np.array([])
single_row = np.array([])
for b in range(val_out.shape[0]):
# concat image into a row
if single_row.size == 0:
single_row = preprocesed[b, :, :, :]
else:
single_row = np.concatenate((single_row, preprocesed[b, :, :, :]), axis=1)
# concat image row to final_image
if (b + 1) % val_block_size == 0:
if final_image.size == 0:
final_image = single_row
else:
final_image = np.concatenate((final_image, single_row), axis=0)
# reset single row
single_row = np.array([])
if final_image.shape[2] == 1:
final_image = np.squeeze(final_image, axis=2)
Image.fromarray(final_image).save(image_path)
def celoss_ones(logits):
# [b, 1]
# [b] = [1, 1, 1, 1,]
# loss = tf.keras.losses.categorical_crossentropy(y_pred=logits,
# y_true=tf.ones_like(logits))
return - tf.reduce_mean(logits)
def celoss_zeros(logits):
# [b, 1]
# [b] = [1, 1, 1, 1,]
# loss = tf.keras.losses.categorical_crossentropy(y_pred=logits,
# y_true=tf.zeros_like(logits))
return tf.reduce_mean(logits)
def gradient_penalty(discriminator, batch_x, fake_image):
# 梯度惩罚项计算函数
batchsz = batch_x.shape[0]
# 每个样本均随机采样t,用于差值,[b, h, w, c]
t = tf.random.uniform([batchsz, 1, 1, 1])
# 自动扩展为x的形状,[b, 1, 1, 1] => [b, h, w, c]
t = tf.broadcast_to(t, batch_x.shape)
# 在真假图片之间做线性差值
interplate = t * batch_x + (1 - t) * fake_image
# 在梯度环境中计算D对差值样本的梯度
with tf.GradientTape() as tape:
tape.watch([interplate]) # 加入梯度观察列表
d_interplote_logits = discriminator(interplate, training=True)
grads = tape.gradient(d_interplote_logits, interplate)
# 计算每个样本的梯度的范数:grads:[b, h, w, c] => [b, -1]
grads = tf.reshape(grads, [grads.shape[0], -1])
gp = tf.norm(grads, axis=1) # [b]
# 计算梯度惩罚项
gp = tf.reduce_mean((gp - 1) ** 2)
return gp
def d_loss_fn(generator, discriminator, batch_z, batch_x, is_training):
# 计算D的损失函数
# 1. treat real image as real
# 2. treat generated image as fake
fake_image = generator(batch_z, is_training) # 假样本
d_fake_logits = discriminator(fake_image, is_training) # 假样本的输出
d_real_logits = discriminator(batch_x, is_training) # 真样本的输出
d_loss_real = celoss_ones(d_real_logits)
d_loss_fake = celoss_zeros(d_fake_logits)
# 计算梯度惩罚项
gp = gradient_penalty(discriminator, batch_x, fake_image)
# WGAN-GP D损失函数的定义,这里并不是计算交叉熵,而是直接最大化正样本的输出
# 最小化假样本的输出和梯度惩罚项
loss = d_loss_real + d_loss_fake + 10. * gp
return loss, gp
def g_loss_fn(generator, discriminator, batch_z, is_training):
# 生成器的损失函数
fake_image = generator(batch_z, is_training)
d_fake_logits = discriminator(fake_image, is_training)
# WGAN-GP G损失函数,最大化假样本的输出值
loss = celoss_ones(d_fake_logits)
return loss
def main():
tf.random.set_seed(233)
np.random.seed(233)
assert tf.__version__.startswith('2.')
# hyper parameters
z_dim = 100
epochs = 3000000
batch_size = 512
learning_rate = 0.0005
is_training = True
img_path = glob.glob(r'/Users/xuruihang/Documents/faces_test/*.jpg')
assert len(img_path) > 0
dataset, img_shape, _ = make_anime_dataset(img_path, batch_size)
print(dataset, img_shape)
sample = next(iter(dataset))
print(sample.shape, tf.reduce_max(sample).numpy(),
tf.reduce_min(sample).numpy())
dataset = dataset.repeat()
db_iter = iter(dataset)
generator = Generator()
generator.build(input_shape=(4, z_dim))
discriminator = Discriminator()
discriminator.build(input_shape=(4, 64, 64, 3))
z_sample = tf.random.normal([100, z_dim])
g_optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate, beta_1=0.5)
d_optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate, beta_1=0.5)
for epoch in range(epochs):
for _ in range(5):
batch_z = tf.random.normal([batch_size, z_dim])
batch_x = next(db_iter)
# train D
with tf.GradientTape() as tape:
d_loss, gp = d_loss_fn(generator, discriminator, batch_z, batch_x, is_training)
grads = tape.gradient(d_loss, discriminator.trainable_variables)
d_optimizer.apply_gradients(zip(grads, discriminator.trainable_variables))
batch_z = tf.random.normal([batch_size, z_dim])
with tf.GradientTape() as tape:
g_loss = g_loss_fn(generator, discriminator, batch_z, is_training)
grads = tape.gradient(g_loss, generator.trainable_variables)
g_optimizer.apply_gradients(zip(grads, generator.trainable_variables))
if epoch % 100 == 0:
print(epoch, 'd-loss:', float(d_loss), 'g-loss:', float(g_loss),
'gp:', float(gp))
z = tf.random.normal([100, z_dim])
fake_image = generator(z, training=False)
img_path = os.path.join('WGAN_iamges_test', 'wgan-%d.png' % epoch)
save_result(fake_image.numpy(), 10, img_path, color_mode='P')
if __name__ == '__main__':
main()