一个mnist的GAN

自己想从0实现一个GAN,读了很多博客,看了开始大神Ian J.Goodfellow的那篇论文,也看了一些视频(觉得还是看论文好,博客写的都是轻描淡写,胡扯一堆,一到具体公式就摸瞎,论文写的真的很清楚;还有就是原始论文都是用多层感知器,梯度上升什么的,现在变化很多,一般都会加卷积核什么东西,效果比论文里要好很多)

反正杂七杂八的反正看了几个小时,看的东西大体的总结起来在以下的几个方面。

看的东西总结

1.GAN相关小故事,什么起源于二元博弈(two-player game),什么造假币和警察,还有,这些基础小故事没少读
2.GAN的结构,生成器和判别器两部分组成,看别人的博客有下面这样一段话:生成模型 G 捕捉样本数据的分布,用服从某一分布(均匀分布,高斯分布等)的噪声 z 生成一个类似真实训练数据的样本,追求效果是越像真实样本越好;判别模型 D 是一个二分类器,估计一个样本来自于训练数据(而非生成数据)的概率,如果样本来自于真实的训练数据,D 输出大概率,否则,D 输出小概率。
这段话我觉得不懂的时候根本看不懂,看懂之后觉得说的Gan也就是这了,也没啥。

我自己的理解

我的理解分7步吧,本来很想说清楚下面那张目标函数的图片,哎,算了:

  1. 其实你的理解要建立在一个基础就是:数据的真实分布其实是可以被一个分布(比如正态分布添点噪音)被似然估计出来的,这样就过网络参数的各种计算,这个分布就能对应上一个真正的样本,这时候D就分不出来,生成模型G就很成功。
  2. 如果你接受了上面的想法,那gan的生成器G的训练就变成了对一个分布p加上噪声z后到一张真正图片中间的参数训练,而判别器d就是和原来一样对原来的数据进行分类,真的就是1,生成的是0,一个二分类
  3. 假如你再接受了上面的生成器和判别器要做的事情,那就可以说他们的损失函数了。D的损失函数有两个,一个是真的图片和1去比的D_loss_real和生成的图片和0去比的D_loss_fake,然后两个加起来D_loss = D_loss_real + D_loss_fake;G的损失函数就是用判别器判别出来的生成的结果和1比的G_loss。
  4. 再总结一下上面说的,你用的都是判别器的结果,无非是谁跟谁去算损失函数的关系。D想的和一般的分类模型没区别,假的就是假的,要往0走就和0比,真的就是真的,要往1走就和1比,然后这个损失越来越小,我这个D就越来越好;而G想的是假的就是真的,那就希望假的往真的走,让判别器都认为假的结果越来越真,然后这个损失越来越小,我这个G就越来越好
  5. 第四点里面看起来很有道理,但实际上有一句很关键的话,就是你怎么让判别器都认为假的是真的呢,而且你刚才不是说了希望判别器能够将假的分成假的,真的分成真的吗,这不是矛盾了吗?首先说不矛盾,而且我觉得Gan的精髓就在这个地方(生成器的目标函数),简直让人拍案叫绝(据说人第一次看懂Gan的目标函数的时候都会忍不住的赞叹)
    先放一张损失函数的图片,这个公式有几个小trick,上面说了正常情况下应该是比3个东西,0和0比,1和1比和生成的0和1比,但是0和0比,1和1比不是生成器那就没法说是一个模型了,好像是一个组合模型,于是0和1比,1和0比,梯度上升,maxD就把这两个东西写到一个公司里了,很妙,你要是一下子就能懂,就不用看后面了:
  6. 为什么不矛盾?因为训练D的时候其实跟正常分类器训练没区别,一句话都不用多说,就是想让判别器越来越好,然后按照0,1分类,有数据有标签判别器是会越来越好的。主要就是G的损失函数,这时候如果固定判别器的参数,就是一个很好的判断真假的判别器的参数,你去训练G,G让假的和真的越来越像,就是使G最小,D最大了。
  7. 其实第6点说的已经很明白了,再结合一下上面图片里的目标函数看一遍,Pdata是真实数据,Pz是生成的分布。因为先训练D在训练G,而且训练谁的时候对方都不变,所以理想的D和理想的G最后就是一直在进行二元博弈。最后训练的G要让理想的D都判断不出来,就变成理想的G了。
  8. 这是一张论文的图,大概意思就是先训练D进行k次,随机梯度上升中间那个式子,正常是1-x比,z和0比,偏偏反着来就是梯度上升,然后换成G,随机梯度下降第二个式子,最后将这两个目标函数放在一起,整体可以写成上面那个式子。

tensorflow实现mnist数据集的GAN

首先说明几点

第一个就是论文梯度上升,我们肯定直接普通着来都会梯度下降;
第二个就是原始的Gan不容易拟合,所以用的是cDCGan,就是条件深度卷积GAN,这个生成收敛效果很多
第三就是因为从网上借鉴了很多的代码改的,具体代码谁写的确定不了,就不表明代码借鉴于谁了
其他的都在代码里细说吧

代码-cDCGAN

c指条件,一般是指把y_label放入
把mnist的数据集(4个gz文件,不要解压)下载到mnist文件夹,然后文件夹和这个python文件一个目录就能直接运行了。

import os, time, itertools, imageio, pickle, random
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data


# leaky_relu
def lrelu(X, leak=0.2):
    f1 = 0.5 * (1 + leak)
    f2 = 0.5 * (1 - leak)
    return f1 * X + f2 * tf.abs(X)


# G(z)
def generator(x, y_label, isTrain=True, reuse=False):
    with tf.variable_scope('generator', reuse=reuse):
        # initializer
        # w_init 正态分布的一个数,主要kernel_initializer使用他生成kernel
        # b_init 常数0,bias_initializer使用它生成bias
        w_init = tf.truncated_normal_initializer(mean=0.0, stddev=0.02)
        b_init = tf.constant_initializer(0.0)

        # concat layer 条件GAN就是,3是代表纬度,纬度从0开始算的
        # 这里的x是生成的噪音z,一个图片的起源就是这个1*1*100的z
        # 这里的y_label是one-hot编码的标签,1*1*10
        # 所以cat1是一个batch_size*1*1*110的shape的矩阵
        cat1 = tf.concat([x, y_label], 3)

        # 反卷积或者上采样,就是由小变大的过程
        # 归一化,预防梯度消失和梯度爆炸
        # 1st hidden layer
        # deconv1 [batch_size,7,7,256]
        deconv1 = tf.layers.conv2d_transpose(cat1, filters=256, kernel_size=[7, 7], strides=(1, 1), padding='valid',
                                             kernel_initializer=w_init, bias_initializer=b_init)
        lrelu1 = lrelu(tf.layers.batch_normalization(deconv1, training=isTrain), 0.2)

        # 2nd hidden layer
        # deconv2 [batch_size,14,14,128]
        deconv2 = tf.layers.conv2d_transpose(lrelu1, 128, [5, 5], strides=(2, 2), padding='same',
                                             kernel_initializer=w_init, bias_initializer=b_init)
        lrelu2 = lrelu(tf.layers.batch_normalization(deconv2, training=isTrain), 0.2)

        # output layer
        # deconv3 [batch_size,28,28,1]
        deconv3 = tf.layers.conv2d_transpose(lrelu2, 1, [5, 5], strides=(2, 2), padding='same',
                                             kernel_initializer=w_init, bias_initializer=b_init)
        # tanh 双曲正切激活函数
        # 最后的o的形状 [batch_size,28,28,1]
        o = tf.nn.tanh(deconv3)

        return o


