GAN的编写 - tensorflow形式(tensorflow与GAN同学习,重点分析训练过程)

20200901 -
(本文完成于20200902下午,前面内容还算整洁,越到后面因为都是自己思考的过程,就导致文章越来越乱,就算是把自己思考的过程给记录下来吧)

0. 引言

之前的时候对keras框架编写的GAN网络进行了介绍《GAN的学习 - 训练过程(冻结判别器)》,但是发现去查看源代码的时候,经常有使用tensorflow框架编写的代码,所以寻思着把tensorflow框架也学一学,但是发现很多内容与keras是不一样的,例如可以自己定位网络参数,训练过程也不仅仅是fit那么简单;在涉及GAN时,更不需要利用trainable这种属性来实现判别器的冻结。
本篇文章学习一下利用tensorflow来进行GAN的编写,一方面是学习tensorflow的代码形式,一方面是学习GAN。

总结(20200910增加)

这里总结一下经过我这么多天潜移默化的理解,最近这些天一直都是在看WGAN及WGAN-GP的内容,同时因为是直接利用tensorflow,这种接近底层数学公式的方式来编程,所以对整体的内容是相对理解更深入了。

  1. 在判别器的优化过程中,就是使用普通的交叉熵函数作为损失函数来进行优化,不管是论文中的伪代码还是实际中的代码都没有什么难度
  2. 在生成器的优化过程中,要考虑一个对抗的过程,也就是往着判别器的反方向来进行优化,这也是实际的论文中伪代码所阐述的内容(注意,按照我的理解,论文中的伪代码应该是使用了0作为标签来训练生成器),没有什么问题;主要的问题是在实际的代码中,使用了tf.log(D_fake)作为损失函数,这一点实际上正是GAN的原作者提出了这种方案。然后理解了这个就能明白,为什么要使用1作为训练生成器的标签了。

1. GAN的代码编写

1.1 原始GAN的简单介绍

关于GAN的具体内容可以查看文章《GAN的学习 - 基础知识了解》。这里要说明的是,在GAN的训练过程中,需要使用两个网络,而且训练过程对两个过程有所区分。而GAN的原始论文训练过程的算法如下:
GAN训练过程
上面的伪代码非常简单,那么下面就来具体说明如何使用tensorflow来进行相应的代码编写。

1.2 tensorflow代码

本部分代码来源于文章[1],生成器和判别器分别使用两层的神经网络来说明,网络结构比较简单,主要是说明代码;而且我觉得,后续自己编写的时候,比如还是写一个GAN,那么就完全可以照着这种模式写,只不过到时要根据进行相应的自己需求的修改。

1.2.1 使用的网络结构

# Discriminator Net
# 两层的判别器模型
X = tf.placeholder(tf.float32, shape=[None, 784], name='X')
D_W1 = tf.Variable(xavier_init([784, 128]), name='D_W1')
D_b1 = tf.Variable(tf.zeros(shape=[128]), name='D_b1')
D_W2 = tf.Variable(xavier_init([128, 1]), name='D_W2')
D_b2 = tf.Variable(tf.zeros(shape=[1]), name='D_b2')
theta_D = [D_W1, D_W2, D_b1, D_b2]

# Generator Net
# 两层的生成器模型
Z = tf.placeholder(tf.float32, shape=[None, 100], name='Z')
G_W1 = tf.Variable(xavier_init([100, 128]), name='G_W1')
G_b1 = tf.Variable(tf.zeros(shape=[128]), name='G_b1')
G_W2 = tf.Variable(xavier_init([128, 784]), name='G_W2')
G_b2 = tf.Variable(tf.zeros(shape=[784]), name='G_b2')
theta_G = [G_W1, G_W2, G_b1, G_b2]

def generator(z):
    G_h1 = tf.nn.relu(tf.matmul(z, G_W1) + G_b1)
    G_log_prob = tf.matmul(G_h1, G_W2) + G_b2
    G_prob = tf.nn.sigmoid(G_log_prob)
    return G_prob

def discriminator(x):
    D_h1 = tf.nn.relu(tf.matmul(x, D_W1) + D_b1)
    D_logit = tf.matmul(D_h1, D_W2) + D_b2
    D_prob = tf.nn.sigmoid(D_logit)
    # 注意这里同时返回了logit
    return D_prob, D_logit

可能还有更简洁的方式来进行编写,但是这里我针对这段代码来进行理解。

  • 定义各层网络的参数
  • 利用函数的方式组织出相应的生成器和判别器

注意,这段代码在网络结构上使用了函数的形式,但我感觉重复使用应该要考虑tensorflow作用域的问题,这里留一个问题后续研究。(虽然看了那么多书介绍tensorflow的书,但是实际应用时,还是有时搞不清楚)。
其与keras的区别:

  • 输入分别是X和Z,而在构造模型时,返回的并不是一个模型(类似keras)而是其输出。
  • 在构造网络时,直接使用各个参数来进行数学运算,而非使用模型的形式
  • 最后的地方,没有模型的编译过程,同时没有直接使用某种类型的损失函数,而是直接对概率进行了输出。
    (20200910 增加)
    从上面的区别中可以看出,tensorflow的形式,就是直接定义了各种运算,从数学的角度来理解更容易。

1.2.2 模型的训练过程

G_sample = generator(Z)
D_real, D_logit_real = discriminator(X)
D_fake, D_logit_fake = discriminator(G_sample)

D_loss = -tf.reduce_mean(tf.log(D_real) + tf.log(1. - D_fake))
G_loss = -tf.reduce_mean(tf.log(D_fake))

# Only update D(X)'s parameters, so var_list = theta_D
D_solver = tf.train.AdamOptimizer().minimize(D_loss, var_list=theta_D)
# Only update G(X)'s parameters, so var_list = theta_G
G_solver = tf.train.AdamOptimizer().minimize(G_loss, var_list=theta_G)

def sample_Z(m, n):
    '''Uniform prior for G(Z)'''
    return np.random.uniform(-1., 1., size=[m, n])

for it in range(1000000):
    X_mb, _ = mnist.train.next_batch(mb_size)
    
    _, D_loss_curr = sess.run([D_solver, D_loss], 
	    feed_dict={X: X_mb, Z: sample_Z(mb_size, Z_dim)})
    _, G_loss_curr = sess.run([G_solver, G_loss], 
    	feed_dict={Z: sample_Z(mb_size, Z_dim)})

从上述代码来看,就是说连损失函数这种东西都需要是自己来控制,上面的的代码中也出现了,而且还要自己控制符号。但是平时使用keras的时候,基本上是不需要考虑这些事情的,所以对这部分内容根本不了解。而这里的关键问题是因为需要对抗学习的过程,一个是最大化,一个是最小化。但是这部分内容,体现在这个实际的权值更新应该是什么地方呢?可以从最开始的是训练算法中也看出,对于两个网络分别采用了最大化最小化的方式。
从这个角度来看,tensorflow的编程方式与keras的编程方式有很大不同。
(前文中的代码,是损失函数自己写的,但在他的github中,更新了这种方式,改为使用了tf的函数)。
(而且,阅读到这里的时候,我就感觉,使用tensorflow的方式更能深入理解神经网络的内容,而keras则是封装的太好了。)

在优化器部分的注释中可以看到,为了控制某一部分网络能够冻结,是在传入变量列表的时候就进行了相应的控制,实现了类似keras的trainable的功能。

2. GAN的训练过程

下午的时候,我一直就是在看这段代码,但是我觉得问题就出在这个训练过程,虽然BP算法什么的都知道,但是映射到代码中的过程不是很熟悉,而且之前的时候也说过,因为使用keras式的编程,对这部分内容一直没有关注,这就导致了根本无法理解这里的损失函数,还有他的梯度下降部分。单纯使用一种神经网络的时候也能理解,既然这样,就从这种方式来进行理解。(问题就出在这里,就是在原始的训练算法部分,要明白他为什么一个是增大,一个是减小。)
再进一步,也就是说GAN的对抗过程,落实到每次训练,应该是怎么样一个梯度修改的过程呢?具体的更新过程
现在算是有点明白了, 重新学习了一下交叉熵损失函数,还有参考了一个博客的说法[2],不过需要注意的是文章[2]中是针对一个完整的损失函数来说的。
完整
下面来从原始论文中,从两个更新过程中分别来说明具体的含义,以及其中的问题。

2.1 针对判别器的训练过程

首先要明白,原始交叉熵的损失函数是带有负号的,这个也是因为我一开始忘了这回事,所以导致一直就想不通他为什么要增加(ascending)权值。
原始交叉熵
上图是原始的交叉熵,其中y是真实的标签值。而对应到实际的GAN的第一个损失函数(也就是第一次采集真实和假数据一起训练的时候),
在这里插入图片描述

他本身就是一个交叉熵的函数。因为假标签的损失部分的数据是生成器产生的。也就是说,原始函数中的两个部分针对每个数据(真假)的时候,只需要看一个就行。判别器在训练过程中,因为后半部分就是假数据的内容,所以没有添加上1-0这个部分。
下面来具体说明一下他的目标。判别器输出是一个概率,也就是经过了sigmod函数计算的。
判别器的目标是尽可能分辨出这些输入的实际标签,也就是真假的结果。同时,如果是对的,就尽可能的把概率调高,而如果判定错误,那么就尽可能将判定缩小。在输入真数据的时候,那么就是logD(x)的部分,如果这个判定错了,概率比较小,那么这部分数值也会大,而如果数值概率就是1,这个数值就会小;在输入假数据的时候,那么正好,本身这个概率就反过来了,你输出一个概率较小,那么本身就是要判定这个类别为假,这个时候1-p正好损失函数也小了。

但是前面也提到了,log函数生成的是负值,要加一个符号,如果是正的,就是应该减小这个东西。这里没有添加负号,所以要增加这个数值。也就是求负值的最大值。
(20200910 增加)
上述部分是比较容易理解的,本质上就是一个简单的交叉熵函数,没有什么难的。对开始这里的困惑,也是因为论文中没有使用负号,而是使用了增加这个梯度的说法,实际上是等价的。不管是伪代码也好,或者实际代码也好,都是没有问题的。

2.2 针对生成器的训练过程

在这里插入图片描述
这里一开始理解的时候也比较疑惑,这里缺少了1-的部分,这正是因为每次训练的时候都是只输入了负例样本,但是,我确实给他了一个正例的标签。但是需要注意的是,这里判别器就已经不动了,他不再改动参数了。但是这里我的确一下子还是没想明白。按说,这个标签已经是真的了,也就是1,应该是前半部分,为什么会是后半部分呢。还是那句话,不管怎么样,交叉熵肯定是必然只剩一个部分,因为1-1的原因,那就是为什么是后半部分。

我昨晚的时候,好好想了一下,还是应该从损失函数入手,也就是从训练这个网络的目的入手,训练这个生成器的目的是什么,应该达到什么样的目标,是最大化这个损失函数,还是最小化这个损失函数,从这个角度就能理解。我中午的时候仔细思考了一下,同时结合这个引用的代码和上面的算法伪代码,他们在处理生成器的时候有些区别,一开始也是这部分理解不了。
在算法伪代码中,训练生成器的时候,还是将标签为0的伪数据输入到整个GAN中,只有这样才能是这个公式。然后,在看他说的,他要减少这个梯度,但是他这里的损失函数是负的,也就是说要最小化这个损失函数。基本上就是上面这些概念了,就能理解了。
再来看GAN的对抗过程,这里训练生成器的时候,判别器是冻结的。在上面训练判别器的时候,他是要最大化这个函数(因为是负的,本质上是最小化负号的),他的目的是为了将判别器的权值能够随着损失函数的最大化来进行变换。而在冻结判别器之后,就是更新生成器的过程是以迷惑判别器为目的,那么怎么来迷惑呢?就是按照最小化这个损失函数,同时权值同步更新。这样就能理解了。


20200910 - 这部分的阐述就是最开始的时候,虽然理解了伪代码中的内容:伪代码在训练生成器的时候,就是将0-负标签输入到生成器中,但是因为这部分和实际引用的代码部分(见下文)产生了冲突,所以一时理解不了。其实经过了这几天我的文章阅读,在很多文章中都指出,GAN的论文作者已经指出,可以使用代码中的形式来做为损失函数,但是这种损失函数当然是使用1作为标签。

(20200910 - 增加)
本质上,问题在于伪代码是log(1 - D_fake),而实际代码是log(D_fake),在最开始记录这部分内容的时候就是因为不理解这个问题。而实际上,在文章[1]和[3]中都说明了这部分内容其实是原始GAN论文作者说明的,可以使用log(D_fake)来替换前面的。不过,但是我没看到,而且没有看原始的GAN论文,所以一直就纠结这部分内容,但最后也是凭逻辑上的推理来结局的,并没有找到实际的数学上的证明。不过,也算是有了一部分理解。
GAN原文的作者的建议:在训练过程中,可以将最小化log(1 - D_fake)变为最大化log(D_fake)
(20200910 - 增加结束)

下面的部分是针对代码中的内容进行理解,与前文的伪代码没有关系

G_sample = generator(Z)
D_real, D_logit_real = discriminator(X)
D_fake, D_logit_fake = discriminator(G_sample)

D_loss = -tf.reduce_mean(tf.log(D_real) + tf.log(1. - D_fake))
G_loss = -tf.reduce_mean(tf.log(D_fake))

# Only update D(X)'s parameters, so var_list = theta_D
D_solver = tf.train.AdamOptimizer().minimize(D_loss, var_list=theta_D)
# Only update G(X)'s parameters, so var_list = theta_G
G_solver = tf.train.AdamOptimizer().minimize(G_loss, var_list=theta_G)

(即使是重新阅读这部分内容,还是花费了我很多时间来理解,感觉还是没有吃透这个损失函数的内容)

而在实际代码中,见上。(在文章[1]中也提到,tensorflow支持的是最小化梯度下降,这个可能也是一个原因把),(这部分在原来使用keras的时候也是,将生成数据的标签按照真来输入)。如果按照标签真的输入,那么损失函数应该就是交叉熵的前半部分,判别器就要降低这部分的损失函数,那么为了是新对抗过程,生成器应该增大这部分损失函数,但是这里也说不通。

如果仅仅是从代码的角度来讲,那么他最小化这个-tf.reduce_mean(tf.log(D_fake)),本质上就是最大化-tf.reduce_mean(tf.log(1-D_fake))。这样从数学的角度来理解好像是对的,因为前面判别器的权值就是要朝着最小化的方向来更新,这样就对应了那个过程。但是还是那句话,这种方式没有从实际意义上来理解,就是为什么把他当作是真的之后,没有这么一个过程。
所以我这里疑惑的问题是,如果是将损失函数变为了-tf.log(D_fake)形式,那么应该怎么从实际的意义上来理解。看对抗的过程,假设这部分输入的是真数据,那么判别器要降低这部分数值,而生成器应该是增大这部分数值。但实际上,这部分是假数据,判别器就会发现这里面很多内容都不对,那么肯定是自己的参数有问题,然后有了损失函数,但在该过程中,判别器实际上是被冻结的,所以这部分的损失函数就传到到了前面。使用正标签的意义就在于,本身判别器就能区分出来,那么就不需要对抗的过程,此时的损失函数也比较小。生成器,就是要最小化这个损失函数,这个损失函数是以欺骗判别器为代价,让他以为这个是正数据,而当我的生成器真的能够输出一个类似真的数据的时候,那么此时的这个损失函数也是最小的。

而如果是仅仅从为了训练的角度出发,也就是从损失函数的角度出发,本来是使用log(1-D_fake)的,但是现在换成了log(D_fake)。从生成器的角度来说,因为传递给判别器的数据是假的,那么此时的损失函数是很大的,所以能够利用这部分的梯度传递回来,最后根据这部分梯度来判定进行权值更新,最终就是能够让判别器能够输出一个比较小的损失函数,也就是说此时判别器已经可以认为这部分数据很像是真的了。本质上,最小化生成器生成的假数据(例如,图片)在判别器的部分也能够得到很小的损失函数。但是这个东西怎么跟对抗挂钩呢?就是判别器是怎么形成了一个反向的更新过程呢?那么从判别器的角度出发,如果它知道这个东西是假的,也就是传递进去的标签是假的,那么它必然是最小化这个数值,-log(1 - D_fake)。但是我传递进去假的,如果这部分梯度被判别器所吸收,然后进行相应的更新,必然是往反方向更新了,其实就是底层的标签被调换了,在概率上他以真数据为主。所以从这个角度是一个对抗过程。

而替换伪log(D_fake)的原因,我觉得是梯度的问题,或者说是损失函数大小的问题。原始的论文损失函数为log(1 - D_fake),而生成器的目的与判别器想法,是为了最大化的这个损失函数。但是实际上,在训练初期,或者得到一个非常好的判别器的时候,那么这个数值是非常小的,也就是判别器的性能非常好,那么这种情况下,就不得不使用其他的方法,而论文作者提出的这个log(D_fake)就很合适,只有在最后的时候才能变小(当然这个损失函数也有缺点,WGAN作者提出)。这样的逻辑就能顺利了。


在文章[1]提供的GAN源码中,针对损失函数,他还提供了另外一种方式。

D_loss_real = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(
	logits=D_logit_real, labels=tf.ones_like(D_logit_real)))
D_loss_fake = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(
	logits=D_logit_fake, labels=tf.zeros_like(D_logit_fake)))
D_loss = D_loss_real + D_loss_fake
G_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(
	logits=D_logit_fake, labels=tf.ones_like(D_logit_fake)))

这个跟前面说的是一样的,只不过是使用了tf自己的库而已。那么如果是按照这样来说明的话, 不管是判别器或者生成器的损失函数,都应该是越来越小为好。产生这种情况的原因,其实本质上是因为他没有像keras一样生成带有标签的数据,而是以一种隐式的方式说明了结果是1还是0。同时也告诉了我,我之前的时候训练一个非常简单的时序数据的GAN时候,那个损失函数肯定是不对的,他们最终应该是收敛于一个数值,而不是一直增加。

后记

本篇文章后面的地方写的很短,就是因为没有办法从实际意义上来完整理解,同时代码也不一样,导致了现在的结果。本身就是要学习wgan的损失函数的,但是因为不理解tf的代码,所以大部分时间都在学习这部分内容。还有一些内容没有记录。这里埋个坑。
Wasserstein GAN implementation in TensorFlow and Pytorch
Building a simple Generative Adversarial Network (GAN) using TensorFlow

参考

[1]Generative Adversarial Nets in TensorFlow
[2]GAN: 原始损失函数详解

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值