深度学习:对抗网络GAN的代码实现流程详细解读(超详细,必看)
写在前面的话

GAN可以用任何形式的generator和discriminator,不一定非得使用神经网络。而神经网络被广泛使用的主要原因是它一种通用函数逼近算法(universal function approximator),即我们能够使用大量节点的神经网络来模拟任何非线性的Input与Output之间的函数,相对其他方法具有更高的自由度,不会因为算法本身的能力而受限。对于generator或discriminator没有任何形式的限制,两者的形式也不必要相同。

如果对GAN对抗网络的基本概念还不清楚,可以参考我上一篇博客深度学习:GAN 对抗网络原理详细解析(零基础必看)。如果您已经了解GAN对抗网络的基本原理,请忽略本段,直接下面的内容。

本文拟分为两个部分:
part1 形象说明GAN对抗网络的形成过程
part2 通过一段代码,对GAN进行逐行解释,让读者更透彻的了解对抗网络到底如何进行。

part1 假设场景
假设我想仿照梵高的星空,为了得到大众的认可,我需要寻求知名画家的评鉴。这里‘我’就是那个生成器(Generator),而那个‘知名画家’就是识别器(Discriminator)。下面就是我的创作过程。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

大家看,我通过不断地修改我地创作并反复交给‘大湿’品鉴,最终我的画作从完全不像到可以以假乱真。这样一个创作/修改/再创作/再修改的过程就是GAN对抗网络的工作流程。

下面我们通过一段代码对GAN的工作流程进行一次实战讲解。其中代码部分来源为github上https://github.com/wiseodd/generative-models

part2 代码实战讲解

import tensorflow as tf #导入tensorflow
from tensorflow.examples.tutorials.mnist import input_data #导入手写数字数据集
import numpy as np #导入numpy
import matplotlib.pyplot as plt #plt是绘图工具,在训练过程中用于输出可视化结果
import matplotlib.gridspec as gridspec #gridspec是图片排列工具,在训练过程中用于输出可视化结果
import os #导入os

上面这段代码主要是对库的引入,与其他卷积网络操作类似

def xavier_init(size): #初始化参数时使用的xavier_init函数
    in_dim = size[0] 
    xavier_stddev = 1. / tf.sqrt(in_dim / 2.) #初始化标准差
    return tf.random_normal(shape=size, stddev=xavier_stddev) #返回初始化的结果

这里引入了一个参数初始化的方法,不是本文关注的重点,这里不提。

X = tf.placeholder(tf.float32, shape=[None, 784]) #X表示真的样本(即真实的手写数字)

识别器会输入这些真的样本,得到真的样本的输出标本,以便与生成的假样本做对比。 (生成器输出的结果的格式要于此统一)

D_W1 = tf.Variable(xavier_init([784, 128])) #表示使用xavier方式初始化的判别器的D_W1参数,是一个784行128列的矩阵
D_b1 = tf.Variable(tf.zeros(shape=[128])) #表示全零方式初始化的判别器的D_1参数,是一个长度为128的向量 
D_W2 = tf.Variable(xavier_init([128, 1])) #表示使用xavier方式初始化的判别器的D_W2参数,是一个128行1列的矩阵
D_b2 = tf.Variable(tf.zeros(shape=[1])) ##表示全零方式初始化的判别器的D_1参数,是一个长度为1的向量
theta_D = [D_W1, D_W2, D_b1, D_b2] #theta_D表示判别器的可训练参数集合

我们给识别器搭建了一个网络架构,这个架构是我们自定义的,搭建的思想和卷积网络的建模思想一致。目的是为了根据输入的图像找到他的特点,最终做比较使用。

Z = tf.placeholder(tf.float32, shape=[None, 100]) #Z表示生成器的输入(在这里是噪声),是一个N列100行的矩阵

我们之前说过,生成器的输入是一组噪声。这组噪声通过生成器进行各种排列足和等,得到我们最终需要的输出结果。

G_W1 = tf.Variable(xavier_init([100, 128])) #表示使用xavier方式初始化的生成器的G_W1参数,是一个100行128列的矩阵
G_b1 = tf.Variable(tf.zeros(shape=[128])) #表示全零方式初始化的生成器的G_b1参数,是一个长度为128的向量 
G_W2 = tf.Variable(xavier_init([128, 784])) #表示使用xavier方式初始化的生成器的G_W2参数,是一个128行784列的矩阵
G_b2 = tf.Variable(tf.zeros(shape=[784])) #表示全零方式初始化的生成器的G_b2参数,是一个长度为784的向量
theta_G = [G_W1, G_W2, G_b1, G_b2] #theta_G表示生成器的可训练参数集合

这是生成器的架构模式。对比这个架构和识别器的架构,有点相似。感觉就是把识别器的架构倒过来了。生成器就是把输入的噪声,通过层层映射最终输出一个图片。而图片的尺寸要和识别器的输入尺寸相对应。(仔细看这里的数据结构,这其实是一个反卷积的过程)

def sample_Z(m, n): #生成维度为[m, n]的随机噪声作为生成器G的输入
    return np.random.uniform(-1., 1., size=[m, n])

对生成器的输入进行了归一化处理

def generator(z): #生成器,z的维度为[N, 100]
    G_h1 = tf.nn.relu(tf.matmul(z, G_W1) + G_b1) #输入的随机噪声乘以G_W1矩阵加上偏置G_b1,G_h1维度为[N, 128]
    G_log_prob = tf.matmul(G_h1, G_W2) + G_b2 #G_h1乘以G_W2矩阵加上偏置G_b2,G_log_prob维度为[N, 784]
    G_prob = tf.nn.sigmoid(G_log_prob) #G_log_prob经过一个sigmoid函数,G_prob维度为[N, 784] 
    return G_prob #返回G_prob

这里噪声输入到生成器里,通过隐藏层和输出层的权重计算,最终得到输出结果,格式就是[N,784],这个格式也是识别器的输入格式。

def discriminator(x): #判别器,x的维度为[N, 784]
    D_h1 = tf.nn.relu(tf.matmul(x, D_W1) + D_b1) #输入乘以D_W1矩阵加上偏置D_b1,D_h1维度为[N, 128]
    D_logit = tf.matmul(D_h1, D_W2) + D_b2 #D_h1乘以D_W2矩阵加上偏置D_b2,D_logit维度为[N, 1]
    D_prob = tf.nn.sigmoid(D_logit) #D_logit经过一个sigmoid函数,D_prob维度为[N, 1]
    return D_prob, D_logit #返回D_prob, D_logit

这是识别器的内部计算过程。

G_sample = generator(Z) #取得生成器的生成结果
D_real, D_logit_real = discriminator(X) #取得判别器判别的真实手写数字的结果
D_fake, D_logit_fake = discriminator(G_sample) #取得判别器判别的生成的手写数字的结果
#对判别器对真实样本的判别结果计算误差(将结果与1比较)
D_loss_real = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logit_real, targets=tf.ones_like(D_logit_real))) 
#对判别器对虚假样本(即生成器生成的手写数字)的判别结果计算误差(将结果与0比较)
D_loss_fake = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logit_fake, targets=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, targets=tf.ones_like(D_logit_fake))) 

mnist = input_data.read_data_sets('../../MNIST_data', one_hot=True) #mnist是手写数字数据集

D_solver = tf.train.AdamOptimizer().minimize(D_loss, var_list=theta_D) #判别器的训练器
G_solver = tf.train.AdamOptimizer().minimize(G_loss, var_list=theta_G) #生成器的训练器

mb_size = 128 #训练的batch_size
Z_dim = 100 #生成器输入的随机噪声的列的维度

定义手写体数据集,用于识别器记录真实样本情况

sess = tf.Session() #会话层
sess.run(tf.initialize_all_variables()) #初始化所有可训练参数

def plot(samples): #保存图片时使用的plot函数
    fig = plt.figure(figsize=(4, 4)) #初始化一个4行4列包含16张子图像的图片
    gs = gridspec.GridSpec(4, 4) #调整子图的位置
    gs.update(wspace=0.05, hspace=0.05) #置子图间的间距
    for i, sample in enumerate(samples): #依次将16张子图填充进需要保存的图像
        ax = plt.subplot(gs[i])
        plt.axis('off')
        ax.set_xticklabels([])
        ax.set_yticklabels([])
        ax.set_aspect('equal')
        plt.imshow(sample.reshape(28, 28), cmap='Greys_r') 
    return fig

通过生成器生成的图片

path = '/data/User/zcc/' #保存可视化结果的路径
i = 0 #训练过程中保存的可视化结果的索引 
for it in range(1000000): #训练100万次
    if it % 1000 == 0: #每训练1000次就保存一下结果
        samples = sess.run(G_sample, feed_dict={Z: sample_Z(16, Z_dim)})
        fig = plot(samples) #通过plot函数生成可视化结果
        plt.savefig(path+'out/{}.png'.format(str(i).zfill(3)), bbox_inches='tight') #保存可视化结果
        i += 1
        
        plt.close(fig)

训练

 X_mb, _ = mnist.train.next_batch(mb_size) #得到训练一个batch所需的真实手写数字(作为判别器的输入)
#下面是得到训练一次的结果,通过sess来run出来
    _, D_loss_curr, D_loss_real, D_loss_fake, D_loss = sess.run([D_solver, D_loss, D_loss_real, D_loss_fake, 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)})
 
    if it % 1000 == 0: #每训练1000次输出一下结果
        print('Iter: {}'.format(it))
        print('D loss: {:.4}'. format(D_loss_curr))
        print('G_loss: {:.4}'.format(G_loss_curr))
        print()

入门完成!