1. 程式人生 > >適用於初學者的生成對抗網路教程

適用於初學者的生成對抗網路教程

初學者實踐生成對抗網路

根據Yann LeCun的說法,“對抗性訓練是自切片面包以來最酷的事情。“切片面包在深度學習社群肯定不會創造這麼多興奮點。自從Ian Goodfellow等人2014年首次發表生成對抗網路(GANs)以來,在短期內大幅提高了人工智慧的可能生成的內容,並得出了積極的研究成果。

GANs是神經網路學習生成與已輸入資料相似的資料。例如,研究人員已經生成了一些令人信服的影象,這些影象涵蓋了從臥室佈置到專輯封面,展示了能夠表現高階邏輯語義的非凡能力

這些例子相當複雜,但建立一個GANs生成簡單影象則很容易。在本教程中,我們將建立一個GANs,通過分析大量手寫體數字,逐漸學會從頭生成新的影象。也就是說,我們會教一個神經網路寫字技能


這裡寫圖片描述
以上是本教程中GANs用於學習和生成的樣本影象。在訓練GANs的過程中,逐漸生成其產生數字的能力。

GaN結構

GANs包括兩個模型:生成模型(generative model)和判別模型(discriminative model)。
這裡寫圖片描述

判別模型是一個分類器,用來確定給定的影象是真正的影象還是像人工創造的影象。這是一個使用卷積神經網路的二元分類。

生成模型通過反捲積神經網路將隨機的輸入值轉換成影象。

在眾多的訓練迭代過程中,生成模型和判別模型通過反向傳播演算法,訓練權重和偏置量。判別模型學習把真實的影象同生成影象區分開來。同時,生成模型通過判別模型反饋者生成更逼真的影象,使得判別模型無法分辨真實影象與生成影象。

開始

下面我們要建立一個GANs,他生成的手寫體數字,可以騙過甚至最好的分類器(當然也包括人類)。我們將谷歌開發的深度學習開源庫tensorflow,可以在GPU上快速訓練神經網路。

本教程需要你已經至少有一點點熟悉tensorflow。如果你沒有的話,我們推薦閱讀“你好,tensorflow!”或在Safari互動教程看“你好,tensorflow!”

載入MNIST資料

在判別器能夠區分真假影象前,我們需要一套手寫體數字影象集。我們將使用MNIST,它是一個深度學習基準資料集,由美國國家標準與技術研究院組織人口普查局的員工和高中生編輯的一個包含70000影象資料集。

我們先匯入tensorflow以及其他一些庫,同時,使用read_data_sets下載MNIST影象。程式碼如下:

import tensorflow as tf
import numpy as np
import datetime
import matplotlib.pyplot as plt
%matplotlib inline

from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/")

上面匯入的MNIST資料集包含影象和標籤,分為測試集(train)和驗證集(validation)(我們不需要擔心在本教程中標籤的準確性)。通過next_batch函式可以取出影象。讓我們看看它載入一個影象。
這些影象已經被格式化為一個包含784點的一維向量。我們可以使用pyplot將其還原為28 x 28畫素的影象.

ample_image = mnist.train.next_batch(1)[0]
print(sample_image.shape)

sample_image = sample_image.reshape([28, 28])
plt.imshow(sample_image, cmap='Greys')

如果你執行上面的程式碼,你會看到一個不同的Mnist集影象。

判別器網路(Discriminator network)

判別器是一個卷積神經網路,輸入為28 x 28大小的灰度影象,並返回一個標量數量描述是否輸入影象是“真實的”或“假”,也就是說是從資料集取出的影象還是自動生成的影象。
這裡寫圖片描述

判別器網路的結構類似TensorFlow的CNN分類模型,它具有兩個卷積層(使用5x5畫素特徵提取)和兩個全連線層,在全連線層上每個畫素有多個權重。

在建立每一層,使用tf.get_variable建立權重和偏置變數。權重初始化為服從正態分佈而偏置量初始化為零。

tf.nn.conv2d() 是TensorFlow的標準卷積函式,它有4個引數。第一個是輸入張量(本例中是28 x 28 x 1的影象)。下一個引數是過濾器/權重矩陣。最後,你還可以改變步幅和卷積padding 。這兩個值影響輸出量的維度。

如果你已經熟悉CNNs,你會發現這是一個簡單的二元分類器而已。

