目录

在上篇博客(
AN网络之入门教程(四)之基于DCGAN动漫头像生成)中,介绍了基于DCGAN的动漫头像生成,时隔几月,序属三秋,在这篇博客中,将介绍如何使用条件GAN网络(conditional GAN)生成符合需求的图片。

做成的效果图如下所示,“一键起飞”

1439869 20201010115523645 509837982 - GAN网络之入门教程(五)之基于条件cGAN动漫头像生成

项目地址:Github

在阅读这篇博客之前,首先得先对GAN和DCGAN有一部分的了解,如果对GAN不是很了解的话,建议先去了解GAN网络,或者也可以参考一下我之前的博客系列

相比较于普通的GAN网络,cgan在网络结构上发生了一些改变,与GAN网络相比,在Input layer添加了一个\(Y\)的标签,其代表图片的属性标签——在Minst数据集中,标签即代表着手写数字为几(如7,3),而在动漫头像数据集中,标签可以表示为头发的颜色,或者眼睛的颜色(当然为其他的属性特征也是的)。

\(G\)网络中,Generator可以根据给的\(z\) (latent noise)和 \(y\) 生成相对应的图片,而\(D\)网络可以根据给的\(x\)(比如说图片)和 \(Y\) 进行评判。下图便是一个CGAN网络的简单示意图。

1439869 20201010115523903 1137910777 - GAN网络之入门教程(五)之基于条件cGAN动漫头像生成

在这篇博客中,使用的框架:

  • Keras version:2.3.1

Prepare

首先的首先,我们需要数据集,里面既需要包括动漫头像的图片,也需要有每一张图片所对应的标签数据。这里我们使用Anime-Face-ACGAN中提供的图片数据集和标签数据集,当然,在我的Github中也提供了数据集的下载(其中,我的数据集对图片进行了清洗,将没有相对应标签的图片进行了删除)。

部分图片数据如下所示:

1439869 20201010115524210 1235203034 - GAN网络之入门教程(五)之基于条件cGAN动漫头像生成

在tags_clean.csv 中,数据形式如下图所示,每一行代表的是相对应图片的标签数据。第一个数据为ID,同时也是图片的文件名字,后面的数据即为图片的特征数据

这里我们需要标签属性的仅仅为eyes的颜色数据和hair的颜色数据,应注意的是在csv中存在某一些图片没有这些数据(如第0个数据)。

1439869 20201010115524582 1363721792 - GAN网络之入门教程(五)之基于条件cGAN动漫头像生成

以上便将这次所需要的数据集介绍完了,下面将简单的介绍一下数据集的加载。

加载数据集

首先我们先进行加载数据集,一共需要加载两个数据集,一个是图片数据集合,一个是标签数据集合。在标签数据集中,我们需要的是眼睛的颜色头发的颜色。在数据集中,一共分别有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图片。

1439869 20201010115525124 282720412 - GAN网络之入门教程(五)之基于条件cGAN动漫头像生成

构建模型图的代码:


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 和 真假概率。

1439869 20201010115525958 1831451306 - GAN网络之入门教程(五)之基于条件cGAN动漫头像生成

网络结构如下所示:

1439869 20201010115526309 526366383 - GAN网络之入门教程(五)之基于条件cGAN动漫头像生成

然后根据上述的网络结构来构建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的输出,网络模型图如下所示:

1439869 20201010115527021 1281023934 - GAN网络之入门教程(五)之基于条件cGAN动漫头像生成

模型图看起来很复杂,但是实际上代码却很简单,针对于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()

界面如下所示

1439869 20201010115528113 1709964283 - GAN网络之入门教程(五)之基于条件cGAN动漫头像生成

总结

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


GAN网络之入门教程(五)之基于条件cGAN动漫头像生成

GAN网络之入门教程(五)之基于条件cGAN动漫头像生成插图

发表评论