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

2022-10-15,,,,

目录
Prepare

在上篇博客(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网络的过程中,我们会这样做:

    真实的图片,真实的标签进行训练 —— 训练判别器对真实图片的判别能力
    G网络产生的图片,虚假的标签进行训练 —— 训练判别器对fake 图片的判别能力

在训练G网路的时候我们会这样做:

    产生噪声,虚假的标签(代码随机生成头发的颜色和眼睛的颜色),然后输入到GAN网络中
    而针对于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

GAN网络之入门教程(五)之基于条件cGAN动漫头像生成的相关教程结束。

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

下载本文的Word格式文档,以方便收藏与打印。