iT邦幫忙

2023 iThome 鐵人賽

DAY 17
0

前言

昨天費盡了九牛二虎之力帶各位推導出BGAN的生成器損失函數,今天就要來實作BGAN了。放心,損失函數定一到了程式環節一樣簡單。

建立BGAN模型

WGAN模型與普通GAN在實作上只有一些差異,分別為:

  1. 生成器 (對抗模型)使用Boundary Loss,激活函數使用Leaky ReLU
  2. 我實際實驗時發現判別器層數太多會梯度爆炸,故判別器層數有減少。

第一步:決定任務類型與要使用的模型

與之前一樣,使用BGANmnist手寫資料集的圖片生成。此外這次的BGAN同樣也是使用DCGAN改來的。

第二步:匯入函式庫

這邊一樣除了Keras後端API是新的以外,其他都與DCGAN差不多。

from tensorflow.keras.datasets import mnist
from tensorflow.keras.layers import Input, Dense, Reshape, Flatten, BatchNormalization, LeakyReLU, Conv2DTranspose, Conv2D
from tensorflow.keras.models import Model, save_model
from tensorflow.keras.optimizers import Adam
import tensorflow.keras.backend as K
import matplotlib.pyplot as plt
import numpy as np
import os

第三步:資料前處理 (Data Preprocessing)

資料前處理老套路了,一模一樣:

def load_data(self):
        (x_train, _), (_, _) = mnist.load_data()  # 底線是未被用到的資料,可忽略
        x_train = (x_train / 127.5)-1  # 正規化
        x_train = x_train.reshape((-1, 28, 28, 1))
        return x_train

第四步:建立生成模型類別

這部分也沒有差別。

class BGAN():
    def __init__(self, generator_lr, discriminator_lr):
        self.generator_lr = generator_lr
        self.discriminator_lr = discriminator_lr

        self.discriminator = self.build_discriminator()
        self.generator = self.build_generator()
        self.adversarial = self.build_adversarialmodel()

        self.gloss = []
        self.dloss = []
        if not os.path.exists('result/BGAN/imgs'):# 將訓練過程產生的圖片儲存起來
            os.makedirs('result/BGAN/imgs')# 如果忘記新增資料夾可以用這個方式建立

第五步:建立模型、定義訓練方法

這個部分只需要注意對抗模型在編譯模型時使用Boundary Loss即可。

生成器
生成器與DCGAN的生成器無異。

def build_generator(self):
        input_ = Input(shape=(100, ))
        x = Dense(7*7*32)(input_)
        x = BatchNormalization(momentum=0.8)(x)
        x = LeakyReLU(alpha=0.2)(x)
        x = Reshape((7, 7, 32))(x)
        x = Conv2DTranspose(128, kernel_size=4, strides=2, padding='same')(x)
        x = BatchNormalization(momentum=0.8)(x)
        x = LeakyReLU(alpha=0.2)(x)
        x = Conv2DTranspose(256, kernel_size=4, strides=2, padding='same')(x)
        x = BatchNormalization(momentum=0.8)(x)
        x = LeakyReLU(alpha=0.2)(x)
        out = Conv2DTranspose(1, kernel_size=4, strides=1, padding='same', activation='tanh')(x)

        model = Model(inputs=input_, outputs=out, name='Generator')
        model.summary()
        return model

判別器
判別器太深容易使訓練梯度爆炸,所以降低了層數。

def build_discriminator(self):
        input_ = Input(shape = (28, 28, 1))
        x = Conv2D(256, kernel_size=4, strides=2, padding='same')(input_)
        x = LeakyReLU(alpha=0.2)(x)
        x = Conv2D(128, kernel_size=4, strides=2, padding='same')(x)
        x = LeakyReLU(alpha=0.2)(x)
        x = Flatten()(x)
        out = Dense(1, activation='sigmoid')(x)

        model = Model(inputs=input_, outputs=out, name='Discriminator')
        dis_optimizer = Adam(learning_rate=self.discriminator_lr, beta_1=0.5)
        model.compile(loss='binary_crossentropy',
                      optimizer=dis_optimizer,
                      metrics=['accuracy'])
        model.summary()
        return model

Boundary Loss :Boundary Loss做起來也較簡單,複習一下昨天講到的結論。
https://ithelp.ithome.com.tw/upload/images/20230920/20151029PB3biDCbOz.png

接著寫成程式:

因為模型訓練時呼叫方法時會確認參數的使用,所以即使是沒使用到y_true這個參數,定義副程式時依然要y_true寫進去喔!y_pred就是對抗模型中生成器生成圖片經過判別器D的判斷結果。

def boundary_loss(self, y_true, y_pred):
        return 0.5 * K.mean((K.log(y_pred) - K.log(1 - y_pred)) ** 2)

對抗模型:
對抗模型的損失使用boundary_loss,其他沒有甚麼改動。

def build_adversarialmodel(self):
        noise_input = Input(shape=(100, ))
        generator_sample = self.generator(noise_input)
        self.discriminator.trainable = False
        out = self.discriminator(generator_sample)
        model = Model(inputs=noise_input, outputs=out)

        adv_optimizer = Adam(learning_rate=self.generator_lr, beta_1=0.5)
        model.compile(loss=self.boundary_loss, optimizer=adv_optimizer)
        model.summary()
        return model

訓練步驟:
這部分也是一樣完全沒有變XD,總覺得今天很輕鬆。因為昨天死了很多腦細胞,今天平衡一下也好。

def train(self, epochs, batch_size=128, sample_interval=50):
        # 準備訓練資料
        x_train = self.load_data()
        # 準備訓練的標籤,分為真實標籤與假標籤
        valid = np.ones((batch_size, 1))
        fake = np.zeros((batch_size, 1))
        for epoch in range(epochs):
            # 隨機取一批次的資料用來訓練
            idx = np.random.randint(0, x_train.shape[0], batch_size)
            imgs = x_train[idx]
            # 從常態分佈中採樣一段雜訊
            noise = np.random.normal(0, 1, (batch_size, 100))
            # 生成一批假圖片
            gen_imgs = self.generator.predict(noise)
            # 判別器訓練判斷真假圖片
            d_loss_real = self.discriminator.train_on_batch(imgs, valid)
            d_loss_fake = self.discriminator.train_on_batch(gen_imgs, fake)
            d_loss = 0.5 * np.add(d_loss_real, d_loss_fake)
            #儲存鑑別器損失變化 索引值0為損失 索引值1為準確率
            self.dloss.append(d_loss[0])
            # 訓練生成器的生成能力
            noise = np.random.normal(0, 1, (batch_size, 100))
            g_loss = self.adversarial.train_on_batch(noise, valid)
            # 儲存生成器損失變化
            self.gloss.append(g_loss)
            # 將這一步的訓練資訊print出來
            print(f"Epoch:{epoch} [D loss: {d_loss[0]}, acc: {100 * d_loss[1]:.2f}] [G loss: {g_loss}]")
            # 在指定的訓練次數中,隨機生成圖片,將訓練過程的圖片儲存起來
            if epoch % sample_interval == 0:
                self.sample(epoch)
        self.save_data()

定義其他副程式

儲存資料的部分也沒有變。
儲存損失變化與模型權重的部分:

def save_data(self):
        np.save(file='./result/BGAN/generator_loss.npy',arr=np.array(self.gloss))
        np.save(file='./result/BGAN/discriminator_loss.npy', arr=np.array(self.dloss))
        save_model(model=self.generator,filepath='./result/BGAN/Generator.h5')
        save_model(model=self.discriminator,filepath='./result/BGAN/Discriminator.h5')
        save_model(model=self.adversarial,filepath='./result/BGAN/Adversarial.h5')

模型生成圖片的部分。

def sample(self, epoch=None, num_images=25, save=True):
        r = int(np.sqrt(num_images))
        noise = np.random.normal(0, 1, (num_images, 100))
        gen_imgs = self.generator.predict(noise)
        gen_imgs = (gen_imgs+1)/2
        fig, axs = plt.subplots(r, r)
        count = 0
        for i in range(r):
            for j in range(r):
                axs[i, j].imshow(gen_imgs[count, :, :, 0], cmap='gray')
                axs[i, j].axis('off')
                count += 1
        if save:
            fig.savefig(f"./result/BGAN/imgs/{epoch}epochs.png")
        else:
            plt.show()
        plt.close()

第六步:開始訓練

這邊參數設定如下表:

參數 參數值
生成器學習率 0.0002
判別器學習率 0.0002
Batch Size 128
訓練次數 10000
if __name__ == '__main__':
    gan = BGAN(generator_lr=0.0002,discriminator_lr=0.0002)
    gan.train(epochs=10000, batch_size=128, sample_interval=200)
    gan.sample(save=False)

第七步:審視訓練結果並改進

損失圖如下,判別器數值一直保持穩定,生成器則會慢慢學習生成判別器評價好的圖片。
https://ithelp.ithome.com.tw/upload/images/20230920/20151029aW2LxeOJN1.png

訓練過程如下,總覺得雖然多樣性提高了,但會生成根本不是數字的東西XD

Epoch=200
https://ithelp.ithome.com.tw/upload/images/20230920/20151029X924upIBtw.png

Epoch=2000
https://ithelp.ithome.com.tw/upload/images/20230920/20151029TGjzkbADnB.png

Epoch=5000,把0生成變成三角形真的是…很可愛。
https://ithelp.ithome.com.tw/upload/images/20230920/20151029nahgm5B9Pd.png

Epoch=10000,有一些圖片看的出來其數字,但也出現了很多莫名其妙的符號。
https://ithelp.ithome.com.tw/upload/images/20230920/20151029vor8o79sCO.png

訓練過程一樣整理成動畫給各位欣賞:
BGAN

結語

今天帶各位實作了BGAN,這幾天介紹的三種GAN都是將雜訊做為輸入,並且隨機生成圖片,我們對於生成結果是不可控的。如果要隨我們的意思生成圖片的話就必須要加入條件控制,至於條件控制如何添加我將在明天開始介紹,希望這幾天對於GAN的實作與理論解說能讓你更了解其運作原理。接著模型會越來越複雜,但也會越來越好玩喔!

附錄:完整程式

from tensorflow.keras.datasets import mnist
from tensorflow.keras.layers import Input, Dense, Reshape, Flatten, BatchNormalization, LeakyReLU, Conv2DTranspose, Conv2D
from tensorflow.keras.models import Model, save_model
from tensorflow.keras.optimizers import Adam
import tensorflow.keras.backend as K
import matplotlib.pyplot as plt
import numpy as np
import os

class BGAN():
    def __init__(self, generator_lr, discriminator_lr):
        self.generator_lr = generator_lr
        self.discriminator_lr = discriminator_lr

        self.discriminator = self.build_discriminator()
        self.generator = self.build_generator()
        self.adversarial = self.build_adversarialmodel()

        self.gloss = []
        self.dloss = []
        if not os.path.exists('result/BGAN/imgs'):# 將訓練過程產生的圖片儲存起來
            os.makedirs('result/BGAN/imgs')# 如果忘記新增資料夾可以用這個方式建立

    def load_data(self):
        (x_train, _), (_, _) = mnist.load_data()  # 底線是未被用到的資料,可忽略
        x_train = (x_train / 127.5)-1  # 正規化
        x_train = x_train.reshape((-1, 28, 28, 1))
        return x_train

    def boundary_loss(self, y_true, y_pred):
        """
        Boundary seeking loss.
        Reference: https://wiseodd.github.io/techblog/2017/03/07/boundary-seeking-gan/
        """
        return 0.5 * K.mean((K.log(y_pred) - K.log(1 - y_pred)) ** 2)
    def build_generator(self):
        input_ = Input(shape=(100, ))
        x = Dense(7*7*32)(input_)
        x = BatchNormalization(momentum=0.8)(x)
        x = LeakyReLU(alpha=0.2)(x)
        x = Reshape((7, 7, 32))(x)
        x = Conv2DTranspose(128, kernel_size=4, strides=2, padding='same')(x)
        x = BatchNormalization(momentum=0.8)(x)
        x = LeakyReLU(alpha=0.2)(x)
        x = Conv2DTranspose(256, kernel_size=4, strides=2, padding='same')(x)
        x = BatchNormalization(momentum=0.8)(x)
        x = LeakyReLU(alpha=0.2)(x)
        out = Conv2DTranspose(1, kernel_size=4, strides=1, padding='same', activation='tanh')(x)

        model = Model(inputs=input_, outputs=out, name='Generator')
        model.summary()
        return model

    def build_discriminator(self):
        input_ = Input(shape = (28, 28, 1))
        x = Conv2D(256, kernel_size=4, strides=2, padding='same')(input_)
        x = LeakyReLU(alpha=0.2)(x)
        x = Conv2D(128, kernel_size=4, strides=2, padding='same')(x)
        x = LeakyReLU(alpha=0.2)(x)
        x = Flatten()(x)
        out = Dense(1, activation='sigmoid')(x)

        model = Model(inputs=input_, outputs=out, name='Discriminator')
        dis_optimizer = Adam(learning_rate=self.discriminator_lr, beta_1=0.5)
        model.compile(loss='binary_crossentropy',
                      optimizer=dis_optimizer,
                      metrics=['accuracy'])
        model.summary()
        return model
    def build_adversarialmodel(self):
        noise_input = Input(shape=(100, ))
        generator_sample = self.generator(noise_input)
        self.discriminator.trainable = False
        out = self.discriminator(generator_sample)
        model = Model(inputs=noise_input, outputs=out)

        adv_optimizer = Adam(learning_rate=self.generator_lr, beta_1=0.5)
        model.compile(loss=self.boundary_loss, optimizer=adv_optimizer)
        model.summary()
        return model

    def train(self, epochs, batch_size=128, sample_interval=50):
        # 準備訓練資料
        x_train = self.load_data()
        # 準備訓練的標籤,分為真實標籤與假標籤
        valid = np.ones((batch_size, 1))
        fake = np.zeros((batch_size, 1))
        for epoch in range(epochs):
            # 隨機取一批次的資料用來訓練
            idx = np.random.randint(0, x_train.shape[0], batch_size)
            imgs = x_train[idx]
            # 從常態分佈中採樣一段雜訊
            noise = np.random.normal(0, 1, (batch_size, 100))
            # 生成一批假圖片
            gen_imgs = self.generator.predict(noise)
            # 判別器訓練判斷真假圖片
            d_loss_real = self.discriminator.train_on_batch(imgs, valid)
            d_loss_fake = self.discriminator.train_on_batch(gen_imgs, fake)
            d_loss = 0.5 * np.add(d_loss_real, d_loss_fake)
            #儲存鑑別器損失變化 索引值0為損失 索引值1為準確率
            self.dloss.append(d_loss[0])
            # 訓練生成器的生成能力
            noise = np.random.normal(0, 1, (batch_size, 100))
            g_loss = self.adversarial.train_on_batch(noise, valid)
            # 儲存生成器損失變化
            self.gloss.append(g_loss)
            # 將這一步的訓練資訊print出來
            print(f"Epoch:{epoch} [D loss: {d_loss[0]}, acc: {100 * d_loss[1]:.2f}] [G loss: {g_loss}]")
            # 在指定的訓練次數中,隨機生成圖片,將訓練過程的圖片儲存起來
            if epoch % sample_interval == 0:
                self.sample(epoch)
        self.save_data()
    def save_data(self):
        np.save(file='./result/BGAN/generator_loss.npy',arr=np.array(self.gloss))
        np.save(file='./result/BGAN/discriminator_loss.npy', arr=np.array(self.dloss))
        save_model(model=self.generator,filepath='./result/BGAN/Generator.h5')
        save_model(model=self.discriminator,filepath='./result/BGAN/Discriminator.h5')
        save_model(model=self.adversarial,filepath='./result/BGAN/Adversarial.h5')

    def sample(self, epoch=None, num_images=25, save=True):
        r = int(np.sqrt(num_images))
        noise = np.random.normal(0, 1, (num_images, 100))
        gen_imgs = self.generator.predict(noise)
        gen_imgs = (gen_imgs+1)/2
        fig, axs = plt.subplots(r, r)
        count = 0
        for i in range(r):
            for j in range(r):
                axs[i, j].imshow(gen_imgs[count, :, :, 0], cmap='gray')
                axs[i, j].axis('off')
                count += 1
        if save:
            fig.savefig(f"./result/BGAN/imgs/{epoch}epochs.png")
        else:
            plt.show()
        plt.close()

if __name__ == '__main__':
    gan = BGAN(generator_lr=0.0002,discriminator_lr=0.0002)
    gan.train(epochs=10000, batch_size=128, sample_interval=200)
    gan.sample(save=False)

上一篇
[Day16]:BGAN原理介紹
下一篇
[Day18]:CGAN原理介紹
系列文
生成式AI到底何方神聖?一窺生程式AI的真面目31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言