前言

我們在第六章介紹了生成對抗網絡,並使用生成對抗網絡訓練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從入門到煉丹》十四——把預測模型部署在服務器


參考資料

  1. https://github.com/oraoto/learn_ml/blob/master/paddle/gan-mnist-split.ipynb
  2. https://www.cnblogs.com/max-hu/p/7129188.html
  3. https://blog.csdn.net/somtian/article/details/72126328
小夜