7 minute read

GAN은 딥페이크의 기반이 되는 신경망 모델이다. 딥페이크가 악용되어서 발생하는 사회적인 문제들이 아주 많지만, 딥페이크는 원래 인공지능이 질병을 잘 학습하고 정확히 진단할 수 있도록 의료용 이미지를 만드는 등의 좋은 목적을 가지고 등장했다. 사생활 문제로 실제 환자의 질병 데이터를 사용할 수 없기 때문에 딥페이크 기술로 생성해낸 이미지나 영상을 가지고 더 많은 데이터를 만들어내는 것이다. 이번 시간에는 가짜 데이터를 생성해내고, 이 데이터가 가짜인지 진짜인지 판별해내는 GAN 모델에 대해 알아보자.

GAN(Generative Adversarial Networks, 생성적 적대 신경망)

GAN은 실제와 유사한 데이터를 만들어내는 생성모델이고, 가짜 데이터를 생성해내는 생성자(Generator)와 이게 가짜인지 진짜인지의 여부를 판별하는 판별자(Discriminator)가 있다. 위조지폐의 예시를 들면, 처음 화폐가 등장했을 때 위조 지폐를 만들면 화폐 이미지가 복잡하지 않았을 것이기 때문에 생성해내기가 쉬웠을 것이고, 판별하기도 쉬웠을 것이다. 하지만, 위조지폐를 판별하는 기술이 점점 발전하면서 더 진짜 같이 만들기 위해 위조지폐를 생성하는 기술로 동시에 발전했고, 현재는 전문가가 아니면 구별하기 힘들정도로 진짜와 같은 위조지폐를 만들어낸다고 한다.

GAN의 작동방식과 위조지폐의 예시가 같다. 처음에는 생성자(Generator)가 진짜 같은 데이터를 생성해내지 못하다가, 판별자(Discriminator)가 계속 잘 판별해내니까 진짜와 정말 비슷한 데이터를 만들어내서 결국 판별자가 진짜인지 가짜인지 잘 판별하지 못하도록 만든다.

GAN은 두 개의 모델(생성자, 판별자)이 존재하기 때문에 하나의 모델만 존재하는 다른 신경망들에 비해 학습이 까다롭다는 특징이 있다.

GAN 작동원리

image

이미지출처: https://www.codetd.com/ko/article/8579237

1) 생성자는 Random한 noise로부터 가짜 데이터를 생성한다.

2) 판별자가 이 데이터가 진짜 데이터인지 이진분류를 수행한다.

3) 학습을 거듭하면서 Random한 noise가 점점 진짜 데이터와 비슷해진다.

4) 결국 판별자가 진짜와 가짜 데이터를 잘 구분하지 못한다.(Accuracy 0.5)

image

이미지출처: http://mcheleplant.blogspot.com/2019/03/gan-generative-adversarial-networks.html

생성자가 생성해내는 데이터는 확률분포(꼭 원본의 확률 분포가 아니여도 되고, 이 확률분포를 학습시키면서 원본의 확률분포와 비슷하게 함)내에서 Random한 값을 가져온다. Random Noise라고 해서 아예 Random한 값이 아니라 확률분포 내의 값을 가져오고 이 확률분포 내의 아무 값을 랜덤하게 가져와서 Random noise 라고 한다. 이 생성자의 분포는 학습을 거듭할수록 실제 데이터의 분포와 비슷하게 된다. 파란색 점선인 판별자는 이진분류이기 때문에 시그모이드와 비슷한 형태를 띄고 학습이 잘 되면 데이터가 일치해서 결국 0과 1일 확률이 0.5가 되기 때문에 직선으로 표현된다.

생성자는 단순히 노이즈를 추가하는 모델이기 때문에 비교할 대상이 없어서 비지도 학습, 판별자는 이 생성한 데이터를 실제 데이터와 비교하기때문에 지도학습이다.

GAN의 손실함수

