三大深度學習生成模型:VAE、GAN及其變種

Candy_GL發表於2018-09-03

版權宣告:本文為博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/heyc861221/article/details/80130968

編者按:本書節選自圖書《深度學習輕鬆學》第十章部分內容,書中以輕鬆直白的語言,生動詳細地介紹了深層模型相關的基礎知識,並深入剖析了演算法的原理與本質。同時還配有大量案例與原始碼,幫助讀者切實體會深度學習的核心思想和精妙之處。

本章將為讀者介紹基於深度學習的生成模型。前面幾章主要介紹了機器學習中的判別式模型,這種模型的形式主要是根據原始影象推測影象具備的一些性質,例如根據數字影象推測數字的名稱,根據自然場景影象推測物體的邊界;而生成模型恰恰相反,通常給出的輸入是影象具備的性質,而輸出是性質對應的影象。這種生成模型相當於構建了影象的分佈,因此利用這類模型,我們可以完成影象自動生成(取樣)、影象資訊補全等工作。

在深度學習之前已經有很多生成模型,但苦於生成模型難以描述難以建模,科研人員遇到了很多挑戰,而深度學習的出現幫助他們解決了不少問題。本章就介紹基於深度學習思想的生成模型——VAE和GAN,以及GAN的變種模型。

VAE

本節將為讀者介紹基於變分思想的深度學習的生成模型——Variational autoencoder,簡稱VAE。

生成式模型

前面的章節裡讀者已經看過很多判別式模型。這些模型大多有下面的規律:已知觀察變數X,和隱含變數z,判別式模型對p(z|X)進行建模,它根據輸入的觀察變數x得到隱含變數z出現的可能性。生成式模型則是將兩者的順序反過來,它要對p(X|z)進行建模,輸入是隱含變數,輸出是觀察變數的概率。

可以想象,不同的模型結構自然有不同的用途。判別模型在判別工作上更適合,生成模型在分佈估計等問題上更有優勢。如果想用生成式模型去解決判別問題,就需要利用貝葉斯公式把這個問題轉換成適合自己處理的樣子:

圖片描述

對於一些簡單的問題,上面的公式還是比較容易解出的,但對於一些複雜的問題,找出從隱含變數到觀察變數之間的關係是一件很困難的事情,生成式模型的建模過程會非常困難,所以對於判別類問題,判別式模型一般更適合。

但對於“隨機生成滿足某些隱含變數特點的資料”這樣的問題來說,判別式模型就會顯得力不從心。如果用判別式模型生成資料,就要通過類似於下面這種方式的方法進行。

第一步,利用簡單隨機一個X。

第二步,用判別式模型計算p(z|X)概率,如果概率滿足,則找到了這個觀察資料,如果不滿足,返回第一步。

這樣用判別式模型生成資料的效率可能會十分低下。而生成式模型解決這個問題就十分簡單,首先確定好z的取值,然後根據p(X|z)的分佈進行隨機取樣就行了。 
瞭解了兩種模型的不同,下面就來看看生成式模型的建模方法。

Variational Lower bound

雖然生成模型和判別模型的形式不同,但兩者建模的方法總體來說相近,生成模型一般也通過最大化後驗概率的形式進行建模優化。也就是利用貝葉斯公式:

圖片描述

這個公式在複雜的模型和大規模資料面前極難求解。為了解決這個問題,這裡將繼續採用變分的方法用一個變分函式q(z)代替p(z|X)。第9章在介紹Dense CRF時已經詳細介紹了變分推導的過程,而這一次的推導並不需要做完整的變分推導,只需要利用變分方法的下界將問題進行轉換即可。

既然希望用q(z)這個新函式代替後驗概率p(z|X),那麼兩個概率分佈需要儘可能地相近,這裡依然選擇KL散度衡量兩者的相近程度。根據KL公式就有:

圖片描述

根據貝葉斯公式進行變換,就得到了:

圖片描述

由於積分的目標是z,這裡再將和z無關的專案從積分符號中拿出來,就得到了:

圖片描述

將等式左右專案交換,就得到了下面的公式:

圖片描述

雖然這個公式還是很複雜,因為KL散度的性質,這個公式中還是令人看到了一絲曙光。

首先看等號左邊,雖然p(X)的概率分佈不容易求出,但在訓練過程中當X已經給定,p(X)已經是個固定值不需要考慮。如果訓練的目標是希望KL(q(z)||p(z|X))儘可能小,就相當於讓等號右邊的那部分儘可能變大。等號右邊的第一項實際上是基於q(z)概率的對數似然期望,第二項又是一個負的KL散度,所以我們可以認為,為了找到一個好的q(z),使得它和p(z|X)儘可能相近,實現最終的優化目標,優化的目標將變為:

  • 右邊第一項的log似然的期望最大化:

圖片描述

  • 右邊第二項的KL散度最小化:

圖片描述

右邊兩個專案的優化難度相對變小了一些,下面就來看看如何基於它們做進一步的計算。

