前言

我們上一章使用MNIST數據集進行訓練,獲得一個可以分類手寫字體的模型。如果我們數據集的數量不夠,不足於讓模型收斂,最直接的是增加數據集。但是我們收集數據並進行標註是非常消耗時間了,而最近非常火的生成對抗網絡就非常方便我們數據的收集。對抗生成網絡可以根據之前的圖片訓練生成更多的圖像,已達到以假亂真的目的。

訓練並預測

創建一個GAN.py文件。首先導入所需要的Python包,其中matplotlib包是之後用於展示出生成的圖片。

import numpy as np
import paddle
import paddle.fluid as fluid
import matplotlib.pyplot as plt

定義網絡

生成對抗網絡由生成器和判別器組合,下面的代碼片段就是一個生成器,生成器的作用是儘可能生成滿足判別器條件的圖像。隨着以上訓練的進行,判別器不斷增強自身的判別能力,而生成器也不斷生成越來越逼真的圖片,以欺騙判別器。生成器主要由兩組全連接和BN層、兩組轉置卷積運算組成,其中最後一層的卷積層的卷積核數量是1,因爲輸出的圖像是一個灰度圖的手寫字體圖片。

# 定義生成器
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=128 * 7 * 7)
        y = fluid.layers.batch_norm(y)
        # 進行形狀變換
        y = fluid.layers.reshape(y, shape=(-1, 128, 7, 7))
        # 第一組轉置卷積運算
        y = deconv(x=y, num_filters=128, act='relu', output_size=[14, 14])
        # 第二組轉置卷積運算
        y = deconv(x=y, num_filters=1, act='tanh', output_size=[28, 28])
    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=5,
                                               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, 1, 28, 28])
        # 第一個卷積池化組
        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

定義訓練程序

定義四個Program和一個噪聲維度,其中使用三個Program分別進行訓練生成器生成圖片、訓練判別器識別真實圖片、訓練判別器識別生成器生成的假圖片,還要一個Program是用於初始化參數的。噪聲的作用是初始化生成圖片。

# 創建判別器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中的獨立參數,因爲我們同時訓練3個Program,其中訓練生成器或訓練判別器時,它們參數的更新不應該互相影響。就是訓練判別器識別真實圖片時,在更新判別器模型參數時,不要更新生成器模型的參數,同理更新生成器模型參數時,不要更新判別器的模型參數。

# 從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)]

定義一個判別器識別真實圖片的程序,這裏判別器傳入的數據是真實的圖片數據。這裏使用的損失函數是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=[1, 28, 28])
    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.AdamOptimizer(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, 1, 1])
    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.AdamOptimizer(learning_rate=2e-4)
    optimizer.minimize(fake_avg_cost, parameter_list=d_params)

最後定義一個訓練生成器生成圖片的模型,這裏也克隆一個預測程序,用於之後在訓練的時候輸出預測的圖片。損失函數和優化方法都一樣,但是要更新的參數是生成器的模型參。

# 訓練生成器G生成符合判別器D標準的假圖片
with fluid.program_guard(train_g, startup):
    # 噪聲生成圖片爲真實圖片的概率,Label爲1
    z = fluid.layers.data(name='z', shape=[z_dim, 1, 1])
    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.AdamOptimizer(learning_rate=2e-4)
    optimizer.minimize(g_avg_cost, parameter_list=g_params)

訓練並預測

通過由噪聲來生成假的圖片數據輸入。

# 噪聲生成
def z_reader():
    while True:
        yield np.random.normal(0.0, 1.0, (z_dim, 1, 1)).astype('float32')

讀取真實圖片的數據集,這裏去除了數據集中的label數據,因爲label在這裏使用不上,這裏不考慮標籤分類問題。

# 讀取MNIST數據集,不使用label
def mnist_reader(reader):
    def r():
        for img, label in reader():
            yield img.reshape(1, 28, 28)
    return r

把預測的圖片保存到本地目錄上,如果使用jupyter,可用把圖片打印到頁面上。

