TensorFlow從1到2(十二)生成對抗網絡GAN和圖片自動生成


生成對抗網絡的概念

上一篇中介紹的VAE自動編碼器具備了一定程度的創造特征,能夠“無中生有”的由一組隨機數向量生成手寫字符的圖片。
這個“創造能力”我們在模型中分為編碼器和解碼器兩個部分。其能力來源實際上是大量樣本經過學習編碼后,在數字層面對編碼結果進行微調,再解碼生成圖片的過程。所生成的圖片,是對原樣本圖的某種變形模仿。

今天的要介紹的生成對抗網絡(GAN)也具備很類似的功能,所建立的模型,能夠生成非常接近樣本圖片的結果。
相對於VAE,生成對抗網絡GAN更接近一種思想,並非針對機器視覺領域,而是一種很通用的機器學習理念。

讓我們用一個例子來理解生成對抗網絡:
比如我們想學習英語朗讀。一開始,我們的朗讀能力肯定很差,每次考試都是不及格。這時候,我們會努力的學習。當然人的學習是通過各種可能手段,聽錄音、看視頻、找外教。學習一段時間后,再去參加考試,如果成績依然很差,我們回來繼續學習。一直到我們得到了一個自己滿意的成績。
這個例子中有幾個重要的因素:學習者本人就是機器學習中的神經網絡,負責生成某個結果,比如朗讀;考官負責判斷我們朗讀的英語是否達到了水平要求。考官實際也是一個網絡模型,本身並不能知道什么樣的朗讀叫好,什么樣的朗讀叫差,其判斷依據來自於對“好的朗讀”樣本的學習;學習者不斷學習提高的過程,這個就相當於網絡模型不斷的訓練迭代。

回到我們的圖片生成過程。圖片生成是一個模型,負責生成所需要的圖片;

(圖片來自官方文檔)
“考官”負責檢查樣本和生成圖。這里有一個區別於VAE模型的重點,VAE是直接比較樣本和生成圖,以兩者的差距作為代價。
而GAN中,考官本身的學習,自動為樣本圖添加標注1,為生成圖添加標注0。完成學習后,如果生成的圖片,考官會判斷為真實樣本,說明所生成的圖片達到了應有的水准。

(圖片來自官方文檔)
這樣的機器學習方式,可以不使用經過標注的樣本數據,能夠大量節省成本。雖然會帶來學習過程的加長和大量算力需求,但通常來說,算力還是更容易獲得的。
另一個角度上說,VAE直接比較樣本圖片和生成圖片,大量的數據和復雜性,導致VAE的損失函數的代碼量大,計算速度也慢。GAN只有真、偽兩個判斷結果,模型輸出簡單,代價函數也容易的多。所以在同一組數據上,使用VAE算法往往會比GAN略慢一些。
看起來如果只是生成圖片這一個維度的結果,GAN似乎更有優勢,但如果考慮到輸出結果的可控性等因素,VAE在機器視覺領域的應用仍然是很廣泛。
不過GAN的思想是比較判斷結果而非原圖,是“裁判”,所以這種思想很容易推廣到多個應用領域,而不僅僅是機器視覺范疇。

GAN實例

本篇我們嘗試使用時尚單品的樣本庫作為訓練數據,最終讓模型可以由隨機的種子向量,生成時尚單品的圖片。
我們前面已經做過介紹,時尚單品的樣本也是28x28單色圖片,同MNIST手寫數字樣本是完全相同的格式。因此換用手寫數字的圖片樣本,只要把載入樣本數據的部分替換掉就可以,其它代碼無需修改。

樣本數據載入的部分:

(train_images, _), (_, _) = keras.datasets.fashion_mnist.load_data()

我們實際只需要了訓練的數據集,測試集和兩個數據集的標注我們都直接拋棄了。GAN是典型的非監督學習,並不需要標注。

源碼中方法make_generator_model用來建立圖片生成模型;make_discriminator_model方法用來建立辨別模型,辨別模型也就是我們剛才說的“考官”。
兩個模型都使用keras.Sequential幫助建立,結構並不復雜。
模型的學習一定要關注輸入和輸出,中間的部分如果沒有理論基礎,反而可以並不是很在意。因為算法的研究會關注模型,軟件開發工程師更關心使用。

