【TensorFlow-windows】學習筆記七——生成對抗網絡

前言

既然學習了變分自編碼(VAE),那也必須來一波生成對抗網絡(GAN)。

國際慣例,參考網址:

論文: Generative Adversarial Nets

PPT:Generative Adversarial Networks (GANs)

Generative Adversarial Nets in TensorFlow

GAN原理學習筆記
GAN — Ways to improve GAN performance

理論

粗略點的講法就說:一個生成器GG,一個判別器DD,前者用來將噪聲輸入轉換成圖片,後者判別當前輸入圖片是真實的還是生成的。

爲了更詳細地瞭解GAN,還是對論文進行簡要的組織、理解吧。有興趣直接看原始論文,這裏進行部分關鍵內容的摘抄。

任意的GGDD函數空間都存在特定解,GG要能表示訓練集的分佈,而DD一定是等於12\frac{1}{2},也就是說判別器無法分辨當前輸入是真的還是假的,這樣就達到了魚目混珠的效果。在GAN中,使用多層感知器構建GGDD,整個模型可以使用反向傳播算法學習。

論文裏面有一句很好的話解釋了GAN的動機:目前深度學習在判別模型的設計中取得了重大成功,但是在生成模型卻鮮有成效,主要原因在於在極大似然估計和相關策略中有很多難以解決的概率計算難題(想想前一篇博客的變分自編碼的理論,闊怕),而且丟失了生成背景下的分段線性單元的優勢,因此作者就提出了新的生成模型估計方法,避開這些難題,也就是傳說中的GAN。它的訓練完全不需要什麼鬼似然估計,只需要使用目前炒雞成功的反傳和dropout算法。

爲了讓生成器學到數據分佈pgp_g,需要定義一個先驗的噪聲輸入pz(z)p_z(z),然後使用G(z;θg)G(z;\theta_g)將其映射到數據空間,這裏的GG是具有參數θg\theta_g的多層感知器。然後定義另一個多層感知器D(x;θd)D(x;\theta_d),輸出一個標量。D(x)D(x)代表的是xx來自於真實樣本而非生成的樣本pgp_g的概率,我們訓練DD去最大化將正確標籤同時賦予訓練集和GG生成的樣本的概率,也就是DD把真的和假的圖片都當成真的了。同時要去訓練GG去最小化log(1D(G(z)))\log (1-D(G(z))),是爲了讓生成的圖片被賦予正樣本標籤的概率大點,損失函數就是:
minGmaxDV(D,G)=Expdata(x)[logD(x)]+Ezpz(z)[log(1D(G(z)))] \min_G \max_D V(D,G)=E_{x\sim p_{data}(x)}[\log D(x)]+E_{z\sim p_z(z)}[\log(1-D(G(z)))]
在優化DD的時候,在訓練的內循環中是無法完成的,計算上不允許,並且在有限數據集上會導致過擬合,因此可以以k:1k:1 的訓練次數比例分別優化DDGG。這能夠讓DD保持在最優解附近,只要GG變化比較緩慢。

而且在實際中,上式可能無法提供足夠的梯度讓GG很好地學習,在訓練早期,當GG很差的時候,DD能夠以很高的概率將其歸爲負樣本,因爲生成的數據與訓練數據差距很大,這樣log(1D(G(z)))\log(1-D(G(z)))就飽和了,與其說最小化log(1D(G(z)))\log(1-D(G(z)))不如去最大化log(D(G(z)))\log(D(G(z))),這個目標函數對GGDD的收斂目標不變,但是能早期學習具有更強的梯度。

訓練算法:

外層一個大循環就不說了,對所有的批數據迭代,內層有一個小循環,控制上面說的判別器DD與生成器GG的訓練比例爲k:1k:1的:

  • 以下步驟執行kk次:

    • 從噪聲先驗pg(z)p_g(z)中採樣mm個噪聲樣本{z(1), ,z(m)}\{z^{(1)},\cdots,z^{(m)}\}

    • 從原始樣本分佈pdata(x)p_{data}(x)中選取mm個樣本x(1)x(m){x^{(1)}\cdots x^{(m)}},說這麼複雜,原始樣本的分佈不就是原始樣本麼,直接從原始樣本里面選一批數據就行了

    • 更新判別器
      θd1mi=1m[logD(x(i))+log(1D(G(z(i))))] \nabla_{\theta_d}\frac{1}{m}\sum_{i=1}^m \left[\log D\left(x^{(i)}\right)+\log \left(1-D\left(G\left(z^{(i)}\right)\right)\right)\right]

  • 從噪聲先驗pg(z)p_g(z)中採樣mm個噪聲樣本{z(1), ,z(m)}\{z^{(1)},\cdots,z^{(m)}\}

  • 更新生成器
    θg1mi=1mlog(1D(G(z(i)))) \nabla \theta_g \frac{1}{m}\sum_{i=1}^m \log\left(1-D\left(G \left( z^{(i)}\right) \right)\right)

後面作者又證明了兩個內容:

  • pg=pdatap_g=p_{data}的全局最優
  • 訓練算法的收斂性

身爲一個合格的程序猿,還是很有必要看看數學推導的o(╯□╰)o雖然不一定能懂

先看一個簡單的式子:yalog(y)+blog(1y)y\to a\log(y)+b\log(1-y),這個式子在[0,1][0,1]範圍取得最大值的點是在aa+b\frac{a}{a+b},證明很簡單,直接兩邊求導,令y=0y'=0就能算出來。

再看看我們的優化目標,當給定生成器GG的時候,也就是GG固定的時候:
V(G,D)=xpdata(x)logD(x)dx+zpz(z)log(1D(g(z)))dz=xpdata(x)log(D(x))+pg(x)log(1D(x))dx V(G,D)=\int_x p_{data}(x)\log D(x)dx+\int _zp_z(z)\log(1-D(g(z)))dz\\ =\int _xp_{data}(x)\log(D(x))+p_g(x)\log(1-D(x))dx
長得挺像,那麼DD的最優解就是:
DG(x)=pdata(x)pdata(x)+pg(x) D_G^*(x)=\frac{p_{data}(x)}{p_{data}(x)+p_g(x)}
DD的訓練目標可以看成最大化對數似然去估算條件概率P(Y=yx)P(Y=y|x),這裏YY表示xx來自於原始數據pdatap_{data}(此時y=1y=1)還是生成數據pgp_g(此時y=0y=0),所以損失函數又可以寫成:
C(G)=maxDV(G,D)=Expdata[logDG(x)]+Ezpz[log(1DG(G(z)))]=Expdata[logDG(x)]+Ezpg[log(1DG(x))]=Expdata[logpdata(x)pdata(x)+pg(x)]+Expg[logpg(x)pdata(x)+pg(x)] \begin{aligned} C(G)&=\max_D V(G,D)\\ &=E_{x\sim p_{data}}\left[\log D_G^*(x)\right]+E_{z\sim p_z}\left[\log(1-D^*_G(G(z)))\right]\\ &=E_{x\sim p_{data}}\left[\log D_G^*(x)\right]+E_{z\sim p_g}\left[\log(1-D^*_G(x))\right]\\ &=E_{x\sim p_{data}}\left[\log\frac{p_{data}(x)}{p_{data}(x)+p_g(x)}\right]+E_{x\sim p_g}\left[\log\frac{p_g(x)}{p_{data}(x)+p_g(x)}\right] \end{aligned}
理論1 當且僅當pg=pdatap_g=p_{data}的時候,訓練目標C(G)C(G)達到全局最小,此時,C(G)C(G)收斂到值log4-\log 4

證明:當pg=pdatap_g=p_{data}的時候,DG(x)=12D^*_G(x)=\frac{1}{2},因此C(G)=log12+log12=log4C(G)=\log \frac{1}{2}+\log\frac{1}{2}=-\log 4,感覺正常應該是:
Expdata[log2]+Expg[log2] E_{x\sim p_{data}}[-\log 2]+E_{x\sim p_g}[-\log 2]
但是作者貌似讓Expdata=Expg=1E_{x\sim p_{data}}=E_{x\sim p_g}=1了,我估計是因爲收斂到最終解的時候,理想狀態是判別器無法分辨哪個真哪個假,所以都當成正樣本了。這樣還能將C(G)C(G)變形:
C(G)=log4+KL(pdatapdata+pg2)+KL(pgpdata+pg2) C(G)=-\log 4+KL\left(p_{data}\parallel\frac{p_{data}+p_g}{2}\right)+KL\left(p_g\parallel \frac{p_{data}+p_g}{2}\right)
其實最終理想狀態下後面兩個KL距離是等於0的,代表衡量的兩個分佈一樣。

這裏作者提到了一個表達式稱爲Jensen-Shannon divergence,衡量模型分佈和數據生成過程:
C(G)=log4+2JSD(pdatapg) C(G)=-\log 4+2\cdot JSD(p_{data}\parallel p_g)
這個JSDJSD始終是非負的,當且僅當pdata=pgp_{data}=p_g的時候取00,意思就是生成模型能夠完美生成數據分佈。

接下來看看算法收斂性

理論2 如果GGDD有足夠的容量,並且在訓練算法的每一步,給定GG時判別器都能達到它的最優,那麼pgp_g的更新便可以通過優化
Expdata[logDG(x)]+Expg[log(1DG(x))] E_{x\sim p_{data}}\left[\log D_G^*(x)\right]+E_{x\sim p_g}\left[\log (1-D_G^*(x))\right]
然後pgp_g就收斂到了pdatap_{data}

證明就不看了,因爲我不是特別懂作者那一段文字,我們只需要知道收斂結果就是pg=pdatap_g=p_{data}就行了。

代碼實現-模型訓練與保存

老流程:讀數據、初始化相關參數、定義數據接受接口、初始化權重和偏置、構建基本模塊(生成器和判別器)、構建模型、定義損失和優化器、訓練

讀取數據

這個就不說了,全博客通用:

IMG_HEIGHT=28
IMG_WIDTH=28
CHANNELS=3
#讀取數據集
def read_images(dataset_path,batch_size):
    imagepaths,labels=list(),list()
    data=open(dataset_path,'r').read().splitlines()
    for d in data:
        imagepaths.append(d.split(' ')[0])
        labels.append(int(d.split(' ')[1]))
    imagepaths=tf.convert_to_tensor(imagepaths,dtype=tf.string)
    labels=tf.convert_to_tensor(labels,dtype=tf.int32)
    image,label=tf.train.slice_input_producer([imagepaths,labels],shuffle=True)

    image=tf.read_file(image)
    image=tf.image.decode_jpeg(image,channels=CHANNELS)
    image=tf.image.rgb_to_grayscale(image)    

    image=tf.reshape(image,[IMG_HEIGHT*IMG_WIDTH])
    image=tf.cast(image,tf.float32)
    image = image / 255.0
    image=tf.convert_to_tensor(image)

    inputX,inputY=tf.train.batch([image,label],
    batch_size=batch_size,capacity=batch_size*8,num_threads=4)
    return inputX,inputY

定義相關參數

主要是學習率,訓練次數,輸入單元、隱單元、輸出單元的神經元個數

#定義相關參數
learning_rate=0.0002
num_steps=1000
batch_size=128
disp_step=1000
num_class=10
gen_hid_num=256
dis_hid_num=256
noise_dim=100
num_input=IMG_HEIGHT*IMG_WIDTH

定義數據接收接口

需要注意GAN主要有兩類接口,生成器接收的是噪聲輸入,判別器接收的是真實圖片或者生成的圖片

#建立生成器、判別器的接收接口
gen_input=tf.placeholder(tf.float32,shape=[None,noise_dim],name='gen_input')
dis_input=tf.placeholder(tf.float32,shape=[None,num_input],name='dis_input')

初始化權重

#定義權重
def glorot_init(shape):
    return tf.random_normal(shape=shape,stddev=1/tf.sqrt(shape[0]/2.0))

weights={
    'gen_hidden1':tf.Variable(glorot_init([noise_dim,gen_hid_num])),
    'gen_out':tf.Variable(glorot_init([gen_hid_num,num_input])),
    'dis_hidden1':tf.Variable(glorot_init([num_input,dis_hid_num])),
    'dis_out':tf.Variable(glorot_init([dis_hid_num,1]))
}
biases={
    'gen_hidden1':tf.Variable(tf.zeros([gen_hid_num])),
    'gen_out':tf.Variable(tf.zeros([num_input])),
    'dis_hidden1':tf.Variable(tf.zeros([dis_hid_num])),
    'dis_out':tf.Variable(tf.zeros([1]))
}

基本模塊:生成器和判別器

#定義基本模塊
def generator(x):
    hidden_layer=tf.add(tf.matmul(x,weights['gen_hidden1']),biases['gen_hidden1'])
    hidden_layer=tf.nn.relu(hidden_layer)
    out_layer=tf.add(tf.matmul(hidden_layer,weights['gen_out']),biases['gen_out'])
    out_layer=tf.nn.sigmoid(out_layer)
    return out_layer

def discriminator(x):
    hidden_layer=tf.add(tf.matmul(x,weights['dis_hidden1']),biases['dis_hidden1'])
    hidden_layer=tf.nn.relu(hidden_layer)
    out_layer=tf.add(tf.matmul(hidden_layer,weights['dis_out']),biases['dis_out'])
    out_layer=tf.nn.sigmoid(out_layer)
    return out_layer

構建模型

注意我們的測試函數是生成器,最終需要它來生成圖片,所以需要加入函數中

#生成器
gen_sample=generator(gen_input)
tf.add_to_collection('generation',gen_sample)
#判別器
dis_real=discriminator(dis_input)
dis_fake=discriminator(gen_sample)

定義損失和優化器

損失函數包含生成器和判別器,但是針對生成器,我們上面說過,最小化log(1D(G(z)))\log (1-D(G(z)))不如最大化log(D(G(z)))\log(D(G(z))),所以生成器的損失可以定義爲負對數,這樣就把最大化又變成最小化了:

gen_loss = -tf.reduce_mean(tf.log(disc_fake))

當然你也可以使用最小化的方法,此博文即用最小化log(1D(G(z)))\log(1-D(G(z)))的方法:

G_loss = tf.reduce_mean(tf.log(1-prob_artist1))

判別器還是老樣子最大化log(D(x))log(D(G(z)))\log (D(x))-\log(D(G(z))),加個負號也是最小化了:

disc_loss = -tf.reduce_mean(tf.log(disc_real) + tf.log(1. - disc_fake))

【注】其實有的時候也可以直接用交叉熵來定義損失,讓判別器對真實圖片的標籤接近1,對假圖片的判別標籤接近0

d_loss_real = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits = D, labels = tf.ones_like(D)))
d_loss_fake = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits = _D, labels = tf.zeros_like(_D)))
d_loss = d_loss_real + d_loss_fake

而對於生成器,希望判別器對假圖片的判別標籤接近1:

g_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits = _D, labels = tf.ones_like(_D)))

話不多說,接下來定義優化器:

optimizer_gen=tf.train.AdamOptimizer(learning_rate=learning_rate)
optimizer_dis=tf.train.AdamOptimizer(learning_rate=learning_rate)

但是因爲採用的類似於固定梯度下降法,即在更新生成器時,判別器參數不動,同理更新判別器時生成器參數不動,所以需要先指定分開訓練的時候分別對誰求梯度:

#因爲採用了固定梯度下降,所以必須知道每個優化器需要優化什麼
gen_var=[weights['gen_hidden1'],weights['gen_out'],biases['gen_hidden1'],biases['gen_out']]
dis_var=[weights['dis_hidden1'],weights['dis_out'],biases['dis_hidden1'],biases['dis_out']]

這樣就可以針對性求解了:

#優化
train_gen=optimizer_gen.minimize(gen_loss,var_list=gen_var)
train_dis=optimizer_dis.minimize(dis_loss,var_list=dis_var)

訓練模型與保存

#初始化
init=tf.global_variables_initializer()
saver=tf.train.Saver()
input_image,input_label=read_images('./mnist/train_labels.txt',batch_size)
with tf.Session() as sess:
    sess.run(init)
    coord=tf.train.Coordinator()
    tf.train.start_queue_runners(sess=sess,coord=coord)
    for step in range(1,num_steps):
        time_start = time.time()
        batch_x,batch_y=sess.run([input_image,tf.one_hot(input_label,num_class,1,0)])
        z=np.random.uniform(-1.0,1.0,size=(batch_size,noise_dim))
        sess.run([train_gen,train_dis],feed_dict={dis_input:batch_x,gen_input:z})
        if step%1000==0 or step==1:                        
            g_loss,d_loss=sess.run([gen_loss,dis_loss],feed_dict={gen_input:z,dis_input:batch_x})
            time_end=time.time()
            print('step:%i----Generator loss:%f-----Discriminator Loss:%f' %(step,g_loss,d_loss))
    coord.request_stop()
    coord.join()
    print('optimization finished')
    saver.save(sess,'./GAN_mnist_model/GAN_mnist')

我沒有訓練多少次,有興趣的可以多訓練,最終讓判別器的損失接近0.50.5就說明接近最優解了,我的訓練結果:

step:1----Generator loss:0.393680-----Discriminator Loss:1.626469
step:1000----Generator loss:3.580971-----Discriminator Loss:0.078812
step:2000----Generator loss:4.907338-----Discriminator Loss:0.037951
step:3000----Generator loss:5.269949-----Discriminator Loss:0.015779
step:4000----Generator loss:3.202836-----Discriminator Loss:0.119377
step:5000----Generator loss:3.977841-----Discriminator Loss:0.140365
step:6000----Generator loss:3.546029-----Discriminator Loss:0.111060
step:7000----Generator loss:3.723459-----Discriminator Loss:0.099416
step:8000----Generator loss:4.479396-----Discriminator Loss:0.130558
step:9000----Generator loss:4.041896-----Discriminator Loss:0.132201
step:10000----Generator loss:3.873767-----Discriminator Loss:0.241299
step:11000----Generator loss:4.237263-----Discriminator Loss:0.162134
step:12000----Generator loss:3.463274-----Discriminator Loss:0.223905
step:13000----Generator loss:3.941289-----Discriminator Loss:0.261881
step:14000----Generator loss:3.292896-----Discriminator Loss:0.356275
optimization finished

【更新日誌】2018-8-27
還是依據論文流程,把判別器的訓練放在前面

d_loss,g_loss=sess.run([dis_loss,gen_loss],feed_dict={dis_input:batch_x,gen_input:z})

代碼實現-模型調用

還是老套路:

  • 載入模型

    sess=tf.Session()
    new_saver=tf.train.import_meta_graph('./GAN_mnist_model/GAN_mnist.meta')
    new_saver.restore(sess,'./GAN_mnist_model/GAN_mnist')
    
  • 載入運算圖

    graph=tf.get_default_graph()
    print(graph.get_all_collection_keys())
    #['generation', 'queue_runners', 'summaries', 'train_op', 'trainable_variables', 'variables']
    
  • 獲取預測函數和數據接收接口

    gen=graph.get_collection('generation')
    gen_input=graph.get_tensor_by_name('gen_input:0')
    
  • 隨便丟個噪聲給生成器

    noise_input=np.random.uniform(-1.0,1.0,size=[1,100])
    g=sess.run(gen,feed_dict={gen_input:noise_input})
    gen_img=g[0]*255.0
    gen_img=gen_img.reshape(28,28)
    plt.imshow(gen_img)
    plt.show()
    

    這裏寫圖片描述

後記

效果貌似不是特別好呢,可能訓練次數不是特別夠,也可能傳統的GAN結構對手寫數字的生成能力不夠,需要加深層數或者使用更好的GAN變種算法,後續打算再找幾個GAN算法研究研究。這裏先貼一下這篇博客關於GAN的損失函數的對比
這裏寫圖片描述

訓練代碼:鏈接:https://pan.baidu.com/s/12_DNKILTtletYbDDhHDi6Q 密碼:vyu7

測試代碼:鏈接:https://pan.baidu.com/s/1rvAKjBnazzKRiL7nPiufVQ 密碼:nreb
【更新日誌】2018-8-27
從論文來看,我們一般需要先訓練判別器,再訓練生成器,但是TensorFlow-Examples中給的例子是

_, _, gl, dl = sess.run([train_gen, train_disc, gen_loss, disc_loss],
                                feed_dict=feed_dict)

建議還是改一下:

_, _, gl, dl = sess.run([train_disc,train_gen,  disc_loss, gen_loss],
                                feed_dict=feed_dict)

但是收斂度遇到問題了,目前正在解決。
【更新日誌2018-8-29日】
找到未收斂或者收斂程度不好的原因了,要使用AdamOptimizer優化器,不要使用AdagradOptimizer優化器

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章