本篇是《GAN实战对抗生成网络》一书的第一个例程,下载书本源代码链接,不过书本中的主程序的的损失函数部分代码需要做些改动,详情可参考GAN系列:代码阅读(2)——Generative Adversarial Networks & 李宏毅老师GAN课程P1+P4和详解GAN代码之逐行解析GAN代码。
备注:本人此处的代码以及我参考的两处博客代码均可跑通,不过win7,8G运存大约需要数小时,运行结果图可见本人下篇文章.
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec #gridspec module: 给图像分格 #gridspec是图片排列工具,在训练过程中用于输出可视化结果
import os
from tensorflow.examples.tutorials.mnist import input_data #导入手写数字数据集
# 定义服从正态分布的函数,产生参数
#初始化参数时使用的xavier_init函数
def xavier_init(size):
# 产生标准差的过程
input_dim = size[0]
#初始化标准差
xavier_variance = 1. / tf.sqrt(input_dim/2.)
# 噪声服从正态分布,size[0]应该是噪声向量长度
return tf.random_normal(shape=size, stddev=xavier_variance)
def save(saver, sess, logdir, step): #保存模型的save函数
model_name = 'model' #模型名前缀
checkpoint_path = os.path.join(logdir, model_name) #保存路径
saver.save(sess, checkpoint_path, global_step=step) #保存模型
print('The checkpoint has been created.')
# 定义画图函数,samples是要画的图像集
def plot(samples):
# 在matplotlib中,整个图像为一个Figure对象。在Figure对象中可以包含一个或者多个Axes对象。
# 每个Axes(ax)对象都是一个拥有自己坐标系统的绘图区域
# 创建图像窗口,定义尺寸4*4
fig = plt.figure(figsize=(4, 4))
# gridspec将整个图像分格:4*4共16个
gs = gridspec.GridSpec(4, 4)
# 调整图像布局参数,只影响当前Gridspec对象
gs.update(wspace=0.05, hspace=0.05)
# 遍历图像集,拼成一个大图用来展示结果
for i, sample in enumerate(samples):
# title为图像标题,Axis为坐标轴,Tick为刻度线,Tick Label为刻度注释
ax = plt.subplot(gs[i])
plt.axis('off')
ax.set_xticklabels([])
ax.set_yticklabels([])
ax.set_aspect('equal')
# 把图像变回28*28,cmap表示使用'Greys_r'灰度图
plt.imshow(sample.reshape(28, 28), cmap='Greys_r')
# 返回整张大图
return fig
# Random noise setting for Generator
# placeholder实例通常用来为算法的实际输入值作占位符,赋值时必须用sess.run中的optional argument:feed_dict进行赋值
## Z为随机噪声的batch,向量长度为100
Z = tf.placeholder(tf.float32, shape=[None, 100], name='Z')
#Generator parameter settings
# 生成器第一层权重和偏置变量
G_W1 = tf.Variable(xavier_init([100, 128]), name='G_W1')
G_b1 = tf.Variable(tf.zeros(shape=[128]), name='G_b1')
# 生成器第二层权重和偏置变量,输出784维向量,作为图片
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]
# Generator Network
# 定义生成器函数:z是输入的随机噪声batch矩阵,网络为全连接
#生成器generator(z)从随机分布中(本例采用统一分布)接收一个100维向量作为输入,并产生一个符合MNIST规范(28X28)的784维向量。
#这里的z是G(z)的先验。通过这种方式可以学习到先验空间和Pdata(真实数据分布)空间之间的映射
def generator(z):
# matmul:矩阵相乘
# 生成器激活函数为relu,只有第一层激活
G_h1 = tf.nn.relu(tf.matmul(z, G_W1) + G_b1)
G_log_prob = tf.matmul(G_h1, G_W2) + G_b2
# 第二层输出前用sigmoid函数,为什么?答:为了进行二分类,或者说是为了输出真假概率. 评:回答错误,判别器是为了二分类,生成器不是
G_prob = tf.nn.sigmoid(G_log_prob)
return G_prob
#Input Image MNIST setting for Discriminator [28x28=784]
#X表示真实图片batch,每个图片向量长度为784:28*28
X = tf.placeholder(tf.float32, shape=[None, 784], name='X')
#Discriminator parameter settings
#Variable创建变量后必须赋值才能使用,在计算图的运算过程中,其值会一直保存到程序运行结束,
#而一般的tensor张量在tensorflow运行过程中只是在计算图中流过,并不会保存下来
# varibale主要用来保存tensorflow构建的一些结构中的参数,
# 这些参数才不会随着运算的消失而消失,才能最终得到一个模型
# 创建初始随机噪声的权重和偏置变量
# 权重服从正态分布,784*128(因为下一层有128个神经元?);偏置初始化为0
D_W1 = tf.Variable(xavier_init([784, 128]), name='D_W1')
D_b1 = tf.Variable(tf.zeros(shape=[128]), name='D_b1')
# 继续创建下一层权重和偏置的变量
# 权重依然为正态分布,128*1(判别器网络只有两层?);偏置依然为0
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]
# Discriminator Network
# 定义判别器函数:x是输入的图像batch矩阵
#判别器discriminator(x)以MNIST图像作为输入并返回一个代表真实图像概率的标量。即输入的是图像,输出的是概率(标量)
def discriminator(x):
# 网络也为全连接,最后sigmoid输出,可以理解为二分类器
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)
# 判别器sigmoid前的结果D_logit也输出了
return D_prob, D_logit
# 把随机噪声batch矩阵输入生成器中,产生生成图像batch矩阵,取得生成器的生成结果
G_sample = generator(Z)
# 把真实图像batch矩阵输入判别器得到结果,取得判别器判别的真实手写数字的结果
# D_logit_real是sigmoid之前的结果,用于logistic回归
D_real, D_logit_real = discriminator(X)
# 把生成图像batch矩阵输入判别器得到结果,取得判别器判别的生成的手写数字的结果
D_fake, D_logit_fake = discriminator(G_sample)
# GAN论文中loss定义:reduce_mean因为求均值降维,得到的loss是标量
# Loss functions from the paper
#D_loss = -tf.reduce_mean(tf.log(D_real) + tf.log(1. - D_fake))
#G_loss = -tf.reduce_mean(tf.log(D_fake))
#此处是github下载的源代码,总是迭代一会之后就会显示loss为nan
#Tensorflow的优化器只能做最小化,因此为了最大化损失函数,我们在上面的代码中给损失加上了一个负号。与此同时,根据论文的伪代码算法,
#我们最好最大化tf.reduce_mean(tf.log(D_fake)),而不是最小化tf.reduce_mean(tf.log(1. - D_fake))。
# Alternative losses:
# -------------------
# 分别计算正负样本与标签1和0之间的交叉熵,再合并
# 和原始方法比,相当于给判别器打分一个基准:0和1
#对判别器对真实样本的判别结果计算误差(将结果与1比较)
D_loss_real = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logit_real, labels=tf.ones_like(D_logit_real)))
#对判别器对虚假样本(即生成器生成的手写数字)的判别结果计算误差(将结果与0比较)
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 #判别器的误差
#生成器的误差(将判别器返回的对虚假样本的判别结果与1比较)
G_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logit_fake, labels=tf.ones_like(D_logit_fake)))
# 优化算法为Adam,两者都是minimize
# 原本的判别器loss形式导致其值越大,判别器效果越好;现在的形式表示loss值越小,性能越好,因此是minimize
#接下来我们通过下面的过程一步步训练损失函数:
# Update D(X)'s parameters only,var_list=theta_D
D_solver = tf.train.AdamOptimizer().minimize(D_loss, var_list=theta_D)
# Update G(Z)'s parameters only,var_list=theta_G
G_solver = tf.train.AdamOptimizer().minimize(G_loss, var_list=theta_G)
# 定义采样函数:随机生成-1到1的值,m*n
def sample_Z(m, n):
return np.random.uniform(-1., 1., size=[m, n])
batch_size = 128
# 随机噪声向量长度
Z_dim = 100
#会话对象封装了执行操作对象和计算张量对象的环境
sess = tf.Session()
# global_variables_initializer:Returns an Op that initializes global variables
# 就是初始化参数的操作
sess.run(tf.global_variables_initializer())
# 载入数据
mnist = input_data.read_data_sets('MNIST/', one_hot=True)
# 如果路径中没有输出图像的文件夹,创建一个
if not os.path.exists('output/'):
os.makedirs('output/')
i = 0
# 一共训练1000000次
for itr in range(10000):
# 每1000次迭代输出一次生成结果并保存
if itr % 1000 == 0:
# 一次结果中包含16个小图片
samples = sess.run(G_sample, feed_dict={Z: sample_Z(16, Z_dim)})
fig = plot(samples)
plt.savefig('output/{}.png'.format(str(i).zfill(3)), bbox_inches='tight')
i += 1
plt.close(fig)
# 按照设置的batch_size从训练集中取图片
X_mb, _ = mnist.train.next_batch(batch_size)
# 训练过程:判别器和生成器训练同样次数?是因为用了Adam的优化算法吗?
_, D_loss_curr = sess.run([D_solver, D_loss], feed_dict={X: X_mb, Z: sample_Z(batch_size, Z_dim)})
_, G_loss_curr = sess.run([G_solver, G_loss], feed_dict={Z: sample_Z(batch_size, Z_dim)})
# 每1000次输出当前迭代的次数,判别器和生成器优化前的loss
if itr % 1000 == 0:
print('Iter: {}'.format(itr))
print('D loss: {:.4}'. format(D_loss_curr))
print('G_loss: {:.4}'.format(G_loss_curr))
print()
#训练过程中会出现loss为nan,具体原因有可能是梯度爆炸,详情见https://blog.csdn.net/qq_33485434/article/details/80733251