GAN, DCGAN, CycleGAN
유투브에서 찾아본 Cycle GAN 논문 리뷰 영상인데, cycle gan 뿐 아니라 앞서 gan에 대한 구조 등 설명도 좋아서 링크 첨부해둔다.
생성자(generator)
는 위조 지폐범처럼 실제와 동일한 데이터를 만들기 위해 노력하고,판별자(discriminator)
는 지폐 감별사처럼 생성된 데이터가 진짜인지 아닌지 판단하게 된다. 처음에는 생성자와 판별자의 성능이 좋지 않지만 둘이 뚝딱뚝딱 하면서 생성자의 성능이 올라가게 된다.생성자
: 진짜같은 가짜 이미지를 만들어냄. 판별자
: 진짜 이미지를 가지고 생성자가 만든 이미지의 가짜/진짜 판별판별자의 손실함수는 real_loss와 fake_loss를 더한 값입니다.
라는 말의 의미를 판별자는 진짜와 가짜 모두를 잘 구분해내야하기 때문에 그 loss를 줄이는 방향으로 학습한다는 것으로 이해했는데 맞나요? 반대로 생성자는 판별자에 의해 fake라고 판별된 결과에 대해 loss를 줄여야 하니까 Fake로 판별된 것의 loss를 줄이는 쪽으로 학습되는 것을 의미하는 거구요. > 맞다고 함. 중요한 것!!!
오늘 GAN 코드 예제에서는 Convolution Layer로 이루어진 DCGAN(Deep Convolution GAN)
을 통해 MNIST 손글씨를 생성하는 것을 다루는 것을 보았다.
오늘 Cycle GAN을 구현한 예제 코드까지 보지는 않았다.
아래는 Lecture Note에 있는 생성자 생성 함수입니다. (...)
- Upsampling 층을 통과한 후의 이미지 shape에 대해 적어주세요.
def make_generator_model():
generator_input = tf.keras.layers.Input(shape=(100,), name='generator_input')
x = generator_input
x = tf.keras.layers.Dense(7*7*256, use_bias=False)(x)
x = tf.keras.layers.BatchNormalization()(x)
x = tf.keras.layers.LeakyReLU()(x)
x = tf.keras.layers.Reshape((7,7,256))(x)
x = tf.keras.layers.UpSampling2D()(x) # => 이전 레이어의 (7,7,256)를 받아 (14, 14, 256)로 업샘플링.
x = tf.keras.layers.Conv2D(128, 5, strides=1, padding='same', use_bias=False)(x) # => (5,5) 필터 사이즈를 128개 생성. stride = 1, padding = 'same' 옵션이 있으므로 통과 후 쉐입은 (14, 14, 128)
x = tf.keras.layers.BatchNormalization()(x)
x = tf.keras.layers.LeakyReLU()(x)
x = tf.keras.layers.UpSampling2D()(x) # => (28, 28, 128)
x = tf.keras.layers.Conv2D(64, 5, strides=1, padding='same', use_bias=False)(x) # => (28, 28, 64)
x = tf.keras.layers.BatchNormalization()(x)
x = tf.keras.layers.LeakyReLU()(x)
x = tf.keras.layers.Conv2DTranspose(64, 5, strides=1, padding='same', use_bias=False)(x) # => (28, 28, 64)
x = tf.keras.layers.Conv2DTranspose(1, 5, strides=1, padding='same', use_bias=False, activation='tanh')(x) # => (28, 28, 1)
generator_output = x
return tf.keras.models.Model(generator_input, generator_output)
model = make_generator_model()
'''
# 참고
- Conv2DTranspose에서 padding = "same"을 걸면 input size * stride = output size가 되도록 padding을 해준다. 출처 https://velog.io/@hayaseleu/tf.keras.layers.Conv2DTransposeoutput-shape-%EC%A0%95%EB%A6%AC
- Con2D에서는 padding = "same", stride = (2,2)일 때 절반으로 사이즈를 줄여준다. pmhttps://stackoverflow.com/questions/69709010/keras-conv2d-strange-i-use-padding-same-but-the-size-is-still-reduced
헷갈리니 잘 기억해두자.
'''
Upsampling + Conv2D
가 Conv2DTranspose
를 대체할 수도 있다는 점도 기억해두자. 구글 QuickDraw 데이터셋을 다시 활용해 고양이를 그려내는 GAN 만들어보기
데이터를 불러오는 단계를 제외하고 GAN 구현 코드가 일부도 주어지지 않아서 살짝 당황하기도 했지만~ 거의 렉쳐 노트에 있는 코드를 이해하고 옮겨오면서 구현해보았다. 난이도가 상당하게 느껴졌다. 나중에 GAN 모델을 만들게 된다면 아마 다시 레퍼런스를 찾아보며 하게될 것 같다.
[훈련 데이터 불러오기]
import tensorflow as tf
import json, glob, imageio
import matplotlib.pyplot as plt
import numpy as np
import os
import time
from IPython import display
from tensorflow.keras import layers
from tensorflow.keras.utils import get_file
BASE_PATH = 'https://storage.googleapis.com/quickdraw_dataset/full/binary/'
path = get_file('cat', BASE_PATH + 'cat.bin')
import PIL
from PIL import ImageDraw
from struct import unpack
from sklearn.model_selection import train_test_split
def load_drows(path, train_size=0.85):
x = []
# 파일을 풀고 낙서를 하나하나 모은다. 낙서는 15바이트 헤더로 시작한다.
with open(path, 'rb') as f:
while True:
img = PIL.Image.new('L', (32, 32), 'white') # 8-bit pixels, black and white #https://pillow.readthedocs.io/en/stable/handbook/concepts.html#concept-modes
draw = ImageDraw.Draw(img)
header = f.read(15)
if len(header) != 15:
break
# 낙서는 x,y 좌표로 구성된 획(stroke) 목록으로 되어 있고, 각 좌표는 분리되어 저장되어 있다.
# 방금 위에서 생성한 ImageDraw 객체의 좌표 목록을 이용하기 위해 zip()함수를 사용하여 합쳐준다.
strokes, = unpack('H', f.read(2))
for i in range(strokes):
n_points, = unpack('H', f.read(2))
fmt = str(n_points) + 'B'
read_scaled = lambda: (p // 8 for
p in unpack(fmt, f.read(n_points)))
points = [*zip(read_scaled(), read_scaled())] # zip 함수
draw.line(points, fill=0, width=2)
img = tf.keras.utils.img_to_array(img)
x.append(img)
x = np.asarray(x) / 255
return train_test_split(x, train_size=train_size)
# 입력받은 10만개의 고양이 낙서 데이터를 활용할 수 있다.
x_train, x_test = load_drows(path)
print(x_train.shape, x_test.shape) # ((104721, 32, 32, 1), (18481, 32, 32, 1))
127.5
로 나누어서 데이터의 범위를 -1~1
사이로 두었는데, 이는 경험적으로 이렇게 정규화하니 가장 성능이 잘 나오더라~ 하는 것이라고 한다. 오늘 학습한 기본적인 GAN을 활용하여 고양이 그림을 그려내는 여러분만의 GAN을 만들어보세요!
다음과 같은 내용을 숙지하며 과제를 진행해달라고 했는데 이것도 참고하여 코드를 볼만 하다.
BUFFER_SIZE = 104721 # => 전체 데이터셋 갯수로 지정함.
BATCH_SIZE = 256
# 데이터 배치를 만들고 섞기.
train_dataset = tf.data.Dataset.from_tensor_slices(x_train).shuffle(BUFFER_SIZE).batch(BATCH_SIZE)
'''
[기록]
레퍼런스
https://kyuns96.tistory.com/3
https://helloyjam.github.io/tensorflow/buffer-size-in-shuffle/
https://velog.io/@crosstar1228/tensorflowtf.data.Dataset-%ED%81%B4%EB%9E%98%EC%8A%A4-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0
- shuffle을 하는 이유는 원래는 mnist 손글씨 예제에서는 1-9 숫자로 이루어졌기 때문에 한 번 섞어서 배치에 1만 들어가 있거나 하는 걸 막기 위해서다.
- 이번 데이터는 그렇지는 않아서 빼도 되긴 하지만, 연산에 무리가 있는 수준은 아니므로 코드를 빼진 않았다.
- 배치를 나누는 이유는 한번에 대량의 데이터를 넣으면 메모리가 터질 수도 있으니까 나눠서 학습해주는 것임.
'''
### Deep Convolution GAN으로 모델링 ###
# generator 생성
'''
# 노트에서는 input shape이 (28,28, 1)이었지만, 위 훈련 데이터는 (32,32,1)이므로 이에 맞추어 층을 쌓아 out shape을 만들어야 한다.
맨 마지막 층에서 output shape부터 적어두고 차근차근 위로 올라오며 필요한 부분 수정해보았다.
'''
def make_generator_model():
"""
모델을 구축하는 함수입니다.
"""
model = tf.keras.Sequential()
model.add(layers.Dense(4*4*256, use_bias=False, input_shape=(100,)))
model.add(layers.BatchNormalization())
model.add(layers.LeakyReLU())
model.add(layers.Reshape((4, 4, 256)))
assert model.output_shape == (None, 4, 4, 256)
model.add(layers.Conv2DTranspose(128, (5, 5), strides=(2, 2), padding='same', use_bias=False))
assert model.output_shape == (None, 8, 8, 128)
model.add(layers.BatchNormalization())
model.add(layers.LeakyReLU())
model.add(layers.Conv2DTranspose(64, (5, 5), strides=(2, 2), padding='same', use_bias=False))
assert model.output_shape == (None, 16, 16, 64)
model.add(layers.BatchNormalization())
model.add(layers.LeakyReLU())
model.add(layers.Conv2DTranspose(1, (5, 5), strides=(2, 2), padding='same', use_bias=False, activation='tanh'))
assert model.output_shape == (None, 32, 32, 1) # => 훈련 데이터와 같은 사이즈로 출력.
return model
# 생성자로 이미지 한 번 생성해보기 (노트 따라하기)
generator = make_generator_model()
noise = tf.random.normal([1, 100]) # => 생성자는 처음엔 아무 의미가 없는 노이즈(=임의의 데이터)로부터 시작한다는 것
generated_image = generator(noise, training=False)
plt.imshow(generated_image[0, :, :, 0], cmap='gray') # => 일단 생성은 잘 되는구나..!
# Discriminator 생성
def make_discriminator_model():
model = tf.keras.Sequential()
model.add(layers.Conv2D(64, (5, 5), strides=(2, 2), padding='same', input_shape=[32, 32, 1])) # input shape (32, 32, 1) !!
model.add(layers.LeakyReLU())
model.add(layers.Dropout(0.3))
model.add(layers.Conv2D(128, (5, 5), strides=(2, 2), padding='same'))
model.add(layers.LeakyReLU())
model.add(layers.Dropout(0.3))
model.add(layers.Flatten())
model.add(layers.Dense(1))
return model
# 위에서 생성한 이미지 판별자에 돌려보기
discriminator = make_discriminator_model()
decision = discriminator(generated_image)
print(decision) # 실제(Real) 이미지에는 양수(Positive)를, 가짜(Fake) 이미지에는 음수(Negative)를 출력하도록 훈련된다.
#=> tf.Tensor([[-0.00103436]], shape=(1, 1), dtype=float32)
# 손실함수 정의
cross_entropy = tf.keras.losses.BinaryCrossentropy(from_logits=True) # => 참고 https://runebook.dev/ko/docs/tensorflow/keras/losses/binarycrossentropy
def discriminator_loss(real_output, fake_output):
"""
"""
real_loss = cross_entropy(tf.ones_like(real_output), real_output) # => ones_like 참고 https://runebook.dev/ko/docs/tensorflow/ones_like
fake_loss = cross_entropy(tf.zeros_like(fake_output), fake_output)
total_loss = real_loss + fake_loss # 둘 다 못 맞추는 걸 줄여 나가야 함.
return total_loss
def generator_loss(fake_output):
return cross_entropy(tf.ones_like(fake_output), fake_output) # 진짜여야 하는데 판별자한테 fake로 걸러진 이 loss를 줄여야 함.
# 옵티마이저 정의
generator_optimizer = tf.keras.optimizers.Adam(1e-4)
discriminator_optimizer = tf.keras.optimizers.Adam(1e-4)
real_loss
와 fake_loss
를 더한 값이다. 아래 말 기억!real_loss
의 경우 전체가 1인 행렬과 real_output
을 비교하여 구하며 fake_loss
의 경우 전체가 0인 행렬과 fake_output
을 비교하여 구합니다. 생성자의 손실 함수는 전체가 1인 행렬과 fake_output
을 비교하여 구합니다."## 모델 구축 ##
# 모델이 저장될 체크포인트 지정 (chekpoint 의미 참고 https://www.tensorflow.org/guide/checkpoint?hl=ko)
checkpoint_dir = './training_checkpoints'
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt")
checkpoint = tf.train.Checkpoint(generator_optimizer=generator_optimizer,
discriminator_optimizer=discriminator_optimizer,
generator=generator,
discriminator=discriminator)
# 훈련 루프 지정
EPOCHS = 50
noise_dim = 100
num_examples_to_generate = 16
seed = tf.random.normal([num_examples_to_generate, noise_dim])
# 생성자, 판별자의 학습 지정
def train_step(images):
"""
위에서 정의한 손실함수를 바탕으로
Iteration(=step) 마다 가중치를 갱신합니다.
Args:
images: 훈련 데이터셋에 있는 실제 이미지입니다.
"""
noise = tf.random.normal([BATCH_SIZE, noise_dim])
with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape:
generated_images = generator(noise, training=True)
real_output = discriminator(images, training=True)
fake_output = discriminator(generated_images, training=True)
gen_loss = generator_loss(fake_output)
disc_loss = discriminator_loss(real_output, fake_output)
gradients_of_generator = gen_tape.gradient(gen_loss, generator.trainable_variables)
gradients_of_discriminator = disc_tape.gradient(disc_loss, discriminator.trainable_variables)
generator_optimizer.apply_gradients(zip(gradients_of_generator, generator.trainable_variables))
discriminator_optimizer.apply_gradients(zip(gradients_of_discriminator, discriminator.trainable_variables))
# 학습 중간에 진행 내용 확인용 함수
def generate_and_save_images(model, epoch, test_input):
"""
모델이 이미지를 생성한 후 저장하는 함수입니다.
Args:
model: 이미지를 생성할 모델입니다.
epoch: 진행 중인 Epoch 숫자입니다.
test_input: model에 입력되는 데이터입니다.
"""
# training=False 이면 모든 층이 추론(inference)모드로 진행됩니다.
predictions = model(test_input, training=False)
fig = plt.figure(figsize=(4,4))
for i in range(predictions.shape[0]):
plt.subplot(4, 4, i+1)
plt.imshow(predictions[i, :, :, 0] * 127.5 + 127.5, cmap='gray')
plt.axis('off')
plt.savefig('image_at_epoch_{:04d}.png'.format(epoch))
plt.show()
def train(dataset, epochs):
"""
학습 중 실행할 동작을 함수로 정의합니다.
Args:
dataset: (훈련) 데이터셋입니다.
epochs: 최종 학습 Epoch 입니다.
"""
for epoch in range(epochs):
start = time.time()
for image_batch in dataset:
train_step(image_batch)
# 이미지를 생성한 뒤 저장합니다.(추후에 만들 GIF를 위함입니다.)
display.clear_output(wait=True)
generate_and_save_images(generator, epoch + 1, seed)
# 15 에포크가 지날 때마다 모델을 Checkpoint에 저장합니다.
if (epoch + 1) % 15 == 0:
checkpoint.save(file_prefix = checkpoint_prefix)
# Epoch 마다 소요 시간을 출력합니다.
print(f'Time for epoch {epoch + 1} is {time.time()-start} sec')
# 마지막 에포크가 끝난 후 이미지를 생성합니다.
display.clear_output(wait=True)
generate_and_save_images(generator, epochs, seed)
%%time
train(train_dataset, EPOCHS) # EPOCHS = 50
형편없지만.. ㅎㅎ,,, 그래도..ㅎㅎ
참고로 특정 epoch의 이미지를 가져오려면 아래와 같은 함수를 활용할 수 있다.
def display_image(epoch_no):
"""
특정 Epoch에 생성된 이미지를 불러오는 함수입니다.
Args:
epoch_no: 특정 Epoch에 해당하는 숫자입니다.
"""
return PIL.Image.open('image_at_epoch_{:04d}.png'.format(epoch_no))
Don't be evil