def discriminator(images, reuse=False):
    if (reuse):
        tf.get_variable_scope().reuse_variables()

    # First convolutional and pool layers
    # This finds 32 different 5 x 5 pixel features
    d_w1 = tf.get_variable('d_w1', [5, 5, 1, 32], initializer=tf.truncated_normal_initializer(stddev=0.02))
    d_b1 = tf.get_variable('d_b1', [32], initializer=tf.constant_initializer(0))
    d1 = tf.nn.conv2d(input=images, filter=d_w1, strides=[1, 1, 1, 1], padding='SAME')
    d1 = d1 + d_b1
    d1 = tf.nn.relu(d1)
    d1 = tf.nn.avg_pool(d1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')

    # Second convolutional and pool layers
    # This finds 64 different 5 x 5 pixel features
    d_w2 = tf.get_variable('d_w2', [5, 5, 32, 64], initializer=tf.truncated_normal_initializer(stddev=0.02))
    d_b2 = tf.get_variable('d_b2', [64], initializer=tf.constant_initializer(0))
    d2 = tf.nn.conv2d(input=d1, filter=d_w2, strides=[1, 1, 1, 1], padding='SAME')
    d2 = d2 + d_b2
    d2 = tf.nn.relu(d2)
    d2 = tf.nn.avg_pool(d2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')

    # First fully connected layer
    d_w3 = tf.get_variable('d_w3', [7 * 7 * 64, 1024], initializer=tf.truncated_normal_initializer(stddev=0.02))
    d_b3 = tf.get_variable('d_b3', [1024], initializer=tf.constant_initializer(0))
    d3 = tf.reshape(d2, [-1, 7 * 7 * 64])
    d3 = tf.matmul(d3, d_w3)
    d3 = d3 + d_b3
    d3 = tf.nn.relu(d3)

    # Second fully connected layer
    d_w4 = tf.get_variable('d_w4', [1024, 1], initializer=tf.truncated_normal_initializer(stddev=0.02))
    d_b4 = tf.get_variable('d_b4', [1], initializer=tf.constant_initializer(0))
    d4 = tf.matmul(d3, d_w4) + d_b4

    # d4 contains unscaled values
    return d4

這裡寫圖片描述

現在,我們定義了判別器(Discriminator),讓我們來看看在生成器模型。我們將基於Tim O’Shea.發表的生成器模型做的一個整體結構。

你可以認為生成器(generator)是一種反捲積神經網路。一個類似Discriminator典型的CNN可以將2維或者3維的畫素矩陣轉換成單一的可能輸出。然而,generator,使用d-維噪聲維向量和upsamples可以生成一個28×28畫素模式的影象,這裡使用Relu函式和batch normalization穩定各層的輸出。

在生成器網路中,使用了三個卷積層插值以生成28 x 28畫素大小的影象。(事實上,你將會看到,我們已經注意到28 x 28 x 1模式的影象;tensorflow具有很多工具用於處理單通常的灰度影象或者3通道的RGB彩色影象。)

在輸出層,我們增加加一個 tf.sigmoid()啟用函式,用於生成一個更清晰的影象。

def generator(z, batch_size, z_dim):
    g_w1 = tf.get_variable('g_w1', [z_dim, 3136], dtype=tf.float32, initializer=tf.truncated_normal_initializer(stddev=0.02))
    g_b1 = tf.get_variable('g_b1', [3136], initializer=tf.truncated_normal_initializer(stddev=0.02))
    g1 = tf.matmul(z, g_w1) + g_b1
    g1 = tf.reshape(g1, [-1, 56, 56, 1])
    g1 = tf.contrib.layers.batch_norm(g1, epsilon=1e-5, scope='bn1')
    g1 = tf.nn.relu(g1)

    # Generate 50 features
    g_w2 = tf.get_variable('g_w2', [3, 3, 1, z_dim/2], dtype=tf.float32, initializer=tf.truncated_normal_initializer(stddev=0.02))
    g_b2 = tf.get_variable('g_b2', [z_dim/2], initializer=tf.truncated_normal_initializer(stddev=0.02))
    g2 = tf.nn.conv2d(g1, g_w2, strides=[1, 2, 2, 1], padding='SAME')
    g2 = g2 + g_b2
    g2 = tf.contrib.layers.batch_norm(g2, epsilon=1e-5, scope='bn2')
    g2 = tf.nn.relu(g2)
    g2 = tf.image.resize_images(g2, [56, 56])

    # Generate 25 features
    g_w3 = tf.get_variable('g_w3', [3, 3, z_dim/2, z_dim/4], dtype=tf.float32, initializer=tf.truncated_normal_initializer(stddev=0.02))
    g_b3 = tf.get_variable('g_b3', [z_dim/4], initializer=tf.truncated_normal_initializer(stddev=0.02))
    g3 = tf.nn.conv2d(g2, g_w3, strides=[1, 2, 2, 1], padding='SAME')
    g3 = g3 + g_b3
    g3 = tf.contrib.layers.batch_norm(g3, epsilon=1e-5, scope='bn3')
    g3 = tf.nn.relu(g3)
    g3 = tf.image.resize_images(g3, [56, 56])

    # Final convolution with one output channel
    g_w4 = tf.get_variable('g_w4', [1, 1, z_dim/4, 1], dtype=tf.float32, initializer=tf.truncated_normal_initializer(stddev=0.02))
    g_b4 = tf.get_variable('g_b4', [1], initializer=tf.truncated_normal_initializer(stddev=0.02))
    g4 = tf.nn.conv2d(g3, g_w4, strides=[1, 2, 2, 1], padding='SAME')
    g4 = g4 + g_b4
    g4 = tf.sigmoid(g4)

    # Dimensions of g4: batch_size x 28 x 28 x 1
    return g4

生成一個樣本影象

現在我們已經定義了生成器和判別器(generator and discriminator )函式。讓我們看看從一個未經訓練的生成器輸出的影象是怎樣的。

首先建立tensorflow會話,在該會話中為generator 生成佔位符。佔位符的shape為None x z_dimensionsNone關鍵字標明該值可以在會話執行的時候再確定。我們通常使用None作為第一個維度,這樣在後期可以選擇不同的batch sizes。(如batch sizes是50,那麼generator 的輸入就是50 x 100)。使用None關鍵字,我們可以在後面指定batch_size

z_dimensions = 100
z_placeholder = tf.placeholder(tf.float32, [None, z_dimensions])

現在,我們建立一個變數(generated_image_output)作為generator 的輸出,同時,初始化輸入使用的隨機噪聲向量。np.random.normal()函式有三個引數。第一和第二引數為正態分佈的均值和標準差(本例中是0和1),和第三個引數為向量的shape(1 x 100)。


generated_image_output = generator(z_placeholder, 1, z_dimensions)
z_batch = np.random.normal(0, 1, [1, z_dimensions])

接下來,我們初始化所有的變數,將z_batch賦值給輸入佔位符,並且執行會話。

sess.run()函式有兩個引數。第一個就是所謂的“fetches”引數;它定義了待取回的計算值。在本例中,我們希望看到生成器的輸出是什麼。如果你回頭看看過去的程式碼,你會看到,生成器的輸出儲存在generated_image_output中,所以我們使用generated_image_output作為第一個引數。

第二個引數是字典值,該值在計算圖執行時代入。這裡就是給佔位符feed。在本例子中,將z_batch賦給先前定義的z_placeholder。如前所示,可以使用PyPlot恢復28 x 28的影象並且進行觀察。

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    generated_image = sess.run(generated_image_output,
                                feed_dict={z_placeholder: z_batch})
    generated_image = generated_image.reshape([28, 28])
    plt.imshow(generated_image, cmap='Greys')

這個影象看起來像噪音,對嗎?現在我們需要訓練generator網路的權重和偏置量,使其可以將隨機數轉為可辨識的影象。下面讓我們來看看損失函式和優化函式!

訓練GANs

建立和調整GANs最棘手的部分是該模型有兩個損失函式:一個鼓勵generator創造更好的影象;另外一個鼓勵discriminator 區分真實影象和生成的影象。

我們同時訓練generator和discriminator 。這麼做可以使得discriminator 可以更準確的區分真實影象和生成的影象,generator能夠更好地調整其權值和閥值產生令人信服的影象。

這裡是我們的網路的輸入和輸出。

tf.reset_default_graph()
batch_size = 50

z_placeholder = tf.placeholder(tf.float32, [None, z_dimensions], name='z_placeholder') 
# z_placeholder is for feeding input noise to the generator

x_placeholder = tf.placeholder(tf.float32, shape = [None,28,28,1], name='x_placeholder') 
# x_placeholder is for feeding input images to the discriminator

Gz = generator(z_placeholder, batch_size, z_dimensions) 
# Gz holds the generated images

Dx = discriminator(x_placeholder) 
# Dx will hold discriminator prediction probabilities
# for the real MNIST images

Dg = discriminator(Gz, reuse=True)
# Dg will hold discriminator prediction probabilities for generated images

所以,讓我們先想想我們希望兩個模型的輸出是什麼。discriminator 的目標是如果將 MNIST 影象給正確的標記出來就返回真(輸出值較高),如果標記錯誤了就返回假(輸出值較小)。計算discriminator 的兩個損失:將Dx1比較,可得到鑑別真實影象損失。比較Dg0可得到鑑別生成影象損失。
使用tf.nn.sigmoid_cross_entropy_with_logits() 函式計算Dx和1與Dg和0之間的交叉熵損失。

使用sigmoid_cross_entropy_with_logits處理標定值而不是從0到1的概率值。注意discriminator 的最後一行:那裡沒有softmax 或者sigmoid層,如果discriminators 過飽和或者或者在鑑別生成的影象的時候返回0,那麼GAN會失效;這使得鑑別不下有用的梯度。

tf.reduce_mean()函式計算交叉熵函式返回的矩陣所有元素的平均值。這種方法可以減少某一維度的損失,而不是整個向量或矩陣的損失。

d_loss_real = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(Dx, tf.ones_like(Dx)))
d_loss_fake = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(Dg, tf.zeros_like(Dg)))

