GAN實戰筆記——第四章深度卷積生成對抗網路(DCGAN)

墨戈發表於2022-02-23

深度卷積生成對抗網路(DCGAN)

我們在第3章實現了一個GAN,其生成器和判別器是具有單個隱藏層的簡單前饋神經網路。儘管很簡單,但GAN的生成器充分訓練後得到的手寫數字影像的真實性有些還是很具說服力的。即使是那些無法被識別為人類手寫數字的字元,也具有許多手寫符號的特徵,例如可辨認的線條邊緣和形狀,特別是與用作生成器原始輸入的隨機噪聲相比,更是如此。

想象一下,如果使用更強大的網路架構可以實現什麼?本章中的生成器和判別器都將使用卷積神經網路(CNN,或 ConvNet),而不再是簡單的雙層前饋網路。這種GAN架構稱為深度卷積生成對抗網路( Deep Convolutional GAN, DCGAN)。

在深入探討 DCGAN實現的細節之前,我們先在本章介紹 ConvNet的關鍵概念,回顧開發DCGAN背後的歷史,並介紹使DCGAN這樣複雜的架構在實踐中變為可行的關鍵性突破之一:批歸一化( batch normalization)。

一、卷積神經網路

1. 卷積濾波器

常規前饋神經網路的神經元排列在平面的全連線層中,而 ConvNet中的層排列在三維(寬高深)中。卷積是通過在輸入層上滑動一個或多個濾波器(filter)來執行的,每個濾波器都有一個相對較小的感受野(寬×高),但它貫穿輸入影像的全部深度。

每個濾波器在輸入影像上滑動每一步,都會輸出一個啟用值:它是輸入值和過濾器值之間的點積,此過程將為每個濾波器生成一個二維的啟用圖( activation map)。將每個濾波器生成的啟用圖堆疊在一起可以形成一個三維輸出層,其輸出深度等於所用濾波器的數量。

2. 引數共享

重要的是,給定濾波器引數被其所有輸入值共享,這具有直觀和實用的優點。直觀地講引數共享能夠有效地學習視覺特徵和形狀(如線條和邊緣),無論它們在輸入影像中位於何處。從實際的角度來看,引數共享可以大大減少可訓練引數的數量。這降低了過擬合的風險,並允許該技術在不增加可訓練引數的情況下擴充套件到更高解析度的影像中,而在同樣情況下,傳統全連線網路卻要指數爆炸一樣增加可訓練引數才可以做到。

3. 卷積神經網路視覺化

如果這樣解釋聽起來有些令人困感,那麼通過視覺化這些概念可以使它們不那麼抽象。下圖展示了單個卷積操作。描述了二維輸入上單個濾波器的卷積運算。實際上,輸入影像通常是三維的而且幾個濾波器堆疊在一起使用。

一個3X3的卷積濾波器在一個5X5的輸入上滑動——從左到右,從上到下。過濾器滑動步長為2,因此一共滑動4次,得到一個2X2的啟用圖。每次滑動,整個過濾器會產生一個啟用值。

image

但基本機制是不變的:不管輸入體積的深度如何,每個濾波器每一步產生一個值,使用的濾波器數量決定了輸出影像的深度,因為它們生成的啟用圖相互疊加,如下圖所示。

image

filter和kernel之間的不同很微妙。很多時候,它們可以互換,所以這可能造成我們的混淆。那它們之間的不同在於哪裡呢?一個"kernel"更傾向於是2D的權重矩陣。而"filter"則是指多個Kernel堆疊的3D結構。如果是一個2D的filter,那麼兩者就是一樣的。但是一個3Dfilter, 在大多數深度學習的卷積中,它是包含kernel的。每個卷積核都是獨一無二的,主要在於強調輸入通道的不同方面。

二、DCGAN簡史

DCGAN於2016年提出,自問世以來便成了GAN領域最重要的早期創新之一。這並不是研究人員第一次在GAN中使用 ConvNet的嘗試,卻是第一次成功的將ConvNet直接整合到完整的GAN模型中。

