반응형

VAE로 이미지의 특성을 뽑아서, 이미지에 변형을 가하며 새로운 이미지를 생성할 수 있다. 웃지 않는 사진에 웃음을 추가한다던지, 선글라스를 씌운다던지 할 수 있다는 것을 할 수 있다. 

또 다른 생성 모델은 'GAN'이다. Generative Adversarial Network, (생성적 적대 신경망)이라고 하는데, 처음 논문이 나왔을 때 아주 혁신적인 아이디어로 세간의 이목을 집중시켰다고 한다.

GAN은 생성자와 판별자, 두 개의 신경망으로 이루어진 신경망이다. 생성자와 판별자는 서로 경쟁하는 구도를 가지는데, 각각의 역할이 존재한다. 판별자는 이미지가 Input으로 들어오면 이 이미지가 원래 있던 이미지인지(target = 1), 아니면 새로 생성된 이미지인지 (target = 0)을 예측하는 역할을 한다. 생성자는 랜덤한 노이즈가 주어지면, 이를 이용해서 이미지를 생성하는 역할을 한다. 이렇게 생성된 이미지는 판별자에게 주어져서, 판별자는 이 이미지의 진위여부를 판단하게 된다. 이 두 모델은 서로 계속해서 학습하며 판별자는 더더욱 생성된 이미지를 잘 구분할 수 있게 되고, 생성자는 더더욱 판별자가 잘 구분할 수 없는 '진짜 같은' 이미지를 만들어내게 된다. 

지금 들어도 예측 모델링만 하던 내가 이 아이디어를 듣고 와 정말 신기하다. 여기서 이렇게 할 수도 있구나 라는 생각을 했었다. 이번에는 camel numpy 파일을 이용해 직접 GAN을 만들어보고, 새로운 데이터를 생성해보자. 

camel numpy 데이터는 구글의 'Quick, Draw!' 데이터셋의 일부이다. 이 데이터셋은 온라인 게임으로부터 수집되었는데, 유저가 그림을 그린 것을 신경망이 맞추는 게임이다. 이 결과물이 주제별로 묶어져 .npy 파일로 저장되어있다. 

http://bit.lly/30HyNqg 

여기서 다운로드 받을 수 있다. 

GAN의 원본 논문에서는 Convolution층 대신에 Fully-Connected 층을 사용했다고 한다. 하지만 그 이후로 합성곱 층이 판별자의 예측 성능을 크게 높여 준다고 밝혀진 이후로, 간단한 GAN 모델들도 합성곱 층을 사용하고 있다. 이러한 모델은 사실 DCGAN (Deep Convolution GAN)이라고 부르는 게 정확하지만, 합성곱 층이 너무나 많이 쓰이고 있어서 GAN이라고 해도 DCGAN의 의미를 포함하고 있다고 한다. 

 


 

1. 패키지 및 데이터 로드

import pandas as pd
import numpy as np
import keras
import keras.backend as K
from keras.layers import Conv2D, Activation, Dropout, Flatten, Dense, BatchNormalization, Reshape, UpSampling2D, Input
from keras.models import Model
from keras.optimizers import RMSprop
from keras.preprocessing.image import array_to_img

import warnings ; warnings.filterwarnings('ignore')

import matplotlib.pyplot as plt
from tqdm import tqdm

패키지를 로드하고, 

Xtrain = np.load('full-numpy_bitmap-camel.npy')
Xtrain = np.reshape(Xtrain, (len(Xtrain), 28, 28, 1))
Xtrain = Xtrain / 255

MNIST 데이터셋처럼 0~255의 픽셀 강도가 numpy로 저장되어있다. 

책과 다른 게시물에서는 -1 ~ 1로 값을 맞춰주는데, 그냥 귀찮아서 0 ~ 1 로 맞추어주었다. 그렇다면, 마지막 단계, 생성자의 이미지 출력물을 0~1 사이로 맞춰주는 sigmoid를 사용해야겠다. 

 


 

2. 판별자 만들기

disc_input = Input(shape=(28, 28, 1))

x = Conv2D(filters=64, kernel_size=5, strides=2, padding='same')(disc_input)
x = Activation('relu')(x)
x = Dropout(rate=0.4)(x)

x = Conv2D(filters = 64, kernel_size=5, strides=2, padding='same')(x)
x = Activation('relu')(x)
x = Dropout(rate=0.4)(x)

x = Conv2D(filters=128, kernel_size=5, strides=2, padding='same')(x)
x = Activation('relu')(x)
x = Dropout(rate=0.4)(x)

x = Conv2D(filters=128, kernel_size=5, strides=1, padding='same')(x)
x = Activation('relu')(x)
x = Dropout(rate=0.4)(x)

x = Flatten()(x)
disc_output = Dense(units=1, activation='sigmoid', kernel_initializer='he_normal')(x)

discriminator = Model(disc_input, disc_output)
discriminator.summary()

discriminator의 앞 4자 disc를 사용해서 판별자를 만들었다. 

판별자를 만드는 코드를 간단히 설명하자면, 단순히 4번의 Convolution을 거쳐서, 해당 이미지가 참인지 거짓인지만을 판별한다. 마지막의 sigmoid 함수를 지나서 이 이미지가 진짜일 확률을 출력하게 된다. 

판별자 모델 요약

 

3. 생성자 만들기

gen_dense_size=(7, 7, 64)

gen_input = Input(shape = (100, ))
x = Dense(units=np.prod(gen_dense_size))(gen_input)
x = BatchNormalization()(x)
x = Activation('relu')(x)
x = Reshape(gen_dense_size)(x)

x = UpSampling2D()(x)
x = Conv2D(filters=128, kernel_size=5, padding='same', strides=1)(x)
x = BatchNormalization(momentum=0.9)(x)
x = Activation('relu')(x)

