在上篇部落格(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