# 顯示圖片
def show_image_grid(images, pass_id=None):
    # fig = plt.figure(figsize=(5, 5))
    # fig.suptitle("Pass {}".format(pass_id))
    # gs = plt.GridSpec(8, 8)
    # gs.update(wspace=0.05, hspace=0.05)

    for i, image in enumerate(images[:64]):
        # 保存生成的圖片
        plt.imsave("image/test_%d.png" % i, image[0])
    # 以下代碼在jupyter可用
    #     ax = plt.subplot(gs[i])
    #     plt.axis('off')
    #     ax.set_xticklabels([])
    #     ax.set_yticklabels([])
    #     ax.set_aspect('equal')
    #     plt.imshow(image[0], cmap='Greys_r')
    # plt.show()

將真實數據和噪聲生成的數據的生成一個reader。

# 生成真實圖片reader
mnist_generator = paddle.batch(
    paddle.reader.shuffle(mnist_reader(paddle.dataset.mnist.train()), 30000), batch_size=128)
# 生成假圖片的reader
z_generator = paddle.batch(z_reader, batch_size=128)()

創建一個執行器,這裏使用的GPU進行訓練,因爲該網絡比較大,使用CPU訓練速度會非常慢。如果讀者沒有GPU只有,可以取消註釋place = fluid.CPUPlace()這行代碼,並註釋place = fluid.CUDAPlace(0)這行代碼,就可以使用CPU進行訓練了。

# 創建執行器
# place = fluid.CPUPlace()
place = fluid.CUDAPlace(0)
exe = fluid.Executor(place)
# 初始化參數
exe.run(startup)

獲取測試需要的噪聲數據,使用這些數據進行預測,獲取預測的圖片。

# 測試噪聲數據
test_z = np.array(next(z_generator))

開始訓練,這裏同時訓練了3個程序,分別是訓練判別器D識別生成器G生成的假圖片、訓練判別器D識別真實圖片、訓練生成器G生成符合判別器D標準的假圖片。通過不斷更新判別器的參數,使得判別器的識別能力越來越強。不斷更新生成器的參數,使得生成器生成的圖像越來越逼近真實圖像。在每一輪訓練結束後,進行一次預測,輸入生成器生成的圖片並顯示出來。

# 開始訓練
for pass_id in range(5):
    for i, real_image in enumerate(mnist_generator()):
        # 訓練判別器D識別生成器G生成的假圖片
        r_fake = exe.run(program=train_d_fake,
                         fetch_list=[fake_avg_cost],
                         feed={'z': np.array(next(z_generator))})

        # 訓練判別器D識別真實圖片
        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': np.array(next(z_generator))})
    print("Pass:%d,fake_avg_cost:%f, real_avg_cost:%f, g_avg_cost:%f" % (pass_id, r_fake[0][0], r_real[0][0], r_g[0][0]))

    # 測試生成的圖片
    r_i = exe.run(program=infer_program,
                  fetch_list=[fake],
                  feed={'z': test_z})

    # 顯示生成的圖片
    show_image_grid(r_i[0], pass_id)

到處爲止,本章就結束了。通過學習本章,是不是覺得生成對抗網絡非常神奇呢,讀者可以參數一下其他的數據,通過生成對抗網絡生成更多有趣的圖像數據集。從本章可以瞭解到深度學習的強大,但深度學習遠遠不止這些,在下一章,我們使用深度學習中的強化學習,通過訓練獲取模型,使用模型來自己玩一個小遊戲。

同步到百度AI Studio平臺:http://aistudio.baidu.com/aistudio/projectdetail/29365
同步到科賽網K-Lab平臺:https://www.kesci.com/home/project/5bf8cd7c954d6e001066d82e
項目代碼GitHub地址:https://github.com/yeyupiaoling/LearnPaddle2/tree/master/note6

注意: 最新代碼以GitHub上的爲準


上一章:《PaddlePaddle從入門到煉丹》五——循環神經網絡
下一章:《PaddlePaddle從入門到煉丹》七——強化學習


參考資料

  1. https://www.cnblogs.com/max-hu/p/7129188.html
  2. https://github.com/oraoto/learn_ml/blob/master/paddle/gan-mnist-split.ipynb
  3. https://blog.csdn.net/somtian/article/details/72126328
  4. http://www.paddlepaddle.org/documentation/api/zh/1.1/layers.html#sigmoid-cross-entropy-with-logits
小夜