前言¶
我們在第六章介紹了生成對抗網絡,並使用生成對抗網絡訓練mnist數據集,生成手寫數字圖片。那麼本章我們將使用對抗生成網絡訓練我們自己的圖片數據集,並生成圖片。在第六章中我們使用的黑白的單通道圖片,在這一章中,我們使用的是3通道的彩色圖。
GitHub地址:https://github.com/yeyupiaoling/LearnPaddle2/tree/master/note13
定義數據讀取¶
我們首先創建一個image_reader.py文件,用於讀取我們自己定義的圖片數據集。首先導入所需的依賴包。
import os
import random
from multiprocessing import cpu_count
import numpy as np
import paddle
from PIL import Image
這裏的圖片預處理主要是對圖片進行等比例壓縮和中心裁剪,這裏爲了避免圖片在圖片在resize時出現變形的情況,導致訓練生成的圖片不是我們真實圖片的樣子。這裏爲了增強數據集,做了隨機水平翻轉。最後在處理圖片的時候,爲了避免數據集中有單通道圖片導致訓練中斷,所以還把單通道圖轉成3通道圖片。
# 測試圖片的預處理
def train_mapper(sample):
img, crop_size = sample
img = Image.open(img)
# 隨機水平翻轉
r1 = random.random()
if r1 > 0.5:
img = img.transpose(Image.FLIP_LEFT_RIGHT)
# 等比例縮放和中心裁剪
width = img.size[0]
height = img.size[1]
if width < height:
ratio = width / crop_size
width = width / ratio
height = height / ratio
img = img.resize((int(width), int(height)), Image.ANTIALIAS)
height = height / 2
crop_size2 = crop_size / 2
box = (0, int(height - crop_size2), int(width), int(height + crop_size2))
else:
ratio = height / crop_size
height = height / ratio
width = width / ratio
img = img.resize((int(width), int(height)), Image.ANTIALIAS)
width = width / 2
crop_size2 = crop_size / 2
box = (int(width - crop_size2), 0, int(width + crop_size2), int(height))
img = img.crop(box)
img = img.resize((crop_size, crop_size), Image.ANTIALIAS)
# 把單通道圖變成3通道
if len(img.getbands()) == 1:
img1 = img2 = img3 = img
img = Image.merge('RGB', (img1, img2, img3))
# 轉換成numpy值
img = np.array(img).astype(np.float32)
# 轉換成CHW
img = img.transpose((2, 0, 1))
# 轉換成BGR
img = img[(2, 1, 0), :, :] / 255.0
return img
在這篇文章中,我們讀取數據集不需要使用到數據列表,因爲我們並沒有進行分類,只是把所有的圖片用於訓練並生成圖片。所有這裏只需要把文件中的所有圖片都讀取進行訓練就 可以了。
# 測試的圖片reader
def train_reader(train_image_path, crop_size):
pathss = []
for root, dirs, files in os.walk(train_image_path):
path = [os.path.join(root, name) for name in files]
pathss.extend(path)
def reader():
for line in pathss:
yield line, crop_size
return paddle.reader.xmap_readers(train_mapper, reader, cpu_count(), 1024)
訓練生成模型¶
下面創建train.py文件,用於訓練對抗生成模型,並在訓練過程中生成圖片和保存預測模型。首先導入所需的依賴包。
import os
import shutil
import numpy as np
import paddle
import paddle.fluid as fluid
import matplotlib.pyplot as plt
import image_reader
下面時定義生成器的,我們在第六章也介紹過。生成器的作用是儘可能生成滿足判別器條件的圖像。隨着以上訓練的進行,判別器不斷增強自身的判別能力,而生成器也不斷生成越來越逼真的圖片,以欺騙判別器。生成器主要由兩組全連接和BN層、兩組轉置卷積運算組成。唯一不同的時在生成器最後輸出的大小是3,因爲我們生成的圖片是3通道的彩色圖片,而且使用的激活函數是sigmoid,保證了輸出的結果都是在0到1範圍之內,這是彩色圖片的顏色範圍。
# 訓練的圖片大小
image_size = 112
# 定義生成器
def Generator(y, name="G"):
def deconv(x, num_filters, filter_size=5, stride=2, dilation=1, padding=2, output_size=None, act=None):
return fluid.layers.conv2d_transpose(input=x,
num_filters=num_filters,
output_size=output_size,
filter_size=filter_size,
stride=stride,
dilation=dilation,
padding=padding,
act=act)
with fluid.unique_name.guard(name + "/"):
# 第一組全連接和BN層
y = fluid.layers.fc(y, size=2048)
y = fluid.layers.batch_norm(y)
# 第二組全連接和BN層
y = fluid.layers.fc(y, size=int(128 * (image_size / 4) * (image_size / 4)))
y = fluid.layers.batch_norm(y)
# 進行形狀變換
y = fluid.layers.reshape(y, shape=[-1, 128, int((image_size / 4)), int((image_size / 4))])
# 第一組轉置卷積運算
y = deconv(x=y, num_filters=128, act='relu', output_size=[int((image_size / 2)), int((image_size / 2))])
# 第二組轉置卷積運算
y = deconv(x=y, num_filters=3, act='sigmoid', output_size=[image_size, image_size])
return y
判別器的作用是訓練真實的數據集,然後使用訓練真實數據集模型去判別生成器生成的假圖片。這一過程可以理解判別器爲一個二分類問題,判別器在訓練真實數據集時,儘量讓其輸出概率爲1,而訓練生成器生成的假圖片輸出概率爲0。這樣不斷給生成器壓力,讓其生成的圖片儘量逼近真實圖片,以至於真實到連判別器也無法判斷這是真實圖像還是假圖片。以下判別器由三組卷積池化層和一個最後全連接層組成,全連接層的大小爲1,輸入一個二分類的結果。
# 判別器 Discriminator
def Discriminator(images, name="D"):
# 定義一個卷積池化組
def conv_pool(input, num_filters, act=None):
return fluid.nets.simple_img_conv_pool(input=input,
filter_size=3,
num_filters=num_filters,
pool_size=2,
pool_stride=2,
act=act)
with fluid.unique_name.guard(name + "/"):
y = fluid.layers.reshape(x=images, shape=[-1, 3, image_size, image_size])
# 第一個卷積池化組
y = conv_pool(input=y, num_filters=64, act='leaky_relu')
# 第一個卷積池化加回歸層
y = conv_pool(input=y, num_filters=128)
y = fluid.layers.batch_norm(input=y, act='leaky_relu')
# 第二個卷積池化加回歸層
y = fluid.layers.fc(input=y, size=1024)
y = fluid.layers.batch_norm(input=y, act='leaky_relu')
# 最後一個分類器輸出
y = fluid.layers.fc(input=y, size=1, act='sigmoid')
return y
然後在這裏獲取所需的程序,如判別器D識別生成器G生成的假圖片程序,判別器D識別真實圖片程序,生成器G生成符合判別器D的程序和初始化的程序。最後定義一個get_params()函數用於獲取參數名稱。
# 創建判別器D識別生成器G生成的假圖片程序
train_d_fake = fluid.Program()
# 創建判別器D識別真實圖片程序
train_d_real = fluid.Program()
# 創建生成器G生成符合判別器D的程序
train_g = fluid.Program()
# 創建共同的一個初始化的程序
startup = fluid.Program()
# 噪聲維度
z_dim = 100
# 從Program獲取prefix開頭的參數名字
def get_params(program, prefix):
all_params = program.global_block().all_parameters()
return [t.name for t in all_params if t.name.startswith(prefix)]
定義一個判別器識別真實圖片的程序,這裏判別器傳入的數據是真實的圖片數據,這裏的輸出圖片是3通道的。這裏使用的損失函數是fluid.layers.sigmoid_cross_entropy_with_logits(),這個損失函數是求它們在任務上的錯誤率,他們的類別是互不排斥的。所以無論真實圖片的標籤是什麼,都不會影響模型識別爲真實圖片。這裏更新的也只有判別器模型的參數,使用的優化方法是Adam。
# 訓練判別器D識別真實圖片
with fluid.program_guard(train_d_real, startup):
# 創建讀取真實數據集圖片的data,並且label爲1
real_image = fluid.layers.data('image', shape=[3, image_size, image_size])
ones = fluid.layers.fill_constant_batch_size_like(real_image, shape=[-1, 1], dtype='float32', value=1)
# 判別器D判斷真實圖片的概率
p_real = Discriminator(real_image)
# 獲取損失函數
real_cost = fluid.layers.sigmoid_cross_entropy_with_logits(p_real, ones)
real_avg_cost = fluid.layers.mean(real_cost)
# 獲取判別器D的參數
d_params = get_params(train_d_real, "D")
# 創建優化方法
optimizer = fluid.optimizer.Adam(learning_rate=2e-4)
optimizer.minimize(real_avg_cost, parameter_list=d_params)
這裏定義一個判別器識別生成器生成的圖片的程序,這裏是使用噪聲的維度進行輸入。這裏判別器識別的是生成器生成的圖片,這裏使用的損失函數同樣是fluid.layers.sigmoid_cross_entropy_with_logits()。這裏更新的參數還是判別器模型的參數,也是使用Adam優化方法。
# 訓練判別器D識別生成器G生成的圖片爲假圖片
with fluid.program_guard(train_d_fake, startup):
# 利用創建假的圖片data,並且label爲0
z = fluid.layers.data(name='z', shape=[z_dim])
zeros = fluid.layers.fill_constant_batch_size_like(z, shape=[-1, 1], dtype='float32', value=0)
# 判別器D判斷假圖片的概率
p_fake = Discriminator(Generator(z))
# 獲取損失函數
fake_cost = fluid.layers.sigmoid_cross_entropy_with_logits(p_fake, zeros)
fake_avg_cost = fluid.layers.mean(fake_cost)
# 獲取判別器D的參數
d_params = get_params(train_d_fake, "D")
# 創建優化方法
optimizer = fluid.optimizer.Adam(learning_rate=2e-4)
optimizer.minimize(fake_avg_cost, parameter_list=d_params)
最後定義一個訓練生成器生成圖片的模型,這裏也克隆一個預測程序,用於之後在訓練的時候輸出預測的圖片。損失函數和優化方法都一樣,但是要更新的參數是生成器的模型參。
# 訓練生成器G生成符合判別器D標準的假圖片
fake = None
with fluid.program_guard(train_g, startup):
# 噪聲生成圖片爲真實圖片的概率,Label爲1
z = fluid.layers.data(name='z', shape=[z_dim])
ones = fluid.layers.fill_constant_batch_size_like(z, shape=[-1, 1], dtype='float32', value=1)
# 生成圖片
fake = Generator(z)
# 克隆預測程序
infer_program = train_g.clone(for_test=True)
# 生成符合判別器的假圖片
p = Discriminator(fake)
# 獲取損失函數
g_cost = fluid.layers.sigmoid_cross_entropy_with_logits(p, ones)
g_avg_cost = fluid.layers.mean(g_cost)
# 獲取G的參數
g_params = get_params(train_g, "G")
# 只訓練G
optimizer = fluid.optimizer.Adam(learning_rate=2e-4)
optimizer.minimize(g_avg_cost, parameter_list=g_params)
這裏創建一個可以生成訓練噪聲數據的reader函數。
# 噪聲生成
def z_reader():
while True:
yield np.random.uniform(-1.0, 1.0, (z_dim)).astype('float32')
這裏定義一個保存在訓練過程生成的圖片,通過觀察生成圖片的情況,可以瞭解到訓練的效果。
# 保存圖片
def show_image_grid(images):
for i, image in enumerate(images):
image = image.transpose((2, 1, 0))
save_image_path = 'train_image'
if not os.path.exists(save_image_path):
os.makedirs(save_image_path)
plt.imsave(os.path.join(save_image_path, "test_%d.png" % i), image)
這裏就開始獲取自定義的圖片數據集,這裏只需要把存放圖片數據集的文件夾傳進去就可以了。
# 生成真實圖片reader
mydata_generator = paddle.batch(reader=image_reader.train_reader('datasets', image_size), batch_size=32)
# 生成假圖片的reader
z_generator = paddle.batch(z_reader, batch_size=32)()
test_z = np.array(next(z_generator))
接着獲取執行器,準備進行訓練,這裏筆者建議最好使用GPU,因爲CPU賊慢。
# 創建執行器,最好使用GPU,CPU速度太慢了
# place = fluid.CPUPlace()
place = fluid.CUDAPlace(0)
exe = fluid.Executor(place)
# 初始化參數
exe.run(startup)
最好就可以開始訓練啦,我們可以在訓練的時候輸出訓練的損失值。在訓練每一個Pass之後又可以使用預測程序生成圖片並進行保存到本地。
# 開始訓練
for pass_id in range(100):
for i, real_image in enumerate(mydata_generator()):
# 訓練判別器D識別真實圖片
r_fake = exe.run(program=train_d_fake,
fetch_list=[fake_avg_cost],
feed={'z': test_z})
# 訓練判別器D識別生成器G生成的假圖片
r_real = exe.run(program=train_d_real,
fetch_list=[real_avg_cost],
feed={'image': np.array(real_image)})
# 訓練生成器G生成符合判別器D標準的假圖片
r_g = exe.run(program=train_g,
fetch_list=[g_avg_cost],
feed={'z': test_z})
if i % 100 == 0:
print("Pass:%d, Batch:%d, 訓練判別器D識別真實圖片Cost:%0.5f, "
"訓練判別器D識別生成器G生成的假圖片Cost:%0.5f, "
"訓練生成器G生成符合判別器D標準的假圖片Cost:%0.5f" % (pass_id, i, r_fake[0], r_real[0], r_g[0]))
# 測試生成的圖片
r_i = exe.run(program=infer_program,
fetch_list=[fake],
feed={'z': test_z})
r_i = np.array(r_i).astype(np.float32)
# 顯示生成的圖片
show_image_grid(r_i[0])
同時在每個Pass之後又可以保存預測函數,用於之後預測生成圖片使用。
# 保存預測模型
save_path = 'infer_model/'
# 刪除舊的模型文件
shutil.rmtree(save_path, ignore_errors=True)
# 創建保持模型文件目錄
os.makedirs(save_path)
# 保存預測模型
fluid.io.save_inference_model(save_path, feeded_var_names=[z.name], target_vars=[fake], executor=exe, main_program=train_g)
在訓練的過程可以輸出每一個訓練程序輸出的損失值:
Pass:0, Batch:0, 訓練判別器D識別真實圖片Cost:1.03734, 訓練判別器D識別生成器G生成的假圖片Cost:0.46931, 訓練生成器G生成符合判別器D標準的假圖片Cost:0.54236
Pass:1, Batch:0, 訓練判別器D識別真實圖片Cost:1.09766, 訓練判別器D識別生成器G生成的假圖片Cost:0.32896, 訓練生成器G生成符合判別器D標準的假圖片Cost:0.44473
Pass:2, Batch:0, 訓練判別器D識別真實圖片Cost:1.17703, 訓練判別器D識別生成器G生成的假圖片Cost:0.38643, 訓練生成器G生成符合判別器D標準的假圖片Cost:0.39445
使用模型生成圖片¶
在上一個文件中,我們已經訓練得到一個預測模型,下面我們將使用這個預測模型直接生成圖片。創建infer.py文件用於預測生成圖片。首先導入相應的依賴包。
import os
import paddle
import matplotlib.pyplot as plt
import numpy as np
import paddle.fluid as fluid
然後創建執行器,這裏可以使用CPU進行預測可以,因爲預測並不需要太大的計算。然後加載上一步訓練保存的預測模型,獲取預測程序,輸入層的名稱,和生成器。
# 創建執行器
place = fluid.CPUPlace()
exe = fluid.Executor(place)
exe.run(fluid.default_startup_program())
# 保存預測模型路徑
save_path = 'infer_model/'
# 從模型中獲取預測程序、輸入數據名稱列表、分類器
[infer_program, feeded_var_names, target_var] = fluid.io.load_inference_model(dirname=save_path, executor=exe)
跟訓練的時候一樣,需要生成噪聲數據作爲輸入數據。這裏說明一下,輸入數據z_generator的batch大小就是生成圖片的數量。
# 噪聲維度
z_dim = 100
# 噪聲生成
def z_reader():
while True:
yield np.random.uniform(-1.0, 1.0, (z_dim)).astype('float32')
z_generator = paddle.batch(z_reader, batch_size=32)()
test_z = np.array(next(z_generator))
這裏創建一個保存生成圖片的函數,用於保存預測生成的圖片。
# 保存圖片
def save_image(images):
for i, image in enumerate(images):
image = image.transpose((2, 1, 0))
save_image_path = 'infer_image'
if not os.path.exists(save_image_path):
os.makedirs(save_image_path)
plt.imsave(os.path.join(save_image_path, "test_%d.png" % i), image)
最後執行預測程序,開始生成圖片。預測輸出的結果就是圖片的數據,通過保存這些數據就是保存圖片了。
# 測試生成的圖片
r_i = exe.run(program=infer_program,
feed={feeded_var_names[0]: test_z},
fetch_list=target_var)
r_i = np.array(r_i).astype(np.float32)
# 顯示生成的圖片
save_image(r_i[0])
print('生成圖片完成')
目前這個網絡在訓練比較複雜的圖片時,模型的擬合效果並不太好,也就是說生成的圖片沒有我們想象那麼好。所以這個網絡還需要不斷調整,如果讀者有更好的建議,歡迎交流一下。
上一章:《PaddlePaddle從入門到煉丹》十二——自定義文本數據集分類¶
下一章:《PaddlePaddle從入門到煉丹》十四——把預測模型部署在服務器¶
參考資料¶
- https://github.com/oraoto/learn_ml/blob/master/paddle/gan-mnist-split.ipynb
- https://www.cnblogs.com/max-hu/p/7129188.html
- https://blog.csdn.net/somtian/article/details/72126328