본문 바로가기
Deep Learning/Hands On Machine Learning

17.3 Stacked AutoEncoder

by 대소기 2021. 12. 23.

Stacked AutoEncoder란

* 우리가 최초로 17.1 에서 살펴보았던 autoencoder의 구조의 경우 input layer, hidden layer, output layer로 이뤄진 가장 기본적인 autoencoder였다. 여기서 hidden layer의 개수를 늘린 것을 stacked autoencoder 혹은 deep autoencoder라고 부른다.

* 이렇게 hidden layer를 늘리는 것은 autoencoder가 더 복잡한 처리를 가능하게 하겠지만, 한 편으로 일반적으로 network가 deep해질 때 생기는 overfitting등의 문제가 발생할 수 있기 때문에 과도하게 deep한 network를 구성하지 않도록 주의해야 한다.

케라스를 이용하여 stacked autoencoder 구현하기

데이터셋 로드

(X_train_full, y_train_full), (X_test, y_test) = keras.datasets.fashion_mnist.load_data()
X_train_full = X_train_full.astype(np.float32) / 255
X_test = X_test.astype(np.float32) / 255
X_train, X_valid = X_train_full[:-5000], X_train_full[-5000:]
y_train, y_valid = y_train_full[:-5000], y_train_full[-5000:]

* keras에서 제공하는 fasion mnist dataset을 활용해 stacked autoencoder를 구성해보겠다.

tf.random.set_seed(42)
np.random.seed(42)

stacked_encoder = keras.models.Sequential([
    keras.layers.Flatten(input_shape=[28, 28]),
    keras.layers.Dense(100, activation="selu"),
    keras.layers.Dense(30, activation="selu"),
])
stacked_decoder = keras.models.Sequential([
    keras.layers.Dense(100, activation="selu", input_shape=[30]),
    keras.layers.Dense(28 * 28, activation="sigmoid"),
    keras.layers.Reshape([28, 28])
])
stacked_ae = keras.models.Sequential([stacked_encoder, stacked_decoder])
stacked_ae.compile(loss="binary_crossentropy",
                   optimizer=keras.optimizers.SGD(learning_rate=1.5), metrics=[rounded_accuracy])
history = stacked_ae.fit(X_train, X_train, epochs=20,
                         validation_data=(X_valid, X_valid))

* 구조는 앞서 살펴본 autoencoder의 hidden layer 개수를 늘린 것에 불과하기 때문에 이해하기 쉬울 것이다. 다만, 3차원 vector와는 달리, fashion mnist data는 28*28 크기의 image가 input되었기 때문에 마지막에 28*28로 reshape을 해준다는 것이 차이점이다.

* 또한 loss로 binary crossentropy를 사용했는데, 이는 recontruction 작업을 다중 레이블 이진 분류 문제로 다루기 때문이다. 각 픽셀의 강도는 픽셀이 검정일 확률을 나타낸다.

17.3.2 재구성 시각화

def plot_image(image):
    plt.imshow(image, cmap="binary")
    plt.axis("off")

def show_reconstructions(model, images=X_valid, n_images=5):
    reconstructions = model.predict(images[:n_images])
    fig = plt.figure(figsize=(n_images * 1.5, 3))
    for image_index in range(n_images):
        plt.subplot(2, n_images, 1 + image_index)
        plot_image(images[image_index])
        plt.subplot(2, n_images, 1 + n_images + image_index)
        plot_image(reconstructions[image_index])

* input data와 output data를 비교해본 결과 원본의 형태는 알아볼 수 있지만, 디테일한 부분들은 복원되지 않은 것을 확인하였다. 이는 network를 더 deep하게 구성하거나, coding의 차원 크기를 더 크게 구성하는 등의 방법으로 개선할 수 있다.

17.3.3 fasion MNIST 데이터셋 시각화

* 위 결과에서 알 수 있듯이 autoencoder를 사용한 차원 축소에는 많은 정보 손실이 발생한다. 차원 축소라는 측면에서만 보면 PCA, t-SNE 등의 차원 축소 알고리즘이 더 효율적이라고 할 수 있다.

* 하지만, 다른 알고리즘들보다 autoencoder를 사용해 차원 축소를 할 때 대용량의 데이터를 처리하기 용이하다는 장점이 있다. 이러한 장단점을 활용하여 차원 축소를 진행한다면, autoencoder로 대용량의 데이터를 input으로 받아 적절한 수준까지(정보가 과다하게 손실되지 않을 수준까지) 차원 축소를 진행하고, 더욱 저차원으로 차원 축소를 진행할 때는 다른 알고리즘을 사용하는 방법을 사용할 수 있다.

np.random.seed(42)

from sklearn.manifold import TSNE

X_valid_compressed = stacked_encoder.predict(X_valid) # 훈련된 모델로 30차원까지 축소
tsne = TSNE()
X_valid_2D = tsne.fit_transform(X_valid_compressed) # tsne를 통해 2차원까지 축소
X_valid_2D = (X_valid_2D - X_valid_2D.min()) / (X_valid_2D.max() - X_valid_2D.min())

plt.scatter(X_valid_2D[:, 0], X_valid_2D[:, 1], c=y_valid, s=10, cmap="tab10")
plt.axis("off")
plt.show()

17.3.4 Stacked AutoEncoder를 이용한 비지도 사전훈련

* 만약 데이터의 일부만 정답 label이 존재한다면, AutoEncoder를 사용해 labeling을 시행할 수 있다.

* 먼저 전체 데이터를 통해 autoencoder 전체를 훈련한다. encoder로 coding을 추출하고, coding으로 encoder의 input과 최대한 유사하게 decoder의 output을 도출하는 훈련을 통해 적절하게 차원을 축소하여 특징을 도출해내는 encoder를 구성한다. 이후 encoder 부분의 훈련된 parameter들을 복사해 새로운 모델에 적용한다.

* 이 새로운 모델의 output layer에는 softmax함수를 적용한다. labeling된 데이터를 집어넣어서 softmax를 통해 올바른 class를 예측하는 훈련을 한다(만약 labeling 된 데이터의 개수가 매우 적으면, 하위 hidden layer의 가중치를 동결시키는 방법을 사용한다). 이 classifier를 가지고 labeling 되지 않은 데이터들을 labeling한다.

17.3.5 가중치 묶기

* 방금 살펴본 것과 같이 autoencoder의 구조가 완벽하게 대칭이라면 encoder의 graident와 decoder의 gradient를 묶어줄 수 있다. 더 쉽게 설명하기 위해, 17장 최초에 살펴봤던 autoencoder를 예로 들어보겠다. 이 autoencoder는 3차원 vector를 집어넣어 2차원 vector로 축소하고, 2차원 vector를 다시 3차원 vector로 복원하는 모델이다. 때문에 encoder부분의 weight matrix는 3x2 matrix일 것이다. 반대로 decoder는 2차원 vector를 3차원 vector로 변환해야 하기 때문에 2x3 matrix를 weight로 사용할 것이다. 이 때 encoder가 하는 작업의 반대 작업을 decoder가 하기 때문에 단순히 encoder의 3x2 weight matrix를 전치시킨 2x3 matrix를 decoder의 weight matrix로 사용할 수 있게 된다.

* 위 설명한 내용을 일반화 하자면 이렇게 표현할 수 있다. N개의 층을 가진 autoencoder의 N번째 층의 weight를 $W_L$이라고 할 때, $W_{N-L+1} = {W_L}^T$ $(L=1,2, ... , N/2)$ 이다.

 

class DenseTranspose(keras.layers.Layer):
    def __init__(self, dense, activation=None, **kwargs):
        self.dense = dense
        self.activation = keras.activations.get(activation)
        super().__init__(**kwargs)
    def build(self, batch_input_shape):
        self.biases = self.add_weight(name="bias",
                                      shape=[self.dense.input_shape[-1]],
                                      initializer="zeros")
        super().build(batch_input_shape)
    def call(self, inputs):
        z = tf.matmul(inputs, self.dense.weights[0], transpose_b=True)#입력된 dense의 weight를 transpose해 곱함
        return self.activation(z + self.biases) 
        

keras.backend.clear_session()
tf.random.set_seed(42)
np.random.seed(42)

dense_1 = keras.layers.Dense(100, activation="selu")
dense_2 = keras.layers.Dense(30, activation="selu")

tied_encoder = keras.models.Sequential([
    keras.layers.Flatten(input_shape=[28, 28]),
    dense_1,
    dense_2
])

tied_decoder = keras.models.Sequential([
    DenseTranspose(dense_2, activation="selu"),
    DenseTranspose(dense_1, activation="sigmoid"),
    keras.layers.Reshape([28, 28])
])

tied_ae = keras.models.Sequential([tied_encoder, tied_decoder])

tied_ae.compile(loss="binary_crossentropy",
                optimizer=keras.optimizers.SGD(learning_rate=1.5), metrics=[rounded_accuracy])
history = tied_ae.fit(X_train, X_train, epochs=10,
                      validation_data=(X_valid, X_valid))

# Epoch 1/10
# 1719/1719 [==============================] - 8s 4ms/step - loss: 0.3269 - rounded_accuracy: 0.8960 - val_loss: 0.3083 - val_rounded_accuracy: 0.9074
# Epoch 2/10
# 1719/1719 [==============================] - 7s 4ms/step - loss: 0.2975 - rounded_accuracy: 0.9224 - val_loss: 0.2950 - val_rounded_accuracy: 0.9285
# Epoch 3/10
# 1719/1719 [==============================] - 7s 4ms/step - loss: 0.2920 - rounded_accuracy: 0.9274 - val_loss: 0.3018 - val_rounded_accuracy: 0.9085
# Epoch 4/10
# 1719/1719 [==============================] - 8s 4ms/step - loss: 0.2889 - rounded_accuracy: 0.9302 - val_loss: 0.2880 - val_rounded_accuracy: 0.9333
# Epoch 5/10
# 1719/1719 [==============================] - 7s 4ms/step - loss: 0.2865 - rounded_accuracy: 0.9325 - val_loss: 0.2873 - val_rounded_accuracy: 0.9316
# Epoch 6/10
# 1719/1719 [==============================] - 8s 4ms/step - loss: 0.2850 - rounded_accuracy: 0.9340 - val_loss: 0.2861 - val_rounded_accuracy: 0.9354
# Epoch 7/10
# 1719/1719 [==============================] - 8s 4ms/step - loss: 0.2838 - rounded_accuracy: 0.9350 - val_loss: 0.2859 - val_rounded_accuracy: 0.9365
# Epoch 8/10
# 1719/1719 [==============================] - 8s 5ms/step - loss: 0.2830 - rounded_accuracy: 0.9357 - val_loss: 0.2837 - val_rounded_accuracy: 0.9368
# Epoch 9/10
# 1719/1719 [==============================] - 7s 4ms/step - loss: 0.2823 - rounded_accuracy: 0.9363 - val_loss: 0.2863 - val_rounded_accuracy: 0.9293
# Epoch 10/10
# 1719/1719 [==============================] - 7s 4ms/step - loss: 0.2817 - rounded_accuracy: 0.9369 - val_loss: 0.2848 - val_rounded_accuracy: 0.9318

* encoder부분의 weight를 transpose해서 decoder부분의 weight로 사용한 모델을 훈련시켰다.

 

show_reconstructions(tied_ae)
plt.show()

 

17.3.6 한 번에 AutoEncoder 한 개씩 훈련하기

* 위와 같이 stacked autoencoder를 층 별로 훈련시키는 것이 가능하다. phase 1에서 훈련된 encoder를 통해 input data에서 차원이 축소된 phase 2의 training set을 구성한다. phase 2의 autoencoder를 훈련시키고 마지막 phase 3에서 훈련된 parameter들을 복사해서 stacked autoencoder를 구성한다. 

* 이러한 방식은 요즘에 많이 사용되지는 않지만, greedy layerwise training관련 논문에서 종종 언급되는 방식이기 때문에 알아두면 좋다.

 

def train_autoencoder(n_neurons, X_train, X_valid, loss, optimizer,
                      n_epochs=10, output_activation=None, metrics=None):
    n_inputs = X_train.shape[-1]
    encoder = keras.models.Sequential([
        keras.layers.Dense(n_neurons, activation="selu", input_shape=[n_inputs])
    ])
    decoder = keras.models.Sequential([
        keras.layers.Dense(n_inputs, activation=output_activation),
    ])
    autoencoder = keras.models.Sequential([encoder, decoder])
    autoencoder.compile(optimizer, loss, metrics=metrics)
    autoencoder.fit(X_train, X_train, epochs=n_epochs,
                    validation_data=(X_valid, X_valid))
    return encoder, decoder, encoder(X_train), encoder(X_valid)
    

tf.random.set_seed(42)
np.random.seed(42)

K = keras.backend
X_train_flat = K.batch_flatten(X_train) # .reshape(-1, 28*28)과 같음.
X_valid_flat = K.batch_flatten(X_valid)
# 100차원으로 축소하는 autoencoder
enc1, dec1, X_train_enc1, X_valid_enc1 = train_autoencoder(
    100, X_train_flat, X_valid_flat, "binary_crossentropy", #output층을 사용할 것이기 때문에 loss binary_crossentropy
    keras.optimizers.SGD(learning_rate=1.5), output_activation="sigmoid",
    metrics=[rounded_accuracy])

# 30차원으로 축소하는 autoencoder
# 입력은 위 autoencoder의 encoder를 통해 축소한 데이터
enc2, dec2, _, _ = train_autoencoder(
    30, X_train_enc1, X_valid_enc1, "mse", keras.optimizers.SGD(learning_rate=0.05), #input과 output이 다른지 확인하기 위해 mse
    output_activation="selu")
    
# Epoch 1/10
# 1719/1719 [==============================] - 8s 4ms/step - loss: 0.3445 - rounded_accuracy: 0.8874 - val_loss: 0.3123 - val_rounded_accuracy: 0.9146
# Epoch 2/10
# 1719/1719 [==============================] - 7s 4ms/step - loss: 0.3039 - rounded_accuracy: 0.9203 - val_loss: 0.3006 - val_rounded_accuracy: 0.9246
# Epoch 3/10
# 1719/1719 [==============================] - 7s 4ms/step - loss: 0.2949 - rounded_accuracy: 0.9286 - val_loss: 0.2934 - val_rounded_accuracy: 0.9317
# Epoch 4/10
# 1719/1719 [==============================] - 7s 4ms/step - loss: 0.2891 - rounded_accuracy: 0.9342 - val_loss: 0.2888 - val_rounded_accuracy: 0.9363
# Epoch 5/10
# 1719/1719 [==============================] - 7s 4ms/step - loss: 0.2853 - rounded_accuracy: 0.9378 - val_loss: 0.2857 - val_rounded_accuracy: 0.9392
# Epoch 6/10
# 1719/1719 [==============================] - 7s 4ms/step - loss: 0.2827 - rounded_accuracy: 0.9403 - val_loss: 0.2834 - val_rounded_accuracy: 0.9409
# Epoch 7/10
# 1719/1719 [==============================] - 7s 4ms/step - loss: 0.2807 - rounded_accuracy: 0.9422 - val_loss: 0.2817 - val_rounded_accuracy: 0.9427
# Epoch 8/10
# 1719/1719 [==============================] - 11s 6ms/step - loss: 0.2792 - rounded_accuracy: 0.9437 - val_loss: 0.2803 - val_rounded_accuracy: 0.9440
# Epoch 9/10
# 1719/1719 [==============================] - 11s 7ms/step - loss: 0.2779 - rounded_accuracy: 0.9449 - val_loss: 0.2792 - val_rounded_accuracy: 0.9450
# Epoch 10/10
# 1719/1719 [==============================] - 12s 7ms/step - loss: 0.2769 - rounded_accuracy: 0.9459 - val_loss: 0.2783 - val_rounded_accuracy: 0.9462
# Epoch 1/10
# 1719/1719 [==============================] - 6s 3ms/step - loss: 0.5620 - val_loss: 0.3469
# Epoch 2/10
# 1719/1719 [==============================] - 6s 3ms/step - loss: 0.2612 - val_loss: 0.2362
# Epoch 3/10
# 1719/1719 [==============================] - 6s 3ms/step - loss: 0.2248 - val_loss: 0.2174
# Epoch 4/10
# 1719/1719 [==============================] - 7s 4ms/step - loss: 0.2109 - val_loss: 0.2058
# Epoch 5/10
# 1719/1719 [==============================] - 11s 7ms/step - loss: 0.2035 - val_loss: 0.1973
# Epoch 6/10
# 1719/1719 [==============================] - 9s 5ms/step - loss: 0.1987 - val_loss: 0.1978
# Epoch 7/10
# 1719/1719 [==============================] - 7s 4ms/step - loss: 0.1971 - val_loss: 0.1992
# Epoch 8/10
# 1719/1719 [==============================] - 12s 7ms/step - loss: 0.1955 - val_loss: 0.2004
# Epoch 9/10
# 1719/1719 [==============================] - 9s 5ms/step - loss: 0.1949 - val_loss: 0.1933
# Epoch 10/10
# 1719/1719 [==============================] - 6s 4ms/step - loss: 0.1940 - val_loss: 0.1952

* autoencoder를 2개 구성하여 모델을 훈련시켰다. 주의할 점은 loss의 경우 이전에 살펴보았던 그림의 phase 1에 해당하는 autoencoder는 binary crossentropy를 사용해야 하고, phase 2에 해당하는 autoencoder는 mse를 사용해야 한다는 점이다. phase 1의 model의 output은 fashion mnist data를 분류해야 하기 때문에 binary crossentropy를 loss로 사용해야 하지만, phase 2의 model은 단순히 입력 vector와 출력 vector간의 차이를 사용해 학습해야 하기 때문에 loss로 mse를 사용한다.

 

# 훈련된 encoder decoder들을 쌓아서 새로운 autoencoder 생성
stacked_ae_1_by_1 = keras.models.Sequential([
    keras.layers.Flatten(input_shape=[28, 28]),
    enc1, enc2, dec2, dec1,
    keras.layers.Reshape([28, 28])
])

show_reconstructions(stacked_ae_1_by_1)
plt.show()