前言
既然学习了变分自编码(VAE),那也必须来一波生成对抗网络(GAN)。
国际惯例,参考网址:
论文: Generative Adversarial Nets
PPT:Generative Adversarial Networks (GANs)
Generative Adversarial Nets in TensorFlow
GAN原理学习笔记
GAN — Ways to improve GAN performance
理论
粗略点的讲法就说:一个生成器GGG,一个判别器DDD,前者用来将噪声输入转换成图片,后者判别当前输入图片是真实的还是生成的。
为了更详细地了解GAN,还是对论文进行简要的组织、理解吧。有兴趣直接看原始论文,这里进行部分关键内容的摘抄。
任意的GGG和DDD函数空间都存在特定解,GGG要能表示训练集的分布,而DDD一定是等于12\frac{1}{2}21,也就是说判别器无法分辨当前输入是真的还是假的,这样就达到了鱼目混珠的效果。在GAN中,使用多层感知器构建GGG和DDD,整个模型可以使用反向传播算法学习。
论文里面有一句很好的话解释了GAN的动机:目前深度学习在判别模型的设计中取得了重大成功,但是在生成模型却鲜有成效,主要原因在于在极大似然估计和相关策略中有很多难以解决的概率计算难题(想想前一篇博客的变分自编码的理论,阔怕),而且丢失了生成背景下的分段线性单元的优势,因此作者就提出了新的生成模型估计方法,避开这些难题,也就是传说中的GAN。它的训练完全不需要什么鬼似然估计,只需要使用目前炒鸡成功的反传和dropout算法。
为了让生成器学到数据分布pgp_gpg,需要定义一个先验的噪声输入pz(z)p_z(z)pz(z),然后使用G(z;θg)G(z;\theta_g)G(z;θg)将其映射到数据空间,这里的GGG是具有参数θg\theta_gθg的多层感知器。然后定义另一个多层感知器D(x;θd)D(x;\theta_d)D(x;θd),输出一个标量。D(x)D(x)D(x)代表的是xxx来自于真实样本而非生成的样本pgp_gpg的概率,我们训练DDD去最大化将正确标签同时赋予训练集和GGG生成的样本的概率,也就是DDD把真的和假的图片都当成真的了。同时要去训练GGG去最小化log(1−D(G(z)))\log (1-D(G(z)))log(1−D(G(z))),是为了让生成的图片被赋予正样本标签的概率大点,损失函数就是:
minGmaxDV(D,G)=Ex∼pdata(x)[logD(x)]+Ez∼pz(z)[log(1−D(G(z)))]\min_G \max_D V(D,G)=E_{x\sim p_{data}(x)}[\log D(x)]+E_{z\sim p_z(z)}[\log(1-D(G(z)))] GminDmaxV(D,G)=Ex∼pdata(x)[logD(x)]+Ez∼pz(z)[log(1−D(G(z)))]
在优化DDD的时候,在训练的内循环中是无法完成的,计算上不允许,并且在有限数据集上会导致过拟合,因此可以以k:1k:1k:1 的训练次数比例分别优化DDD和GGG。这能够让DDD保持在最优解附近,只要GGG变化比较缓慢。
而且在实际中,上式可能无法提供足够的梯度让GGG很好地学习,在训练早期,当GGG很差的时候,DDD能够以很高的概率将其归为负样本,因为生成的数据与训练数据差距很大,这样log(1−D(G(z)))\log(1-D(G(z)))log(1−D(G(z)))就饱和了,与其说最小化log(1−D(G(z)))\log(1-D(G(z)))log(1−D(G(z)))不如去最大化log(D(G(z)))\log(D(G(z)))log(D(G(z))),这个目标函数对GGG和DDD的收敛目标不变,但是能早期学习具有更强的梯度。
训练算法:
外层一个大循环就不说了,对所有的批数据迭代,内层有一个小循环,控制上面说的判别器DDD与生成器GGG的训练比例为k:1k:1k:1的:
-
以下步骤执行kkk次:
-
从噪声先验pg(z)p_g(z)pg(z)中采样mmm个噪声样本{z(1),⋯ ,z(m)}\{z^{(1)},\cdots,z^{(m)}\}{z(1),⋯,z(m)}
-
从原始样本分布pdata(x)p_{data}(x)pdata(x)中选取mmm个样本x(1)⋯x(m){x^{(1)}\cdots x^{(m)}}x(1)⋯x(m),说这么复杂,原始样本的分布不就是原始样本么,直接从原始样本里面选一批数据就行了
-
更新判别器
∇θd1m∑i=1m[logD(x(i))+log(1−D(G(z(i))))]\nabla_{\theta_d}\frac{1}{m}\sum_{i=1}^m \left[\log D\left(x^{(i)}\right)+\log \left(1-D\left(G\left(z^{(i)}\right)\right)\right)\right] ∇θdm1i=1∑m[logD(x(i))+log(1−D(G(z(i))))]
-
-
从噪声先验pg(z)p_g(z)pg(z)中采样mmm个噪声样本{z(1),⋯ ,z(m)}\{z^{(1)},\cdots,z^{(m)}\}{z(1),⋯,z(m)}
-
更新生成器
∇θg1m∑i=1mlog(1−D(G(z(i))))\nabla \theta_g \frac{1}{m}\sum_{i=1}^m \log\left(1-D\left(G \left( z^{(i)}\right) \right)\right) ∇θgm1i=1∑mlog(1−D(G(z(i))))
后面作者又证明了两个内容:
- pg=pdatap_g=p_{data}pg=pdata的全局最优
- 训练算法的收敛性
身为一个合格的程序猿,还是很有必要看看数学推导的o(╯□╰)o虽然不一定能懂
先看一个简单的式子:y→alog(y)+blog(1−y)y\to a\log(y)+b\log(1-y)y→alog(y)+blog(1−y),这个式子在[0,1][0,1][0,1]范围取得最大值的点是在aa+b\frac{a}{a+b}a+ba,证明很简单,直接两边求导,令y′=0y'=0y′=0就能算出来。
再看看我们的优化目标,当给定生成器GGG的时候,也就是GGG固定的时候:
V(G,D)=∫xpdata(x)logD(x)dx+∫zpz(z)log(1−D(g(z)))dz=∫xpdata(x)log(D(x))+pg(x)log(1−D(x))dxV(G,D)=\int_x p_{data}(x)\log D(x)dx+\int _zp_z(z)\log(1-D(g(z)))dz\\ =\int _xp_{data}(x)\log(D(x))+p_g(x)\log(1-D(x))dx V(G,D)=∫xpdata(x)logD(x)dx+∫zpz(z)log(1−D(g(z)))dz=∫xpdata(x)log(D(x))+pg(x)log(1−D(x))dx
长得挺像,那么DDD的最优解就是:
DG∗(x)=pdata(x)pdata(x)+pg(x)D_G^*(x)=\frac{p_{data}(x)}{p_{data}(x)+p_g(x)} DG∗(x)=pdata(x)+pg(x)pdata(x)
对DDD的训练目标可以看成最大化对数似然去估算条件概率P(Y=y∣x)P(Y=y|x)P(Y=y∣x),这里YYY表示xxx来自于原始数据pdatap_{data}pdata(此时y=1y=1y=1)还是生成数据pgp_gpg(此时y=0y=0y=0),所以损失函数又可以写成:
C(G)=maxDV(G,D)=Ex∼pdata[logDG∗(x)]+Ez∼pz[log(1−DG∗(G(z)))]=Ex∼pdata[logDG∗(x)]+Ez∼pg[log(1−DG∗(x))]=Ex∼pdata[logpdata(x)pdata(x)+pg(x)]+Ex∼pg[logpg(x)pdata(x)+pg(x)]\begin{aligned} C(G)&=\max_D V(G,D)\\ &=E_{x\sim p_{data}}\left[\log D_G^*(x)\right]+E_{z\sim p_z}\left[\log(1-D^*_G(G(z)))\right]\\ &=E_{x\sim p_{data}}\left[\log D_G^*(x)\right]+E_{z\sim p_g}\left[\log(1-D^*_G(x))\right]\\ &=E_{x\sim p_{data}}\left[\log\frac{p_{data}(x)}{p_{data}(x)+p_g(x)}\right]+E_{x\sim p_g}\left[\log\frac{p_g(x)}{p_{data}(x)+p_g(x)}\right] \end{aligned} C(G)=DmaxV(G,D)=Ex∼pdata[logDG∗(x)]+Ez∼pz[log(1−DG∗(G(z)))]=Ex∼pdata[logDG∗(x)]+Ez∼pg[log(1−DG∗(x))]=Ex∼pdata[logpdata(x)+pg(x)pdata(x)]+Ex∼pg[logpdata(x)+pg(x)pg(x)]
理论1 当且仅当pg=pdatap_g=p_{data}pg=pdata的时候,训练目标C(G)C(G)C(G)达到全局最小,此时,C(G)C(G)C(G)收敛到值−log4-\log 4−log4
证明:当pg=pdatap_g=p_{data}pg=pdata的时候,DG∗(x)=12D^*_G(x)=\frac{1}{2}DG∗(x)=21,因此C(G)=log12+log12=−log4C(G)=\log \frac{1}{2}+\log\frac{1}{2}=-\log 4C(G)=log21+log21=−log4,感觉正常应该是:
Ex∼pdata[−log2]+Ex∼pg[−log2]E_{x\sim p_{data}}[-\log 2]+E_{x\sim p_g}[-\log 2] Ex∼pdata[−log2]+Ex∼pg[−log2]
但是作者貌似让Ex∼pdata=Ex∼pg=1E_{x\sim p_{data}}=E_{x\sim p_g}=1Ex∼pdata=Ex∼pg=1了,我估计是因为收敛到最终解的时候,理想状态是判别器无法分辨哪个真哪个假,所以都当成正样本了。这样还能将C(G)C(G)C(G)变形:
C(G)=−log4+KL(pdata∥pdata+pg2)+KL(pg∥pdata+pg2)C(G)=-\log 4+KL\left(p_{data}\parallel\frac{p_{data}+p_g}{2}\right)+KL\left(p_g\parallel \frac{p_{data}+p_g}{2}\right) C(G)=−log4+KL(pdata∥2pdata+pg)+KL(pg∥2pdata+pg)
其实最终理想状态下后面两个KL
距离是等于0的,代表衡量的两个分布一样。
这里作者提到了一个表达式称为Jensen-Shannon divergence
,衡量模型分布和数据生成过程:
C(G)=−log4+2⋅JSD(pdata∥pg)C(G)=-\log 4+2\cdot JSD(p_{data}\parallel p_g) C(G)=−log4+2⋅JSD(pdata∥pg)
这个JSDJSDJSD始终是非负的,当且仅当pdata=pgp_{data}=p_gpdata=pg的时候取000,意思就是生成模型能够完美生成数据分布。
接下来看看算法收敛性
理论2 如果GGG和DDD有足够的容量,并且在训练算法的每一步,给定GGG时判别器都能达到它的最优,那么pgp_gpg的更新便可以通过优化
Ex∼pdata[logDG∗(x)]+Ex∼pg[log(1−DG∗(x))]E_{x\sim p_{data}}\left[\log D_G^*(x)\right]+E_{x\sim p_g}\left[\log (1-D_G^*(x))\right] Ex∼pdata[logDG∗(x)]+Ex∼pg[log(1−DG∗(x))]
然后pgp_gpg就收敛到了pdatap_{data}pdata
证明就不看了,因为我不是特别懂作者那一段文字,我们只需要知道收敛结果就是pg=pdatap_g=p_{data}pg=pdata就行了。
代码实现-模型训练与保存
老流程:读数据、初始化相关参数、定义数据接受接口、初始化权重和偏置、构建基本模块(生成器和判别器)、构建模型、定义损失和优化器、训练
读取数据
这个就不说了,全博客通用:
IMG_HEIGHT=28
IMG_WIDTH=28
CHANNELS=3
#读取数据集
def read_images(dataset_path,batch_size):imagepaths,labels=list(),list()data=open(dataset_path,'r').read().splitlines()for d in data:imagepaths.append(d.split(' ')[0])labels.append(int(d.split(' ')[1]))imagepaths=tf.convert_to_tensor(imagepaths,dtype=tf.string)labels=tf.convert_to_tensor(labels,dtype=tf.int32)image,label=tf.train.slice_input_producer([imagepaths,labels],shuffle=True)image=tf.read_file(image)image=tf.image.decode_jpeg(image,channels=CHANNELS)image=tf.image.rgb_to_grayscale(image) image=tf.reshape(image,[IMG_HEIGHT*IMG_WIDTH])image=tf.cast(image,tf.float32)image = image / 255.0image=tf.convert_to_tensor(image)inputX,inputY=tf.train.batch([image,label],batch_size=batch_size,capacity=batch_size*8,num_threads=4)return inputX,inputY
定义相关参数
主要是学习率,训练次数,输入单元、隐单元、输出单元的神经元个数
#定义相关参数
learning_rate=0.0002
num_steps=1000
batch_size=128
disp_step=1000
num_class=10
gen_hid_num=256
dis_hid_num=256
noise_dim=100
num_input=IMG_HEIGHT*IMG_WIDTH
定义数据接收接口
需要注意GAN
主要有两类接口,生成器接收的是噪声输入,判别器接收的是真实图片或者生成的图片
#建立生成器、判别器的接收接口
gen_input=tf.placeholder(tf.float32,shape=[None,noise_dim],name='gen_input')
dis_input=tf.placeholder(tf.float32,shape=[None,num_input],name='dis_input')
初始化权重
#定义权重
def glorot_init(shape):return tf.random_normal(shape=shape,stddev=1/tf.sqrt(shape[0]/2.0))weights={'gen_hidden1':tf.Variable(glorot_init([noise_dim,gen_hid_num])),'gen_out':tf.Variable(glorot_init([gen_hid_num,num_input])),'dis_hidden1':tf.Variable(glorot_init([num_input,dis_hid_num])),'dis_out':tf.Variable(glorot_init([dis_hid_num,1]))
}
biases={'gen_hidden1':tf.Variable(tf.zeros([gen_hid_num])),'gen_out':tf.Variable(tf.zeros([num_input])),'dis_hidden1':tf.Variable(tf.zeros([dis_hid_num])),'dis_out':tf.Variable(tf.zeros([1]))
}
基本模块:生成器和判别器
#定义基本模块
def generator(x):hidden_layer=tf.add(tf.matmul(x,weights['gen_hidden1']),biases['gen_hidden1'])hidden_layer=tf.nn.relu(hidden_layer)out_layer=tf.add(tf.matmul(hidden_layer,weights['gen_out']),biases['gen_out'])out_layer=tf.nn.sigmoid(out_layer)return out_layerdef discriminator(x):hidden_layer=tf.add(tf.matmul(x,weights['dis_hidden1']),biases['dis_hidden1'])hidden_layer=tf.nn.relu(hidden_layer)out_layer=tf.add(tf.matmul(hidden_layer,weights['dis_out']),biases['dis_out'])out_layer=tf.nn.sigmoid(out_layer)return out_layer
构建模型
注意我们的测试函数是生成器,最终需要它来生成图片,所以需要加入函数中
#生成器
gen_sample=generator(gen_input)
tf.add_to_collection('generation',gen_sample)
#判别器
dis_real=discriminator(dis_input)
dis_fake=discriminator(gen_sample)
定义损失和优化器
损失函数包含生成器和判别器,但是针对生成器,我们上面说过,最小化log(1−D(G(z)))\log (1-D(G(z)))log(1−D(G(z)))不如最大化log(D(G(z)))\log(D(G(z)))log(D(G(z))),所以生成器的损失可以定义为负对数,这样就把最大化又变成最小化了:
gen_loss = -tf.reduce_mean(tf.log(disc_fake))
当然你也可以使用最小化的方法,此博文即用最小化log(1−D(G(z)))\log(1-D(G(z)))log(1−D(G(z)))的方法:
G_loss = tf.reduce_mean(tf.log(1-prob_artist1))
判别器还是老样子最大化log(D(x))−log(D(G(z)))\log (D(x))-\log(D(G(z)))log(D(x))−log(D(G(z))),加个负号也是最小化了:
disc_loss = -tf.reduce_mean(tf.log(disc_real) + tf.log(1. - disc_fake))
【注】其实有的时候也可以直接用交叉熵来定义损失,让判别器对真实图片的标签接近1,对假图片的判别标签接近0
d_loss_real = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits = D, labels = tf.ones_like(D)))
d_loss_fake = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits = _D, labels = tf.zeros_like(_D)))
d_loss = d_loss_real + d_loss_fake
而对于生成器,希望判别器对假图片的判别标签接近1:
g_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits = _D, labels = tf.ones_like(_D)))
话不多说,接下来定义优化器:
optimizer_gen=tf.train.AdamOptimizer(learning_rate=learning_rate)
optimizer_dis=tf.train.AdamOptimizer(learning_rate=learning_rate)
但是因为采用的类似于固定梯度下降法,即在更新生成器时,判别器参数不动,同理更新判别器时生成器参数不动,所以需要先指定分开训练的时候分别对谁求梯度:
#因为采用了固定梯度下降,所以必须知道每个优化器需要优化什么
gen_var=[weights['gen_hidden1'],weights['gen_out'],biases['gen_hidden1'],biases['gen_out']]
dis_var=[weights['dis_hidden1'],weights['dis_out'],biases['dis_hidden1'],biases['dis_out']]
这样就可以针对性求解了:
#优化
train_gen=optimizer_gen.minimize(gen_loss,var_list=gen_var)
train_dis=optimizer_dis.minimize(dis_loss,var_list=dis_var)
训练模型与保存
#初始化
init=tf.global_variables_initializer()
saver=tf.train.Saver()
input_image,input_label=read_images('./mnist/train_labels.txt',batch_size)
with tf.Session() as sess:sess.run(init)coord=tf.train.Coordinator()tf.train.start_queue_runners(sess=sess,coord=coord)for step in range(1,num_steps):time_start = time.time()batch_x,batch_y=sess.run([input_image,tf.one_hot(input_label,num_class,1,0)])z=np.random.uniform(-1.0,1.0,size=(batch_size,noise_dim))sess.run([train_gen,train_dis],feed_dict={dis_input:batch_x,gen_input:z})if step%1000==0 or step==1: g_loss,d_loss=sess.run([gen_loss,dis_loss],feed_dict={gen_input:z,dis_input:batch_x})time_end=time.time()print('step:%i----Generator loss:%f-----Discriminator Loss:%f' %(step,g_loss,d_loss))coord.request_stop()coord.join()print('optimization finished')saver.save(sess,'./GAN_mnist_model/GAN_mnist')
我没有训练多少次,有兴趣的可以多训练,最终让判别器的损失接近0.50.50.5就说明接近最优解了,我的训练结果:
step:1----Generator loss:0.393680-----Discriminator Loss:1.626469
step:1000----Generator loss:3.580971-----Discriminator Loss:0.078812
step:2000----Generator loss:4.907338-----Discriminator Loss:0.037951
step:3000----Generator loss:5.269949-----Discriminator Loss:0.015779
step:4000----Generator loss:3.202836-----Discriminator Loss:0.119377
step:5000----Generator loss:3.977841-----Discriminator Loss:0.140365
step:6000----Generator loss:3.546029-----Discriminator Loss:0.111060
step:7000----Generator loss:3.723459-----Discriminator Loss:0.099416
step:8000----Generator loss:4.479396-----Discriminator Loss:0.130558
step:9000----Generator loss:4.041896-----Discriminator Loss:0.132201
step:10000----Generator loss:3.873767-----Discriminator Loss:0.241299
step:11000----Generator loss:4.237263-----Discriminator Loss:0.162134
step:12000----Generator loss:3.463274-----Discriminator Loss:0.223905
step:13000----Generator loss:3.941289-----Discriminator Loss:0.261881
step:14000----Generator loss:3.292896-----Discriminator Loss:0.356275
optimization finished
【更新日志】2018-8-27
还是依据论文流程,把判别器的训练放在前面
d_loss,g_loss=sess.run([dis_loss,gen_loss],feed_dict={dis_input:batch_x,gen_input:z})
代码实现-模型调用
还是老套路:
-
载入模型
sess=tf.Session() new_saver=tf.train.import_meta_graph('./GAN_mnist_model/GAN_mnist.meta') new_saver.restore(sess,'./GAN_mnist_model/GAN_mnist')
-
载入运算图
graph=tf.get_default_graph() print(graph.get_all_collection_keys()) #['generation', 'queue_runners', 'summaries', 'train_op', 'trainable_variables', 'variables']
-
获取预测函数和数据接收接口
gen=graph.get_collection('generation') gen_input=graph.get_tensor_by_name('gen_input:0')
-
随便丢个噪声给生成器
noise_input=np.random.uniform(-1.0,1.0,size=[1,100]) g=sess.run(gen,feed_dict={gen_input:noise_input}) gen_img=g[0]*255.0 gen_img=gen_img.reshape(28,28) plt.imshow(gen_img) plt.show()
后记
效果貌似不是特别好呢,可能训练次数不是特别够,也可能传统的GAN
结构对手写数字的生成能力不够,需要加深层数或者使用更好的GAN
变种算法,后续打算再找几个GAN
算法研究研究。这里先贴一下这篇博客关于GAN的损失函数的对比
训练代码:链接:https://pan.baidu.com/s/12_DNKILTtletYbDDhHDi6Q 密码:vyu7
测试代码:链接:https://pan.baidu.com/s/1rvAKjBnazzKRiL7nPiufVQ 密码:nreb
【更新日志】2018-8-27
从论文来看,我们一般需要先训练判别器,再训练生成器,但是TensorFlow-Examples中给的例子是
_, _, gl, dl = sess.run([train_gen, train_disc, gen_loss, disc_loss],feed_dict=feed_dict)
建议还是改一下:
_, _, gl, dl = sess.run([train_disc,train_gen, disc_loss, gen_loss],feed_dict=feed_dict)
但是收敛度遇到问题了,目前正在解决。
【更新日志2018-8-29日】
找到未收敛或者收敛程度不好的原因了,要使用AdamOptimizer
优化器,不要使用AdagradOptimizer
优化器