GAN網路之入門教程(五)之基於條件cGAN動漫頭像生成

段小輝發表於2020-10-10

目錄

在上篇部落格(AN網路之入門教程(四)之基於DCGAN動漫頭像生成)中,介紹了基於DCGAN的動漫頭像生成,時隔幾月,序屬三秋,在這篇部落格中,將介紹如何使用條件GAN網路(conditional GAN)生成符合需求的圖片。

做成的效果圖如下所示,“一鍵起飛”

專案地址:Github

在閱讀這篇部落格之前,首先得先對GAN和DCGAN有一部分的瞭解,如果對GAN不是很瞭解的話,建議先去了解GAN網路,或者也可以參考一下我之前的部落格系列

相比較於普通的GAN網路,cgan在網路結構上發生了一些改變,與GAN網路相比,在Input layer新增了一個\(Y\)的標籤,其代表圖片的屬性標籤——在Minst資料集中,標籤即代表著手寫數字為幾(如7,3),而在動漫頭像資料集中,標籤可以表示為頭髮的顏色,或者眼睛的顏色(當然為其他的屬性特徵也是?的)。

\(G\)網路中,Generator可以根據給的\(z\) (latent noise)和 \(y\) 生成相對應的圖片,而\(D\)網路可以根據給的\(x\)(比如說圖片)和 \(Y\) 進行評判。下圖便是一個CGAN網路的簡單示意圖。

在這篇部落格中,使用的框架:

  • Keras version:2.3.1

Prepare

首先的首先,我們需要資料集,裡面既需要包括動漫頭像的圖片,也需要有每一張圖片所對應的標籤資料。這裡我們使用Anime-Face-ACGAN中提供的圖片資料集和標籤資料集,當然,在我的Github中也提供了資料集的下載(其中,我的資料集對圖片進行了清洗,將沒有相對應標籤的圖片進行了刪除)。

部分圖片資料如下所示:

在tags_clean.csv 中,資料形式如下圖所示,每一行代表的是相對應圖片的標籤資料。第一個資料為ID,同時也是圖片的檔名字,後面的資料即為圖片的特徵資料

這裡我們需要標籤屬性的僅僅為eyes的顏色資料和hair的顏色資料,應注意的是在csv中存在某一些圖片沒有這些資料(如第0個資料)。

以上便將這次所需要的資料集介紹完了,下面將簡單的介紹一下資料集的載入。

載入資料集

首先我們先進行載入資料集,一共需要載入兩個資料集,一個是圖片資料集合,一個是標籤資料集合。在標籤資料集中,我們需要的是眼睛的顏色頭髮的顏色。在資料集中,一共分別有12種頭髮的顏色和11種眼睛的顏色。

# 頭髮的種類
HAIRS = ['orange hair', 'white hair', 'aqua hair', 'gray hair', 'green hair', 'red hair', 'purple hair', 'pink hair','blue hair', 'black hair', 'brown hair', 'blonde hair']
# 眼睛的種類
EYES = ['gray eyes', 'black eyes', 'orange eyes', 'pink eyes', 'yellow eyes', 'aqua eyes', 'purple eyes', 'green eyes','brown eyes', 'red eyes', 'blue eyes']

接下來載入資料集,在這個操作中,我們提取出csv中的hair和eye的顏色並得到相對應的id,然後將其儲存到numpy陣列中。

# 載入標籤資料
import numpy as np
import csv
with open('tags_clean.csv', 'r') as file:
    lines = csv.reader(file, delimiter=',')
    y_hairs = []
    y_eyes = []
    y_index = []
    for i, line in enumerate(lines):
        # id 對應的是圖片的名字
        idx = line[0]
        # tags 代表圖片的所有特徵(有hair,eyes,doll等等,當時我們只關注eye 和 hari)
        tags = line[1]
        tags = tags.split('\t')[:-1]
        y_hair = []
        y_eye = []
        for tag in tags:
            tag = tag[:tag.index(':')]
            if (tag in HAIRS):
                y_hair.append(HAIRS.index(tag))
            if (tag in EYES):
                y_eye.append(EYES.index(tag))
        # 如果同時存在hair 和 eye標籤就代表這個標籤是有用標籤。
        if (len(y_hair) == 1 and len(y_eye) == 1):
            y_hairs.append(y_hair)
            y_eyes.append(y_eye)
            y_index.append(idx)
    y_eyes = np.array(y_eyes)
    y_hairs = np.array(y_hairs)
    y_index = np.array(y_index)
    print("一種有{0}個有用的標籤".format(len(y_index)))

通過上述的操作,我們就提取出了在csv檔案中同時存在eye顏色hair顏色標籤的資料了。並儲存了所對應圖片的id資料

接下來我們就是根據id資料去讀取出相對應的圖片了,其中,所有的圖片均為(64,64,3)的RGB圖片,並且圖片的儲存位置為/faces

import os
import cv2
# 建立資料集images_data
images_data = np.zeros((len(y_index), 64, 64, 3))
# 從本地檔案讀取圖片載入到images_data中。
for index,file_index in enumerate (y_index):
    images_data[index] = cv2.cvtColor(
        cv2.resize(
            cv2.imread(os.path.join("faces", str(file_index) + '.jpg'), cv2.IMREAD_COLOR),
            (64, 64)),cv2.COLOR_BGR2RGB
            )

接下來將圖片進行歸一化(一般來說都需要將圖片進行歸一化提高收斂的速度):

images_data = (images_data / 127.5) - 1

通過以上的操作,我們就將資料匯入記憶體中了,因為這個資料集比較小,因此將其全部匯入到記憶體中是完全?的。

構建網路

first of all,我們將我們需要的庫匯入:

from keras.layers import Input, Dense, Reshape, Flatten, Dropout, multiply, Activation
from keras.layers import BatchNormalization, Activation, Embedding, ZeroPadding2D
from keras.layers import Conv2D, Conv2DTranspose, Dropout, UpSampling2D, MaxPooling2D,Concatenate
from keras.layers.advanced_activations import LeakyReLU
from keras.models import Sequential, Model, load_model
from keras.optimizers import SGD, Adam, RMSprop
from keras.utils import to_categorical,plot_model
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec

構建Generator

關於G網路的模型圖如下所示,而程式碼便是按照如下的模型圖來構建網路模型:

  • Input:頭髮的顏色,眼睛的顏色,100維的高斯噪聲。
  • Output:(64,64,3)的RGB圖片。

構建模型圖的程式碼:


def build_generator_model(noise_dim, hair_num_class, eye_num_class):
    """
    定義generator的生成方法
    :param noise_dim: 噪聲的維度
    :param hair_num_class: hair標籤的種類個數
    :param eye_num_class: eye標籤的種類個數
    :return: generator
    """
    # kernel初始化模式
    kernel_init = 'glorot_uniform'

    model = Sequential(name='generator')

    model.add(Reshape((1, 1, -1), input_shape=(noise_dim + 16,)))
    model.add(Conv2DTranspose(filters=512, kernel_size=(4, 4), strides=(1, 1), padding="valid",
                              data_format="channels_last", kernel_initializer=kernel_init, ))
    model.add(BatchNormalization(momentum=0.5))
    model.add(LeakyReLU(0.2))
    model.add(Conv2DTranspose(filters=256, kernel_size=(4, 4), strides=(2, 2), padding="same",
                              data_format="channels_last", kernel_initializer=kernel_init))
    model.add(BatchNormalization(momentum=0.5))
    model.add(LeakyReLU(0.2))
    model.add(Conv2DTranspose(filters=128, kernel_size=(4, 4), strides=(2, 2), padding="same",
                              data_format="channels_last", kernel_initializer=kernel_init))
    model.add(BatchNormalization(momentum=0.5))
    model.add(LeakyReLU(0.2))
    model.add(Conv2DTranspose(filters=64, kernel_size=(4, 4), strides=(2, 2), padding="same",
                              data_format="channels_last", kernel_initializer=kernel_init))
    model.add(BatchNormalization(momentum=0.5))
    model.add(LeakyReLU(0.2))
    model.add(Conv2D(filters=64, kernel_size=(3, 3), strides=(1, 1), padding="same", data_format="channels_last",
                     kernel_initializer=kernel_init))
    model.add(BatchNormalization(momentum=0.5))
    model.add(LeakyReLU(0.2))
    model.add(Conv2DTranspose(filters=3, kernel_size=(4, 4), strides=(2, 2), padding="same",
                              data_format="channels_last", kernel_initializer=kernel_init))
    model.add(Activation('tanh'))

    latent = Input(shape=(noise_dim,))
    eyes_class = Input(shape=(1,), dtype='int32')
    hairs_class = Input(shape=(1,), dtype='int32')

    hairs = Flatten()(Embedding(hair_num_class, 8, init='glorot_normal')(hairs_class))
    eyes = Flatten()(Embedding(eye_num_class, 8, init='glorot_normal')(eyes_class))
    # 連線模型的輸入
    con = Concatenate()([latent, hairs, eyes])
    # 模型的輸出
    fake_image = model(con)
    # 建立模型
    m = Model(input=[latent, hairs_class, eyes_class], output=fake_image)
    return m

構建G網路:

# 生成網路
G = build_generator_model(100,len(HAIRS),len(EYES))
# 呼叫這個方法可以畫出模型圖
# plot_model(G, to_file='generator.png', show_shapes=True, expand_nested=True, dpi=500)

構建Discriminator

這裡我們的discriminator的網路結構上文中的cgan網路結構稍有不同。在前文中,我們是在Discriminator的輸入端的輸入是圖片標籤,而在這裡,我們的Discriminator的輸入僅僅是圖片,輸出才是label 和 真假概率。

網路結構如下所示:

然後根據上述的網路結構來構建discriminator,程式碼如下:

def build_discriminator_model(hair_num_class, eye_num_class):
    """
    定義生成 discriminator 的方法
    :param hair_num_class: 頭髮顏色的種類
    :param eye_num_class: 眼睛顏色的種類
    :return: discriminator
    """
    kernel_init = 'glorot_uniform'
    discriminator_model = Sequential(name="discriminator_model")
    discriminator_model.add(Conv2D(filters=64, kernel_size=(4, 4), strides=(2, 2), padding="same",
                                   data_format="channels_last", kernel_initializer=kernel_init,
                                   input_shape=(64, 64, 3)))
    discriminator_model.add(LeakyReLU(0.2))
    discriminator_model.add(Conv2D(filters=128, kernel_size=(4, 4), strides=(2, 2), padding="same",
                                   data_format="channels_last", kernel_initializer=kernel_init))
    discriminator_model.add(BatchNormalization(momentum=0.5))
    discriminator_model.add(LeakyReLU(0.2))
    discriminator_model.add(Conv2D(filters=256, kernel_size=(4, 4), strides=(2, 2), padding="same",
                                   data_format="channels_last", kernel_initializer=kernel_init))
    discriminator_model.add(BatchNormalization(momentum=0.5))
    discriminator_model.add(LeakyReLU(0.2))
    discriminator_model.add(Conv2D(filters=512, kernel_size=(4, 4), strides=(2, 2), padding="same",
                                   data_format="channels_last", kernel_initializer=kernel_init))
    discriminator_model.add(BatchNormalization(momentum=0.5))
    discriminator_model.add(LeakyReLU(0.2))
    discriminator_model.add(Flatten())
    # 網路的輸入
    dis_input = Input(shape=(64, 64, 3))

    features = discriminator_model(dis_input)
    # 真/假概率的輸出
    validity = Dense(1, activation="sigmoid")(features)
    # 頭髮顏色種類的輸出
    label_hair = Dense(hair_num_class, activation="softmax")(features)
    # 眼睛顏色種類的輸出
    label_eyes = Dense(eye_num_class, activation="softmax")(features)
    m = Model(dis_input, [validity, label_hair, label_eyes])
    return m

然後呼叫方法建立discriminator。

D = build_discriminator_model(len(HAIRS),len(EYES))
# 畫出模型圖
# plot_model(D, to_file='discriminator.png', show_shapes=True, expand_nested=True, dpi=500)

構建cGAN網路

cgan網路的輸入是generator的輸入,cgan的輸出是discriminator的輸出,網路模型圖如下所示:

模型圖看起來很複雜,但是實際上程式碼卻很簡單,針對於GAN網路,我們只需要將GAN網路中的D網路進行凍結(將trainable變成False)即可。

def build_ACGAN(gen_lr=0.00015, dis_lr=0.0002, noise_size=100):
    """
    生成
    :param gen_lr: generator的學習率
    :param dis_lr: discriminator的學習率
    :param noise_size: 噪聲維度size
    :return:
    """
    # D網路優化器
    dis_opt = Adam(lr=dis_lr, beta_1=0.5)
    # D網路loss
    losses = ['binary_crossentropy', 'categorical_crossentropy', 'categorical_crossentropy']
    # 配置D網路
    D.compile(loss=losses, loss_weights=[1.4, 0.8, 0.8], optimizer=dis_opt, metrics=['accuracy'])

    # 在訓練的generator時,凍結discriminator的權重
    D.trainable = False

    opt = Adam(lr=gen_lr, beta_1=0.5)
    gen_inp = Input(shape=(noise_size,))
    hairs_inp = Input(shape=(1,), dtype='int32')
    eyes_inp = Input(shape=(1,), dtype='int32')
    GAN_inp = G([gen_inp, hairs_inp, eyes_inp])
    GAN_opt = D(GAN_inp)
    gan = Model(input=[gen_inp, hairs_inp, eyes_inp], output=GAN_opt)
    gan.compile(loss=losses, optimizer=opt, metrics=['accuracy'])
    return gan

然後呼叫方法構建GAN網路即可:

gan = build_ACGAN()
# plot_model(gan, to_file='gan.png', show_shapes=True, expand_nested=True, dpi=500)

工具方法

然後我們定義一些方法,有:

  • 產生噪聲:gen_noise
  • G網路產生圖片,並將生成的圖片進行儲存
  • 從資料集中隨機獲取動漫頭像和標籤資料

關於這些程式碼具體的說明,可以看一下注釋。

def gen_noise(batch_size, noise_size=100):
    """
    生成高斯噪聲
    :param batch_size: 生成噪聲的數量
    :param noise_size: 噪聲的維度
    :return: (batch_size,noise)的高斯噪聲
    """
    return np.random.normal(0, 1, size=(batch_size, noise_size))


def generate_images(generator,img_path):
    """
    G網路生成圖片
    :param generator: 生成器
    :return: (64,64,3)維度 16張圖片
    """
    noise = gen_noise(16, 100)
    hairs = np.zeros(16)
    eyes = np.zeros(16)

    # 指令生成頭髮,和眼睛的顏色
    for h in range(len(HAIRS)):
        hairs[h] = h

    for e in range(len(EYES)):
        eyes[e] = e
    # 生成圖片
    fake_data_X = generator.predict([noise, hairs, eyes])
    plt.figure(figsize=(4, 4))
    gs1 = gridspec.GridSpec(4, 4)
    gs1.update(wspace=0, hspace=0)
    for i in range(16):
        ax1 = plt.subplot(gs1[i])
        ax1.set_aspect('equal')
        image = fake_data_X[i, :, :, :]
        fig = plt.imshow(image)
        plt.axis('off')
        fig.axes.get_xaxis().set_visible(False)
        fig.axes.get_yaxis().set_visible(False)
    plt.tight_layout()
    # 儲存圖片
    plt.savefig(img_path, bbox_inches='tight', pad_inches=0)

def sample_from_dataset(batch_size, images, hair_tags, eye_tags):
    """
    從資料集中隨機獲取圖片
    :param batch_size: 批處理大小
    :param images: 資料集
    :param hair_tags: 頭髮顏色標籤資料集
    :param eye_tags: 眼睛顏色標籤資料集
    :return:
    """
    choice_indices = np.random.choice(len(images), batch_size)
    sample = images[choice_indices]
    y_hair_label = hair_tags[choice_indices]
    y_eyes_label = eye_tags[choice_indices]
    return sample, y_hair_label, y_eyes_label

