咱們先來看看納什均衡的經濟學定義:php
所謂納什均衡,指的是參與人的這樣一種策略組合,在該策略組合上,任何參與人單獨改變策略都不會獲得好處。換句話說,若是在一個策略組合上,當全部其餘人都不改變策略時,沒有人會改變本身的策略,則該策略組合就是一個納什均衡。css
市場上有2家企業A和B,都是賣紙的,紙的成本都是2元錢,A和B都賣5塊錢。在最開始,A、B企業都是盈利3塊,這種狀態叫」社會最優解(Social optimal solution)「。但問題是,社會最優解是一個不穩定的狀態,就如同下圖中這個優化曲面上那個紅球點同樣,雖然該小球目前處於曲面最高點,可是隻要施加一些輕微的擾動,小球就會馬上向山下滑落:html
如今企業A和B準備開展商業競爭:git
但若是價格戰一直這樣打下去,這個過程顯然不可能無限迭代下去。當A和B都降價到了3塊時,雙方都達到了成本的臨界點,既不敢漲價,也不敢降價。漲價了市場就丟了,降價了,就賺不到錢甚至賠錢。因此A和B都不會再去作改變,這就是納什均衡。github
A和B怎樣可以得到最大利潤呢,就是A和B坐到一塊兒商量,同時把價格提升,這就叫共謀,但法律爲了保障消費者利益,禁止共謀。補充一句,共謀在機器學習中被稱做」模型坍塌「,指的對對抗的模型雙方都進入了一個互相承認的局部最優區而再也不變化,具體的技術細節咱們後面會討論。web
囚徒困境是說:有兩個小偷集體做案,而後被警察捉住。警察對兩我的分別審訊,而且告訴他們政策:算法
兩我的的收益狀況以下所示:shell
由於A和B是不能互相通訊的,所以這是一個靜態不徹底信息博弈,咱們分別考慮雙方的決策面:網絡
所以最終納什均衡點在兩我的都坦白,各判八年這裏。架構
顯然,集體最優解在兩我的都抗拒,這樣一來每一個人都判一年就出來了。可是,納什均衡點卻不在這裏。並且,在納什均衡點上,任何一我的都沒有改變本身決策的動力。由於一旦單方面改變決策,那我的的收益就會降低。
咱們知道,在國內開車夾塞很常見。若是你們都不夾塞,是總體的最優解,可是按照納什均衡理論,任何一個司機都會考慮,不管別人是否夾塞,我夾塞均可以使本身的收益變大。因而最終你們都會夾塞,加重擁堵,反而不如你們都不加塞走的快。
那麼,有沒有辦法使我的最優變成集體最優呢?方法就是共謀。兩個小偷在做案以前能夠說好,我們若是進去了,必定都抗拒。若是你這一次敢反悔,那麼之後道上的人不再會有人跟你一塊兒了。也就是說,在屢次博弈過程當中,共謀是可能的。可是若是這個小偷想幹完這一票就走,共謀就是不牢靠的。
在社會領域,共謀是靠法律完成的。你們約定的共謀結論就是法律,若是有人不按照約定作,就會受到法律的懲罰。經過這種方式保證最終決策從我的最優的納什均衡點變爲集體最優勢。
另一方面,如今不少汽車廠商提出了車聯網的概念,在路上的每一輛車都經過物聯網連成一個臨時網絡,全部車按照一個最優的協同算法共同協定最優的行車路線、行車速度、路口等待等行爲,這樣總體交通能夠達到一個總體最優,全部人都節省了時間。
彼此痛恨的甲、乙、丙三個槍手準備決鬥,他們各自的水平以下:
首先明確一點,這是一個靜態不徹底信息博弈,每一個搶手在開槍前都不知道其餘對手的策略,只能在猜想其餘對手策略的基礎上,選擇對本身最優的策略。
咱們來分析一下第一輪槍戰各個槍手的策略。
第一輪槍戰事後,有幾種可能的結果:
如今進入第二輪槍戰:
除非第一輪甲乙雙亡,不然丙就必定處於劣勢,由於不論甲或乙,他們的命中率都比丙的命中率爲高。
這就是槍手丙的悲哀。能力不行的丙玩些花樣雖然能在第一輪槍戰中暫時獲勝。可是,若是甲乙在第一輪槍戰中沒有雙亡的話,在第二輪槍戰結束後,丙的存活的概率就必定比甲或乙爲低。
這彷佛說明,能力差的人在競爭中耍弄手腕能贏一時,但最終每每不能成事。
咱們如今改變遊戲規則,假定甲乙丙不是同時開槍,而是他們輪流開一槍。先假定開槍的順序是甲、乙、丙,咱們來分析一下槍戰過程:
若是是丙先開槍,狀況又如何呢?
咱們經過這個例子,能夠理解人們在博弈中可否獲勝,不單純取決於他們的實力,更重要的是取決於博弈方實力對比所造成的關係。
在上面的例子中,乙和丙其實是一種聯盟關係,先把甲幹掉,他們的生存概率都上升了。咱們如今來判斷一下,乙和丙之中,誰更有可能背叛,誰更可能忠誠?
任何一個聯盟的成員都會時刻權衡利弊,一旦背叛的好處大於忠誠的好處,聯盟就會破裂。在乙和丙的聯盟中,乙是最忠誠的。這不是由於乙自己具備更加忠誠的品質,而是利益關係使然。只要甲不死,乙的槍口就必定會瞄準甲。但丙就不是這樣了,丙不瞄準甲而胡亂開一槍顯然違背了聯盟關係,丙這樣作的結果,將使乙處於更危險的境地。
合做才能對抗強敵。只有乙丙合做,才能把甲先幹掉。若是,乙丙不和,乙或丙單獨對甲都不佔優,必然被甲前後解決。、
1966年經典電影《黃金三鏢客》中的最後一幕,三個主人公手持槍桿站在墓地中,爲了寶藏隨時準備決一雌雄。爲了活着拿到寶藏,倖存下來的最優策略是什麼呢?
當時,蒙古軍事實力最強,金國次之,南宋武力最弱。原本南宋應該和金國結盟,幫助金國抵禦蒙古的入侵纔是上策,或者至少保持中立。可是,當時的南宋採起了和蒙古結盟的政策。南宋當局先是糊塗地贊成了拖雷借道宋地伐金。1231年,蒙古軍隊在宋朝的先遣隊伍引導下,借道四川等地,北度漢水殲滅了金軍有生力量。
1233年,南宋軍隊與蒙古軍隊合圍蔡州,金朝最後一個皇帝在城破後死於亂兵,金至此滅亡。1279年,南宋正式亡於蒙古。
若是南宋當政者有戰略眼光,捐棄前嫌,與世仇金結盟對抗最強大的敵人蒙古,宋和金都不至於那麼快就前後滅亡了。
豬圈裏面有兩隻豬, 一隻大,一隻小。豬圈很長,一頭有一個踏板,另外一頭是飼料的出口和食槽。每踩一下踏板,在遠離踏板的豬圈的另外一邊的投食口就會落下少許的食物。若是有一隻豬去踩踏板,另外一隻豬就有機會搶先吃到另外一邊落下的食物。
那麼,兩隻豬各會採起什麼策略?使人出乎意料的是,答案竟然是:小豬將選擇「搭便車」策略,也就是舒舒服服地等在食槽邊;而大豬則爲一點殘羹不知疲倦地奔忙於踏板和食槽之間。
緣由何在呢?咱們來分析一下,首先這是一個靜態不徹底信息博弈:
「智豬博弈」的結論彷佛是,在一個雙方公平、公正、合理和共享競爭環境中,有時佔優點的一方最終獲得的結果卻有悖於他的初始理性。這種狀況在現實中比比皆是。
好比,在某種新產品剛上市,其性能和功用還不爲人所熟識的狀況下,若是進行新產品生產的不只是一家小企業,還有其餘生產能力和銷售能力更強的企業。那麼,小企業徹底沒有必要做出頭鳥,本身去投入大量廣告作產品宣傳,只要採用跟隨戰略便可。
「智豬博弈」告訴咱們,誰先去踩這個踏板,就會造福全體,但多勞卻並不必定多得。
在現實生活中,不少人都只想付出最小的代價,獲得最大的回報,爭着作那隻不勞而獲的小豬。「一個和尚挑水喝,兩個和尚擡水喝,三個和尚沒水喝」說的正是這樣一個道理。這三個和尚都想作「小豬」,卻不想付出勞動,不肯承擔起「大豬」的義務,最後致使每一個人都沒法得到利益。
金融證券市場是一個羣體博弈的場所,其真實狀況很是複雜。在證券交易中,其結果不只依賴於單個參與者自身的策略和市場條件,也依賴其餘人的選擇及策略。
在「智豬博弈」的情景中,大豬是佔據比較優點的,可是,因爲小豬別無選擇,使得大豬爲了本身能吃到食物,不得不辛勤忙碌,反而讓小豬搭了便車,並且比大豬還得意。這個博弈中的關鍵要素是豬圈的設計, 即踩踏板的成本。
證券投資中也是有這種情形的。例如,當莊家在底位買入大量股票後,已經付出了至關多的資金和時間成本,若是不等價格上升就撤退,就只有接受虧損。
因此,基於和大豬同樣的貪吃本能,只要大勢不是太糟糕,莊家通常都會擡高股價,以求實現手中股票的增值。這時的中小散戶,就能夠對該股追加資金,當一隻聰明的「小豬」,而讓 「大豬」莊家力擡股價。固然,這種股票的發覺並不容易,因此當「小豬」所須要的條件,就是發現有這種狀況存在的豬圈,並衝進去。這樣,你就成爲一隻聰明的「小豬」。
股市中,散戶投資者與小豬的命運有類似之處,沒有能力承擔炒做成本,因此就應該充分利用資金靈活、成本低和不怕被套的優點,發現並選擇那些機構投資者已經或可能坐莊的股票,等着大豬們爲本身服務。
由此看到,散戶和機構的博弈中,散戶並非總沒有優點的,關鍵是找到有大豬的那個食槽,並等到對本身有利的遊戲規則造成時再進入。
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
GAN的起源之做鼻祖是 Ian Goodfellow 在 2014 年發表在 ICLR 的論文:Generative Adversarial Networks」。
按照筆者的理解,提出GAN網絡的出發點有以下幾個:
爲了清楚地闡述這個概念,筆者先從對抗樣本這個話題開始提及。
對抗樣本(adversarial example)是指通過精心計算獲得的用於誤導分類器的樣本。例以下圖就是一個例子,左邊是一個熊貓,可是添加了少許隨機噪聲變成右圖後,分類器給出的預測類別倒是長臂猿,但視覺上左右兩幅圖片並無太大改變。
出現這種狀況的緣由是什麼呢?
簡單來講,就是預測器發生了過擬合。圖像分類器本質上是高維空間的一個複雜的決策函數,在高維空間上,圖像分類器過度考慮了全像素區間內的細節信息,致使預測器對圖像的細節信息太敏感,微小的擾動就可能致使預測器的預測行爲產生很大的變化。
關於這個話題,筆者在另外一篇文章中對過擬合現象以及規避方法進行了詳細討論。
除了添加」隨機噪聲驅動的像素擾動」這種方法以外,還能夠經過圖像變形的方式,使得新圖像和原始圖像視覺上同樣的狀況下,讓分類器獲得有很高置信度的錯誤分類結果。這種過程也被稱爲對抗攻擊(adversarial attack)。
人類經過觀察和體驗物理世界來學習,咱們的大腦十分擅長預測,不須要顯式地通過複雜計算就能夠獲得正確的答案。監督學習的過程就是學習數據和標籤之間的相關關係。
可是在非監督學習中,數據並無被標記,並且目標一般也不是對新數據進行預測。
在現實世界中,標記數據是十分稀有和昂貴的。生成對抗網絡經過生成僞造的/合成的數據並嘗試判斷生成樣本真僞的方法學習,這本質上至關於採用了監督學習的方法來作無監督學習。作分類任務的判別器在這裏是一個監督學習的組件,生成器的目標是瞭解真實數據的模樣(機率分佈),並根據學到的知識生成新的數據。
Relevant Link:
https://www.jiqizhixin.com/articles/2018-03-05-4
GAN網絡發展到現在已經有不少的變種,在arxiv上天天都會有大量的新的研究論文被提出。可是筆者這裏不許備枚舉全部的網絡結構,而是僅僅討論GAN中最核心的思想,經過筆者本身的論文閱讀,將我認爲最精彩的思想和學術創新提煉出來給你們,從此咱們也能夠根據本身的理解,將其餘領域的思想交叉引入進來,繼續不斷創新發展。
經典的GAN網絡由兩部分組成,分別稱之爲判別器D和生成器G,兩個網絡的工做原理能夠以下圖所示,
D 的目標就是判別真實圖片和 G 生成的圖片的真假,而 G 是輸入一個隨機噪聲來生成圖片,並努力欺騙 D。
簡單來講,GAN 的基本思想就是一個最小最大定理,當兩個玩家(D 和 G)彼此競爭時(零和博弈),雙方都假設對方採起最優的步驟而本身也以最優的策略應對(最小最大策略),那麼結果就會進入一個肯定的均衡狀態(納什均衡)。
生成器網絡以隨機的噪聲z做爲輸入並試圖生成樣本數據,並將生成的僞造樣本數據提供給判別器網絡D,
能夠看到,G 網絡的訓練目標就是讓 D(G(z)) 趨近於 1,即徹底騙過判別器(判別器將生成器生成的僞造樣本所有誤判爲真)。G 網絡經過接受 D 網絡的反饋做爲梯度改進方向,經過BP過程反向調整本身的網絡結構參數。
判別器網絡以真實數據x或者僞造數據G(z)做爲輸入,並試圖預測當前輸入是真實數據仍是生成的僞造數據,併產生一個【0,1】範圍內的預測標量值。
D 網絡的訓練目標是區分真假數據,D 網絡的訓練目標是讓 D(x) 趨近於 1(真實的樣本判真),而 D(G(z)) 趨近於0(僞造的樣本判黑)。D 網絡同時接受真實樣本和 G 網絡傳入的僞造樣本做爲梯度改進方向,,經過BP過程反向調整本身的網絡結構參數。
生成器和判別器網絡的損失函數結合起來就是生成對抗網絡(GAN)的綜合損失函數:
兩個網絡相互對抗,彼此博弈,如上所示,綜合損失函數是一個極大極小函數;
整個相互對抗的過程,Ian Goodfellow 在論文中用下圖來描述:
黑色曲線表示輸入數據 x 的實際分佈,綠色曲線表示的是 G 網絡生成數據的分佈,紫色的曲線表示的是生成數據對應於 D 的分佈的差別距離(KL散度)
GAN網絡訓練的目標是但願着實際分佈曲線x,和G網絡生成的數據,兩條曲線能夠相互重合,也就是兩個數據分佈一致(達到納什均衡)。
論文給出的算法實現過程以下所示:
一些細節須要注意:
GAN的巧妙之處在於其目標函數的設定,由於此,GAN有以下幾個優勢:
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/
在閱讀了不少GAN衍生論文以及GAN原始論文以後,筆者一直在思考的一個問題是:GAN背後的底層思想是什麼?GAN衍生和改進算法的靈感和思路又是從哪裏來的?
通過一段時間思考以及和同行同窗討論後,我得出了一些思考,這裏分享以下,但願對讀者朋友有幫助。
咱們先來看什麼是判別模型和生成模型:
從機率論的視角來看,咱們來看一下原始GAN網絡的架構:
遵循這種框架進行思考,CGAN只是將v_input中的隨機噪聲z替換成了另外一種向量(文本或者標籤向量),而Pix2pixGAN是將一個圖像向量做爲v_input輸入GAN網絡。
GAN的發展離不開goodfellow後來的學者們不斷的研究與發展,目前已經提出了不少優秀的新GAN架構,而且這個發展還在繼續。爲了讓本博文能保持必定的環境獨立性,筆者這裏不作完整的羅列與枚舉,相反,筆者但願從兩條脈絡來展開討論:
Alec Radford,Luke Metz,Soumith Chintala等人在「Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks」提出了DCGAN。這是GAN研究的一個重要里程碑,由於它提出了一個重要的架構變化來解決訓練不穩定,模式崩潰和內部協變量轉換等問題。從那時起,基於DCGAN的架構就被應用到了許多GAN架構。
DCGAN的提出主要是爲了解決原始GAN架構的原生架構問題,咱們接下來來討論下。
生成器從潛在空間中獲得100維噪聲向量z,經過一系列卷積和上採樣操做,將z映射到一個像素矩陣對應的空間中,以下圖:
DCGAN經過下面的一些架構性約束來固化網絡:
生成器和判別器都是經過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產生的手寫數字輸出
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,同時鏈接了判別器和生成器網絡。
在訓練過程,咱們將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手寫數字圖像
CycleGANs 由Jun-Yan Zhu,Taesung Park,Phillip Isola和Alexei A. Efros在題爲「Unpaired Image-to-Image Translation using Cycle-Consistent Adversarial Networks」的論文中提出
CycleGAN用來實現不須要其餘額外信息,就能將一張圖像從源領域映射到目標領域的方法,例如將照片轉換爲繪畫,將夏季拍攝的照片轉換爲冬季拍攝的照片,或將馬的照片轉換爲斑馬照片,或者相反。總結來講,CycleGAN常備用於不一樣的圖像到圖像翻譯。
CycleGAN背後的核心思想是兩個轉換器F和G,其中:
所以,
和原始的GAN結構相比,由單個G->D的單向開放結構,變成了由兩對G<->D組成的雙向循環的封閉結構,但形式上依然是G給D輸入僞造樣本。但區別在於梯度的反饋是雙向循環的。
CycleGAN模型有如下兩個損失函數:
完整的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上找到。
StackJANs由Han Zhang,Tao Xu,Hongsheng Li還有其餘人在題爲「StackGAN: Text to Photo-Realistic Image Synthesis with Stacked Generative Adversarial Networks」的論文中提出。他們使用StackGAN來探索文本到圖像的合成,獲得了很是好的結果。
一個StackGAN由一對網絡組成,當提供文本描述時,能夠生成逼真的圖像。
pix2pix網絡由Phillip Isola,Jun-Yan Zhu,Tinghui Zhou和Alexei A. Efros在他們的題爲「Image-to-Image Translation with Conditional Adversarial Networks」的論文中提出。
對於圖像到圖像的翻譯任務,pix2pix也顯示出了使人印象深入的結果。不管是將夜間圖像轉換爲白天的圖像仍是給黑白圖像着色,或者將草圖轉換爲逼真的照片等等,Pix2pix在這些例子中都表現很是出色。
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
咱們用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生成的樣原本看,網絡很快陷入了一個局部最優區間中。
關於這個問題,學術界已經有比較多的討論和分析,筆者這裏列舉以下:
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 依舊很小。
基本上說,學術界對文本的見解是將其是作一個時序依賴的序列,因此主流方向是使用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,直接可導了。
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