Reparameterization Trick

為了更方便地求解上面的公式,這裡需要做一點小小的trick工作。上面提到了q(z)這個變分函式,為了近似後驗概率,它實際上代表了給定某個X的情況下z的分佈情況,如果將它的概率形式寫完整,那麼它應該是q(z|X)。這個結構實際上對後面的運算產生了一些障礙,那麼能不能想辦法把X抽離出來呢?

例如,有一個隨機變數a服從均值為1,方差為1的高斯分佈,那麼根據高斯分佈的性質,隨機變數b=a-1將服從均值為0,方差為1的高斯分佈,換句話說,我們可以用一個均值為0,方差為1的隨機變數加上一個常量1來表示現在的隨機變數a。這樣一個隨機變數就被分成了兩部分——一部分是確定的,一部分是隨機的。

實際上,q(z|X)也可以採用上面的方法完成。這個條件概率可以拆分成兩部分,一部分是一個觀察變數g?(X),它代表了條件概率的確定部分,它的值和一個隨機變數的期望值類似;另一部分是隨機變數ε,它負責隨機的部分,基於這樣的表示方法,條件概率中的隨機性將主要來自這裡。

這樣做有什麼好處呢?經過變換,如果z條件概率值完全取決於ε的概率。也就是說如果z(i)=g?(X+ε(i)),那麼q(z(i))=p(ε(i)),那麼上面關於變分推導的公式就變成了下面的公式:

圖片描述

這就是替換的一小步,求解的一大步!這個公式已經很接近問題最終的答案了,既然?完全決定了z的分佈,那麼假設一個?服從某個分佈,這個變分函式的建模就完成了。如果?服從某個分佈,那麼z的條件概率是不是也服從這個分佈呢?不一定。z的條件分佈會根據訓練資料進行學習,由於經過了函式g?()的計算,z的分佈有可能產生了很大的變化。而這個函式,就可以用深度學習模型表示。前面的章節讀者已經瞭解到深層模型的強大威力,那麼從一個簡單常見的隨機變數對映到複雜分佈的變數,對深層模型來說是一件很平常的事情,它可以做得很好。

於是這個假設?服從多維且各維度獨立高斯分佈。同時,z的先驗和後驗也被假設成一個多維且各維度獨立的高斯分佈。下面就來看看兩個優化目標的最終形式。

Encoder和Decoder的計算公式

回顧一下10.1.2的兩個優化目標,下面就來想辦法求解這兩個目標。首先來看看第二個優化目標,也就是讓公式右邊第二項KL(q(z)||p(z))最小化。剛才z的先驗被假設成一個多維且各維度獨立的高斯分佈,這裡可以給出一個更強的假設,那就是這個高斯分佈各維度的均值為0,協方差為單位矩陣,那麼前面提到的KL散度公式就從:

圖片描述

瞬間簡化成為:

圖片描述

前面提到了一個用深層網路實現的模型g?(X,?),它的輸入是一批影象,輸出是z,因此這裡需要它通過X生成z,並將這一個批次的資料彙總計算得到它們的均值和方差。這樣利用上面的公式,KL散度最小化的模型就建立好了。

實際計算過程中不需要將協方差表示成矩陣的形狀,只需要一個向量σ1來表示協方差矩陣的主對角線即可,公式將被進一步簡化:

圖片描述

由於函式g?()實現了從觀測資料到隱含資料的轉變,因此這個模型被稱為Encoder模型。

接下來是第一個優化目標,也就是讓公式左邊第一項的似然期望最大化。這一部分的內容相對簡單,由於前面的Encoder模型已經計算出了一批觀察變數X對應的隱含變數z,那麼這裡就可以再建立一個深層模型,根據似然進行建模,輸入為隱含變數z,輸出為觀察變數X。如果輸出的影象和前面生成的影象相近,那麼就可以認為似然得到了最大化。這個模型被稱為Decoder,也就是本章的主題——生成模型。

到這裡VAE的核心計算推導就結束了。由於模型推導的過程有些複雜,下面就來看看VAE實現的程式碼,同時來看看VAE模型生成的影象是什麼樣子。

實現

