生成對抗網絡(Generative Adversarial Networks,GAN)初探

1. 從納什均衡(Nash equilibrium)提及

咱們先來看看納什均衡的經濟學定義:php

所謂納什均衡,指的是參與人的這樣一種策略組合,在該策略組合上,任何參與人單獨改變策略都不會獲得好處。換句話說,若是在一個策略組合上,當全部其餘人都不改變策略時,沒有人會改變本身的策略,則該策略組合就是一個納什均衡css

B站上有一個關於」海灘2個兄弟賣雪糕「造成納什均衡的 視頻,講的很生動。
無論系統中的雙方一開始處於什麼樣的狀態,只要系統中參與競爭的個體都是」理性經濟人「,即 每一個人在考慮其餘人的可能動做的基礎上,出於最大化本身的我的利益做爲下一步行動的考慮,那麼最終系統都必定會進入納什均衡狀態,這個狀態也許對於系統來講不是全局最優的,可是對於系統中的每個個體來講都是理論最優的
這樣可能仍是有一些抽象,咱們用幾個例子來講明。

0x1:價格戰中的納什均衡

市場上有2家企業A和B,都是賣紙的,紙的成本都是2元錢,A和B都賣5塊錢。在最開始,A、B企業都是盈利3塊,這種狀態叫」社會最優解(Social optimal solution)「。但問題是,社會最優解是一個不穩定的狀態,就如同下圖中這個優化曲面上那個紅球點同樣,雖然該小球目前處於曲面最高點,可是隻要施加一些輕微的擾動,小球就會馬上向山下滑落:html

如今企業A和B準備開展商業競爭:git

  • 有一天,A企業率先降價到4塊錢,因而A銷量大增,B銷量大減。
  • B看到了後,降價到3塊錢,因而B銷量大增,A銷量大減。
  • ......

但若是價格戰一直這樣打下去,這個過程顯然不可能無限迭代下去。當A和B都降價到了3塊時,雙方都達到了成本的臨界點,既不敢漲價,也不敢降價。漲價了市場就丟了,降價了,就賺不到錢甚至賠錢。因此A和B都不會再去作改變,這就是納什均衡。github

A和B怎樣可以得到最大利潤呢,就是A和B坐到一塊兒商量,同時把價格提升,這就叫共謀,但法律爲了保障消費者利益,禁止共謀。補充一句,共謀在機器學習中被稱做」模型坍塌「,指的對對抗的模型雙方都進入了一個互相承認的局部最優區而再也不變化,具體的技術細節咱們後面會討論。web

0x2:囚徒困境中的納什均衡

囚徒困境是說:有兩個小偷集體做案,而後被警察捉住。警察對兩我的分別審訊,而且告訴他們政策:算法

  • 若是兩我的都交代坦白,就能夠定罪,兩我的各判八年。
  • 若是一我的交代另外一個不交代,那麼同樣能夠定罪。可是交代的人從寬處罰,批評教育就釋放。不交代的人從嚴處罰,判十年。
  • 若是兩我的都不交代,無法定罪,每一個人判一年意思一下。

兩我的的收益狀況以下所示:shell

由於A和B是不能互相通訊的,所以這是一個靜態不徹底信息博弈,咱們分別考慮雙方的決策面:網絡

  • A的決策。A會想,我如何才能得到更大收益呢?
    • 先考慮最壞的狀況:若是B坦白了,那麼我坦白就會判8年,我抗拒就會判十年,我應該坦白;
    • 再考慮最好的狀況:若是B抗拒了,我坦白會判0年,我抗拒會判1年,我仍是應該坦白;
    • 因此最終A會選擇坦白。
  • 一樣,B也會這樣想。

所以最終納什均衡點在兩我的都坦白,各判八年這裏。架構

顯然,集體最優解在兩我的都抗拒,這樣一來每一個人都判一年就出來了。可是,納什均衡點卻不在這裏。並且,在納什均衡點上,任何一我的都沒有改變本身決策的動力。由於一旦單方面改變決策,那我的的收益就會降低。

0x3:開車加塞現象的納什均衡

咱們知道,在國內開車夾塞很常見。若是你們都不夾塞,是總體的最優解,可是按照納什均衡理論,任何一個司機都會考慮,不管別人是否夾塞,我夾塞均可以使本身的收益變大。因而最終你們都會夾塞,加重擁堵,反而不如你們都不加塞走的快。

那麼,有沒有辦法使我的最優變成集體最優呢?方法就是共謀。兩個小偷在做案以前能夠說好,我們若是進去了,必定都抗拒。若是你這一次敢反悔,那麼之後道上的人不再會有人跟你一塊兒了。也就是說,在屢次博弈過程當中,共謀是可能的。可是若是這個小偷想幹完這一票就走,共謀就是不牢靠的。

在社會領域,共謀是靠法律完成的。你們約定的共謀結論就是法律,若是有人不按照約定作,就會受到法律的懲罰。經過這種方式保證最終決策從我的最優的納什均衡點變爲集體最優勢。

另一方面,如今不少汽車廠商提出了車聯網的概念,在路上的每一輛車都經過物聯網連成一個臨時網絡,全部車按照一個最優的協同算法共同協定最優的行車路線、行車速度、路口等待等行爲,這樣總體交通能夠達到一個總體最優,全部人都節省了時間。

0x3:槍手博弈

彼此痛恨的甲、乙、丙三個槍手準備決鬥,他們各自的水平以下:

  • 甲槍法最好,十發八中;
  • 乙槍法次之,十發六中;
  • 丙槍法最差,十發四中;

1. 場景一:三人同時開槍,而且每人只發一槍。每一輪槍戰後,誰活下來的機會大一些?

首先明確一點,這是一個靜態不徹底信息博弈,每一個搶手在開槍前都不知道其餘對手的策略,只能在猜想其餘對手策略的基礎上,選擇對本身最優的策略。

咱們來分析一下第一輪槍戰各個槍手的策略。

  • 槍手甲必定要對槍手乙先開槍。由於乙對甲的威脅要比丙對甲的威脅更大,甲應該首先幹掉乙,這是甲的最佳策略。
  • 一樣的道理,槍手乙的最佳策略是第一槍瞄準甲。乙一旦將甲幹掉,乙和丙進行對決,乙勝算的機率天然大不少。
  • 槍手丙的最佳策略也是先對甲開槍。乙的槍法畢竟比甲差一些,丙先把甲幹掉再與乙進行對決,丙的存活機率仍是要高一些。

第一輪槍戰事後,有幾種可能的結果:

  • 甲乙雙亡,丙獲勝
  • 甲亡,乙丙存活
  • 乙亡,甲丙存活

如今進入第二輪槍戰:

除非第一輪甲乙雙亡,不然丙就必定處於劣勢,由於不論甲或乙,他們的命中率都比丙的命中率爲高。

這就是槍手丙的悲哀。能力不行的丙玩些花樣雖然能在第一輪槍戰中暫時獲勝。可是,若是甲乙在第一輪槍戰中沒有雙亡的話,在第二輪槍戰結束後,丙的存活的概率就必定比甲或乙爲低。

這彷佛說明,能力差的人在競爭中耍弄手腕能贏一時,但最終每每不能成事。

2. 場景二:三人輪流開槍,沒人只發一槍。丙最後發槍。

咱們如今改變遊戲規則,假定甲乙丙不是同時開槍,而是他們輪流開一槍。先假定開槍的順序是甲、乙、丙,咱們來分析一下槍戰過程:

  • 甲一槍將乙幹掉後(80%的概率),就輪到丙開槍,丙有40%的概率一槍將甲幹掉。
  • 乙躲過甲的第一槍(20%概率),輪到乙開槍,乙仍是會瞄準槍法最好的甲開槍,即便乙這一槍幹掉了甲(60%概率),下一輪仍然是輪到丙開槍(40%概率)。不管是甲或者乙先開槍,乙都有在下一輪先開槍的優點。

若是是丙先開槍,狀況又如何呢?

3. 場景三:三人輪流開槍,沒人只發一槍。丙第一個發槍。

  • 丙能夠向甲先開槍(40%概率),
    • 即便丙打不中甲,甲的最佳策略仍然是向乙開槍。
    • 可是,若是丙打中了甲,下一輪可就是乙開槍打丙了。
  • 所以,丙的最佳策略是胡亂開一槍,只要丙不打中甲或者乙,在下一輪射擊中他就處於有利的形勢(先發優點)。

咱們經過這個例子,能夠理解人們在博弈中可否獲勝,不單純取決於他們的實力,更重要的是取決於博弈方實力對比所造成的關係

在上面的例子中,乙和丙其實是一種聯盟關係,先把甲幹掉,他們的生存概率都上升了。咱們如今來判斷一下,乙和丙之中,誰更有可能背叛,誰更可能忠誠?

任何一個聯盟的成員都會時刻權衡利弊,一旦背叛的好處大於忠誠的好處,聯盟就會破裂。在乙和丙的聯盟中,乙是最忠誠的。這不是由於乙自己具備更加忠誠的品質,而是利益關係使然。只要甲不死,乙的槍口就必定會瞄準甲。但丙就不是這樣了,丙不瞄準甲而胡亂開一槍顯然違背了聯盟關係,丙這樣作的結果,將使乙處於更危險的境地。

合做才能對抗強敵。只有乙丙合做,才能把甲先幹掉。若是,乙丙不和,乙或丙單獨對甲都不佔優,必然被甲前後解決。、

1966年經典電影《黃金三鏢客》中的最後一幕,三個主人公手持槍桿站在墓地中,爲了寶藏隨時準備決一雌雄。爲了活着拿到寶藏,倖存下來的最優策略是什麼呢? 

0x4:蒙古聯合南宋滅金

當時,蒙古軍事實力最強,金國次之,南宋武力最弱。原本南宋應該和金國結盟,幫助金國抵禦蒙古的入侵纔是上策,或者至少保持中立。可是,當時的南宋採起了和蒙古結盟的政策。南宋當局先是糊塗地贊成了拖雷借道宋地伐金。1231年,蒙古軍隊在宋朝的先遣隊伍引導下,借道四川等地,北度漢水殲滅了金軍有生力量。

1233年,南宋軍隊與蒙古軍隊合圍蔡州,金朝最後一個皇帝在城破後死於亂兵,金至此滅亡。1279年,南宋正式亡於蒙古。

若是南宋當政者有戰略眼光,捐棄前嫌,與世仇金結盟對抗最強大的敵人蒙古,宋和金都不至於那麼快就前後滅亡了。

0x5:智豬博弈

豬圈裏面有兩隻豬, 一隻大,一隻小。豬圈很長,一頭有一個踏板,另外一頭是飼料的出口和食槽。每踩一下踏板,在遠離踏板的豬圈的另外一邊的投食口就會落下少許的食物。若是有一隻豬去踩踏板,另外一隻豬就有機會搶先吃到另外一邊落下的食物。

  • 當小豬踩動踏板時,大豬會在小豬跑到食槽以前恰好吃光全部的食物;
  • 如果大豬踩動了踏板,則還有機會在小豬吃完落下的食物以前跑到食槽,爭吃到另外一半殘羹。

那麼,兩隻豬各會採起什麼策略?使人出乎意料的是,答案竟然是:小豬將選擇「搭便車」策略,也就是舒舒服服地等在食槽邊;而大豬則爲一點殘羹不知疲倦地奔忙於踏板和食槽之間。

緣由何在呢?咱們來分析一下,首先這是一個靜態不徹底信息博弈

  • 小豬踩踏板:小豬將一無所得,不踩踏板反而能吃上食物。對小豬而言,不管大豬是否踩動踏板,不踩踏板老是好的選擇。
  • 反觀大豬,已明知小豬是不會去踩動踏板的,本身親自去踩踏板總比不踩強吧,因此只好親力親爲了。

「智豬博弈」的結論彷佛是,在一個雙方公平、公正、合理和共享競爭環境中,有時佔優點的一方最終獲得的結果卻有悖於他的初始理性。這種狀況在現實中比比皆是。

好比,在某種新產品剛上市,其性能和功用還不爲人所熟識的狀況下,若是進行新產品生產的不只是一家小企業,還有其餘生產能力和銷售能力更強的企業。那麼,小企業徹底沒有必要做出頭鳥,本身去投入大量廣告作產品宣傳,只要採用跟隨戰略便可。

「智豬博弈」告訴咱們,誰先去踩這個踏板,就會造福全體,但多勞卻並不必定多得。

在現實生活中,不少人都只想付出最小的代價,獲得最大的回報,爭着作那隻不勞而獲的小豬。「一個和尚挑水喝,兩個和尚擡水喝,三個和尚沒水喝」說的正是這樣一個道理。這三個和尚都想作「小豬」,卻不想付出勞動,不肯承擔起「大豬」的義務,最後致使每一個人都沒法得到利益。

0x6:證券市場中的「智豬博弈」

金融證券市場是一個羣體博弈的場所,其真實狀況很是複雜。在證券交易中,其結果不只依賴於單個參與者自身的策略和市場條件,也依賴其餘人的選擇及策略。

在「智豬博弈」的情景中,大豬是佔據比較優點的,可是,因爲小豬別無選擇,使得大豬爲了本身能吃到食物,不得不辛勤忙碌,反而讓小豬搭了便車,並且比大豬還得意。這個博弈中的關鍵要素是豬圈的設計, 即踩踏板的成本。

證券投資中也是有這種情形的。例如,當莊家在底位買入大量股票後,已經付出了至關多的資金和時間成本,若是不等價格上升就撤退,就只有接受虧損。

因此,基於和大豬同樣的貪吃本能,只要大勢不是太糟糕,莊家通常都會擡高股價,以求實現手中股票的增值。這時的中小散戶,就能夠對該股追加資金,當一隻聰明的「小豬」,而讓 「大豬」莊家力擡股價。固然,這種股票的發覺並不容易,因此當「小豬」所須要的條件,就是發現有這種狀況存在的豬圈,並衝進去。這樣,你就成爲一隻聰明的「小豬」。

股市中,散戶投資者與小豬的命運有類似之處,沒有能力承擔炒做成本,因此就應該充分利用資金靈活、成本低和不怕被套的優點,發現並選擇那些機構投資者已經或可能坐莊的股票,等着大豬們爲本身服務。

由此看到,散戶和機構的博弈中,散戶並非總沒有優點的,關鍵是找到有大豬的那個食槽,並等到對本身有利的遊戲規則造成時再進入。

0x7:納什均衡博弈與GAN網絡的關係

GAN的主要靈感來源於博弈論中零和博弈的思想。

應用到深度學習神經網絡上來講,就是經過生成網絡G(Generator)和判別網絡D(Discriminator)不斷博弈,進而使 G 學習到數據的分佈,同時時 D 得到更好的魯棒性和泛化能力。

舉個例子:用在圖片生成上,咱們想讓最後的 G 能夠從一段隨機數中生成逼真的圖像:

上圖中:

  • G是一個生成式的網絡,它接收一個隨機的噪聲 z(隨機數),而後經過這個噪聲生成圖像。

  • D是一個判別網絡,判別一張圖片是否是 「真實的」。它的輸入是一張圖片,輸出的 D(x) 表明 x 爲真實圖片的機率,若是爲 1,就表明 100% 是真實的圖片,而輸出爲 0,就表明不多是真實的圖片。

那麼這個訓練的過程是什麼樣子的呢?在訓練中:

  • G 的目標就是儘可能生成真實的圖片去欺騙判別網絡 D。

  • D的目標就是儘可能辨別出G生成的假圖像和真實的圖像。

這樣,G 和 D 就構成了一個動態的「博弈過程」,最終的平衡點即納什均衡點

Relevant Link:     

https://baijiahao.baidu.com/s?id=1611846467821315306&wfr=spider&for=pc
https://www.jianshu.com/p/fadba906f5d3 

 

2. GAN網絡的思想起源

GAN的起源之做鼻祖是 Ian Goodfellow 在 2014 年發表在 ICLR 的論文:Generative Adversarial Networks」。

按照筆者的理解,提出GAN網絡的出發點有以下幾個:

  • 最核心的做用是提升分類器的魯棒能力,由於生成器不斷生成」儘可能逼近真實樣本「的僞造圖像,而分類器爲了能正確區分出僞造和真實的樣本,就須要不斷地挖掘樣本中真正蘊含的潛在機率信息,而拋棄無用的多餘特徵,這就起到了提升魯棒和泛化能力的做用。從某種程度上來講,GAN起到了和正則化約束的效果。
  • 基於隨機擾動,有針對性地生成新樣本。可是要注意的一點是,GAN生成的樣本並非徹底的未知新樣本,GAN的generator生成的新樣本更多的側重點是經過增長可控的擾動來嘗試躲避discriminator的檢測。實際上,GAN對生成0day樣本的能力頗有限。

爲了清楚地闡述這個概念,筆者先從對抗樣本這個話題開始提及。

0x1:對抗樣本(adversarial example)

對抗樣本(adversarial example)是指通過精心計算獲得的用於誤導分類器的樣本。例以下圖就是一個例子,左邊是一個熊貓,可是添加了少許隨機噪聲變成右圖後,分類器給出的預測類別倒是長臂猿,但視覺上左右兩幅圖片並無太大改變。

出現這種狀況的緣由是什麼呢?

簡單來講,就是預測器發生了過擬合。圖像分類器本質上是高維空間的一個複雜的決策函數,在高維空間上,圖像分類器過度考慮了全像素區間內的細節信息,致使預測器對圖像的細節信息太敏感,微小的擾動就可能致使預測器的預測行爲產生很大的變化。