# D(x)
def discriminator(x, y_fill, isTrain=True, reuse=False):
    with tf.variable_scope('discriminator', reuse=reuse):
        # initializer,和生成器一样
        w_init = tf.truncated_normal_initializer(mean=0.0, stddev=0.02)
        b_init = tf.constant_initializer(0.0)

        # concat layer,将x和y_fill连接起来
        # 这里的x有两种,刚才的o和真实的图片x,他们两个的形状都是[batch_size,28,28,1]
        # 这里的y_fill也是有两种,不过都是y_label进行转换的,刚才的o全都是随机生成的y_label转换的,真实的图片是用真实的图片label转换的
        # cat1的shape [batch_size*28*28*11]
        cat1 = tf.concat([x, y_fill], 3)

        # 1st hidden layer
        # conv1的shape [batch_size*14*14*128]
        conv1 = tf.layers.conv2d(cat1, 128, [5, 5], strides=(2, 2), padding='same', kernel_initializer=w_init,
                                 bias_initializer=b_init)
        lrelu1 = lrelu(conv1, 0.2)

        # 2nd hidden layer
        # conv2的shape [batch_size*7*7*256]
        conv2 = tf.layers.conv2d(lrelu1, 256, [5, 5], strides=(2, 2), padding='same', kernel_initializer=w_init,
                                 bias_initializer=b_init)
        lrelu2 = lrelu(tf.layers.batch_normalization(conv2, training=isTrain), 0.2)

        # output layer
        # conv3的shape [batch_size*1*1*1]
        conv3 = tf.layers.conv2d(lrelu2, 1, [7, 7], strides=(1, 1), padding='valid', kernel_initializer=w_init)
        # sigmoid激活,label
        o = tf.nn.sigmoid(conv3)

        return o, conv3


# 预处理,为了生成图片展示

# 10*10的单位矩阵
onehot = np.eye(10)
# 正态分布 temp_z_ shape:[10, 1, 1, 100]
temp_z_ = np.random.normal(0, 1, (10, 1, 1, 100))
fixed_z_ = temp_z_

fixed_y_ = np.zeros((10, 1))
for i in range(9):
    fixed_z_ = np.concatenate([fixed_z_, temp_z_], 0)
    temp = np.ones((10, 1)) + i
    fixed_y_ = np.concatenate([fixed_y_, temp], 0)

fixed_y_ = onehot[fixed_y_.astype(np.int32)].reshape((100, 1, 1, 10))


def show_result(num_epoch, show=False, save=False, path='result.png'):
    test_images = sess.run(G_z, {z: fixed_z_, y_label: fixed_y_, isTrain: False})

    size_figure_grid = 10
    fig, ax = plt.subplots(size_figure_grid, size_figure_grid, figsize=(5, 5))
    for i, j in itertools.product(range(size_figure_grid), range(size_figure_grid)):
        ax[i, j].get_xaxis().set_visible(False)
        ax[i, j].get_yaxis().set_visible(False)

    for k in range(10 * 10):
        i = k // 10
        j = k % 10
        ax[i, j].cla()
        ax[i, j].imshow(np.reshape(test_images[k], (img_size, img_size)), cmap='gray')

    label = 'Epoch {0}'.format(num_epoch)
    fig.text(0.5, 0.04, label, ha='center')

    if save:
        plt.savefig(path)

    if show:
        plt.show()
    else:
        plt.close()


def show_train_hist(hist, show=False, save=False, path='Train_hist.png'):
    x = range(len(hist['D_losses']))

    y1 = hist['D_losses']
    y2 = hist['G_losses']

    plt.plot(x, y1, label='D_loss')
    plt.plot(x, y2, label='G_loss')

    plt.xlabel('Epoch')
    plt.ylabel('Loss')

    plt.legend(loc=4)
    plt.grid(True)
    plt.tight_layout()

    if save:
        plt.savefig(path)

    if show:
        plt.show()
    else:
        plt.close()


# training parameters
img_size = 28
batch_size = 100
# lr = 0.0002
train_epoch = 30
global_step = tf.Variable(0, trainable=False)
# 学习率采用指数衰减的方式
lr = tf.train.exponential_decay(0.0002, global_step, 500, 0.95, staircase=True)
# load MNIST
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True, reshape=[])

# variables : input
x = tf.placeholder(tf.float32, shape=(None, img_size, img_size, 1))
z = tf.placeholder(tf.float32, shape=(None, 1, 1, 100))
# 会被onehot编码
y_label = tf.placeholder(tf.float32, shape=(None, 1, 1, 10))
y_fill = tf.placeholder(tf.float32, shape=(None, img_size, img_size, 10))
isTrain = tf.placeholder(dtype=tf.bool)

# networks : generator
# 生成器使用,上面说的很清楚了
G_z = generator(z, y_label, isTrain)

# networks : discriminator
# 判别器,对真的x去进行多分类的判别
D_real, D_real_logits = discriminator(x, y_fill, isTrain)
# 判别器,对真的x去进行多分类的判别
D_fake, D_fake_logits = discriminator(G_z, y_fill, isTrain, reuse=True)

# loss for each network
# 真的和1比,假的和0比
D_loss_real = tf.reduce_mean(
    tf.nn.sigmoid_cross_entropy_with_logits(logits=D_real_logits, labels=tf.ones([batch_size, 1, 1, 1])))
D_loss_fake = tf.reduce_mean(
    tf.nn.sigmoid_cross_entropy_with_logits(logits=D_fake_logits, labels=tf.zeros([batch_size, 1, 1, 1])))
D_loss = D_loss_real + D_loss_fake
# G的损失就是假的和1比
G_loss = tf.reduce_mean(
    tf.nn.sigmoid_cross_entropy_with_logits(logits=D_fake_logits, labels=tf.ones([batch_size, 1, 1, 1])))

# trainable variables for each network
# https://blog.csdn.net/jerr__y/article/details/70809528
# 分开D和W的参数,分别存进D_vars和G_vars,以便于AdamOptimizer确定更新是哪里的参数
# 这个地方保证了训练G,固定D,训练D,固定G
T_vars = tf.trainable_variables()
D_vars = [var for var in T_vars if var.name.startswith('discriminator')]
G_vars = [var for var in T_vars if var.name.startswith('generator')]

# optimizer for each network
# https://tensorflow.google.cn/api_docs/python/tf/layers/batch_normalization
# tf.control_dependencies(tf.get_collection(tf.GraphKeys.UPDATE_OPS))是和归一化一同使用的,官网上这样写的
with tf.control_dependencies(tf.get_collection(tf.GraphKeys.UPDATE_OPS)):
    optim = tf.train.AdamOptimizer(lr, beta1=0.5)
    D_optim = optim.minimize(D_loss, global_step=global_step, var_list=D_vars)
    # D_optim = tf.train.AdamOptimizer(lr, beta1=0.5).minimize(D_loss, var_list=D_vars)
    G_optim = optim.minimize(G_loss, var_list=G_vars)

# open session and initialize all variables
sess = tf.InteractiveSession()
tf.global_variables_initializer().run()

# MNIST resize and normalization
# train_set = tf.image.resize_images(mnist.train.images, [img_size, img_size]).eval()
# train_set = (train_set - 0.5) / 0.5  # normalization; range: -1 ~ 1
# mnist.train.images 是一个55000*28*28*1的矩阵, mnist.train.labels是一个已经onehot编码过的55000*10的矩阵
train_set = (mnist.train.images - 0.5) / 0.5
train_label = mnist.train.labels

# results save folder
root = 'MNIST_cDCGAN_results/'
model = 'MNIST_cDCGAN_'
if not os.path.isdir(root):
    os.mkdir(root)
if not os.path.isdir(root + 'Fixed_results'):
    os.mkdir(root + 'Fixed_results')

train_hist = {}
train_hist['D_losses'] = []
train_hist['G_losses'] = []
train_hist['per_epoch_ptimes'] = []
train_hist['total_ptime'] = []

