본문 바로가기
ML & DL/tensorflow

AutoEncoder(오토인코더) - MNIST dataset 사용

by 별준 2020. 11. 25.

MNIST dataset을 사용해서 여러 모델로 오토인코더를 구성해보았습니다.

 

내용은 아래 사이트를 참조하였습니다.

keraskorea.github.io/posts/2018-10-23-keras_autoencoder/

 

케라스로 이해하는 Autoencoder

Building Autoencoders in Keras

keraskorea.github.io

 

AutoEncoder(오터인코더)란 ?

오토인코더는 비지도 방식을 사용해서 데이터코딩을 학습하는데 사용되는 신경망입니다. 단순하게 말하자면 입력을 출력으로 복사하는 신경망인데, 네트워크의 layer를 쌓으면서 데이터를 압축하거나 입력 데이터의 노이즈를 제거해서 깔끔한 이미지로 복원한다거나하는 여러가지 다양한 오토인코더가 있습니다.

모델을 구성할 때 여러가지 제약사항들을 주어서, 오토인코더가 단순히 입력을 출력으로 내보내지 않고, 데이터를 학습하고 효율적으로 표현할 수 있도록 합니다.

 

하지만, 오토인코더는 실제 응용에서는 거의 사용되지는 않습니다. 

비지도 학습(unsupervised learning)의 문제를 풀어낼 잠재적인 수단으로 오랫동안 연구되었지만, 실제로는 self-supervised 학습이기 때문입니다. 즉, 입력 데이터로부터 출력을 만들어냅니다. 

 

오토인코더는 데이터의 노이즈를 제거하거나, 데이터의 시각화를 위한 차원 축소에 적절하게 사용될 수는 있습니다.

 

간단한 autoencoder

single fully-connected neural layer를 사용해서 만들어보겠습니다.

import tensorflow as tf
import numpy as np
from tensorflow.keras.layers import *
from tensorflow.keras.models import Model

# encoding dimension
encoding_dim = 32

input_img = Input(shape=(784,)) # 28*28
encoded = Dense(encoding_dim, activation='relu')(input_img)
decoded = Dense(784, activation='sigmoid')(encoded)

# autoencoder
autoencoder = Model(input_img, decoded)

# encoder
encoder = Model(input_img, encoded)

# decoder
encoded_input = Input(shape=(encoding_dim,))
decoded_layer = autoencoder.layers[-1]
decoder = Model(encoded_input, decoded_layer(encoded_input))

autoencoder.compile(optimizer='adam', loss='binary_crossentropy')

사용될 mnist data는 28*28 사이즈의 이미지이므로, 28x28=784로 펼쳐서 입력으로 받습니다. 그리고 hidden layer는 32개의 unit으로 구성됩니다. 여기까지가 encoder의 역할이고, 이제 encoding한 데이터를 다시 784로 디코딩하는 decoder layer를 구성합니다.

 

autoencoder는 encoder와 decoder가 합쳐진 모델이며, 각각의 모델도 구성해둡니다.

 

그리고 데이터를 받고, 0~1 사이의 값으로 normalization을 해주고, 28x28이미지를 784의 크기의 벡터로 만들어줍니다.

# data load
(x_train, _), (x_test, _) = tf.keras.datasets.mnist.load_data()
# normalization
x_train = x_train.astype(np.float32)/255.
x_test = x_test.astype(np.float32)/255.
# flatten
x_train_flatten = x_train.reshape((x_train.shape[0], -1))
x_test_flatten = x_test.reshape((x_test.shape[0], -1))

print(x_train_flatten.shape)
print(x_test_flatten.shape)

이제 오토인코더를 아래와 같이 학습해보겠습니다.

autoencoder.fit(x_train_flatten, x_train_flatten, 
                batch_size=256, epochs=50, 
                validation_data=(x_test_flatten,x_test_flatten))

대략 0.09의 loss값을 가지고 있습니다. 

이제 재구성된 입력과 오토인코딩을 거친 출력을 시각화해서 비교해보도록 하겠습니다.

encoded_imgs = encoder.predict(x_test_flatten)
decoded_imgs = decoder.predict(encoded_imgs)
# for testing decoded_imgs matched valid_imgs
valid_imgs = autoencoder.predict(x_test_flatten)

valid_imgs는 정상적으로 encoder와 decoder를 분리했는지 확인하기 위해서 autoencoder를 통한 결과와 encoder와 decoder를 각각 거친 결과가 동일한지 확인해보았습니다.

import matplotlib.pyplot as plt

n = 10  # 이미지 갯수
plt.figure(figsize=(20, 4))
for i in range(1, n+1):
    # 원본 데이터
    ax = plt.subplot(2, n, i)
    plt.imshow(x_test_flatten[i].reshape(28, 28))
    plt.gray()
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)

    # 재구성된 데이터
    ax = plt.subplot(2, n, i + n)
    plt.imshow(decoded_imgs[i].reshape(28, 28))
    plt.gray()
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
plt.show()

위의 줄이 원본 데이터이고, 아래 줄이 오토인코더를 통해 재구성된 데이터입니다. 간단한 모델로는 꽤 손실이 있음을 볼 수 있습니다.

 

방금 예제에서는 출력이 hidden layer의 unit크기(32)에만 영향을 받았고, 이런 상황에서는 PCA와 비슷하게 학습합니다. 조금 더 효율적인 압축을 위해서는 sparsity(희소성)를 추가해주는 방법이 있습니다.

Tensorflow에서 regularization을 적용하여 학습할 수 있습니다.(100 epochs로 진행)

# encoding dimension
encoding_dim = 32

input_img = Input(shape=(784,)) # 28*28
encoded = Dense(encoding_dim, activation='relu',
               kernel_regularizer=tf.keras.regularizers.l1(10e-5))(input_img)
decoded = Dense(784, activation='sigmoid')(encoded)

# autoencoder
autoencoder = Model(input_img, decoded)

# encoder
encoder = Model(input_img, encoded)

# decoder
encoded_input = Input(shape=(encoding_dim,))
decoded_layer = autoencoder.layers[-1]
decoder = Model(encoded_input, decoded_layer(encoded_input))

autoencoder.compile(optimizer='adam', loss='binary_crossentropy')
autoencoder.fit(x_train_flatten, x_train_flatten, 
                batch_size=256, epochs=100, 
                validation_data=(x_test_flatten,x_test_flatten))
encoded_imgs = encoder.predict(x_test_flatten)
decoded_imgs = decoder.predict(encoded_imgs)

n = 10  # 이미지 갯수
plt.figure(figsize=(20, 4))
for i in range(1, n+1):
    # 원본 데이터
    ax = plt.subplot(2, n, i)
    plt.imshow(x_test_flatten[i].reshape(28, 28))
    plt.gray()
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)

    # 재구성된 데이터
    ax = plt.subplot(2, n, i + n)
    plt.imshow(decoded_imgs[i].reshape(28, 28))
    plt.gray()
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
plt.show()

이전 모델과 큰 차이가 없습니다. 다만, 인코딩된 데이터의 sparsity가 조금 다릅니다. 

encoded_imgs.mean()를 입력하면 이전 결과는 8.79의 값을 얻었지만, 이 모델은 1.27의 값을 얻었습니다.

 

Deep autoencoder

이번에는 조금 더 Deep한 autoencoder 모델을 사용해보겠습니다.

# encoding dimension
encoding_dim = 32

input_img = Input(shape=(784,)) # 28*28
encoded = Dense(128, activation='relu')(input_img)
encoded = Dense(64, activation='relu')(encoded)
encoded = Dense(encoding_dim, activation='relu')(encoded)

decoded = Dense(64, activation='relu')(encoded)
decoded = Dense(128, activation='relu')(decoded)
decoded = Dense(784, activation='sigmoid')(decoded)

# autoencoder
autoencoder = Model(input_img, decoded)

# encoder
encoder = Model(input_img, encoded)

# decoder
encoded_input = Input(shape=(encoding_dim,))
decoded_layer = autoencoder.layers[4](encoded_input)
decoded_layer = autoencoder.layers[5](decoded_layer)
decoded_layer = autoencoder.layers[6](decoded_layer)
decoder = Model(encoded_input, decoded_layer)

autoencoder.compile(optimizer='adam', loss='binary_crossentropy')
autoencoder.fit(x_train_flatten, x_train_flatten, 
                batch_size=256, epochs=100, 
                validation_data=(x_test_flatten,x_test_flatten))
