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


目錄

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

這篇博客有一個錯誤,在接下來的文章中構建的網絡是ACGAN網絡,並不是cgan網絡。感謝Shinjii指出這個錯誤。

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

項目地址: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



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM