使用TensorFlow构建简单的生成对抗网络(GAN)
生成敌对网络或GAN是深度学习研究和开发中最活跃的领域之一,因为它们具有惊人的产生综合结果的能力。我们将通过一个具体的例子来构建GAN的基本直觉。这篇文章按以下方式分解:
生成敌对网络工作背后的基本思想和直觉
实现一个基于GAN的模型,从简单的分布生成数据
可视化和分析GAN的不同方面,以更好地理解幕后发生的事情
生成敌对网络
GAN背后的基本理念其实很简单。GAN的核心是包含两个具有相互竞争目标的代理,这些目标通过相反的目标发挥作用。这种相对简单的设置会导致代理人想出越来越复杂的方式来欺骗对方。这种情况可以用博弈论作为极大极小(minimax)游戏来模拟。
让我们举一个货币造假过程的理论例子。在这个过程中,我们可以想象两种类型的代理人:罪犯和警察。让我们看看他们的竞争目标:
罪犯的目的:罪犯的主要目的是想出复杂的伪造货币的方法,这样警察就不能区分假币和真金。
警察的目标:警察的主要目标是想出复杂的方法来区分假币和真金。
随着这一过程的发展,警察开发出越来越复杂的技术来检测伪钞和犯罪,开发出越来越复杂的伪造货币的技术。这就是所谓的对抗性过程的基础。
生成的对抗网络利用对抗性的过程来训练两个相互竞争的神经网络,直到达到理想的平衡为止。在这种情况下,我们有一个Generator Network G(Z),它接受输入随机噪声,并试图生成非常接近于我们的数据集的数据。另一种网络称为Discriminator Network D(X),它接收生成的数据,并试图区分生成的数据和真实数据。该网络在其核心实现了二进制分类,并输出输入数据实际上来自真实数据集(而不是合成数据或假数据)的概率。
在正式意义上,这整个过程的目标函数可以写成:
对于以上定义的GANs,通常理想的平衡点是,Generator 应该对真实的数据进行建模,而Discriminator 应该输出0.5的概率,因为生成的数据与实际数据相同——也就是说,不确定来自生成器的新数据是否真实或具有相同的概率。
您可能想知道为什么需要这样一个复杂的学习过程?学习这种模式的好处是什么?如果我们能够从一个模型生成真正的数据分布,那么这意味着我们知道所有关于这个模型的信息。很多时候这些真实的分布包含了数以百万计的图像我们可以用一个有上千个参数的模型来生成它们这些参数捕捉了给定图像的本质。
实施GAN
我们将生成一个非常简单的数据分布,并尝试使用上面描述的GANs模型来学习生成该分布数据的生成器函数。本节大致分为3个部分。首先,我们将编写一个基本函数来生成二次分布(真实的数据分布)。其次,我们编写了生成器和鉴别器网络的代码。然后我们将使用数据和网络编写代码,以一种对抗性的方式来训练这两个网络。
这个实现的目标是学习一个新的函数,它可以从与训练数据相同的分布中生成数据。训练的期望是我们的Generator 网络应该开始产生跟随二次分布的数据。虽然我们从非常简单的数据分布开始,但是这种方法可以很容易地扩展,以从更复杂的数据集生成数据。有几个例子,GANs成功地生成了手写数字的图像,名人,动物,等等。
生成训练数据
我们通过使用numpy库生成随机样本然后使用某种函数生成第二个坐标来实现我们的真实数据集。为了演示的目的,我们将函数保持为简单的二次函数。您可以使用此代码来生成具有更多维数和/或其特征之间更复杂关系的数据集,例如更高阶多项式,正弦,余弦等。
import numpy as np def get_y(x): return 10 + x*x def sample_data(n=10000, scale=100): data = [] x = scale*(np.random.random_sample((n,))-0.5) for i in range(n): yi = get_y(x[i]) data.append([x[i], yi]) return np.array(data)
生成的数据非常简单,可以如下所示绘制:
Generator 和Discriminator 网络实现
现在我们将使用张量流层来实现Generator 和Discriminator网络。我们使用以下函数实现Generator网络:
def generator(Z,hsize=[16, 16],reuse=False): with tf.variable_scope("GAN/Generator",reuse=reuse): h1 = tf.layers.dense(Z,hsize[0],activation=tf.nn.leaky_relu) h2 = tf.layers.dense(h1,hsize[1],activation=tf.nn.leaky_relu) out = tf.layers.dense(h2,2) return out
该函数接受placeholder随机样本(Z),一个数组hsize用于2个隐藏层中的单元数,以及一个reuse用于重复使用相同图层的变量。使用这些输入,它创建了具有给定数量的节点的2个隐藏层的全连接的神经网络。这个函数的输出是一个二维向量,它对应于我们试图学习的真实数据集的维度。上述功能可以很容易修改,以包含更多的隐藏层,不同类型的层,不同的激活和不同的输出映射。
我们使用以下函数实现Discriminator网络:
def discriminator(X,hsize=[16, 16],reuse=False): with tf.variable_scope("GAN/Discriminator",reuse=reuse): h1 = tf.layers.dense(X,hsize[0],activation=tf.nn.leaky_relu) h2 = tf.layers.dense(h1,hsize[1],activation=tf.nn.leaky_relu) h3 = tf.layers.dense(h2,2) out = tf.layers.dense(h3,1) return out, h3
该函数placeholder为真实数据集的向量空间中的样本提供输入。样本可以是真实样本,也可以是Generator 网络生成的样本。类似于上面的Generator 网络,它也需要输入hsize和reuse。我们对Discriminator使用3个隐藏层,其中前两层是我们输入的。我们将第三个隐藏层的大小固定为2,以便我们可以在2D平面中可视化变换的特征空间,如后面部分所述。这个函数的输出是logit预测X最后一层的给定和输出,这是由Discriminator学习的特征变换X。该logit 函数是sigmoid函数的反函数,用于表示可能性的对数(变量为1的概率与0的概率的比率)。
对抗训练
为了训练的目的,我们分别为实际样本和随机噪声样本定义了以下占位符X和Z:
X = tf.placeholder(tf.float32,[None,2]) Z = tf.placeholder(tf.float32,[None,2])
我们还需要创建用于从Generator网络生成样本的图表,并将实际和生成的样本提供给Discriminator网络。这是通过使用上面定义的函数和占位符完成的:
G_sample = generator(Z) r_logits, r_rep = discriminator(X) f_logits, g_rep = discriminator(G_sample,reuse=True)
使用生成数据和实际数据的logits,我们为Generator和Discriminator网络定义损失函数,如下所示:
disc_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=r_logits,labels=tf.ones_like(r_logits)) + tf.nn.sigmoid_cross_entropy_with_logits(logits=f_logits,labels=tf.zeros_like(f_logits))) gen_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=f_logits,labels=tf.ones_like(f_logits)))
这些损失是基于我们上面定义的方程的sigmoid交叉熵。这是离散分类常用的损失函数。它需要输入logit(由我们的Discriminator网络给出)和每个样本的真实标签。然后计算每个样本的误差。我们使用的优化版本是由TensorFlow实现的,它更稳定,然后直接计算交叉熵。有关更多细节,您可以查看相关的TensorFlow API。
接下来,我们定义了两个网络的优化器,使用了上面定义的损失函数和在Generator和Discriminator函数中定义的层的范围。我们在两个网络中使用RMSProp优化器,学习速率为0.001。使用范围我们仅为给定的网络获取权值/变量。
gen_vars = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES,scope="GAN/Generator") disc_vars = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES,scope="GAN/Discriminator") gen_step = tf.train.RMSPropOptimizer(learning_rate=0.001).minimize(gen_loss,var_list = gen_vars) # G Train step disc_step = tf.train.RMSPropOptimizer(learning_rate=0.001).minimize(disc_loss,var_list = disc_vars) # D Train step
然后,我们以交替的方式训练两个网络,以实现所需的步骤数量:
for i in range(100001): X_batch = sample_data(n=batch_size) Z_batch = sample_Z(batch_size, 2) _, dloss = sess.run([disc_step, disc_loss], feed_dict={X: X_batch, Z: Z_batch}) _, gloss = sess.run([gen_step, gen_loss], feed_dict={Z: Z_batch}) print "Iterations: %d Discriminator loss: %.4f Generator loss: %.4f"%(i,dloss,gloss)
上述代码可以修改为包括更复杂的训练过程,例如运行Discriminator和/或Generator更新的多个步骤,获取真实和生成样本的特征并绘制生成的样本。
分析GANs
可视化训练损失
为了更好地理解这个过程中发生的事情,我们可以在每10次迭代之后绘制培训损失。从下面的图中我们可以看到损失的变化是如何逐渐减少的,在训练结束的时候,损失几乎是恒定的。
在训练过程中可视化样本
我们也可以在每1000次训练之后绘制真实和生成的样本。这些图可视化了Generator网络是如何从输入和数据集向量空间之间的随机初始映射开始的,然后逐渐地发展到类似于真实的数据集样例。正如您所看到的,“假”样本开始越来越像“真实”的数据分布。
在本节中,我们可以看到在敌对训练过程中更新发电机网络权重的效果。我们通过绘制Discriminator网络最后一层隐藏层的激活来做到这一点。我们选择最后一个隐藏层的大小为2,以便在不需要降维的情况下进行绘图(即将输入样本转换为不同的向量空间)。我们感兴趣的是可视化Discriminator网络学习到的特征转换功能。这个功能是我们网络学习的功能,所以真实的和假的数据是可分离的。
在我们更新Generator网络的权重之前和之后,我们绘制了Discriminator网络最后一层学到的真实样本和生成样本的特征变换。我们还绘制了我们在输入样本的特征变换之后获得的点的质心。最后,在生成器更新之前和之后,我们分别计算真实和假数据的点的质心。从图中我们可以推断出以下几点:
如预期的那样,实际数据样本的变换特征没有变化。从图形中,我们可以看到它们完全一致。
从质心中,我们可以看到生成的数据样本的特征质心几乎总是朝着实际数据样本的特征质心移动。
我们还可以看到,随着迭代次数的增加,实际样本的变换特征越来越多地与生成样本的变换特征混合在一起。这也是预料之中的,因为在训练结束时,鉴别器网络不应该能够区分实际样本和生成的样本。因此,在训练结束时,两个样本的变换特征应该重合。