x = UpSampling2D()(x)
x = Conv2D(filters = 64, kernel_size=5, padding='same', strides=1)(x)
x = BatchNormalization(momentum=0.9)(x)
x = Activation('relu')(x)

x = Conv2D(filters=64, kernel_size=5, padding='same', strides=1)(x)
x = BatchNormalization(momentum=0.9)(x)
x = Activation('relu')(x)

x = Conv2D(filters=1, kernel_size=5, padding='same', strides=1)(x)
gen_output = Activation('sigmoid')(x)

generator = Model(gen_input, gen_output)
generator.summary()

gen_dense_size에 shape를 하나 주었는데, 이 값은 랜덤 노이즈로부터 처음 생성할 Raw 이미지의 사이즈이다. 이 Raw 이미지는 몇 번의 UpSampling2D와 Convolution2D 레이어를 거쳐서 원본 이미지와 같은 shape를 가진 이미지로 바뀐다. 여기서 Conv2DTranspose (전치 합성곱) 층 대신에 UpSampling2D를 사용했는데, 이 둘은 픽셀을 다루는 원리가 약간씩 다르다. Conv2D 같은 경우에는 합성곱 연산을 진행하기 전에 픽셀 사이에 0을 추가하는 반면, UpSampling2D는 각 행렬을 단순히 반복하여 이미지 사이즈를 두배로 만든다. Conv2DTranspose를 사용하면 이미지에 계단 모양이나 체크무늬 패턴을 만들어서 이미지 품질을 저하시킬 수 있다고 알려져 있지만, 뛰어난 GAN 논문에서 여전히 많이 사용된다. 따라서 두 방식을 모두 시도해보고 더 좋은 결과를 출력하는 방법을 사용하는 것이 바람직하다. 

생성자의 코드를 확인해보자면, 100차원의 랜덤 노이즈를 7 * 7 * 64 사이즈의 이미지로 만들고, 이를 키우면서 진행된다. 이전 레이어에서 받은 이미지를 UpSampling 하여 두 배로 크기를 늘리고, Convolution을 진행하고, 다시 UpSampling 하고, Convolution 하고를 반복한다. 마지막에는 sigmoid 함수를 적용시켜서 출력 결과물이 원본 데이터와 같은 스케일 안에 있게끔 만든다. 내가 시작할 때 Xtrain 데이터를 0 ~ 1 사이의 값으로 만들었기 때문에 다른 코드들처럼 tanh를 사용하지 않고 sigmoid 함수를 사용하는 것이 맞다고 판단했다. 

생성자 모델 요약

 


 

4. 모델 컴파일

discriminator.compile(optimizer=RMSprop(lr=0.0008), loss='binary_crossentropy', metrics=['accuracy'])

판별자를 컴파일해준다. 이미지의 진위를 판단하는 Binary 문제이므로 loss function은 binary_crossentropy를 사용하고, metrics 에는 정확도를 사용하여 모니터링한다. 

 

discriminator.trainable = False
model_input = Input(shape=(100, ))
model_output = discriminator(generator(model_input))
model = Model(model_input, model_output) 

model.compile(optimizer=RMSprop(lr=0.0004), loss='binary_crossentropy', metrics=['accuracy'])

이제 생성자를 컴파일해줘야 하는데, 생성자에서 생성한 이미지를 판별자에 넣고 그 이미지의 참 거짓을 판별하도록 하는 모델을 만들어서 컴파일을 한다. 어차피 이렇게 모델을 새로 컴파일해도, 그 안에 있는 생성자도 같이 컴파일되므로, 추후에 생성자만 이용해서 이미지를 생성하는 것이 가능하다. 

일반적으로 판별자가 더 강력해야, 생성자가 판별자의 학습을 따라가면서 점점 좋은 결과물을 생산해내기 때문에, 판별자의 learning rate의 절반으로 생성자를 컴파일해준다. 

이때 헷갈리는 것 중 하나가, 'discriminator.trainable = False'인데, 이 코드가 있어야 생성자에서 생성된 데이터를 학습하는 과정에서 판별자의 가중치가 업데이트되지 않는다. 이 trainable 속성은 모델이 컴파일할 때 작동하는데, 설명하자면 아래와 같다. 

 

1. 판별자를 컴파일할 때는 trainable=True로 되어있는 상태로 컴파일 진행. 
2. 판별자를 컴파일한 이후 trainable=False로 설정해주어도 이미 컴파일된 판별자에는 적용되지 않음. 
3. 새로 판별자를 컴파일한 모델을 만드는데, 이때는 아까 trainable=False로 해두었기 때문에 판별자의 가중치가 업데이트되지 않음. 하지만 기존에 판별자만 컴파일할 때 trainable=True 이므로, 판별자만 학습을 진행할 때는 가중치가 업데이트됨.

정도로 이해하면 된다. trainable 속성이 컴파일 시에 적용되는 건지를 모르고 있다가 이제야 알게 되었다. 

요약하자면, 새로 만든 model 객체는, 100차원의 데이터를 받아서 생성자에 입력하고, 생성자가 출력한 이미지를 판별자에 넣어서 이 이미지의 진위여부를 판별자로 하여금 예측하도록 한다. 

 


 

5. GAN 모델 훈련

GAN은 다른 것보다 훈련하는 아이디어가 핵심이다. 

1. 원본 데이터에서 랜덤하게 데이터를 추출하여 판별자 학습. (target = 1)

2. 학습되지 않은 생성자가 랜덤 노이즈를 이용해서 생성한 이미지로 판별자 학습 (target = 0)

을 해주면서 판별자는 원본 이미지와 생성된 이미지의 차이를 구분하게 된다. 생성자가 잘 훈련이 되려면, 이 이미지가 판별자의 input으로 주어졌을 때, 판별자가 1을 뱉어내기 시작한다면 학습이 잘 진행되고 있다는 의미이다. 

def train_discriminator(x_train, batch_size):
    valid = np.ones((batch_size, 1))
    fake = np.zeros((batch_size, 1))
    
    idx = np.random.randint(0, len(Xtrain), batch_size)
    true_imgs = Xtrain[idx]
    discriminator.fit(true_imgs, valid, verbose=0)
    
    noise = np.random.normal(0, 1, (batch_size, 100))
    gen_imgs = generator.predict(noise)
    
    discriminator.fit(gen_imgs, fake, verbose=0)
    
def train_generator(batch_size):
    valid = np.ones((batch_size, 1))
    noise = np.random.normal(0, 1, (batch_size, 100))
    model.fit(noise, valid, verbose=1)

코드를 요약하자면, 판별자 학습 함수, 생성자 학습 함수를 만든다. 

판별자 학습 함수에서는, 원본 데이터에서 배치 사이즈만큼 랜덤하게 뽑아와서 타겟 1 (진짜)를 주고 학습을 시키고, 생성자가 만들어낸 이미지에다가는 타겟 0 (가짜)를 주고 학습을 시킨다. 생성자를 훈련할 때 이 생성자는 점점 진짜 같은 이미지를 생성하게 되므로 이렇게 학습을 반복해주면 판별자는 원본 이미지와 만들어진 이미지를 더욱 잘 구분할 수 있게 된다. 

학습은 다음과 같은 과정의 반복으로 이루어진다. 

1. 판별자는 원본 이미지를 가져와서 1로, 생성된 이미지를 0으로 학습한다. 

2. 생성자는 랜덤 노이즈로부터 이미지를 1이라고 지정하고 학습하며, 가중치를 업데이트한다. 

3. 판별자는 다시 원본 이미지와 (업데이트된) 생성자가 생성한 이미지를 가지고 다시 원본 이미지와 생성된 이미지를 각각 1, 0으로 학습한다. 

판별자는 원본과 생성 이미지를 정확히 구분하면 loss 가 낮아진다. 반면 생성자의 목표는 판별자의 loss를 높이는(원본과 생성이미지를 구분하지 못하도록)것이 목표이다. model 객체에서 표시되는 loss는, 생성된 이미지에 대한 판별자의 loss를 의미하므로, 학습이 잘 진행될수록 이 결과는 점점 높아질 것이다. 

배치 사이즈 64로, 2000 에포크만큼 학습을 진행해보았다. 

for epoch in tqdm(range(2000)):
    train_discriminator(Xtrain, 64)
    train_generator(64)

학습 초반
학습 중반
학습 후반

학습이 진행될수록 'model' 객체에 표시되는 loss 가 점차적으로 증가하고 있다. 다시 한번 요약하자면, 여기에 표시되는 loss는 새로 생성된 이미지에 대한 판별자의 loss이기 때문에, 점점 증가해야 한다. 

 


 

6. 결과물 확인

original=array_to_img(Xtrain[0])
plt.imshow(original, cmap='gray')

원본 이미지

random_noise=np.random.normal(0, 1, (1, 100))
gen_result=generator.predict(random_noise)
gen_img=array_to_img(gen_result[0])
plt.imshow(gen_img, cmap='gray')

생성된 이미지

생성자가 어느 정도 학습이 되었다. 랜덤한 노이즈를 입력받으며 출발해서, 원본과 상당히 비슷한 낙타 모양으로 만들어냈다. 

보고 있는 책에서는 6000 에포크만큼 학습을 진행하였는데, 나도 해봐야겠다. 

반응형
Posted by Jamm_
반응형

VAE(Variational AutoEncoder)와 AE(AutoEncoder)는 구조상으로는 비슷하지만 의미적인(?) 측면에서 약간의 차이가 있다. 일반적인 오토인코더는 입력된 데이터를 저차원으로 최대한 잘 압축하는 방법에 그 초점이 맞춰져 있다면, VAE는 입력 데이터를 저차원으로 잘 압축시키고, 그 특성에 따라 새로운 데이터를 잘 생성해 내는 것에 초점이 더 맞춰져 있다. 

AE의 문제라고 한다면, AE의 결과물은 잠재 공간의 한 포인트가 되므로, 잠재 공간이 연속적으로 만들어지지 않는다는 것이다. 만약 인코딩된 결과물 (2, -2)가 숫자 4의 이미지로 완벽하게 디코딩이 되더라도, 포인트 (2.1, -2.1) 이 4와 비슷한 이미지로 디코딩될 것이라는 보장은 그 어디에도 없다. 그럴 필요도 없고. 잠재 공간이 2차원이라면 이런 문제는 상당히 덜해지지만, 잠재 공간의 차원이 늘어날수록 이 문제는 더더욱 심해지게 된다. 

VAE는 여기서 출발하는데, AE와 가장 직접적인 차이점은 VAE는 이미지를 잠재 공간의 한 점으로 직접 매핑시키는 것이 아니라, (그 포인트의 주변까지도 약간씩 포함 할 수 있도록) 그 점 주변의 다변수 정규 분포 함수에 매핑한다는 것이다. 이렇게 되면 인코딩 결과물 (2,-2)가 숫자 4의 이미지로 정확히 디코딩될 수 있다면, (2.1, -2.1) 역시 점 (2, -2)와 상당히 비슷한 위치에 있기 때문에 숫자 4와 상당히 비슷한 디코딩 결과를 낸다고 추론할 수 있다. 

요약하자면, VAE의 인코더는 입력된 이미지를 잠재 공간의 (한 포인트를 중심으로 하는) 다변수 정규 분포로 만들게 된다. 이미지 1을 일반적인 오토인코더로 인코딩한 결과가 (1, 1)이라면, VAE는 이 이미지 1을 (1, 1)을 중심으로 하는 (다변수) 정규 분포 함수를 만든다. 따라서 디코더는 이 (1, 1) 근처의 점들은 정규분포에 따라서 이미지 1 일 '확률' 이 높다고 판단하고, 이미지 1과 비슷한 형태를 띠도록 디코딩을 학습할 가능성이 높다. 하지만 점 (1, 1)과 멀리 떨어진 점 (100, 100)에 대해서는 정규분포에 따라서 이미지 1일 '확률' 이 높지 않다고 판단하고, 디코딩 결과물이 이미지 1과 비슷해지지 않도록 학습할 것이다. 

 

정규 분포를 정의하려면 두가지의 변수가 필요한데, 바로 평균과 분산이다. 따라서, 인코더는 입력된 이미지에 대하여, 평균과 분산, 두 가지 아웃풋을 낸다. 여기에다가 'epsilon' 항을 추가해서 정규 분포를 만드는데, epsilon 은 표준 정규분포에서 랜덤하게 샘플링 한 값이다. 이 epsilon의 역할은 '인코더가 만들어낸 다차원 정규분포에서 얼마나 떨어진 곳을 디코딩 시작 포인트로 잡을까'를 결정한다. 이 값이 결정되면 디코더는 인코더의 정확한 결과물 (인코더가 만들어낸 정규분포의 중심점) 이 아닌, 그 포인트 주변의 약간 떨어진 점으로부터 디코딩을 시작해야 하므로 한 번도 본 적 없는 포인트에 대해서 디코딩을 진행해야 한다. 하지만 그 시작점이 인코더의 정확한 결과물과 아주 크게 다르지는 않기 때문에 (epsilon도 정규분포를 따르므로, epsilon 값이 비정상적으로 큰 값이 나올 확률 매우 적다) 원본과 비슷한 결과물을 내야만 Loss 가 커지지 않을 것이다. 

책을 천천히 다시 읽어보고 이해한 결과를 정리했다. 머릿속으로 3차원을 넘어가는 그림이 그려지지는 않아서, (그게 됐다면 난 이런거 쓰고 있지 않았겠지?) 그리고 고등학교 수포자 이과생인 전력이 있는 나이기 때문에 정확한 수학적 이해나, 확률론(?)적으로 깔끔하게 설명할 수는 없지만, 굳이 이해를 하자면 이렇게 될 것이다. 

VAE에서는 잠재 공간의 차원 사이에는 어떤 상관관계가 없다고 가정하고 시작한다. 다시 말하면, 잠재 공간에서의 차원간의 상관계수는 0이고, 공분산 행렬은 계산할 필요가 없으며, 그러므로 인코더가 입력을 평균, 분산으로만 매핑할 수 있게끔 가정을 한다. 

VAE를 만드는 일은 생각보다 간단한데, 일반적인 오토인코더를 만든 후, 인코더가 정규분포를 만들 수 있도록 수정, 손실 함수가 정규분포를 인식할 수 있도록 수정해주면 된다. 단어가 조금 이상하다. 

대충 코드를 보자면 다음과 같다. 

 


 

1. VAE 만들기

import pandas as pd
import numpy as np
import keras
from keras.layers import Input, Conv2D, Flatten, Dense, Conv2DTranspose, Reshape, Activation, LeakyReLU, Dropout, BatchNormalization, MaxPooling2D, Lambda
from keras.models import Model
from keras import backend as K
from keras.optimizers import Adam
from keras.utils import to_categorical
from sklearn.metrics import mean_squared_error as mse

import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns

패키지 import 하고, 

(Xtrain, Ytrain), (Xtest, Ytest) = keras.datasets.mnist.load_data()
Xtrain=Xtrain/255
Xtest=Xtest/255

Xtrain=Xtrain.reshape(len(Xtrain), 28, 28, 1)
Xtest=Xtest.reshape(len(Xtest), 28, 28, 1)

Ytrain=to_categorical(Ytrain)
Ytest=to_categorical(Ytest)

데이터 불러와서 저장, 전처리

 

1.1. 인코더 만들기

#Encoder
encoder_input=Input(shape=(28,28,1))

x=Conv2D(filters=32, kernel_size=3, strides=1, padding='same')(encoder_input)
x=LeakyReLU()(x)

x=Conv2D(filters=64, kernel_size=3, strides=2, padding='same')(x)
x=LeakyReLU()(x)

x=Conv2D(filters=64, kernel_size=3, strides=2, padding='same')(x)
x=LeakyReLU()(x)

x=Conv2D(filters=64, kernel_size=3, strides=1, padding='same')(x)
x=LeakyReLU()(x)

shape_before_flatten=K.int_shape(x)[1:]
x=Flatten()(x)

#############################################################################
mu=Dense(units=2)(x)
log_var=Dense(units=2)(x)

def sampling(args):
    mu, log_var = args
    epsilon = K.random_normal(shape=K.shape(mu), mean=0, stddev=1)
    return mu + K.exp(log_var / 2) * epsilon

encoder_output=Lambda(sampling)([mu, log_var])
model_encoder = Model(encoder_input, encoder_output)
############################################################################
print(model_encoder.summary())

코드를 자세히 뜯어보면 윗부분은 똑같다. 복사해서 갖다썼으니까 똑같다.

다만 아랫부분 ##### 으로 표시한 부분이 약간 다른데, (이래서 함수형 API 쓰라고 하는구나) 몇 번의 Convolution Layer를 지난 데이터는 Flatten 이후에 두 경로로 가게 된다. 

한쪽은 mu(뮤, 평균)를  정하고, 다른 한쪽은 log_var(로그 분산)을 정한다. (분산은 항상 양수이므로 로그를 씌워도 괜찮다)

아래 공식으로 표준과 표준편차가 정의된 정규분포에서 특정 포인트를 샘플링할 수 있다. (표준편차는 분산의 양의 제곱근이므로 분산도 오케이다.) 공식의 epsilon은 표준 정규분포(평균=0, 표준편차=1 인 정규분포)를 따르는 임의의 점이다. 

코드의 Sampling 함수에 대한 설명

그리고 그 아래에 있는 'sampling' 함수는 위에서 만들어진 평균과, (로그) 분산으로 정의된 정규 분포(분포 A라고 하자)에서 epsilon값을 추가해 분포 A에서 한 포인트를 샘플링한다. 

인코더의 아웃풋 층은 뮤, 로그분산 두 가지를 입력으로 묶어서 입력받은 후, 샘플링 함수를 실행해서 샘플링된 포인트를 반환한다.

인코더를 이렇게 변경해주고, 디코더를 똑같이 연결해주면 VAE 설계의 첫 번째 단계가 끝난다. 

#Decoder
decoder_input=Input(shape=(2,))
x=Dense(units=np.prod(shape_before_flatten))(decoder_input)
x=Reshape(shape_before_flatten)(x)

x=Conv2DTranspose(filters=64, kernel_size=3, strides=1, padding='same')(x)
x=LeakyReLU()(x)

x=Conv2DTranspose(filters=64, kernel_size=3, strides=2, padding='same')(x)
x=LeakyReLU()(x)

x=Conv2DTranspose(filters=32, kernel_size=3, strides=2, padding='same')(x)
x=LeakyReLU()(x)

x=Conv2DTranspose(filters=1, kernel_size=3, strides=1, padding='same')(x)
x=Activation('sigmoid')(x)

decoder_output=x
model_decoder=Model(decoder_input, decoder_output)
print(model_decoder.summary())


#Connect Two Models
model_input = encoder_input
model_output = model_decoder(encoder_output)

AutoEncoder=Model(model_input, model_output)

 

1.2. 손실 함수 수정

인코더가 정규분포(에서 샘플링한 한 포인트) 를 내뱉도록 설계가 완료되었으면, 이제 손실 함수를 수정하면 된다. 기존의 AE에서는 픽셀 별로 값을 비교해서 구한 mean squared error 만 사용했다면, 여기서는 KL 발산(Kullback - Leibler Divergence, 쿨백 - 라이블러 발산)을 추가로 사용한다. 

KL발산에 대한 설명을 간단히 요약하자면, 한 확률분포가 다른 확률분포와 얼마나 다른지를 측정하는 방법이라고 할 수 있다. VAE가 생성해낸 평균 mu, 분산 log_var인 정규분포가 표준 정규분포와 얼마나 다른지를 측정해야 하기 때문에, 손실 함수(loss_function) 에 KL발산 항을 추가한다. 이때 KL 발산을 계산하려면 다음과 같이 하면 된다. 

kl_loss = -0.5 * sum( 1 + log_var - mu^2 - exp(log_var)

모든 차원에서 잠재공간상의 정규분포가 mu = 0, log_var = 0 일 때 kl_loss는 최소가 된다. 이렇게 추가한 KL 발산 항은 네트워크가 샘플을 표준 정규분포에서 크게 벗어나게 인코딩할 때 큰 페널티를 가하게 된다. 

optimizer=Adam(lr=0.0005)
r_loss_factor=1000   # This is a Hyperparameter

def vae_r_loss(y_true, y_pred):    ## MSE
    r_loss = K.mean(K.square(y_true-y_pred), axis=[1,2,3])
    return r_loss_factor * r_loss

def vae_kl_loss(y_true, y_pred):   ## KL-Divergence
    kl_loss= -0.5 * K.sum(1+log_var - K.square(mu) - K.exp(log_var), axis=1)
    return kl_loss

def vae_loss(y_true, y_pred): 
    r_loss=vae_r_loss(y_true, y_pred) #Loss of Decoder
    kl_loss = vae_kl_loss(y_true, y_pred) #Loss of Encoder
    return r_loss + kl_loss #Sum of these two


AutoEncoder.compile(optimizer=optimizer, loss= vae_loss, metrics=[vae_r_loss, vae_kl_loss])

새로 만들 VAE에서의 loss function은 여기서 정의한 'vae_loss' 함수를 이용한다. vae_loss 는 기존의 MSE 손실 함수와 KL 발산의 합으로 계산된다. MSE 손실 함수는 (원본 이미지 픽셀과 생성 이미지 픽셀을 비교하여) 디코더가 원본과 비슷한 이미지를 생성해내지 못할 시 더 큰 페널티를 주고, KL발산은 인코더가 표준 정규분포와 많이 다른 분포로 데이터를 인코딩할수록 더 큰 페널티를 주게 된다. 

여기서는 또 'r_loss_factor' 이라는 것이 추가되었는데, 이는 mse에 곱해주는 가중치로 KL 발산 손실과 균형을 맞추는 용도이다. 이 'r_loss_factor'는 하이퍼파라미터로, VAE를 훈련할 때 튜닝을 잘해주어야 한다. 이 가중치가 너무 크면, KL발산(인코더의 Loss)이 역할을 제대로 못하게 되고, 너무 작으면 (디코더의 Loss에 대한 비중이 적어지기때문에) 생성된 이미지의 퀄리티가 좋지 않을 것이다. 

2. VAE 학습시키기

for i in range(10):
    print(f"Trial {i}...")
    AutoEncoder.fit(Xtrain, Xtrain, batch_size=32, shuffle=True, epochs=20)
    result=AutoEncoder.predict(Xtrain)
    
    random=np.random.randint(0, len(Xtrain))
    
    fig = plt.figure()
    rows = 1
    cols = 2
    img1 = Xtrain[random].reshape(28,28)
    img2 = result[random].reshape(28,28)
    ax1 = fig.add_subplot(rows, cols, 1)
    ax1.imshow(img1)
    ax1.set_title('Correct')
    ax1.axis("off")
    ax2 = fig.add_subplot(rows, cols, 2)
    ax2.imshow(img2)
    ax2.set_title('Generated')
    ax2.axis("off")
    plt.show()

그래프를 띄우는 코드를 똑같이 복사해서 가져왔다. 20에포크당 생성 이미지를 한 번씩 띄우는 과정을 10회 반복하여 총 200 에포크만큼의 반복 학습을 진행한다. 

 


 

다음 장으로 넘어가기 전에 이 r_loss_factor의 값을 조금씩 만져보며 수정해보고, 손실이 수렴해가는 과정을 좀 지켜보고, 결과물도 이미지로 꺼내서 한번 봐야겠다. 이 다음에는 VAE로 MNIST 데이터 말고, 얼굴 이미지를 생성하는 것을 해봐야겠다. 

반응형
Posted by Jamm_
반응형

세상에서 가장 간단한 오토인코더를 만들었었다. 이 오토인코더에 몇 개의 레이어를 추가해 성능을 조금 더 향상시켜보자. 

원래 코드를 맥북에서 작성하는게 더 편해서, 간단한 딥러닝 모델 정도는 맥북으로 그냥 돌리고 마는데, 여기부터는 컴퓨터가 힘들어하기 시작해서 게이밍 노트북으로 옮겨서 실행했다. 역시 GPU는 짱짱이다. 

그냥 뭐 설명을 적어놓을건 딱히 더 없으니 코드만 대충 약간 올려놔야겠다. 

 


 

 

import pandas as pd
import numpy as np
import keras
from keras.layers import Input, Conv2D, Flatten, Dense, Conv2DTranspose, Reshape, Activation, LeakyReLU, Dropout, BatchNormalization, MaxPooling2D, Lambda
from keras.models import Model
from keras import backend as K
from keras.optimizers import Adam
from keras.utils import to_categorical
from sklearn.metrics import mean_squared_error as mse

import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns
(Xtrain, Ytrain), (Xtest, Ytest) = keras.datasets.mnist.load_data()
Xtrain=Xtrain/255
Xtest=Xtest/255

Xtrain=Xtrain.reshape(len(Xtrain), 28, 28, 1)
Xtest=Xtest.reshape(len(Xtest), 28, 28, 1)

Ytrain=to_categorical(Ytrain)
Ytest=to_categorical(Ytest)
#Encoder
encoder_input=Input(shape=(28,28,1))

x=Conv2D(filters=32, kernel_size=3, strides=1, padding='same')(encoder_input)
x=LeakyReLU()(x)

x=Conv2D(filters=64, kernel_size=3, strides=2, padding='same')(x)
x=LeakyReLU()(x)

x=Conv2D(filters=64, kernel_size=3, strides=2, padding='same')(x)
x=LeakyReLU()(x)

x=Conv2D(filters=64, kernel_size=3, strides=1, padding='same')(x)
x=LeakyReLU()(x)

shape_before_flatten=K.int_shape(x)[1:]
x=Flatten()(x)
encoder_output=Dense(units=2)(x)
model_encoder=Model(encoder_input, encoder_output)
print(model_encoder.summary())

#Decoder
decoder_input=Input(shape=(2,))
x=Dense(units=np.prod(shape_before_flatten))(decoder_input)
x=Reshape(shape_before_flatten)(x)

x=Conv2DTranspose(filters=64, kernel_size=3, strides=1, padding='same')(x)
x=LeakyReLU()(x)

x=Conv2DTranspose(filters=64, kernel_size=3, strides=2, padding='same')(x)
x=LeakyReLU()(x)

x=Conv2DTranspose(filters=32, kernel_size=3, strides=2, padding='same')(x)
x=LeakyReLU()(x)

x=Conv2DTranspose(filters=1, kernel_size=3, strides=1, padding='same')(x)
x=Activation('sigmoid')(x)

decoder_output=x
model_decoder=Model(decoder_input, decoder_output)
print(model_decoder.summary())


#Connect Two Models
model_input = encoder_input
model_output = model_decoder(encoder_output)

AutoEncoder=Model(model_input, model_output)

인코더 모델과 디코더 모델의 .summary() 메소드 결과물

인코더의 Input shape와, 디코더의 Output shape 가 같으므로 돌아갈 수 있는 모델이다. 

optimizer=Adam(lr=0.0005)
AutoEncoder.compile(optimizer=optimizer, loss='mean_squared_error')


for i in range(10):
    print(f"Trial {i}...")
    AutoEncoder.fit(Xtrain, Xtrain, batch_size=32, shuffle=True, epochs=20)
    result=AutoEncoder.predict(Xtrain)
    
    random=np.random.randint(0, len(Xtrain))
    
    fig = plt.figure()
    rows = 1
    cols = 2
    img1 = Xtrain[random].reshape(28,28)
    img2 = result[random].reshape(28,28)
    ax1 = fig.add_subplot(rows, cols, 1)
    ax1.imshow(img1)
    ax1.set_title('Correct')
    ax1.axis("off")
    ax2 = fig.add_subplot(rows, cols, 2)
    ax2.imshow(img2)
    ax2.set_title('Generated')
    ax2.axis("off")
    plt.show()

오토인코더 학습을 반복문으로 10회 반복 학습시키고, 학습당 20 에포크를 준다. 시간 여유가 좀 있으니까 (+GPU를 사용하면 빠르니까) 총 200 에포크만큼 반복 학습을 시킨다. 이때 20 에포크당 Xtrain에서 랜덤하게 데이터 1개를 가져와서 결과물 이미지랑 비교한다. 

얼추 그럴싸해보이는 이미지 복원 결과물
5 모양이 학습이 힘든가보다. 여전히 3이랑 더 가까워보인다. 
1처럼 단순한 형태는 쉽게 학습하는듯

총 200번의 에포크를 성공적으로 학습했다. 역시 딥러닝에는 GPU를 써야 정신건강에 도움이 되는 것 같다. 이제 VAE에 대해서 좀 더 알아봐야겠다. 

반응형
Posted by Jamm_
반응형

오토인코더는 '인코더'와 '디코더' 두 개의 모델로 이루어진 신경망이다. 이 둘의 역할은

인코더 : 고차원의 입력 데이터를 저차원의 표현 벡터로 압축.

디코더 : 주어진 표현 벡터를 고차원의 데이터로 압축 해제.

와 같은 구조를 가진 신경망이다. 

예를 들면, 인코더는 숫자 2 라는 이미지를 받아서 이를 분석한 후 2차원 좌표평면에 있는 점 (1, 1)에 점을 찍는다. 숫자 2 이미지는 위에서 설명한 고차원 데이터이고, 결과물 표현 벡터는 점 (1, 1)에 해당한다. 이때 2차원 좌표평면을 잠재 공간(latent space)라고 한다. 디코더는 이제 (1, 1)이라는 점을 보고 숫자 2의 이미지를 생성해내는 신경망이다. 따라서 디코더는 잠재 공간의 어떤 점의 좌표가 주어진다면, 그 점에 해당하는 이미지를 생성할 수 있다. 예를 들어 (9, 8)이라는 좌표를 보고 숫자 7의 이미지를 생성하는 식이다. 오토인코더는 이러한 일련의 과정을 학습하며, 점차 이미지의 특성을 잡아내면서, 이미지를 재생성할 수 있는 능력을 갖게 된다. 인코더는 더욱 효과적으로 표현 벡터를 나타내는 방법을, 디코더는 표현 벡터로부터 이미지의 특징을 더해 원본과 비슷한 이미지를 생성하는 과정을 학습하게 된다. 

 


 

1. 가장 간단한 오토인코더 만들기

1.1. 패키지 import 

import pandas as pd
import numpy as np
import keras
from keras.layers import Input, Conv2D, Flatten, Dense, Conv2DTranspose, Reshape, Activation, LeakyReLU, Dropout, BatchNormalization, MaxPooling2D
from keras.models import Model
from keras import backend as K
from keras.optimizers import Adam
from keras.utils import to_categorical
from sklearn.metrics import mean_squared_error as mse

import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns

패키지들을 불러오자. 한번에 싹 불러오면 편할 것이다. 

1.2. 데이터 불러오기

(Xtrain, Ytrain), (Xtest, Ytest) = keras.datasets.mnist.load_data()
Xtrain=Xtrain/255
Xtest=Xtest/255

Xtrain=Xtrain.reshape(len(Xtrain), 28, 28, 1)
Xtest=Xtest.reshape(len(Xtest), 28, 28, 1)

Ytrain=to_categorical(Ytrain)
Ytest=to_categorical(Ytest)

이번에는 손글씨로 이루어진 숫자 이미지 데이터셋 (MNIST) 를 사용한다. 이 데이터셋은 28 * 28 사이즈의 손글씨로 적힌 숫자를 포함하는 이미지이고, 데이터는 0 ~ 255의 픽셀 강도로 이루어져 있다. 일반적으로 신경망은 -1 ~ 1 사이의 값에서 가장 잘 동작하므로 255로 데이터를 나누어 주면 입력 데이터를 0 ~ 1로 맞춰줄 수 있다. 

1.3. 인코더 모델

encoder_input=Input(shape=(28,28,1))

x=Conv2D(filters=4, kernel_size=3, strides=1, padding='same')(encoder_input)
x=BatchNormalization()(x)
x=LeakyReLU()(x)
shape_before_flatten=K.int_shape(x)[1:]
x=Flatten()(x)
encoder_output=Dense(units=2)(x)

model_encoder=Model(encoder_input, encoder_output)
model_encoder.summary()

model_encoder.summary() 의 결과물

입력으로 28 * 28 * 1(흑백 1채널) 의 이미지를 받아, 한 번의 Convolution, 배치 정규화, LeakyReLU 활성화 함수, 2차원 잠재 공간으로 매핑하는 신경망이다. 매우 간단하다. 

1.4. 디코더 모델

decoder_input=Input(shape=(2,))

x=Dense(units=np.prod(shape_before_flatten))(decoder_input)
x=Reshape(shape_before_flatten)(x)
x=LeakyReLU()(x)
x=BatchNormalization()(x)
x=Conv2DTranspose(filters=1, kernel_size=3, strides=1, padding='same')(x)
x=Activation('sigmoid')(x)
decoder_output=x

model_decoder=Model(decoder_input, decoder_output)
model_decoder.summary()

model_decoder.summary() 의 결과물

디코더 모델은 의외로 간단하게 만들 수 있다. 인코더 모델의 layer 들을 역순으로 해주면 쉽게 만들 수 있다. 

이때 꼭 인코더 모델과 대칭(?)을 이루도록 만들지 않아도 된다는 점이 포인트이다. 인코더에서는 Convolution을 두 번 거쳤지만, Decoder에서는 한 번만 해도 된다는 뜻이다. 인코더에 입력으로 들어오는 데이터와 같은 차원, 같은 크기이기만 하면 어떤 구조를 가져도 상관없다. 

여기서는 또 특이한 점 두가지 중 첫 번째는, Conv2DTranspose 가 들어가 있다. 이것은 일반적인 Conv2D의 역연산이라고 생각하면 쉽다. (Convolution 자체의 수학적 원리를 아직 모르기 때문에... 물어보지 마세요 엉엉)

두 번째는 마지막에 오는 활성화 함수인데, 'Sigmoid' (시그모이드 함수) 라 불린다. Logistic Regression에서 주로 봤었던 Sigmoid... 여기서는 Conv2 DTranspose의 결과물을 0 ~ 1 사이의 값으로 만들어주는 역할을 한다. 그럼 입력 데이터와 값의 스케일이 맞겠다. 단지 그뿐이다. 

1.5. 인코더, 디코더 연결하기

입력 이미지는 인코더를 거쳐 -> 잠재 공간의 한 벡터로 변환될 것이고 -> 이 벡터는 디코더를 거쳐 -> 다시 원본 이미지를 만들어내게끔 학습할 것이다. 이제, 인코더와 디코더를 연결만 해주면 되는데, 케라스에서는 이 과정은 그냥 껌이다. 

model_input = encoder_input
model_output = model_decoder(encoder_output)

AutoEncoder=Model(model_input, model_output)

 

끝. 

이렇게 만든 'AutoEncoder' 모델을 한번에 컴파일해도, 'model_encoder' 와 'model_decoder' 를 각각 사용할 수 있다. 

 


 

2. 가장 간단한 오토인코더 학습 & 새로운 이미지 생성

이제 마찬가지로 모델을 컴파일하고, 학습을 시키면 된다.

#Compile AE Model
optimizer=Adam(lr=0.001)
AutoEncoder.compile(optimizer=optimizer, loss='mean_squared_error')

#Fit AE Model
AutoEncoder.fit(Xtrain, Xtrain, batch_size=10, shuffle=True, epochs=1)

시간이 없으니, 1회만 학습시켜보자.

여기서 내가 처음에 실수했던 게, 타겟 변수에다가 자기 자신을 넣지 않고, 원래 타겟 변수를 넣었다. 자기 자신의 이미지와, 새로 생성한 자기 자신의 이미지를 비교하는 것이기 때문에, 입력 : Xtrain, 타겟 : Xtrain 이 맞다. loss 하이퍼파라미터의 'mean_squared_error'는 각각의 픽셀에 대하여 오차를 계산한다. 생성된 이미지가 원본과 비슷한 숫자라면, 비슷한 위치의 값이 같이 올라갈 것이다. 

result=AutoEncoder.predict(Xtrain)
print(result[0].shape, Xtrain[0].shape)

생성된 결과물과 원본의 shape를 확인해보면 둘다 (28, 28, 1) 임을 확인할 수 있다. 입력과 생성값의 차원이 같으므로 일단 '세상에서 가장 단순한 오토 인코더'를 성공적으로 만들었다!

그럼 이미지를 띄워볼까?

fig = plt.figure()
rows = 1
cols = 2

img1 = Xtrain[0].reshape(28,28)
img2 = result[0].reshape(28,28)
 
ax1 = fig.add_subplot(rows, cols, 1)
ax1.imshow(img1)
ax1.set_title('Correct')
ax1.axis("off")
 
ax2 = fig.add_subplot(rows, cols, 2)
ax2.imshow(img2)
ax2.set_title('Generated')
ax2.axis("off")
plt.show()

위 코드를 실행하면 Xtrain(원본) 과, result(생성 이미지)의 첫 번째 값을 이미지 파일로 볼 수 있다. 

대충 확인해보자면

원본 이미지와 새로 생성된 이미지 비교

원본은 딱 봐도 5이지만, 새로 생성한 이미지는 대충 보면 3같이 보인다. 억지로 보면 5로 이어서 볼 수는 있지만, 딱 봐도 5처럼 보여야 진짜 잘 생성한 이미지이니... 썩 좋은 결과는 아니지만, 그래도 모델이 돌아갔다는 것에 의의를 두자. Convolution과, 전결합층을 적절히 쌓고, 에포크를 충분히 주고 학습시키면 더 좋은 결과물을 얻을 수 있을지도 모른다. 

반응형
Posted by Jamm_
2020. 2. 25. 15:07

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

반응형

최근에 책을 구입한 게 이 책이다. http://www.yes24.com/Product/Goods/81538614http://www.yes24.com/Product/Goods/81538614

 

미술관에 GAN 딥러닝 실전 프로젝트

창조에 다가서는 GAN의 4가지 생성 프로젝트 이 책은 케라스를 사용한 딥러닝 기초부터 AI 분야 최신 알고리즘까지 설명한다. 기계 스스로 그림을 그리고, 글을 쓰고, 음악을 작곡하고, 게임을 하는 딥러닝 생성 모델을 재현하는 과정에서 독자는 변이형 오토인코더(VAE), 생성적 적대 신경망(GAN), 인코더-디코더 모델, 월드 모델 등을...

www.yes24.com

처음에는 텐서플로우와 케라스가 뭔지도 몰랐다. "아니, 텐서플로우는 텐서플로우고, 케라스는 그럼 뭡니까? 왜 텐서플로우랑 같이 언급이 되는 거죠?"라고 묻던 게 불과 몇 주 전의 일이었다. 처음 딥러닝을 해봐야겠다 생각이 들었을 때는 텐서플로우 버전에 대한 개념이 없어서, 텐서플로우 2.0 버전을 깔고, 'tf.placeholder' 같은 코드를 따라 치다가 실행이 안 되기 시작하면서 때려치웠던 기억이 있다. (tf.placeholder... 코드는 TF 1.x 버전에서 주로 쓰던 코드라고 들었다. 지금은 쓰이지 않는다고...) 그래서 파이토치 코드를 따라 쳤는데, MNIST에서 돌아가는 데 성공했다. 단순히 실행에 성공했다는 이유로, 파이토치를 사용했었는데, 케라스를 조금 익히니까, 이거, 물건이다. 정말 간편하다. 케라스를 익힌 이유는, 이 책에서 예제 코드가 모두 케라스로 작성되어 있다... (만약 파이토치로 작성되어 있었다면, 케라스 같은 건 모르고 있었겠지.)

 


 

아무튼, 목표는 작곡하는 인공지능 만들기, 강화학습으로 핸드폰 게임 클리어하기이다.  나는 음악도 매우 좋아하는데, 신은 불행하게도 나에게 '보통 사람들보다 약간 좋은 음악적 감' 은 주셨지만, '작곡을 하며 음악을 즐길 수 있을 정도의 능력' 은 주시지 않으셨다. 대신 '코드를 읽고 쓰고 배우고 작성할 수 있는 능력' 은 주셨으니, 난 이걸로 음악을 즐겨야겠다.

반응형
Posted by Jamm_