引言中已經較為詳細的介紹了GAN的理論基礎和模型本身的原理。這里主要是研讀Goodfellow的第一篇GAN論文。
0. 對抗網絡
如引言中所述,對抗網絡其實就是一個零和游戲中的2人最小最大游戲,主要就是為了處理下面的函數\(V(G,D)\):
在實現過程中,如果將D和G都寫入同一個循環中,即迭代一次D,迭代一次G,這種情況會讓在有限的數據集基礎上會導致過擬合。所以Goodfellow推薦:先訓練D模型K步,然后再訓練G一步。這樣可以讓D很好的接近最優解,並且讓G改變的足夠慢。
圖0.1 GAN的訓練流程偽代碼
1. 理論結果
1.1 \(p_g=p_{data}\)的全局優化
首先,我們討論下基於任何給定的生成器G的基礎上,最優的判決器D。
[待續]
1.2 算法的收斂
[待續]
2. 實驗結果
[待續]
3. 優缺點
[待續]
4.示例代碼解析
此部分主要參考自github。這里主要涉及到4個點:
- 1 - 讀取mnist的數據;
- 2 - 構建一個判別器網絡;
- 3 - 構建一個生成器網絡;
- 4 - 基於SGD,采用聯合更新的方式來訓練這兩個網絡從而完成生成對抗網絡的訓練。
4.1 載入前置模塊及mnist數據
import tensorflow as tf
import random
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/")#手動下載mnist的4個gz壓縮包,放入當前路徑的MNIST_data文件夾下面
4.2 構建判別器網絡
基於CNN網絡結構,構建一個判別器,用於輸出當前輸入的圖片為real data的概率值。
#先定義卷積和平均池化的函數,這2個就是常見的CNN的卷積和池化操作
def conv2d(x, W):
#input:[batch, in_height, in_width, in_channels]
#filter:[filter_height, filter_width, in_channels, out_channels]
return tf.nn.conv2d(input=x, filter=W, strides=[1, 1, 1, 1], padding='SAME')
def avg_pool_2x2(x):
return tf.nn.avg_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
因卷積和池化操作會多次用到,所以先建立成函數,下面構建判別器網絡結構
def discriminator(x_image, reuse=False):
with tf.variable_scope('discriminator') as scope:
if reuse:
tf.get_variable_scope().reuse_variables()
'''第一層:卷積層和池化層,該層的激活函數為ReLU'''
#結構為:conv->ReLU->avgPool
#卷積層感受野大小5x5,輸入channel(或者叫做depth)為1,輸出channel為8; 輸出的feature map為14*14*8
W_conv1 = tf.get_variable('d_wconv1', [5, 5, 1, 8], initializer=tf.truncated_normal_initializer(stddev=0.02))
b_conv1 = tf.get_variable('d_bconv1', [8], initializer=tf.constant_initializer(0))
h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
h_pool1 = avg_pool_2x2(h_conv1)
'''第二層:卷積層和池化層,其他如第一層所述; '''
#輸出的feature map為7*7*16
W_conv2 = tf.get_variable('d_wconv2', [5, 5, 8, 16], initializer=tf.truncated_normal_initializer(stddev=0.02))
b_conv2 = tf.get_variable('d_bconv2', [16], initializer=tf.constant_initializer(0))
h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = avg_pool_2x2(h_conv2)
'''第三層:一個全連接層,輸入維度7*7*16,輸出維度32'''
W_fc1 = tf.get_variable('d_wfc1', [7 * 7 * 16, 32], initializer=tf.truncated_normal_initializer(stddev=0.02))
b_fc1 = tf.get_variable('d_bfc1', [32], initializer=tf.constant_initializer(0))
h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*16])
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)
'''第四層:一個全連接層,輸入維度32,輸出維度1,用於判別當前輸入圖片屬於real data的概率,此處無激活函數'''
W_fc2 = tf.get_variable('d_wfc2', [32, 1], initializer=tf.truncated_normal_initializer(stddev=0.02))
b_fc2 = tf.get_variable('d_bfc2', [1], initializer=tf.constant_initializer(0))
y_conv=(tf.matmul(h_fc1, W_fc2) + b_fc2)
return y_conv
4.3 構建生成器網絡
CNN可以被看成是輸入一個2維矩陣或者3維的張量,輸出一個單一的概率值;而生成器,就是輸入一個d維的噪音向量,上采樣成一個2維的矩陣或者3維的張量。
def generator(z, batch_size, z_dim, reuse=False):
#z:輸入的噪音向量
with tf.variable_scope('generator') as scope:
if reuse:
tf.get_variable_scope().reuse_variables()
g_dim = 64 #生成器第一層的channel個數
c_dim = 1 #輸出的顏色空間維度 (MNIST 是灰度圖片,所以 c_dim = 1)
s = 28 #圖片的輸出尺寸
s2, s4, s8, s16 = int(s/2), int(s/4), int(s/8), int(s/16) #為了緩慢的上采樣,變化盡可能的小。分別為14,7,3,2
'''輸入z是基於隨機采樣生成的,即噪音輸入'''
#h0 的維度:[ batch_size, 2, 2, 25],所以z的維度為[batch_size, 100]
h0 = tf.reshape(z, [batch_size, s16+1, s16+1, 25])
h0 = tf.nn.relu(h0)
'''第一個解卷積層,采用conv2d_transpose實現'''
#先定義權重和偏置,
#H_conv1的維度:[batch_size, 3, 3, 256]
output1_shape = [batch_size, s8, s8, g_dim*4]#[batch_size,3,3,64]
W_conv1 = tf.get_variable('g_wconv1', [5, 5, output1_shape[-1], int(h0.get_shape()[-1])],
initializer=tf.truncated_normal_initializer(stddev=0.1))
#b_conv1 = tf.get_variable('g_bconv1', [output1_shape[-1]], initializer=tf.constant_initializer(.1))
#采用conv2d_transpose實現解卷積,並加上BN,ReLU
#conv2d_transpose:
# 參數1 input(h0) - [batch, height, width, in_channels]或者batch, in_channels, height, width]
# 參數2 filter(W_conv1) - [height, width, output_channels, in_channels]
H_conv1 = tf.nn.conv2d_transpose(h0, W_conv1, output_shape=output1_shape, strides=[1, 2, 2, 1], padding='SAME')
#H_conv1 = tf.reshape(tf.nn.bias_add(H_conv1, b_conv1), H_conv1.get_shape())
H_conv1 = tf.contrib.layers.batch_norm(inputs = H_conv1, center=True, scale=True, is_training=True, scope="g_bn1")
H_conv1 = tf.nn.relu(H_conv1)
'''第二個解卷積層'''
#H_conv2的維度:[batch_size, 6, 6, 128]
output2_shape = [batch_size, s4 - 1, s4 - 1, g_dim*2]
W_conv2 = tf.get_variable('g_wconv2', [5, 5, output2_shape[-1], int(H_conv1.get_shape()[-1])],
initializer=tf.truncated_normal_initializer(stddev=0.1))
#b_conv2 = tf.get_variable('g_bconv2', [output2_shape[-1]], initializer=tf.constant_initializer(.1))
H_conv2 = tf.nn.conv2d_transpose(H_conv1, W_conv2, output_shape=output2_shape, strides=[1, 2, 2, 1], padding='SAME')
#H_conv2 = tf.reshape(tf.nn.bias_add(H_conv2, b_conv2), H_conv2.get_shape())
H_conv2 = tf.contrib.layers.batch_norm(inputs = H_conv2, center=True, scale=True, is_training=True, scope="g_bn2")
H_conv2 = tf.nn.relu(H_conv2)
'''第三個解卷積層'''
#H_conv3的維度:[batch_size, 12, 12, 64]
output3_shape = [batch_size, s2 - 2, s2 - 2, g_dim*1]
W_conv3 = tf.get_variable('g_wconv3', [5, 5, output3_shape[-1], int(H_conv2.get_shape()[-1])],
initializer=tf.truncated_normal_initializer(stddev=0.1))
#b_conv3 = tf.get_variable('g_bconv3', [output3_shape[-1]], initializer=tf.constant_initializer(.1))
H_conv3 = tf.nn.conv2d_transpose(H_conv2, W_conv3, output_shape=output3_shape, strides=[1, 2, 2, 1], padding='SAME')
#H_conv3 = tf.reshape(tf.nn.bias_add(H_conv3, b_conv3 ), H_conv3.get_shape())
H_conv3 = tf.contrib.layers.batch_norm(inputs = H_conv3, center=True, scale=True, is_training=True, scope="g_bn3")
H_conv3 = tf.nn.relu(H_conv3)
'''第四個解卷積層'''
#H_conv4的維度:[batch_size, 28, 28, 1]
output4_shape = [batch_size, s, s, c_dim]
W_conv4 = tf.get_variable('g_wconv4', [5, 5, output4_shape[-1], int(H_conv3.get_shape()[-1])],
initializer=tf.truncated_normal_initializer(stddev=0.1))
#b_conv4 = tf.get_variable('g_bconv4', [output4_shape[-1]], initializer=tf.constant_initializer(.1))
H_conv4 = tf.nn.conv2d_transpose(H_conv3, W_conv4, output_shape=output4_shape, strides=[1, 2, 2, 1], padding='VALID')
#H_conv4 = tf.reshape(tf.nn.bias_add(H_conv4, b_conv4), H_conv4.get_shape())
H_conv4 = tf.nn.tanh(H_conv4)
return H_conv4
4.4 聯合訓練
聯合訓練判別器和生成器
batch_size = 16
z_dimensions = 100
tf.reset_default_graph() #
sess = tf.Session()
'''設定real data和噪音的占位數據 '''
x_placeholder = tf.placeholder("float", shape = [None,28,28,1]) #real data的輸入
z_placeholder = tf.placeholder(tf.float32, [None, z_dimensions]) #輸入到生成器中的噪音
'''基於編寫好的判別器和生成器建立關系 '''
Dx = discriminator(x_placeholder) #判別器,對real data的判別概率(unnormalized)
Gz = generator(z_placeholder, batch_size, z_dimensions) #生成器,基於噪音數據,生成偽造數據
Dg = discriminator(Gz, reuse=True) #判別器,對生成器生成的偽造圖片的判別概率 (unnormalized)
'''生成器的loss定義 '''
#'''對偽造圖片判別結果的loss值: <判別結果, 期望其為1>之間的交叉熵值 '''
g_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=Dg, labels=tf.ones_like(Dg)))
'''判別器的loss定義 '''
#'''對真實圖片判別結果的loss值: <判別結果,本身為1>之間的交叉熵值 '''
d_loss_real = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=Dx, labels=tf.ones_like(Dx)))
#'''對偽造圖片判別結果loss值: <判別結果,本身為0>之間的交叉熵值 '''
d_loss_fake = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=Dg, labels=tf.zeros_like(Dg)))
#上述兩個loss值相加
d_loss = d_loss_real + d_loss_fake
'''從graph中提取所有可以訓練的變量,並區分出判別器的變量和生成器的變量 '''
tvars = tf.trainable_variables()
d_vars = [var for var in tvars if 'd_' in var.name]
g_vars = [var for var in tvars if 'g_' in var.name]
'''調用SGD進行判別器和生成器loss的迭代訓練 '''
with tf.variable_scope(tf.get_variable_scope(), reuse=False):
trainerD = tf.train.AdamOptimizer().minimize(d_loss, var_list=d_vars)
trainerG = tf.train.AdamOptimizer().minimize(g_loss, var_list=g_vars)
sess.run(tf.global_variables_initializer())
iterations = 3000
for i in range(iterations):
'''生成噪音數據和讀取真實數據 '''
z_batch = np.random.normal(-1, 1, size=[batch_size, z_dimensions])#生成噪音數據
real_image_batch = mnist.train.next_batch(batch_size)#提取真實圖片的minibatch並進行reshape
real_image_batch = np.reshape(real_image_batch[0],[batch_size,28,28,1])
'''訓練判別器,生成器 '''
_,dLoss = sess.run([trainerD, d_loss],feed_dict={z_placeholder:z_batch,x_placeholder:real_image_batch}) #判別器
_,gLoss = sess.run([trainerG,g_loss],feed_dict={z_placeholder:z_batch}) #生成器
'''訓練結束之后,利用訓練好的生成器,生成圖片 '''
sample_image = generator(z_placeholder, 1, z_dimensions, reuse=True)
z_batch = np.random.normal(-1, 1, size=[1, z_dimensions])
temp = (sess.run(sample_image, feed_dict={z_placeholder: z_batch}))
my_i = temp.squeeze()
plt.imshow(my_i, cmap='gray_r')
生成圖片
PS:GAN是很難訓練的,其需要【正確的超參數,網絡結構,訓練流程】,否則會有很大幾率生成器或者判別器會超過另一個。比如:
- 生成器找到了判別器的一個漏洞,從而重復的輸出可以欺騙判別器的圖片,但是圖片本身卻並不具有可視性(比如對抗樣本);
- 生成器陷入單點上,因而無法輸出多樣化的數據,即總是輸出同一類同一張圖片;
- 判別器太厲害了,以至於怎么訓練都被區分出真假。(一個方法是生成器學習率大於判別器,不過不一定有效)
這些現象背后的直觀數學解釋是:在實際實現GAN中,是采用梯度下降的方式尋找cost函數的最小值,而不是真的實現零和游戲的納什平衡。當真的采用納什平衡時,這些算法都是無法收斂的。所以需要研究出穩定的優化算法從而能夠如訓練CNN一樣訓練GAN。
在原始論文中,生成器和判別器的loss如下:
log = lambda x: tf.log(x + 1e-7)
'''生成器的loss:最大化log(D(G(z)))'''
g_loss = -tf.reduce_mean(log(Dg))
'''判別器的loss:最大化log(D(x)) + log(1 - D(G(z)))'''
d_loss = -tf.reduce_mean(log(Dx) + log(1. - Dg))
如果采用上述原文loss,記得在def discriminator(x_image, reuse=False)的輸出部分加上:
y_conv = tf.nn.sigmoid(y_conv)
參考資料:
[] - .tutorial