進行訓練

然後定義訓練方法, 在訓練的過程中,我們一般來說會將10進行smooth,讓它們在一定的範圍內波動。同時我們在訓練D網路的過程中,我們會這樣做:

  1. 真實的圖片,真實的標籤進行訓練 —— 訓練判別器對真實圖片的判別能力
  2. G網路產生的圖片,虛假的標籤進行訓練 —— 訓練判別器對fake 圖片的判別能力

在訓練G網路的時候我們會這樣做:

  1. 產生噪聲,虛假的標籤(程式碼隨機生成頭髮的顏色和眼睛的顏色),然後輸入到GAN網路中
  2. 而針對於GAN網路的輸出,我們將其定義為[1(認為其為真實圖片)],[輸入端的標籤]。GAN網路的輸出認為是1(實際上是虛假的圖片),這樣就能夠產生一個loss,從而通過反向傳播來更新G網路的權值(在這一個步驟中,D網路的權值並不會進行更新。)
def train(epochs, batch_size, noise_size, hair_num_class, eye_num_class):
    """
    進行訓練
    :param epochs: 訓練的步數
    :param batch_size: 訓練的批處理大小
    :param noise_size: 噪聲維度大小
    :param hair_num_class: 頭髮顏色種類
    :param eye_num_class: 眼睛顏色種類
    :return:
    """
    for step in range(0, epochs):

        # 每隔100輪儲存資料
        if (step % 100) == 0:
            step_num = str(step).zfill(6)
            generate_images(G, os.path.join("./generate_img", step_num + "_img.png"))

        # 隨機產生資料並進行編碼
        sampled_label_hairs = np.random.randint(0, hair_num_class, batch_size).reshape(-1, 1)
        sampled_label_eyes = np.random.randint(0, eye_num_class, batch_size).reshape(-1, 1)
        sampled_label_hairs_cat = to_categorical(sampled_label_hairs, num_classes=hair_num_class)
        sampled_label_eyes_cat = to_categorical(sampled_label_eyes, num_classes=eye_num_class)
        noise = gen_noise(batch_size, noise_size)
        # G網路生成圖片
        fake_data_X = G.predict([noise, sampled_label_hairs, sampled_label_eyes])

        # 隨機獲得真實資料並進行編碼
        real_data_X, real_label_hairs, real_label_eyes = sample_from_dataset(
            batch_size, images_data, y_hairs, y_eyes)
        real_label_hairs_cat = to_categorical(real_label_hairs, num_classes=hair_num_class)
        real_label_eyes_cat = to_categorical(real_label_eyes, num_classes=eye_num_class)

        # 產生0,1標籤並進行smooth
        real_data_Y = np.ones(batch_size) - np.random.random_sample(batch_size) * 0.2
        fake_data_Y = np.random.random_sample(batch_size) * 0.2

        # 訓練D網路
        dis_metrics_real = D.train_on_batch(real_data_X, [real_data_Y, real_label_hairs_cat,
                                                          real_label_eyes_cat])
        dis_metrics_fake = D.train_on_batch(fake_data_X, [fake_data_Y, sampled_label_hairs_cat,
                                                          sampled_label_eyes_cat])


        noise = gen_noise(batch_size, noise_size)
        # 產生隨機的hair 和 eyes標籤
        sampled_label_hairs = np.random.randint(0, hair_num_class, batch_size).reshape(-1, 1)
        sampled_label_eyes = np.random.randint(0, eye_num_class, batch_size).reshape(-1, 1)

        # 將標籤變成(,12)或者(,11)型別的
        sampled_label_hairs_cat = to_categorical(sampled_label_hairs, num_classes=hair_num_class)
        sampled_label_eyes_cat = to_categorical(sampled_label_eyes, num_classes=eye_num_class)

        real_data_Y = np.ones(batch_size) - np.random.random_sample(batch_size) * 0.2
        # GAN網路的輸入
        GAN_X = [noise, sampled_label_hairs, sampled_label_eyes]
        # GAN網路的輸出
        GAN_Y = [real_data_Y, sampled_label_hairs_cat, sampled_label_eyes_cat]
        # 對GAN網路進行訓練
        gan_metrics = gan.train_on_batch(GAN_X, GAN_Y)

        # 儲存生成器
        if step % 100 == 0:
            print("Step: ", step)
            print("Discriminator: real/fake loss %f, %f" % (dis_metrics_real[0], dis_metrics_fake[0]))
            print("GAN loss: %f" % (gan_metrics[0]))
            G.save(os.path.join('./model', str(step) + "_GENERATOR.hdf5"))

