【AI模型訓練】真假分不清!訓練假臉產生器

作者:曾成訓(CH.Tseng)

如果想虛擬出人類的五官,用大頭照是最適合的,這邊使用的訓練樣本是來自 Flickr 的資料庫—Flickr Faces HQ Dataset(FFHQ)。

  • 圖片數目:70,000 張
  • 圖片種類:PNG 圖檔
  • 圖片尺寸:共三種(裁切臉部的高品質 HQ、低解像 Thumbnail、in-the-wild 相片原圖各 70,000 張)
  • HQ:1024×1024 Thumbnail:128×128
  • 圖片特性:每一張圖片皆使用 Dlib 自動裁切臉部及校正對齊

這次所使用的程式,參考自周凡剛老師所開的課程並作了些許修改(如果您對 GAN 有興趣,相當推薦周老師的這個課程唷!)

Generator模型

主要修改基本的 MNIST DCGAN 架構,讓 Generator 能產生 56×56 的彩色 RGB 圖片。由於每次採樣會增加二倍尺寸,因此一開始的 7×7 需要三次的 up sampling 才會達到所需的 56×56(7x7x128 🡪 14x14x128 🡪 28x28x128 🡪 56×56×128 🡪 56x56x3),因此使用迴圈的方式會比較簡潔;另外,這邊使用的 128 是參考 VGG 的作法,這個數值可以更改。

def generator_model(dim, base_gen, rand_dim):
    gen = Sequential()
    gen.add(Dense(dim*dim*base_gen, input_dim=rand_dim, activation='relu'))
    gen.add(Reshape((dim, dim, base_gen))) # 轉換成三維

    for i in range(0, int(base_gen/28 + 1)):
        # 上採樣, 長寬會變兩倍
        gen.add(UpSampling2D(size=(2, 2)))
        # (4, 4)卷積窗的卷積, DCGAN模型的標準用法。
        gen.add(Conv2D(base_gen, kernel_size=(4, 4), activation='relu', padding='same'))
        # 卷積層間使用BN來normalize,也是DCGAM的標準用法。
        gen.add(BatchNormalization())

    #因為是RGB圖片, 因此讓filter為3, 輸出channel為3的圖片
    #一樣使用tanh作為激活
    gen.add(Conv2D(3, kernel_size=(4, 4), activation='tanh', padding='same'))

    return gen

Discriminator模型

基本上 Discriminator 方面,只要將輸入的部份修改成與 Generator 輸出的尺寸相同(56x56x3)即可。

def discriminator_model(in_shape, base_gen):
    discriminator = Sequential()
    #步長2卷積,這是 DCGAN的標準用法。
    discriminator.add(Conv2D(int(base_gen/8), kernel_size=4,
                         strides=2,
                         input_shape=in_shape,
                         padding="same",
                         activation='relu'))
    #DCGAN都會在兩層卷積中加入BN
    discriminator.add(BatchNormalization())
    discriminator.add(Conv2D(int(base_gen/4), kernel_size=4,
                         strides=2,
                         padding="same",
                         activation='relu'))
    discriminator.add(BatchNormalization())
    discriminator.add(Conv2D(int(base_gen/2), kernel_size=4,
                         strides=2,
                         padding="same",
                         activation='relu'))
    discriminator.add(BatchNormalization())
    discriminator.add(Conv2D(base_gen, kernel_size=4,
                         strides=2,
                         padding="same",
                         activation='relu'))
    discriminator.add(BatchNormalization())

    #全連接層(MLP)
    discriminator.add(Flatten())
    discriminator.add(Dense(base_gen*2, activation='relu'))
    discriminator.add(Dropout(0.25))
    discriminator.add(Dense(1, activation='sigmoid'))

    return discriminator

DCGAN的訓練

在進行 GAN 訓練時,雖然 Discriminator 與 Generator 都包含在 GAN 模型中,但是由於 Discriminator 的 trainable 設定為 False,這表示在訓練 GAN 時,只有 Generator 被訓練到,此時 GAN 輸出的 loss 視為 Generator 的 loss;而 Discriminator 自己單獨進行訓練,它的 loss 是由兩種 loss 加總:判斷 Generator 產生的虛擬圖片、判斷 Dataset 中真實的圖片。

loss_file = os.path.join(output_folder, "loss_" + str(epoch_count) + "epochs.csv")
f = open(loss_file, 'w')
f.write("epoch,d_loss,g_loss \n")
f.close()
for epoch in range(0, epoch_count):
    for batch_count in range(0, 100):
        idx = np.random.randint(0, x_train.shape[0], batch_size)
        imgs = x_train_shaped[idx]

        valid = np.ones((batch_size, 1))
        fake = np.zeros((batch_size, 1))
        #Generator製造出虛擬圖片,這些圖片由Discriminator來判斷是否為False
        noise = np.random.normal(0, 1, (batch_size, random_dim))
        gen_imgs = generator.predict(noise)

        discriminator.trainable = True
        #由Discriminator判斷由Dataset來的圖片是否為True。
        d_loss_real = discriminator.train_on_batch(imgs, valid)
        #由Discriminator判斷由Generator產生的虛擬圖片。
        d_loss_fake = discriminator.train_on_batch(gen_imgs, fake)
        #兩者加總即為Discriminator的Loss
        d_loss = (d_loss_real + d_loss_fake) / 2

        discriminator.trainable = False
        noise = np.random.normal(0, 1, (batch_size, random_dim))
        #由於GAN模型中,Discriminator設為不可訓練,因此訓練GAN模型,可視為僅訓練Generator。
        g_loss = gan.train_on_batch(noise, valid)