本節要介紹VAE模型的一個比較不錯的實現——GitHub - cdoersch/vae_tutorial: Caffe code to accompany my Tutorial on Variational Autoencoders(https://link.zhihu.com/?target=https%3A//github.com/cdoersch/vae_tutorial),這個工程還配有一個介紹VAE的文章[2],感興趣的讀者可以閱讀,讀後會有更多啟發。這個實現使用的目標資料集依然是MNIST,模型的架構如圖10-1所示。為了更好地瞭解模型的架構,這裡將模型中的一些細節隱去,只留下核心的資料流動和Loss計算部分。

圖片描述

圖10-1 VAE模型結構圖

 

圖中粗框表示求解Loss的部分。虛線展現了兩個模組之間資料共享的情況。可以看出圖的上半部分是優化Encoder的部分,下面是優化Decoder的部分,除了Encoder和Decoder,圖中還有三個主要部分。

  • Encoder的Loss計算:KL散度。
  • z的重取樣生成。
  • Decoder的Loss計算:最大似然。

這其中最複雜的就是第一項,Encoder的Loss計算。由於Caffe在實際計算過程中只能採用向量的計算方式,沒有廣播計算的機制,所以前面的公式需要進行一定的變換:

圖片描述

在完成了前面的向量級別計算後,最後一步就是完成彙總加和的過程。這樣Loss計算就順利完成了。

經過上面對VAE理論和實驗的介紹,相信讀者對VAE模型有了更清晰的認識。經過訓練後VAE的解碼器在MNIST資料庫上生成的字元如圖10-2所示。

圖片描述

圖10-2 VAE生成的數字圖

 

MNIST生成模型視覺化

除了直接觀察最終生成的數字結果,實際上還有另一種觀察資料的方式,那就是站在隱變數空間的角度觀察分佈的生成情況。實現這個效果需要完成以下兩個工作。

  1. 隱變數的維度為2,相當於把生成的數字圖片投影到2維平面上,這樣更方便視覺化觀察分析。
  2. 由於隱變數的維度為2,就可以從二維平面上等間距地取樣一批隱變數,這樣這批隱變數可以代表整個二維平面上隱變數的分佈,然後這批隱變數經過解碼器處理後展示,這樣就可以看到影象的分佈情況了。

上面描述的演算法的流程如圖10-3所示。

圖片描述

圖10-3 模型視覺化流程圖。上圖主要標識了模型的修改部分,下圖介紹隱變數取樣和生成的形式

 

圖片描述

圖10-4 模型視覺化結果

 

圖10-4所示的模型很好地完成了隱變數的建模,絕大多數數字出現在了這個平面分佈中,數字與數字一些過渡區域,這些過渡區域的影象擁有多個數字的特徵,而這些數字的外形確實存在著相似之處。可以明顯地感受到,影象隨著隱變數變換產生了變換。

VAE的內容就介紹到這裡,下面來看看另一個生成模型。

GAN

前面我們介紹了VAE,下面來看看GAN(Generative Adversarial Network)[3],這個網路組是站在對抗博弈的角度展現生成模型和判別模型各自的威力的,在效果上比VAE還要好些。

GAN的概念

同VAE模型類似,GAN模型也包含了一對子模型。GAN的名字中包含一個對抗的概念,為了體現對抗這個概念,除了生成模型,其中還有另外一個模型幫助生成模型更好地學習觀測資料的條件分佈。這個模型可以稱作判別模型D,它的輸入是資料空間內的任意一張影象x,輸出是一個概率值,表示這張影象屬於真實資料的概率。對於生成模型G來說,它的輸入是一個隨機變數z,z服從某種分佈,輸出是一張影象G(z),如果它生成的影象經過模型D後的概率值很高,就說明生成模型已經比較好地掌握了資料的分佈模式,可以產生符合要求的樣本;反之則沒有達到要求,還需要繼續訓練。

兩個模型的目標如下所示。

  • 判別模型的目標是最大化這個公式:Ex[D(x)],也就是甄別出哪些圖是真實資料分佈中的。
  • 生成模型的目標是最大化這個公式:Ez[D(G(z))],也就是讓自己生成的圖被判別模型判斷為來自真實資料分佈。

看上去兩個模型目標聯絡並不大,下面就要增加兩個模型的聯絡,如果生成模型生成的影象和真實的影象有區別,判別模型要給它判定比較低的概率。這裡可以舉個形象的例子,x好比是一種商品,D是商品的檢驗方,負責檢驗商品是否是正品;G是一家山寨公司,希望根據拿到手的一批產品x研究出生產山寨商品x的方式。對於D來說,不管G生產出來的商品多像正品,都應該被判定為贗品,更何況一開始G的技術水品不高,生產出來的產品必然是漏洞百出,所以被判定為贗品也不算冤枉,只有不斷地提高技術,才有可能迷惑檢驗方。

基於上面的例子,兩個模型的目標就可以統一成一個充滿硝煙味的目標函式。

圖片描述

上面這個公式對應的模型架構如圖10-5所示。

圖片描述

圖10-5 GAN的基本形式

 

對應的模型學習演算法虛擬碼如下所示:

def GAN(G,D,X):
    # G 表示生成模型
    # D 表示判別模型
    # X 表示訓練資料
    for iter in range(MAX_ITER):
        for step in range(K):
            x = data_sample(X)
            z = noise_sample()
            optimize_D(G, D, x, z)
        z = noise_sample()
        optimize_G(G, D, z)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

上面的程式碼只是從巨集觀的層面介紹了模型的優化方法,其中K表示了判別模型D的迭代次數,K一般大於等於1。從上面的公式可以看出,兩個模型的目標是對立的。生成模型希望最大化自己生成影象的似然,判別模型希望最大化原始資料的似然的同時,能夠最小化G生成的影象的似然。既然是對立的,那麼兩個模型經過訓練產生的能力就可能有很多種情況。它們既可能上演“魔高一尺,道高一尺”,“道高一丈,魔高十丈”的競爭戲碼,在競爭中共同成長,最終產生兩個強大的模型;也可能產生一個強大的模型,將另一方完全壓倒。

如果判別模型太過強大,那麼生成模型會產生兩種情況:一種情況是發現自己完全被針對,模型引數無法優化;另外一種情況是發現判別模型的一些漏洞後,它的模型將退化,不管輸入是什麼樣子,輸出統一變成之前突破了判別模型防線的那幾類結果。這種情況被稱為“Mode Collapse”,有點像一個複雜強大的模型崩塌成一個簡單弱小的模型,這樣的模型即使優化結果很好,也不能拿去使用。

如果判別模型不夠強大,它的判別不夠精準,而生成模型又是按照它的判別結果生產,那麼生產出的產品不會很穩定,這同樣不是我們想看到的結果。

總而言之,對抗是GAN這個模型要面對的一個大問題。雖然論文中作者試圖將兩個模型共同優化的問題轉換成類似Coordinate Ascent那樣的優化問題,並證明像Coordinate Ascent這樣的演算法可以收斂,那麼GAN這個模型也可以。不過作者在完成證明後立刻翻臉,說證明結果和實驗結果不符。所以這個問題在當時也就變成了一個懸案。

GAN的訓練分析

關於GAN訓練求解的過程,作者用了十分數學化的方式進行了推演。我們首先來證明第一步:當生成模型固定時,判別模型的最優形式。

首先將目標函式做變換:

圖片描述

由於組成式子的兩部分積分的區域不同,會對後面的計算造成困難,我們首先將兩個積分割槽域統一。我們將生成影象G(z)的分佈與真實影象x的分佈做一個投射,只要判別式能夠在真實資料出現的地方保證判別正確最大化即可,於是公式就變成了:

圖片描述

只要讓積分內部的公式最大化,整個公式就可以實現最大化。這樣問題就轉變為最大化下面的公式:

圖片描述

對它進行求導取極值,可以得到:

圖片描述

令上面的式子為0,我們可以得到結果:

圖片描述

這就是理論上判別式的預測結果,如果一張影象在真實分佈中出現的概率大而在生成分佈中出現的概率小,那麼最優的判別模型會認為它是真實影象,反之則認為不是真實影象。如果生成模型已經達到了完美的狀態,也就是說對每一幅影象都有:

圖片描述

接下來就可以利用上面的結果,計算當生成模型達到完美狀態時,損失函式的值。我們將D*(x)=1/2的結果代入,可以得到:

圖片描述

也就是說生成模型損失函式的理論最小值為-2log2。那麼,一般情況下它的損失函式是什麼樣子呢?我們假設在某一時刻判別式經過優化已經達到最優,所以

圖片描述

我們將這個公式代入之前的公式,可以得到:

圖片描述

後面的兩個KL散度的計算公式可以轉化為Jenson-Shannon散度,也就是:

圖片描述

這其實是生成模型真正的優化目標函式。在介紹VAE時,讀者已經瞭解了KL散度,也瞭解了它的一些基本知識,那麼這個JS散度又是什麼?它又有什麼特性和優勢?從最直觀的角度,讀者可以發現一個KL散度不具備的性質——JS散度是對稱的:

圖片描述

對稱又能帶來什麼好處呢?它能讓散度度量更準確。接下來將用一段程式碼展示這其中的道理。首先給出兩個離散隨機變數的KL散度和JS散度的計算方法:

import numpy as np
import math
def KL(p, q):
    # p,q為兩個list,裡面存著對應的取值的概率,整個list相加為1
    if 0 in q:
        raise ValueError
    return sum(_p * math.log(_p/_q) for (_p,_q) in zip(p, q) if _p != 0)

def JS(p, q):
    M = [0.5 * (_p + _q) for (_p, _q) in zip(p, q)]
    return 0.5 * (KL(p, M) + KL(q, M))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

下面將用3組實驗看看兩個散度的計算結果。首先選定一個簡單的離散分佈,然後求出它的KL散度和JS散度。在此基礎上,把兩個分佈分別做一定的調整。首先是基礎的分佈:

def exp(a, b):
    a = np.array(a, dtype=np.float32)
    b = np.array(b, dtype=np.float32)
    a /= a.sum()
    b /= b.sum()
    print a
    print b
    print KL(a,b)
    print JS(a,b)

# exp 1
exp([1,2,3,4,5],[5,4,3,2,1])
#以下為執行結果顯示
[ 0.066  0.133  0.2         0.266  0.333]
[ 0.333  0.266  0.2         0.133  0.066]
0.521
0.119
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

接下來把公式中第二個分佈做修改,假設這個分佈中有某個值的取值非常小,就有可能增加兩個分佈的散度值,它的程式碼如下所示:

# exp 2
exp([1,2,3,4,5],[1e-12,4,3,2,1])
exp([1,2,3,4,5],[5,4,3,2,1e-12])
#以下為執行結果顯示
[ 0.066  0.133  0.2         0.266  0.333]
[ 9.999e-14   4.000e-01   3.000e-01   2.000e-01   1.000e-01]
2.06550201846
0.0985487692551

[ 0.066  0.133  0.2         0.266  0.333]
[ 3.571e-01   2.857e-01   2.142e-01   1.428e-01   7.142e-14]
9.662
0.193
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

可以看出KL散度的波動比較大,而JS的波動相對小。

最後修改前面的分佈,程式碼如下所示:

# exp 3
exp([1e-12,2,3,4,5],[5,4,3,2,1])
exp([1,2,3,4,1e-12],[5,4,3,2,1])
  • 1
  • 2
  • 3
  • 4

這回得到的結果是這樣的:

[ 7.142e-14   1.428e-01   2.142e-01   2.857e-01   3.571e-01]
[ 0.333  0.266  0.2         0.133  0.0666]
0.742
0.193
[ 1.000e-01   2.000e-01   3.000e-01   4.000e-01   9.999e-14]
[ 0.333  0.266  0.2         0.133  0.066]
0.383
0.098
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

如果將第二個實驗和第三個實驗做對比,就可以發現KL散度在衡量兩個分佈的差異時具有很大的不對稱性。如果後面的分佈在某一個值上缺失,就會得到很大的散度值;但是如果前面的分佈在某一個值上缺失,最終的KL散度並沒有太大的波動。這個例子可以很清楚地看出KL不對稱性帶來的一些小問題。而JS具有對稱性,所以第二個實驗和第三個實驗的JS散度實際上是距離相等的分佈組。

從這個小例子我們可以看出,有時KL散度下降的程度和兩個分佈靠近的程度不成比例,而JS散度靠近的程度更令人滿意,這也是GAN模型的一大優勢。

GAN實戰

看完了前面關於GAN的理論分析,下面我們開始實戰。在實戰之前目標函式還要做一點改動。從前面的公式中可以看出這個模型和VAE一樣都是有巢狀關係的模型,那麼生成模型G要想完成前向後向的計算,要先將計算結果傳遞到判別模型計算損失函式,然後將梯度反向傳播回來。那麼不可避免地我們會遇到一個問題,如果梯度在判別模型那邊消失了,生成模型豈不是沒法更新了?生成模型的目標函式如下所示:

圖片描述

如果判別模型非常厲害,成功地讓D(G(z))等於一個接近0的數字,那麼這個損失函式的梯度就消失了。其實從理論上分析這個結果很正常,生成模型的梯度只能從判別模型這邊傳過來,這個結果接近0對於判別模型來說是滿意的,所以它不需要更新,梯度就沒有了,於是生成模型就沒法訓練了。所以作者又設計了新的函式目標:

圖片描述

這樣一來梯度又有了,生成模型也可以繼續訓練。當然,這個目標函式也有不足的地方。

下面來看一個具體的基於深層模型的實現——DC-GAN。全稱是Deep Convolution GAN。也就是用深度卷積網路進行對抗生成網路的建模。在此之前,也有一些基於卷積神經網路的GAN實現,但是相對來說,DC-GAN的最終表現與同期的模型相比更優秀,在介紹它的論文中,作者也詳細介紹了模型的一些改進細節。

  • 將Pooling層替換成帶有stride的卷積層
  • 使用Batch Normalization
  • 放棄使用全連線層
  • 將卷積層的非線性部分換成ReLU或者Leaky ReLU

下面將使用DC-GAN的模型進行實驗,這個實驗使用的資料集還是MNIST。由於Caffe並不是十分適合構建GAN這樣的模型,因此這裡使用另外一個十分流行且簡單易懂的框架——Keras來展示DC-GAN的一些細節。程式碼來自https://github.com/jacobgil/keras-dcgan。由於Keras的程式碼十分直觀,這裡就直接給出原始碼。首先是生成模型:

def generator_model():
    model = Sequential()
    model.add(Dense(input_dim=100, output_dim=1024))
    model.add(Activation('tanh'))
    model.add(Dense(out_dim=128*7*7))
    model.add(BatchNormalization())
    model.add(Activation('tanh'))
    model.add(Reshape((128, 7, 7), input_shape=(128*7*7,)))
    model.add(UpSampling2D(size=(2, 2)))
    model.add(Convolution2D(out_channel=64, kernel_height=5, kernel_width=5, border_mode='same'))
    model.add(Activation('tanh'))
    model.add(UpSampling2D(size=(2, 2)))
    model.add(Convolution2D(out_channel=1, kernel_height=5, kernel_width=5, border_mode='same'))
    model.add(Activation('tanh'))
    return model
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

這裡需要說明的一點是,這個實現和論文中的描述有些不同,不過對於MNIST這樣的小資料集,這樣的模型差異不影響效果。

判別模型的結構如下所示,仔細地讀一遍就可以理解,這裡不再贅述。

def discriminator_model():
    model = Sequential()
    model.add(Convolution2D(64, 5, 5, border_mode='same', input_shape=(1, 28, 28)))
    model.add(Activation('tanh'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Convolution2D(128, 5, 5))
    model.add(Activation('tanh'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Flatten())
    model.add(Dense(1024))
    model.add(Activation('tanh'))
    model.add(Dense(1))
    model.add(Activation('sigmoid'))
    return model
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

完成訓練後,生成模型生成的手寫數字如圖10-6所示。

圖片描述

圖10-6 GAN生成的影象

 

除了個別數字外,大多數數字生成得和真實資料很像。將圖10-6和圖10-2進行對比,我們可以發現,GAN模型生成的數字相對而言更為“清晰”,而VAE模型的數字略顯模糊,這和兩個模型的目標函式有很大的關係。另外,兩個模型在訓練過程中的Loss曲線如圖10-7所示。

圖片描述

圖10-7 GAN中生成模型和判別模型的損失函式

 

其中上面的曲線表示生成模型的Loss,下面的曲線是判別模型的Loss,雖然這兩個Loss的絕對數值看上去不能說明什麼問題,但是相信讀者還是可以看出兩個模型的Loss存在著強相關的關係,這也算是對抗過程中的此消彼長。

最終生成的資料還算令人滿意,我們還很好奇,在模型優化過程中生成模型生成的影象都是什麼樣的呢?接下來就來觀察生成影象的演變過程。在優化開始時,隨機生成的影象如圖10-8所示。

圖片描述

圖10-8 GAN生成模型的初始影象

 

其實就是噪聲圖片,一點都不像數字。經過400輪的迭代,生成模型可以生成的影象如圖10-9所示。

圖片描述

圖10-9 GAN生成模型400輪迭代訓練後的影象

 

可以看出數字的大體結構已經形成,但是能夠表徵數字細節的特徵還沒有出現。 
經過10個Epoch後,生成模型的作品如圖10-10所示。

圖片描述

圖10-10 GAN生成模型經過10個Epoch迭代訓練後的影象

 

這時有些數字已經成形,但是還有一些數字仍然存在欠缺。

20輪Epoch後的結果如圖10-11所示。

圖片描述

圖10-11 GAN生成模型經過20個Epoch迭代訓練後的影象

 

這時的數字已經具有很強的辨識度,但與此同時,我們發現生成的數字中有大量的“1”。

當完成了所有的訓練,取出生成模型在最後一輪生成的影象,如圖10-12所示。

圖片描述

圖10-12 GAN生成模型最終生成的影象

 

可以看出這裡面的數字質量更高一些,但是裡面的“1”更多了。

從模型的訓練過程中可以看出,一開始生成的數字質量都很差,但生成數字的多樣性比較好,後來的數字質量比較高但數字的多樣性逐漸變差,模型的特性在不斷髮生變化。這個現象和兩個模型的對抗有關係,也和增強學習中的“探索—利用”困境很類似。

站在生成模型的角度思考,一開始生成模型會盡可能地生成各種各樣形狀的數字,而判別模型會識別出一些形狀較差的模型,而放過一些形狀較好的模型,隨著學習的程式不斷推進,判別模型的能力也在不斷地加強,生成模型慢慢發現有一些固定的模式比較容易通過,而其他的模式不那麼容易通過,於是它就會盡可能地增大這些正確模式出現的概率,讓自己的Loss變小。這樣,一個從探索為主的模型變成了一個以利用為主的模型,因此它的資料分佈已經不像剛開始那麼均勻了。

如果這個模型繼續訓練下去,生成模型有可能進一步地利用這個模式,這和機器學習中的過擬合也有很相近的地方。

Info-GAN

本節將要介紹GAN模型的一個變種——InfoGAN,它要解決隱變數可解釋性的問題。前面提到GAN的隱變數服從某種分佈,但是這個分佈背後的含義卻不得而知。雖然經過訓練的GAN可以生成新的影象,但是它卻無法解決一個問題——生成具有某種特徵的影象。例如,對於MNIST的資料,生成某個具體數字的影象,生成筆畫較粗、方向傾斜的影象等,這時就會發現經典的GAN已經無法解決這樣的問題,想要解決就需要想點別的辦法。

首先想到的方法就是生成模型建模的方法:挑出幾個隱變數,強制指定它們用來表示這些特性的屬性,例如數字名稱和方向。這樣看上去似乎沒有解決問題,但這種方法需要提前知道可以建模的隱變數內容,還要為這些隱變數設定好獨立的分佈假設,實際上有些麻煩又不夠靈活。本節的主角——InfoGAN,將從資訊理論角度,嘗試解決GAN隱變數可解釋性問題。

互資訊

介紹演算法前要簡單回顧機器學習中的資訊理論基本知識。第2章已經介紹了熵和“驚喜度”這些概念,熵衡量了一個隨機變數帶來的“驚喜度”。本節要介紹的概念叫做互資訊,它衡量了隨機變數之間的關聯關係。假設隨機事件A的結果已經知道,現在要猜測某個事件B的結果,那麼知道A的取值對猜測B有多大幫助?這就是互資訊要表達的東西。

我們以擲骰子為例,如果我們知道手中的骰子是不是“韋小寶特製”骰子這件事,那麼它會對我們猜測最終投擲的點數有幫助嗎?當然有幫助,因為一旦確定這個骰子是“韋小寶特製”,那麼骰子點數是幾這個資訊就變得沒有“驚喜”了。同理,“美國第45屆總統是誰”這個訊息對我們手中骰子投擲出的點數這個事情就沒那麼多幫助了,所以這兩件事情的互資訊就低,甚至可以說這兩個事件是相互獨立的。

瞭解了上面比較直觀的例子,下面就可以給出連續隨機變數X,Y互資訊的計算公式:

圖片描述

上面的公式可以做如下變換:

圖片描述

就可以發現互資訊的進一步解釋:它可以變為熵和條件熵的差。同樣地,這個公式還可以轉變為:I(X;Y)=H(X)-H(X|Y)

最終表示為熵和條件熵的差距。用通俗的話解釋,兩個隨機變數的互資訊就是在知道和不知道一個隨機變數取值的情況下,另一個隨機變數“驚喜度”的變化。互資訊的計算方法的程式碼如下所示:

import numpy as np
import math

def mutual_info(x_var, y_var):
    sum = 0.0
    x_set = set(x_var)
    y_set = set(y_var)
    for x_val in x_set:
        px = float(np.sum(x_var == x_val)) / x_var.size
        x_idx = np.where(x_var == x_val)[0]
        for y_val in y_set:
            py = float(np.sum(y_var == y_val)) / y_var.size
            y_idx = np.where(y_var == y_val)[0]
            pxy = float(np.intersect1d(x_idx, y_idx).size) / x_var.size
            if pxy > 0.0:
                sum += pxy * math.log((pxy / (px * py)), 10)
    return sum
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

下面隨意給出一對隨機變數和它們的概率分佈,並用上面的程式碼分析這對變數的互資訊:

a = np.array([0,0,5,6,0,4,4,3,1,2])
b = np.array([3,4,5,5,3,7,7,6,5,1])
print mutual_info(a,b)
# 0.653

a = np.array([0,0,5,6,0,4,4,3,1,2])
b = np.array([3,3,5,6,3,7,7,9,4,8])
print mutual_info(a,b)
# 0.796
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

很明顯,下面一組資料的相關性更強,知道其中一個隨機變數的取值,就會非常容易猜出同一時刻另外一個隨機變數的取樣值。如果我們進一步觀察第二組資料,會發現任意一組資料的熵都是0.796,也就是說當知道其中一個隨機變數的值後,它們的條件熵就變成了0,另一個隨機變數變得完全“驚喜”了。雖然條件熵為0這個資訊並沒有展現在互資訊的數值中,但互資訊實際上就是在衡量一個相對的資訊差距,並不像熵那樣衡量資訊絕對量。

其實數學包含了很多人生哲理和智慧。人的一生實際上一直在和熵作鬥爭,每個人的人生軌跡的熵意味著什麼?一個人未來的不確定性?一個人未來的“驚喜”程度?有的人說自己“一輩子也就這樣了”的時候,是不是表示這個人的未來已經從一個隨機變數變成了常量,它的熵變成了0?為什麼人們總是嚮往青春,是不是因為那些年華充滿了各種不確定性與精彩,可以理解為熵很大?

“身體和靈魂,總有一個在路上”,是不是標榜追求最大熵的一個口號?“公務員這種穩定工作才是好工作”是不是一種追求最小化熵的行為呢?那麼對於一個人來說,究竟是熵越大越好,還是熵越小越好?

回到問題,互資訊在這個問題中有什麼用?如果說隱變數的確定對確定生成影象的樣子有幫助,那麼隱變數和最終的影象之間的互資訊就應該很大:當知道了隱變數這個資訊,影象的資訊對變得更確定了。所以InfoGAN這個演算法就是要通過約束互資訊使隱變數“更有價值”。

InfoGAN模型

那麼,InfoGAN模型的具體形式是什麼樣的呢?如果把互資訊定義為損失函式的一部分,這部分損失函式就是InfoGAN中基於經典GAN修改的部分。前面的小節已經推匯出了互資訊的公式,那麼在具體計算時要使用哪個公式計算呢?

  • I(X;Z)=H(X)-H(X|Z)
  • I(X;Z)=H(Z)-H(Z|X)

最終的選擇是後者,因為影象X的分佈太難確定,求解它的熵肯定相當困難,所以前者的第一項非常難計算。當然,即使選擇了第二項,這個公式也不是很好優化,因為其中還有一個後驗項P(Z|X)需要求解,這也是個大麻煩,不過這裡可以使用本書多次提到的方法——Variational Inference求解這個後驗項。

在介紹VAE時我們曾經運用過Reparameterization Trick這個方法,這裡將再次採用類似的方法。在VAE中,Trick公式是z(i)=g?(X+ε(i)),在Encoder的過程中,輸入部分被分解成確定部分和不確定部分,然後利用一個高維非線性模型擬合輸入到輸出的對映。這裡要求出的X,和VAE正好相反,需要的是這樣的一個公式:X=g?([c,z]+?)

其中c表示與影象有相關關係的隱變數,z表示與影象無關的隱變數。於是互資訊計算公式就變成了:

圖片描述

從實踐上講,?項可以忽略,於是公式可以做進一步簡化:

圖片描述

接下來將期望用蒙特卡羅方法代替,訓練時可以通過計算大量樣本求平均來代替期望值,於是公式又變成了:

圖片描述

這個方程變簡單了很多。當然,我們也看出上面的公式中我們有一個Q,這個Q函式可以理解為一個Encoder,這部分模型在經典GAN中並不存在,但是在實際建模過程中,由於Encoder和判別模型的輸入相同,且模型目標比較相近,因此二者部分網路結構可以共享。論文的作者提供了InfoGAN的原始碼,程式碼的連結在https://github.com/openai/InfoGAN,程式碼使用的框架為TensorFlow,感興趣的讀者可以自行閱讀。模型實現的結構如圖10-13所示。

圖片描述

圖10-13 InfoGAN模型結構圖

 

虛線部分表示的就是計算互資訊的目標函式,這部分內容看似比較複雜,實則不然。由於InfoGAN模型中定義了兩種型別的隨機變數——服從Categorical分佈、用於表示數字內容的離散型別變數,和服從均勻分佈用於表示其他連續特徵的連續型變數,而兩種型別的變數在計算熵的方法不同,因此上面的計算圖對它們進行分情況處理。

互資訊計算起始於如下兩個變數。

  • reg_z:表示了模型開始隨機生成的隱變數。
  • fake_ref_z_dist_info:表示了經過Encoder計算後的隱變數分佈資訊。

接下來,根據連續型和離散型的分類,兩個變數分成了以下四個變數。

  • cont_reg_z:reg_z的連續變數部分
  • cont_reg_dist_info:fake_ref_z_dist_info的連續變數部分
  • disc_reg_z:reg_z的離散變數部分
  • disc_reg_dist_info:fake_ref_z_dist_info的連續變數部分
  • 接下來,四個變數兩兩組隊完成了後驗公式P(c)logQ(c|g?([c,z])的計算:

  • cont_log_q_c_given_x:連續變數的後驗

  • disc_log_q_c_given_x:離散變數的後驗

同時,輸入的隱變數也各自完成先驗P(c)logP(c)的計算:

  • cont_log_q_c:連續變數的先驗
  • disc_log_q_c:離散變數的後驗

由於上面的運算全部是元素級的計算,還要把向量求出的內容彙總,得到∑P(c)logQ(c|g?([c,z])和∑P(c)logP(c) 。

  • cont_cross_ent:連續變數的交叉熵
  • cont_ent:連續變數的熵
  • disc_cross_ent:離散變數的交叉熵
  • disc_ent:離散變數的熵

接下來,根據互資訊公式兩兩相減,得到各自的互資訊損失。

  • cont_mi_est:連續變數的互資訊
  • disc_mi_est:離散變數的互資訊

最後將兩者相加就得到了最終的互資訊損失。

模型在訓練前定義了12個和影象有強烈互資訊的隨機變數,其中10個變數表示顯示的數字,它們組成一個Categorical的離散隨機向量;另外2個是服從範圍為[-1,1]的連續隨機變數。訓練完成後,調整離散隨機變數輸入並生成影象,得到如圖10-14所示的數字影象。

圖片描述

圖10-14 10個離散隨機變數對生成數字的影響

 

可以看出模型很好地識別了這些數字。調整另外兩個連續隨機變數,可以生成如圖10-15所示的數字影象。

圖片描述

圖10-15 2個連續隨機變數對生成數字的影響

 

可以看出,這兩個連續隨機變數學到了數字粗細和傾斜的特徵,而且這是在完全沒有暗示的情況下完成的。可見InfoGAN模型的能力。

到此InfoGAN的介紹就結束了。從這個模型可以看出,在經典GAN模型基礎上新增更多的內容會產生更多意想不到的效果。

總結

本章主要介紹了基於深度學習的生成模型,它們在生成影象上有著很強的能力。

  • VAE:基於變分下界約束得到的Encoder-Decoder模型對。
  • GAN:基於對抗的Generator-Discriminator模型對。
  • InfoGAN:挖掘GAN模型隱變數特點的模型。

《深度學習輕鬆學》訂購連結:https://item.jd.com/12106435.html# 
點選參與贈送此書活動 
圖片描述

相關文章