[tensorflow] MNIST를 이용한 오토인코더 (Auto Encoder)

 

MNIST 필기 숫자 데이터를 가지고 오토인코더를 구현한다.

📃 Tensorflow 라이브러리에서 MNIST 데이터를 가져올 수 있다.

전체 코드


import numpy as np
from tensorflow.keras.datasets import mnist
from tensorflow.keras.layers import Input, Dense, Flatten, Reshape, Conv2D, Conv2DTranspose
from tensorflow.keras.models import Model
from tensorflow.keras import backend as K

#MNIST 데이터 읽고 신경망 입력 준비
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train = x_train.astype('float32')/255.
y_train = y_train.astype('float32')/255.
x_train = np.reshape(x_train, (len(x_train),28,28,1))
x_test = np.reshape(x_test, (len(x_test),28,28,1))

zdim = 32 #잠복 공간의 차원

#오토인코더의 인코더 부분 설계
encoder_input = Input(shape=(28,28,1))
x= Conv2D(32,(3,3),activation='relu', padding='same', strides=(1,1))(encoder_input)
x= Conv2D(64,(3,3),activation='relu', padding='same', strides=(2,2))(x)
x= Conv2D(64,(3,3),activation='relu', padding='same', strides=(2,2))(x)
x= Conv2D(64,(3,3),activation='relu', padding='same', strides=(1,1))(x)
x= Flatten()(x)
encoder_output= Dense(zdim)(x)
model_encoder = Model(encoder_input, encoder_output)
model_encoder.summary()

#오토인코더의 디코더 부분 설계
decoder_input = Input(shape=(zdim,))
x=Dense(3136)(decoder_input)
x=Reshape((7,7,64))(x)
x=Conv2DTranspose(64,(3,3),activation='relu', padding='same', strides=(1,1))(x)
x=Conv2DTranspose(64,(3,3),activation='relu', padding='same', strides=(2,2))(x)
x=Conv2DTranspose(32,(3,3),activation='relu', padding='same', strides=(2,2))(x)
x=Conv2DTranspose(1,(3,3),activation='relu', padding='same', strides=(1,1))(x)
decoder_output=x
model_decoder = Model(decoder_input, decoder_output)
model_decoder.summary()

#인코더와 디코더를 결합하여 오토인코더 모델 구축
model_input = encoder_input
model_output = model_decoder(encoder_output)
model= Model(model_input, model_output)

#오토인코더 학습
model.compile(optimizer='Adam', loss ='mse')
model.fit(x_train, x_train, epochs=5, batch_size = 128, shuffle=True, validation_data=(x_test, x_test))

#복원 실험 1: x_test를 복원하는 예측 실험
decoded_img = model.predict(x_test)

import matplotlib.pyplot as plt

n=10
plt.figure(figsize=(20,4))
for i in range(n):
    plt.subplot(2,n,i+1)
    plt.imshow(x_test[i].reshape(28,28),cmap='gray')
    plt.xticks([]); plt.yticks([]);
    plt.subplot(2, n, i+n+1)
    plt.imshow(decoded_img[i].reshape(28,28),cmap='gray')
    plt.xticks([]); plt.yticks([]); 
plt.show()

 

실행결과


오토인코더가 샘플 10개를 모두 비슷하게 재현했음을 확인할 수 있다.

 

코드 해설


📌 오토인코더의 구조

오토인코더는 인코더와 디코더로 구성되는데, 인코더는 차원을 점점 줄이며 디코더는 차원을 점점 늘려 출력층에서 원래 차원을 회복한다.

 

 

📢 프로그램에 필요한 라이브러리

import numpy as np
from tensorflow.keras.datasets import mnist
from tensorflow.keras.layers import Input, Dense, Flatten, Reshape, Conv2D, Conv2DTranspose
from tensorflow.keras.models import Model
from tensorflow.keras import backend as K