生成網絡輸入隨機數種子向量序列,輸出是28x28x1的圖片序列。一次調用可以生成多幅圖片。
辨別模型輸入是28x28x1的序列圖片,輸出只有1維。輸出值接近0代表辨別結果是偽圖片,輸出值接近1表示辨別結果是真實樣本圖片。
其中的卷積網絡層,我們在上一個系列中做了仔細的介紹,這里可以再稍微復習一下關於卷積的輸出維度。卷積層的輸入必須是寬x高x色深的多維數組。輸出的色深部分,同卷積層的節點數相同。寬、高則同卷積核的步長數相關,一般是乘的關系。比如:

    ...假設本層輸入為7x7x256...
layers.Conv2DTranspose(128, (5, 5), strides=(1, 1), padding='same', use_bias=False))
    ...因為節點數為128,步長是1...
    ...所以輸出維度是7x7x128...
layers.Conv2DTranspose(64, (5, 5), strides=(2, 2), padding='same', use_bias=False))
    ...輸出維度為14x14x64...

使用Keras之后,這些細節一般都不需要自己去算了。但在這種圖片作為輸入、輸出參數的模型中,為了保證結果圖片是指定分辨率,這樣的計算還是難以避免的。

兩個模型分別使用兩個不同的代價函數,生成模型的代價函數很簡單。我們期望生成網絡的圖片,經過辨別模型后,結果無限接近1,也就是真實樣本的水平:

# 生成模型的損失函數
def generator_loss(fake_output):
    # 生成模型期望最終的結果越來越接近1,也就是真實樣本
    return cross_entropy(tf.ones_like(fake_output), fake_output)

辨別模型的代價函數,則是要對所有的樣本圖片人為指定標注結果是1,對所有生成的圖片,則人為指定標注結果0。這目的是訓練辨別模型對於辨別真偽的能力越來越強,從而可以判斷生成的圖片,是否能無限接近真實樣本圖片的水平。這個過程,其實就是“對抗”的過程。

# 辨別模型損失函數
def discriminator_loss(real_output, fake_output):
    # 樣本圖希望結果趨近1
    real_loss = cross_entropy(tf.ones_like(real_output), real_output)
    # 自己生成的圖希望結果趨近0
    fake_loss = cross_entropy(tf.zeros_like(fake_output), fake_output)
    # 總損失
    total_loss = real_loss + fake_loss
    return total_loss

完整源碼

程序的其它部分,都同通常的機器學習項目非常類似,應當讀起來沒有難度了。

#!/usr/bin/env python3

from __future__ import absolute_import, division, print_function, unicode_literals
import tensorflow as tf
from tensorflow import keras
import glob
import imageio
import matplotlib.pyplot as plt
import numpy as np
import os
import PIL
from tensorflow.keras import layers
import time
import sys

# 如果使用train參數運行則進入訓練模式
TRAIN = False
if len(sys.argv) == 2 and sys.argv[1] == 'train':
    TRAIN = True

# 使用手寫字體樣本做訓練
# (train_images, _), (_, _) = keras.datasets.mnist.load_data()
# 使用時尚單品樣本做訓練
(train_images, _), (_, _) = keras.datasets.fashion_mnist.load_data()

# 因為卷積層的需求,增加色深維度
train_images = train_images.reshape(train_images.shape[0], 28, 28, 1).astype('float32')
# 規范化為-1 - +1
train_images = (train_images - 127.5) / 127.5

BUFFER_SIZE = 60000
BATCH_SIZE = 256
train_dataset = tf.data.Dataset.from_tensor_slices(train_images).shuffle(BUFFER_SIZE).batch(BATCH_SIZE)

# 圖片生成模型
def make_generator_model():
    model = tf.keras.Sequential()
    model.add(layers.Dense(7*7*256, use_bias=False, input_shape=(100,)))
    model.add(layers.BatchNormalization())
    model.add(layers.LeakyReLU())

    model.add(layers.Reshape((7, 7, 256)))
    assert model.output_shape == (None, 7, 7, 256) # Note: None is the batch size

    model.add(layers.Conv2DTranspose(128, (5, 5), strides=(1, 1), padding='same', use_bias=False))
    assert model.output_shape == (None, 7, 7, 128)  
    model.add(layers.BatchNormalization())
    model.add(layers.LeakyReLU())

    model.add(layers.Conv2DTranspose(64, (5, 5), strides=(2, 2), padding='same', use_bias=False))
    assert model.output_shape == (None, 14, 14, 64)    
    model.add(layers.BatchNormalization())
    model.add(layers.LeakyReLU())

    model.add(layers.Conv2DTranspose(1, (5, 5), strides=(2, 2), padding='same', use_bias=False, activation='tanh'))
    assert model.output_shape == (None, 28, 28, 1)

    return model

generator = make_generator_model()

# 原圖、生成圖辨別網絡
def make_discriminator_model():
    model = tf.keras.Sequential()
    model.add(layers.Conv2D(64, (5, 5), strides=(2, 2), padding='same', 
                                     input_shape=[28, 28, 1]))
    model.add(layers.LeakyReLU())
    model.add(layers.Dropout(0.3))

    model.add(layers.Conv2D(128, (5, 5), strides=(2, 2), padding='same'))
    model.add(layers.LeakyReLU())
    model.add(layers.Dropout(0.3))

    model.add(layers.Flatten())
    model.add(layers.Dense(1))

    return model

discriminator = make_discriminator_model()

# 隨機生成一個向量,用於生成圖片
noise = tf.random.normal([1, 100])
# 生成一張,此時模型未經訓練,圖片為噪點
generated_image = generator(noise, training=False)
# plt.imshow(generated_image[0, :, :, 0], cmap='gray')
# 判斷結果
decision = discriminator(generated_image)
# 此時的結果應當應當趨近於0,表示為偽造圖片
print(decision)

# 交叉熵損失函數
cross_entropy = tf.keras.losses.BinaryCrossentropy(from_logits=True)

# 辨別模型損失函數
def discriminator_loss(real_output, fake_output):
    # 樣本圖希望結果趨近1
    real_loss = cross_entropy(tf.ones_like(real_output), real_output)
    # 自己生成的圖希望結果趨近0
    fake_loss = cross_entropy(tf.zeros_like(fake_output), fake_output)
    # 總損失
    total_loss = real_loss + fake_loss
    return total_loss

# 生成模型的損失函數
def generator_loss(fake_output):
    # 生成模型期望最終的結果越來越接近1,也就是真實樣本
    return cross_entropy(tf.ones_like(fake_output), fake_output)

generator_optimizer = tf.keras.optimizers.Adam(1e-4)
discriminator_optimizer = tf.keras.optimizers.Adam(1e-4)

# 訓練結果保存
checkpoint_dir = 'dcgan_training_checkpoints'
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt")
checkpoint = tf.train.Checkpoint(generator_optimizer=generator_optimizer,
                                 discriminator_optimizer=discriminator_optimizer,
                                 generator=generator,
                                 discriminator=discriminator)

EPOCHS = 100
noise_dim = 100
num_examples_to_generate = 16

# 初始化16個種子向量,用於生成4x4的圖片
seed = tf.random.normal([num_examples_to_generate, noise_dim])

# @tf.function表示TensorFlow編譯、緩存此函數,用於在訓練中快速調用
@tf.function
def train_step(images):
    # 隨機生成一個批次的種子向量
    noise = tf.random.normal([BATCH_SIZE, noise_dim])

    with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape:
        # 生成一個批次的圖片
        generated_images = generator(noise, training=True)

        # 辨別一個批次的真實樣本
        real_output = discriminator(images, training=True)
        # 辨別一個批次的生成圖片
        fake_output = discriminator(generated_images, training=True)

        # 計算兩個損失值
        gen_loss = generator_loss(fake_output)
        disc_loss = discriminator_loss(real_output, fake_output)

    # 根據損失值調整模型的權重參量
    gradients_of_generator = gen_tape.gradient(gen_loss, generator.trainable_variables)
    gradients_of_discriminator = disc_tape.gradient(disc_loss, discriminator.trainable_variables)

    # 計算出的參量應用到模型
    generator_optimizer.apply_gradients(zip(gradients_of_generator, generator.trainable_variables))
    discriminator_optimizer.apply_gradients(zip(gradients_of_discriminator, discriminator.trainable_variables))