關於這個話題,筆者在另外一篇文章中對過擬合現象以及規避方法進行了詳細討論。

除了添加」隨機噪聲驅動的像素擾動」這種方法以外,還能夠經過圖像變形的方式,使得新圖像和原始圖像視覺上同樣的狀況下,讓分類器獲得有很高置信度的錯誤分類結果。這種過程也被稱爲對抗攻擊(adversarial attack)

0x2:有監督驅動的無監督學習

人類經過觀察和體驗物理世界來學習,咱們的大腦十分擅長預測,不須要顯式地通過複雜計算就能夠獲得正確的答案。監督學習的過程就是學習數據和標籤之間的相關關係。

可是在非監督學習中,數據並無被標記,並且目標一般也不是對新數據進行預測。

在現實世界中,標記數據是十分稀有和昂貴的。生成對抗網絡經過生成僞造的/合成的數據並嘗試判斷生成樣本真僞的方法學習,這本質上至關於採用了監督學習的方法來作無監督學習。作分類任務的判別器在這裏是一個監督學習的組件,生成器的目標是瞭解真實數據的模樣(機率分佈),並根據學到的知識生成新的數據。

Relevant Link:  

https://www.jiqizhixin.com/articles/2018-03-05-4

 

3. GAN網絡基本原理

GAN網絡發展到現在已經有不少的變種,在arxiv上天天都會有大量的新的研究論文被提出。可是筆者這裏不許備枚舉全部的網絡結構,而是僅僅討論GAN中最核心的思想,經過筆者本身的論文閱讀,將我認爲最精彩的思想和學術創新提煉出來給你們,從此咱們也能夠根據本身的理解,將其餘領域的思想交叉引入進來,繼續不斷創新發展。

0x1:GAN的組成

 

經典的GAN網絡由兩部分組成,分別稱之爲判別器D生成器G,兩個網絡的工做原理能夠以下圖所示,

D 的目標就是判別真實圖片和 G 生成的圖片的真假,而 G 是輸入一個隨機噪聲來生成圖片,並努力欺騙 D。

簡單來講,GAN 的基本思想就是一個最小最大定理,當兩個玩家(D 和 G)彼此競爭時(零和博弈),雙方都假設對方採起最優的步驟而本身也以最優的策略應對(最小最大策略),那麼結果就會進入一個肯定的均衡狀態(納什均衡)。

0x2:損失函數分析

1. 生成器(generator)損失函數

生成器網絡以隨機的噪聲z做爲輸入並試圖生成樣本數據,並將生成的僞造樣本數據提供給判別器網絡D,

能夠看到,G 網絡的訓練目標就是讓 D(G(z)) 趨近於 1,即徹底騙過判別器(判別器將生成器生成的僞造樣本所有誤判爲真)。G 網絡經過接受 D 網絡的反饋做爲梯度改進方向,經過BP過程反向調整本身的網絡結構參數。

2. 判別器(discriminator)

判別器網絡以真實數據x或者僞造數據G(z)做爲輸入,並試圖預測當前輸入是真實數據仍是生成的僞造數據,併產生一個【0,1】範圍內的預測標量值。

D 網絡的訓練目標是區分真假數據,D 網絡的訓練目標是讓 D(x) 趨近於 1(真實的樣本判真),而 D(G(z)) 趨近於0(僞造的樣本判黑)。D 網絡同時接受真實樣本和 G 網絡傳入的僞造樣本做爲梯度改進方向,,經過BP過程反向調整本身的網絡結構參數。

3. 綜合損失函數

生成器和判別器網絡的損失函數結合起來就是生成對抗網絡(GAN)的綜合損失函數:

兩個網絡相互對抗,彼此博弈,如上所示,綜合損失函數是一個極大極小函數;

  • 損失函數第一項:會驅使判別器儘可能將真實樣本都判真
  • 損失函數第二項:會驅使判別器儘可能將僞造樣本都判黑。但同時,生成器G會對抗這個過程

整個相互對抗的過程,Ian Goodfellow 在論文中用下圖來描述:

 

 黑色曲線表示輸入數據 x 的實際分佈,綠色曲線表示的是 G 網絡生成數據的分佈,紫色的曲線表示的是生成數據對應於 D 的分佈的差別距離(KL散度)

GAN網絡訓練的目標是但願着實際分佈曲線x,和G網絡生成的數據,兩條曲線能夠相互重合,也就是兩個數據分佈一致(達到納什均衡)。

  • a圖:網絡剛開始訓練,D 的分類能力還不是最好,所以有所波動,而生成數據的分佈也天然和真實數據分佈不一樣,畢竟 G 網絡輸入是隨機生成的噪聲;
  • b圖:隨着訓練的進行,D 網絡的分類能力就比較好了,能夠看到對於真實數據和生成數據,它是明顯能夠區分出來,也就是給出的機率是不一樣的;
  • c圖:因爲 D 網絡先行提升的性能,隨後 G 網絡開始追趕,G 網絡的目標是學習真實數據的分佈,即綠色的曲線,因此它會往藍色曲線方向移動。由於 G 和 D 是相互對抗的,當 G 網絡提高,也會影響 D 網絡的分辨能力;
  • d圖:當假設 G 網絡不變(G已經優化到收斂狀態),繼續訓練 D 網絡,最優的狀況會是,也就是當生成數據的分佈趨近於真實數據分佈的時候,D 網絡輸出的機率會趨近於 0.5(真實樣本和僞造樣本各佔一半,生成器沒法再僞造了,判別器也沒法再優化了,也能夠說對於判別器來講其沒法從樣本中區分中真實樣本和僞造樣本),這也是最終但願達到的訓練結果,這時候 G 和 D 網絡也就達到一個平衡狀態。

0x3:算法僞碼流程

論文給出的算法實現過程以下所示:

一些細節須要注意:

  • 首先 G 和 D 是同步訓練,但二者訓練次數不同,一般是 D 網絡訓練 k 次後,G 訓練一次。主要緣由是 GAN 剛開始訓練時候會很不穩定,須要讓判別器D儘快先進入收斂區間;
  • D 的訓練是同時輸入真實數據和生成數據來計算 loss,而不是採用交叉熵(cross entropy)分開計算。不採用 cross entropy 的緣由是這會讓 D(G(z)) 變爲 0,致使沒有梯度提供給 G 更新,而如今 GAN 的作法是會收斂到 0.5;
  • 實際訓練的時候,做者是採用來代替,這是但願在訓練初始就能夠加大梯度信息,這是由於初始階段 D 的分類能力會遠大於 G 生成足夠真實數據的能力,但這種修改也將讓整個 GAN 再也不是一個完美的零和博弈。

0x4:算法的優勢

GAN的巧妙之處在於其目標函數的設定,由於此,GAN有以下幾個優勢:

  • GAN 中的 G 做爲生成模型,不須要像傳統圖模型同樣,須要一個嚴格的生成數據的機率表達式。這就避免了當數據很是複雜的時候,複雜度過分增加致使的不可計算。
  • GAN 不須要 inference 模型中的一些龐大計算量的求和計算。它惟一的須要的就是,一個噪音輸入,一堆無標準的真實數據,兩個能夠逼近函數的網絡。

0x5:算法的挑戰與缺陷

初代GAN有一些缺點,或者是說挑戰,筆者這裏介紹以下:
  • 啓動及初始化的問題:GAN的訓練目標是讓生成器和判別器最終達到一個納什均衡狀態,此時兩個網絡都沒法繼續再往前作任何優化,優化結束。梯度降低的啓動會選擇一個減少所定義問題損失的方法,可是並無理論保證GAN必定能夠100%進入納什均衡狀態,這是一個高維度的非凸優化目標。網絡試圖在接下來的步驟中最小化非凸優化目標,可是最終可能致使進入震盪而不是收斂到底層真實目標。
  • GAN 過於自由致使訓練難以收斂以及不穩定。
  • 梯度消失問題:原始 G 的損失函數沒有意義,它是讓 G 最小化 D 識別出本身生成的假樣本的機率,但實際上它會致使梯度消失問題,這是因爲開始訓練的時候,G 生成的圖片很是糟糕,D 能夠垂手可得的識別出來,這樣 D 的訓練沒有任何損失,也就沒有有效的梯度信息回傳給 G 去優化它本身,這就是梯度消失了。最後,雖然做者意識到這個問題,在實際應用中改用來代替,這至關於從最小化 D 揪出本身的機率,變成了最大化 D 抓不到本身的機率。雖然直觀上感受是一致的,但其實並不在理論上等價,也更沒有了理論保證在這樣的替代目標函數訓練下,GAN 還會達到平衡。這個結果會進一步致使模式奔潰問題。
  • 模型坍塌:基本原理是生成器可能會在某種狀況下重複生成徹底一致的圖像(也能夠理解爲梯度消失),這其中的緣由和博弈論中的啓動問題相關。咱們能夠這樣來想象GAN的訓練過程,
    • 先從判別器的角度試圖最大化,再從生成器的角度試圖最小化。若是生成器最小化開始以前,判別器已經徹底最大化,全部工做還能夠正常運行;
    • 若是首先最小化生成器,再從判別器的角度試圖最大化。若是判別器最大化開始以前,生成器已經徹底最小化,那麼工做就沒法運行。緣由在於若是咱們保持判別器不變,它會將空間中的某些點標記爲最有多是真的而不是假的(由於生成器已經最小化了),這樣生成器就會選擇將全部的噪聲輸入映射到那些最可能爲真的點上,這就陷入了局部最優的陷阱中了,優化過程就提早中止了。