위 라이브러리를 import 하고 이제 코드를 하나씩 살펴보자

 

데이터 준비

(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train = x_train.astype('float32')/255.
y_train = y_train.astype('float32')/255.
x_train = np.reshape(x_train, (len(x_train),28,28,1))
x_test = np.reshape(x_test, (len(x_test),28,28,1))

신경망에 입력할 수 있는 형태로 변환하는 코드이다.

x_trian은 아래와 같이 배열에 정수들이 담겨져있는데 이를 float형으로 바꾸고 255로 나눠 정규화 (x_test도 똑같이)

또한 x_train과 x_test배열은 각각 (60000, 28, 28), (10000, 28, 28)의 shape를 가지는데 이를 (60000, 28, 28,1), (10000, 28, 28,1)로 확장하였다.

 

인코더 구현

x= Conv2D(32,(3,3),activation='relu', padding='same', strides=(1,1))(encoder_input)
x= Conv2D(64,(3,3),activation='relu', padding='same', strides=(2,2))(x)
x= Conv2D(64,(3,3),activation='relu', padding='same', strides=(2,2))(x)
x= Conv2D(64,(3,3),activation='relu', padding='same', strides=(1,1))(x)
x= Flatten()(x)
encoder_output= Dense(zdim)(x)
model_encoder = Model(encoder_input, encoder_output)

C-C-C-C-FC의 다섯 층을 쌓아 인코더를 구현한 부분이다.

이전까지는 층을 쌓을때 model.add(Conv2D(...))의 방식으로 코딩하였다.  Sequential 방식

여기서는 x = Conv2D(...)(x) 방식으로 코딩한다. Functional API 방식

<Sequential 방식과 Functional API 방식>

아래 표는 C-C-FC 구조의 신경망을 설계하는 각각의 방식이다.
Sequential 에서는 중간 결과를 따로 빼내어 사용할 방법이 없다.
따라서 출력이 하나이면 충분한 상황에서 사용한다

Functinal API 에서는 예제처럼 x1,x2,x3 이라는 서로 다른 객체에 중간 결과를 저장할 수 있음.
중간 결과에 접근하여 또 다른 데이터 흐름을 만들 수 있음.
즉, 이 방식에서는 데이터의 흐름을 여러 줄기로 나누어 신경망이 여러 개의 데이터를 출력하게 만들 수 있다.

오토인코더에서는 중간에 있는 잠복 공간에 접근해야만 생성 모델로 활용할 수 있기 때문에 Functianl API 방식을 사용함

 

위 코드를 하나씩 보자

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

MNIST가 28*28*1의 크기를 갖기 때문에 이를 입력 크기로 주었음

x= Conv2D(32,(3,3),activation='relu', padding='same', strides=(1,1))(encoder_input)

3*3 커널이 32개인 컨볼루션을 적용함, 보폭이 1이기 때문에 28*28*32 텐서가 됨

x= Conv2D(64,(3,3),activation='relu', padding='same', strides=(2,2))(x)

보폭이 2이므로 14*14*64 텐서가 됨

x= Conv2D(64,(3,3),activation='relu', padding='same', strides=(2,2))(x)

보폭이 2이므로 7*7*64 텐서가 됨

x= Conv2D(64,(3,3),activation='relu', padding='same', strides=(1,1))(x)

보폭이 1이므로 7*7*64 텐서가 됨

x= Flatten()(x)

위의 7*7*64 텐서를 일려로 펼쳐 3136 크기의 1차원 텐서로 변환

encoder_output= Dense(zdim)(x)

완전연결층을 통해 zdim크기의 텐서로 축소

model_encoder = Model(encoder_input, encoder_output)

Model 함수를 사용해 encoder_input을 입력으로 받고 encoder_output을 출력하는 모델을 생성한다.

 

이렇게하면 위 그림에서 왼쪽 부분을 구현한 것이다.

이제 오른쪽 부분인 디코더를 구현하자.

 

디코더 구현

decoder_input = Input(shape=(zdim,))
x=Dense(3136)(decoder_input)
x=Reshape((7,7,64))(x)
x=Conv2DTranspose(64,(3,3),activation='relu', padding='same', strides=(1,1))(x)
x=Conv2DTranspose(64,(3,3),activation='relu', padding='same', strides=(2,2))(x)
x=Conv2DTranspose(32,(3,3),activation='relu', padding='same', strides=(2,2))(x)
x=Conv2DTranspose(1,(3,3),activation='relu', padding='same', strides=(1,1))(x)
decoder_output=x
model_decoder = Model(decoder_input, decoder_output)
model_decoder.summary()

위 코드도 하나씩 살펴보자.

decoder_input = Input(shape=(zdim,))

zdim 크기의 텐서를 디코더의 입력으로 설정한다.

x=Dense(3136)(decoder_input)

완전연결층을 이용해 3136 크기의 텐서로 확장

x=Reshape((7,7,64))(x)

컨볼루션층에 입력할 수 있도록 7*7*64 텐서로 변환함 (Flatten의 역이라고 보자)

이처럼 앞으로도 인코더의 역으로 컨볼루션층을 쌓는다.

x=Conv2DTranspose(64,(3,3),activation='relu', padding='same', strides=(1,1))(x)
x=Conv2DTranspose(64,(3,3),activation='relu', padding='same', strides=(2,2))(x)
x=Conv2DTranspose(32,(3,3),activation='relu', padding='same', strides=(2,2))(x)
x=Conv2DTranspose(1,(3,3),activation='relu', padding='same', strides=(1,1))(x)

이렇게 하면 원래 패턴과 같은 28*28*1 텐서가 된다.

model_decoder = Model(decoder_input, decoder_output)

Model 함수를 이용해 decoder_input을 입력으로 받고, decoder_output을 출력하는 모델을 생성한다.

 

인코더와 디코더 결합

model_input = encoder_input
model_output = model_decoder(encoder_output)
model= Model(model_input, model_output)

오토인코더를 만드는 아주 중요한 코드임!

오토인코더의 입력은 인코더의 입력인 encoder_input이고, 출력은 디코더의 출력인 decoder_output이다.

그런데 잠복공간에서 인코더의 출력 encoder_output을 디코더의 입력으로 연결해야 함

model_input = encoder_input

encoder_input을 인코더의 입력으로 설정 

model_output = model_decoder(encoder_output)

 

인코더의 출력을 디코더인 model_decoder에 입력하고 model_decoder의 출력을 model_output에 저장

model= Model(model_input, model_output)

Model 함수를 사용해 model_input을 입력으로 받고, model_output을 출력하는 모델을 생성

이 model객체가 인코더와 디코더가 연결된 전체 오토인코더가 된다.

위 과정을 줄여서 아래와 같이 표현할 수 있음
model_output = model_decoder(model_encoder(encoder_input))
model = Model(encoder_input, model_output)

 

학습

model.compile(optimizer='Adam', loss ='mse')
model.fit(x_train, x_train, epochs=5, batch_size = 128, shuffle=True, validation_data=(x_test, x_test))

옵티마이저로 Adam을 사용하고 손실함수로 mse 사용

fit함수로 학습을 실행

이 행에서 첫번째와 두번째 매개변수에 주목해야함!!

둘 다 x_train으로 똑같음!  x_train이 입력되면 x_train이 출력되야 하기 때문이다.

mse는 레이블인 x_train과 신경망이 예측한 값 사이의 평균제곱오차를 출력하고

옵티마이저는 평균제곱오차를 줄이는 방향으로 학습을 진행한다.

 

복원 실험

decoded_img = model.predict(x_test)

학습을 마친 오토인코더를 이용해 테스트 집합인 x_test에 대한 예측 수행함

 


참고교재 파이썬으로 만드는 인공지능 - 한빛미디어