在上篇博客(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
进行训练
然后定义训练方法, 在训练的过程中,我们一般来说会将1
,0
进行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.
参考
GAN — CGAN & InfoGAN (using labels to improve GAN)
A tutorial on Conditional Generative Adversarial Nets + Keras implementation