1. 程式人生 > >一篇讀懂生成對抗網路(GAN)原理+tensorflow程式碼實現

一篇讀懂生成對抗網路(GAN)原理+tensorflow程式碼實現

作者:JASON
2017.10.15

在這裡插入圖片描述

  生成對抗網路GAN(Generative adversarial networks)是最近很火的深度學習方法,要理解它可以把它分成生成模型和判別模型兩個部分,簡單來說就是:兩個人比賽,看是 A 的矛厲害,還是 B 的盾厲害。比如,有一個業餘畫家總喜歡仿造著名畫家的畫,把仿造的畫和真實的畫混在一起,然後有一個專家想辦法來區分那些是真跡,那些是贗品。通過不斷的相互博弈,業餘畫家的仿造能力日益上升,與此同時,通過不斷的判斷結果反饋,積累了不少經驗,專家的鑑別能力也在上升,進一步促使業餘專家的仿造能力大幅提升,最後使得業餘專家的仿造作品無限接近與真跡,使得鑑別專家無法辨別,最後判斷的

準確率為0.5

  總的來說,Goodfellow等人提出的GAN是通過對抗過程來估計生成模型的框架。在這種框架下,我們需要同時訓練兩個網路,即一個能獲取資料分佈的生成模型G和一個估計資料來源於真實樣本概率的判別模型D。生成器的訓練目的是最大化判別器犯錯誤的概率,而判別器的訓練過程是最小化犯錯誤的概率。因此這一過程存在一個極大極小博弈(minimax game)。在所有可能的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 和生成器G對價值函式 V(G,D) 進行了極小極大化博弈:
這裡寫圖片描述


  此外,Goodfellow 等人在論文中使用如下案例為我們簡要介紹了基本概念。
此外,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))。©隨後固定判別器並訓練生成器,在更新 G 之後,D 的梯度會引導 G(z)流向更可能被 D 分類為真實資料的方向。(d)經過若干次訓練後,如果 G 和 D 有足夠的複雜度,那麼它們就會到達一個均衡點。這個時候 P_g=P_data,即生成器的概率密度函式等於真實資料的概率密度函式,也即生成的資料和真實資料是一樣的。在均衡點上 D 和 G 都不能得到進一步提升,並且判別器無法判斷資料到底是來自真實樣本還是偽造的資料,即 D(x)= 1/2。
  上面是比較精簡地介紹了生成對抗網路的基本概念,下一節將會把這些概念形式化,並描述優化的大致過程。

概念與過程的形式化

1. 理論完美的生成器

  該演算法的目標是令生成器生成與真實資料幾乎沒有區別的樣本,即一個造假一流的 A,就是我們想要的生成模型。數學上,即將隨機變數生成為某一種概率分佈,也可以說概率密度函式為相等的:P_G(x)=P_data(x)。這正是數學上證明生成器高效性的策略:即定義一個最優化問題,其中最優生成器 G 滿足 P_G(x)=P_data(x)。如果我們知道求解的 G 最後會滿足該關係,那麼我們就可以合理地期望神經網路通過典型的 SGD 訓練就能得到最優的 G。

2. 最優化問題

  正如最開始我們瞭解的警察與造假者案例,定義最優化問題的方法就可以由以下兩部分組成。首先我們需要定義一個判別器 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

Dependencies:
tensorflow: at least 1.1.0
matplotlib
numpy

本次試驗專案描述如下:
  讓生成器學習如何畫一條曲線,不同於其他神經網路框架,GAN要求同時求出生成模型的誤差G_LOSS和判別模型的誤差D_LOSS,然後再用以下兩個訓練op去同時訓練,其中LR表示學習率。

  • Train_G=optimizer(LR).minimize(G_LOSS)
  • Train_D=optimizer(LR).minimize(D_LOSS)

  把這兩個train_op同時扔到sess.run()裡面去訓練更新引數

#總的框架:
def artist_works():#專家畫的畫
    return paintings

with tf.variable_scope('Generator'):
with tf.variable_scope('Discriminator'):

D_loss = -tf.reduce_mean(tf.log(prob_artist0) + tf.log(1-prob_artist1))
G_loss = tf.reduce_mean(tf.log(1-prob_artist1))
#固定生成器之後,tf.log(prob_artist0)等於一個常數,所以沒有加入
train_D = tf.train.AdamOptimizer(LR_D).minimize(D_loss, )
train_G = tf.train.AdamOptimizer(LR_G).minimize(G_loss, )

sess = tf.Session()
sess.run(tf.global_variables_initializer())
for step in range(5000):
    G_paintings, pa0, Dl = sess.run([G_out, prob_artist0, D_loss, train_D, train_G],{G_in: G_ideas, real_art: artist_paintings})[:3]

第一步
設定超引數

import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt

tf.set_random_seed(1)
np.random.seed(1)

## 設定超引數
BATCH_SIZE = 64		# 批量大小
LR_G = 0.0001           # 生成器的學習率
LR_D = 0.0001           # 判別器的學習率
N_IDEAS = 5             # 認為這是生成5種藝術作品(5種初始化曲線)
ART_COMPONENTS = 15     # 在畫上畫15個點練成一條線
PAINT_POINTS = np.vstack([np.linspace(-1, 1, ART_COMPONENTS) for _ in range(BATCH_SIZE)])
#列表解析式代替了for迴圈,PAINT_POINTS.shape=(64,15),
#np.vstack()預設逐行疊加(axis=0)

第二步
專家開始作畫

def artist_works():    
    a = np.random.uniform(1, 2, size=BATCH_SIZE)[:, np.newaxis]
    #a為64個1到2均勻分佈抽取的值,shape=(64,1)
    paintings = a * np.power(PAINT_POINTS, 2) + (a-1)
    return paintings

第三步
設定生成器網路結構,生成一副業餘畫家的畫

with tf.variable_scope('Generator'):
    G_in = tf.placeholder(tf.float32, [None, N_IDEAS]) # 隨機的ideals(來源於正態分佈)
    G_l1 = tf.layers.dense(G_in, 128, tf.nn.relu)
    G_out = tf.layers.dense(G_l1, ART_COMPONENTS)      # 生成一副業餘專家的畫(15個數據點)

第四步
設定判別器網路結構,先輸入專家畫,返回判斷真的概率,再輸入業餘專家的畫,同樣返回判為真概率

with tf.variable_scope('Discriminator'):
    """判別器與生成器不同,生成器只需要輸入生成的資料就行,它無法接觸到專家的畫,
    如果能輸入專家的畫,那就不用學習了,直接匯入到判別器就是0.5的概率,換句話說,
    生成器只能通過生成器的誤差反饋來調節權重,使得逐漸生成逼真的畫出來。"""
	
    # 接受專家的畫
    real_art = tf.placeholder(tf.float32, [None, ART_COMPONENTS], name='real_in')   
	
    # 將專家的畫輸入到判別器,判別器判斷這副畫來自於專家的概率
    D_l0 = tf.layers.dense(real_art, 128, tf.nn.relu, name='Discri')
    prob_artist0 = tf.layers.dense(D_l0, 1, tf.nn.sigmoid, name='out')  
    
    # 之後輸入業餘專家的畫,G_out代入到判別器中。
    D_l1 = tf.layers.dense(G_out, 128, tf.nn.relu, name='Discri', reuse=True)  
	
    # 代入生成的畫,判別器判斷這副畫來自於專家的概率
    prob_artist1 = tf.layers.dense(D_l1, 1, tf.nn.sigmoid, name='out', reuse=True) 
    """注意到,判別器中當輸入業餘專家的畫時,這層是可以重複利用的,通過動態調整這次的權重來完成判別器的loss最小,關鍵一步。"""

第五步
定義誤差loss

根據公式:
價值函式
對於D_loss先固定G(生成器),先讓判別器學習一下專家的畫,對專家的畫有了“印象”之後再去接受業餘的畫,再對D(判別器)上求V的最大值化以此來得到此時的最優解D。由於tensorflow只支援minimize(),所以這裡新增“-”號,來轉化為求最大值。
對於G_loss先固定D,等同於把logD(X)的期望當作常數,所以只需要最小化後面那一部分即可。

#判別器loss,此時需同時優化兩部分的概率
D_loss = -tf.reduce_mean(tf.log(prob_artist0) + tf.log(1-prob_artist1))

#對於生成器的loss,此時prob_artist0是固定的,可以看到生成器並沒有輸入專家的畫,
所以tf.log(prob_artist0)是一個常數,故在這裡不用考慮。
G_loss = tf.reduce_mean(tf.log(1-prob_artist1))

第六步
定義Train_D和Train_G

train_D = tf.train.AdamOptimizer(LR_D).minimize(
    D_loss, var_list=tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope='Discriminator'))
train_G = tf.train.AdamOptimizer(LR_G).minimize(
    G_loss, var_list=tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope='Generator'))

第七步
定義sess,初始化所以變數

sess = tf.Session()
sess.run(tf.global_variables_initializer())

第八步
畫圖,實時展示結果

plt.ion()   # 連續畫圖
for step in range(5000):
    artist_paintings = artist_works()           # 專家的畫,每一輪專家的畫都是隨機生成的!
    G_ideas = np.random.randn(BATCH_SIZE, N_IDEAS)  #業餘畫家的5個想法
    G_paintings, pa0, Dl = sess.run([G_out, prob_artist0, D_loss, train_D, train_G],
     {G_in: G_ideas, real_art: artist_paintings})[:3]   # 訓練和獲取結果
                                    
    if step % 50 == 0:  # 每50步訓練畫一次圖
        plt.cla()
        plt.plot(PAINT_POINTS[0], G_paintings[0], c='#4AD631', lw=3, label='生成的畫',)
        plt.plot(PAINT_POINTS[0], artist_paintings[0], c='#4AD632', lw=3, label='專家的畫',)
        plt.plot(PAINT_POINTS[0], 2 * np.power(PAINT_POINTS[0], 2) + 1, c='#74BCFF', lw=3, label='上限')
        plt.plot(PAINT_POINTS[0], 1 * np.power(PAINT_POINTS[0], 2) + 0, c='#FF9359', lw=3, label='下限')
        plt.text(-.5, 2.3, 'D accuracy=%.2f (0.5 for D to converge)' % pa0.mean(), fontdict={'size': 15})
        plt.text(-.5, 2, 'D score= %.2f (-1.38 for G to converge)' % -Dl, fontdict={'size': 15})
        plt.ylim((0, 3)); plt.legend(loc='upper right', fontsize=12); plt.draw(); plt.pause(0.01)

plt.ioff()
plt.show()

最終結果展示:

在這裡插入圖片描述

  如上圖所示,綠色曲線是專家的畫,每一輪都會隨機重新生成,黃色曲線是業餘畫家的畫。可以看到,隨著訓練的進行,通過不段學習和模仿專家的畫,黃的曲線逐漸變得像綠色曲線一樣的彎曲,並最終使得判斷器或者生成器輸出的概率=0.5,已經無法分辨到底是專家的畫還是業餘畫家的畫。如果增加epoch的話,理論上可以使它趨近於0.5,判別誤差收斂於-1.38。

點贊是我繼續分享的動力,原始碼可以直接執行,謝謝大家!