encoded_imgs = encoder.predict(x_test_flatten)
decoded_imgs = decoder.predict(encoded_imgs)

n = 10  # 이미지 갯수
plt.figure(figsize=(20, 4))
for i in range(1, n+1):
    # 원본 데이터
    ax = plt.subplot(2, n, i)
    plt.imshow(x_test_flatten[i].reshape(28, 28))
    plt.gray()
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)

    # 재구성된 데이터
    ax = plt.subplot(2, n, i + n)
    plt.imshow(decoded_imgs[i].reshape(28, 28))
    plt.gray()
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
plt.show()

100 epoch 에서, train/test loss가 0.8까지 도달합니다. 이전 모델보다 훨씬 향상되었고, 이미지 또한 훨씬 선명해진 것 같습니다.

 

Convolutional autoencoder

입력이 이미지이기 때문에 CNN 구조로 인코더/디코더를 구성할 수 있습니다. 실제로 이미지에 적용되는 autoencoder는 항상 convolutional autoencoder입니다. 성능이 더 좋기 때문이죠.

 

인코더는 Conv2D와 MaxPooling2D로 구성되고, 디코더는 Conv2D와 UpSampling2D로 구성됩니다.

그리고 입력이 (28, 28, 1)의 차원을 가지므로, 데이터의 차원을 변경해줍니다.

x_train = x_train.reshape((-1,28,28,1))
x_test = x_test.reshape((-1,28,28,1))
print(x_train.shape)
print(x_test.shape)
input_img = Input(shape=(28, 28, 1))  # 'channels_firtst'이미지 데이터 형식을 사용하는 경우 이를 적용

x = Conv2D(16, (3, 3), activation='relu', padding='same')(input_img)
x = MaxPooling2D((2, 2), padding='same')(x)
x = Conv2D(8, (3, 3), activation='relu', padding='same')(x)
x = MaxPooling2D((2, 2), padding='same')(x)
x = Conv2D(8, (3, 3), activation='relu', padding='same')(x)
encoded = MaxPooling2D((2, 2), padding='same')(x)

# encoder의 shape = (samples, 4, 4, 8)

x = Conv2D(8, (3, 3), activation='relu', padding='same')(encoded)
x = UpSampling2D((2, 2))(x)
x = Conv2D(8, (3, 3), activation='relu', padding='same')(x)
x = UpSampling2D((2, 2))(x)
x = Conv2D(16, (3, 3), activation='relu')(x)
x = UpSampling2D((2, 2))(x)
decoded = Conv2D(1, (3, 3), activation='sigmoid', padding='same')(x)

# decoder의 shape = (sampels, 28, 28, 1)

autoencoder = Model(input_img, decoded)
autoencoder.compile(optimizer='adam', loss='binary_crossentropy')

encoder = Model(input_img, encoded)

decoded_input = Input(shape=(4,4,8))
decoded_layer = autoencoder.layers[7](decoded_input)
for i in range(8,len(autoencoder.layers)):
    decoded_layer = autoencoder.layers[i](decoded_layer)
decoder = Model(decoded_input, decoded_layer)

from tensorflow.keras.callbacks import TensorBoard
autoencoder.fit(x_train, x_train, 
                batch_size=256, epochs=100, 
                validation_data=(x_test,x_test),
                callbacks=[TensorBoard(log_dir='/tmp/autoencoder')])

0.899의 loss로 수렴하고 있습니다.

주황 line : train loss / 파란 line : val loss

재구성된 데이터를 살펴보겠습니다.

encoded_imgs = encoder.predict(x_test)
decoded_imgs = decoder.predict(encoded_imgs)

n = 10  # 이미지 갯수
plt.figure(figsize=(20, 4))
for i in range(1, n+1):
    # 원본 데이터
    ax = plt.subplot(2, n, i)
    plt.imshow(x_test_flatten[i].reshape(28, 28))
    plt.gray()
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)

    # 재구성된 데이터
    ax = plt.subplot(2, n, i + n)
    plt.imshow(decoded_imgs[i].reshape(28, 28))
    plt.gray()
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
plt.show()

또한, 인코딩된 데이터도 살펴볼 수 있습니다. 인코딩 데이터의 차원은 (8,4,4) 이기 때문에, grayscale로 (4,32)로 reshape해서 표현해보도록 하겠습니다.