# training-loop
np.random.seed(int(time.time()))
print('training start!')
start_time = time.time()
for epoch in range(train_epoch):
    G_losses = []
    D_losses = []
    epoch_start_time = time.time()
    # 将55000个数打乱
    shuffle_idxs = random.sample(range(0, train_set.shape[0]), train_set.shape[0])
    shuffled_set = train_set[shuffle_idxs]
    shuffled_label = train_label[shuffle_idxs]
    for iter in range(shuffled_set.shape[0] // batch_size):
        # update discriminator
        x_ = shuffled_set[iter * batch_size:(iter + 1) * batch_size]
        y_label_ = shuffled_label[iter * batch_size:(iter + 1) * batch_size].reshape([batch_size, 1, 1, 10])
        y_fill_ = y_label_ * np.ones([batch_size, img_size, img_size, 10])
        z_ = np.random.normal(0, 1, (batch_size, 1, 1, 100))

        loss_d_, _ = sess.run([D_loss, D_optim], {x: x_, z: z_, y_fill: y_fill_, y_label: y_label_, isTrain: True})

        # update generator
        # 因为假设x服从z分布,所以只要将一样的x放进一样的分布,其结果y应该是一样的,这是为什么D和G都是在自己直接生产z_,因为无所谓
        # 标准正态分布,μ=0,σ=1,第三个是生成的shape
        z_ = np.random.normal(0, 1, (batch_size, 1, 1, 100))
        y_ = np.random.randint(0, 9, (batch_size, 1))
        y_label_ = onehot[y_.astype(np.int32)].reshape([batch_size, 1, 1, 10])
        # 就是想让形状相同
        y_fill_ = y_label_ * np.ones([batch_size, img_size, img_size, 10])
        loss_g_, _ = sess.run([G_loss, G_optim], {z: z_, x: x_, y_fill: y_fill_, y_label: y_label_, isTrain: True})

        errD_fake = D_loss_fake.eval({z: z_, y_label: y_label_, y_fill: y_fill_, isTrain: False})
        errD_real = D_loss_real.eval({x: x_, y_label: y_label_, y_fill: y_fill_, isTrain: False})
        errG = G_loss.eval({z: z_, y_label: y_label_, y_fill: y_fill_, isTrain: False})

        D_losses.append(errD_fake + errD_real)
        G_losses.append(errG)

    epoch_end_time = time.time()
    per_epoch_ptime = epoch_end_time - epoch_start_time
    print('[%d/%d] - ptime: %.2f loss_d: %.3f, loss_g: %.3f' % (
        (epoch + 1), train_epoch, per_epoch_ptime, np.mean(D_losses), np.mean(G_losses)))
    fixed_p = root + 'Fixed_results/' + model + str(epoch + 1) + '.png'
    show_result((epoch + 1), save=True, path=fixed_p)
    train_hist['D_losses'].append(np.mean(D_losses))
    train_hist['G_losses'].append(np.mean(G_losses))
    train_hist['per_epoch_ptimes'].append(per_epoch_ptime)

end_time = time.time()
total_ptime = end_time - start_time
train_hist['total_ptime'].append(total_ptime)

print('Avg per epoch ptime: %.2f, total %d epochs ptime: %.2f' % (
    np.mean(train_hist['per_epoch_ptimes']), train_epoch, total_ptime))
print("Training finish!... save training results")
with open(root + model + 'train_hist.pkl', 'wb') as f:
    pickle.dump(train_hist, f)

show_train_hist(train_hist, save=True, path=root + model + 'train_hist.png')

images = []
for e in range(train_epoch):
    img_name = root + 'Fixed_results/' + model + str(e + 1) + '.png'
    images.append(imageio.imread(img_name))
imageio.mimsave(root + model + 'generation_animation.gif', images, fps=5)

sess.close()

代码解释

leaky_relu

ReLU是将所有的负值都设为零,相反,Leaky ReLU是给所有负值赋予一个非零斜率。Leaky ReLU激活函数是在声学模型(2013)中首次提出的。

def lrelu(X, leak=0.2):
    f1 = 0.5 * (1 + leak)
    f2 = 0.5 * (1 - leak)
    return f1 * X + f2 * tf.abs(X)

上面这段代码,如果X里的某个xi是正数,这个xi不变,如果xi是负数,就变成了-0.2*xi

充分理解 name/variable_scope

https://blog.csdn.net/jerr__y/article/details/70809528

最后想说的话

P.S.写到最后发现自己写的跟别人写的也差不多,本来想着能把目标函数解释清楚,最后发现明白大概意思很简单,严格论证必须看论文,而且那个目标函数中间用的有梯度上升,就是假的和1比,真的和0比,这个越大越好,不过是为了式子的统一,真的是太精简了,说那个很难说明白,遂放弃。。。。。

发表评论

电子邮件地址不会被公开。