固然上面提到的不少缺點已經在後續的學術論文中被新提出的修改算法解決了,咱們接下來討論其主要解決思想。

0x6:提高GAN訓練效果的一些方法

1. 中間層特徵驅動損失函數

針對GAN不穩定的問題,學者們提出了經過 使用判別器中間層的特徵來預測圖像,並將結果做爲監督信息來反饋給生成器
經過這種方式,訓練獲得的生成器的生成數據會匹配真實數據的統計特性以及判別器中間層的預期特徵值。這樣強迫判別器去尋找那些最能很好地判別真實數據的潛在特徵,而不是那些由當前模型生成數據的表層特徵。

2. 小批量度量輸入樣本類似度

模型坍塌的問題能夠經過 引入額外的度量特徵來解決(例如KL散度)。這樣判別器每次收到的是一小批樣本而不是一個單獨樣本,判別器能夠使用例如KL散度來度量樣本之間的距離,這樣就很容易檢測出當前的生成器是否是已經開始坍塌。從而阻止了生成器繼續向局部最大似然點滑落。
整體來講,小批量樣本表現更接近實際,並且能夠保證不一樣樣本之間在空間上有合適的距離。

3. 引入歷史平均

歷史平均的思想是加入一個懲罰項來懲罰那些和歷史平均權重相差過多的權重值。即若是當前參數值和歷史上最近t批該參數平均值的距離越近,給予的懲罰越大。
經過這種方式,能夠緩解目標函數在收斂後期的震盪。

4. 單側標籤平滑

一般狀況下咱們使用標籤0表明真實圖像,使用1表明僞造圖像。咱們還能夠使用一些更平滑的標籤,例如0.1和0.9,它們能夠使得網絡在一些對抗的例子中更加健壯。

5. 輸入規範化

使用tanh做爲生成器最後一層激活函數,能夠得到更平滑的收斂效果。

6. 批規範化

在每個批次的數據中標準化前一層的激活項, 即,應用一個維持激活項平均值接近 0,標準差接近 1 的轉換。

7. 利用ReLU和MaxPool避免梯度稀疏

若是梯度稀疏,GAN博弈的穩定性會受到很大影響,Leaky ReLU對生成和判別器的梯度稀疏問題都會有緩解做用。

Relevant Link:  

https://arxiv.org/pdf/1406.2661.pdf 
https://juejin.im/post/5bdd70886fb9a049f912028d 
http://www.iterate.site/2018/07/27/gan-%E7%94%9F%E6%88%90%E5%AF%B9%E6%8A%97%E7%BD%91%E7%BB%9C%E4%BB%8B%E7%BB%8D/

 

4. 從生成模型和判別模型的機率視角看GAN

在閱讀了不少GAN衍生論文以及GAN原始論文以後,筆者一直在思考的一個問題是:GAN背後的底層思想是什麼?GAN衍生和改進算法的靈感和思路又是從哪裏來的?

通過一段時間思考以及和同行同窗討論後,我得出了一些思考,這裏分享以下,但願對讀者朋友有幫助。

咱們先來看什麼是判別模型和生成模型:

  • 判別式模型學習某種分佈下的條件機率p(y|x),即在特定x條件下y發生的機率。判別器模型十分依賴數據的質量,機率分佈p(y|x)能夠直接將一個特定的x分類到某個標籤y上。以邏輯迴歸爲例,咱們所須要作的是最小化損失函數。
  • 生成式模型學習的是聯合分佈機率p(x,y),x是輸入數據,y是所指望的分類。一個生成模型能夠根據當前數據的假設生成更多新樣本。

從機率論的視角來看,咱們來看一下原始GAN網絡的架構:

  • 生成器本質上是一個由輸入向量和生成器結構所表明的向量組成的聯合機率分佈P(v_input, v_G_structure)
    • v_input:表明一種輸入向量,能夠是隨機噪聲向量z
    • v_G_structure:網絡本質上是對輸入向量進行線性和非線性變化,由於能夠將其抽象爲一個動態變化的向量函數
  • 判別器本質上是一個由(真實樣本,僞造樣本)做爲輸入x,進行後驗預測p(y|x)的機率模型

遵循這種框架進行思考,CGAN只是將v_input中的隨機噪聲z替換成了另外一種向量(文本或者標籤向量),而Pix2pixGAN是將一個圖像向量做爲v_input輸入GAN網絡。

 

5. 從原始GAN網絡中衍生出的流行GAN架構

GAN的發展離不開goodfellow後來的學者們不斷的研究與發展,目前已經提出了不少優秀的新GAN架構,而且這個發展還在繼續。爲了讓本博文能保持必定的環境獨立性,筆者這裏不作完整的羅列與枚舉,相反,筆者但願從兩條脈絡來展開討論:

  • 解決問題導向:爲了解決原始GAN或者當前學術研究中發現的關於GAN網絡的性能和架構問題而提出的新理論與新框架
  • 新場景應用導向:爲了將GAN應用在新的領域中而提出的新的GAN架構

0x1:DCGAN(Deep Convolutional Generative Adversarial Networks)

Alec Radford,Luke Metz,Soumith Chintala等人在「Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks」提出了DCGAN。這是GAN研究的一個重要里程碑,由於它提出了一個重要的架構變化來解決訓練不穩定,模式崩潰和內部協變量轉換等問題。從那時起,基於DCGAN的架構就被應用到了許多GAN架構。

DCGAN的提出主要是爲了解決原始GAN架構的原生架構問題,咱們接下來來討論下。

1. 生成器的架構優化

生成器從潛在空間中獲得100維噪聲向量z,經過一系列卷積上採樣操做,將z映射到一個像素矩陣對應的空間中,以下圖:

DCGAN經過下面的一些架構性約束來固化網絡: 

  • 在判別器中使用步數卷積來取代池化層,在生成器中使用小步數卷積來取代池化層;
  • 在生成器和判別器中均使用批規範化,批規範化是一種經過零均值和單位方差的方法進行輸入規範化使得學習過程固話的技術。這項技術在實踐中被證明能夠在許多場合提高訓練速度,減小初始化不佳帶來的啓動問題,而且一般能產生更準確的結果;
  • 消除原架構中較深的全鏈接隱藏層,而且在最後只使用簡單的平均值池化;
  • 在生成器輸出層使用tanh,在其它層均使用ReLU激發;
  • 在判別器的全部層中都使用Leaky ReLU激發;

2. 模型訓練

生成器和判別器都是經過binary_crossentropy做爲損失函數來進行訓練的。以後的每一個階段,生成器產生一個MNIST圖像,判別器嘗試在真實MNIST圖像和生成圖像的數據集中進行學習。

通過一段時間後,生成器就能夠自動學會如何製做僞造的數字。

from __future__ import print_function, division

from keras.datasets import mnist
from keras.layers import Input, Dense, Reshape, Flatten, Dropout
from keras.layers import BatchNormalization, Activation, ZeroPadding2D
from keras.layers.advanced_activations import LeakyReLU
from keras.layers.convolutional import UpSampling2D, Conv2D
from keras.models import Sequential, Model
from keras.optimizers import Adam

import matplotlib.pyplot as plt

import sys

import numpy as np

class DCGAN():
    def __init__(self):
        # Input shape
        self.img_rows = 28
        self.img_cols = 28
        self.channels = 1
        self.img_shape = (self.img_rows, self.img_cols, self.channels)
        self.latent_dim = 100

        optimizer = Adam(0.0002, 0.5)

        # Build and compile the discriminator
        self.discriminator = self.build_discriminator()
        self.discriminator.compile(
            loss='binary_crossentropy',
            optimizer=optimizer,
            metrics=['accuracy']
        )

        # Build the generator
        self.generator = self.build_generator()

        # The generator takes noise as input and generates imgs
        z = Input(shape=(self.latent_dim,))
        img = self.generator(z)

        # For the combined model we will only train the generator
        self.discriminator.trainable = False

        # The discriminator takes generated images as input and determines validity
        valid = self.discriminator(img)

        # The combined model  (stacked generator and discriminator)
        # Trains the generator to fool the discriminator
        self.combined = Model(z, valid)
        self.combined.compile(loss='binary_crossentropy', optimizer=optimizer)

    def build_generator(self):

        model = Sequential()

        model.add(Dense(128 * 7 * 7, activation="relu", input_dim=self.latent_dim))
        model.add(Reshape((7, 7, 128)))
        model.add(UpSampling2D())
        model.add(Conv2D(128, kernel_size=3, padding="same"))
        model.add(BatchNormalization(momentum=0.8))
        model.add(Activation("relu"))
        model.add(UpSampling2D())
        model.add(Conv2D(64, kernel_size=3, padding="same"))
        model.add(BatchNormalization(momentum=0.8))
        model.add(Activation("relu"))
        model.add(Conv2D(self.channels, kernel_size=3, padding="same"))
        model.add(Activation("tanh"))

        model.summary()

        noise = Input(shape=(self.latent_dim,))
        img = model(noise)

        return Model(noise, img)

    def build_discriminator(self):

        model = Sequential()

        model.add(Conv2D(32, kernel_size=3, strides=2, input_shape=self.img_shape, padding="same"))
        model.add(LeakyReLU(alpha=0.2))
        model.add(Dropout(0.25))
        model.add(Conv2D(64, kernel_size=3, strides=2, padding="same"))
        model.add(ZeroPadding2D(padding=((0,1),(0,1))))
        model.add(BatchNormalization(momentum=0.8))
        model.add(LeakyReLU(alpha=0.2))
        model.add(Dropout(0.25))
        model.add(Conv2D(128, kernel_size=3, strides=2, padding="same"))
        model.add(BatchNormalization(momentum=0.8))
        model.add(LeakyReLU(alpha=0.2))
        model.add(Dropout(0.25))
        model.add(Conv2D(256, kernel_size=3, strides=1, padding="same"))
        model.add(BatchNormalization(momentum=0.8))
        model.add(LeakyReLU(alpha=0.2))
        model.add(Dropout(0.25))
        model.add(Flatten())
        model.add(Dense(1, activation='sigmoid'))

        model.summary()

        img = Input(shape=self.img_shape)
        validity = model(img)

        return Model(img, validity)

    def train(self, epochs, batch_size=128, save_interval=50):

        # Load the dataset
        (X_train, _), (_, _) = mnist.load_data()

        # Rescale -1 to 1
        X_train = X_train / 127.5 - 1.
        X_train = np.expand_dims(X_train, axis=3)

        # Adversarial ground truths
        valid = np.ones((batch_size, 1))
        fake = np.zeros((batch_size, 1))

        for epoch in range(epochs):

            # ---------------------
            #  Train Discriminator
            # ---------------------

            # Select a random half of images
            idx = np.random.randint(0, X_train.shape[0], batch_size)
            imgs = X_train[idx]

            # Sample noise and generate a batch of new images
            noise = np.random.normal(0, 1, (batch_size, self.latent_dim))
            gen_imgs = self.generator.predict(noise)

            # Train the discriminator (real classified as ones and generated as zeros)
            d_loss_real = self.discriminator.train_on_batch(imgs, valid)
            d_loss_fake = self.discriminator.train_on_batch(gen_imgs, fake)
            d_loss = 0.5 * np.add(d_loss_real, d_loss_fake)

            # ---------------------
            #  Train Generator
            # ---------------------

            # Train the generator (wants discriminator to mistake images as real)
            g_loss = self.combined.train_on_batch(noise, valid)

            # Plot the progress
            print ("%d [D loss: %f, acc.: %.2f%%] [G loss: %f]" % (epoch, d_loss[0], 100*d_loss[1], g_loss))

            # If at save interval => save generated image samples
            if epoch % save_interval == 0:
                self.save_imgs(epoch)

    def save_imgs(self, epoch):
        r, c = 5, 5
        noise = np.random.normal(0, 1, (r * c, self.latent_dim))
        gen_imgs = self.generator.predict(noise)

        # Rescale images 0 - 1
        gen_imgs = 0.5 * gen_imgs + 0.5

        fig, axs = plt.subplots(r, c)
        cnt = 0
        for i in range(r):
            for j in range(c):
                axs[i,j].imshow(gen_imgs[cnt, :,:,0], cmap='gray')
                axs[i,j].axis('off')
                cnt += 1
        fig.savefig("images/mnist_%d.png" % epoch)
        plt.close()


if __name__ == '__main__':
    dcgan = DCGAN()
    dcgan.train(epochs=4000, batch_size=32, save_interval=50)

DCGAN產生的手寫數字輸出

0x2:CGAN(Conditional GAN,CGAN)

1. 有輸入條件約束的生成器網絡架構 

CGAN由Mehdi Mirza,Simon Osindero在論文「Conditional Generative Adversarial Nets」中首次提出。

在條件GAN中,生成器並非從一個隨機的噪聲分佈中開始學習,而是經過一個特定的條件或某些特徵(例如一個圖像標籤或者一些文本信息)開始學習如何生成僞造樣本。

 

在CGAN中,生成器和判別器的輸入都會增長一些條件變量y,這樣判別器D(x,y)和生成器G(z,y)都有了一組聯合條件變量。

咱們將CGAN的目標函數和GAN進行對比會發現:

 

 GAN目標函數

 

CGAN目標函數

GAN和CGAN的損失函數區別在於判別器和生成器多出來一個參數y,架構上,CGAN相比於GAN增長了一個輸入層條件向量C,同時鏈接了判別器和生成器網絡。

 

2. 訓練過程

在訓練過程,咱們將y輸入給生成器和判別器網絡。 

from __future__ import print_function, division

from keras.datasets import mnist
from keras.layers import Input, Dense, Reshape, Flatten, Dropout, multiply
from keras.layers import BatchNormalization, Activation, Embedding, ZeroPadding2D
from keras.layers.advanced_activations import LeakyReLU
from keras.layers.convolutional import UpSampling2D, Conv2D
from keras.models import Sequential, Model
from keras.optimizers import Adam

import matplotlib.pyplot as plt

import numpy as np

class CGAN():
    def __init__(self):
        # Input shape
        self.img_rows = 28
        self.img_cols = 28
        self.channels = 1
        self.img_shape = (self.img_rows, self.img_cols, self.channels)
        self.num_classes = 10
        self.latent_dim = 100

        optimizer = Adam(0.0002, 0.5)

        # Build and compile the discriminator
        self.discriminator = self.build_discriminator()
        self.discriminator.compile(
            loss=['binary_crossentropy'],
            optimizer=optimizer,
            metrics=['accuracy']
        )

        # Build the generator
        self.generator = self.build_generator()

        # The generator takes noise and the target label as input
        # and generates the corresponding digit of that label
        noise = Input(shape=(self.latent_dim,))
        label = Input(shape=(1,))
        img = self.generator([noise, label])

        # For the combined model we will only train the generator
        self.discriminator.trainable = False

        # The discriminator takes generated image as input and determines validity
        # and the label of that image
        valid = self.discriminator([img, label])

        # The combined model  (stacked generator and discriminator)
        # Trains generator to fool discriminator
        self.combined = Model([noise, label], valid)
        self.combined.compile(loss=['binary_crossentropy'],
            optimizer=optimizer)

    def build_generator(self):

        model = Sequential()

        model.add(Dense(256, input_dim=self.latent_dim))
        model.add(LeakyReLU(alpha=0.2))
        model.add(BatchNormalization(momentum=0.8))
        model.add(Dense(512))
        model.add(LeakyReLU(alpha=0.2))
        model.add(BatchNormalization(momentum=0.8))
        model.add(Dense(1024))
        model.add(LeakyReLU(alpha=0.2))
        model.add(BatchNormalization(momentum=0.8))
        model.add(Dense(np.prod(self.img_shape), activation='tanh'))
        model.add(Reshape(self.img_shape))

        model.summary()

        noise = Input(shape=(self.latent_dim,))
        label = Input(shape=(1,), dtype='int32')
        label_embedding = Flatten()(Embedding(self.num_classes, self.latent_dim)(label))

        model_input = multiply([noise, label_embedding])
        img = model(model_input)

        return Model([noise, label], img)

    def build_discriminator(self):

        model = Sequential()

        model.add(Dense(512, input_dim=np.prod(self.img_shape)))
        model.add(LeakyReLU(alpha=0.2))
        model.add(Dense(512))
        model.add(LeakyReLU(alpha=0.2))
        model.add(Dropout(0.4))
        model.add(Dense(512))
        model.add(LeakyReLU(alpha=0.2))
        model.add(Dropout(0.4))
        model.add(Dense(1, activation='sigmoid'))
        model.summary()

        img = Input(shape=self.img_shape)
        label = Input(shape=(1,), dtype='int32')

        label_embedding = Flatten()(Embedding(self.num_classes, np.prod(self.img_shape))(label))
        flat_img = Flatten()(img)

        model_input = multiply([flat_img, label_embedding])

        validity = model(model_input)

        return Model([img, label], validity)

    def train(self, epochs, batch_size=128, sample_interval=50):

        # Load the dataset
        (X_train, y_train), (_, _) = mnist.load_data()

        # Configure input
        X_train = (X_train.astype(np.float32) - 127.5) / 127.5
        X_train = np.expand_dims(X_train, axis=3)
        y_train = y_train.reshape(-1, 1)

        # Adversarial ground truths
        valid = np.ones((batch_size, 1))
        fake = np.zeros((batch_size, 1))

        for epoch in range(epochs):

            # ---------------------
            #  Train Discriminator
            # ---------------------

            # Select a random half batch of images
            idx = np.random.randint(0, X_train.shape[0], batch_size)
            imgs, labels = X_train[idx], y_train[idx]

            # Sample noise as generator input
            noise = np.random.normal(0, 1, (batch_size, 100))

            # Generate a half batch of new images
            gen_imgs = self.generator.predict([noise, labels])

            # Train the discriminator
            d_loss_real = self.discriminator.train_on_batch([imgs, labels], valid)
            d_loss_fake = self.discriminator.train_on_batch([gen_imgs, labels], fake)
            d_loss = 0.5 * np.add(d_loss_real, d_loss_fake)

            # ---------------------
            #  Train Generator
            # ---------------------

            # Condition on labels
            sampled_labels = np.random.randint(0, 10, batch_size).reshape(-1, 1)

            # Train the generator
            g_loss = self.combined.train_on_batch([noise, sampled_labels], valid)

            # Plot the progress
            print ("%d [D loss: %f, acc.: %.2f%%] [G loss: %f]" % (epoch, d_loss[0], 100*d_loss[1], g_loss))

            # If at save interval => save generated image samples
            if epoch % sample_interval == 0:
                self.sample_images(epoch)

    def sample_images(self, epoch):
        r, c = 2, 5
        noise = np.random.normal(0, 1, (r * c, 100))
        sampled_labels = np.arange(0, 10).reshape(-1, 1)

        gen_imgs = self.generator.predict([noise, sampled_labels])

        # Rescale images 0 - 1
        gen_imgs = 0.5 * gen_imgs + 0.5

        fig, axs = plt.subplots(r, c)
        cnt = 0
        for i in range(r):
            for j in range(c):
                axs[i,j].imshow(gen_imgs[cnt,:,:,0], cmap='gray')
                axs[i,j].set_title("Digit: %d" % sampled_labels[cnt])
                axs[i,j].axis('off')
                cnt += 1
        fig.savefig("images/%d.png" % epoch)
        plt.close()