image

이미지출처: https://m.blog.naver.com/PostView.naver?blogId=euleekwon&logNo=221558014002&targetKeyword=&targetRecommendationCode=1

\[log(1) = 0\] \[log(0) = -infinity\]

손실함수인 위식을 보면 판별자 D의 관점에서는 V(D,G)가 max가 되어야하고, 생성자 G의 관점에서는 min이 되어야 가장 이상적인 손실이 발생한다. x는 실제 데이터, z는 생성자가 생성한 데이터를 나타내는데 판별자는 생성된 데이터는 가짜로 D(G(z)), 실제 데이터는 진짜로 D(x) 판별하기 때문에 x와 z에 대한 손실이 모두 필요하고, 반면 생성자는 실제 데이터의 여부와는 관계없이 자신이 생성해낸 데이터가 판별자를 헷갈리게 하는 것이기 때문에 z에 대한 손실만 고려하면 된다. 즉, D의 관점에서는 log(D(x))와 log(D(G(z)))가 포함된 항이 모두 필요하고, G의 관점에서는 z가 포함된 D(G(z)))의 항만 필요하다.

판별자 D의 최종적인 목표는 실제 데이터는 1(D(x) = 1)로, 가짜 데이터는 (D(G(x)) = 0)으로 판별하는 것이다. 따라서 왼쪽에 있는 항은 log(D(x))가 log(1)이 되어서 0이 되어야하고, 오른쪽 항도 log((1-D(G(z))))가 log(1-0)이 되어서 0이 되어야한다. 여기서 헷갈리는 점은 log(1)은 0이 되는데 판별자는 D의 관점에서 V(D,G)를 최대화시켜야 한다는 것이다. 하지만, 실제 데이터와 생성해낸 데이터는 0과 1사이의 값으로만 이루어져있고 log0은 -무한대, log1은 0이기 때문에 이 사이에서 가장 큰 수는 결국 왼쪽과 오른쪽 항을 모두 0으로 만들어서 결국 max V(D,G)가 0이 되어야하는 것이다. D가 만들어야할 가장 나은 선택이 0이 되는 것이다.

반대로 G의 목표는 판별자가 가짜 데이터를 진짜 데이터와 헷갈려서 진짜 데이터로 판별하는 것이다. G의 관점에서는 D(x)가 고려되지 않아서 D(G(z))가 포함된 오른쪽 항으로만 손실을 계산하게 되는데 판별자가 가짜 데이터를 진짜로 판단하는 것은 D(G(z))) 가 1이 되어야하는 것이다. 결국 log(1-1) 은 -무한대가 되기 때문에 최대한 이 -무한대를 만들기 위한, 즉, V(D,G)를 최소화시켜야 하기 때문에 min을 사용한다.

정리하자면, 판별자의 손실함수는 진짜데이터의 손실(real_loss) + 가짜 데이터의 손실(fake_loss)이다. 진짜를 얼마나 진짜라고 잘 맞췄고, 가짜를 얼마나 가짜라고 잘 맞췄는지. 따라서, 판별자에게 real_loss는 전체가 1인 행렬과(진짜로 이루어진 행렬) real_output과 차이, fake_loss는 전체가 0인 행렬(가짜로 이뤄진 행렬)과 fake_output 과의 차이의 합으로 손실을 계산한다.

생성자는 가짜만 만들어내기 때문에 real_loss를 따로 구하진 않고, 전체가 1인 행렬(판별자가 가짜를 진짜라고 판단하게끔)과 fake_output의 차이를 손실함수로 사용한다.

GAN에서 특이한 점은 정확도가 50%일 때(판별자가 진짜인지, 가짜인지 구분을 못해서 아무거나 막 찍을 때)가 가장 좋은 모델이 된다는 것이다.

GAN 코드 실습

GAN을 통해 MNIST 손글씨 데이터를 생성해보자. 이번 예제에서는 Convolution Layer로 이루어진 DCGAN(Deep Convolution GAN) 모델을 사용해보자.

from tensorflow.keras import layers
from IPython import display

import glob
import imageio
import os
import PIL
import time

import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
# 테스트 데이터 굳이 안 필요함
(train_images, train_labels), (_, _) = tf.keras.datasets.mnist.load_data()
print(train_images.shape) # (60000, 28, 28)
 # 합성곱을 위한 채널추가
train_images = train_images.reshape(train_images.shape[0], 28, 28, 1).astype('float32')
train_images = (train_images - 127.5) / 127.5 # 이미지를 [-1, 1]로 정규화
BUFFER_SIZE = 60000
BATCH_SIZE = 256

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

1) 모델 구축하기

  • 생성자(Generator) 모델

Random noise로부터 이미지를 생성하기 위해 Transpose convolution을 사용한 Upsampling.

첫 Dense 층에는 Random Noise를 입력받는다. 그 후 최종 이미지 사이즈인 28 * 28이 나오도록 Conv2DTranspose를 쌓는다.

은닉층의 활성화 함수는 LeakyReLU를 사용하고, 활성화 함수 이전에는 학습을 더 잘 시키기 위해 배치 정규화를 적용한다.

# 모델 구축함수

def make_generator_model():
    model = tf.keras.Sequential()
    model.add(layers.Dense(7*7*256, use_bias=False, input_shape=(100,))) # 100차원의 랜덤 노이즈를 입력 받고
    # 7*7*256의 사이즈로 키워준다.
    model.add(layers.BatchNormalization())
    model.add(layers.LeakyReLU())

    model.add(layers.Reshape((7, 7, 256))) # 7*7, batch size 256으로 키워줌

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

    return model



  • 판별자(Discriminator) 모델

판별자는 합성곱 신경망 기반의 이미지 분류기이고, 여기서는 Pooling을 따로 사용하진 않았다.

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])) # 생성자에서 넘어온 이미지 input
    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
cross_entropy = tf.keras.losses.BinaryCrossentropy(from_logits=True)
#판별자의 손실함수

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
#생성자의 손실함수

def generator_loss(fake_output):
    return cross_entropy(tf.ones_like(fake_output), fake_output)
#생성자, 판별자 모델 만들기
generator = make_generator_model()
discriminator = make_discriminator_model()
#옵티마이저 정의
generator_optimizer = tf.keras.optimizers.Adam(1e-4)
discriminator_optimizer = tf.keras.optimizers.Adam(1e-4)
checkpoint_dir = './training_checkpoints'
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt")
checkpoint = tf.train.Checkpoint(generator_optimizer=generator_optimizer, # 여기서 그냥 'adam'으로 하면 안 됨.
                                 discriminator_optimizer=discriminator_optimizer,
                                 generator=generator,
                                 discriminator=discriminator)
EPOCHS = 50
noise_dim = 100 # 랜덤노이즈 차원. 위에서 input shape을 100으로 정의
num_examples_to_generate = 16 # 총 16개의 손글씨
 
seed = tf.random.normal([num_examples_to_generate, noise_dim])
# 모델을 만들고, 경사하강법 적용
@tf.function
def train_step(images):
    noise = tf.random.normal([BATCH_SIZE, noise_dim]) # 케라스에서 자동으로 확률분포를 반영해서 noise를 생성해낸다.

    with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape:
        generated_images = generator(noise, training=True) # 위에서 만든 모델에 노이즈 추가, 학습 시작

        real_output = discriminator(images, training=True) # images는 실제 데이터
        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):

    # 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):
    for epoch in range(epochs):
        start = time.time()

        for image_batch in dataset:
            train_step(image_batch) # 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)

각 에포크마다 이미지를 불러와서 gif파일을 만든 결과. 형태를 알 수 없는 noise에서 시작해서 에포크가 커질수록 점점 더 원래 이미지와 비슷한 형태로 손글씨가 변하는 것을 알 수 있다.

dcgan

CycleGAN

image

이미지 출처: https://www.tensorflow.org/tutorials/generative/cyclegan?hl=ko

CycleGAN은 위에서 사용했던 DCGAN의 구조를 변경하여 만든 새로운 신경망이다. 일반적인 GAN은 실제 데이터가 존재하고, 그 데이터와 유사한 데이터를 생성자가 만들 수 있도록 학습이 되었는데 CycleGAN은 특징 데이터의 특징을 다른 데이터에 적용하는 작업을 할 수 있게 한다. 위의 그림처럼 실제 이미지와 아예 똑같은 데이터가 아니라, 그 이미지의 특징을 파악해서 색을 바꾸거나 패턴을 적용해서 비슷한 이미지를 만들어내는 것이다.

DCGAN과 비슷한 작업을 수행하는 Pix2Pix라는 모델에서는 학습 데이터셋을 만들 때 input Data와 레이블(실제 데이터)에 해당하는 데이터를 무조건 짝지어 주어야 했다. 예를 들어, 100가지의 흑백 이미지를 입력으로 사용하고 이를 컬러 이미지를 바꾸려면 입력에 사용된 흑백 이미지와 똑같은 컬러 이미지 100개를 각 이미지에 맞게 매칭시켜주어야 한다. 하지만, 모든 데이터를 다 매칭시키는 것은 데이터의 수가 많아질수록 불가능해질 것이고, 따라서 CycleGAN은 두 이미지가 전혀 다른 이미지더라도 레이블에 사용된 이미지의 특징만 파악한다면 입력된 이미지에 그 특징을 적용시켜서 비슷한 이미지를 출력할 수 있기 때문에 성능이나 유용성 면에서 더 좋다는 장점이 있다. 또한, input과 레이블의 이미지 매칭으로부터 자유로워져서 학습 데이터셋을 구하기도 쉽다.

  • CycleGAN의 원리

image

image

이미지 출처: https://hardikbansal.github.io/CycleGANBlog/

위의 그림은 갈색말과 얼룩말 이미지의 스타일을 서로 변환하는 CycelGAN의 학습방식이다. 일반적인 GAN과 다르게 CycleGAN에서는 생성자(Generator A2B, Generator B2A)와 판별자(Discriminator A, Discriminator B)가 각각 2개씩 필요하다. 두 사진 중 위의 사진을 보면, 생성자는 갈색말(A)에서 얼룩말(B)로 이미지를 변경하고 생성된 얼룩말 이미지인 B를 판별자를 통해 판별한다. 그 후 이 B 이미지를 다시 갈색말(A) 이미지로 바꿔서 input과 같은 형태로 생성한다. A -> B와 B -> A의 이미지가 생성된다.

아래 사진에서는 얼룩말(B) 이미지를 입력으로 사용해서 갈색말(A) 이미지로 변경시키고, 이 A 이미지를 판별한다. 또한, 위의 사진과 같은 방법으로 생성된 A이미지를 다시 입력에 사용된 B 이미지로 변경시킨다. B -> A, A -> B

CycleGAN은 비슷한 이미지에 대해 1대1로 매칭을 시켜 이미지를 학습하는 것이 아니라서 A에서 B로 또는 B에서 A로 이미지를 변환하면 input에 사용되었던 이미지의 정보가 손실되어 입력 데이터의 특징을 잃어버릴 수 있다. 따라서, 원본 데이터로 돌아갈 수 있는 정도로만 변환 시키기 위해 변환된 데이터를 판별 후 굳이 다시 입력에 사용되었던 데이터의 형태로 변환시켜서 입력 데이터의 특징을 잃어버리지 않는 방법을 사용한다. 이렇게 다시 입력 데이터로 돌아가는 순환구조로 이루어져있어서 CycleGAN이라는 명칭을 사용한다.

Categories:

Updated: