DCGAN

이번 년도에는 꼭 딥러닝 관련 지식을 쌓고 싶어 관련 수업들을 듣게 되었는데, 비지도 학습에 대한 프로젝트를 진행하게 되어 공부한 내용들을 조금씩 써보고자 합니다. 완전히 맞지 않는 설명이 있을 수도 있으니 유의하면서 읽어 주세요 :)

이 포스팅은 tensorflow 공식 tutorial 의 게시물을 바탕으로 작성 되었습니다.
(코드 또한 tutorial 에서 확인하실 수 있습니다.)
https://www.tensorflow.org/tutorials/generative/dcgan?authuser=1&hl=ko

DCGAN?

이번에 공부하면서 처음 알게 된 모델입니다. 비지도 학습의 대표적인 모델이라고 할 수 있을 것 같습니다. 생성자(Generator)감별자(Discriminator) 가 동시에 학습을 진행하면서 generator 에서는 loss 를 줄여가는 형식으로 실제 이미지와 유사한 이미지를 만들어가며, 감별자는 학습을 통해 어떤 이미지가 실제 이미지 데이터에서 왔는지, 아니면 generator 에서 만들어진 것인지 점차 잘 구별하게 됩니다. 최종적으로 discriminator 에서 참 거짓을 구별 하지 못할 만큼 generator 에서 실제 이미지와 유사한 이미지를 만들어 내는 것이 이 모델의 목표 입니다.
기존의 GAN 모델에 Convolutional layer, batch normalization layer, LeakyReLU 를 사용하여 모델을 구성 하였다고 합니다.

Code 구현 내용

Model

생성자(G) 모델과 감별자(D) 모델을 만들어야 합니다.
일단 생성자 모델부터 확인 해보면 다음과 같습니다.

def make_generator_model():
    model = tf.keras.Sequential()
    model.add(layers.Dense(7*7*256, use_bias=False, input_shape=(100,)))
    model.add(layers.BatchNormalization())
    model.add(layers.LeakyReLU())

    model.add(layers.Reshape((7, 7, 256)))
    assert model.output_shape == (None, 7, 7, 256) # 주목: 배치사이즈로 None이 주어집니다.

    model.add(layers.Conv2DTranspose(128, (5, 5), strides=(1, 1), padding='same', use_bias=False))
    assert model.output_shape == (None, 7, 7, 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, 14, 14, 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, 28, 28, 1)

    return model

생성자 모델은 다음과 같이 만들 수 있습니다. (100,) 인 1차원 array 가 생성자 모델의 input 이고, 이 모델을 거치면 28 X 28 X 1 (channel last policy) 인 이미지가 생성 되어야 합니다. 28 X 28 X 1 의 데이터가 생성되어야 하는 이유는 이 코드 자체가 MNIST 데이터를 기준으로 하고 있기 때문입니다.

다음으로는 감별자 모델을 확인해보겠습니다.