if __name__ == '__main__':
    cgan = CGAN()
    cgan.train(epochs=20000, batch_size=32, sample_interval=200)

根據輸入數字生成對應的MNIST手寫數字圖像

0x3:CycleGAN(Cycle Consistent GAN,循環一致生成網絡)

CycleGANs 由Jun-Yan Zhu,Taesung Park,Phillip Isola和Alexei A. Efros在題爲「Unpaired Image-to-Image Translation using Cycle-Consistent Adversarial Networks」的論文中提出

CycleGAN用來實現不須要其餘額外信息,就能將一張圖像從源領域映射到目標領域的方法,例如將照片轉換爲繪畫,將夏季拍攝的照片轉換爲冬季拍攝的照片,或將馬的照片轉換爲斑馬照片,或者相反。總結來講,CycleGAN常備用於不一樣的圖像到圖像翻譯。

 

1. 循環網絡架構

CycleGAN背後的核心思想是兩個轉換器F和G,其中:

  • F會將圖像從域A轉換到域B;
  • G會將圖像從域B轉換到域A;

所以,

  • 對於一個在域A的圖像x,咱們指望函數G(F(x))的結果與x相同,即 x == G(F(x));
  • 對於一個在域B的圖像y,咱們指望函數F(G(y))的結果與y相同,即 y == F(G(y));

和原始的GAN結構相比,由單個G->D的單向開放結構,變成了由兩對G<->D組成的雙向循環的封閉結構,但形式上依然是G給D輸入僞造樣本。但區別在於梯度的反饋是雙向循環的。

2. 損失函數

CycleGAN模型有如下兩個損失函數:

  • 對抗損失(Adversarial Loss):判別器和生成器之間互相對抗的損失,這就是原始GAN網絡的損失函數公式:
  • 循環一致損失(Cycle Consistency Loss):綜合權衡轉換器F和G的損失,F和G之間是編碼與解碼的對抗關係,不可能同時取到最小值,只能獲得總體的平衡最優值:

完整的CycleGAN目標函數以下:

from __future__ import print_function, division
import scipy

from keras.datasets import mnist
from keras_contrib.layers.normalization.instancenormalization import InstanceNormalization
from keras.layers import Input, Dense, Reshape, Flatten, Dropout, Concatenate
from keras.layers import BatchNormalization, Activation, ZeroPadding2D
from keras.layers.advanced_activations import LeakyReLU
from keras.layers.convolutional import UpSampling2D, Conv2D
from keras.models import Sequential, Model
from keras.optimizers import Adam
import datetime
import matplotlib.pyplot as plt
import sys
from data_loader import DataLoader
import numpy as np
import os

class CycleGAN():
    def __init__(self):
        # Input shape
        self.img_rows = 128
        self.img_cols = 128
        self.channels = 3
        self.img_shape = (self.img_rows, self.img_cols, self.channels)

        # Configure data loader
        self.dataset_name = 'horse2zebra'
        self.data_loader = DataLoader(
            dataset_name=self.dataset_name,
            img_res=(self.img_rows, self.img_cols)
        )

        # Calculate output shape of D (PatchGAN)
        patch = int(self.img_rows / 2**4)
        self.disc_patch = (patch, patch, 1)

        # Number of filters in the first layer of G and D
        self.gf = 32
        self.df = 64

        # Loss weights
        self.lambda_cycle = 10.0                    # Cycle-consistency loss
        self.lambda_id = 0.1 * self.lambda_cycle    # Identity loss

        optimizer = Adam(0.0002, 0.5)

        # Build and compile the discriminators
        self.d_A = self.build_discriminator()
        self.d_B = self.build_discriminator()
        self.d_A.compile(
            loss='mse',
            optimizer=optimizer,
            metrics=['accuracy']
        )
        self.d_B.compile(
            loss='mse',
            optimizer=optimizer,
            metrics=['accuracy']
        )

        # -------------------------
        # Construct Computational
        #   Graph of Generators
        # -------------------------

        # Build the generators
        self.g_AB = self.build_generator()
        self.g_BA = self.build_generator()

        # Input images from both domains
        img_A = Input(shape=self.img_shape)
        img_B = Input(shape=self.img_shape)

        # Translate images to the other domain
        fake_B = self.g_AB(img_A)
        fake_A = self.g_BA(img_B)
        # Translate images back to original domain
        reconstr_A = self.g_BA(fake_B)
        reconstr_B = self.g_AB(fake_A)
        # Identity mapping of images
        img_A_id = self.g_BA(img_A)
        img_B_id = self.g_AB(img_B)

        # For the combined model we will only train the generators
        self.d_A.trainable = False
        self.d_B.trainable = False

        # Discriminators determines validity of translated images
        valid_A = self.d_A(fake_A)
        valid_B = self.d_B(fake_B)

        # Combined model trains generators to fool discriminators
        self.combined = Model(
            inputs=[img_A, img_B],
            outputs=[valid_A, valid_B, reconstr_A, reconstr_B, img_A_id, img_B_id ]
        )
        self.combined.compile(
            loss=['mse', 'mse', 'mae', 'mae', 'mae', 'mae'],
            loss_weights=[1, 1, self.lambda_cycle, self.lambda_cycle, self.lambda_id, self.lambda_id],
            optimizer=optimizer
        )

    def build_generator(self):
        """U-Net Generator"""

        def conv2d(layer_input, filters, f_size=4):
            """Layers used during downsampling"""
            d = Conv2D(filters, kernel_size=f_size, strides=2, padding='same')(layer_input)
            d = LeakyReLU(alpha=0.2)(d)
            d = InstanceNormalization()(d)
            return d

        def deconv2d(layer_input, skip_input, filters, f_size=4, dropout_rate=0):
            """Layers used during upsampling"""
            u = UpSampling2D(size=2)(layer_input)
            u = Conv2D(filters, kernel_size=f_size, strides=1, padding='same', activation='relu')(u)
            if dropout_rate:
                u = Dropout(dropout_rate)(u)
            u = InstanceNormalization()(u)
            u = Concatenate()([u, skip_input])
            return u

        # Image input
        d0 = Input(shape=self.img_shape)

        # Downsampling
        d1 = conv2d(d0, self.gf)
        d2 = conv2d(d1, self.gf*2)
        d3 = conv2d(d2, self.gf*4)
        d4 = conv2d(d3, self.gf*8)

        # Upsampling
        u1 = deconv2d(d4, d3, self.gf*4)
        u2 = deconv2d(u1, d2, self.gf*2)
        u3 = deconv2d(u2, d1, self.gf)

        u4 = UpSampling2D(size=2)(u3)
        output_img = Conv2D(self.channels, kernel_size=4, strides=1, padding='same', activation='tanh')(u4)

        return Model(d0, output_img)

    def build_discriminator(self):

        def d_layer(layer_input, filters, f_size=4, normalization=True):
            """Discriminator layer"""
            d = Conv2D(filters, kernel_size=f_size, strides=2, padding='same')(layer_input)
            d = LeakyReLU(alpha=0.2)(d)
            if normalization:
                d = InstanceNormalization()(d)
            return d

        img = Input(shape=self.img_shape)

        d1 = d_layer(img, self.df, normalization=False)
        d2 = d_layer(d1, self.df*2)
        d3 = d_layer(d2, self.df*4)
        d4 = d_layer(d3, self.df*8)

        validity = Conv2D(1, kernel_size=4, strides=1, padding='same')(d4)

        return Model(img, validity)

    def train(self, epochs, batch_size=1, sample_interval=50):

        start_time = datetime.datetime.now()

        # Adversarial loss ground truths
        valid = np.ones((batch_size,) + self.disc_patch)
        fake = np.zeros((batch_size,) + self.disc_patch)

        for epoch in range(epochs):
            for batch_i, (imgs_A, imgs_B) in enumerate(self.data_loader.load_batch(batch_size)):

                # ----------------------
                #  Train Discriminators
                # ----------------------

                # Translate images to opposite domain
                fake_B = self.g_AB.predict(imgs_A)
                fake_A = self.g_BA.predict(imgs_B)

                # Train the discriminators (original images = real / translated = Fake)
                dA_loss_real = self.d_A.train_on_batch(imgs_A, valid)
                dA_loss_fake = self.d_A.train_on_batch(fake_A, fake)
                dA_loss = 0.5 * np.add(dA_loss_real, dA_loss_fake)

                dB_loss_real = self.d_B.train_on_batch(imgs_B, valid)
                dB_loss_fake = self.d_B.train_on_batch(fake_B, fake)
                dB_loss = 0.5 * np.add(dB_loss_real, dB_loss_fake)

                # Total disciminator loss
                d_loss = 0.5 * np.add(dA_loss, dB_loss)


                # ------------------
                #  Train Generators
                # ------------------

                # Train the generators
                g_loss = self.combined.train_on_batch([imgs_A, imgs_B],
                                                        [valid, valid,
                                                        imgs_A, imgs_B,
                                                        imgs_A, imgs_B])

                elapsed_time = datetime.datetime.now() - start_time

                # Plot the progress
                print ("[Epoch %d/%d] [Batch %d/%d] [D loss: %f, acc: %3d%%] [G loss: %05f, adv: %05f, recon: %05f, id: %05f] time: %s " \
                                                                        % ( epoch, epochs,
                                                                            batch_i, self.data_loader.n_batches,
                                                                            d_loss[0], 100*d_loss[1],
                                                                            g_loss[0],
                                                                            np.mean(g_loss[1:3]),
                                                                            np.mean(g_loss[3:5]),
                                                                            np.mean(g_loss[5:6]),
                                                                            elapsed_time))

                # If at save interval => save generated image samples
                if batch_i % sample_interval == 0:
                    self.sample_images(epoch, batch_i)

    def sample_images(self, epoch, batch_i):
        if not os.path.exists('images/%s' % self.dataset_name):
            os.makedirs('images/%s' % self.dataset_name)
        r, c = 2, 3

        imgs_A = self.data_loader.load_data(domain="A", batch_size=1, is_testing=True)
        imgs_B = self.data_loader.load_data(domain="B", batch_size=1, is_testing=True)

        # Demo (for GIF)
        #imgs_A = self.data_loader.load_img('datasets/apple2orange/testA/n07740461_1541.jpg')
        #imgs_B = self.data_loader.load_img('datasets/apple2orange/testB/n07749192_4241.jpg')

        # Translate images to the other domain
        fake_B = self.g_AB.predict(imgs_A)
        fake_A = self.g_BA.predict(imgs_B)
        # Translate back to original domain
        reconstr_A = self.g_BA.predict(fake_B)
        reconstr_B = self.g_AB.predict(fake_A)

        gen_imgs = np.concatenate([imgs_A, fake_B, reconstr_A, imgs_B, fake_A, reconstr_B])

        # Rescale images 0 - 1
        gen_imgs = 0.5 * gen_imgs + 0.5

        titles = ['Original', 'Translated', 'Reconstructed']
        fig, axs = plt.subplots(r, c)
        cnt = 0
        for i in range(r):
            for j in range(c):
                axs[i,j].imshow(gen_imgs[cnt])
                axs[i, j].set_title(titles[j])
                axs[i,j].axis('off')
                cnt += 1
        fig.savefig("images/%s/%d_%d.png" % (self.dataset_name, epoch, batch_i))
        plt.close()


if __name__ == '__main__':
    gan = CycleGAN()
    gan.train(epochs=200, batch_size=1, sample_interval=200)

蘋果->橙子->蘋果 

有相似架構思想的還有DiscoGAN,相關論文能夠在axiv上找到。

0x4:StackGAN

StackJANs由Han Zhang,Tao Xu,Hongsheng Li還有其餘人在題爲「StackGAN: Text to Photo-Realistic Image Synthesis with Stacked Generative Adversarial Networks」的論文中提出。他們使用StackGAN來探索文本到圖像的合成,獲得了很是好的結果。

一個StackGAN由一對網絡組成,當提供文本描述時,能夠生成逼真的圖像。

0x5:Pix2pix

pix2pix網絡由Phillip Isola,Jun-Yan Zhu,Tinghui Zhou和Alexei A. Efros在他們的題爲「Image-to-Image Translation with Conditional Adversarial Networks」的論文中提出。

對於圖像到圖像的翻譯任務,pix2pix也顯示出了使人印象深入的結果。不管是將夜間圖像轉換爲白天的圖像仍是給黑白圖像着色,或者將草圖轉換爲逼真的照片等等,Pix2pix在這些例子中都表現很是出色。

0x6:Age-cGAN(Age Conditional Generative Adversarial Networks)

Grigory Antipov,Moez Baccouche和Jean-Luc Dugelay在他們的題爲「Face Aging with Conditional Generative Adversarial Networks」的論文中提出了用條件GAN進行面部老化。

面部老化有許多行業用例,包括跨年齡人臉識別,尋找失蹤兒童,或者用於娛樂,本質上它屬於cGAN的一種場景應用。

Relevant Link:  

https://arxiv.org/pdf/1511.06434.pdf
https://github.com/hindupuravinash/the-gan-zoo
https://github.com/eriklindernoren/Keras-GAN
https://zhuanlan.zhihu.com/p/63428113

 

6. 基於GAN自動生成Webshell樣本

0x1:原始GAN結構在NLP領域應用的挑戰

咱們用DNN架構重寫原始GAN代碼,並使用一批php webshell做爲真實樣本,嘗試用GAN進行僞造樣本生成。

from keras.layers import Input, Dense, Reshape, Flatten, Dropout
from keras.layers import BatchNormalization, Activation, ZeroPadding2D
from keras.layers.advanced_activations import LeakyReLU
from keras.layers.convolutional import UpSampling2D, Conv2D
from keras.models import Sequential, Model
from keras.optimizers import Adam

from keras.preprocessing import sequence
from sklearn.externals import joblib

import re
import os

import numpy as np
# np.set_printoptions(threshold=np.nan)

