飛槳PaddlePaddle手把手教你完成圖像生成任務
生成對抗網絡(Generative Adversarial Network,簡稱GAN)是非監督式學習的一種方法,通過讓兩個神經網絡相互博弈的方式進行學習。
生成對抗網絡由兩種子網絡組成:一個生成器與一個判別期。生成器從潛在空間(latent space)中隨機采樣作為輸入,其輸出結果需要盡量模仿訓練集中的真實圖像。判別器的輸入為真實圖像或生成網絡的輸出圖像,其目的是將生成器的輸出圖像從真實圖像中盡可能分辨出來。而生成器則要盡可能地欺騙判別器。兩個網絡相互對抗、不斷調整參數,提升自己的能力。
生成對抗網絡常用于生成以假亂真的圖片,常用場景有手寫體生成、人臉合成、風格遷移、圖像修復等。此外,該方法還被用于生成視頻、三維物體模型等。
項目地址:
https://github.com/PaddlePaddle/book/tree/develop/09.gan
效果展示
先來看一下DCGAN的最終效果:將 MNIST 數據集輸入網絡進行訓練,經過19輪訓練后的結果如下圖,前8行是真實圖片的樣子,后8行是網絡生成的圖像效果。可以看到,生成的圖片已經非常接近真實圖片的樣子。
模型概覽
GAN
GAN是一種通過對抗的方式,去學習數據分布的生成模型。以生成圖片為例說明:
- 生成器(G)接收一個隨機的噪聲z,盡可能的生成近似樣本的圖片,記為G(z)。
- 判別器(D)接收一張輸入圖片x,盡可以去判別該圖像是真實圖片還是網絡生成的假圖片,判別器的輸出 D(x) 代表 x 為真實圖片的概率。如果 D(x)=1 說明判別器認為該輸入一定是真實圖片,如果 D(x)=0 說明判別器認為該輸入一定是假圖片。
在訓練的過程中,兩個網絡互相對抗,最終形成了一個動態的平衡,上述過程用公式可以被描述為:
在最理想的情況下,G 可以生成與真實圖片極其相似的圖片G(z),而 D 很難判斷這張生成的圖片是否為真,對圖片的真假進行隨機猜測,即 D(G(z))=0.5。
下圖展示了生成對抗網絡的訓練過程,假設在訓練開始時,真實樣本分布、生成樣本分布以及判別模型分別是圖中的黑線、綠線和藍線。在訓練開始時,判別模型是無法很好地區分真實樣本和生成樣本的。接下來當我們固定生成模型,而優化判別模型時,優化結果如第二幅圖所示,可以看出,這個時候判別模型已經可以較好地區分生成圖片和真實圖片了。第三步是固定判別模型,改進生成模型,試圖讓判別模型無法區分生成圖片與真實圖片,在這個過程中,可以看出由模型生成的圖片分布與真實圖片分布更加接近,這樣的迭代不斷進行,直到最終收斂,生成分布和真實分布重合,判別模型無法區分真實圖片與生成圖片。
圖:GAN 訓練過程
但是在實際過程中,很難得到這個完美的平衡點,關于GAN的收斂理論還在持續不斷的研究中。
DCGAN
DCGAN是深層卷積網絡與 GAN 的結合,其基本原理與 GAN 相同,只是將生成器和判別器用兩個卷積網絡(CNN)替代。為了提高生成樣本的質量和網絡的收斂速度,論文中的 DCGAN 在網絡結構上進行了一些改進:
- 取消 pooling 層:在網絡中,所有的pooling層使用步幅卷積(strided convolutions)(判別器)和微步幅度卷積(fractional-strided convolutions)(生成器)進行替換。
- 加入 batch normalization:在生成器和判別器中均加入batchnorm。
- 使用全卷積網絡:去掉了FC層,以實現更深的網絡結構。
- 激活函數:在生成器(G)中,最后一層使用Tanh函數,其余層采用 ReLu 函數 ; 判別器(D)中都采用LeakyReLu。
圖:DCGAN中的生成器(G)
快速開始
本文的DCGAN任務依賴于 Paddle Fluid v1.3 及以上版本,請參考官網安裝指南進行安裝。
數據準備
本文使用數據規模較小的MNIST 訓練生成器和判別器,該數據集可通過paddle.dataset模塊自動下載到本地。
加載包
首先加載 Paddle Fluid和其他相關包:
- 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
- from __future__ import absolute_import
- from __future__ import division
- from __future__ import print_function
定義輔助工具
定義 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 實現卷積池化組,卷積核大小為3x3,池化窗口大小為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=-1.0, high=1.0,
- size=[batch_size, NOISE_SIZE]).astype('float32')
- dg_loss_n = exe.run(dg_program,
- feed={'noise': noise_data},
- fetch_list={dg_loss})[0][0]
- losses[1].append(dg_loss_n)
- t_time += (time.time() - s_time)
- if batch_id % 10 == 0 and not run_ce:
- if not os.path.exists(output):
- os.makedirs(output)
- # 每輪的生成結果
- generated_images = exe.run(g_program_test,
- feed={'noise': const_n},
- fetch_list={g_img})[0]
- # 將真實圖片和生成圖片連接
- total_images = np.concatenate([real_image, generated_images])
- fig = plot(total_images)
- msg = "Epoch ID={0} Batch ID={1} D-Loss={2} DG-Loss={3}\n ".format(
- pass_id, batch_id,
- d_loss_n, dg_loss_n)
- print(msg)
- plt.title(msg)
- plt.savefig(
- '{}/{:04d}_{:04d}.png'.format(output, pass_id,
- batch_id),
- bbox_inches='tight')
- plt.close(fig)
打印特定輪次的生成結果:
- def display_image(epoch_no,batch_id):
- return PIL.Image.open('output_dcgan/{:04d}_{:04d}.png'.format(epoch_no,batch_id))
- # 觀察第10個epoch,460個batch的生成圖像:
- display_image(10,460)
總結
DCGAN采用一個隨機噪聲向量作為輸入,輸入通過與CNN類似但是相反的結構,將輸入放大成二維數據。采用這種結構的生成模型和CNN結構的判別模型,DCGAN在圖片生成上可以達到相當可觀的效果。本文中,我們利用DCGAN生成了手寫數字圖片,您可以嘗試更換數據集生成符合個人需求的圖片,完成您的圖像生成任務,快來試試吧!
更多內容,可點擊文末閱讀原文或查看:
https://github.com/PaddlePaddle/book/tree/develop/09.gan
ps:最后給大家推薦一個GPU福利-Tesla V100免費算力!配合PaddleHub能讓模型原地起飛~掃碼下方二維碼申請~