DCGAN(深度卷積對抗網絡)案例

介紹

圖片描述

如圖所示,GAN網絡會同時訓練兩個模型。生成器:負責生成數據(比如:照片);判別器:判別所生成照片的真假。訓練過程中,生成器生成的照片會越來越接近真實照片,直到判別器無法區分照片真假。

圖片描述

DCGAN(深度卷積對抗生成網絡)是GAN的變體,是一種將卷積引入模型的網絡。特點是:

  • 判別器使用strided convolutions來替代空間池化,生成器使用反捲積
  • 使用BN穩定學習,有助於處理初始化不良導致的訓練問題
  • 生成器輸出層使用Tanh激活函數,其它層使用Relu激活函數。判別器上使用Leaky Relu激活函數。

本次案例我們將使用mnist作爲數據集訓練DCGAN網絡,程序最後將使用GIF的方式展示訓練效果。

數據導入

import tensorflow as tf
import glob
import imageio
import matplotlib.pyplot as plt
import numpy as np
import os
import tensorflow.contrib as tcon
import PIL
import time
from IPython import display

# shape:(60000,28,28)
(train_images,train_labels),(_,_)=tf.keras.datasets.mnist.load_data()
# shape:[batch_size,height,width,channel]
train_images_reshape=tf.reshape(train_images,shape=(train_images.shape[0],28,28,1)).astype(tf.float32)
# 縮放圖片[-1,1]
train_images_nor=(train_images-127.5)/127.5

dataset加載數據

BUFFER_SIZE=60000
BATCH_SIZE=256

# 優化輸入管道需要從:讀取,轉換,加載三方面考慮。
train_dataset=tf.data.Dataset.from_tensor_slices(train_images).shuffle(buffer_size=BUFFER_SIZE).batch(BATCH_SIZE)

生成模型

該生成模型將使用反捲積層,我們首先創建全連接層然後通過兩次上採樣將圖片分辨率擴充至28x28x1。我們將逐步提升分辨率降低depth,除最後一層使用tanh激活函數,其它層都使用Leaky Relu激活函數。

def make_generator_model():
    # 反捲積,從後往前
    model=tf.keras.Sequential()
    model.add(
        tf.keras.layers.Dense(
            input_dim=7*7*256,
            
            # 不使用bias的原因是我們使用了BN,BN會抵消掉bias的作用。
            # bias的作用:
            # 提升網絡擬合能力,而且計算簡單(只要一次加法)。
            # 能力的提升源於調整輸出的整體分佈
            use_bias=False,
            # noise dim
            input_shape=(100,)
        )
    )
    """
    隨着神經網絡的訓練,網絡層的輸入分佈會發生變動,逐漸向激活函數取值兩端靠攏,如:sigmoid激活函數,
    此時會進入飽和狀態,梯度更新緩慢,對輸入變動不敏感,甚至梯度消失導致模型難以訓練。
    BN,在網絡層輸入激活函數輸入值之前加入,可以將分佈拉到均值爲0,標準差爲1的正態分佈,從而
    使激活函數處於對輸入值敏感的區域,從而加快模型訓練。此外,BN還能起到類似dropout的正則化作用,由於我們會有
    ‘強拉’操作,所以對初始化要求沒有那麼高,可以使用較大的學習率。
    """
    model.add(tf.keras.layers.BatchNormalization())
    """
    relu 激活函數在輸入爲負值的時候,激活值爲0,此時神經元無法學習
    leakyrelu 激活函數在輸入爲負值的時候,激活值不爲0(但值很小),神經元可以繼續學習
    """
    model.add(tf.keras.layers.LeakyReLU())

    model.add(tf.keras.layers.Reshape(input_shape=(7,7,256)))
    assert model.output_shape == (None,7,7,256)

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

    # 卷積核爲奇數:圖像兩邊可以對稱padding 00xxxx00
    model.add(tf.keras.layers.Conv2DTranspose(
        filters=64,
        kernel_size=5,
        strides=2,
        padding='same',
        use_bias='False'
    ))
    assert model.output_shape == (None,14,14,64)
    model.add(tf.keras.layers.BatchNormalization())
    model.add(tf.keras.layers.LeakyReLU())

    model.add(tf.keras.layers.Conv2DTranspose(
        filters=1,
        kernel_size=5,
        strides=2,
        padding='same',
        use_bias='False',
        
        # tanh激活函數值區間[-1,1],均值爲0關於原點中心對稱。、
        # sigmoid激活函數梯度在反向傳播過程中會出全正數或全負數,導致權重更新出現Z型下降。
        activation='tanh'
    ))
    assert model.output_shape == (None,28,28,1)

    return model

判別模型

判別器使用strided convolutions來替代空間池化,比如這裏strided=2。卷積層使用LeakyReLU替代Relu,並使用Dropout爲全連接層提供加噪聲的輸入。

def make_discriminator_model():
    # 常規卷積操作
    model = tf.keras.Sequential()
    model.add(tf.keras.layers.Conv2D(64, (5, 5), strides=(2, 2), padding='same'))
    model.add(tf.keras.layers.LeakyReLU())
    
    # dropout常見於全連接層,其實卷積層也是可以使用的。
    # 這裏簡單翻譯下dropout論文觀點:
    """
    可能很多人認爲因爲卷積層參數較少,過擬合發生概率較低,所以dropout作用並不大。
    但是,dropout在前面幾層依然有幫助,因爲它爲後面的全連接層提供了加噪聲的輸入,從而防止過擬合。
    """
    model.add(tf.keras.layers.Dropout(0.3))
      
    model.add(tf.keras.layers.Conv2D(128, (5, 5), strides=(2, 2), padding='same'))
    model.add(tf.keras.layers.LeakyReLU())
    model.add(tf.keras.layers.Dropout(0.3))
       
    model.add(tf.keras.layers.Flatten())
    model.add(tf.keras.layers.Dense(1))
     
    return model

損失函數

獲取模型:

generator = make_generator_model()
discriminator = make_discriminator_model()

生成器損失函數:

損失函數使用sigmoid cross entropylabels使用值全爲1的數組。

def generator_loss(generator_output):
    return tf.losses.sigmoid_cross_entropy(
        multi_class_labels=tf.ones_like(generator_output),
        logits=generator_output
    )

判別器損失函數

判別器損失函數接受兩種輸入,生成器生成的圖像和數據集中的真實圖像,損失函數計算方法如下:

  • 使用sigmoid cross entropy損失函數計算數據集中真實圖像的損失,labels使用值全爲1的數組。
  • 使用sigmoid cross entropy損失函數計算生成器圖像的損失,labels使用值全爲0的數組。
  • 將以上損失相加得到判別器損失。
def discriminator_loss(real_output, generated_output):
    # real:[1,1,...,1] 
    real_loss = tf.losses.sigmoid_cross_entropy(multi_class_labels=tf.ones_like(real_output), logits=real_output)
    #:generated:[0,0,...,0] 
    generated_loss = tf.losses.sigmoid_cross_entropy(multi_class_labels=tf.zeros_like(generated_output), logits=generated_output)
    
    # 總損失爲兩者相加
    total_loss = real_loss + generated_loss
    return total_loss

模型保存:

# 兩種模型同時訓練,自然需要使用兩種優化器,學習率爲:0.0001
generator_optimizer = tf.train.AdamOptimizer(1e-4)
discriminator_optimizer = tf.train.AdamOptimizer(1e-4)
checkpoint_dir = './training_checkpoints'
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt")

# checkpoint配置
checkpoint = tf.train.Checkpoint(generator_optimizer=generator_optimizer,
                                 discriminator_optimizer=discriminator_optimizer,
                                 generator=generator,
                                 discriminator=discriminator)

模型訓練

訓練參數配置:

# 數據集迭代次數
EPOCHS = 50
# 生成器噪聲維度
noise_dim = 100

# 可視化效果數量設置
num_examples_to_generate = 16
random_vector_for_generation = tf.random_normal([num_examples_to_generate,
                                                 noise_dim])

生成器將我們設定的正態分佈的噪聲向量作爲輸入,用來生成圖像。判別器將同時顯示數據集真實圖像和生成器生成的圖像用於判別。隨後,我們計算生成器和判斷器損失函數對參數的梯度,然後使用梯度下降進行更新。

def train_step(images):
      # 正態分佈噪聲作爲生成器輸入
      noise = tf.random_normal([BATCH_SIZE, noise_dim])
      
      # tf.GradientTape進行記錄
      with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape:
        generated_images = generator(noise, training=True)
        
        # 判別器中真實圖像和生成器的假圖像
        real_output = discriminator(images, training=True)
        generated_output = discriminator(generated_images, training=True)
        
        gen_loss = generator_loss(generated_output)
        disc_loss = discriminator_loss(real_output, generated_output)
        
      gradients_of_generator = gen_tape.gradient(gen_loss, generator.variables)
      gradients_of_discriminator = disc_tape.gradient(disc_loss, discriminator.variables)
      
      generator_optimizer.apply_gradients(zip(gradients_of_generator, generator.variables))
      discriminator_optimizer.apply_gradients(zip(gradients_of_discriminator, discriminator.variables))

開始訓練:

加速計算節約內存,但是不可以使用'pdb','print'。

train_step = tf.contrib.eager.defun(train_step)
def train(dataset, epochs):  
  for epoch in range(epochs):
    start = time.time()
    
    # 迭代數據集
    for images in dataset:
      train_step(images)

    display.clear_output(wait=True)
    
    # 保存圖像用於後面的可視化
    generate_and_save_images(generator,
                               epoch + 1,
                               random_vector_for_generation)
    
    # 每迭代15次數據集保存一次模型
    # 如需部署至tensorflow serving需要使用savemodel
    if (epoch + 1) % 15 == 0:
      checkpoint.save(file_prefix = checkpoint_prefix)
    
    print ('Time taken for epoch {} is {} sec'.format(epoch + 1,
                                                      time.time()-start))
  display.clear_output(wait=True)
  generate_and_save_images(generator,
                           epochs,
                           random_vector_for_generation)

可視化生成器圖像:

def generate_and_save_images(model, epoch, test_input):
  # training:False 不訓練BN
  predictions = model(test_input, training=False)

  fig = plt.figure(figsize=(4,4))
  
  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')
        
  plt.savefig('image_at_epoch_{:04d}.png'.format(epoch))
  plt.show()

train(train_dataset, EPOCHS)

可視化模型訓練結果

展示照片:

def display_image(epoch_no):
  return PIL.Image.open('image_at_epoch_{:04d}.png'.format(epoch_no))

動畫展示訓練結果:

with imageio.get_writer('dcgan.gif', 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)
    
os.system('cp dcgan.gif dcgan.gif.png')
display.Image(filename="dcgan.gif.png")

總結

DCGAN中生成器判別器都使用卷積網絡來提升生成和判別能力,其中生成器利用反捲積,判別器利用常規卷積。生成器用隨機噪聲向量作爲輸入來生成假圖像,判別器通過對真實樣本的學習判斷生成器圖像真僞,如果判斷爲假,生成器重新調校訓練,直到判別器無法區分真實樣本圖像和生成器的圖像。

本文代碼部分參考Yash Katariya,在此表示感謝。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章