class DCGAN():
    def __init__(self):
        # Input shape
        self.charlen = 64
        self.fileshape = (self.charlen, )
        self.latent_dim = 100

        self.ENCODER = joblib.load("./CHAR_SEQUENCE_TOKENIZER_INDEX_TABLE_PICKLE.encoder")
        self.rerange_dim = (len(self.ENCODER.word_index) + 1) / 2. - 0.5

        optimizer = Adam(0.0002, 0.5)

        # Build and compile the discriminator
        self.discriminator = self.build_discriminator()
        self.discriminator.compile(
            loss='binary_crossentropy',
            optimizer=optimizer,
            metrics=['accuracy']
        )

        # Build the generator
        self.generator = self.build_generator()

        # The generator takes noise as input and generates imgs
        z = Input(shape=(self.latent_dim,))
        img = self.generator(z)

        # For the combined model we will only train the generator
        self.discriminator.trainable = False

        # The discriminator takes generated images as input and determines validity
        valid = self.discriminator(img)

        # The combined model  (stacked generator and discriminator)
        # Trains the generator to fool the discriminator
        self.combined = Model(z, valid)
        self.combined.compile(loss='binary_crossentropy', optimizer=optimizer)

    def build_generator(self):

        model = Sequential()

        model.add(Dense(64, activation="relu"))
        model.add(Dense(128, activation="relu"))
        model.add(Dense(256, activation="relu"))
        model.add(Dense(128, activation="relu"))
        model.add(Dense(self.charlen, activation="relu"))

        # model.summary()

        noise = Input(shape=(self.latent_dim,))
        img = model(noise)

        return Model(noise, img)

    def build_discriminator(self):

        model = Sequential()

        model.add(Dense(128, activation="relu"))
        model.add(LeakyReLU(alpha=0.2))
        model.add(Dropout(0.5))
        model.add(Dense(256, activation="relu"))
        model.add(LeakyReLU(alpha=0.2))
        model.add(Dropout(0.5))
        model.add(Dense(512, activation="relu"))
        model.add(LeakyReLU(alpha=0.2))
        model.add(Dropout(0.5))
        model.add(Dense(128, activation="relu"))
        model.add(LeakyReLU(alpha=0.2))
        model.add(Dropout(0.5))
        model.add(Dense(1, activation='sigmoid'))

        # model.summary()

        img = Input(shape=self.fileshape)
        validity = model(img)

        return Model(img, validity)

    def train(self, epochs, batch_size=64, save_interval=50):
        # Load the dataset
        X_train = self.load_webfile_data()

        # Adversarial ground truths
        valid = np.ones((batch_size, 1))
        fake = np.zeros((batch_size, 1))

        for epoch in range(epochs):

            # ---------------------
            #  Train Discriminator
            # ---------------------

            # Select a random half of images
            idx = np.random.randint(0, X_train.shape[0], batch_size)
            imgs = X_train[idx]

            # Sample noise and generate a batch of new images
            noise = np.random.normal(0, 1, (batch_size, self.latent_dim))
            gen_imgs = self.generator.predict(noise)
            # print gen_imgs
            # print np.shape(gen_imgs)

            # Train the discriminator (real classified as ones and generated as zeros)
            d_loss_real = self.discriminator.train_on_batch(imgs, valid)
            d_loss_fake = self.discriminator.train_on_batch(gen_imgs, fake)
            d_loss = 0.5 * np.add(d_loss_real, d_loss_fake)

            # ---------------------
            #  Train Generator
            # ---------------------

            # Train the generator (wants discriminator to mistake images as real)
            g_loss = self.combined.train_on_batch(noise, valid)

            # Plot the progress
            print ("%d [D loss: %f, acc.: %.2f%%] [G loss: %f]" % (epoch, d_loss[0], 100*d_loss[1], g_loss))

            # If at save interval => save generated image samples
            if epoch % save_interval == 0:
                self.save_imgs(epoch)

    def save_imgs(self, epoch):
        r, c = 5, 5
        noise = np.random.normal(1, 2, (r * c, self.latent_dim))
        gen_imgs = self.generator.predict(noise)

        # Rescale [-1,1] back to [0, ascii_char] range
        gen_imgs = (gen_imgs + 1.) * self.rerange_dim
        gen_text_vec = gen_imgs.reshape((np.shape(gen_imgs)[0], self.charlen))
        gen_text_vec = gen_text_vec.astype(int)
        # reconver back to ascii
        #print "gen_text_vec: ", gen_text_vec
        gen_text = self.ENCODER.sequences_to_texts(gen_text_vec)
        #print "gen_text:", gen_text
        with open('./gen_webfile/{0}.txt'.format(epoch), 'wb') as f:
            for file_vec in gen_text:
                fcontent = ""
                for c in file_vec:
                    fcontent += c
                fcontent = re.sub(r"\s+", "", fcontent)
                f.write(fcontent)

    def load_webfile_data(self):
        vec_dict = {
            'raw_ascii': []
        }

        rootDir = "./webdata"
        for lists in os.listdir(rootDir):
            if lists == '.DS_Store':
                continue
            webpath = os.path.join(rootDir, lists)
            with open(webpath, 'r') as fp:
                fcontent = fp.read()
                # remove space
                fcontent = re.sub(r"\s+", " ", fcontent)
                fcontent_ = ""
                for c in fcontent:
                    fcontent_ += c + " "
                vec_dict['raw_ascii'].append(fcontent_)

        # convert to ascii sequence vec
        raw_ascii_sequence_vec = self.ENCODER.texts_to_sequences(vec_dict['raw_ascii'])
        raw_ascii_sequence_vec = sequence.pad_sequences(
            raw_ascii_sequence_vec,
            maxlen=self.charlen, padding='post',
            truncating='post',
            dtype='float32'
        )

        # reshape to 2d array
        raw_ascii_sequence_vec = raw_ascii_sequence_vec.reshape((np.shape(raw_ascii_sequence_vec)[0], self.charlen))

        # ascii is range in [1, 128], we need Rescale -1 to 1
        print "rerange_dim: ", self.rerange_dim
        raw_ascii_sequence_vec = raw_ascii_sequence_vec / self.rerange_dim - 1.

        # raw_ascii_sequence_vec = np.expand_dims(raw_ascii_sequence_vec, axis=3)
        print "np.shape(raw_ascii_sequence_vec): ", np.shape(raw_ascii_sequence_vec)

        return raw_ascii_sequence_vec


if __name__ == '__main__':
    dcgan = DCGAN()
    dcgan.train(epochs=8000, batch_size=8, save_interval=20)
    #print dcgan.load_webfile_data()

實驗的結果並不理想,GAN很快遇到了模型坍塌問題,從G生成的樣原本看,網絡很快陷入了一個局部最優區間中。

關於這個問題,學術界已經有比較多的討論和分析,筆者這裏列舉以下:

  • 原始GAN主要應用實數空間(連續型數據)上,在生成離散數據(texts)這個問題上並不work。最初的 GANs 僅僅定義在實數領域,GANs 經過訓練出的生成器來產生合成數據,而後在合成數據上運行判別器,判別器的輸出梯度將會產生梯度反饋,告訴生成器如何經過略微改變合成數據而使其更加現實。通常來講只有在數據連續的狀況下,生成器才能夠略微改變合成的數據,而若是數據是離散的,則不能簡單的經過改變合成數據。例如,若是你輸出了一張圖片,其像素值是1.0,那麼接下來你能夠將這個值改成1.0001。若是輸出了一個單詞「penguin」,那麼接下來就不能將其改變爲「penguin + .001」,由於沒有「penguin +.001」這個單詞。 由於全部的天然語言處理(NLP)的基礎都是離散值,如「單詞」、「字母」或者「音節」。
  • Sparse reward:adversarial training 沒起做用很大的一個緣由就在於,discriminator 提供的 reward 具有的 guide signal 太少,Classifier-based Discriminator 提供的只是一個爲真或者假的機率做爲 reward,而這個 reward 在大部分狀況下,是 0。這是由於對於 CNN 來講,分出 fake text 和 real text 是很是容易的,CNN 能在 Classification 任務上作到 99% 的 accuracy,而建模 Language Model 來進行生成,是很是困難的。除此之外,即便 generator 在這樣的 reward 指導下有一些提高,此後的 reward 依舊很小。

  • Search complexity:在以SeqGAN爲表明的用RNN做爲生成器G的一類的工做中,對於 Reward 的評估都是基於句級別的,也就是會先使用 Monte Carlo Search 的方法將句子進行補全再交給 Discriminator,可是這個採樣方法的時間複雜度是 O(nmL2),其中 n 是 batch size,m 是採樣的次數,L 是句子的 max len。就 SeqGAN 的實驗來講,每次計算 reward 就會來帶很大的開銷。

0x2:GAN In NLP的主要發展思路和方向

基本上說,學術界對文本的見解是將其是作一個時序依賴的序列,因此主流方向是使用RNN/LSTM這類模型做爲生成器來生成僞造文本序列。而接下要要解決的重點問題是,如何有效地將判別器的反饋有效地傳遞給生成器

  • 增長reward signal強度和平滑度:從這一點出發,現有很多工做一方法再也不使用簡單的 fake/true probability 做爲 reward。

    • LeakyGAN(把 CNN 的 feature 泄露給 generator),RankGAN (用 IR 中的排序做爲 reward)等工做來提供更加豐富的 reward;

    • 另外一個解決的思路是使用 language model-based discriminator,以提供更多的區分度,北大孫栩老師組的 DP-GAN 在使用了 Languag model discrminator 以後,在 true data 和 fake data 中間架起了一座橋樑:

  • 使用離散數據的可導的損失函數:經過改造原始softmax函數,使用新的gumble softmax,它能夠代替policy gradient,直接可導了。
  • 使用RL提供梯度反饋:使用RL的policy gradient代替原始gradient,將reward傳導回去,這是如今比較主流的作法

Relevant Link:  

https://github.com/LantaoYu/SeqGAN 
https://zhuanlan.zhihu.com/p/25168509
https://tobiaslee.top/2018/09/30/Text-Generation-with-GAN/
https://zhuanlan.zhihu.com/p/36880287
https://www.jianshu.com/p/32e164883eab
相關文章
相關標籤/搜索