教你如何使用GAN为口袋妖怪上色
阿丽66
发表于 2023-1-14 11:50:51
231
0
0
在之前的Demo中,我们使用了条件GAN来生成了手写数字图像。那么除了生成数字图像以外我们还能用神经网络来干些什么呢?. q; I: O, H/ K; Y
在本案例中,我们用神经网络来给口袋妖怪的线框图上色。/ d: c7 V5 o/ Y( D3 j4 y2 I1 G; ^
第一步: 导入使用库
from __future__ import absolute_import, division, print_function, unicode_literals2 u3 k) f3 @8 ^4 R2 V+ P! b
import tensorflow as tf
tf.enable_eager_execution()5 A2 m/ x# N0 B, K2 T& d0 i6 r* u
import numpy as np# P2 X/ N& H: S& d4 Q! u
import pandas as pd
import os+ U/ I6 i2 q) R0 u; t
import time
import matplotlib.pyplot as plt
from IPython.display import clear_output9 B5 S4 N4 A' F: H
口袋妖怪上色的模型训练过程中,需要比较大的显存。为了保证我们的模型能在2070上顺利的运行,我们限制了显存的使用量为90%, 来避免显存不足的引起的错误。! f7 G% P& a; T
config = tf.compat.v1.ConfigProto()8 V5 M( ^( o% W' F2 N
config.gpu_options.per_process_gpu_memory_fraction = 0.9
session = tf.compat.v1.Session(config=config) D4 q- j8 @& `) m! b
定义需要使用到的常量。( D5 ^* X1 [" [( w$ f3 R' r
BUFFER_SIZE = 400
BATCH_SIZE = 1
IMG_WIDTH = 256
IMG_HEIGHT = 256
PATH = 'dataset/'
OUTPUT_CHANNELS = 30 z2 |% I* ^% w: g
LAMBDA = 100
EPOCHS = 10" R4 B. V6 B5 g3 l6 o7 f
第二步: 定义需要使用的函数
图片数据加载函数,主要的作用是使用Tensorflow的io接口读入图片,并且放入tensor的对象中,方便后续使用
def load(image_file):9 c8 U) S% H8 c6 x& X
image = tf.io.read_file(image_file)! V$ r' C5 _: M
image = tf.image.decode_jpeg(image)
w = tf.shape(image)[1]
w = w // 25 P7 Z* p. T1 U& @+ T
input_image = image[:, :w, :]
real_image = image[:, w:, :]
input_image = tf.cast(input_image, tf.float32)4 z( ^$ T% T2 b$ E( D8 x5 d
real_image = tf.cast(real_image, tf.float32)& q9 ` w- |* A4 W2 s
return input_image, real_image
tensor对象转成numpy对象的函数
在训练过程中,我会可视化一些训练的结果以及中间状态的图片。Tensorflow的tensor对象无法直接在matplot中直接使用,因此我们需要一个函数,将tensor转成numpy对象。
def tensor_to_array(tensor1):& h" f: E2 f! @6 F$ \
return tensor1.numpy()
第三步: 数据可视化+ M1 G1 A5 W( u" P# v: m
我们先来看下我们的训练数据长成什么样。
我们每张数据图片分成了两个部分,左边部分是线框图,我们用来作为输入数据,右边部分是上色图,我们用来作为训练的目标图片。
我们使用上面定义的load函数来加载一张图片看下
input, real = load(PATH+'train/114.jpg') m, v1 U6 _+ V, A8 d# A
plt.figure()6 `$ z) @( I$ v! X& |
plt.imshow(tensor_to_array(input)/255.0)
plt.figure()4 t3 S+ J+ Q0 @, H; F& p' d
plt.imshow(tensor_to_array(real)/255.0)4 V, ]8 j9 k. l4 h0 ?& N0 S% v
3 R) c# S& n1 h; e! x
第四步: 数据增强/ e8 Q4 q3 @) X5 ~" A7 V3 F% o
由于我们的训练数据不够多,我们使用数据增强来增加我们的样本。从而让小样本的数据也能达到更好的效果。, }) {$ P8 ~! M( ?: N
我们采取如下的数据增强方案:2 S* }* ^" ?, U: g/ f1 f; k% F
图片缩放, 将输入数据的图片缩放到我们指定的图片的大小随机裁剪数据归一化左右翻转 E8 [* I% T# {2 [
def resize(input_image, real_image, height, width):) h$ D# o- P7 V( W+ I/ h" ]6 K
input_image = tf.image.resize(input_image, [height, width], method=tf.image.ResizeMethod.NEAREST_NEIGHBOR)
real_image = tf.image.resize(real_image, [height, width], method=tf.image.ResizeMethod.NEAREST_NEIGHBOR)
return input_image, real_image
def random_crop(input_image, real_image):& F9 F1 u* r' t
stacked_image = tf.stack([input_image, real_image], axis=0)
cropped_image = tf.image.random_crop(stacked_image, size=[2, IMG_HEIGHT, IMG_WIDTH, 3])& Z2 n8 l: J N ]& k7 Z$ B
return cropped_image[0], cropped_image[1]
def random_crop(input_image, real_image):
stacked_image = tf.stack([input_image, real_image], axis=0); J7 F d; X4 R+ n- e
cropped_image = tf.image.random_crop(stacked_image, size=[2, IMG_HEIGHT, IMG_WIDTH, 3]) n7 f; i' C0 b0 Y& i \* z% o
return cropped_image[0], cropped_image[1]
我们将上述的增强方案做成一个函数,其中左右翻转是随机进行
@tf.function()
def random_jitter(input_image, real_image):0 i! `% D5 _0 G) ?+ t
input_image, real_image = resize(input_image, real_image, 286, 286)
input_image, real_image = random_crop(input_image, real_image)
if tf.random.uniform(()) > 0.5:' k0 n" O2 S; I l6 m" n0 z
input_image = tf.image.flip_left_right(input_image)
real_image = tf.image.flip_left_right(real_image)
return input_image, real_image1 m% X% d+ V0 T+ a1 W7 J! A; c
数据增强的效果( ~$ T5 d7 I/ S3 p
plt.figure(figsize=(6, 6))
for i in range(4):1 ]6 Y- w8 N3 S) j
input_image, real_image = random_jitter(input, real)
plt.subplot(2, 2, i+1)
plt.imshow(tensor_to_array(input_image)/255.0)9 G" C2 T: M) \1 v; f4 U
plt.axis('off')( O M! K( m( D( s# v- \
plt.show()! x0 r, l$ G- b% L& L9 C
, i* n. F" l# m( \* F. I
第五步: 训练数据的准备: M, @% ]. a5 [% i/ c. e
定义训练数据跟测试数据的加载函数
def load_image_train(image_file):
input_image, real_image = load(image_file)
input_image, real_image = random_jitter(input_image, real_image)
input_image, real_image = normalize(input_image, real_image). l6 x5 K, r3 w* D' q
return input_image, real_image
def load_image_test(image_file):
input_image, real_image = load(image_file)
input_image, real_image = resize(input_image, real_image, IMG_HEIGHT, IMG_WIDTH): w0 z; B0 S) F% D/ \3 d+ e- p
input_image, real_image = normalize(input_image, real_image)
return input_image, real_image
使用tensorflow的DataSet来加载训练和测试数据, 定义我们的训练数据跟测试数据集对象
train_dataset = tf.data.Dataset.list_files(PATH+'train/*.jpg')6 W% D# A+ i0 a5 C, w! y h
train_dataset = train_dataset.map(load_image_train, num_parallel_calls=tf.data.experimental.AUTOTUNE)
train_dataset = train_dataset.cache().shuffle(BUFFER_SIZE)
train_dataset = train_dataset.batch(1)
test_dataset = tf.data.Dataset.list_files(PATH+'test/*.jpg')( ?8 w7 K2 `4 z* ^- [ B
test_dataset = test_dataset.map(load_image_test)
test_dataset = test_dataset.batch(1)% m4 \& y; ^: b( T8 _
第六步: 定义模型
口袋妖怪的上色,我们使用的是GAN模型来训练, 相比上个条件GAN生成手写数字图片,这次的GAN模型的复杂读更加的高。0 {, |' B9 D* q8 m# F% Z$ q% Z$ X
我们先来看下生成网络跟判别网络的整体结构* E' k+ Y: r0 G/ U9 G
生成网络
生成网络使用了U-Net的基本框架,编码阶段的每一个Block我们使用, 卷积层->BN层->LeakyReLU的方式。解码阶段的每一个Block我们使用, 反卷积->BN层->Dropout或者ReLU。其中前三个Block我们使用Dropout, 后面的我们使用ReLU。每一个编码层的Block输出还连接了与之对应的解码层的Block. 具体可以参考U-Net的skip connection.
定义编码Block2 P3 x1 T* G8 h- o6 C: V/ `
def downsample(filters, size, apply_batchnorm=True):
initializer = tf.random_normal_initializer(0., 0.02)
result = tf.keras.Sequential(). A$ W7 z1 M6 Y2 }- s! [; y
result.add(tf.keras.layers.Conv2D(filters, size, strides=2, padding='same', kernel_initializer=initializer, use_bias=False))6 X$ }" O( ~* c* [, H
if apply_batchnorm:1 i- L4 y1 R( L, Z3 G
result.add(tf.keras.layers.BatchNormalization())" n5 |5 M- Q: E
result.add(tf.keras.layers.LeakyReLU())
return result7 G0 D. R( `3 P% J: B. P
down_model = downsample(3, 4)
定义解码Block
def upsample(filters, size, apply_dropout=False):
initializer = tf.random_normal_initializer(0., 0.02) o7 W. v- Z3 |7 `3 Z- { B5 Z1 d
result = tf.keras.Sequential()
result.add(tf.keras.layers.Conv2DTranspose(filters, size, strides=2, padding='same', kernel_initializer=initializer, use_bias=False))
result.add(tf.keras.layers.BatchNormalization())( B# N4 r( a- X2 v( b8 |
if apply_dropout:
result.add(tf.keras.layers.Dropout(0.5))# H: m7 ^: J# q) v; D9 R% Y
result.add(tf.keras.layers.ReLU())
return result
up_model = upsample(3, 4)
定义生成网络模型
def Generator():' y d: t3 B7 l" Q
down_stack = [$ X8 }3 V1 w, V+ K0 L! T
downsample(64, 4, apply_batchnorm=False), # (bs, 128, 128, 64)
downsample(128, 4), # (bs, 64, 64, 128)
downsample(256, 4), # (bs, 32, 32, 256)
downsample(512, 4), # (bs, 16, 16, 512)
downsample(512, 4), # (bs, 8, 8, 512), U% o4 G) m9 ?( N( q! q' L1 T
downsample(512, 4), # (bs, 4, 4, 512)1 o; n$ ^2 H* w$ W z. w
downsample(512, 4), # (bs, 2, 2, 512)4 r0 P% v- e( p) D" H z
downsample(512, 4), # (bs, 1, 1, 512)2 t+ N2 X' x& R( w: `+ q
]6 M0 G/ k [' Y
up_stack = [
upsample(512, 4, apply_dropout=True), # (bs, 2, 2, 1024)
upsample(512, 4, apply_dropout=True), # (bs, 4, 4, 1024)" U6 i: |1 W% F* Q
upsample(512, 4, apply_dropout=True), # (bs, 8, 8, 1024)
upsample(512, 4), # (bs, 16, 16, 1024)4 E/ x) m! f8 |$ K1 K
upsample(256, 4), # (bs, 32, 32, 512), z: ~- R0 z( u6 G1 \# c9 o7 h
upsample(128, 4), # (bs, 64, 64, 256)
upsample(64, 4), # (bs, 128, 128, 128)
]
initializer = tf.random_normal_initializer(0., 0.02)
last = tf.keras.layers.Conv2DTranspose(OUTPUT_CHANNELS, 4,
strides=2,+ i6 m7 q8 y# A* i- k" g
padding='same',
kernel_initializer=initializer,
activation='tanh') # (bs, 256, 256, 3)6 b) r5 B( @! q T" o
concat = tf.keras.layers.Concatenate()8 k" {: A- C7 |" Z# L1 |
inputs = tf.keras.layers.Input(shape=[None,None,3])1 }9 q) w6 S/ x G
x = inputs
skips = []
for down in down_stack:
x = down(x)
skips.append(x)
skips = reversed(skips[:-1])- f' d( o9 B% i3 F
for up, skip in zip(up_stack, skips):. c8 e" v5 a8 \
x = up(x)& o8 u, [& @0 c. G+ \. y
x = concat([x, skip])
x = last(x)3 q# `+ w8 X% b% d% O; S7 G
return tf.keras.Model(inputs=inputs, outputs=x)6 M/ p. }+ ~/ k
generator = Generator()
判别网络+ X$ W" i' V# A, R8 n* [" _" |
判别网络我们使用PatchGAN, PatchGAN又称之为马尔可夫判别器。传统的基于CNN的分类模型有很多都是在最后引入了一个全连接层,然后将判别的结果输出。然而PatchGAN却不一样,它完全由卷积层构成,最后输出的是一个纬度为N的方阵。然后计算矩阵的均值作真或者假的输出。从直观上看,输出方阵的每一个输出,是模型对原图中的一个感受野,这个感受野对应了原图中的一块地方,也称之为Patch,因此,把这种结构的GAN称之为PatchGAN。
PatchGAN中的每一个Block是由卷积层->BN层->Leaky ReLU组成的。/ n7 J9 d8 h# p9 u) S& b3 W0 W* S
在我们的这个模型中,最后一层我们的输出的纬度是(Batch Size, 30, 30, 1), 其中1表示图片的通道。6 Q! s" a' `9 u: } q, K
每个30x30的输出对应着原图的70x70的区域。详细的结构可以参考这篇论文。
def Discriminator():6 \% r7 h% a! J% t1 I
initializer = tf.random_normal_initializer(0., 0.02)- d% P8 k" M- a& A& X' Z0 s- t K1 \
inp = tf.keras.layers.Input(shape=[None, None, 3], name='input_image')
tar = tf.keras.layers.Input(shape=[None, None, 3], name='target_image'): `* K* h4 _9 z" e( A& M
# (batch size, 256, 256, channels*2)
x = tf.keras.layers.concatenate([inp, tar])9 X2 e" }3 T1 g" ~* K
# (batch size, 128, 128, 64)
down1 = downsample(64, 4, False)(x)
# (batch size, 64, 64, 128)
down2 = downsample(128, 4)(down1)
# (batch size, 32, 32, 256)
down3 = downsample(256, 4)(down2)
# (batch size, 34, 34, 256): P" g- R1 S! ], ~
zero_pad1 = tf.keras.layers.ZeroPadding2D()(down3)1 G' l I0 s: c# r+ U* `) E/ X
, A2 U4 d: A# p0 C0 u3 C) r
# (batch size, 31, 31, 512)+ ]# [3 j7 w% c9 j' ?
conv = tf.keras.layers.Conv2D(512, 4, strides=1, kernel_initializer=initializer, use_bias=False)(zero_pad1)
batchnorm1 = tf.keras.layers.BatchNormalization()(conv)
leaky_relu = tf.keras.layers.LeakyReLU()(batchnorm1)4 ?2 i2 M3 t2 T- Z$ y. @$ H3 |- u2 \; ~
# (batch size, 33, 33, 512)' k$ \3 U1 }+ ~+ D
zero_pad2 = tf.keras.layers.ZeroPadding2D()(leaky_relu)
# (batch size, 30, 30, 1)% b' }* ~4 h' K5 [4 D6 X6 D& i
last = tf.keras.layers.Conv2D(1, 4, strides=1, kernel_initializer=initializer)(zero_pad2)/ I9 T" Z- S( J K" w* C# ~" t' T' \
return tf.keras.Model(inputs=[inp, tar], outputs=last)% b! M' U. R- [' I" u
discriminator = Discriminator()- o, i, L4 U( S. m
第七步: 定义损失函数和优化器5 c% S( O0 Q1 B& i
** x, h o& P/ q
**3 S$ c' r* y8 v- U
loss_object = tf.keras.losses.BinaryCrossentropy(from_logits=True)
**1 I3 I$ N! M1 v% p" ]
def discriminator_loss(disc_real_output, disc_generated_output):6 _/ S, L# b1 \ W
real_loss = loss_object(tf.ones_like(disc_real_output), disc_real_output)
generated_loss = loss_object(tf.zeros_like(disc_generated_output), disc_generated_output)& O3 O! w: n/ h$ x5 k
total_disc_loss = real_loss + generated_loss
return total_disc_loss' F; x Q2 l! R, e o! o- R
def generator_loss(disc_generated_output, gen_output, target):
gan_loss = loss_object(tf.ones_like(disc_generated_output), disc_generated_output)
l1_loss = tf.reduce_mean(tf.abs(target - gen_output))% l) I; y3 ^' L
total_gen_loss = gan_loss + (LAMBDA * l1_loss)' W! V8 i/ `9 }, G# v7 K; e
return total_gen_loss
generator_optimizer = tf.keras.optimizers.Adam(2e-4, beta_1=0.5)2 B+ h' l* ~7 {
discriminator_optimizer = tf.keras.optimizers.Adam(2e-4, beta_1=0.5)
第八步: 定义CheckPoint函数& B' E+ X# i* ~
由于我们的训练时间较长,因此我们会保存中间的训练状态,方便后续加载继续训练
checkpoint = tf.train.Checkpoint(generator_optimizer=generator_optimizer,5 V* ~% M& _$ x7 O! t, |( R' z
discriminator_optimizer=discriminator_optimizer,
generator=generator,' I4 n e( ~& v3 c
discriminator=discriminator)* w& N3 k3 J/ M. F% p4 ^5 J
如果我们保存了之前的训练的结果,我们加载保存的数据。然后我们应用上次保存的模型来输出下我们的测试数据。
def generate_images(model, test_input, tar):
prediction = model(test_input, training=True)% `- U ~4 ~) H& n7 V4 i
plt.figure(figsize=(15,15))2 f6 ~, a) a- O
display_list = [test_input[0], tar[0], prediction[0]]
title = ['Input', 'Target', 'Predicted']
for i in range(3):# @7 R1 Z8 \1 b
plt.subplot(1, 3, i+1)2 i2 A% O$ F/ `: m$ ?. n
plt.title(title)
plt.imshow(tensor_to_array(display_list) * 0.5 + 0.5)# k% L) Y, w) t7 w) {9 G, h* Q
plt.axis('off')
plt.show()5 ~2 m( w* L; a; {
ckpt_manager = tf.train.CheckpointManager(checkpoint, "./", max_to_keep=2)# p8 \& L5 T Z. v) M
if ckpt_manager.latest_checkpoint:* e+ K: v. {1 r! @
checkpoint.restore(ckpt_manager.latest_checkpoint)
for inp, tar in test_dataset.take(20):
generate_images(generator, inp, tar)$ n# {. r4 L/ T! B5 P# K m- Z$ K" c
第九步: 训练( ] m: D( I- A
在训练中,我们输出第一张图片来查看每个epoch给我们的预测结果带来的变化。让大家感受到其中的乐趣
每20个epoch我们保存一次状态7 K" D' g+ J$ }8 m0 {# q
@tf.function. p. ?/ v; I% k! z, e5 S0 K3 Z! P5 K; b
def train_step(input_image, target):: e$ P4 o) O7 T! I7 |, N
with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape:
gen_output = generator(input_image, training=True)
disc_real_output = discriminator([input_image, target], training=True)
disc_generated_output = discriminator([input_image, gen_output], training=True)
gen_loss = generator_loss(disc_generated_output, gen_output, target)8 V% _! [0 x/ |+ o2 W- ?
disc_loss = discriminator_loss(disc_real_output, disc_generated_output)
generator_gradients = gen_tape.gradient(gen_loss,& M0 R/ O8 [) X5 ~$ d" \
generator.trainable_variables)
discriminator_gradients = disc_tape.gradient(disc_loss,. ]% K9 \; I5 z$ p4 E
discriminator.trainable_variables)& _8 @* d4 ?& S0 A& U& R
generator_optimizer.apply_gradients(zip(generator_gradients,
generator.trainable_variables)); l% D# J9 m' y- {- M$ @9 {
discriminator_optimizer.apply_gradients(zip(discriminator_gradients,
discriminator.trainable_variables))' F, Y7 z' o+ }9 ?: z3 I0 A
def fit(train_ds, epochs, test_ds):5 Q& b9 N* @- y+ g* [$ a1 o' m
for epoch in range(epochs):6 P- i& w g' F _5 g) e% O0 x
start = time.time()
for input_image, target in train_ds:8 t% _" k0 c% f* v- C2 t# t
train_step(input_image, target)
clear_output(wait=True)
for example_input, example_target in test_ds.take(1):; r& X+ }: w' M$ {0 D1 R9 U
generate_images(generator, example_input, example_target)
if (epoch + 1) % 20 == 0:+ q1 {: e$ ~9 C* [+ X7 q: l& ^) l
ckpt_save_path = ckpt_manager.save()
print ('保存第{}个epoch到{}\n'.format(epoch+1, ckpt_save_path))& I! E/ w, t W. E2 N
print ('训练第{}个epoch所用的时间为{:.2f}秒\n'.format(epoch + 1, time.time()-start))
fit(train_dataset, EPOCHS, test_dataset)
/ e4 P$ M3 [6 G+ g. i; K9 E( x6 ?3 b5 V5 I
训练第8个epoch所用的时间为51.33秒。1 c- ?2 @( {& Q
第十步: 使用测试数据上色,查看下我们的效果
for input, target in test_dataset.take(20):2 R! }: y$ {( e6 W" G$ y! q3 l. x5 f
generate_images(generator, input, target)
矩池云现在已经上架 “口袋妖怪上色” 镜像;感兴趣的小伙伴可以通过矩池云官网“Jupyter 教程 Demo” 镜像中尝试使用。
成为第一个吐槽的人