def make_discriminator_model():
    model = tf.keras.Sequential()
    model.add(layers.Conv2D(64, (5, 5), strides=(2, 2), padding='same',
                                     input_shape=[28, 28, 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

감별자 모델은 28 X 28 X 1 의 이미지가 input 으로 들어오면 해당 이미지에 대한 classification 을 진행하여 진짜 사용자가 넣은 데이터에서부터의 이미지인지 아니면 생성자가 생성해낸 이미지인지 구별해야 합니다. 그러므로 마지막에 Flatten 을 진행하여 모든 피쳐들을 일차원 배열로 펼치고 Dense layer 을 통해 1가지의 결과를 내는 것입니다.

loss function and optimizer

cross_entropy = tf.keras.losses.BinaryCrossentropy(from_logits=True)

일단 크로스 엔트로피 손실함수를 계산하기 위한 헬퍼 함수를 호출을 해놓고 변수 안에 넣어줘야 합니다.

감별자 손실함수는 감별자가 얼마나 진짜와 가짜(from G) 를 잘 구분하는지 계산하는 역할을 합니다. 진짜라고 예측해야 하는 이미지들 (1이라는 값을 가지고 있어야 함) 과 1로 이루어진 행렬에 대한 계산과 가짜라고 예측해야 하는 이미지들(0 이라는 값을 가져야 함) 과 0으로 이루어진 행렬에 대한 계산을 하고 두 값을 더해주면 감별자 손실함수를 구할 수 있습니다.

def discriminator_loss(real_output, fake_output):
    real_loss = cross_entropy(tf.ones_like(real_output), real_output)
    fake_loss = cross_entropy(tf.zeros_like(fake_output), fake_output)
    total_loss = real_loss + fake_loss
    return total_loss

생성자 손실함수는 감별자를 얼마나 잘 속였는지, 즉 생성자가 만들어낸 이미지에 대해 감별자가 1이라는 값을 얼마나 내놓았는지 알아야 하는 역할입니다. 생성자가 만들어낸 값들과 1로 이루어진 행렬에 대한 크로스 엔트로피를 계산하여 반환하면 됩니다.

def generator_loss(fake_output):
    return cross_entropy(tf.ones_like(fake_output), fake_output)

생성자와 감별자는 따로따로 훈련되기 때문에 옵티마이저가 달라야 하며, 옵티마이저로는 Adam 을 사용합니다. (이 부분은 설명할 것이 있기 때문에 학습한 내용으로 다시 포스팅을 쓸 예정입니다.)

training

이렇게 모델과 손실함수, 옵티마이저를 정의했으면 훈련 시킬 수 있습니다.

# `tf.function`이 어떻게 사용되는지 주목해 주세요.
# 이 데코레이터는 함수를 "컴파일"합니다.
@tf.function
def train_step(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))

이 부분이 개인적으로 이해하기 좀 어렵긴 했습니다. 일단 순서대로 설명하자면 다음과 같이 설명할 수 있을 것 같습니다.
일단 noise (generator 의 input) 을 (100,) 인 배열로 배치 사이즈 개수만큼 만들어서 generated_images 를 배치 사이즈의 개수만큼 만들어냅니다. real_output 은 감별자가 진짜 이미지에 대해 감별한 결과이고, fake_output 은 generated_images 에 대해 감별한 결과 입니다. 이 결과들을 통해 아까 정의 했던 손실 함수에 output 을 넣어서 감별자 손실함수 값과 생성자 손실함수 값을 구합니다. 저도 처음 본 tf.GradientTape 라는 훈련 루프를 사용하는데, 여기서 gradient 를 구해주고 각각의 옵티마이저에 gradient 값을 넣어주어 최적화를 시키는 데 사용하는 것 같습니다. 위 함수는 훈련 시 한 루프를 돌 때 실행해야 하는 함수이고, 아래 함수가 훈련하는 함수 입니다.

def train(dataset, epochs):
  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 에포크가 지날 때마다 모델을 저장합니다.
    if (epoch + 1) % 15 == 0:
      checkpoint.save(file_prefix = checkpoint_prefix)

    # print (' 에포크 {} 에서 걸린 시간은 {} 초 입니다'.format(epoch +1, time.time()-start))
    print ('Time for epoch {} is {} sec'.format(epoch + 1, time.time()-start))

  # 마지막 에포크가 끝난 후 생성합니다.
  display.clear_output(wait=True)
  generate_and_save_images(generator,
                           epochs,
                           seed)

이 예제에서는 epochs 횟수는 50회로 정했습니다. 여기서 dataset 은 배치 사이즈(이 예제에서는 256 으로 설정되었음) 개수만큼 slice 를 진행하여 셔플을 진행한 상태입니다.

# 데이터 배치를 만들고 섞습니다.
train_dataset = tf.data.Dataset.from_tensor_slices(train_images).shuffle(BUFFER_SIZE).batch(BATCH_SIZE)

그러면 50 회 루프를 도는 동안 256 씩 나누어진 전체 데이터셋에 대한 훈련을 진행할 것입니다.
이 모델을 훈련 시킨 결과는 다음과 같습니다.

진짜 숫자와 비슷한 모양을 생성해낸 걸 보니 신기 하기도 합니다 ㅎㅎ
비지도 학습을 사용하여 Anomaly Detection 을 하기 위해 DCGAN 공부부터 시작했는데요, 쉽지 않은 내용이라 계속 보면서 공부해봐야겠습니다. 여기 나온 개념들이나 이후 공부하는 부분도 포스팅 하도록 하겠습니다 ^^

Pytorch 로 공부하시고 싶으신 분들은 https://pytorch.org/tutorials/beginner/dcgan_faces_tutorial.html?highlight=gan
위 링크에 tutorial 이 있으니 시도 해보시면 좋을 것 같습니다. 저도 한 번 해보려고 합니다!!!

좋은 웹페이지 즐겨찾기