GAN實戰筆記——第六章漸進式增長生成對抗網路(PGGAN)

墨戈發表於2022-03-06

漸進式增長生成對抗網路(PGGAN)

使用 TensorFlow和 TensorFlow Hub( TFHUB)構建漸進式增長生成對抗網路( Progressive GAN, PGGAN或 PROGAN)——一種能夠生成全高清的具有照片級真實感影像的前沿技術。這項技術在頂級機器學習會議ICLR2018上提出時引起了轟動,以至於谷歌立即將其整合為 TensorFlow Hub中的幾個模型之一。這項技術被深度學習的鼻祖之一 Yoshua Bengio稱讚為“好得令人難以置信”,在其釋出後,立即成為學術報告和實驗專案的最愛。

本章將給出如下兩個主要例子。

  1. PGGAN的關鍵創新部分的程式碼,具體來說,就是平滑地增大高解析度層以及前面列出的其他3個創新點。
  2. 谷歌在TFHub上提供了一個預訓練好的且易於下載的實現。TFHub是一個用於機器學習模型的新的集中式倉庫,類似於 Docker Hub或 Conda以及PyPI。此復現能夠進行潛在空間插值以控制生成樣本的特徵。這會簡要涉及生成器潛在空間中的種子向量,以便獲得想要的圖片。

這裡使用TFHub而不是像其他章那樣從頭開始實現 PGGAN,原因有如下3個。

  1. 尤其是對於從業人員,我們希望確保你瞭解到可以加快工作流程的軟體工程最佳實踐。想嘗試快速用GAN解決問題嗎?使用TFHub上的其中一種實現即可。TFHub現在有更多的實現,包括許多參考實現。因為這就是機器學習的發展方式——儘可能地使機器學習自動化,這樣我們就可以專注於最重要的事情:產生影響。谷歌的 Cloud Automl和亞馬遜的 Sagemaker是這種趨勢的主要例子,甚至 Facebook最近都推出了 PyTorch Hub,所以兩種主要機器學習框架現在都有一個倉庫了。
  2. NVIDIA研究人員花了一到兩個月的時間來執行最初的 PGGAN。任何人想獨自執行它都是不切實際的,特別是在進行實驗或出現錯誤情況下。TFHub也提供了一個完全可訓練的PGGAN,因此,如果想利用做計算的日子來做其他事,你也可以從頭訓練!
  3. TFHub使我們可以跳過無關緊要的樣板程式碼,而專注於實現重要的想法。

一、潛在空間價值

第2章中有一個較低解析度的空間(潛在空間),可以為輸出提供隨機初始值,對於DCGAN以及PGGAN,初始訓練的潛在空間具有語義上有意義的性質。這意味著可以找到向量偏移量,例如,將眼鏡引入人臉影像,相同的偏移量會在新的影像中引入眼鏡。還可以選擇兩個隨機向量,然後在它們之間每次移動相等的增量,從而逐漸平滑地獲得與第二個向量匹配的影像。

上述方法稱為插值,如下圖所示。(我們可以進行潛在空間插值,因為傳送給生成器的潛在向量會產生一致的結果,這種結果在某些方面是可以預測的。如果考慮潛在向量的變化,不僅生成過程是可預測的,輸出也不是參差不齊的,對微小的變化也不會劇出劇烈的反應。例如,想要一幅由兩張臉混合生成的影像,在兩個向量的平均值附近搜尋即可)正如 BIGGAN論文的作者所說,從一個向量到另一個向量的有意義的轉換表明GAN已經學習到了一些底層結構。

image

在前面的章節中,我們已經瞭解到使用GAN可以輕鬆實現哪些結果,難以實現哪些結果,還對模式崩潰(只展示了總體分佈的幾個樣本)和缺乏收斂性(導致結果質量較差的原因之一)有了一定的瞭解。

芬蘭 NVIDIA的一個團隊發表了一篇論文,這篇論文成功擊敗了之前的許多前沿論文,這就是 Tero Karras等人所撰寫的 Progressive Growing of GAN for Improved Qualin Stability, and Variation該論文有4個基本的創新點,讓我們依次來看看。

1. 高解析度層的漸進增長和平滑

在深入研究 PGGAN的作用之前,我們先來看一個簡單的類比!想象一下,俯瞰某處山區:山區有許多山谷,那裡有漂亮的小溪和村莊,非常宜居。但是其間也會有許多山坡,它們崎嶇不平,而且由於天氣原因,通常不宜居住。我們用山谷和山坡類比損失函式,希望沿著山坡進入更好的山谷,以最小化損失。

我們可以把訓練想象成將登山者放到這處山區的任意地方,讓他順著山坡往下的路進入山谷——這就是隨機梯度下降所做的。但是,假設從一處非常複雜的山脈開始,登山者不知道該往哪個方向走。他周圍的地勢是崎嶇不平的,以至於他很難弄清楚哪裡是有宜居地的最宜人、最低的山谷。假如我們拉遠畫面並降低山脈的複雜度,使登山者對這一特定區域有一個高層次的瞭解。

隨著登山者越來越接近山谷,我們再通過放大地形增加複雜性。這樣不再只看到租糙畫素化的構造,而是可以看到更精細的細節。這種方法的優勢在於,當登山者沿著斜坡下山時,他可以很容易地進行一些小的優化以使旅行更加輕鬆,例如,他可以沿著一條幹涸的小溪行走以更快地到達山谷。這就是漸進式増長(progressive growing):隨著登山者的行進,提高地形的解析度。

然而,如果你玩過一款沙箱類遊戲,或者帶著3D眼鏡在谷歌地圖上快速移動,就會知道快速增加周圍地形的解析度是驚心動魄且不愉快的——所有物體突然映入眼簾。因此,隨著登山者越來越接近目標,我們漸進式地、平滑地並慢慢地引入更多的複雜性。

用專業術語來說,就是訓練過程正在從幾個低解析度的卷積層發展到多個高解析度的層。先訓練早期的層,再引入更高解析度的層——高解析度層中的損失空間很難應對。從簡單的(如經過幾步訓練得到的4x4)開始,最後到更復雜的(如經過多個時期訓練的1024×1024),如下圖所示。能看到如何從平滑的山脈開始,通過放大逐漸增加複雜性嗎?實際上這就是新增額外層對損失函式的影響。這很方便,因為山區(損失函式)在平坦的情況下更容易導航。可以這樣認為:當結構更復雜時(b)損失函式凹凸不平且難以導航(d),因為引數太多(尤其是在早期層中)會產生巨大的影響——通常會增加問題的維數。但是,如果最初刪除部分複雜度(a),就可以在早期獲得更容易導航的損失函式(c),並且只有我們確信自己處於損失空間近似正確的部分,才會増加複雜性。只有這樣,オ能從(a)和(c)轉變為(b)和(d)

image

這種情況下的問題是,即便一次增加一個層(例如,從4x4到8×8),也會給訓練帶來巨大的影響。 PGGAN所做的就是平滑地增加這些層,如下圖所示,訓練了足夠送代次數的16×16解析度後(a),在生成器(G)中引入了另一個轉置卷積,在判別器(D)中引入了另一個卷積,使G和D之間的“介面”為32×32。生成32×32層有兩條路徑:(1-a)乘以 簡單地以最近鄰插值增加尺度的層,這沒有任何經過訓練的引數,比較直接;(a)乘以額外的轉置卷積的輸出層,這需要訓練,但最後會表現得更好。二者相連,以形成新的32×32的生成影像,a從0到1線性縮放,當a達到1時,從16×16開始的最近鄰插值將完全為零。這種平滑的過渡機制極大地穩定了PGGAN架構以給系統適應更高的解析度的時間。

image

但不是立即跳到該分率,而是在通過引數\(\alpha\)(介於0和1之間,從0到1線性縮放)平滑地增加高分率的新層。\(\alpha\)​會影響舊的但擴大規模的層和新生成的更大的層的利用程度。虛線下方的D部分,只是簡單地縮小至1/2,再平滑地注入訓練過的層以用於鑑別,如下圖(b)所示,如果我們對這一新層有信心,保持在32×32(下圖(c)),然後在恰當地訓練好32×32解析度的層之後,就準備再次增長。

2. 示例實現

漸進式平滑增長的程式碼如下所示。

import tensorflow as tf
import keras as K

def upscale_layer(layer, upscale_factor):
    height = layer.get_shape()[1]
    width = layer.get_shape()[2]
    size = (upscale_factor * height, upscale_factor * width)
    upscaled_layer = tf.image.resize_nearest_neighbor(layer, size)
    return upscaled_layer

def smoothly_merge_last_layer(list_of_layers, alpha):
    last_fully_trained_layer = list_of_layers[-2]#如果使用的是tf而不是keras,要記得scope
    last_layer_upscaled = upscale_layer(last_fully_trained_layer, 2)#現在有了最初訓練過的層
    larger_native_layer = list_of_layers[-1]#新加的層還沒有完全訓練
    #assert(斷言)用於判斷一個表示式,在表示式條件為 false 的時候觸發異常
    #這確保可以執行合併程式碼
    assert larger_native_layer.get_shape() == last_layer_upscaled.get_shape()
    new_layer = (1-alpha) * upscaled_layer + larger_native_layer * alpha#利用廣播功能
    return new_layer

3. 小批量標準偏差

人們通常希望能夠生成真實資料集中所有人的臉,而不是隻能生成某個女人的一張照片。為此, Karras等人創造了一種方法,使判別器可以辨別所獲取的樣本是否足夠多樣。這種方法本質上是為判別器計算了一個額外的標量統計量。此統計量是生成器生成的或來自真實資料的小批量中所有畫素的標準偏差。

這是一個非常簡單而優雅的解決方案:現在判別器需要學習的是,如果正在評估的批量中影像的標準偏差很低,則該影像很可能是偽造的——因為真實資料所具有的偏差較大。生成器別無選擇,只能增加生成樣本的偏差,才有機會欺騙判別器。

上述內容理解起來很直觀,技術實現起來也非常簡單,因為它只應用在判別器中。考慮到我們還想最小化可訓練引數的數量,只新增一個額外的數字似乎就夠了。該數字作為特徵圖附加到判別器上——維度或tf. shape列表中的最後一個數字。

具體步驟如下。

(1)[4D—>3D] 計算批次中所有影像和所有通道(高度、寬度和顏色)的標準偏差,然後得到關於每個畫素和每個通道的標準偏差的一個影像。
(2)[3D—>2D] 對所有通道的標準差取均值,得到畫素的一個特徵圖或標準差矩陣,但是顏色通道摺疊了。
(3)[2D一>標量/0D] 對前一個矩陣內所有畫素的標準偏差取均值,以獲得一個標量值。

def minibatch_std_layer(layer, group_size=4):
    #如果使用的是Tensorflow而不是Keras,那麼scope一個小批量組必須被group_size所整除(或<=)
    group_size = K.backend.minimum(group_size, tf.shape(layer)[0])
    #獲得一些形狀資訊,以便快速呼叫和確保預設值
    #從tf.shape中得到輸入,因為pre-image維度通常在圖形執行之前轉換為None
    shape = list(K.int_shape(input))
    shape[0] = tf.shape(input)[0]
    #改變形狀,以便在小批量的水平上進行操作
    #假設層是[Group(G), Mini-batch(M), Width(W), Height(H), Channel(C)]
    minibatch = K.backend.reshape(layer, (group_size, -1, shape[1], shape[2], shape[3]))
    #將均值集中於組[M, W, H, C]
    minibatch -= tf.reduce_mean(minibatch, axis=0, keepdims=True)
    #計算組[M, W, H, C]的方差
    minibatch = tf.reduce_mean(K.backend.square(minibatch), axis = 0)
    #計算組 [M,W,H,C] 的標準偏差
    minibatch = K.backend.square(minibatch + 1e8)
    #對特徵圖和畫素取平均值 [M,1,1,1]
    minibatch = tf.reduce_mean(minibatch, axis=[1,2,4], keepdims=True)
    #轉換標量值以適應組和畫素
    minibatch = K.backend.tile(minibatch, [group_size, 1, shape[2], shape[3]])
    #附加一個特徵圖
    return K.backend.concatenate([layer, minibatch], axis=1)