def train(dataset, epochs):  
    for epoch in range(epochs+1):
        start = time.time()

        for image_batch in dataset:
            train_step(image_batch)

        # 每個訓練批次生成一張圖片作為階段成功
        print("=======================================")
        generate_and_save_images(
            generator,
            epoch + 1,
            seed)

        # 每20次迭代保存一次訓練數據
        if (epoch + 1) % 20 == 0:
            checkpoint.save(file_prefix=checkpoint_prefix)

        print('Time for epoch {} is {} sec'.format(epoch + 1, time.time()-start))

def generate_and_save_images(model, epoch, test_input):
    # 設置為非訓練狀態,生成一組圖片
    predictions = model(test_input, training=False)

    fig = plt.figure(figsize=(4,4))

    # 4格x4格拼接
    for i in range(predictions.shape[0]):
        plt.subplot(4, 4, i+1)
        plt.imshow(predictions[i, :, :, 0] * 127.5 + 127.5, cmap='gray')
        plt.axis('off')

    # 保存為png
    plt.savefig('image_at_epoch_{:04d}.png'.format(epoch))
    # plt.show()
    plt.close()

# 遍歷所有png圖片,匯總為gif動圖
def write_gif():
    anim_file = 'dcgan.gif'
    with imageio.get_writer(anim_file, mode='I') as writer:
        filenames = glob.glob('image*.png')
        filenames = sorted(filenames)
        last = -1
        for i, filename in enumerate(filenames):
            frame = 2*(i**0.5)
            if round(frame) > round(last):
                last = frame
            else:
                continue
            image = imageio.imread(filename)
            writer.append_data(image)
        image = imageio.imread(filename)
        writer.append_data(image)

# 生成一張初始狀態的4格圖片,應當是噪點
generate_and_save_images(
        generator,
        0000,
        seed)

if TRAIN:
    # 以訓練模式運行,進入訓練狀態
    train(train_dataset, EPOCHS)
    write_gif()
else:
    # 非訓練模式,恢復訓練數據
    checkpoint.restore(tf.train.latest_checkpoint(checkpoint_dir))
    print("After training:")
    # 顯示訓練完成后,生成圖片的辨別結果
    generated_image = generator(noise, training=False)
    decision = discriminator(generated_image)
    # 結果應當趨近1
    print(decision)
    # 重新生成隨機值,生成一組圖片保存
    seed = tf.random.normal([num_examples_to_generate, noise_dim])
    generate_and_save_images(
            generator,
            9999,
            seed)

程序經過100次迭代,最終生成的的圖片類似這個樣子:

還是老話,看起來的確一般,但應當說已經有一些神似真實的樣本了。

而把完整的訓練過程連續起來作為一張動圖,同VAE一樣,是一幅從噪聲到清晰,緩慢的漸進過程。因為GAN網絡並非直接比較圖片結果,無法更直接的指出圖片差距,因此在漸進過程中,能看到一些反復和跳動。這說明,在機器視覺領域GAN的可控性並不如VAE。

在所有模型未經訓練的時候,我們隨機生成了一幅圖片,使用辨別器進行了判斷。在訓練完成之后,我們再次重復這一過程。通過命令行的輸出,我們可以看到類似這樣的結果:

tf.Tensor([[-4.8871705e-05]], shape=(1, 1), dtype=float32)
After training:
tf.Tensor([[-1.5235078]], shape=(1, 1), dtype=float32)

一開始是一個很趨近於0的值,這是因為那張完全是噪點組成的生成圖片,同真實樣本圖片完全沒有相似點,雖然辨別模型並未訓練,但這依然是很低的得分。
在訓練完成后,所生成的圖片,從辨別器的眼中看來,已經很接近真實樣本,因此我們獲得了一個較高的得分。

GAN參考論文:《NIPS 2016 Tutorial: Generative Adversarial Networks>

(待續...)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM