在上篇博客(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
進行訓練
然后定義訓練方法, 在訓練的過程中,我們一般來說會將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