Open In Colab

GAN (Generative Adversarial Network) 생산적 적대 신경망이란?

GAN을 간단히 설명하면 저번에 공부했던 오토인코더의 뒷부분만 뗀것과 유사합니다.

임의에 특성들로부터 이미지 등을 복구한 디코더를 얘기합니다. 다만 이 구조로는 원래 데이터를 모르기 때문에 학습이 불가능한데요.

image.png

Discriminator(구별자)를 사용해서 학습을 진행하게 됩니다.

자세히 설명하면 실제 데이터 내에서 뽑고, Generator(생성자)를 이용해서 유사 데이터를 뽑습니다.

그 뒤 Discriminator를 이용해서 주어진 데이터가 실제 데이터인지 Generator를 이용해서 만든 가짜 데이터인지 구분합니다.

이 과정을 반복해서 Generator가 생성하는 데이터를 실제 데이터와 유사하게 만들어서 Discriminator가 구분하지 못할 정도로 만드는 것이 목표입니다.

image.png

방금 말한 이론을 현실세계에 간단한 예시를 통해 관찰하겠습니다. 지폐위조범과 경찰의 관계입니다.

지폐위조범이 가짜돈을 찍어냅니다 -> 그럼 경찰이 이걸 보고 가짜인지 진짜인지 구별합니다.

이때 처음 찍어낸 돈은 품질이 안좋아서 경찰이 쉽게 가짜를 구별합니다.

하지만 지폐위조범은 점점 더 정교하게 가짜돈을 만들고, 경찰은 그걸 보고 계속 돈을 구분할 것입니다.

이 상황에서 경찰은 최대한 가짜 돈을 잘 구분하려고 노력하겠죠.

반복되면 경찰이 지폐위조범이 만든 돈을 구분 못하는 상황이 올겁니다. 가짜 돈을 잘 만드는 생성자를 만들어 낸 것입니다.

(참고자료 : https://lifeignite.tistory.com/53)

GAN단한 실습

데이터 불러오기

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers

import matplotlib.pyplot as plt
import numpy as np

import time

(train_images, train_labels), (test_images, test_labels) = tf.keras.datasets.mnist.load_data()
train_images.shape
(60000, 28, 28)

텐서플로 패키지를 다운하고, 텐서플로우 내 mnist 데이터를 불러옵니다.

테스트 데이터의 개수는 6만개이고 28 * 28 형태이며 값은 0~255 사이인 이미지 데이터 입니다.

train_images = train_images.reshape(train_images.shape[0], 28, 28, 1).astype('float32')
train_images = (train_images - 127.5) / 127.5  # 값이 0~255에 존재하는데, -1~1 사이 데이터로 변환해줍니다.

test_images = test_images.reshape(test_images.shape[0], 28, 28, 1).astype('float32')
test_images = (test_images - 127.5) / 127.5

입력된 데이터를 형변환하고, 값을 -1 ~ 1 내에 존재하도록 전처리합니다.

BUFFER_SIZE = 60000
BATCH_SIZE = 128

train_dataset = tf.data.Dataset.from_tensor_slices(train_images).shuffle(BUFFER_SIZE).batch(BATCH_SIZE)

텐서형태로 데이터를 변환해 넣어줍니다. 이때 데이터를 섞어주며, 배치사이즈로 분리합니다.

Generator(생성자), Discriminator(구별자) 모델 생성

inputs = keras.Input(shape=(100,))
x = inputs
x = layers.Dense(256)(x)
x = layers.LeakyReLU()(x)
x = layers.Dense(28*28, activation = 'tanh')(x)
outputs = layers.Reshape((28, 28))(x)

G = keras.Model(inputs, outputs)
G.summary()
Model: "model_5"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_6 (InputLayer)        [(None, 100)]             0         
                                                                 
 dense_10 (Dense)            (None, 256)               25856     
                                                                 
 leaky_re_lu_5 (LeakyReLU)   (None, 256)               0         
                                                                 
 dense_11 (Dense)            (None, 784)               201488    
                                                                 
 reshape_3 (Reshape)         (None, 28, 28)            0         
                                                                 
=================================================================
Total params: 227,344
Trainable params: 227,344
Non-trainable params: 0
_________________________________________________________________

100차원의 값을 입력받아 이미지 데이터와 같은 형태인 28 * 28 형태로 출력하는 모델을 만들었습니다.

더 자세히 살펴보면 100개의 입력에서 256개의 특성으로, 또 다음 층을 지나면 784개의 특성을 만들어내는 모델입니다.

이 모델은 Generator(생성자) 모델로, 가짜 이미지를 만드는 모델입니다.

inputs = keras.Input(shape=(28, 28))
x = layers.Flatten()(inputs)
x = layers.Dense(256)(x)
x = layers.LeakyReLU()(x)
x = layers.Dropout(0.3)(x)
outputs = layers.Dense(1)(x)

D = keras.Model(inputs, outputs)
D.summary()
Model: "model_6"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_7 (InputLayer)        [(None, 28, 28)]          0         
                                                                 
 flatten_2 (Flatten)         (None, 784)               0         
                                                                 
 dense_12 (Dense)            (None, 256)               200960    
                                                                 
 leaky_re_lu_6 (LeakyReLU)   (None, 256)               0         
                                                                 
 dropout_2 (Dropout)         (None, 256)               0         
                                                                 
 dense_13 (Dense)            (None, 1)                 257       
                                                                 
=================================================================
Total params: 201,217
Trainable params: 201,217
Non-trainable params: 0
_________________________________________________________________

28 * 28 형태의 이미지 데이터를 입력받아 256 차원으로 바꾼뒤, 최종 1개의 값을 내보내는 모델입니다.

이미지를 입력받아 실제 이미지인지, 생성자에서 만든 이미지인지 구분하는 Discriminator(구별자) 입니다.

test_noise = tf.random.normal([1, 100])
fake_image_test = G(test_noise, training = False)

plt.imshow(fake_image_test[0], cmap = 'gray')
<matplotlib.image.AxesImage at 0x7f44f725ee50>

임의에 값 100개를 사용하여 생성자 G 모델을 통과시켜서 28 * 28 이미지를 만들었습니다.

랜덤 값으로 만든 이미지이기 때문에 아무런 특이점이 없는 형태이죠.

decision = D(fake_image_test, training = False)
print(decision)
tf.Tensor([[0.4609413]], shape=(1, 1), dtype=float32)

방금 만든 이미지 데이터를 가지고 실제 데이터가 맞는지 판단하는 구별자 D 모델을 통과시킨 모습입니다.

아직 학습을 안시켰는데 우연인건지 0에 가까운 정확한 결과를 출력시켰네요.

모델 훈련

EPOCHS = 50
noise_dim = 100

seed = tf.random.normal([BATCH_SIZE, noise_dim])

G_optimizer = tf.keras.optimizers.Adam(1e-4)
D_optimizer = tf.keras.optimizers.Adam(1e-4)

에포크는 50으로, 시드값도 넣어주고 옵티마이저는 생성자 구별자 모두 Adam을 사용했습니다.

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

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

def D_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, 가짜 이미지를 의미하는 값은 0인데요.

생성자는 최대한 생성한 이미지가 판별자에게 1값을 배출하는 이미지를 만들기 위해 파라미터를 업데이트 해야합니다.

구별자는 최대한 진짜 이미지는 1, 생성자가 만든 이미지는 0값을 출력해주도록 파라미터를 업데이트 해야합니다.

두 모델의 최종 목적이 상충되는 것이 있는데, 두 모델을 경쟁시키며 둘 다 우수한 모델이 되는 것이 목표입니다.

@tf.function # 데코레이션?
def train_step(real_images):
    # 100개의 변수는 랜덤값을 계속 줌. 이건 중요한 값이 아닙니다.
    noises = tf.random.normal([BATCH_SIZE, noise_dim])
    
    with tf.GradientTape() as gen_tape, tf.GradientTape() as dsc_tape:
        # 생성자로 가짜 이미지를 생성합니다.
        fake_images = G(noises, training = True) 

        # 진짜 이미지를 구별자에 학습시킵니다.
        real_output = D(real_images, training = True)
        # 가짜 이미지를 구별자에 학습시킵니다.
        fake_output = D(fake_images, training = True)

        # 로스를 출력합니다.
        gen_loss = G_loss(fake_output)
        dsc_loss = D_loss(real_output, fake_output)
    
    # 로스 값을 통해 각 모델의 파라미터를 개산해줍니다.
    gen_gradients = gen_tape.gradient(gen_loss, G.trainable_variables)
    dsc_gradients = dsc_tape.gradient(dsc_loss, D.trainable_variables)

    # 옵티마이저를 이용해 파라미터를 업데이트 해줍니다.
    G_optimizer.apply_gradients(zip(gen_gradients, G.trainable_variables))
    D_optimizer.apply_gradients(zip(dsc_gradients, D.trainable_variables))

배치 한 개의 값이 입력됬을때 수행하는 일을 모은 train_step 함수를 만듭니다.

gen_losses = []
dsc_losses = []

def test_step(real_images):
    noises = tf.random.normal([BATCH_SIZE, noise_dim])

    fake_images = G(noises, training = False)

    real_output = D(real_images, training = False)
    fake_output = D(fake_images, training = False)

    gen_loss = G_loss(fake_output)
    gen_losses.append(gen_loss)
    dsc_loss = D_loss(real_output, fake_output)
    dsc_losses.append(dsc_loss)

    print('Generator loss:', gen_loss.numpy(), ', Discriminator loss:', dsc_loss.numpy())

테스트 하는 부분도 비슷하게 test_step 함수를 만듭니다.

def train(dataset, epochs):
    for epoch in range(epochs):
        start = time.time()

        for i, image_batch in enumerate(dataset):
            train_step(image_batch)
            if i == 0:
                test_step(image_batch)
        
        print ('Time for epoch {} is {} sec'.format(epoch + 1, time.time()-start))

앞서 정의한 train_step과 test_step 함수를 작동시키는 train 함수를 만듭니다.

train(train_dataset, EPOCHS)
Generator loss: 0.71649 , Discriminator loss: 1.5534838
Time for epoch 1 is 7.659214973449707 sec
Generator loss: 1.4269571 , Discriminator loss: 0.34772688
Time for epoch 2 is 6.5700695514678955 sec
Generator loss: 1.5954232 , Discriminator loss: 0.30279088
Time for epoch 3 is 8.152190446853638 sec
Generator loss: 1.36361 , Discriminator loss: 0.4483151
Time for epoch 4 is 6.410968065261841 sec
Generator loss: 1.165875 , Discriminator loss: 0.6234964
Time for epoch 5 is 6.4502575397491455 sec
Generator loss: 1.0780177 , Discriminator loss: 0.6841619
Time for epoch 6 is 6.431274652481079 sec
Generator loss: 1.3505816 , Discriminator loss: 0.5721281
Time for epoch 7 is 6.4662909507751465 sec
Generator loss: 1.2779344 , Discriminator loss: 0.62072957
Time for epoch 8 is 6.369911193847656 sec
Generator loss: 1.5677459 , Discriminator loss: 0.4794592
Time for epoch 9 is 6.448152780532837 sec
Generator loss: 1.8098621 , Discriminator loss: 0.37730467
Time for epoch 10 is 6.668363332748413 sec
Generator loss: 1.7685666 , Discriminator loss: 0.46769577
Time for epoch 11 is 9.194777011871338 sec
Generator loss: 1.5252336 , Discriminator loss: 0.56575656
Time for epoch 12 is 7.199241399765015 sec
Generator loss: 1.7163453 , Discriminator loss: 0.44394356
Time for epoch 13 is 6.342915296554565 sec
Generator loss: 1.5267828 , Discriminator loss: 0.4659177
Time for epoch 14 is 6.351556777954102 sec
Generator loss: 1.8910414 , Discriminator loss: 0.46936297
Time for epoch 15 is 6.366145372390747 sec
Generator loss: 1.5646715 , Discriminator loss: 0.50378096
Time for epoch 16 is 6.389109134674072 sec
Generator loss: 1.7325975 , Discriminator loss: 0.5011499
Time for epoch 17 is 6.3877105712890625 sec
Generator loss: 1.6206157 , Discriminator loss: 0.53547317
Time for epoch 18 is 6.335543632507324 sec
Generator loss: 1.4234588 , Discriminator loss: 0.6910653
Time for epoch 19 is 6.378859043121338 sec
Generator loss: 1.5831845 , Discriminator loss: 0.62829065
Time for epoch 20 is 6.430606842041016 sec
Generator loss: 1.8133811 , Discriminator loss: 0.4704691
Time for epoch 21 is 6.33375358581543 sec
Generator loss: 1.5101905 , Discriminator loss: 0.6232215
Time for epoch 22 is 6.405717372894287 sec
Generator loss: 1.4687159 , Discriminator loss: 0.6265195
Time for epoch 23 is 6.3467371463775635 sec
Generator loss: 1.723053 , Discriminator loss: 0.49364492
Time for epoch 24 is 6.364705801010132 sec
Generator loss: 1.610209 , Discriminator loss: 0.63721573
Time for epoch 25 is 6.332318067550659 sec
Generator loss: 1.7356936 , Discriminator loss: 0.52721524
Time for epoch 26 is 6.364203453063965 sec
Generator loss: 1.9121637 , Discriminator loss: 0.5568584
Time for epoch 27 is 6.319864273071289 sec
Generator loss: 1.6766417 , Discriminator loss: 0.5997259
Time for epoch 28 is 6.387562990188599 sec
Generator loss: 1.75248 , Discriminator loss: 0.62925696
Time for epoch 29 is 6.367716312408447 sec
Generator loss: 1.7534504 , Discriminator loss: 0.5443964
Time for epoch 30 is 6.331058979034424 sec
Generator loss: 1.5471501 , Discriminator loss: 0.6470097
Time for epoch 31 is 6.286182641983032 sec
Generator loss: 1.5621777 , Discriminator loss: 0.6091302
Time for epoch 32 is 6.333756446838379 sec
Generator loss: 1.6764431 , Discriminator loss: 0.6616212
Time for epoch 33 is 6.318872690200806 sec
Generator loss: 1.4045196 , Discriminator loss: 0.7134573
Time for epoch 34 is 6.299607038497925 sec
Generator loss: 1.4713526 , Discriminator loss: 0.7262584
Time for epoch 35 is 6.3872082233428955 sec
Generator loss: 1.3149565 , Discriminator loss: 0.8259001
Time for epoch 36 is 6.349704265594482 sec
Generator loss: 1.110297 , Discriminator loss: 0.9021453
Time for epoch 37 is 9.245652437210083 sec
Generator loss: 1.3754975 , Discriminator loss: 0.82389057
Time for epoch 38 is 6.385930061340332 sec
Generator loss: 1.1597505 , Discriminator loss: 0.8702109
Time for epoch 39 is 6.327707529067993 sec
Generator loss: 1.2481728 , Discriminator loss: 0.8911562
Time for epoch 40 is 6.268001079559326 sec
Generator loss: 1.3623071 , Discriminator loss: 0.8037815
Time for epoch 41 is 6.32326602935791 sec
Generator loss: 1.0813665 , Discriminator loss: 0.92148185
Time for epoch 42 is 6.305378437042236 sec
Generator loss: 1.1077981 , Discriminator loss: 0.82926035
Time for epoch 43 is 6.32554030418396 sec
Generator loss: 1.3075023 , Discriminator loss: 0.87365437
Time for epoch 44 is 6.315555810928345 sec
Generator loss: 1.1911769 , Discriminator loss: 0.8892145
Time for epoch 45 is 6.272730350494385 sec
Generator loss: 1.1509132 , Discriminator loss: 0.94256556
Time for epoch 46 is 6.288074493408203 sec
Generator loss: 1.207123 , Discriminator loss: 0.82477653
Time for epoch 47 is 6.279074430465698 sec
Generator loss: 1.1665838 , Discriminator loss: 0.8166743
Time for epoch 48 is 6.324838876724243 sec
Generator loss: 1.5069995 , Discriminator loss: 0.73425233
Time for epoch 49 is 6.327970266342163 sec
Generator loss: 1.2253973 , Discriminator loss: 0.951316
Time for epoch 50 is 6.335601329803467 sec

결과 관찰

epo = range(1,EPOCHS + 1)

plt.plot(epo, gen_losses)
plt.plot(epo, dsc_losses)
plt.show()

두 로스 값이 에포크마다 어떻게 변하는지 관찰해봤습니다. 파란색이 생성자 로스, 노란색이 구별자 로스입니다.

다른 딥러닝 로스 값은 에포크가 진행될 때 마다 계속 감소하는 모양을 띄거나 과적합이 되면 더 이상 감소되지 않는 모양을 보입니다.

반면 이 두 모델의 로스 값은 다소 특이하죠. 로스 값이 증가와 감소를 계속 반복합니다.

생성자가 계속해서 구별자를 햇갈리계 하면서 빈틈을 노리고, 구별자는 진화하는 생성자에 대항하기 위해 업데이트 되는 재밌는 현상이네요.

noises = tf.random.normal([50, 100])
generated_image = G(noises, training = False)

fig, axes = plt.subplots(nrows = 3, ncols = 2, figsize = (10, 10))

for ax in axes.flat:
    ax.axis('off')

axes[0,0].imshow(generated_image[0], cmap = 'gray')
axes[0,1].imshow(generated_image[1], cmap = 'gray')
axes[1,0].imshow(generated_image[2], cmap = 'gray')
axes[1,1].imshow(generated_image[3], cmap = 'gray')
axes[2,0].imshow(generated_image[4], cmap = 'gray')
axes[2,1].imshow(generated_image[5], cmap = 'gray')

plt.show()

이미지가 원하는 만큼까진 아니지만 어느정도 MNIST 그림과 유사하려고 노력한 것 같습니다.

심플한 GAN모델을 사용했기 때문이며, 더 깊은 층을 쌓거나 비지도 학습에서 벗어나 label 정보를 더 준다면(숫자 값) 더 정교한 가짜 이미지도 만들 수 있을 것입니다.

출처 : https://dataplay.tistory.com/39

느낀점

딥러닝의 개넘, 구조는 많이 학습했지만 텐서플로/파이토치 같은 도구들을 활용하는데 아직 어색합니다.

때문에 GAN이라는 더 응용된 분야를 마음껏 학습하는데 막히는 것이 많았습니다. 오늘 공부한 내용은 GAN이 무엇인지, 간단한 실습 정도 해봤는데요.

개인적으로 아쉽습니다. 더 디테일하게 공부하지 못해서. 빨리 텐서플로/파이토치와 친해져야겠다는 생각이 들었습니다.

오늘 느낀것은 딥러닝구조를 잘 이해만 한다면 이런식의 응용이 또 있을 수 있겠다는 생각이 들었습니다.

딥러닝 기술을 어떻게 응용할지 어느 산업에서 일을 하던 끊임없이 생각하도록 노력해야겠습니다.