飛槳PaddlePaddle手把手教你完成圖像生成任務(wù)
生成對抗網(wǎng)絡(luò)(Generative Adversarial Network,簡稱GAN)是非監(jiān)督式學(xué)習(xí)的一種方法,通過讓兩個神經(jīng)網(wǎng)絡(luò)相互博弈的方式進行學(xué)習(xí)。
生成對抗網(wǎng)絡(luò)由兩種子網(wǎng)絡(luò)組成:一個生成器與一個判別期。生成器從潛在空間(latent space)中隨機采樣作為輸入,其輸出結(jié)果需要盡量模仿訓(xùn)練集中的真實圖像。判別器的輸入為真實圖像或生成網(wǎng)絡(luò)的輸出圖像,其目的是將生成器的輸出圖像從真實圖像中盡可能分辨出來。而生成器則要盡可能地欺騙判別器。兩個網(wǎng)絡(luò)相互對抗、不斷調(diào)整參數(shù),提升自己的能力。
生成對抗網(wǎng)絡(luò)常用于生成以假亂真的圖片,常用場景有手寫體生成、人臉合成、風(fēng)格遷移、圖像修復(fù)等。此外,該方法還被用于生成視頻、三維物體模型等。
項目地址:
https://github.com/PaddlePaddle/book/tree/develop/09.gan
效果展示
先來看一下DCGAN的最終效果:將 MNIST 數(shù)據(jù)集輸入網(wǎng)絡(luò)進行訓(xùn)練,經(jīng)過19輪訓(xùn)練后的結(jié)果如下圖,前8行是真實圖片的樣子,后8行是網(wǎng)絡(luò)生成的圖像效果。可以看到,生成的圖片已經(jīng)非常接近真實圖片的樣子。
模型概覽
GAN
GAN是一種通過對抗的方式,去學(xué)習(xí)數(shù)據(jù)分布的生成模型。以生成圖片為例說明:
- 生成器(G)接收一個隨機的噪聲z,盡可能的生成近似樣本的圖片,記為G(z)。
- 判別器(D)接收一張輸入圖片x,盡可以去判別該圖像是真實圖片還是網(wǎng)絡(luò)生成的假圖片,判別器的輸出 D(x) 代表 x 為真實圖片的概率。如果 D(x)=1 說明判別器認為該輸入一定是真實圖片,如果 D(x)=0 說明判別器認為該輸入一定是假圖片。
在訓(xùn)練的過程中,兩個網(wǎng)絡(luò)互相對抗,最終形成了一個動態(tài)的平衡,上述過程用公式可以被描述為:
在最理想的情況下,G 可以生成與真實圖片極其相似的圖片G(z),而 D 很難判斷這張生成的圖片是否為真,對圖片的真假進行隨機猜測,即 D(G(z))=0.5。
下圖展示了生成對抗網(wǎng)絡(luò)的訓(xùn)練過程,假設(shè)在訓(xùn)練開始時,真實樣本分布、生成樣本分布以及判別模型分別是圖中的黑線、綠線和藍線。在訓(xùn)練開始時,判別模型是無法很好地區(qū)分真實樣本和生成樣本的。接下來當(dāng)我們固定生成模型,而優(yōu)化判別模型時,優(yōu)化結(jié)果如第二幅圖所示,可以看出,這個時候判別模型已經(jīng)可以較好地區(qū)分生成圖片和真實圖片了。第三步是固定判別模型,改進生成模型,試圖讓判別模型無法區(qū)分生成圖片與真實圖片,在這個過程中,可以看出由模型生成的圖片分布與真實圖片分布更加接近,這樣的迭代不斷進行,直到最終收斂,生成分布和真實分布重合,判別模型無法區(qū)分真實圖片與生成圖片。
圖:GAN 訓(xùn)練過程
但是在實際過程中,很難得到這個完美的平衡點,關(guān)于GAN的收斂理論還在持續(xù)不斷的研究中。
DCGAN
DCGAN是深層卷積網(wǎng)絡(luò)與 GAN 的結(jié)合,其基本原理與 GAN 相同,只是將生成器和判別器用兩個卷積網(wǎng)絡(luò)(CNN)替代。為了提高生成樣本的質(zhì)量和網(wǎng)絡(luò)的收斂速度,論文中的 DCGAN 在網(wǎng)絡(luò)結(jié)構(gòu)上進行了一些改進:
- 取消 pooling 層:在網(wǎng)絡(luò)中,所有的pooling層使用步幅卷積(strided convolutions)(判別器)和微步幅度卷積(fractional-strided convolutions)(生成器)進行替換。
- 加入 batch normalization:在生成器和判別器中均加入batchnorm。
- 使用全卷積網(wǎng)絡(luò):去掉了FC層,以實現(xiàn)更深的網(wǎng)絡(luò)結(jié)構(gòu)。
- 激活函數(shù):在生成器(G)中,最后一層使用Tanh函數(shù),其余層采用 ReLu 函數(shù) ; 判別器(D)中都采用LeakyReLu。
圖:DCGAN中的生成器(G)
快速開始
本文的DCGAN任務(wù)依賴于 Paddle Fluid v1.3 及以上版本,請參考官網(wǎng)安裝指南進行安裝。
數(shù)據(jù)準(zhǔn)備
本文使用數(shù)據(jù)規(guī)模較小的MNIST 訓(xùn)練生成器和判別器,該數(shù)據(jù)集可通過paddle.dataset模塊自動下載到本地。
加載包
首先加載 Paddle Fluid和其他相關(guān)包:
- 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 函數(shù),將圖像生成過程可視化:
- 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
定義超參數(shù)
- gf_dim = 64 # 生成器的feature map的基礎(chǔ)通道數(shù)量,生成器中所有的feature map的通道數(shù)量都是基礎(chǔ)通道數(shù)量的倍數(shù)
- df_dim = 64 # 判別器的feature map的基礎(chǔ)通道數(shù)量,判別器中所有的feature map的通道數(shù)量都是基礎(chǔ)通道數(shù)量的倍數(shù)
- gfc_dim = 1024 * 2 # 生成器的全連接層維度
- dfc_dim = 1024 # 判別器的全連接層維度
- img_dim = 28 # 輸入圖片的尺寸
- NOISE_SIZE = 100 # 輸入噪聲的維度
- LEARNING_RATE = 2e-4 # 訓(xùn)練的學(xué)習(xí)率
- epoch = 20 # 訓(xùn)練的epoch數(shù)
- output = "./output_dcgan" # 模型和測試結(jié)果的存儲路徑
- use_cudnn = False # 是否使用cuDNN
- use_gpu=False # 是否使用GPU訓(xùn)練
定義網(wǎng)絡(luò)結(jié)構(gòu)
bn 層
調(diào)用 fluid.layers.batch_norm 接口實現(xiàn)bn層,激活函數(shù)默認使用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)
卷積層
調(diào)用 fluid.nets.simple_img_conv_pool 實現(xiàn)卷積池化組,卷積核大小為3x3,池化窗口大小為2x2,窗口滑動步長為2,激活函數(shù)類型由具體網(wǎng)絡(luò)結(jié)構(gòu)指定。
- 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')
轉(zhuǎn)置卷積層
在生成器中,需要用隨機采樣值生成全尺寸圖像,dcgan使用轉(zhuǎn)置卷積層進行上采樣,在Fluid中,我們調(diào)用 fluid.layers.conv2d_transpose 實現(xiàn)轉(zhuǎn)置卷積。
- 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)
判別器
判別器使用真實數(shù)據(jù)集和生成器生成的假圖片共同進行訓(xùn)練,在訓(xùn)練過程中盡量使真實數(shù)據(jù)集的輸出結(jié)果為1,生成的假圖片輸出結(jié)果為0。本文中實現(xiàn)的判別器由兩個卷積池化層和兩個全連接層組成,其中最后一個全連接層的神經(jīng)元個數(shù)為1,輸出一個二分類結(jié)果。
- 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的全連接層和兩組轉(zhuǎn)置卷積層組成,網(wǎng)絡(luò)輸入為隨機的噪聲數(shù)據(jù),最后一層轉(zhuǎn)置卷積的卷積核數(shù)為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
損失函數(shù)
損失函數(shù)使用 sigmoid_cross_entropy_with_logits
- def loss(x, label):
- return fluid.layers.mean(
- fluid.layers.sigmoid_cross_entropy_with_logits(x=x, label=label))
創(chuàng)建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')
- # 標(biāo)簽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')
- # 噪聲數(shù)據(jù)作為輸入得到生成圖片
- 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作為優(yōu)化器,分別優(yōu)化判別真實圖片的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)
數(shù)據(jù)集 Feeders 配置
下一步,我們開始訓(xùn)練過程。paddle.dataset.mnist.train()用做訓(xùn)練數(shù)據(jù)集。這個函數(shù)返回一個reader——飛槳(PaddlePaddle)中的reader是一個Python函數(shù),每次調(diào)用的時候返回一個Python yield generator。
下面shuffle是一個reader decorator,它接受一個reader A,返回另一個reader B。reader B 每次讀入buffer_size條訓(xùn)練數(shù)據(jù)到一個buffer里,然后隨機打亂其順序,并且逐條輸出。
batch是一個特殊的decorator,它的輸入是一個reader,輸出是一個batched reader。在飛槳(PaddlePaddle)里,一個reader每次yield一條訓(xùn)練數(shù)據(jù),而一個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)
創(chuàng)建執(zhí)行器
- if use_gpu:
- exe = fluid.Executor(fluid.CUDAPlace(0))
- else:
- exe = fluid.Executor(fluid.CPUPlace())
- exe.run(fluid.default_startup_program())
開始訓(xùn)練
訓(xùn)練過程中的每一次迭代,生成器和判別器分別設(shè)置自己的迭代次數(shù)。為了避免判別器快速收斂到0,本文默認每迭代一次,訓(xùn)練一次判別器,兩次生成器。
- t_time = 0
- losses = [[], []]
- # 判別器的迭代次數(shù)
- NUM_TRAIN_TIMES_OF_DG = 2
- # 最終生成圖像的噪聲數(shù)據(jù)
- 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
- # 生成訓(xùn)練過程的噪聲數(shù)據(jù)
- 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')
- # 真實標(biāo)簽
- real_labels = np.ones(
- shape=[real_image.shape[0], 1], dtype='float32')
- # 虛假標(biāo)簽
- 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)
- # 訓(xù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)
- # 每輪的生成結(jié)果
- 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)
打印特定輪次的生成結(jié)果:
- 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)
總結(jié)
DCGAN采用一個隨機噪聲向量作為輸入,輸入通過與CNN類似但是相反的結(jié)構(gòu),將輸入放大成二維數(shù)據(jù)。采用這種結(jié)構(gòu)的生成模型和CNN結(jié)構(gòu)的判別模型,DCGAN在圖片生成上可以達到相當(dāng)可觀的效果。本文中,我們利用DCGAN生成了手寫數(shù)字圖片,您可以嘗試更換數(shù)據(jù)集生成符合個人需求的圖片,完成您的圖像生成任務(wù),快來試試吧!
更多內(nèi)容,可點擊文末閱讀原文或查看:
https://github.com/PaddlePaddle/book/tree/develop/09.gan
ps:最后給大家推薦一個GPU福利-Tesla V100免費算力!配合PaddleHub能讓模型原地起飛~掃碼下方二維碼申請~