生成式对抗网络(GAN,generative adversarial network)由 Goodfellow 等人于 2014 年提出,它可以替代VAE来学习图像的潜在空间。 '''
# 它能够迫使生成图像与真实图像在统计上几乎无法区分,从而生成相当逼真的合成图像。
#GAN工作原理:一个伪造者网络和一个专家网络,二者训练的目的都是为了打败彼此
因此,GAN由以下两部分组成。
生成器网络(generator network):它以一个随机向量(潜在空间中的一个随机点)作为输入,并将其解码为一张合成图像。
判别器网络(discriminator network)或对手(adversary):以一张图像(真实的或合成的均可)作为输入,并预测该图像是来自训练集还是由生成器网络创建。
训练生成器网络的目的是使其能够欺骗判别器网络,因此随着训练的进行,它能够逐渐生 成越来越逼真的图像,即看起来与真实图像无法区分的人造图像,以至于判别器网络无法区分二者;
与此同时,判别器也在不断适应生成器逐渐提高的能力,为生成图像的真实性设置了很高的标准。一旦训练结束,生成器就能够将其输入空间中的任何点转换为一张可信图像
通常来说,梯度下降是沿着静态的损失地形滚下山坡。但对于GAN而言,每下山一步, 都会对整个地形造成一点改变。它是一个动态的系统,其最优化过程寻找的不是一个最小值, 而是两股力量之间的平衡。
因此,GAN的训练极其困难,想要让GAN正常运行,需要对模型架构和训练参数进行大量的仔细调整
'''
深度卷积生成式对抗网络(DCGAN,deep convolutional GAN),即生成器和判别器都是深度卷积神经网络的GAN。特别地,它在生成器中使用Conv2DTranspose 层进行图像上采样。
GAN的简要实现流程如下所示。
(1) generator网络将形状为(latent_dim,)的向量映射到形状为(32, 32, 3)的图像。
(2) discriminator 网络将形状为(32, 32, 3)的图像映射到一个二进制分数,用于评估图像为真的概率。
(3) gan网络将generator网络和discriminator网络连接在一起:gan(x) = discriminator (generator(x))。生成器将潜在空间向量解码为图像,判别器对这些图像的真实性进行评估,因此这个 gan 网络是将这些潜在向量映射到判别器的评估结果。
(4) 我们使用带有“真”/“假”标签的真假图像样本来训练判别器,就和训练普通的图像分类模型一样。
(5) 为了训练生成器,我们要使用 gan 模型的损失相对于生成器权重的梯度。这意味着, 在每一步都要移动生成器的权重,其移动方向是让判别器更有可能将生成器解码的图像划分为“真”。换句话说,我们训练生成器来欺骗判别器。
大量技巧:
1.我们使用tanh 作为生成器最后一层的激活,而不用 sigmoid,后者在其他类型的模型中更加常见
2.我们使用正态分布(高斯分布)对潜在空间中的点进行采样,而不用均匀分布。
3.随机性能够提高稳健性。训练GAN得到的是一个动态平衡,所以GAN可能以各种方式“卡住”。在训练过程中引入随机性有助于防止出现这种情况。我们通过两种方式引入随机性: 一种是在判别器中使用dropout,另一种是向判别器的标签添加随机噪声
4.稀疏的梯度会妨碍GAN的训练。在深度学习中,稀疏性通常是我们需要的属性,但在GAN中并非如此。有两件事情可能导致梯度稀疏:最大池化运算和 ReLU 激活。我们推荐使用步进卷积代替最大池化来进行下采样,还推荐使用 LeakyReLU 层来代替 ReLU 激活。LeakyReLU 和 ReLU类似,但它允许较小的负数激活值,从而放宽了稀疏性限制。
5.在生成的图像中,经常会见到棋盘状伪影,这是由生成器中像素空间的不均匀覆盖导致的(见图 8-17)。为了解决这个问题,每当在生成器和判别器中都使用步进的 Conv2DTranpose 或 Conv2D 时,使用的内核大小要能够被步幅大小整除。
'''
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import pylab
from pandas import DataFrame, Series
from keras import models, layers, optimizers, losses, metrics
from keras.utils.np_utils import to_categorical
plt.rcParams['font.sans-serif'] = ['SimHei'] #指定默认字体
plt.rcParams['axes.unicode_minus'] = False #解决保存图像是负号'-'显示为方块的问题
#GAN生成器网络
#generator 模型,它将一个向量(来自潜在空间,训练过程中对其随机 采样)转换为一张候选图像。GAN常见的诸多问题之一,就是生成器“卡在”看似噪声的生成图像上。一种可行的解决方案是在判别器和生成器中都使用 dropout。
import keras
latent_dim=32
height=32
width=32
channels=3
generator_input=keras.Input(shape=(latent_dim,))
x=layers.Dense(128*16*16)(generator_input)
x=layers.LeakyReLU()(x)
x=layers.Reshape((16,16,128))(x)#将输入转换为大小为 16×16 的 128 个通道的特征图
x=layers.Conv2D(256,5,padding='same')(x)
x=layers.LeakyReLU()(x)
x=layers.Conv2DTranspose(256,4,strides=2,padding='same')(x)#上采样为32*32
x=layers.LeakyReLU()(x)
x=layers.Conv2D(256,5,padding='same')(x)
x=layers.LeakyReLU()(x)
x=layers.Conv2D(256,5,padding='same')(x)
x=layers.LeakyReLU()(x)
x=layers.Conv2D(channels,7,activation='tanh',padding='same')(x)#生成一个大小为 32×32 的单通道特征图 (即 CIFAR10 图像的形状)
generator=keras.models.Model(generator_input,x)
generator.summary()
#GAN判别器网络
#discriminator模型,它接收一张候选图像(真实的或合成的)作为输入,并将其划分到这两个类别之一:“生成图像”或“来自训练集的真实图像”
discrimination_input=layers.Input(shape=(height,width,channels))#判别器输入为生成图像与真实图像的拼接,以判断图像的‘真假’
x=layers.Conv2D(128,3)(discrimination_input)
x=layers.LeakyReLU()(x)
x=layers.Conv2D(128,4,strides=2)(x)#卷积窗口4*4,步幅为2
x=layers.LeakyReLU()(x)
x=layers.Conv2D(128,4,strides=2)(x)
x=layers.LeakyReLU()(x)
x=layers.Conv2D(128,4,strides=2)(x)
x=layers.LeakyReLU()(x)
x=layers.Flatten()(x)
x=layers.Dropout(0.4)(x)
x=layers.Dense(1,activation='sigmoid')(x)#分类层(真或假)
discriminator=keras.models.Model(discrimination_input,x)#将判别器模型实例化,这里它将形状为 (32, 32, 3)的输入转换为一个二进制分类决策(真/假)
discriminator.summary()
discriminator_optimizer=optimizers.RMSprop(
lr=0.0008,
clipvalue=1.0,#优化器中使用梯度裁剪(限制梯度的范围)[它是一个动态的系统,其最优化过程寻找的不是一个最小值,而是两股力量之间的平衡。]
decay=1e-8#为了稳定训练过程,使用学习率衰减
)
discriminator.compile(optimizer=discriminator_optimizer,loss='binary_crossentropy')
#对抗网络
'''
最后,我们要设置GAN,将生成器和判别器连接在一起。
训练时,这个模型将让生成器向某个方向移动,从而提高它欺骗判别器的能力。这个模型将潜在空间的点转换为一个分类决策(即“真”或“假”),它训练的标签都是“真实图像”。
因此,训练 gan 将会更新 generator 的权重,使得 discriminator 在观察假图像时更有可能预测为“真”。
请注意,有一点很重要,就是在训练过程中需要将判别器设置为冻结(即不可训练),这样在训练 gan 时它的权重才不会更新。
如果在此过程中可以对判别器的权重进行更新,那么我们就是在训练判别器始终预测“真”,但这并不是我们想要的!
'''
discriminator.trainable=False#将判别器权重设置为不可训练 (仅应用于 gan 模型)
gan_input=keras.Input(shape=(latent_dim,))
gan_output=discriminator(generator(gan_input))
gan=keras.models.Model(gan_input,gan_output)
gan_optimizer = keras.optimizers.RMSprop(
lr=0.0004,
clipvalue=1.0,
decay=1e-8
)
gan.compile(optimizer=gan_optimizer, loss='binary_crossentropy')
#训练DCGAN
'''
每轮都进行以下操作:
(1) 从潜在空间中抽取随机的点(随机噪声)。
(2) 利用这个随机噪声用 generator 生成图像。
(3) 将生成图像与真实图像混合。
(4) 使用这些混合后的图像以及相应的标签(真实图像为“真”,生成图像为“假”)来训练 discriminator,如图 8-18 所示。
(5) 在潜在空间中随机抽取新的点。
(6) 使用这些随机向量以及全部是“真实图像”的标签来训练gan。这会更新生成器的权重(只更新生成器的权重,因为判别器在 gan中被冻结),其更新方向是使得判别器能够将生成图像预测为“真实图像”。这个过程是训练生成器去欺骗判别器。
'''
import os
import keras
from keras.preprocessing import image
(x_train, y_train), (_, _) = keras.datasets.cifar10.load_data()
x_train = x_train[y_train.flatten() == 6]#选择青蛙图像(类别编号为 6)
print(x_train.shape)#(5000, 32, 32, 3)
x_train = x_train.reshape(
(x_train.shape[0],) +
(height, width, channels)).astype('float32') / 255.#数据标准化
iterations=10000
batch_size=20
save_dir='datasets/gan_output'
start=0#记录当前批处理的位置
for step in range(iterations):
random_latent_vectors=np.random.normal(size=(batch_size,latent_dim))#潜在空间中采样随机点
generated_images=generator.predict(random_latent_vectors)#利用生成器解码为虚假图像
stop=start+batch_size
real_images=x_train[start:stop]#
combined_images=np.concatenate([generated_images,real_images])#拼接,默认0轴(纵向)
labels=np.concatenate([np.ones((batch_size,1)),np.zeros((batch_size,1))])#列向量,1表示生成的图像,0表示真实的图像
labels+=0.05*np.random.random(labels.shape)#向标签中添加随机噪声
d_loss=discriminator.train_on_batch(combined_images,labels)#返回判别器损失:使用的是二进制交叉熵
random_latent_vectors=np.random.normal(size=(batch_size,latent_dim))
misleading_targets=np.zeros((batch_size,1))
a_loss=gan.train_on_batch(#通过gan模型训练生成器
random_latent_vectors,
misleading_targets#冻结判别器权重(置0)
)
start+=batch_size
if start>len(x_train)-batch_size:
start=0
if step%100==0:#每100步保存并绘图
gan.save_weights('gan.h5')#保存模型权重
print('discriminator loss:', d_loss)
print('adversarial loss',a_loss)
img = image.array_to_img(generated_images[0] * 255., scale=False)#转换成图像并保存
img.save(os.path.join(save_dir, 'generated_frog' + str(step) + '.png'))
img=image.array_to_img(real_images[0]*255.,scale=False)
img.save(os.path.join(save_dir,'real_frog'+str(step)+'.png'))
训练时你可能会看到,对抗损失开始大幅增加,而判别损失则趋向于零,即判别器最终支配了生成器。如果出现了这种情况,你可以尝试减小判别器的学习率,并增大判别器的 dropout 比率。