一般來說,訓練1w輪就可以得到一個比較好的結果了(部落格的開頭的那兩張圖片就是訓練1w輪的模型生成的),不過值得注意的是,在訓練輪數過多的情況下產生了過擬合(產生的圖片逐漸一毛一樣)。

train(1000000,64,100,len(HAIRS),len(EYES))

視覺化介面

視覺化介面的程式碼如下所示,也是我從Anime-Face-ACGAN裡面copy的,沒什麼好說的,就是直接使用tk框架搭建了一個介面,一個按鈕。

import tkinter as tk
from tkinter import ttk

import imageio
import numpy as np
from PIL import Image, ImageTk
from keras.models import load_model

num_class_hairs = 12
num_class_eyes = 11
def load_model():
    # 這裡使用的是1w輪的訓練模型
    g = load_model(str(10000) + '_GENERATOR.hdf5')
    return g
# 載入模型
G = load_model()
# 建立窗體
win = tk.Tk()
win.title('視覺化GUI')
win.geometry('400x200')

def gen_noise(batch_size, latent_size):
    return np.random.normal(0, 1, size=(batch_size, latent_size))

def generate_images(generator, latent_size, hair_color, eyes_color):
    noise = gen_noise(1, latent_size)
    return generator.predict([noise, hair_color, eyes_color])

def create():
    hair_color = np.array(comboxlist1.current()).reshape(1, 1)
    eye_color = np.array(comboxlist2.current()).reshape(1, 1)

    image = generate_images(G, 100, hair_color, eye_color)[0]
    imageio.imwrite('anime.png', image)
    img_open = Image.open('anime.png')
    img = ImageTk.PhotoImage(img_open)
    label.configure(image=img)
    label.image = img


comvalue1 = tk.StringVar()  # 窗體自帶的文字,新建一個值
comboxlist1 = ttk.Combobox(win, textvariable=comvalue1)
comboxlist1["values"] = (
    'orange hair', 'white hair', 'aqua hair', 'gray hair', 'green hair', 'red hair', 'purple hair', 'pink hair',
    'blue hair', 'black hair', 'brown hair', 'blonde hair')
# 預設選擇第一個
comboxlist1.current(0)
comboxlist1.pack()

comvalue2 = tk.StringVar()
comboxlist2 = ttk.Combobox(win, textvariable=comvalue2)
comboxlist2["values"] = (
    'gray eyes', 'black eyes', 'orange eyes', 'pink eyes', 'yellow eyes', 'aqua eyes', 'purple eyes', 'green eyes',
    'brown eyes', 'red eyes', 'blue eyes')
# 預設選擇第一個
comboxlist2.current(0)
comboxlist2.pack()

bm = tk.PhotoImage(file='anime.png')
label = tk.Label(win, image=bm)
label.pack()

b = tk.Button(win,
              text='一鍵起飛',  # 顯示在按鈕上的文字
              width=15, height=2,
              command=create)  # 點選按鈕式執行的命令
b.pack()
win.mainloop()

介面如下所示

總結

cgan網相比較dcgan而言,差別不是很大,只不過是加了一個標籤label而已。不過該篇部落格的程式碼還是大量的借鑑了Anime-Face-ACGAN的程式碼,因為我也是一個新手,Just Study Together.

參考

Anime-Face-ACGAN

GAN — CGAN & InfoGAN (using labels to improve GAN)

A tutorial on Conditional Generative Adversarial Nets + Keras implementation

How to Develop a Conditional GAN (cGAN) From Scratch


相關文章