这一节将会谈到如何使用tensoflow2.0去成功复现出DCGAN的结构并应用在我们的MNIST数据集上。在这里,我们简单的说一下GAN和DCGAN的相关知识。更仔细的原理部分大家可以参照其它博主的博客或者我的后续系列的分享中也会详细提到。
这里附上原官方教程的链接:https://tensorflow.google.cn/tutorials/generative/dcgan。
GAN全称为Generative Adversarial Networks(生成式对抗网络)。这个模型网络可以通过学习一个类别的图片,然后相应的产生该类别的图片。简单来说就是通过不断学习从而生成出一些以假乱真,现实中原来中不存在的图片。从下面的图中可以看到,我们传进一堆动漫图片之后,最后模型训练完后也可以生成出相似的动漫图片。
那么GAN的结构是什么?主要有两个部分:生成器和判别器。生成器的作用是在于生成图片,而判别器的作用则是判别这张图片生成的质量,也就是真(1)和假(0)。那么这中间模型学习的地方在哪呢?简单来说就是,生成器先输入一个随机噪声,生成出一张图片,然后判别器将他和真实的图片一起进行判别。如果生成器生成的图片判别的结果为假,则说明生成的图片质量不过关。这时候将会根据生成器和判别器的损失函数进行优化并重新让生成器学习生成质量更高的图片,直到判别器认为生成器生成的图片已经足够以假乱真的时候,训练才结束。
那么DCGAN又是什么呢?其实它是一种GAN的改进模型。因为GAN训练时的不稳定和不可控性等等原因,因此有研究者对它的结构进行了修改。DCGAN将判别器和生成器中原有的多层感知机模型都替换成了CNN(卷积神经网络),同时为了使整个网络可微,拿掉了CNN 中的池化层,另外将全连接层以全局池化层替代以减轻计算量。
简单介绍了GAN和DCGAN的知识后,我们可以看看代码的搭建过程,可以更一步加深理解。
1.相关库的导入。
from __future__ import absolute_import, division, print_function, unicode_literals
import tensorflow as tf
import glob
import imageio
import matplotlib.pyplot as plt
import numpy as np
import os
import PIL
from tensorflow.keras import layers
import time
from IPython import display
2.下载MNIST手写数据集。
(train_images, train_labels), (_, _) = tf.keras.datasets.mnist.load_data()
3.图片预处理,修改图片形状以及标准化。这里将图片标准化到[-1,1]区间,因此使用127.5。
train_images = train_images.reshape(train_images.shape[0], 28, 28, 1).astype('float32')
train_images = (train_images - 127.5) / 127.5 # 将图片标准化到 [-1, 1] 区间内
4.打乱图片顺序并将数据集批量化。
BUFFER_SIZE = 60000
BATCH_SIZE = 256
train_dataset = tf.data.Dataset.from_tensor_slices(train_images).shuffle(BUFFER_SIZE).batch(BATCH_SIZE)
1.定义生成器。一开始层的input_shape定为100,因为我们准备生成大小为1x100大小噪声并传入模型。为什么不是100x1而是1x100?因为模型的第一层为100x12544(7*7*256个神经元)。因此为了满足矩阵相乘,我们将大小设置为该尺寸。相乘完后我们得到了1x12544大小的向量。但是因为最后输出的图片是以28x28x1的形式的(和MNIST数据集匹配)。因从我们需要先将图片reshape成(7x7x256),之后再通过3次反卷积生成图片(28,28,1)的满足要求的图片大小。
def make_generator_model():
model = tf.keras.Sequential()
model.add(layers.Dense(7*7*256, use_bias=False, input_shape=(100,)))
model.add(layers.BatchNormalization())
model.add(layers.LeakyReLU())
model.add(layers.Reshape((7, 7, 256))) #修改图片大小
assert model.output_shape == (None, 7, 7, 256) # batch size 没有限制
model.add(layers.Conv2DTranspose(128, (5, 5), strides=(1, 1), padding='same', use_bias=False)) #反卷积
assert model.output_shape == (None, 7, 7, 128)
model.add(layers.BatchNormalization())
model.add(layers.LeakyReLU())
model.add(layers.Conv2DTranspose(64, (5, 5), strides=(2, 2), padding='same', use_bias=False))
assert model.output_shape == (None, 14, 14, 64)
model.add(layers.BatchNormalization())
model.add(layers.LeakyReLU())
model.add(layers.Conv2DTranspose(1, (5, 5), strides=(2, 2), padding='same', use_bias=False, activation='tanh'))
assert model.output_shape == (None, 28, 28, 1)
return model
2.测试模型的可行性。随机生成1x100的噪声序列传入模型并查看结果。
generator = make_generator_model()
noise = tf.random.normal([1,100])
generated_image = generator(noise, training=False) #设置模型为未训练模式
plt.imshow(generated_image[0, :, :, 0], cmap='gray')
可以看出图片生成成功,但是这时候的图片就是一堆噪声。因此我们需要后续的训练使其生成的结果像各种数字。
1.判别器的构造比较简单,其实就是对图片不断的进行卷积并提取特征,最后转为一维向量得到结果。这和我们前面的分类案例的过程差不多。
def make_discriminator_model():
model = tf.keras.Sequential()
model.add(layers.Conv2D(64, (5, 5), strides=(2, 2), padding='same',
input_shape=[28, 28, 1]))
model.add(layers.LeakyReLU())
model.add(layers.Dropout(0.3))
model.add(layers.Conv2D(128, (5, 5), strides=(2, 2), padding='same'))
model.add(layers.LeakyReLU())
model.add(layers.Dropout(0.3))
model.add(layers.Flatten())
model.add(layers.Dense(1))
return model
2.将我们刚才生成的噪声图片传入判别器,验证判别器的可行性。
discriminator = make_discriminator_model()
decision = discriminator(generated_image)
print (decision)
验证成功。
1.定义损失函数。因为是真(1)和假(0)分类,因此我们选用binarycrossentropy。这里的from_logits意思是直接让分类结果为1或0,而不是返回概率。
cross_entropy = tf.keras.losses.BinaryCrossentropy(from_logits=True)
2.定义判别器损失函数。因为真实图片最后判别的结果必须要为1,因此real_loss的意思是真实的图片x和正确判别结果y(全部为1的图片矩阵)的loss值。而假的图片最后判别的结果是0,因此fake_loss的意思是生成的假图片x和正确判别结果y(全部为0的图片矩阵的)的loss值。
def discriminator_loss(real_output, fake_output):
real_loss = cross_entropy(tf.ones_like(real_output), real_output)
fake_loss = cross_entropy(tf.zeros_like(fake_output), fake_output)
total_loss = real_loss + fake_loss #总的loss
return total_loss
3.定义生成器损失函数。因为要让生成器生成以假乱真的图片,如果判别器判别的结果为(1)真,则会传给生成器(1)真。因此在生成器中将判别器的判别结果和1(真)之间的loss即为我们的生成器损失函数。
def generator_loss(fake_output):
return cross_entropy(tf.ones_like(fake_output), fake_output)
4.定义优化器。
generator_optimizer = tf.keras.optimizers.Adam(1e-4)
discriminator_optimizer = tf.keras.optimizers.Adam(1e-4)
5.定义checkpoint。训练时自动保存。
checkpoint_dir = './training_checkpoints'
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt")
checkpoint = tf.train.Checkpoint(generator_optimizer=generator_optimizer,
discriminator_optimizer=discriminator_optimizer,
generator=generator,
discriminator=discriminator)
6.定义训练次数和噪声维度等参数。
EPOCHS = 40
noise_dim = 100
num_examples_to_generate = 16
seed = tf.random.normal([num_examples_to_generate, noise_dim]) #随机种子
1.定义每步模型训练的过程。这里我们要通过前几节提到的gradient函数自动求微分。
def train_step(images):
noise = tf.random.normal([BATCH_SIZE, noise_dim])
with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape:
generated_images = generator(noise, training=True) #生成的图片
real_output = discriminator(images, training=True) #真实图片的判别结果
fake_output = discriminator(generated_images, training=True) #生成图片的判别结果
gen_loss = generator_loss(fake_output) #生成器损失函数
disc_loss = discriminator_loss(real_output, fake_output) #判别器损失函数
gradients_of_generator = gen_tape.gradient(gen_loss, generator.trainable_variables) #生成器的梯度
gradients_of_discriminator = disc_tape.gradient(disc_loss, discriminator.trainable_variables) #判别器的梯度
generator_optimizer.apply_gradients(zip(gradients_of_generator, generator.trainable_variables)) #对生成器进行优化
discriminator_optimizer.apply_gradients(zip(gradients_of_discriminator, discriminator.trainable_variables)) #对判别器进行优化
2.定义模型训练函数。
def train(dataset, epochs):
for epoch in range(epochs): #总训练的次数
start = time.time()
for image_batch in dataset: #一次训练时训练所有的batch
train_step(image_batch)
display.clear_output(wait=True)
generate_and_save_images(generator,
epoch + 1,
seed)
# 每 15 个 epoch 保存一次模型
if (epoch + 1) % 15 == 0:
checkpoint.save(file_prefix = checkpoint_prefix)
print ('Time for epoch {} is {} sec'.format(epoch + 1, time.time()-start))
# 最后一个 epoch 结束后生成图片
display.clear_output(wait=True)
generate_and_save_images(generator,
epochs,
seed)
3.进行模型训练,并查看最后一次的训练结果。
train(train_dataset, EPOCHS)
可以看出训练了几十次后,模型生成的图片很接近手写的数字了。
以上就是这节分享的内容,谢谢大家的支持和观看!