ConvNet的使用加劇了困擾GAN訓練的許多困難,包括訓練不穩定和梯度飽和。的確這些挑戰是如此艱鉅,以至於有些研究人員求助於其他方法,如拉普拉斯生成對抗網路( LAPGAN)——它使用拉普拉斯金字塔式的級聯卷積網路,也就是說,在每一層使用GAN框架對單獨的卷積神經網路進行訓練。由於被更優越的方法所取代, LAPGAN在很大程度上已經被扔進了歷史的垃圾筒,因此瞭解它的內部原理並不重要。

儘管 LAPGAN笨拙複雜且計算煩瑣,但在其釋出時仍提供了當時質量最高的影像,與原始GAN相比改進了4倍( LAPGAN有40%,原始GAN有10%的生成影像被人工評估者誤認為是真實的)。因此, LAPGAN展示了將GAN與 ConvNet結合的巨大潛力。

在 DCGAN中, Radford和他的合作者引入了一些技術和優化方法,使 ConvNet可以擴充套件到完整的GAN框架,而無須修改底層的GAN架構,也不需要將GAN簡化為更復雜的模型框架的子結構(如 LAPGAN)。Radford等人引入的關鍵技術之一就是使用了批歸一化——通過歸一化應用它的每一層的輸入來幫助穩定訓練過程。下面仔細看看什麼是批歸一化以及它又是怎麼起作用的。

三、批歸一化

就像對網路輸入進行歸一化一樣,在每個小批量訓練資料通過網路時,對每個層的輸入進行歸一化。

1.理解歸一化

本節的內容有助於提醒我們什麼是歸一化以及為什麼先要對輸入特徵值進行歸一化。歸一化( normalization)是資料的縮放,使它具有零均值和單位方差。這是通過取每個資料點x減去平均值\({\mu}\)​,然後除以標準偏差得到的,如下式所示。

\[\hat{x} = \frac{x - \mu}{\sigma} \]

歸一化有幾個優點。最重要的一點或許是使得具有巨大尺度差異的特徵之間的比較變得更容易,進而使訓練過程對特徵的尺度不那麼敏感。下面考慮一個(虛構的)例子,假設我們嘗試基於兩個特徵來預測一個家庭的每月支出:家庭的年收入和家庭成員數。一般而言,一個家庭的收入越多,家庭成員越多,支出就越多。

但是這兩個特徵的尺度截然不同:年收入增加10美元可能不會影響一個家庭的支出,但增加10個成員可能會嚴重影響任何一個家庭的預算。歸一化通過將每個特徵值縮放到一個標準化的尺度上解決了這個問題,這樣一來每個資料點都不表示為其實際值,而是以一個相對的“分數”表示給定資料點與平均值的標準偏差。

批歸一化背後所體現的理念是,在處理具有多層的深度神經網路時,僅規範化輸入可能還遠遠不夠。當輸入值經過一層又一層網路時,它們將被每一層中的可訓練引數進行縮放。當引數通過反向傳播得到調整時,每一層輸入的分佈在隨後的訓練迭代中都容易發生變化,從而影響學習過程的穩定性。在學術界,這個問題稱為協變數偏移( covariate shift)。批歸一化通過按每個小批量的均值和方差縮放每個小批量中的值來解決該問題。

2. 計算批歸一化

批歸一化的計算方式與之前介紹的簡單歸一化方程在幾個方面有所不同。我們將在本節進行介紹。

\(\mu_{B}\)為小批量B的平均值,\(\sigma^2_{B}\)為小批量B的方差(均方誤差)。歸一化值的計算如下式所示。

\[\hat{x} = \frac{x - \mu_{B}}{\sqrt{\sigma^2_{B}+\varepsilon}} \]

增加\(\varepsilon\)項是為了保持數值穩定性,主要是為了避免被零除,一般設定為一較小的正常數,例如0.001。

在批歸一化中不直接使用這些歸一化值,而是將它們乘以\(\gamma\)並加上\(\beta\)後,再作為輸入傳遞到下一層,如下式所示。

\[y = \gamma\hat{x}+\beta \]

重要的是,\(\gamma\)\(\beta\)可訓練的引數,就像權重和偏置一樣在網路訓練期間進行調整。這樣做有助於將中間的輸入值標準化,使其均值在0附近(但非0)。方差也不是1。\(\gamma\)\(\beta\)是可訓練的,因此網路可以學習哪些值最有效。

幸運的是,我們不必操心這些。Keras中的函式 Keras.layers.BatchNormalization可以處理所有小批量計算並在後臺進行更新。

批歸一化限制了更新前一層中的引數對當前層接收的輸入分佈可能的影響。這減少了跨層引數之間不必要的相互依賴,從而有助於加快網路訓練並增強魯棒性,特別是在網路引數初始化方面。

批歸一化已被證明對包括 DCGAN在內的許多深度學習架構是否可行至關重要。

四、用DCGAN生成手寫數字

我們將在本節回顧第3章中的生成 MNIST手寫數字。這次將使用 DCGAN架構,並將生成器和判別器都換成卷積網路,如下圖所示。除此更改外,其餘網路結構保持不變。在本教程的最後,我們將比較兩個GAN(傳統GAN與 DCGAN)生成的手寫數字的質量,以展示更高階的網路結構帶來的改進。

image

1. 匯入模組並指定模型輸入維度

首先匯入訓練和執行模型所需的所有包、模組以及庫。直接從keras.datasets匯入MNIST手寫數字資料集。

#匯入宣告
import matplotlib.pyplot as plt
import numpy as np

from keras.datasets import mnist
from keras.layers import (
    Activation, BatchNormalization, Dense, Dropout, Flatten, Reshape
)
from keras.layers.advanced_activations import LeakyReLU
from keras.layers.convolutional import Conv2D, Conv2DTranspose#卷積層
from keras.models import Sequential
from keras.optimizers import Adam
#模型輸入維度
img_rows = 28
img_cols = 28
channels = 1

img_shape = (img_rows, img_cols, channels)#輸入影像的維度

z_dim = 100#用於輸入生成器的噪聲向量的大小

2. 構造生成器

ConvNet傳統上用於影像分類任務,影像以尺寸——高度x寬度x彩色通道數作為輸入,並通過一系列卷積層輸出一個維數為1×n的類別得分向量,n是類別標籤數。要使用ConvNet結構生成影像,則是上述過程的逆過程:並非獲取影像再將其處理為向量,而是獲取向量並調整其大小以使之變為影像。

這一過程的關鍵是轉置卷積(transposed convolution)。我們通常使用卷積減小輸入的寬度和高度,同時增加其深度。轉置卷積與其相反,用於增加寬度和高度,同時減小深度,如下圖的生成器網路圖所示。(生成器將隨機噪聲向量作為輸入並生成28×28×1的影像。這一過程通過多層轉置卷積實現,在卷積層之間應用批歸一化來穩定訓練過程(影像未按比例繪製))

image

生成器從噪聲向量z開始,使用一個全連線層將向量重塑為具有小的寬x高和大的深度的三維隱藏層。使用轉置卷積對輸入進行逐步重塑,以使其寬×高增大而深度減小,直到具有想要合成的影像大小28×28×1。

在每個轉置卷積層之後,應用批歸一化和 LeakyReLU啟用函式;在最後一層不應用批歸一化,並且使用tanh啟用函式代替ReLU。

綜合所有步驟如下。

  1. 取一個隨機噪聲向量z,通過全連線層將其重塑為7×7×256張量。
  2. 使用轉置卷積,將7×7×256張量轉換為14×14×128張量。
  3. 應用批歸一化和 LeakyReLU啟用函式。
  4. 使用轉置卷積,將14×14×128張量轉換為14×14×64張量。注意:寬度和高度尺寸保持不變。可以通過將Conv2DTranspose中的 stride引數設定為1來實現。
  5. 應用批歸一化和 LeakyReLU啟用函式。
  6. 使用轉置卷積,將14×14×64張量轉換為輸出影像大小28×28×1。
  7. 應用tanh啟用函式。
def build_generator(z_dim):
    model = Sequential()
    model.add(Dense(128*7*7, input_dim=z_dim))
    model.add(LeakyReLU())
    model.add(BatchNormalization())
    model.add(LeakyReLU())
    model.add(Reshape((7, 7, 128)))
    model.add(Conv2DTranspose(64, 3, 2, padding='same'))
    #步長為1時,padding為"SAME"可以保持輸出與輸入的尺寸具有相同的大小
    #輸出矩陣的大小:padding為SAME時:ceil(i/s),其中i表示輸入矩陣的大小,s表示卷積核的步長,
    #ceil函式表示向上取整。
    #padding為VALID時:ceil((i-k+1)/s),k表示卷積核的尺寸。
    model.add(LeakyReLU())
    model.add(Conv2DTranspose(1, 3, 2, padding='same'))
    model.add(Activation('tanh'))#帶tanh啟用函式的輸出層
    
    return model

3. 構造判別器

判別器是一種ConvNet,它接收影像並輸出預測向量:在這種情況下,它是一種二值分類,表明輸入的影像是否被認為是真實的而不是假的。如下圖所示是我們將要實現的判別器網路。(判別器將28×28×1影像作為輸入經過多個卷積層,使用sigmoid啟用函式\(\sigma\)​輸入/輸出影像是真實的概率。在卷積層之間應用批歸一化來穩定訓練過程(影像未按比例繪製))

image

判別器的輸入是28×28×1的影像。應用卷積可以對影像進行變換,使其寬×高逐漸變小,深度逐漸變深。在所有卷積層中使用 LeakyReLU啟用函式;批歸一化用於除第一層以外的所有卷積層;輸出使用全連線層和sigmoid啟用函式。

綜合所有步驟如下。

  1. 使用卷積層將28×28×1的輸入影像轉換為28×28×64的張量。
  2. 應用 LeakyReLU啟用函式。
  3. 使用卷積層將28×28×64的張量轉換為12×12×128的張量。
  4. 應用LeakyReLU啟用函式。
  5. 使用卷積層將12×12×128的張量轉換為4×4×128的張量。
  6. 應用LeakyReLU啟用函式。
  7. 將4×4×128張量展成大小為4×4×128=2048的向量。
  8. 使用全連線層,輸入sigmoid啟用函式計算輸入影像是否真實的概率。
def build_discriminator(img_shape):
    model = Sequential()
    model.add(Conv2D(64, 5, 1, padding='same', input_shape=img_shape))
    model.add(LeakyReLU())
    model.add(Conv2D(128, 5, 2))
    model.add(LeakyReLU())
    model.add(Conv2D(128, 5, 2))
    model.add(Flatten())
    model.add(Dense(1024))#隨著網路深度增加,網路寬度也相應增加
    model.add(LeakyReLU())
    model.add(Dense(1))
    model.add(Activation('sigmoid'))
    #use_bias:一般來說要設成True。但是當卷積層後根由BatchNorm或者InstanceNorm層時,最好設為False,
    #因為歸一化層會歸一化卷積層輸出並且加上自己的bias,卷積層的(如果有)bias就是多餘的了
    
    return model

4. 構建並執行DCGAN

1. 完整程式碼

除了生成器和判別器的網路結構,DCGAN的其他設定和實現與第3章中簡單GAN的網路相同。這體現了GAN架構的通用性。

def build_dcgan(generator, discriminator):
    model = Sequential()
    
    #生成器和判別器結合為一個模型
    model.add(generator)
    model.add(discriminator)
    
    return model

#構建並編譯判別器
discriminator = build_discriminator(img_shape)
discriminator.compile(
    loss='binary_crossentropy',
    optimizer=Adam(lr=1e-3),
    metrics=['acc']
)

#構建生成器
generator = build_generator(z_dim)
#生成器訓練時保持判別器引數不變
discriminator.trainable = False

#構建並編譯判別器固定的GAN模型來訓練生成器
dcgan = build_dcgan(generator, discriminator)
dcgan.compile(
    loss='binary_crossentropy',
    optimizer=Adam(lr=1e-3)
)
losses = []
accuracies = []
iteration_checkpoints = []

def train(iterations, batch_size, sample_interval):
    (x_train, _), (_, _) = mnist.load_data()#載入mnist資料集
    x_train = x_train / 127.5 - 1.0#灰度畫素值從[0, 255]縮放到[-1, 1]
    x_train = np.expand_dims(x_train, axis=3)
    
    real = np.ones((batch_size, 1))#真實影像標籤為1
    fake = np.zeros((batch_size, 1))#偽影像標籤為0
    
    for iteration in range(iterations):
        #獲取一批真實影像
        idx = np.random.randint(0, x_train.shape[0], batch_size)
        imgs = x_train[idx]
        
        #生成一批偽影像
        z = np.random.normal(0, 1, (batch_size, 100))
        gen_imgs = generator.predict(z)#生成batch_size個形狀為(28, 28, 1)的影像
        
        #訓練判別器
        d_loss_real = discriminator.train_on_batch(imgs, real)
        d_loss_fake = discriminator.train_on_batch(gen_imgs, fake)
        d_loss, accuracy = 0.5 * np.add(d_loss_real, d_loss_fake)#計算總的loos和acc的值:取真實影像和偽影像的均值

        z = np.random.normal(0, 1, (batch_size, 100))#生成128行100列偽資料, 即一個資料有100列特徵,生成128個資料(一個batchsize)
        gen_imgs = generator.predict(z)
        
        g_loss = gan.train_on_batch(z, real)
        
        if (iteration + 1) % sample_interval == 0:
            #儲存訓練損失和準確率以便訓練後繪圖
            losses.append((d_loss, g_loss))
            accuracies.appennd(100.0 * accuracy)
            iteration_checkpoints.append(iteration + 1)
            
            print("%d [D loss: :%f, acc.: %.2f] [G loss: %f]" % (iteration + 1, d_loss, 100.0 * accuracy, g_loss))
            sample_image(generator)#輸出生成影像的取樣
#顯示生成影像
def sample_image(generator, image_grid_rows=4, image_grid_columns=4):
    z = np.random.normal(0, 1, (image_grid_rows * image_grid_columns, z_dim))#隨機噪聲取樣
    
    gen_imgs = generator.predict(z)#從隨機噪聲生成影像
    
    gen_imgs = 0.5 * gen_imgs + 0.5#影像縮放到[0, 1]:[-1, 1]--->[0, 1]
    
    fig, axs = plt.subplots(
        img_rows,
        img_cols,
        figsize(16, 16),
        sharex=True, 
        sharey=True
    )
    cnt = 0
    for i in range(image_grid_rows):
        for j in range(image_grid_columns):
            axs[i, j].imshow(gen_imgs[cnt, :, :, 0], cmap='gray')
            axs[i, j].axis('off')
            cnt += 1
#執行模型
iterations = 20000
batch_size = 128
sample_interval = 1000

train(iterations, batch_size, sample_interval)

2. 模型輸出

充分訓練後的DCGAN的生成器生成的手寫數字如下圖左所示,為了便於同時比較,GAN生成的數字樣本如下圖右所示。

image

下圖是MNIST真實手寫數字樣本。可以看到DCGAN生成的手寫數字與真實樣本幾乎沒有區別。

image

五、結論

DCGAN展示了GAN框架的通用性。理論上來說,判別器和生成器可以用任何可微函式表示,甚至可以用多層卷積網路這樣複雜的函式表示。但是 DCGAN也表明,要使更復雜的實現在實踐中真正起作用,還存在很大的障礙。沒有批歸一化等突破性技術, DCGAN將無法正確訓練。

六、小結

  1. 卷積神經網路( ConvNet)使用一個或多個在輸入影像上滑動的卷積濾波器。在輸入影像上滑動的每一步,濾波器都會使用一組引數來產生一個啟用值。來自所有濾波器的所有啟用值共同生成輸出層。
  2. 批歸一化是指在將每一層的輸出作為輸入傳遞到下一層之前,對其進行歸一化,以減小神經網路中協變數偏移(訓練期間各層之間輸入值分佈的變化)。
  3. 深度卷積生成對抗網路( DCGAN)以卷積神經網路為生成器和判別器。它在影像處理任務(如手寫數字生成)中實現了優異的效能。

相關文章