來源:https://www.numpy.org.cn/deep/basics/gan.html
生成對抗網絡
本教程源代碼目錄在book/09.gan,初次使用請您參考Book文檔使用說明。
#說明:
- 硬件環境要求: 本文可支持在CPU、GPU下運行
- Docker鏡像支持的CUDA/cuDNN版本: 如果使用了Docker運行Book,請注意:這里所提供的默認鏡像的GPU環境為 CUDA 8/cuDNN 5,對於NVIDIA Tesla V100等要求CUDA 9的 GPU,使用該鏡像可能會運行失敗。
- 文檔和腳本中代碼的一致性問題: 請注意:為使本文更加易讀易用,我們拆分、調整了dc_gan.py的代碼並放入本文。本文中代碼與dc_gan.py的運行結果一致,可直接運行dc_gan.py進行驗證。
#背景介紹
生成對抗網絡(Generative Adversarial Network [1],簡稱GAN)是非監督式學習的一種方法,通過讓兩個神經網絡相互博弈的方式進行學習。該方法最初由 lan·Goodfellow 等人於2014年提出,原論文見 Generative Adversarial Network。
生成對抗網絡由一個生成網絡與一個判別網絡組成。生成網絡從潛在空間(latent space)中隨機采樣作為輸入,其輸出結果需要盡量模仿訓練集中的真實樣本。判別網絡的輸入為真實樣本或生成網絡的輸出,其目的是將生成網絡的輸出從真實樣本中盡可能分辨出來。而生成網絡則要盡可能地欺騙判別網絡。兩個網絡相互對抗、不斷調整參數,其目的是將生成網絡生成的樣本和真實樣本盡可能的區分開[2] )。
生成對抗網絡常用於生成以假亂真的圖片 [3] )。此外,該方法還被用於生成視頻、三維物體模型等。
#效果展示
本教程將 MNIST 數據集輸入網絡進行訓練,經過19輪訓練后可以看到,生成的圖片已經非常接近真實圖片的樣子,下圖中前8行是真實圖片的樣子,后8行是網絡生成的圖像效果:
圖1. GAN 生成手寫數字效果
#模型概覽
#GAN
GAN 網絡顧名思義,是一種通過對抗的方式,去學習數據分布的生成模型。其中,“對抗”指的是生成網絡(Generator)和判別網絡(Discriminator)的相互對抗。這里以生成圖片為例進行說明:
- 生成網絡(G)接收一個隨機的噪聲z,盡可能的生成近似樣本的圖像,記為G(z)
- 判別網絡(D)接收一張輸入圖片x,盡可以去判別該圖像是真實樣本還是網絡生成的假樣本,判別網絡的輸出 D(x) 代表 x 為真實圖片的概率。如果 D(x)=1 說明判別網絡認為該輸入一定是真實圖片,如果 D(x)=0 說明判別網絡認為該輸入一定是假圖片。
在訓練的過程中,兩個網絡互相對抗,最終形成了一個動態的平衡,上述過程用公式可以被描述為:
在最理想的情況下,G 可以生成與真實樣本極其相似的圖片G(z),而 D 很難判斷這張生成的圖片是否為真,對圖片的真假進行隨機猜測,即 D(G(z))=0.5。
下圖展示了生成對抗網絡的訓練過程,假設在訓練開始時,真實樣本分布、生成樣本分布以及判別模型分別是圖中的黑線、綠線和藍線。在訓練開始時,判別模型是無法很好地區分真實樣本和生成樣本的。接下來當我們固定生成模型,而優化判別模型時,優化結果如第二幅圖所示,可以看出,這個時候判別模型已經可以較好地區分生成數據和真實數據了。第三步是固定判別模型,改進生成模型,試圖讓判別模型無法區分生成圖片與真實圖片,在這個過程中,可以看出由模型生成的圖片分布與真實圖片分布更加接近,這樣的迭代不斷進行,直到最終收斂,生成分布和真實分布重合,判別模型無法區分真實圖片與生成圖片。
圖2. GAN 訓練過程
但是在實際過程中,很難得到這個完美的平衡點,關於GAN的收斂理論還在持續不斷的研究中。
#DCGAN
DCGAN [4] 是深層卷積網絡與 GAN 的結合,其基本原理與 GAN 相同,只是將生成網絡和判別網絡用兩個卷積網絡(CNN)替代。為了提高生成樣本的質量和網絡的收斂速度,論文中的 DCGAN 在網絡結構上進行了一些改進:
- 取消 pooling 層:在網絡中,所有的pooling層使用步幅卷積(strided convolutions)(判別器)和微步幅度卷積(fractional-strided convolutions)(生成器)進行替換。
- 加入 batch normalization:在生成器和判別器中均加入batchnorm。
- 使用全卷積網絡:去掉了FC層,以實現更深的網絡結構。
- 激活函數:在生成器(G)中,最后一層使用Tanh函數,其余層采用 ReLu 函數 ; 判別器(D)中都采用LeakyReLu。
DCGAN中的生成器(G)結構如下圖所示:
圖3. DCGAN中的生成器(G)
#數據准備
本次教程使用數據規模較小的 MNIST 訓練生成器和判別器,該數據集可通過paddle.dataset模塊自動下載到本地。
關於 MNIST 的詳細介紹可參考數字識別。
#訓練模型
09.gan/dc_gan.py
演示了訓練的整體過程。
#加載包
首先加載 PaddlePaddle 的 Fluid 和其他相關包
from __future__ import absolute_import from __future__ import division from __future__ import print_function import sys import os import matplotlib import PIL import six import numpy as np import math import time import paddle import paddle.fluid as fluid matplotlib.use('agg') import matplotlib.pyplot as plt import matplotlib.gridspec as gridspec
#定義輔助工具
定義 plot 函數,將圖像生成過程可視化
def plot(gen_data): pad_dim = 1 paded = pad_dim + img_dim gen_data = gen_data.reshape(gen_data.shape[0], img_dim, img_dim) n = int(math.ceil(math.sqrt(gen_data.shape[0]))) gen_data = (np.pad( gen_data, [[0, n * n - gen_data.shape[0]], [pad_dim, 0], [pad_dim, 0]], 'constant').reshape((n, n, paded, paded)).transpose((0, 2, 1, 3)) .reshape((n * paded, n * paded))) fig = plt.figure(figsize=(8, 8)) plt.axis('off') plt.imshow(gen_data, cmap='Greys_r', vmin=-1, vmax=1) return fig
#定義超參數
gf_dim = 64 # 生成器的feature map的基礎通道數量,生成器中所有的feature map的通道數量都是基礎通道數量的倍數 df_dim = 64 # 判別器的feature map的基礎通道數量,判別器中所有的feature map的通道數量都是基礎通道數量的倍數 gfc_dim = 1024 * 2 # 生成器的全連接層維度 dfc_dim = 1024 # 判別器的全連接層維度 img_dim = 28 # 輸入圖片的尺寸 NOISE_SIZE = 100 # 輸入噪聲的維度 LEARNING_RATE = 2e-4 # 訓練的學習率 epoch = 20 # 訓練的epoch數 output = "./output_dcgan" # 模型和測試結果的存儲路徑 use_cudnn = False # 是否使用cuDNN use_gpu=False # 是否使用GPU訓練
#定義網絡結構
- bn 層
調用 fluid.layers.batch_norm
接口實現bn層,激活函數默認使用ReLu。
def bn(x, name=None, act='relu'): return fluid.layers.batch_norm( x, param_attr=name + '1', bias_attr=name + '2', moving_mean_name=name + '3', moving_variance_name=name + '4', name=name, act=act)
- 卷積層
調用 fluid.nets.simple_img_conv_pool
實現卷積池化組,卷積核大小為5x5,池化窗口大小為2x2,窗口滑動步長為2,激活函數類型由具體網絡結構指定。
def conv(x, num_filters, name=None, act=None): return fluid.nets.simple_img_conv_pool( input=x, filter_size=5, num_filters=num_filters, pool_size=2, pool_stride=2, param_attr=name + 'w', bias_attr=name + 'b', use_cudnn=use_cudnn, act=act)
- 全連接層
def fc(x, num_filters, name=None, act=None): return fluid.layers.fc(input=x, size=num_filters, act=act, param_attr=name + 'w', bias_attr=name + 'b')
- 轉置卷積層
在生成器中,需要用隨機采樣值生成全尺寸圖像,dcgan使用轉置卷積層進行上采樣,在Fluid中,我們調用 fluid.layers.conv2d_transpose
實現轉置卷積。
def deconv(x, num_filters, name=None, filter_size=5, stride=2, dilation=1, padding=2, output_size=None, act=None): return fluid.layers.conv2d_transpose( input=x, param_attr=name + 'w', bias_attr=name + 'b', num_filters=num_filters, output_size=output_size, filter_size=filter_size, stride=stride, dilation=dilation, padding=padding, use_cudnn=use_cudnn, act=act)
- 判別器
判別器使用真實數據集和生成器生成的假圖片共同進行訓練,在訓練過程中盡量使真實數據集的輸出結果為1,生成的假圖片輸出結果為0。本教程中實現的判別器由兩個卷積池化層和兩個全連接層組成,其中最后一個全連接層的神經元個數為1,輸出一個二分類結果。
def D(x): x = fluid.layers.reshape(x=x, shape=[-1, 1, 28, 28]) x = conv(x, df_dim, act='leaky_relu',name='conv1') x = bn(conv(x, df_dim * 2,name='conv2'), act='leaky_relu',name='bn1') x = bn(fc(x, dfc_dim,name='fc1'), act='leaky_relu',name='bn2') x = fc(x, 1, act='sigmoid',name='fc2') return x
- 生成器
生成器由兩組帶BN的全連接層和兩組轉置卷積層組成,網絡輸入為隨機的噪聲數據,最后一層轉置卷積的卷積核數為1,表示輸出為灰度圖片。
def G(x): x = bn(fc(x, gfc_dim,name='fc3'),name='bn3') x = bn(fc(x, gf_dim * 2 * img_dim // 4 * img_dim // 4,name='fc4'),name='bn4') x = fluid.layers.reshape(x, [-1, gf_dim * 2, img_dim // 4, img_dim // 4]) x = deconv(x, gf_dim * 2, act='relu', output_size=[14, 14],name='deconv1') x = deconv(x, num_filters=1, filter_size=5, padding=2, act='tanh', output_size=[28, 28],name='deconv2') x = fluid.layers.reshape(x, shape=[-1, 28 * 28]) return x
#損失函數
損失函數使用 sigmoid_cross_entropy_with_logits
def loss(x, label): return fluid.layers.mean( fluid.layers.sigmoid_cross_entropy_with_logits(x=x, label=label))
#創建Program
d_program = fluid.Program() dg_program = fluid.Program() # 定義判別真實圖片的program with fluid.program_guard(d_program): # 輸入圖片大小為28*28=784 img = fluid.layers.data(name='img', shape=[784], dtype='float32') # 標簽shape=1 label = fluid.layers.data(name='label', shape=[1], dtype='float32') d_logit = D(img) d_loss = loss(d_logit, label) # 定義判別生成圖片的program with fluid.program_guard(dg_program): noise = fluid.layers.data( name='noise', shape=[NOISE_SIZE], dtype='float32') # 噪聲數據作為輸入得到生成圖片 g_img = G(x=noise) g_program = dg_program.clone() g_program_test = dg_program.clone(for_test=True) # 判斷生成圖片為真實樣本的概率 dg_logit = D(g_img) # 計算生成圖片被判別為真實樣本的loss dg_loss = loss( dg_logit, fluid.layers.fill_constant_batch_size_like( input=noise, dtype='float32', shape=[-1, 1], value=1.0))
使用adam作為優化器,分別優化判別真實圖片的loss和判別生成圖片的loss。
opt = fluid.optimizer.Adam(learning_rate=LEARNING_RATE) opt.minimize(loss=d_loss) parameters = [p.name for p in g_program.global_block().all_parameters()] opt.minimize(loss=dg_loss, parameter_list=parameters)
#數據集 Feeders 配置
下一步,我們開始訓練過程。paddle.dataset.mnist.train()用做訓練數據集。這個函數返回一個reader——PaddlePaddle中的reader是一個Python函數,每次調用的時候返回一個Python yield generator。
下面shuffle是一個reader decorator,它接受一個reader A,返回另一個reader B。reader B 每次讀入buffer_size條訓練數據到一個buffer里,然后隨機打亂其順序,並且逐條輸出。
batch是一個特殊的decorator,它的輸入是一個reader,輸出是一個batched reader。在PaddlePaddle里,一個reader每次yield一條訓練數據,而一個batched reader每次yield一個minibatch。
batch_size = 128 # Minibatch size train_reader = paddle.batch( paddle.reader.shuffle( paddle.dataset.mnist.train(), buf_size=60000), batch_size=batch_size)
#創建執行器
if use_gpu: exe = fluid.Executor(fluid.CUDAPlace(0)) else: exe = fluid.Executor(fluid.CPUPlace()) exe.run(fluid.default_startup_program())
#開始訓練
訓練過程中的每一次迭代,生成器和判別器分別設置自己的迭代次數。為了避免判別器快速收斂到0,本教程默認每迭代一次,訓練一次判別器,兩次生成器。
t_time = 0 losses = [[], []] # 判別器的迭代次數 NUM_TRAIN_TIMES_OF_DG = 2 # 最終生成圖像的噪聲數據 const_n = np.random.uniform( low=-1.0, high=1.0, size=[batch_size, NOISE_SIZE]).astype('float32') for pass_id in range(epoch): for batch_id, data in enumerate(train_reader()): if len(data) != batch_size: continue # 生成訓練過程的噪聲數據 noise_data = np.random.uniform( low=-1.0, high=1.0, size=[batch_size, NOISE_SIZE]).astype('float32') # 真實圖片 real_image = np.array(list(map(lambda x: x[0], data))).reshape( -1, 784).astype('float32') # 真實標簽 real_labels = np.ones( shape=[real_image.shape[0], 1], dtype='float32') # 虛假標簽 fake_labels = np.zeros( shape=[real_image.shape[0], 1], dtype='float32') total_label = np.concatenate([real_labels, fake_labels]) s_time = time.time() # 虛假圖片 generated_image = exe.run(g_program, feed={'noise': noise_data}, fetch_list=[g_img])[0] total_images = np.concatenate([real_image, generated_image]) # D 判斷虛假圖片為假的loss d_loss_1 = exe.run(d_program, feed={ 'img': generated_image, 'label': fake_labels, }, fetch_list=[d_loss])[0][0] # D 判斷真實圖片為真的loss d_loss_2 = exe.run(d_program, feed={ 'img': real_image, 'label': real_labels, }, fetch_list=[d_loss])[0][0] d_loss_n = d_loss_1 + d_loss_2 losses[0].append(d_loss_n) # 訓練生成器 for _ in six.moves.xrange(NUM_TRAIN_TIMES_OF_DG): noise_data = np.random.uniform( low