Generative Adversarial Nets in TensorFlow
生成對抗網絡(簡稱GAN)是一種非常流行的神經網絡。它由Ian Goodfellow等人在2014年NIPS論文中首次引入。這篇論文引發了對神經網絡對抗訓練的研究熱潮。突然之間,GAN的許多改進都出現了:DCGAN,Sequence-GAN,LSTM-GAN等等。在2016年NIPS會議上,甚至有一整個專門針對GAN的研討會!
請注意,該代碼可在https://github.com/wiseodd/generative-models中找到。
首先,讓我們回顧一下這篇論文的要點。之後,我們將一如既往地嘗試使用TensorFlow和MNIST數據來實現GAN。
Generative Adversarial Nets
讓我們舉一個“造假幣的罪犯”和“警察”之間的玫瑰關係的例子。在假幣方面,罪犯的目標是什麼?警察的目標是什麼?我們列舉一下:
- 爲了成爲一個成功的僞造犯,罪犯想欺騙警察,這樣警察就不能分辨出假幣與真錢的區別
- 爲了成爲正義的典範,警察希望儘可能地發現假幣
在那裏,我們看到產生了衝突。博弈論中的這種情況可以模擬爲一個極小化極大遊戲(minimax game)。這個過程被稱爲對抗過程。
生成對抗網(GAN)是對抗過程的一個特例,其組成部分(警察和罪犯)是神經網絡。第一個網絡生成數據,第二個網絡試圖分辨真實數據和第一個網絡生成的假數據之間的差異。第二個網絡將輸出[0, 1],表示實際數據概率的標量。
在GAN中,第一個網絡被稱爲發生器網絡G(Z)而第二個網絡叫做辨別器網絡D(X)。
在最小極大值的平衡點上,第一個網絡將產生近似真實的數據,第二個網絡將輸出0.5的概率,作爲第一個網絡產生的數據=真實數據的輸出。
“爲什麼我們對GAN的感興趣?這是因爲數據Pdata的概率分佈可能是一個非常複雜的分佈,非常難以推斷。因此,有一個“生成器”可以直接生成樣本Pd而不必處理討厭的概率分佈是非常好的。如果我們有這個,那麼我們就可以將它用於另一個需要從Pdata中抽樣的過程,因爲我們可以使用訓練有素的“生成器”相對便宜地獲取樣本。
GAN Implementation
根據GAN的定義,我們需要兩個網絡。(不管是像convnet一樣複雜的網絡還是隻是一個兩層的神經網絡),先讓我們變得簡單,使用兩層的網絡。我們將使用TensorFlow來達到這個目的。
# Discriminator Net
X = tf.placeholder(tf.float32, shape=[None, 784], name='X')
D_W1 = tf.Variable(xavier_init([784, 128]), name='D_W1')
D_b1 = tf.Variable(tf.zeros(shape=[128]), name='D_b1')
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]
# Generator Net
Z = tf.placeholder(tf.float32, shape=[None, 100], name='Z')
G_W1 = tf.Variable(xavier_init([100, 128]), name='G_W1')
G_b1 = tf.Variable(tf.zeros(shape=[128]), name='G_b1')
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]
def generator(z):
G_h1 = tf.nn.relu(tf.matmul(z, G_W1) + G_b1)
G_log_prob = tf.matmul(G_h1, G_W2) + G_b2
G_prob = tf.nn.sigmoid(G_log_prob)
return G_prob
def discriminator(x):
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)
return D_prob, D_logit
在上面的代碼中,generator(z)輸入100維矢量並返回786維矢量,即MNIST圖像(28x28)。z表示G(Z)。從某種意義上來說,它學習了先驗空間與Pdata之間的映射關係。
discriminator(x)輸入MNIST圖像,並返回表示真實MNIST圖像的概率的標量。
現在,我們定義訓練這個GAN的“對抗進程”。以下是論文中的訓練算法:
G_sample = generator(Z)
D_real, D_logit_real = discriminator(X)
D_fake, D_logit_fake = discriminator(G_sample)
D_loss = -tf.reduce_mean(tf.log(D_real) + tf.log(1. - D_fake))
G_loss = -tf.reduce_mean(tf.log(D_fake))
在上面,我們加了負號作爲損失函數。因爲它們需要最大化,而TensorFlow的優化器只能做到最小化。
另外,根據論文的建議,在上述算法中,最好是最大化tf.reduce_mean(tf.log(D_fake))而不是最小化tf.reduce_mean(1 - tf.log(D_fake))。
然後,我們用以上那些損失函數來一個接一個地訓練網絡。
# Only update D(X)'s parameters, so var_list = theta_D
D_solver = tf.train.AdamOptimizer().minimize(D_loss, var_list=theta_D)
# Only update G(X)'s parameters, so var_list = theta_G
G_solver = tf.train.AdamOptimizer().minimize(G_loss, var_list=theta_G)
def sample_Z(m, n):
'''Uniform prior for G(Z)'''
return np.random.uniform(-1., 1., size=[m, n])
for it in range(1000000):
X_mb, _ = mnist.train.next_batch(mb_size)
_, D_loss_curr = sess.run([D_solver, 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)})
我們完成了!我們可以通過每隔一段時間地抽樣G(Z)來看訓練過程
我們從隨機噪聲開始,隨着訓練的進行,G(Z)開始越來越接近Pdata,這表明G(Z)生成的樣本與MNIST數據越來越相似。
Alternative Loss Formulation
我們還可以使用另一種方法定義損失函數D_loss和G_loss。
這是受到Brandon Amos博客中關於圖像補全的文章的啓發。
http://bamos.github.io/2016/08/09/deep-completion/
對於鑑別器discriminator():
根據定義,希望discriminator(X)使所有輸出爲1,因爲我們希望輸入實際數據後,最大化模型輸出的概率。
根據定義,希望discriminator(G_sample)使所有的輸出爲0,因爲我們希望輸入虛假數據後,最小化模型輸出的概率。
那麼generator(Z)呢?它想要最大化假數據的概率!這是恰恰是D(G(Z))的相反目標!
因此,我們可以按照以下方式來制定損失。
# Alternative losses:
# -------------------
D_loss_real = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(D_logit_real, tf.ones_like(D_logit_real)))
D_loss_fake = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(D_logit_fake, tf.zeros_like(D_logit_fake)))
D_loss = D_loss_real + D_loss_fake
G_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(D_logit_fake, tf.ones_like(D_logit_fake)))
根據上述的想法,我們使用Logistic Loss。改變損失函數不會影響我們訓練GAN,因爲這只是思考問題的另一種方式。
Conclusion
在這篇文章中,我們研究了由Ian Goodfellow等人發表的“生成對抗網絡”(GAN)。我們探討了對抗過程的表述以及背後的直覺。
接下來,我們爲生成器和鑑別器實現了GAN的2層神經網絡。然後,我們遵循Goodfellow等人在2014年提出的算法來訓練GAN。
最後,我們展示了另一種思考GAN損失函數的方法。我們用直覺思考這兩個網絡,並使用Logistic Loss作爲損失函數。
完整的代碼,請訪問https://github.com/wiseodd/generative-models!
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import os
# xavier初始化
# 如果使用small random numbers,深層網絡由於反覆累乘會出現All activations become zero
# 如果初始值稍微大一點,會幾乎所有的值集中在-1或1附近,神經元saturated
# 注意到tanh在-1和1附近的gradient都接近0,這同樣導致了gradient太小,參數難以被更新。
def xavier_init(size):
in_dim = size[0]
xavier_stddev = 1. / tf.sqrt(in_dim / 2.)
return tf.random_normal(shape=size, stddev=xavier_stddev)
# MNIST數據爲28 x 28的灰度圖
X = tf.placeholder(tf.float32, shape=[None, 784])
# discriminator的參數
# 第一層網絡128個神經元
D_W1 = tf.Variable(xavier_init([784, 128]))
D_b1 = tf.Variable(tf.zeros(shape=[128]))
D_W2 = tf.Variable(xavier_init([128, 1]))
D_b2 = tf.Variable(tf.zeros(shape=[1]))
theta_D = [D_W1, D_W2, D_b1, D_b2]
# Z爲generator最初的100個隨機數
Z = tf.placeholder(tf.float32, shape=[None, 100])
# generator的參數
# 第一層網絡128個神經元
G_W1 = tf.Variable(xavier_init([100, 128]))
G_b1 = tf.Variable(tf.zeros(shape=[128]))
G_W2 = tf.Variable(xavier_init([128, 784]))
G_b2 = tf.Variable(tf.zeros(shape=[784]))
theta_G = [G_W1, G_W2, G_b1, G_b2]
# 對generator生成[-1,1]均勻分佈的隨機數
def sample_Z(m, n):
return np.random.uniform(-1., 1., size=[m, n])
# generator網絡
def generator(z):
G_h1 = tf.nn.relu(tf.matmul(z, G_W1) + G_b1)
G_log_prob = tf.matmul(G_h1, G_W2) + G_b2
G_prob = tf.nn.sigmoid(G_log_prob)
return G_prob
# discriminator網絡
def discriminator(x):
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)
return D_prob, D_logit
# 繪製“生成樣本”的圖
def plot(samples):
fig = plt.figure(figsize=(4, 4))
gs = gridspec.GridSpec(4, 4)
gs.update(wspace=0.05, hspace=0.05)
for i, sample in enumerate(samples):
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
G_sample = generator(Z)
D_real, D_logit_real = discriminator(X)
D_fake, D_logit_fake = discriminator(G_sample)
# D_loss = -tf.reduce_mean(tf.log(D_real) + tf.log(1. - D_fake))
# G_loss = -tf.reduce_mean(tf.log(D_fake))
# Alternative losses:
# -------------------
# discriminator希望把所有real預測爲1,把所有fake預測爲0
# generator希望把所有fake預測爲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)))
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
G_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logit_fake, labels=tf.ones_like(D_logit_fake)))
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
Z_dim = 100
mnist = input_data.read_data_sets('./MNIST_data', one_hot=True)
sess = tf.Session()
sess.run(tf.global_variables_initializer())
if not os.path.exists('out/'):
os.makedirs('out/')
i = 0
for it in range(1000000):
#每1000步輸出一次
if it % 1000 == 0:
#使用當前的generator生成圖片
samples = sess.run(G_sample, feed_dict={Z: sample_Z(16, Z_dim)})
fig = plot(samples)
plt.savefig('out/{}.png'.format(str(i).zfill(3)), bbox_inches='tight')
i += 1
plt.close(fig)
#取出mb_size個的樣本和標籤
X_mb, _ = mnist.train.next_batch(mb_size)
_, D_loss_curr = sess.run([D_solver, 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:
print('Iter: {}'.format(it))
print('D loss: {:.4}'. format(D_loss_curr))
print('G_loss: {:.4}'.format(G_loss_curr))
print()