訓練時的資訊輸出

因為想要看看 Generator 在迭代訓練時,每批次訓練出來的臉孔圖片有否進步,同時也希望將模型的 loss值輸出以利統計,因此加入了下方的程式,這可以讓其在指定間隔的 epoch 次數達到時,輸出 Generator 的預測圖片、loss 值以及匯出模型的權重。

 out_path = output_folder +'/' + str(epoch+1) + '/'

    if (epoch + 1) % epoch_interval_output_log == 0:
        f = open(loss_file, 'a')
        msg_line = "{} epoch Discriminator loss:{} Generator loss:{}".format(epoch + 1, d_loss, g_loss)
        print(msg_line)
        f.write( "{},{},{}\n".format(epoch+1,d_loss,g_loss))
        f.close()

    if (epoch + 1) % epoch_interval_output_model == 0:
        if( not os.path.isdir(out_path)):
            os.mkdir(out_path)
        print("Output models.")
        output_model(out_path, output_images_num, random_dim, epoch+1, generator, discriminator, gan)

    if (epoch + 1) % epoch_interval_output_img == 0:
        if( not os.path.isdir(out_path)):
            os.mkdir(out_path)
        print("Output images.")
        output_img(out_path, output_images_num, random_dim, epoch+1, generator, discriminator, gan)

訓練結果

將 Generator 和 Discriminator 的 loss 輸出後,會看到它的訓練曲線圖如下,看起來並沒有明顯的下降趨勢,這是由於其 loss 是雙方相互拉距拔河的結果,Generator 強時 Discriminator 弱,反之亦同,因此就沒有一般深度學習模型訓練 loss 愈來愈低的情況。

(圖片來源:曾成訓提供)

另外值得一提的是,起初在訓練時,Generator 處於很吃虧的情況,因為要由無生有地產生以假亂真的圖片,一定不知道要如何產生,因此它產生的圖片很容易就會被 Discriminator 判定為假,造成 Generator  不知道要如何產生真的圖片而被迫提早放棄。

幸好本範例使用 56×56 的圖片還能訓練出來,但如果擴大一倍到 112×112,對 Generator 來說可能就很難訓練了。

下面是 Generator 各訓練階段的虛擬能力:

(圖片來源:曾成訓提供)

(圖片來源:曾成訓提供)

(圖片來源:曾成訓提供)

從上方的迭代訓練結果可以看出,一開始 Generator 是從大體結構開始學習,60 epochs 有了五官,100 epochs 開始有了臉部輪廓,200 epochs 後逐漸著重在細節的部份,訓練到最後的 5000 epochs,Generator 產生的人像已經相當接近實際的了。

這是最後第 5000 epochs 的訓練結果。你想像得出來這些臉孔都是虛擬出來的嗎?是不是有一些人讓你有似曾相似的熟悉感?

(圖片來源:曾成訓提供)

DCGAN 模型的利用

DCGAN 是一種非監督式學習,因為它不需要事先標記好的圖片便能訓練,那麼這個能夠產生無限多陌生臉孔的 GAN 模型能做什麼呢?目前我想到的是:

  1. 用於社交網站上,讓使用者產生代表自己的虛擬頭像
  2. 用在需要人臉臉孔的廣告或宣傳用途(擔心侵犯人身權但又不想花錢找真正的模特兒)
  3. 用於訓練標記點模型或臉孔偵測模型(使用 GAN 虛擬出大量的人臉圖案,再使用自動工具來標記人臉五官,達到完全不需搜集人臉相片便可獨立的訓練人臉模型)

DCGAN架構總結

這篇論文提出標準的 DCGAN 架構,能夠穩定訓練出品質相當好的虛擬圖片。

DCGAN 架構(圖片來源:曾成訓提供)

論文中所展示的 Generator 架構,開頭輸入的是 100 dim 常態分配的亂數,輸出為 64×64 的 RGB 圖片,若搭配程式仔細看看 Generator 和 Discriminator 的模型架構,會發現:

  1. Discriminator 不使用池化層(pooling),而使用步長(strides)為 2 來取代 pooling
  2. Generator 主要使用 upsampling(稱上採樣或反捲積)來擴大圖片
  3. 每層之間皆使用 batch normalization
  4. 取消隱藏層(全連結層)
  5. Kernel(稱捲積核)皆使用 4×4
  6. Generator 的 activation 除了輸出層使用 Tanh,其餘皆使用 ReLU
  7. Discriminator 的 activation 使用 LeakyReLU 或 ReLU

(本文經作者同意轉載自 CH.TSENG 部落格原文連結;責任編輯:賴佩萱)

曾 成訓

Author: 曾 成訓

人到中年就像沒對準的描圖紙,一點一點的錯開,我只能當個Maker來使它復位。

Share This Post On

Submit a Comment

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *