原文地址:https://www.jiqizhixin.com/articles/2017-10-1-1
生成對抗網絡基本概念
要理解生成對抗模型(GAN),首先要了解生成對抗模型可以拆分為兩個模塊:一個是判別模型,另一個是生成模型。簡單來說就是:兩個人比賽,看是 A 的矛厲害,還是 B 的盾厲害。比如,我們有一些真實數據,同時也有一把隨機生成的假數據。A 拼命地把隨手拿過來的假數據模仿成真實數據,並揉進真實數據里。B 則拼命地想把真實數據和假數據區分開。
這里,A 就是一個生成模型,類似於造假幣的,一個勁地學習如何騙過 B。而 B 則是一個判別模型,類似於稽查警察,一個勁地學習如何分辨出 A 的造假技巧。
如此這般,隨着 B 的鑒別技巧的越來越厲害,A 的造假技巧也是越來越純熟,而一個一流的假幣制造者就是我們所需要的。雖然 GAN 背后的思想十分直觀與朴素,但我們需要更進一步了解該理論背后的證明與推導。
總地來說,Goodfellow 等人提出來的 GAN 是通過對抗過程估計生成模型的新框架。在這種框架下,我們需要同時訓練兩個模型,即一個能捕獲數據分布的生成模型 G 和一個能估計數據來源於真實樣本概率的判別模型 D。生成器 G 的訓練過程是最大化判別器犯錯誤的概率,即判別器誤以為數據是真實樣本而不是生成器生成的假樣本。因此,這一框架就對應於兩個參與者的極小極大博弈(minimax game)。在所有可能的函數 G 和 D 中,我們可以求出唯一均衡解,即 G 可以生成與訓練樣本相同的分布,而 D 判斷的概率處處為 1/2,這一過程的推導與證明將在后文詳細解釋。
當模型都為多層感知機時,對抗性建模框架可以最直接地應用。為了學習到生成器在數據 x 上的分布 P_g,我們先定義一個先驗的輸入噪聲變量 P_z(z),然后根據 G(z;θ_g) 將其映射到數據空間中,其中 G 為多層感知機所表征的可微函數。我們同樣需要定義第二個多層感知機 D(s;θ_d),它的輸出為單個標量。D(x) 表示 x 來源於真實數據而不是 P_g 的概率。我們訓練 D 以最大化正確分配真實樣本和生成樣本的概率,因此我們就可以通過最小化 log(1-D(G(z))) 而同時訓練 G。也就是說判別器 D 和生成器對價值函數 V(G,D) 進行了極小極大化博弈:
我們后一部分會對對抗網絡進行理論上的分析,該理論分析本質上可以表明如果 G 和 D 的模型復雜度足夠(即在非參數限制下),那么對抗網絡就能生成數據分布。此外,Goodfellow 等人在論文中使用如下案例為我們簡要介紹了基本概念。
如上圖所示,生成對抗網絡會訓練並更新判別分布(即 D,藍色的虛線),更新判別器后就能將數據真實分布(黑點組成的線)從生成分布 P_g(G)(綠色實線)中判別出來。下方的水平線代表采樣域 Z,其中等距線表示 Z 中的樣本為均勻分布,上方的水平線代表真實數據 X 中的一部分。向上的箭頭表示映射 x=G(z) 如何對噪聲樣本(均勻采樣)施加一個不均勻的分布 P_g。(a)考慮在收斂點附近的對抗訓練:P_g 和 P_data 已經十分相似,D 是一個局部准確的分類器。(b)在算法內部循環中訓練 D 以從數據中判別出真實樣本,該循環最終會收斂到 D(x)=P_data(x)/(P_data(x)+P_g(x))。(c)隨后固定判別器並訓練生成器,在更新 G 之后,D 的梯度會引導 G(z)流向更可能被 D 分類為真實數據的方向。(d)經過若干次訓練后,如果 G 和 D 有足夠的復雜度,那么它們就會到達一個均衡點。這個時候 P_g=P_data,即生成器的概率密度函數等於真實數據的概率密度函數,也即生成的數據和真實數據是一樣的。在均衡點上 D 和 G 都不能得到進一步提升,並且判別器無法判斷數據到底是來自真實樣本還是偽造的數據,即 D(x)= 1/2。
上面是比較精簡地介紹了生成對抗網絡的基本概念,下一節將會把這些概念形式化,並描述優化的大致過程。
概念與過程的形式化
理論完美的生成器
該算法的目標是令生成器生成與真實數據幾乎沒有區別的樣本,即一個造假一流的 A,就是我們想要的生成模型。數學上,即將隨機變量生成為某一種概率分布,也可以說概率密度函數為相等的:P_G(x)=P_data(x)。這正是數學上證明生成器高效性的策略:即定義一個最優化問題,其中最優生成器 G 滿足 P_G(x)=P_data(x)。如果我們知道求解的 G 最后會滿足該關系,那么我們就可以合理地期望神經網絡通過典型的 SGD 訓練就能得到最優的 G。
最優化問題
正如最開始我們了解的警察與造假者案例,定義最優化問題的方法就可以由以下兩部分組成。首先我們需要定義一個判別器 D 以判別樣本是不是從 P_data(x) 分布中取出來的,因此有:
其中 E 指代取期望。這一項是根據「正類」(即辨別出 x 屬於真實數據 data)的對數損失函數而構建的。最大化這一項相當於令判別器 D 在 x 服從於 data 的概率密度時能准確地預測 D(x)=1,即:
另外一項是企圖欺騙判別器的生成器 G。該項根據「負類」的對數損失函數而構建,即:
因為 x<1 的對數為負,那么如果最大化該項的值,則需要令均值 D(G(z))≈0,因此 G 並沒有欺騙 D。為了結合這兩個概念,判別器的目標為最大化:
給定生成器 G,其代表了判別器 D 正確地識別了真實和偽造數據點。給定一個生成器 G,上式所得出來的最優判別器可以表示為 (下文用 D_G*表示)。定義價值函數為:
然后我們可以將最優化問題表述為:
現在 G 的目標已經相反了,當 D=D_G*時,最優的 G 為最小化前面的等式。在論文中,作者更喜歡求解最優化價值函的 G 和 D 以求解極小極大博弈:
對於 D 而言要盡量使公式最大化(識別能力強),而對於 G 又想使之最小(生成的數據接近實際數據)。整個訓練是一個迭代過程。其實極小極大化博弈可以分開理解,即在給定 G 的情況下先最大化 V(D,G) 而取 D,然后固定 D,並最小化 V(D,G) 而得到 G。其中,給定 G,最大化 V(D,G) 評估了 P_G 和 P_data 之間的差異或距離。
最后,我們可以將最優化問題表達為:
上文給出了 GAN 概念和優化過程的形式化表達。通過這些表達,我們可以理解整個生成對抗網絡的基本過程與優化方法。當然,有了這些概念我們完全可以直接在 GitHub 上找一段 GAN 代碼稍加修改並很好地運行它。但如果我們希望更加透徹地理解 GAN,更加全面地理解實現代碼,那么我們還需要知道很多推導過程。比如什么時候 D 能令價值函數 V(D,G) 取最大值、G 能令 V(D,G) 取最小值,而 D 和 G 該用什么樣的神經網絡(或函數),它們的損失函數又需要用什么等等。總之,還有很多理論細節與推導過程需要我們進一步挖掘。
理論推導
在原 GAN 論文中,度量生成分布與真實分布之間差異或距離的方法是 JS 散度,而 JS 散度是我們在推導訓練過程中使用 KL 散度所構建出來的。所以這一部分將從理論基礎出發再進一步推導最優判別器和生成器所需要滿足的條件,最后我們將利用推導結果在數學上重述訓練過程。這一部分為我們下一部分理解具體實現提供了強大的理論支持。
KL 散度
在信息論中,我們可以使用香農熵(Shannon entropy)來對整個概率分布中的不確定性總量進行量化:
如果我們對於同一個隨機變量 x 有兩個單獨的概率分布 P(x) 和 Q(x),我們可 以使用 KL 散度(Kullback-Leibler divergence)來衡量這兩個分布的差異:
在離散型變量的情況下,KL 散度衡量的是,當我們使用一種被設計成能夠使 得概率分布 Q 產生的消息的長度最小的編碼,發送包含由概率分布 P 產生的符號 的消息時,所需要的額外信息量。
KL 散度有很多有用的性質,最重要的是它是非負的。KL 散度為 0 當且僅當 P 和 Q 在離散型變量的情況下是相同的分布,或者在連續型變量的情況下是 『幾乎 處處』 相同的。因為 KL 散度是非負的並且衡量的是兩個分布之間的差異,它經常 被用作分布之間的某種距離。然而,它並不是真的距離因為它不是對稱的:對於某 些 P 和 Q,D_KL(P||Q) 不等於 D_KL(Q||P)。這種非對稱性意味着選擇 D_KL(P||Q) 還是 D_KL(Q||P) 影響很大。
在李弘毅的講解中,KL 散度可以從極大似然估計中推導而出。若給定一個樣本數據的分布 P_data(x) 和生成的數據分布 P_G(x;θ),那么 GAN 希望能找到一組參數θ使分布 P_g(x;θ) 和 P_data(x) 之間的距離最短,也就是找到一組生成器參數而使得生成器能生成十分逼真的圖片。
現在我們可以從訓練集抽取一組真實圖片來訓練 P_G(x;θ) 分布中的參數θ使其能逼近於真實分布。因此,現在從 P_data(x) 中抽取 m 個真實樣本 {𝑥^1,𝑥^2,…,𝑥^𝑚},其中符號「^」代表上標,即 x 中的第 i 個樣本。對於每一個真實樣本,我們可以計算 P_G(x^i;θ),即在由θ確定的生成分布中,x^i 樣本所出現的概率。因此,我們就可以構建似然函數:
其中「∏」代表累乘、P_G(x^i;θ) 代表第 i 個樣本在生成分布出現的概率。從該似然函數可知,我們抽取的 m 個真實樣本在 P_G(x;θ) 分布中全部出現的概率值可以表達為 L。又因為若 P_G(x;θ) 分布和 P_data(x) 分布相似,那么真實數據很可能就會出現在 P_G(x;θ) 分布中,因此 m 個樣本都出現在 P_G(x;θ) 分布中的概率就會十分大。
下面我們就可以最大化似然函數 L 而求得離真實分布最近的生成分布(即最優的參數θ):
在上面的推導中,我們希望最大化似然函數 L。若對似然函數取對數,那么累乘∏就能轉化為累加∑,並且這一過程並不會改變最優化的結果。因此我們可以將極大似然估計化為求令 log[P_G(x;θ)] 期望最大化的θ,而期望 E[logP_G(x;θ)] 可以展開為在 x 上的積分形式:∫P_data(x)logP_G(x;θ)dx。又因為該最優化過程是針對θ的,所以我們添加一項不含θ的積分並不影響最優化效果,即可添加 -∫P_data(x)logP_data(x)dx。添加該積分后,我們可以合並這兩個積分並構建類似 KL 散度的形式。該過程如下:
這一個積分就是 KL 散度的積分形式,因此,如果我們需要求令生成分布 P_G(x;θ) 盡可能靠近真實分布 P_data(x) 的參數θ,那么我們只需要求令 KL 散度最小的參數θ。若取得最優參數θ,那么生成器生成的圖像將顯得非常真實。
推導存在的問題
下面,我們必須證明該最優化問題有唯一解 G*,並且該唯一解滿足 P_G=P_data。不過在開始推導最優判別器和最優生成器之前,我們需要了解 Scott Rome 對原論文推導的觀點,他認為原論文忽略了可逆條件,因此最優解的推導不夠完美。
在 GAN 原論文中,有一個思想和其它很多方法都不同,即生成器 G 不需要滿足可逆條件。Scott Rome 認為這一點非常重要,因為實踐中 G 就是不可逆的。而很多證明筆記都忽略了這一點,他們在證明時錯誤地使用了積分換元公式,而積分換元卻又恰好基於 G 的可逆條件。Scott 認為證明只能基於以下等式的成立性:
該等式來源於測度論中的 Radon-Nikodym 定理,它展示在原論文的命題 1 中,並且表達為以下等式:
我們看到該講義使用了積分換元公式,但進行積分換元就必須計算 G^(-1),而 G 的逆卻並沒有假定為存在。並且在神經網絡的實踐中,它也並不存在。可能這個方法在機器學習和統計學文獻中太常見了,因此我們忽略了它。
最優判別器
在極小極大博弈的第一步中,給定生成器 G,最大化 V(D,G) 而得出最優判別器 D。其中,最大化 V(D,G) 評估了 P_G 和 P_data 之間的差異或距離。因為在原論文中價值函數可寫為在 x 上的積分,即將數學期望展開為積分形式:
其實求積分的最大值可以轉化為求被積函數的最大值。而求被積函數的最大值是為了求得最優判別器 D,因此不涉及判別器的項都可以看作為常數項。如下所示,P_data(x) 和 P_G(x) 都為標量,因此被積函數可表示為 a*D(x)+b*log(1-D(x))。
若令判別器 D(x) 等於 y,那么被積函數可以寫為:
為了找到最優的極值點,如果 a+b≠0,我們可以用以下一階導求解:
如果我們繼續求表達式 f(y) 在駐點的二階導:
其中 a,b∈(0,1)。因為一階導等於零、二階導小於零,所以我們知道 a/(a+b) 為極大值。若將 a=P_data(x)、b=P_G(x) 代入該極值,那么最優判別器 D(x)=P_data(x)/(P_data(x)+P_G(x))。
最后我們可以將價值函數表達式寫為:
如果我們令 D(x)=P_data/(P_data+p_G),那么我們就可以令價值函數 V(G,D) 取極大值。因為 f(y) 在定義域內有唯一的極大值,最優 D 也是唯一的,並且沒有其它的 D 能實現極大值。
其實該最優的 D 在實踐中並不是可計算的,但在數學上十分重要。我們並不知道先驗的 P_data(x),所以我們在訓練中永遠不會用到它。另一方面,它的存在令我們可以證明最優的 G 是存在的,並且在訓練中我們只需要逼近 D。
最優生成器
當然 GAN 過程的目標是令 P_G=P_data。這對最優的 D 意味着什么呢?我們可以將這一等式代入 D_G*的表達式中:
這意味着判別器已經完全困惑了,它完全分辨不出 P_data 和 P_G 的區別,即判斷樣本來自 P_data 和 P_G 的概率都為 1/2。基於這一觀點,GAN 作者證明了 G 就是極小極大博弈的解。該定理如下:
「當且僅當 P_G=P_data,訓練標准 C(G)=maxV(G,D) 的全局最小點可以達到。」
以上定理即極大極小博弈的第二步,求令 V(G,D*) 最小的生成器 G(其中 G*代表最優的判別器)。之所以當 P_G(x)=P_data(x) 可以令價值函數最小化,是因為這時候兩個分布的 JS 散度 [JSD(P_data(x) || P_G(x))] 等於零,這一過程的詳細解釋如下。
原論文中的這一定理是「當且僅當」聲明,所以我們需要從兩個方向證明。首先我們先從反向逼近並證明 C(G) 的取值,然后再利用由反向獲得的新知識從正向證明。設 P_G=P_data(反向指預先知道最優條件並做推導),我們可以反向推出:
該值是全局最小值的候選,因為它只有在 P_G=P_data 的時候才出現。我們現在需要從正向證明這一個值常常為最小值,也就是同時滿足「當」和「僅當」的條件。現在放棄 P_G=P_data 的假設,對任意一個 G,我們可以將上一步求出的最優判別器 D* 代入到 C(G)=maxV(G,D) 中:
因為已知 -log4 為全局最小候選值,所以我們希望構造某個值以使方程式中出現 log2。因此我們可以在每個積分中加上或減去 log2,並乘上概率密度。這是一個十分常見並且不會改變等式的數學證明技巧,因為本質上我們只是在方程加上了 0。
采用該技巧主要是希望能夠構建成含 log2 和 JS 散度的形式,上式化簡后可以得到以下表達式:
因為概率密度的定義,P_G 和 P_data 在它們積分域上的積分等於 1,即:
此外,根據對數的定義,我們有:
因此代入該等式,我們可以寫為:
現在,如果讀者閱讀了前文的 KL 散度(Kullback-Leibler divergence),那么我們就會發現每一個積分正好就是它。具體來說:
KL 散度是非負的,所以我們馬上就能看出來-log4 為 C(G) 的全局最小值。
如果我們進一步證明只有一個 G 能達到這一個值,因為 P_G=P_data 將會成為令 C(G)=−log4 的唯一點,所以整個證明就能完成了。
從前文可知 KL 散度是非對稱的,所以 C(G) 中的 KL(P_data || (P_data+P_G)/2) 左右兩項是不能交換的,但如果同時加上另一項 KL(P_G || (P_data+P_G)/2),它們的和就能變成對稱項。這兩項 KL 散度的和即可以表示為 JS 散度(Jenson-Shannon divergence):
假設存在兩個分布 P 和 Q,且這兩個分布的平均分布 M=(P+Q)/2,那么這兩個分布之間的 JS 散度為 P 與 M 之間的 KL 散度加上 Q 與 M 之間的 KL 散度再除以 2。
JS 散度的取值為 0 到 log2。若兩個分布完全沒有交集,那么 JS 散度取最大值 log2;若兩個分布完全一樣,那么 JS 散度取最小值 0。
因此 C(G) 可以根據 JS 散度的定義改寫為:
這一散度其實就是 Jenson-Shannon 距離度量的平方。根據它的屬性:當 P_G=P_data 時,JSD(P_data||P_G) 為 0。綜上所述,生成分布當且僅當等於真實數據分布式時,我們可以取得最優生成器。
收斂
現在,該論文的主要部分已經得到了證明:即 P_G=P_data 為 maxV(G,D) 的最優點。此外,原論文還有額外的證明白表示:給定足夠的訓練數據和正確的環境,訓練過程將收斂到最優 G,我們並不詳細討論這一塊。
重述訓練過程
下面是推導的最后一步,我們會重述整個參數優化過程,並簡要介紹實際訓練中涉及的各個過程。
1.參數優化過程
若我們需要尋找最優的生成器,那么給定一個判別器 D,我們可以將 maxV(G,D) 看作訓練生成器的損失函數 L(G)。既然設定了損失函數,那么我們就能使用 SGD、Adam 等優化算法更新生成器 G 的參數,梯度下降的參數優化過程如下:
其中求 L(G) 對θ_G 的偏導數涉及到求 max{V(G,D)} 的偏導數,這種對 max 函數求微分的方式是存在且可用的。
現在給定一個初始 G_0,我們需要找到令 V(G_0,D) 最大的 D_0*,因此判別器更新的過程也就可以看作損失函數為-V(G,D) 的訓練過程。並且由前面的推導可知,V(G,D) 實際上與分布 P_data(x) 和 P_G(x) 之間的 JS 散度只差了一個常數項。因此這樣一個循環對抗的過程就能表述為:
- 給定 G_0,最大化 V(G_0,D) 以求得 D_0*,即 max[JSD(P_data(x)||P_G0(x)];
- 固定 D_0*,計算θ_G1 ← θ_G0 −η(𝜕V(G,D_0*) /𝜕θ_G) 以求得更新后的 G_1;
- 固定 G_1,最大化 V(G_1,D_0*) 以求得 D_1*,即 max[JSD(P_data(x)||P_G1(x)];
- 固定 D_1*,計算θ_G2 ← θ_G1 −η(𝜕V(𝐺,𝐷_0*) /𝜕θ_G) 以求得更新后的 G_2;
- 。。。
2.實際訓練過程
根據前面價值函數 V(G,D) 的定義,我們需要求兩個數學期望,即 E[log(D(x))] 和 E[log(1-D(G(z)))],其中 x 服從真實數據分布,z 服從初始化分布。但在實踐中,我們是沒有辦法利用積分求這兩個數學期望的,所以一般我們能從無窮的真實數據和無窮的生成器中做采樣以逼近真實的數學期望。
若現在給定生成器 G,並希望計算 maxV(G,D) 以求得判別器 D,那么我們首先需要從 P_data(x) 采樣 m 個樣本 {𝑥^1,𝑥^2,…,𝑥^𝑚},從生成器 P_G(x) 采樣 m 個樣本 。因此最大化價值函數 V(G,D) 就可以使用以下表達式近似替代:
若我們需要計算上述的極大化過程,可以采用等價形式的訓練方法。若我們有一個二元分類器 D(參數為θ_d),當然該分類器可以是深度神經網絡,那么極大化過程的輸出就為該分類器 D(x)。現在我們從 P_data(x) 抽取樣本作為正樣本,從 P_G(x) 抽取樣本作為負樣本,同時將逼近負 V(G,D) 的函數作為損失函數,因此我們就將其表述為一個標准的二元分類器的訓練過程:
在實踐中,我們必須使用迭代和數值計算的方法實現極小極大化博弈過程。在訓練的內部循環中完整地優化 D 在計算上是不允許的,並且有限的數據集也會導致過擬合。因此我們可以在 k 個優化 D 的步驟和一個優化 G 的步驟間交替進行。那么我們只需慢慢地更新 G,D 就會一直處於最優解的附近,這種策略類似於 SML/PCD 訓練的方式。
綜上,我們可以描述整個訓練過程,對於每一次迭代:
- 從真實數據分布 P_data 抽取 m 個樣本
- 從先驗分布 P_prior(z) 抽取 m 個噪聲樣本
- 將噪聲樣本投入 G 而生成數據
,通過最大化 V 的近似而更新判別器參數θ_d,即極大化
,且判別器參數的更新迭代式為
以上是學習判別器 D 的過程。因為學習 D 的過程是計算 JS 散度的過程,並且我們希望能最大化價值函數,所以該步驟會重復 k 次。
- 從先驗分布 P_prior(z) 中抽取另外 m 個噪聲樣本 {z^1,...,z^m}
- 通過極小化 V^tilde 而更新生成器參數θ_g,即極小化
,且生成器參數的更新迭代式為
以上是學習生成器參數的過程,這一過程在一次迭代中只會進行一次,因此可以避免更新太多而令 JS 散度上升。
實現
在上一期機器之心 GitHub 項目中,我們從零開始使用 TensorFlow 實現了簡單的 CNN,我們不僅介紹了 TensorFlow 基本的操作,並從全連接神經網絡開始簡單地實現了 LeNet-5。在第一期 GitHub 實現中,我們陸續上傳了三段實現代碼,第二次上傳補充的是全連接網絡進行 MNIST 圖像識別,我們逐行注釋了該模型的所有代碼。第三次上傳補充的是使用 Keras 構建簡單的 CNN,我們同樣添加了大量注釋。本文是第二期 GitHub 實現,首先提供的是 GAN 實現代碼與注釋,隨后我們會將以上的理論分析與實現代碼相結合並展示在 Jupyter Notebook 中。雖然首次實現使用的是比較簡單的高級 API(Keras),但后面我們會補充使用 TensorFlow 構建 GAN 的代碼與注釋。
GitHub 實現地址:https://github.com/jiqizhixin/ML-Tutorial-Experiment
機器之心首先使用基於 TensorFlow 后端的 Keras 實現了該生成對抗網絡,並且我們在 MNIST 數據集上對模型進行訓練並生成了一系列手寫字體。這一章節只簡要解釋部分實現代碼,更完整與詳細的注釋請查看 GitHub 項目地址。
生成模型
首先需要定義一個生成器 G,該生成器需要將輸入的隨機噪聲變換為圖像。以下是定義的生成模型,該模型首先輸入有 100 個元素的向量,該向量隨機生成於某分布。隨后利用兩個全連接層接連將該輸入向量擴展到 1024 維和 128*7*7 維,后面就開始將全連接層所產生的一維張量重新塑造成二維張量,即 MNIST 中的灰度圖。我們注意到該模型采用的激活函數為 tanh,所以也嘗試過將其轉換為 relu 函數,但發現生成模型如果轉化為 relu 函數,那么它的輸出就會成為一片灰色。
由全連接傳遞的數據會經過幾個上采樣層和卷積層,我們注意到最后一個卷積層所采用的卷積核為 1,所以經過最后卷積層所生成的圖像是一張二維灰度圖像,更詳細的分析請查看機器之心 GitHub 項目。
def generator_model(): #下面搭建生成器的架構,首先導入序貫模型(sequential),即多個網絡層的線性堆疊 model = Sequential() #添加一個全連接層,輸入為100維向量,輸出為1024維 model.add(Dense(input_dim=100, output_dim=1024)) #添加一個激活函數tanh model.add(Activation('tanh')) #添加一個全連接層,輸出為128×7×7維度 model.add(Dense(128*7*7)) #添加一個批量歸一化層,該層在每個batch上將前一層的激活值重新規范化,即使得其輸出數據的均值接近0,其標准差接近1 model.add(BatchNormalization()) model.add(Activation('tanh')) #Reshape層用來將輸入shape轉換為特定的shape,將含有128*7*7個元素的向量轉化為7×7×128張量 model.add(Reshape((7, 7, 128), input_shape=(128*7*7,))) #2維上采樣層,即將數據的行和列分別重復2次 model.add(UpSampling2D(size=(2, 2))) #添加一個2維卷積層,卷積核大小為5×5,激活函數為tanh,共64個卷積核,並采用padding以保持圖像尺寸不變 model.add(Conv2D(64, (5, 5), padding='same')) model.add(Activation('tanh')) model.add(UpSampling2D(size=(2, 2))) #卷積核設為1即輸出圖像的維度 model.add(Conv2D(1, (5, 5), padding='same')) model.add(Activation('tanh')) return model
拼接
前面定義的是可生成圖像的模型 G(z;θ_g),而我們在訓練生成模型時,需要固定判別模型 D 以極小化價值函數而尋求更好的生成模型,這就意味着我們需要將生成模型與判別模型拼接在一起,並固定 D 的權重以訓練 G 的權重。下面就定義了這一過程,我們先添加前面定義的生成模型,再將定義的判別模型拼接在生成模型下方,並且我們將判別模型設置為不可訓練。因此,訓練這個組合模型才能真正更新生成模型的參數。
def generator_containing_discriminator(g, d): #將前面定義的生成器架構和判別器架構組拼接成一個大的神經網絡,用於判別生成的圖片 model = Sequential() #先添加生成器架構,再令d不可訓練,即固定d #因此在給定d的情況下訓練生成器,即通過將生成的結果投入到判別器進行辨別而優化生成器 model.add(g) d.trainable = False model.add(d) return model
判別模型
判別模型相對來說就是比較傳統的圖像識別模型,前面我們可以按照經典的方法采用幾個卷積層與最大池化層,而后再展開為一維張量並采用幾個全連接層作為架構。我們嘗試了將 tanh 激活函數改為 relu 激活函數,在前兩個 epoch 基本上沒有什么明顯的變化。
def discriminator_model(): #下面搭建判別器架構,同樣采用序貫模型 model = Sequential() #添加2維卷積層,卷積核大小為5×5,激活函數為tanh,輸入shape在‘channels_first’模式下為(samples,channels,rows,cols) #在‘channels_last’模式下為(samples,rows,cols,channels),輸出為64維 model.add( Conv2D(64, (5, 5), padding='same', input_shape=(28, 28, 1)) ) model.add(Activation('tanh')) #為空域信號施加最大值池化,pool_size取(2,2)代表使圖片在兩個維度上均變為原長的一半 model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Conv2D(128, (5, 5))) model.add(Activation('tanh')) model.add(MaxPooling2D(pool_size=(2, 2))) #Flatten層把多維輸入一維化,常用在從卷積層到全連接層的過渡 model.add(Flatten()) model.add(Dense(1024)) model.add(Activation('tanh')) #一個結點進行二值分類,並采用sigmoid函數的輸出作為概念 model.add(Dense(1)) model.add(Activation('sigmoid')) return model
訓練
訓練這一部分比較長,也值得我們進行詳細的探討。總的來說,以下訓練過程可簡述為:
- 加載 MNIST 數據
- 將數據分割為訓練與測試集,並賦值給變量
- 設置訓練模型的超參數
- 編譯模型的訓練過程
- 在每一次迭代內,抽取生成圖像與真實圖像,並打上標注
- 隨后將數據投入到判別模型中,並進行訓練與計算損失
- 固定判別模型,訓練生成模型並計算損失,結束這一次迭代
以上是下面訓練過程的簡要介紹,我們將結合上文的理論推導在 GitHub 中展示更詳細的分析。
def train(BATCH_SIZE): # 國內好像不能直接導入數據集,我們試了幾次都不行,后來將數據集下載到本地'~/.keras/datasets/',也就是當前目錄(我的是用戶文件夾下)下的.keras文件夾中。 #下載的地址為:https://s3.amazonaws.com/img-datasets/mnist.npz (X_train, y_train), (X_test, y_test) = mnist.load_data() #iamge_data_format選擇"channels_last"或"channels_first",該選項指定了Keras將要使用的維度順序。 #"channels_first"假定2D數據的維度順序為(channels, rows, cols),3D數據的維度順序為(channels, conv_dim1, conv_dim2, conv_dim3) #轉換字段類型,並將數據導入變量中 X_train = (X_train.astype(np.float32) - 127.5)/127.5 X_train = X_train[:, :, :, None] X_test = X_test[:, :, :, None] # X_train = X_train.reshape((X_train.shape, 1) + X_train.shape[1:]) #將定義好的模型架構賦值給特定的變量 d = discriminator_model() g = generator_model() d_on_g = generator_containing_discriminator(g, d) #定義生成器模型判別器模型更新所使用的優化算法及超參數 d_optim = SGD(lr=0.001, momentum=0.9, nesterov=True) g_optim = SGD(lr=0.001, momentum=0.9, nesterov=True) #編譯三個神經網絡並設置損失函數和優化算法,其中損失函數都是用的是二元分類交叉熵函數。編譯是用來配置模型學習過程的 g.compile(loss='binary_crossentropy', optimizer="SGD") d_on_g.compile(loss='binary_crossentropy', optimizer=g_optim) #前一個架構在固定判別器的情況下訓練了生成器,所以在訓練判別器之前先要設定其為可訓練。 d.trainable = True d.compile(loss='binary_crossentropy', optimizer=d_optim) #下面在滿足epoch條件下進行訓練 for epoch in range(30): print("Epoch is", epoch) #計算一個epoch所需要的迭代數量,即訓練樣本數除批量大小數的值取整;其中shape[0]就是讀取矩陣第一維度的長度 print("Number of batches", int(X_train.shape[0]/BATCH_SIZE)) #在一個epoch內進行迭代訓練 for index in range(int(X_train.shape[0]/BATCH_SIZE)): #隨機生成的噪聲服從均勻分布,且采樣下界為-1、采樣上界為1,輸出BATCH_SIZE×100個樣本;即抽取一個批量的隨機樣本 noise = np.random.uniform(-1, 1, size=(BATCH_SIZE, 100)) #抽取一個批量的真實圖片 image_batch = X_train[index*BATCH_SIZE:(index+1)*BATCH_SIZE] #生成的圖片使用生成器對隨機噪聲進行推斷;verbose為日志顯示,0為不在標准輸出流輸出日志信息,1為輸出進度條記錄 generated_images = g.predict(noise, verbose=0) #每經過100次迭代輸出一張生成的圖片 if index % 100 == 0: image = combine_images(generated_images) image = image*127.5+127.5 Image.fromarray(image.astype(np.uint8)).save( "./GAN/"+str(epoch)+"_"+str(index)+".png") #將真實的圖片和生成的圖片以多維數組的形式拼接在一起,真實圖片在上,生成圖片在下 X = np.concatenate((image_batch, generated_images)) #生成圖片真假標簽,即一個包含兩倍批量大小的列表;前一個批量大小都是1,代表真實圖片,后一個批量大小都是0,代表偽造圖片 y = [1] * BATCH_SIZE + [0] * BATCH_SIZE #判別器的損失;在一個batch的數據上進行一次參數更新 d_loss = d.train_on_batch(X, y) print("batch %d d_loss : %f" % (index, d_loss)) #隨機生成的噪聲服從均勻分布 noise = np.random.uniform(-1, 1, (BATCH_SIZE, 100)) #固定判別器 d.trainable = False #計算生成器損失;在一個batch的數據上進行一次參數更新 g_loss = d_on_g.train_on_batch(noise, [1] * BATCH_SIZE) #令判別器可訓練 d.trainable = True print("batch %d g_loss : %f" % (index, g_loss)) #每100次迭代保存一次生成器和判別器的權重 if index % 100 == 9: g.save_weights('generator', True) d.save_weights('discriminator', True)
試驗
在實踐中,我們訓練 30 個 epoch 后能得到如下不錯的生成結果:
當然,中間我們還發現很多訓練上的問題,比如說學習率、批量大小、激活函數等。學習率一般我們設置為 0.001 到 0.0005,其它的學習率還有很多沒有測試。批量大小我們使用的比較小,例如 16、32、64 等,較小的批量大小可能訓練的 epoch 數就不需要那么多。我們發現若將生成模型的激活函數修改為 relu,那么生成的圖像很可能會顯示為一片灰色,生成模型和判別模型的訓練損失可能會表現為:
#batch_size=32 batch 2000 d_loss : 0.000318 batch 2000 g_loss : 7.618911
以上是在迭代 2000 次后所出現的情況,判別模型的損失一直在下降,而生成模型的損失一直在上升。而正常情況下,我發現生成模型的損失和判別模型的損失會在一定范圍內交替上升與下降,而迭代 2000 次后訓練損失情況為:
#batch_size=36 batch 2000 g_loss : 1.663281 batch 2000 d_loss : 0.483616
此外,我們還發現很多出現問題的生成模式,比如說如下生成結果更多是傾向於 0 與 1:
最后,附上我們結束訓練的標志。
參考文獻:
- 生成對抗網絡原論文:https://arxiv.org/pdf/1406.2661.pdf
- Goodfellow NIPS 2016 Tutorial:https://arxiv.org/abs/1701.00160
- 李宏毅 MLDS17:http://speech.ee.ntu.edu.tw/~tlkagk/courses_MLDS17.html
- Scott Rome GAN 推導:http://srome.github.io//An-Annotated-Proof-of-Generative-Adversarial-Networks-with-Implementation-Notes/