GAN生成对抗网络

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θmaxL ,并写成期望形式:
在这里插入图片描述

.
  对于生成网络 G(𝒛),我们希望 𝒙𝑓 = 𝐺(𝒛) 能够很好地骗过判别网络 D,假样本 𝒙𝑓 在判别网络的输出越接近真实的标签越好。也就是说,在训练生成网络时,希望判别网络的输出 𝐷(𝐺(𝒛)) 越逼近 1 越好,最小化 𝐷(𝐺(𝒛)) 与 1 之间的交叉熵损失函数:
在这里插入图片描述

min ⁡ ϕ L \min\limits_{\phi}L ϕminL 问题转换为 max ⁡ ϕ − L \max\limits_{\phi}-L ϕmaxL ,并写成期望形式:
在这里插入图片描述
再次等价转化为:
在这里插入图片描述

其中 ϕ \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()

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值