오토 인코더는 아래와 같은 구조를 가지고 있습니다.
출처 > A Better Autoencoder for Image: Convolutional Autoencoder
인코더는 고차원 입력 데이터를 저차원 표현 벡터로 압축하고
디코더는 표현 벡터를 다시 원본 차원으로 압축 해제합니다.
표현 벡터는 원본 이미지를 저차원 잠재 공간으로 압축한 것입니다.
이때 디코더는 저차원 잠재 공간에 있는 한 지점을 재구성 이미지로 변환 시키는 것을
학습하기 때문에 잠재 공간에 있는 어떠한 지점을 선택하더라도
새 이미지를 생성할 수 있습니다.
인코더는 입력 이미지를 잠재 공간의 한 포인트로 매핑하는걸 목표로 하기 때문에
합성곱(Convolution)층으로 구성됩니다.
디코더가 잠재 공간의 한 포인트를 재구성 이미지로 변환 하기 위해서는
전치 합성곱층을 사용합니다.
일반적인 표준 합성곱 층과 달리 전치 합성곱층의 strides는
이미지 픽셀 사이에 추가되는 제로 패딩을 결정합니다.
또한 전치 합성곱은 항상 커널을 1픽셀 씩 이동하게 됩니다.
VAE(이하 변이형 오토인코더)와 AE(이하 오토 인코더)의 차이점은
인코더와 손실함수 뿐입니다.
변이형 오토인코더에서 인코더는 입력 이미지를 잠재 공간에 있는
포인트 주변의 다변수 정규 분포에 매핑합니다.
다음 공식을 사용하여 평균이 이고 편차가 인 정규 분포에서
포인트 를 샘플링할 수 있습니다.
이때 은 표준 정규 분포에서 샘플링됩니다.
변이형 오토인코더는 입력 이미지를 받아 잠재 공간의
다변수 정규 분포를 정의하는 mu와 log_var 2개의 벡터로 인코딩 합니다.
오토인코더는 원본 이미지와 재구성 이미지 사이의 RMSE 손실로만 구성되었지만
변이형 오토인코더는 추가적으로 쿨백 - 라이블러 발산 (Kullback - Leibler divergence)
을 사용합니다.
수식은 아래와 같습니다.
이 식의 덧셈은 잠재 공간의 모든 차원에 대해서 수행됩니다.
kl_loss는 모든 차원의 대해 mu = 0 이고 log_var = 0 일때 최소입니다.
VAE는 다음과 같이 정의할 수 있습니다.
class VAE():
def __init__(self,
input_dim,
encoder_conv_filters,
encoder_conv_kernel_size,
encoder_conv_strides,
decoder_conv_t_filters,
decoder_conv_t_kernel_size,
decoder_conv_t_strides,
z_dim,
use_batch_norm = False,
use_dropout = False):
self.name = 'variational_autoencoder'
self.input_dim = input_dim
self.encoder_conv_filters = encoder_conv_filters
self.encoder_conv_kernel_size = encoder_conv_kernel_size
self.encoder_conv_strides = encoder_conv_strides
self.decoder_conv_t_filters = decoder_conv_t_filters
self.decoder_conv_t_kernel_size = decoder_conv_t_kernel_size
self.decoder_conv_t_strides = decoder_conv_t_strides
self.z_dim = z_dim
self.use_batch_norm = use_batch_norm
self.use_dropout = use_dropout
self.n_layers_encoder = len(encoder_conv_filters)
self.n_layers_decoder = len(decoder_conv_t_filters)
self._build()
def _build(self):
encoder_input = Input(shape=self.input_dim, name='encoder_input')
x = encoder_input
for i in range(self.n_layers_encoder):
conv_layer = Conv2D(
filters = self.encoder_conv_filters[i],
kernel_size = self.encoder_conv_kernel_size[i],
strides = self.encoder_conv_strides[i],
padding = 'same',
name = 'encoder_conv_' + str(i)
)
x = conv_layer(x)
if self.use_batch_norm:
x = BatchNormalization()(x)
x = LeakyReLU()(x)
if self.use_dropout:
x = Dropout(rate = 0.2)(x)
shape_before_flattening = K.int_shape(x)[1:]
x = Flatten()(x)
self.mu = Dense(self.z_dim, name='mu')(x)
self.log_var = Dense(self.z_dim, name='log_var')(x)
encoder_mu_log_var = Model(encoder_input, (self.mu, self.log_var))
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, name='encoder_output')([self.mu, self.log_var])
self.encoder = Model(encoder_input, encoder_output)
decoder_input = Input(shape=(self.z_dim,), name='decoder_input')
x = Dense(np.prod(shape_before_flattening))(decoder_input)
x = Reshape(shape_before_flattening)(x)
for i in range(self.n_layers_decoder):
conv_t_layer = Conv2DTranspose(
filters = self.decoder_conv_t_filters[i],
kernel_size = self.decoder_conv_t_kernel_size[i],
strides = self.decoder_conv_t_strides[i],
padding = 'same',
name = 'decoder_conv_t_' + str(i)
)
x = conv_t_layer(x)
if i < self.n_layers_decoder - 1:
if self.use_batch_norm:
x = BatchNormalization()(x)
x = LeakyReLU()(x)
if self.use_dropout:
x = Dropout(rate = 0.2)(x)
else:
x = Activation('sigmoid')(x)
decoder_output = x
self.decoder = Model(decoder_input, decoder_output)
model_input = encoder_input
model_output = self.decoder(encoder_output)
self.model = Model(model_input, model_output)
def compile(self, learning_rate, r_loss_factor):
optimizer = tf.keras.optimizers.Adam(lr=learning_rate)
def vae_r_loss(y_true, y_pred):
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_loss = -0.5 * K.sum(1 + self.log_var - K.square(self.mu) - K.exp(self.log_var), axis = 1)
return kl_loss
def vae_loss(y_true, y_pred):
r_loss = vae_r_loss(y_true, y_pred)
kl_loss = vae_kl_loss(y_true, y_pred)
return r_loss + kl_loss
self.model.compile(optimizer=optimizer, loss=vae_loss, metrics=[vae_r_loss, vae_kl_loss])
def train(self, data_flow, epochs):
self.model.fit_generator(data_flow, epochs=epochs, shuffle=True)
def save_weights(self, path):
self.model.save_weights(path)
def load_weights(self, path):
self.model.load_weights(path)
제가 사용한 데이터셋은 Towards the Automatic Anime Characters Creation with Generative Adversarial Networks 논문에 소개된 데이터셋을 사용하였습니다.
캐글 데이터로도 다운받을 수 있습니다.
다음 그림은 훈련 epochs=200을 수행한 VAE 모델의 예측 결과입니다.
(랜덤한 잠재공간 샘플을 디코더가 예측한 16개 결과입니다)