4. 均衡學習率

需要確保將所有權重\((w)\)歸一化\((w')\)在一定範圍內,這樣\(w'=w/c\)就需要一個常數c,這個常數c對於每一層都是不同的,具體取決於權重矩陣的形狀。這也確保瞭如果任何引數需要採取更大的操作來達到最優(因為它們往往變化更大),那麼使用相關引數可以很容易做到。

RMSProp等人使用簡單的標準正態初始化,然後在執行時縮放每層的權重。Adam允許不同引數的學習率不同,但它有一個陷阱。Adam通過引數的估計標準偏差來調整反向傳播的梯度,從而確保該引數的大小與更新無關。Adam在不同的方向上有不同的學習率,但並不總是考慮動態範圍——在給定的小批量中,維度或特徵的變化有多大。這似乎解決了一個類似於權重初始化的問題。

def equalize_learning_rate(shape, gain, fan_in=None):
    #預設為所有形狀維度減去特徵圖維數的乘積——這給出了每個神經元的傳入連線數
    if fan_in is None: fan_in = np.prod(shape[:-1])
    #這使用了初始化常量
    std = gain / K.sqrt(fan_in)
    #在調整之外建立一個常量
    wscale = K.constant(std, name='wscale', dtype=np.float32)
    #獲取權重值,然後使用廣播機制應用調整
    adjusted_weights = K.get_value('layer', shape=shape, 
            initializer=tf.initializers.random_normal()) * wscale
    return adjusted_weights

5. 生成器中的畫素級特徵歸一化

歸一化特徵的動機是為了使訓練更加穩定。大多數網路都使用了某種形式的歸一化。通常使用的是批歸一化或其虛擬版本。下表部分概述了迄今為止在GAN中使用的歸一化技術。為了使批歸一化及其虛擬版本能夠等效工作,我們必須擁有大量的小批處理,以便求各個樣本的平均值。

方法 生成器歸一化 判別器歸一化
DCGAN 批歸一化 批歸一化
改進的GAN 虛擬批歸一化 虛擬批歸一化
WGAN —— 批歸一化
WGAN-GP 批歸一化 層歸一化

基於“所有主要的實現使用了歸一化”這一事實,我們可以斷定它顯然很重要,但是為什麼不直接使用標準的批歸一化呢?這是因為在想要達到的解析度下,批歸一化過於佔用記憶體。我們必須想出使用少量樣本(適用於使用並行網路圖的GPU視訊記憶體記憶體)仍然可以正常工作的一些辦法。至此,我們瞭解了畫素級特徵歸一化的需求從何而來以及為什麼要進行畫素級特徵歸一化。

如果從演算法角度說,畫素級歸一化將在輸入被送到下一層之前在每一層獲得啟用幅度。

畫素級特徵歸一化
對每個特徵圖,執行

  • 在位置(x,y)上獲取該特徵圖(fm)的畫素值。

  • 為每個(x,y)構造一個向量,其中

    • a. \(v_{0, 0}=[fm_1的(0, 0)值,fm_2的(0, 0)值,...,fm_n的(0, 0)]值\)​。

    • b. \(v_{0, 1}=[fm_1的(0, 1)值,fm_2的(0, 1)值,...,fm_n的(0, 1)]值\)

      ...

    • c. \(v_{n, n}=[fm_1的(n, n)值,fm_2的(n, n)值,...,fm_n的(n, n)]值\)

  • 將步驟2中定義的每個向量\(v_{i,i}\)歸一化得到單位標準值,稱之為\(n_{i,i}\)

  • 將原來的張量形狀傳遞到下一層。

結束

畫素特徵歸一化的過程如下圖所示。將影像中的所有點(步驟1)對映到一組向量(步驟2),然後對其進行歸一化,以使它們都在同一範圍內(通常在高維空間中介於0和1之間),這就是步驟3

image

步驟3的準確描述如下式所示。

\[n_{(x, y)}=a_{(x, y)} / \sqrt{\frac{1}{N} \sum_{j=0}^{N-1}\left(a_{x, y}^{\prime}\right)^{2}+\varepsilon} \]

上式將圖中步驟2中構造的每個向量歸ー化(除以根號下的表示式)。該表示式只是特定(x,y)畫素每個平方值的平均值。另外增加了一個小的噪聲項(\(\varepsilon\))。這只是確保不被零除的一種方法。

最後要注意的是,畫素級特徵歸一化這一技巧僅用於生成器,因為兩個網路都使用時,啟用幅度的爆炸會導致軍備競賽(預防式的對抗)。

def pixelwise_feat_norm(inputs, **kwargs):
    normalization_constant = K.backend.sqrt(K.backend.mean(
    inputs**2, axis=-1, keepdims=True) + 1.0e-8)
    return inputs / normalization_constant

三、TensorFlow Hub庫及其實踐

匯入hub模組並呼叫正確的URL後, Tensorflow會自行下載並匯入模型,然後就可以開始使用了。這些模型在用於下載模型的同一個URL中有很好的文件記錄,只需將它們輸入測覽器中即可。實際上,要獲得經過預訓練的 PGGAN,輸入一個 import語句和一行程式碼即可。

以下程式碼給出的是一個完整的程式碼示例,該程式碼根據latent_ vector中指定的隨機種子生成一張人臉。輸出如下圖所示。

import matplotlib.pyplot as plt 
import tensorflow as tf
#先安裝pip install tensorflow_datasets -i https://pypi.douban.com/simple
#再安裝pip install tensorflow_hub -i https://pypi.douban.com/simple
import tensorflow_hub as hub
import os
import getpass

#下載位置預設為本地臨時目錄
#但可以通過設定環境變數 TFHUB_CACHE_DIR進行自定義
os.environ["TFHUB_CACHE_DIR"] = "E:\keras\TFhub"

with tf.Graph().as_default():
    module_url = 'E:./progan-128_1'#從本地中匯入PGGAN
    print("Loading model from {}".format(module_url))
    module = hub.Module(module_url)
    latent_dim = 512#執行時取樣的潛在維度
    
    
    latent_vector = tf.random.normal([1, latent_dim], seed=222)#改變種子得到不同人臉
    interpolated_image = module(latent_vector)#使用該模組從潛在空間生成影像
    #執行tf session得到(1, 128, 128, 3)的影像
    with tf.compat.v1.Session() as session:
        session.run(tf.compat.v1.global_variables_initializer())
        image_out = session.run(interpolated_image)
plt.imshow(image_out.reshape(128, 128, 3))
plt.show()

image

四、小結

  1. 藉助最先進的 PGGAN技術,我們可以實現百萬畫素的合成影像。
  2. PGGAN技術具有4個關鍵的訓練創新。
    • 漸進式和平滑地增大高解析度的層。
    • 小批處理標準偏差,以強制所生成的樣本多樣化。
    • 均衡學習率,確保在每個方向上採取適當大小的學習步驟。
    • 畫素特徵向量歸一化,確保生成器和判別器在竟爭中不會失控。

相關文章