現在讓我們建立generator的損失函式。我們希望generator網路創造的影象,可以騙過discriminators 。generator要discriminators 的在判別生成的影象的時候輸出值接近1。因此,我們要計算Dg和1的損失。

g_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(Dg, tf.ones_like(Dg)))

現在,我們有了損失函式,下一步定義優化函式。對於generator網路優化函式只需要更新generator的權重,不更新discriminators的權重 。同樣,當我們訓練discriminators,我們要保持generator的權重固定。

為了區別這種操作,我們需要建立兩個變數列表,一個discriminators的權重和偏置項,和另一個是generator的權重和偏置項。這裡有一種“貼心”的方案可以命名所有的tensorflow變數。

vars = 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]

print([v.name for v in d_vars])
print([v.name for v in g_vars])

接下來,我們指定我們的優化器(optimizers)。GANs通常選用Adam演算法作為優化演算法;採用自適應學習率和momentum。在訓練generator和discriminator時,我們呼叫把Adam演算法最小化功能,並且宣告用於更新generator和discriminator的權重和偏置量的變數。

在discriminator中,我們建立了兩個不同的訓練操作:一是訓練鑑別真實影象和一個訓練鑑別假影象。對這兩個訓練操作來說,使用不同的學習率有時候是有用的,或用它們分別使用不同的方法調節學習。(不通順,大家將就看吧)

# Train the discriminator
d_trainer_fake = tf.train.AdamOptimizer(0.0003).minimize(d_loss_fake, var_list=d_vars)
d_trainer_real = tf.train.AdamOptimizer(0.0003).minimize(d_loss_real, var_list=d_vars)

# Train the generator
g_trainer = tf.train.AdamOptimizer(0.0001).minimize(g_loss, var_list=g_vars)

這個演算法很難讓GANs收斂,而且往往需要很長時間的訓練。這個時候使用視覺化tensorboard跟蹤訓練過程很有用,它可以圖形化展示標量如損失函式,顯示樣本影象訓練時候的樣本影象,或者說明神經網路的拓撲結構。

如果你在自己的機器上執行包括下面的單元在內的指令碼。方法如下:在一個終端視窗,中運行tensorboard -- logdir = tensorboard /,然後在瀏覽器中訪問網址:http://localhost:6006即可開啟TensorBoard.

tf.summary.scalar('Generator_loss', g_loss)
tf.summary.scalar('Discriminator_loss_real', d_loss_real)
tf.summary.scalar('Discriminator_loss_fake', d_loss_fake)

images_for_tensorboard = generator(z_placeholder, batch_size, z_dimensions)
tf.summary.image('Generated_images', images_for_tensorboard, 5)
merged = tf.summary.merge_all()
logdir = "tensorboard/" + datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + "/"
writer = tf.summary.FileWriter(logdir, sess.graph)

現在我們開始迭代計算。首先我們簡單的給discriminator一些初始訓練,幫助它生成對generator有用的梯度。

然後我們繼續訓練迴圈。當我們訓練generator時,feed一個隨機向量Z給generator,並將輸出傳遞給discriminator(即之前提到過的Dg)。generator的權重和偏置項將不斷被更新,其產生影象可以達到以假亂真的效果(discriminator區分不開)。

為訓練discriminator,feed 給discriminatorMNIST資料集的一批影象作為正例(positive examples),然後與feed生成的影象得到的結果做對比。記住,如果generator提高了輸出影象質量,discriminator將持續認為生成的高質量影象為假。

因為它需要很長的時間來訓練GANs,初次學習本教程,我們建議不要執行這個程式碼塊。為繼續學習本教程,接下來的程式碼會載入一個預先訓練好的模型。

如果你自己想執行此程式碼,在快速的GPU上大概需3個小時,在桌面CPU上可能需要十倍的時間。

sess = tf.Session()
sess.run(tf.global_variables_initializer())

# Pre-train discriminator
for i in range(300):
    z_batch = np.random.normal(0, 1, size=[batch_size, z_dimensions])
    real_image_batch = mnist.train.next_batch(batch_size)[0].reshape([batch_size, 28, 28, 1])
    _, __, dLossReal, dLossFake = sess.run([d_trainer_real, d_trainer_fake, d_loss_real, d_loss_fake],
                                           {x_placeholder: real_image_batch, z_placeholder: z_batch})

    if(i % 100 == 0):
        print("dLossReal:", dLossReal, "dLossFake:", dLossFake)

# Train generator and discriminator together
for i in range(100000):
    real_image_batch = mnist.train.next_batch(batch_size)[0].reshape([batch_size, 28, 28, 1])
    z_batch = np.random.normal(0, 1, size=[batch_size, z_dimensions])

    # Train discriminator on both real and fake images
    _, __, dLossReal, dLossFake = sess.run([d_trainer_real, d_trainer_fake, d_loss_real, d_loss_fake],
                                           {x_placeholder: real_image_batch, z_placeholder: z_batch})

    # Train generator
    z_batch = np.random.normal(0, 1, size=[batch_size, z_dimensions])
    _ = sess.run(g_trainer, feed_dict={z_placeholder: z_batch})

    if i % 10 == 0:
        # Update TensorBoard with summary statistics
        z_batch = np.random.normal(0, 1, size=[batch_size, z_dimensions])
        summary = sess.run(merged, {z_placeholder: z_batch, x_placeholder: real_image_batch})
        writer.add_summary(summary, i)

    if i % 100 == 0:
        # Every 100 iterations, show a generated image
        print("Iteration:", i, "at", datetime.datetime.now())
        z_batch = np.random.normal(0, 1, size=[1, z_dimensions])
        generated_images = generator(z_placeholder, 1, z_dimensions)
        images = sess.run(generated_images, {z_placeholder: z_batch})
        plt.imshow(images[0].reshape([28, 28]), cmap='Greys')
        plt.show()

        # Show discriminator's estimate
        im = images[0].reshape([1, 28, 28, 1])
        result = discriminator(x_placeholder)
        estimate = sess.run(result, {x_placeholder: im})
        print("Estimate:", estimate)

因為以上程式碼會花很長的時間來訓練GANs,我們建議你跳過以上程式碼執行下面的程式碼塊。這裡會載入一個在GPU上花了好幾個小時訓練好了的模型,並讓您體驗一個訓練好了的GAN的輸出。

saver = tf.train.Saver()
with tf.Session() as sess:
    saver.restore(sess, 'pretrained-model/pretrained_gan.ckpt')
    z_batch = np.random.normal(0, 1, size=[10, z_dimensions])
    z_placeholder = tf.placeholder(tf.float32, [None, z_dimensions], name='z_placeholder') 
    generated_images = generator(z_placeholder, 10, z_dimensions)
    images = sess.run(generated_images, {z_placeholder: z_batch})
    for i in range(10):
        plt.imshow(images[i].reshape([28, 28]), cmap='Greys')
        plt.show()

訓練中的困難

GANs太難訓練了。缺乏正確的超引數、網路結構,在訓練過程中,discriminator可以壓倒generator,反之亦然。

一個常見的失效模式(discriminator壓倒了generator),100%能判斷出生成的影象是假的。當discriminator做出100%肯定的時候,它導致沒有generator沒有可下降的梯度(過擬合了)。這是為什麼discriminator直接產生unscaled的輸出而不是將輸出通過一個Sigmoid函式後,將取值範圍定義在0和1之間。

另一個常見的失效模式,稱為模式崩潰(mode collapse),這種情況下generator發現並利用了discriminator的弱點。如果generator無視輸入Z的變化產生的影象非常相似就可以看做是”模式崩潰”。模式崩潰有時可以通過在某一方面“加強”discriminator得到校正,例如通過調整其訓練速度或調整訓練層數。

研究人員已經確定了一些對於建立穩定的GANs有幫助的”GAN hacks”。

最後的想法

Gans具有重塑我們每天進行互動的數字世界的巨大潛力。這個領域還很年輕,和下一個關於GANs偉大的發現可能是你做出的!

其他資源

1:Ian Goodfellow和他的合作者發表於2014年的關於GAN的原始論文
2:Goodfellow最近的關於GAN的教程,某種程度上更接地氣的解釋了GAN。
3:Alec Radford、Luke Metz和Soumith Chintala的論文,介紹深層卷積GAN(DCGAN),其基本結構在我們的本教程的generator就是其基本結構。DCGAN程式碼在GitHub上
這篇文章O’Reilly和tensorflow合作的部分成果,詳見文章獨立性宣告。