你可以在这里下载本教程的示例代码。
根据Yann LeCun的说法,“对抗性训练是自切片面包以来最酷的事情。“切片面包在深度学习社区肯定不会创造这么多兴奋点。自从Ian Goodfellow等人2014年首次发表生成对抗网络(GANs)以来,在短期内大幅提高了人工智能的可能生成的内容,并得出了积极的研究成果。
GANs是神经网络学习生成与已输入数据相似的数据。例如,研究人员已经生成了一些令人信服的图像,这些图像涵盖了从卧室布置到专辑封面,展示了能够表现高阶逻辑语义的非凡能力。
这些例子相当复杂,但建立一个GANs生成简单图像则很容易。在本教程中,我们将建立一个GANs,通过分析大量手写体数字,逐渐学会从头生成新的图像。也就是说,我们会教一个神经网络写字技能。
以上是本教程中GANs用于学习和生成的样本图像。在训练GANs的过程中,逐渐生成其产生数字的能力。
GANs包括两个模型:生成模型(generative model)和判别模型(discriminative model)。
判别模型是一个分类器,用来确定给定的图像是真正的图像还是像人工创造的图像。这是一个使用卷积神经网络的二元分类。
生成模型通过反卷积神经网络将随机的输入值转换成图像。
在众多的训练迭代过程中,生成模型和判别模型通过反向传播算法,训练权重和偏置量。判别模型学习把真实的图像同生成图像区分开来。同时,生成模型通过判别模型反馈者生成更逼真的图像,使得判别模型无法分辨真实图像与生成图像。
下面我们要创建一个GANs,他生成的手写体数字,可以骗过甚至最好的分类器(当然也包括人类)。我们将谷歌开发的深度学习开源库tensorflow,可以在GPU上快速训练神经网络。
本教程需要你已经至少有一点点熟悉tensorflow。如果你没有的话,我们推荐阅读“你好,tensorflow!”或在Safari互动教程看“你好,tensorflow!”。
在判别器能够区分真假图像前,我们需要一套手写体数字图像集。我们将使用MNIST,它是一个深度学习基准数据集,由美国国家标准与技术研究院组织人口普查局的员工和高中生编辑的一个包含70000图像数据集。
我们先导入tensorflow以及其他一些库,同时,使用read_data_sets下载MNIST图像。代码如下:
import tensorflow as tf
import numpy as np
import datetime
import matplotlib.pyplot as plt
%matplotlib inline
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/")
上面导入的MNIST数据集包含图像和标签,分为测试集(train)和验证集(validation)(我们不需要担心在本教程中标签的准确性)。通过next_batch函数可以取出图像。让我们看看它加载一个图像。
这些图像已经被格式化为一个包含784点的一维向量。我们可以使用pyplot将其还原为28 x 28像素的图像.
ample_image = mnist.train.next_batch(1)[0]
print(sample_image.shape) sample_image = sample_image.reshape([28, 28]) plt.imshow(sample_image, cmap='Greys')
如果你运行上面的代码,你会看到一个不同的Mnist集图像。
判别器是一个卷积神经网络,输入为28 x 28大小的灰度图像,并返回一个标量数量描述是否输入图像是“真实的”或“假”,也就是说是从数据集取出的图像还是自动生成的图像。
判别器网络的结构类似TensorFlow的CNN分类模型,它具有两个卷积层(使用5x5像素特征提取)和两个全连接层,在全连接层上每个像素有多个权重。
在建立每一层,使用tf.get_variable
建立权重和偏置变量。权重初始化为服从正态分布而偏置量初始化为零。
tf.nn.conv2d()
是TensorFlow的标准卷积函数,它有4个参数。第一个是输入张量(本例中是28 x 28 x 1的图像)。下一个参数是过滤器/权重矩阵。最后,你还可以改变步幅和卷积padding 。这两个值影响输出量的维度。
如果你已经熟悉CNNs,你会发现这是一个简单的二元分类器而已。
def discriminator(images, reuse=False):
if (reuse):
tf.get_variable_scope().reuse_variables()
# First convolutional and pool layers
# This finds 32 different 5 x 5 pixel features
d_w1 = tf.get_variable('d_w1', [5, 5, 1, 32], initializer=tf.truncated_normal_initializer(stddev=0.02))
d_b1 = tf.get_variable('d_b1', [32], initializer=tf.constant_initializer(0))
d1 = tf.nn.conv2d(input=images, filter=d_w1, strides=[1, 1, 1, 1], padding='SAME')
d1 = d1 + d_b1
d1 = tf.nn.relu(d1)
d1 = tf.nn.avg_pool(d1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
# Second convolutional and pool layers
# This finds 64 different 5 x 5 pixel features
d_w2 = tf.get_variable('d_w2', [5, 5, 32, 64], initializer=tf.truncated_normal_initializer(stddev=0.02))
d_b2 = tf.get_variable('d_b2', [64], initializer=tf.constant_initializer(0))
d2 = tf.nn.conv2d(input=d1, filter=d_w2, strides=[1, 1, 1, 1], padding='SAME')
d2 = d2 + d_b2
d2 = tf.nn.relu(d2)
d2 = tf.nn.avg_pool(d2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
# First fully connected layer
d_w3 = tf.get_variable('d_w3', [7 * 7 * 64, 1024], initializer=tf.truncated_normal_initializer(stddev=0.02))
d_b3 = tf.get_variable('d_b3', [1024], initializer=tf.constant_initializer(0))
d3 = tf.reshape(d2, [-1, 7 * 7 * 64])
d3 = tf.matmul(d3, d_w3)
d3 = d3 + d_b3
d3 = tf.nn.relu(d3)
# Second fully connected layer
d_w4 = tf.get_variable('d_w4', [1024, 1], initializer=tf.truncated_normal_initializer(stddev=0.02))
d_b4 = tf.get_variable('d_b4', [1], initializer=tf.constant_initializer(0))
d4 = tf.matmul(d3, d_w4) + d_b4
# d4 contains unscaled values
return d4
现在,我们定义了判别器(Discriminator),让我们来看看在生成器模型。我们将基于Tim O’Shea.发表的生成器模型做的一个整体结构。
你可以认为生成器(generator)是一种反卷积神经网络。一个类似Discriminator典型的CNN可以将2维或者3维的像素矩阵转换成单一的可能输出。然而,generator,使用d-维噪声维向量和upsamples可以生成一个28×28像素模式的图像,这里使用Relu函数和batch normalization稳定各层的输出。
在生成器网络中,使用了三个卷积层插值以生成28 x 28像素大小的图像。(事实上,你将会看到,我们已经注意到28 x 28 x 1模式的图像;tensorflow具有很多工具用于处理单通常的灰度图像或者3通道的RGB彩色图像。)
在输出层,我们增加加一个 tf.sigmoid()
激活函数,用于生成一个更清晰的图像。
def generator(z, batch_size, z_dim):
g_w1 = tf.get_variable('g_w1', [z_dim, 3136], dtype=tf.float32, initializer=tf.truncated_normal_initializer(stddev=0.02))
g_b1 = tf.get_variable('g_b1', [3136], initializer=tf.truncated_normal_initializer(stddev=0.02))
g1 = tf.matmul(z, g_w1) + g_b1
g1 = tf.reshape(g1, [-1, 56, 56, 1])
g1 = tf.contrib.layers.batch_norm(g1, epsilon=1e-5, scope='bn1')
g1 = tf.nn.relu(g1)
# Generate 50 features
g_w2 = tf.get_variable('g_w2', [3, 3, 1, z_dim/2], dtype=tf.float32, initializer=tf.truncated_normal_initializer(stddev=0.02))
g_b2 = tf.get_variable('g_b2', [z_dim/2], initializer=tf.truncated_normal_initializer(stddev=0.02))
g2 = tf.nn.conv2d(g1, g_w2, strides=[1, 2, 2, 1], padding='SAME')
g2 = g2 + g_b2
g2 = tf.contrib.layers.batch_norm(g2, epsilon=1e-5, scope='bn2')
g2 = tf.nn.relu(g2)
g2 = tf.image.resize_images(g2, [56, 56])
# Generate 25 features
g_w3 = tf.get_variable('g_w3', [3, 3, z_dim/2, z_dim/4], dtype=tf.float32, initializer=tf.truncated_normal_initializer(stddev=0.02))
g_b3 = tf.get_variable('g_b3', [z_dim/4], initializer=tf.truncated_normal_initializer(stddev=0.02))
g3 = tf.nn.conv2d(g2, g_w3, strides=[1, 2, 2, 1], padding='SAME')
g3 = g3 + g_b3
g3 = tf.contrib.layers.batch_norm(g3, epsilon=1e-5, scope='bn3')
g3 = tf.nn.relu(g3)
g3 = tf.image.resize_images(g3, [56, 56])
# Final convolution with one output channel
g_w4 = tf.get_variable('g_w4', [1, 1, z_dim/4, 1], dtype=tf.float32, initializer=tf.truncated_normal_initializer(stddev=0.02))
g_b4 = tf.get_variable('g_b4', [1], initializer=tf.truncated_normal_initializer(stddev=0.02))
g4 = tf.nn.conv2d(g3, g_w4, strides=[1, 2, 2, 1], padding='SAME')
g4 = g4 + g_b4
g4 = tf.sigmoid(g4)
# Dimensions of g4: batch_size x 28 x 28 x 1
return g4
现在我们已经定义了生成器和判别器(generator and discriminator )函数。让我们看看从一个未经训练的生成器输出的图像是怎样的。
首先创建tensorflow会话,在该会话中为generator 生成占位符。占位符的shape为None x z_dimensions
。None
关键字标明该值可以在会话运行的时候再确定。我们通常使用None
作为第一个维度,这样在后期可以选择不同的batch sizes。(如batch sizes是50,那么generator 的输入就是50 x 100)。使用None
关键字,我们可以在后面指定batch_size
。
z_dimensions = 100
z_placeholder = tf.placeholder(tf.float32, [None, z_dimensions])
现在,我们创建一个变量(generated_image_output
)作为generator 的输出,同时,初始化输入使用的随机噪声向量。np.random.normal()
函数有三个参数。第一和第二参数为正态分布的均值和标准差(本例中是0和1),和第三个参数为向量的shape(1 x 100
)。
generated_image_output = generator(z_placeholder, 1, z_dimensions)
z_batch = np.random.normal(0, 1, [1, z_dimensions])
接下来,我们初始化所有的变量,将z_batch
赋值给输入占位符,并且运行会话。
sess.run()
函数有两个参数。第一个就是所谓的“fetches”参数;它定义了待取回的计算值。在本例中,我们希望看到生成器的输出是什么。如果你回头看看过去的代码,你会看到,生成器的输出存储在generated_image_output中
,所以我们使用generated_image_output
作为第一个参数。
第二个参数是字典值,该值在计算图运行时代入。这里就是给占位符feed。在本例子中,将z_batch
赋给先前定义的z_placeholder
。如前所示,可以使用PyPlot恢复28 x 28的图像并且进行观察。
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
generated_image = sess.run(generated_image_output,
feed_dict={z_placeholder: z_batch})
generated_image = generated_image.reshape([28, 28])
plt.imshow(generated_image, cmap='Greys')
这个图像看起来像噪音,对吗?现在我们需要训练generator网络的权重和偏置量,使其可以将随机数转为可辨识的图像。下面让我们来看看损失函数和优化函数!
建立和调整GANs最棘手的部分是该模型有两个损失函数:一个鼓励generator创造更好的图像;另外一个鼓励discriminator 区分真实图像和生成的图像。
我们同时训练generator和discriminator 。这么做可以使得discriminator 可以更准确的区分真实图像和生成的图像,generator能够更好地调整其权值和阀值产生令人信服的图像。
这里是我们的网络的输入和输出。
tf.reset_default_graph()
batch_size = 50
z_placeholder = tf.placeholder(tf.float32, [None, z_dimensions], name='z_placeholder')
# z_placeholder is for feeding input noise to the generator
x_placeholder = tf.placeholder(tf.float32, shape = [None,28,28,1], name='x_placeholder')
# x_placeholder is for feeding input images to the discriminator
Gz = generator(z_placeholder, batch_size, z_dimensions)
# Gz holds the generated images
Dx = discriminator(x_placeholder)
# Dx will hold discriminator prediction probabilities
# for the real MNIST images
Dg = discriminator(Gz, reuse=True)
# Dg will hold discriminator prediction probabilities for generated images
所以,让我们先想想我们希望两个模型的输出是什么。discriminator 的目标是如果将 MNIST 图像给正确的标记出来就返回真(输出值较高),如果标记错误了就返回假(输出值较小)。计算discriminator 的两个损失:将Dx
和1
比较,可得到鉴别真实图像损失。比较Dg
和0
可得到鉴别生成图像损失。
使用tf.nn.sigmoid_cross_entropy_with_logits()
函数计算Dx
和1与Dg
和0之间的交叉熵损失。
使用sigmoid_cross_entropy_with_logits
处理标定值而不是从0到1的概率值。注意discriminator 的最后一行:那里没有softmax 或者sigmoid层,如果discriminators 过饱和或者或者在鉴别生成的图像的时候返回0,那么GAN会失效;这使得鉴别不下有用的梯度。
tf.reduce_mean()
函数计算交叉熵函数返回的矩阵所有元素的平均值。这种方法可以减少某一维度的损失,而不是整个向量或矩阵的损失。
d_loss_real = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(Dx, tf.ones_like(Dx)))
d_loss_fake = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(Dg, tf.zeros_like(Dg)))
现在让我们建立generator的损失函数。我们希望generator网络创造的图像,可以骗过discriminators 。generator要discriminators 的在判别生成的图像的时候输出值接近1。因此,我们要计算Dg
和1的损失。
g_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(Dg, tf.ones_like(Dg)))
现在,我们有了损失函数,下一步定义优化函数。对于generator网络优化函数只需要更新generator的权重,不更新discriminators的权重 。同样,当我们训练discriminators,我们要保持generator的权重固定。
为了区别这种操作,我们需要创建两个变量列表,一个discriminators的权重和偏置项,和另一个是generator的权重和偏置项。这里有一种“贴心”的方案可以命名所有的tensorflow变量。
vars = tf.trainable_variables()
d_vars = [var for var in tvars if 'd_' in var.name]
g_vars = [var for var in tvars if 'g_' in var.name]
print([v.name for v in d_vars])
print([v.name for v in g_vars])
接下来,我们指定我们的优化器(optimizers)。GANs通常选用Adam算法作为优化算法;采用自适应学习率和momentum。在训练generator和discriminator时,我们调用把Adam算法最小化功能,并且声明用于更新generator和discriminator的权重和偏置量的变量。
在discriminator中,我们建立了两个不同的训练操作:一是训练鉴别真实图像和一个训练鉴别假图像。对这两个训练操作来说,使用不同的学习率有时候是有用的,或用它们分别使用不同的方法调节学习。(不通顺,大家将就看吧)
# Train the discriminator
d_trainer_fake = tf.train.AdamOptimizer(0.0003).minimize(d_loss_fake, var_list=d_vars)
d_trainer_real = tf.train.AdamOptimizer(0.0003).minimize(d_loss_real, var_list=d_vars)
# Train the generator
g_trainer = tf.train.AdamOptimizer(0.0001).minimize(g_loss, var_list=g_vars)
这个算法很难让GANs收敛,而且往往需要很长时间的训练。这个时候使用可视化tensorboard跟踪训练过程很有用,它可以图形化展示标量如损失函数,显示样本图像训练时候的样本图像,或者说明神经网络的拓扑结构。
如果你在自己的机器上运行包括下面的单元在内的脚本。方法如下:在一个终端窗口,中运行tensorboard -- logdir = tensorboard /
,然后在浏览器中访问网址:http://localhost:6006
即可打开TensorBoard.
tf.summary.scalar('Generator_loss', g_loss)
tf.summary.scalar('Discriminator_loss_real', d_loss_real)
tf.summary.scalar('Discriminator_loss_fake', d_loss_fake)
images_for_tensorboard = generator(z_placeholder, batch_size, z_dimensions)
tf.summary.image('Generated_images', images_for_tensorboard, 5)
merged = tf.summary.merge_all()
logdir = "tensorboard/" + datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + "/"
writer = tf.summary.FileWriter(logdir, sess.graph)
现在我们开始迭代计算。首先我们简单的给discriminator一些初始训练,帮助它生成对generator有用的梯度。
然后我们继续训练循环。当我们训练generator时,feed一个随机向量Z给generator,并将输出传递给discriminator(即之前提到过的Dg)。generator的权重和偏置项将不断被更新,其产生图像可以达到以假乱真的效果(discriminator区分不开)。
为训练discriminator,feed 给discriminatorMNIST数据集的一批图像作为正例(positive examples),然后与feed生成的图像得到的结果做对比。记住,如果generator提高了输出图像质量,discriminator将持续认为生成的高质量图像为假。
因为它需要很长的时间来训练GANs,初次学习本教程,我们建议不要运行这个代码块。为继续学习本教程,接下来的代码会加载一个预先训练好的模型。
如果你自己想运行此代码,在快速的GPU上大概需3个小时,在桌面CPU上可能需要十倍的时间。
sess = tf.Session()
sess.run(tf.global_variables_initializer())
# Pre-train discriminator
for i in range(300):
z_batch = np.random.normal(0, 1, size=[batch_size, z_dimensions])
real_image_batch = mnist.train.next_batch(batch_size)[0].reshape([batch_size, 28, 28, 1])
_, __, dLossReal, dLossFake = sess.run([d_trainer_real, d_trainer_fake, d_loss_real, d_loss_fake],
{x_placeholder: real_image_batch, z_placeholder: z_batch})
if(i % 100 == 0):
print("dLossReal:", dLossReal, "dLossFake:", dLossFake)
# Train generator and discriminator together
for i in range(100000):
real_image_batch = mnist.train.next_batch(batch_size)[0].reshape([batch_size, 28, 28, 1])
z_batch = np.random.normal(0, 1, size=[batch_size, z_dimensions])
# Train discriminator on both real and fake images
_, __, dLossReal, dLossFake = sess.run([d_trainer_real, d_trainer_fake, d_loss_real, d_loss_fake],
{x_placeholder: real_image_batch, z_placeholder: z_batch})
# Train generator
z_batch = np.random.normal(0, 1, size=[batch_size, z_dimensions])
_ = sess.run(g_trainer, feed_dict={z_placeholder: z_batch})
if i % 10 == 0:
# Update TensorBoard with summary statistics
z_batch = np.random.normal(0, 1, size=[batch_size, z_dimensions])
summary = sess.run(merged, {z_placeholder: z_batch, x_placeholder: real_image_batch})
writer.add_summary(summary, i)
if i % 100 == 0:
# Every 100 iterations, show a generated image
print("Iteration:", i, "at", datetime.datetime.now())
z_batch = np.random.normal(0, 1, size=[1, z_dimensions])
generated_images = generator(z_placeholder, 1, z_dimensions)
images = sess.run(generated_images, {z_placeholder: z_batch})
plt.imshow(images[0].reshape([28, 28]), cmap='Greys')
plt.show()
# Show discriminator's estimate
im = images[0].reshape([1, 28, 28, 1])
result = discriminator(x_placeholder)
estimate = sess.run(result, {x_placeholder: im})
print("Estimate:", estimate)
因为以上代码会花很长的时间来训练GANs,我们建议你跳过以上代码执行下面的代码块。这里会加载一个在GPU上花了好几个小时训练好了的模型,并让您体验一个训练好了的GAN的输出。
saver = tf.train.Saver()
with tf.Session() as sess:
saver.restore(sess, 'pretrained-model/pretrained_gan.ckpt')
z_batch = np.random.normal(0, 1, size=[10, z_dimensions])
z_placeholder = tf.placeholder(tf.float32, [None, z_dimensions], name='z_placeholder')
generated_images = generator(z_placeholder, 10, z_dimensions)
images = sess.run(generated_images, {z_placeholder: z_batch})
for i in range(10):
plt.imshow(images[i].reshape([28, 28]), cmap='Greys')
plt.show()
GANs太难训练了。缺乏正确的超参数、网络结构,在训练过程中,discriminator可以压倒generator,反之亦然。
一个常见的失效模式(discriminator压倒了generator),100%能判断出生成的图像是假的。当discriminator做出100%肯定的时候,它导致没有generator没有可下降的梯度(过拟合了)。这是为什么discriminator直接产生unscaled的输出而不是将输出通过一个Sigmoid函数后,将取值范围定义在0和1之间。
另一个常见的失效模式,称为模式崩溃(mode collapse),这种情况下generator发现并利用了discriminator的弱点。如果generator无视输入Z的变化产生的图像非常相似就可以看做是”模式崩溃”。模式崩溃有时可以通过在某一方面“加强”discriminator得到校正,例如通过调整其训练速度或调整训练层数。
研究人员已经确定了一些对于建立稳定的GANs有帮助的”GAN hacks”。
Gans具有重塑我们每天进行交互的数字世界的巨大潜力。这个领域还很年轻,和下一个关于GANs伟大的发现可能是你做出的!
1:Ian Goodfellow和他的合作者发表于2014年的关于GAN的原始论文
2:Goodfellow最近的关于GAN的教程,某种程度上更接地气的解释了GAN。
3:Alec Radford、Luke Metz和Soumith Chintala的论文,介绍深层卷积GAN(DCGAN),其基本结构在我们的本教程的generator就是其基本结构。DCGAN代码在GitHub上。
这篇文章O’Reilly和tensorflow合作的部分成果,详见文章独立性声明。