介绍
如图所示,GAN网络会同时训练两个模型。生成器:负责生成数据(比如:照片);判别器:判别所生成照片的真假。训练过程中,生成器生成的照片会越来越接近真实照片,直到判别器无法区分照片真假。
DCGAN(深度卷积对抗生成网络)是GAN的变体,是一种将卷积引入模型的网络。特点是:
- 判别器使用strided convolutions来替代空间池化,生成器使用反卷积
- 使用BN稳定学习,有助于处理初始化不良导致的训练问题
- 生成器输出层使用Tanh激活函数,其它层使用Relu激活函数。判别器上使用Leaky Relu激活函数。
本次案例我们将使用mnist作为数据集训练DCGAN网络,程序最后将使用GIF的方式展示训练效果。
数据导入
import tensorflow as tf
import glob
import imageio
import matplotlib.pyplot as plt
import numpy as np
import os
import tensorflow.contrib as tcon
import PIL
import time
from IPython import display
# shape:(60000,28,28)
(train_images,train_labels),(_,_)=tf.keras.datasets.mnist.load_data()
# shape:[batch_size,height,width,channel]
train_images_reshape=tf.reshape(train_images,shape=(train_images.shape[0],28,28,1)).astype(tf.float32)
# 缩放图片[-1,1]
train_images_nor=(train_images-127.5)/127.5
dataset加载数据
BUFFER_SIZE=60000
BATCH_SIZE=256
# 优化输入管道需要从:读取,转换,加载三方面考虑。
train_dataset=tf.data.Dataset.from_tensor_slices(train_images).shuffle(buffer_size=BUFFER_SIZE).batch(BATCH_SIZE)
生成模型
该生成模型将使用反卷积层,我们首先创建全连接层然后通过两次上采样将图片分辨率扩充至28x28x1。我们将逐步提升分辨率降低depth,除最后一层使用tanh激活函数,其它层都使用Leaky Relu激活函数。
def make_generator_model():
# 反卷积,从后往前
model=tf.keras.Sequential()
model.add(
tf.keras.layers.Dense(
input_dim=7*7*256,
# 不使用bias的原因是我们使用了BN,BN会抵消掉bias的作用。
# bias的作用:
# 提升网络拟合能力,而且计算简单(只要一次加法)。
# 能力的提升源于调整输出的整体分布
use_bias=False,
# noise dim
input_shape=(100,)
)
)
"""
随着神经网络的训练,网络层的输入分布会发生变动,逐渐向激活函数取值两端靠拢,如:sigmoid激活函数,
此时会进入饱和状态,梯度更新缓慢,对输入变动不敏感,甚至梯度消失导致模型难以训练。
BN,在网络层输入激活函数输入值之前加入,可以将分布拉到均值为0,标准差为1的正态分布,从而
使激活函数处于对输入值敏感的区域,从而加快模型训练。此外,BN还能起到类似dropout的正则化作用,由于我们会有
‘强拉’操作,所以对初始化要求没有那么高,可以使用较大的学习率。
"""
model.add(tf.keras.layers.BatchNormalization())
"""
relu 激活函数在输入为负值的时候,激活值为0,此时神经元无法学习
leakyrelu 激活函数在输入为负值的时候,激活值不为0(但值很小),神经元可以继续学习
"""
model.add(tf.keras.layers.LeakyReLU())
model.add(tf.keras.layers.Reshape(input_shape=(7,7,256)))
assert model.output_shape == (None,7,7,256)
model.add(tf.keras.layers.Conv2DTranspose(
filters=128,
kernel_size=5,
strides=1,
padding='same',
use_bias='False'
))
assert model.output_shape == (None,7,7,128)
model.add(tf.keras.layers.BatchNormalization())
model.add(tf.keras.layers.LeakyReLU())
# 卷积核为奇数:图像两边可以对称padding 00xxxx00
model.add(tf.keras.layers.Conv2DTranspose(
filters=64,
kernel_size=5,
strides=2,
padding='same',
use_bias='False'
))
assert model.output_shape == (None,14,14,64)
model.add(tf.keras.layers.BatchNormalization())
model.add(tf.keras.layers.LeakyReLU())
model.add(tf.keras.layers.Conv2DTranspose(
filters=1,
kernel_size=5,
strides=2,
padding='same',
use_bias='False',
# tanh激活函数值区间[-1,1],均值为0关于原点中心对称。、
# sigmoid激活函数梯度在反向传播过程中会出全正数或全负数,导致权重更新出现Z型下降。
activation='tanh'
))
assert model.output_shape == (None,28,28,1)
return model
判别模型
判别器使用strided convolutions来替代空间池化,比如这里strided=2。卷积层使用LeakyReLU替代Relu,并使用Dropout为全连接层提供加噪声的输入。
def make_discriminator_model():
# 常规卷积操作
model = tf.keras.Sequential()
model.add(tf.keras.layers.Conv2D(64, (5, 5), strides=(2, 2), padding='same'))
model.add(tf.keras.layers.LeakyReLU())
# dropout常见于全连接层,其实卷积层也是可以使用的。
# 这里简单翻译下dropout论文观点:
"""
可能很多人认为因为卷积层参数较少,过拟合发生概率较低,所以dropout作用并不大。
但是,dropout在前面几层依然有帮助,因为它为后面的全连接层提供了加噪声的输入,从而防止过拟合。
"""
model.add(tf.keras.layers.Dropout(0.3))
model.add(tf.keras.layers.Conv2D(128, (5, 5), strides=(2, 2), padding='same'))
model.add(tf.keras.layers.LeakyReLU())
model.add(tf.keras.layers.Dropout(0.3))
model.add(tf.keras.layers.Flatten())
model.add(tf.keras.layers.Dense(1))
return model
损失函数
获取模型:
generator = make_generator_model()
discriminator = make_discriminator_model()
生成器损失函数:
损失函数使用sigmoid cross entropy,labels使用值全为1的数组。
def generator_loss(generator_output):
return tf.losses.sigmoid_cross_entropy(
multi_class_labels=tf.ones_like(generator_output),
logits=generator_output
)
判别器损失函数
判别器损失函数接受两种输入,生成器生成的图像和数据集中的真实图像,损失函数计算方法如下:
- 使用sigmoid cross entropy损失函数计算数据集中真实图像的损失,labels使用值全为1的数组。
- 使用sigmoid cross entropy损失函数计算生成器图像的损失,labels使用值全为0的数组。
- 将以上损失相加得到判别器损失。
def discriminator_loss(real_output, generated_output):
# real:[1,1,...,1]
real_loss = tf.losses.sigmoid_cross_entropy(multi_class_labels=tf.ones_like(real_output), logits=real_output)
#:generated:[0,0,...,0]
generated_loss = tf.losses.sigmoid_cross_entropy(multi_class_labels=tf.zeros_like(generated_output), logits=generated_output)
# 总损失为两者相加
total_loss = real_loss + generated_loss
return total_loss
模型保存:
# 两种模型同时训练,自然需要使用两种优化器,学习率为:0.0001
generator_optimizer = tf.train.AdamOptimizer(1e-4)
discriminator_optimizer = tf.train.AdamOptimizer(1e-4)
checkpoint_dir = './training_checkpoints'
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt")
# checkpoint配置
checkpoint = tf.train.Checkpoint(generator_optimizer=generator_optimizer,
discriminator_optimizer=discriminator_optimizer,
generator=generator,
discriminator=discriminator)
模型训练
训练参数配置:
# 数据集迭代次数
EPOCHS = 50
# 生成器噪声维度
noise_dim = 100
# 可视化效果数量设置
num_examples_to_generate = 16
random_vector_for_generation = tf.random_normal([num_examples_to_generate,
noise_dim])
生成器将我们设定的正态分布的噪声向量作为输入,用来生成图像。判别器将同时显示数据集真实图像和生成器生成的图像用于判别。随后,我们计算生成器和判断器损失函数对参数的梯度,然后使用梯度下降进行更新。
def train_step(images):
# 正态分布噪声作为生成器输入
noise = tf.random_normal([BATCH_SIZE, noise_dim])
# tf.GradientTape进行记录
with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape:
generated_images = generator(noise, training=True)
# 判别器中真实图像和生成器的假图像
real_output = discriminator(images, training=True)
generated_output = discriminator(generated_images, training=True)
gen_loss = generator_loss(generated_output)
disc_loss = discriminator_loss(real_output, generated_output)
gradients_of_generator = gen_tape.gradient(gen_loss, generator.variables)
gradients_of_discriminator = disc_tape.gradient(disc_loss, discriminator.variables)
generator_optimizer.apply_gradients(zip(gradients_of_generator, generator.variables))
discriminator_optimizer.apply_gradients(zip(gradients_of_discriminator, discriminator.variables))
开始训练:
加速计算节约内存,但是不可以使用'pdb','print'。
train_step = tf.contrib.eager.defun(train_step)
def train(dataset, epochs):
for epoch in range(epochs):
start = time.time()
# 迭代数据集
for images in dataset:
train_step(images)
display.clear_output(wait=True)
# 保存图像用于后面的可视化
generate_and_save_images(generator,
epoch + 1,
random_vector_for_generation)
# 每迭代15次数据集保存一次模型
# 如需部署至tensorflow serving需要使用savemodel
if (epoch + 1) % 15 == 0:
checkpoint.save(file_prefix = checkpoint_prefix)
print ('Time taken for epoch {} is {} sec'.format(epoch + 1,
time.time()-start))
display.clear_output(wait=True)
generate_and_save_images(generator,
epochs,
random_vector_for_generation)
可视化生成器图像:
def generate_and_save_images(model, epoch, test_input):
# training:False 不训练BN
predictions = model(test_input, training=False)
fig = plt.figure(figsize=(4,4))
for i in range(predictions.shape[0]):
plt.subplot(4, 4, i+1)
plt.imshow(predictions[i, :, :, 0] * 127.5 + 127.5, cmap='gray')
plt.axis('off')
plt.savefig('image_at_epoch_{:04d}.png'.format(epoch))
plt.show()
train(train_dataset, EPOCHS)
可视化模型训练结果
展示照片:
def display_image(epoch_no):
return PIL.Image.open('image_at_epoch_{:04d}.png'.format(epoch_no))
动画展示训练结果:
with imageio.get_writer('dcgan.gif', mode='I') as writer:
filenames = glob.glob('image*.png')
filenames = sorted(filenames)
last = -1
for i,filename in enumerate(filenames):
frame = 2*(i**0.5)
if round(frame) > round(last):
last = frame
else:
continue
image = imageio.imread(filename)
writer.append_data(image)
image = imageio.imread(filename)
writer.append_data(image)
os.system('cp dcgan.gif dcgan.gif.png')
display.Image(filename="dcgan.gif.png")
总结
DCGAN中生成器判别器都使用卷积网络来提升生成和判别能力,其中生成器利用反卷积,判别器利用常规卷积。生成器用随机噪声向量作为输入来生成假图像,判别器通过对真实样本的学习判断生成器图像真伪,如果判断为假,生成器重新调校训练,直到判别器无法区分真实样本图像和生成器的图像。
本文代码部分参考Yash Katariya,在此表示感谢。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。