n = 10
plt.figure(figsize=(20, 8))
for i in range(n):
    ax = plt.subplot(1, n, i+1)
    plt.imshow(encoded_imgs[i].reshape(4, 4 * 8).T)
    plt.gray()
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
plt.show()

무엇을 의미하는지는 잘 모르겠군요...

 

이미지 노이즈 제거

아까 글 초반부에 노이즈를 제거하는데 오토인코더를 사용할 수 있다고 했습니다. 

denosing을 할 수 있도록 학습하는데, 노이지한 이미지를 클린한 이미지로 매핑하도록 훈련시켜보겠습니다.

 

우선 원본 데이터에 가우스 노이즈 행렬을 적용해서 노이지한 사진으로 합성하겠습니다.

noise_factor = 0.5
x_train_noisy = x_train + noise_factor * np.random.normal(loc=0.0, scale=1.0, size=x_train.shape) 
x_test_noisy = x_test + noise_factor * np.random.normal(loc=0.0, scale=1.0, size=x_test.shape) 

x_train_noisy = np.clip(x_train_noisy, 0., 1.)
x_test_noisy = np.clip(x_test_noisy, 0., 1.)

n = 10
plt.figure(figsize=(20, 2))
for i in range(1, n+1):
    ax = plt.subplot(1, n, i)
    plt.imshow(x_test_noisy[i].reshape(28, 28))
    plt.gray()
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
plt.show()

이전의 Convolutional autoencoder를 조금 변형하도록 하겠습니다. 재구성된 이미지의 질을 향상시키기 위해서는 layer당 더 많은 필터를 사용합니다.

input_img = Input(shape=(28, 28, 1))  # 'channels_firtst'이미지 데이터 형식을 사용하는 경우 이를 적용

x = Conv2D(32, (3, 3), activation='relu', padding='same')(input_img)
x = MaxPooling2D((2, 2), padding='same')(x)
x = Conv2D(32, (3, 3), activation='relu', padding='same')(x)
encoded = MaxPooling2D((2, 2), padding='same')(x)

# encoder의 shape = (samples, 4, 4, 8)

x = Conv2D(32, (3, 3), activation='relu', padding='same')(encoded)
x = UpSampling2D((2, 2))(x)
x = Conv2D(32, (3, 3), activation='relu', padding='same')(x)
x = UpSampling2D((2, 2))(x)
decoded = Conv2D(1, (3, 3), activation='sigmoid', padding='same')(x)

# decoder의 shape = (sampels, 28, 28, 1)

autoencoder = Model(input_img, decoded)
autoencoder.compile(optimizer='adam', loss='binary_crossentropy')

encoder = Model(input_img, encoded)

decoded_input = Input(shape=(7,7,32))
decoded_layer = autoencoder.layers[5](decoded_input)
for i in range(6,len(autoencoder.layers)):
    decoded_layer = autoencoder.layers[i](decoded_layer)
decoder = Model(decoded_input, decoded_layer)

autoencoder.fit(x_train_noisy, x_train, 
                batch_size=256, epochs=100, 
                validation_data=(x_test_noisy,x_test),
                callbacks=[TensorBoard(log_dir='/tmp/autoencoder', histogram_freq=0, write_graph=False)])
encoded_imgs = encoder.predict(x_test_noisy)
decoded_imgs = decoder.predict(encoded_imgs)

n = 10  # 이미지 갯수
plt.figure(figsize=(20, 4))
for i in range(1, n+1):
    # 원본 데이터
    ax = plt.subplot(2, n, i)
    plt.imshow(x_test_noisy[i])
    plt.gray()
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)

    # 재구성된 데이터
    ax = plt.subplot(2, n, i + n)
    plt.imshow(decoded_imgs[i])
    plt.gray()
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
plt.show()

노이즈가 있는 숫자가 클린하게 재구성되었습니다. 

이와 관련해서 www.kaggle.com/c/denoising-dirty-documents 에서 dirty document를 재구성하는 모델을 연습할 수 있습니다.

 

지금까지 오토인코더에 대해서 알아봤는데, 다음에 기회가 된다면 Sequence-to-sequence autoencoder와 Variational autoencoder(VAE)에 대해서도 알아보도록 하겠습니다.

댓글