VAE는 “Variational Autoencoder”의 약자로, 딥러닝과 생성 모델링 분야에서 사용되는 확률적인 생성 모델 중 하나입니다.
VAE는 주로 데이터의 차원 축소, 생성, 잠재 변수의 학습, 이미지 생성, 자연어 처리 등 다양한 응용 분야에서 사용됩니다.
이글에서는 VAE에 대해 간단히 소개하고 파이썬 코드로 작성 해 보겠습니다.
목차
"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."
참고: VAE 논문
오토인코더 (Autoencoder):
다변수 오토인코더를 살펴보기 전에, 우선 오토인코더에 대해 알아보겠습니다. 오토인코더는 입력 데이터를 효율적으로 압축하고 복원하는 모델입니다. 이는 데이터의 표현을 학습하기 위해 사용됩니다.
오토인코더는 일반적으로 인코더(encoder)와 디코더(decoder) 두 부분으로 구성됩니다. 인코더는 입력 데이터를 저차원 잠재 공간(latent space)으로 인코딩하고, 디코더는 이 잠재 공간의 표현을 다시 원래 입력 데이터로 디코딩합니다.
VAE (Variational autoencoder)
VAE는 오토 인코더의 변형으로, 확률적 잠재 공간을 사용합니다. 이것은 VAE가 더 유연한 데이터 생성 및 잠재 공간 탐색을 가능하게 합니다.
VAE는 잠재 공간의 각 포인트에 대한 확률 분포를 모델링 하며, 이 분포는 일반적으로 정규 분포로 가정 됩니다.
Encoder와 Decoder의 역할:
인코더: 입력 데이터를 잠재 공간에서의 확률 분포 파라미터(평균과 표준 편차)를 출력합니다. 이 확률 분포를 통해 잠재 공간의 포인트를 샘플링 합니다. 즉
z = 평균 + 분산 * 파라미터 재구성 표준 정규 분포에서 나온 한 개의 샘플링 값 즉z=\mu+\sigma*\epsilon 입니다. 기본적인 오토 인코더와 달리 이 부분에서 학습이 가능합니다.
디코더: 이렇게 샘플링된 잠재 포인트를 사용하여 원래 입력 데이터를 재구성합니다.
손실 함수 (Loss Function):
VAE의 핵심은 재구성 손실(reconstruction loss)과 정규화 손실(regularization loss)을 함께 최소화하려는 것입니다.
재구성 손실은 입력 데이터와 디코더의 출력 간의 차이를 측정합니다. 상황에 따라 다르겠지만 디코더의 분포가 베르누이 분포일때는 Cross enthropy Error 를 쓰고 가우시안 분포일때는 Mean Squrered Error 를 이용합니다. 이글에서는 예시로 CEE(Cross Entropy Error)를 이용했습니다. \text{CEE} \ = -\sum P(x)\log{Q(x)}
정규화 손실은 잠재 공간의 확률 분포와 정규 분포 간의 유사성을 측정합니다.
Kullback-Leibler divergence는 두 확률 분포 의 차이를 측정하는 것입니다. 학습은 이 두 분포의 차이를 줄여나가는 방식으로 진행이 됩니다. \text{KL}(P|Q) = \sum\limits P(x) \times \log( \frac{P(x)}{Q(x)} ) 이식은 두 확률분포 P 와 Q 사이의 divergence 즉 차이를 구하는 공식입니다.
총 손실 (Total Loss): 이제 재구성 손실과 정규화 손실을 더해줍니다. 즉 \text{Total Loss} = CEE(P,Q) + KL(P|Q) = -\sum P(x)\log{Q(x)} + \sum\limits P(x) \times \log\left( \frac{P(x)}{Q(x)}\right)
하지만 KL-divergence를 이용시에 발산하여 NAN에러를 발생할 확률이 크기 때문에 재구성 손실과, 정규화 손실값의 중요도를 적당히 조절해야합니다.
생성과 잠재 공간 탐색:
VAE를 훈련한 후에는 잠재 공간에서 샘플링하여 새로운 데이터를 생성할 수 있습니다. 이것은 생성 모델로 사용됩니다.
잠재 공간에서의 유사한 포인트들은 원본 데이터 공간에서 유사한 데이터를 생성합니다. 따라서 잠재 공간에서의 탐색은 데이터 생성 및 변형에 활용될 수 있습니다.
파이썬 코드 작성:
이제 VAE를 파이썬 코드로 작성 해 보겠습니다.
1. 필요한 라이브러리 가져오기 및 GPU 설정
import os
import numpy as np
import matplotlib.pyplot as plt
import random
import tensorflow as tf
# GPU 디바이스 목록 가져오기
gpus = tf.config.experimental.list_physical_devices('GPU')
if gpus:
print("사용 가능한 GPU 디바이스:")
for gpu in gpus:
print("디바이스 이름:", gpu.name)
else:
print("사용 가능한 GPU 디바이스가 없습니다.")
2. Celeb A 데이터셋 이미지 불러오기 및 전처리
# 이미지 전처리 코드
image_dir = "C:\\Users\\crazy\\Downloads\\archive\\img_align_celeba" #이미지 데이터셋 경로입니다.
resize_width, resize_height = 128, 128 # 변경할 크기
# 이미지 목록 가져오기
image_files = os.listdir(image_dir)
# 이미지를 NumPy 배열로 로드하고 크기 변경하기
images = []
for filename in image_files:
img = tf.keras.preprocessing.image.load_img(os.path.join(image_dir, filename), target_size=(resize_width, resize_height))
img = tf.keras.preprocessing.image.img_to_array(img)
img = img / 255.0 # 이미지 정규화
images.append(img)
images = np.array(images)
# 이미지 중에서 무작위로 하나 선택
random_index = random.randint(0, len(images) - 1)
selected_image = images[random_index]
3. VAE 모델을 위한 인코더, 디코더 및 샘플링 레이어 정의
keras = tf.keras
K = tf.keras.backend
#샘플링 클래스 정의
#keras의 layer를 상속받아 만들어진 클래스 입니다.
#call 메서드는 z_mean(평균) 과 z_log_var(로그 분산)을 받습니다.
#z_mean 과 z_log_var를 사용해 잠재변수(z)를 return 합니다.
class Sampling(keras.layers.Layer):
def call(self, inputs):
z_mean, z_log_var = inputs
batch = tf.shape(z_mean)[0]
dim = tf.shape(z_mean)[1]
epsilon = K.random_normal(shape=(batch, dim))
return z_mean + tf.exp(0.5 * z_log_var) * epsilon
#인코더 부분
encoder_input = keras.layers.Input(shape=(resize_width, resize_height, 3), name="encoder_input")
x = keras.layers.Conv2D(32, (3, 3), strides=2, activation="relu", padding="same")(encoder_input)
x = keras.layers.Conv2D(64, (3, 3), strides=2, activation="relu", padding="same")(x)
x = keras.layers.Conv2D(128, (3, 3), strides=2, activation="relu", padding="same")(x)
shape_before_flattening = K.int_shape(x)[1:]
x = keras.layers.Flatten()(x)
z_mean = keras.layers.Dense(200, name="z_mean")(x)
z_log_var = keras.layers.Dense(200, name="z_log_var")(x)
z = Sampling()([z_mean, z_log_var])
encoder = keras.models.Model(encoder_input, [z_mean, z_log_var, z], name="encoder")
#디코더 부분
decoder_input = keras.layers.Input(shape=(200,), name="decoder_input")
x = keras.layers.Dense(np.prod(shape_before_flattening))(decoder_input)
x = keras.layers.Reshape(shape_before_flattening)(x)
x = keras.layers.Conv2DTranspose(128, (3, 3), strides=2, activation = 'relu', padding="same")(x)
x = keras.layers.Conv2DTranspose(64, (3, 3), strides=2, activation = 'relu', padding="same")(x)
x = keras.layers.Conv2DTranspose(32, (3, 3), strides=2, activation = 'relu', padding="same")(x)
decoder_output = keras.layers.Conv2D(3,(3, 3),strides = 1, activation="relu", padding="same", name="decoder_output")(x)
decoder = keras.models.Model(decoder_input, decoder_output)
인코더와 디코더 네트워크는 아래와 같이 구성됩니다.
인코더 부분
Layer (type) | Output Shape | Param # | Connected to |
---|---|---|---|
encoder_input | [(None, 128, 128, 3)] | 0 | |
conv2d_6 (Conv2D) | (None, 64, 64, 32) | 896 | encoder_input[0][0] |
conv2d_7 (Conv2D) | (None, 32, 32, 64) | 18496 | conv2d_6[0][0] |
conv2d_8 (Conv2D) | (None, 16, 16, 128) | 73856 | conv2d_7[0][0] |
flatten_2 (Flatten) | (None, 32768) | 0 | conv2d_8[0][0] |
z_mean (Dense) | (None, 200) | 6553800 | flatten_2[0][0] |
z_log_var (Dense) | (None, 200) | 6553800 | flatten_2[0][0] |
sampling_2 (Sampling) | (None, 200) | 0 | z_mean[0][0], z_log_var[0][0] |
Total params: 13,200,848
Trainable params: 13,200,848
Non-trainable params: 0
디코더 부분
Layer (type) | Output Shape | Param # |
---|---|---|
decoder_input | [(None, 200)] | 0 |
dense_2 (Dense) | (None, 32768) | 6586368 |
reshape_2 (Reshape) | (None, 16, 16, 128) | 0 |
conv2d_transpose_6 | (None, 32, 32, 128) | 147584 |
conv2d_transpose_7 | (None, 64, 64, 64) | 73792 |
conv2d_transpose_8 | (None, 128, 128, 32) | 18464 |
decoder_output | (None, 128, 128, 3) | 867 |
Total params: 6,827,075
Trainable params: 6,827,075
Non-trainable params: 0
4. VAE (Variational Autoencoder) 모델 클래스 정의와 손실 함수 설정
#VAE 클래스는 Keras의 Model 클래스를 상속받아서 만들어진 클래스입니다. 이 클래스는 VAE 모델을 정의하고 학습하기 위한 기능을 포함합니다.
#__init__ 메서드: 클래스의 생성자로, encoder와 decoder 모델을 인수로 받습니다. 이 두 모델은 VAE의 핵심 구성 요소인 인코더와 디코더입니다.
#또한 **kwargs를 사용하여 추가 인수를 받을 수 있습니다.
class VAE(keras.models.Model):
def __init__(self, encoder, decoder, **kwargs):
super(VAE, self).__init__(**kwargs)
self.encoder = encoder
self.decoder = decoder
#loss 값을 모니터링 하는 요소
self.total_loss_tracker = keras.metrics.Mean(name='total_loss')
self.reconstruction_loss_tracker = keras.metrics.Mean(name="reconstruction_loss")
self.kl_loss_tracker = keras.metrics.Mean(name="kl_loss")
#metrics 프로퍼티: VAE 모델의 성능을 모니터링하는 메트릭을 정의합니다. 이 모델은 총 손실 (total_loss), 재구성 손실 (reconstruction_loss), 및 KL-다이버전스 손실 (kl_loss)을 모니터링합니다.
@property
def metrics(self):
return [
self.total_loss_tracker,
self.reconstruction_loss_tracker,
self.kl_loss_tracker,
]
#call 메서드는 모델을 호출할 때 실행되는 메서드입니다. 입력 이미지를 받아서 잠재 변수를 인코더로부터 추출하고, 해당 잠재 변수를 사용하여 이미지를 디코더로부터 재구성합니다.
#input은 입력 이미지 데이터를 나타냅니다.
#z_mean, z_log_var, z는 인코더로부터 얻은 잠재 변수의 평균, 로그 분산 및 샘플링된 잠재 변수를 나타냅니다.
#reconstruction은 디코더로부터 재구성된 이미지를 나타냅니다.
def call(self, input):
z_mean, z_log_var, z = encoder(input)
reconstruction = decoder(z)
return z_mean, z_log_var, reconstruction
#train_step 메서드는 VAE 모델의 학습 단계를 정의합니다.
#입력으로 데이터 (data)를 받아서 학습을 진행합니다.
#tf.GradientTape() 내에서 재구성 손실과 KL-다이버전스 손실을 계산합니다.
#reconstruction_loss는 재구성 손실을 계산하는 부분입니다. tf.losses.binary_crossentropy를 사용하여 재구성 손실을 계산하며, 이 값을 500으로 스케일링합니다.
#kl_loss는 KL-다이버전스 손실을 계산하는 부분입니다. 이것은 VAE의 정규화 항으로, 잠재 변수의 분포와 정규 분포 간의 차이를 측정합니다.
#total_loss는 재구성 손실과 KL-다이버전스 손실을 더한 총 손실입니다.
#경사 하강법을 사용하여 총 손실을 최소화하도록 모델을 업데이트하고, 메트릭 값을 업데이트합니다.
#최종적으로 업데이트된 메트릭 값을 딕셔너리로 반환합니다.
def train_step(self, data):
with tf.GradientTape() as tape:
z_mean, z_log_var, reconstruction = self(data)
reconstruction_loss = tf.reduce_mean(500 * tf.losses.binary_crossentropy(data, reconstruction, axis=(1,2,3)))
kl_loss = tf.reduce_mean(tf.reduce_sum(-0.5 * (1 + z_log_var - tf.square(z_mean) - tf.exp(z_log_var)), axis=1))
total_loss = reconstruction_loss + kl_loss
grads = tape.gradient(total_loss, self.trainable_weights)
self.optimizer.apply_gradients(zip(grads, self.trainable_weights))
self.total_loss_tracker.update_state(total_loss)
self.reconstruction_loss_tracker.update_state(reconstruction_loss)
self.kl_loss_tracker.update_state(kl_loss)
return {m.name: m.result() for m in self.metrics}
5. VAE 모델 학습과 훈련 단계 정의 및 이미지 데이터로 VAE 모델 학습
#모델 새로 정의할때
vae = VAE(encoder, decoder)
우선 위와같이 새로 모델을 정의하고 아래와 같이 훈련을 시켜줍니다.
#훈련시키기 (훈련후에 결과가 맘에들지않으면 다시 훈련을 시켜줍니다.)
vae.compile(optimizer="adam")
vae.fit(images, epochs=100, batch_size=320)
6. 결과 확인을 위한 이미지 선택 및 재구성 및 원본 이미지와 재구성 이미지 시각화
이제 훈련된 자료를 가지고 두개의 이미지를 뽑아 원본과 결과(인코딩-잠재공간-디코딩)를 비교하고 그 두 이미지 사이의 잠재공간을 이용해 다시 디코딩을 하여 결과를 비교해 보겠습니다.
# 이미지 중에서 무작위로 하나 선택
random_index1 = random.randint(0, len(images) - 1)
selected_image1 = images[random_index1]
# 또 다른 이미지를 무작위로 선택
random_index2 = random.randint(0, len(images) - 1)
selected_image2 = images[random_index2]
# 선택한 이미지 표시
plt.figure(figsize=(10, 5)) # 그림 크기 조정
plt.subplot(1, 2, 1) # 1x2 그리드의 첫 번째 위치
plt.imshow(selected_image1)
plt.title("Original Image 1")
# 인코더에 넣어서 임베딩 벡터 확인
embedded1 = np.array(encoder.predict(np.array([selected_image1])))[2]
# 임베딩 벡터를 디코더에 넣어서 결과값 예측
predicted_img1 = decoder.predict(embedded1)
plt.subplot(1, 2, 2) # 1x2 그리드의 두 번째 위치
plt.imshow(predicted_img1.reshape(resize_width, resize_height, 3))
plt.title("Reconstructed Image 1")
plt.show()
# 선택한 다른 이미지 표시
plt.figure(figsize=(10, 5)) # 그림 크기 조정
plt.subplot(1, 2, 1) # 1x2 그리드의 첫 번째 위치
plt.imshow(selected_image2)
plt.title("Original Image 2")
# 인코더에 넣어서 임베딩 벡터 확인
embedded2 = np.array(encoder.predict(np.array([selected_image2])))[2]
# 임베딩 벡터를 디코더에 넣어서 결과값 예측
predicted_img2 = decoder.predict(embedded2)
plt.subplot(1, 2, 2) # 1x2 그리드의 두 번째 위치
plt.imshow(predicted_img2.reshape(resize_width, resize_height, 3))
plt.title("Reconstructed Image 2")
plt.show()
# 두 이미지의 임베딩 벡터의 중간값을 이용해 결과값 예측
predicted_img_combined = decoder.predict((embedded1 + embedded2) / 2)
plt.figure(figsize=(5, 5))
plt.imshow(predicted_img_combined.reshape(resize_width, resize_height, 3))
plt.title("Reconstructed Combined Image")
plt.show()
훈련양이 그리 많지 않아서 원하지 않은 결과가 나오긴 했지만 디코더로 재구성한 결과를 보면 아래와 같이 나왔습니다. 또한 두 임베딩 벡터사이의 벡터를 이용해 디코딩을 해보면 두 결과의 사이값의 결과가 나왔습니다.
결론
Variational Autoencoder (VAE)에 대한 개요를 제공하고 파이썬 코드로 VAE를 작성하는 방법을 설명하였습니다. VAE는 딥러닝과 생성 모델링 분야에서 사용되며, 주로 데이터의 차원 축소, 생성, 잠재 변수의 학습, 이미지 생성, 자연어 처리 등 다양한 응용 분야에서 활용됩니다.’
요약하자면 아래와 같습니다.
- VAE는 확률적인 생성 모델 중 하나로, 오토인코더의 변형입니다.
- VAE는 확률적 잠재 공간을 사용하여 더 유연한 데이터 생성 및 잠재 공간 탐색이 가능합니다.
- VAE의 핵심 요소로는 인코더와 디코더가 있으며, 인코더는 입력 데이터를 잠재 공간으로 인코딩하고, 디코더는 잠재 공간의 표현을 다시 원래 입력 데이터로 디코딩합니다.
- VAE의 학습 목표는 재구성 손실과 정규화 손실 을 최소화하는 것입니다.
- 재구성 손실은 입력 데이터와 디코더의 출력 간의 차이를 측정하며, 정규화 손실은 잠재 공간의 확률 분포와 정규 분포 간의 유사성을 측정합니다.
- VAE를 사용하여 데이터 생성 및 잠재 공간 탐색이 가능하며, 잠재 공간에서 유사한 포인트는 원본 데이터 공간에서 유사한 데이터를 생성합니다.
- 파이썬 코드를 사용하여 VAE 모델을 작성하고 이미지 데이터로 학습하였습니다.
이제 코드를 사용하여 VAE 모델을 학습하고, 선택한 이미지의 재구성 및 시각화를 확인할 수 있습니다. VAE는 이미지 생성 및 잠재 공간 탐색과 같은 다양한 응용 분야에서 활용할 수 있는 강력한 생성 모델입니다.
함께 참고하면 좋은 글
GAN (Generative Adversarial Nets): 생성적 적대 신경망