본문 바로가기
딥러닝 with Python

[생성 AI] 오토인코더(Auto Encoder)

by CodeCrafter 2024. 5. 26.
반응형

[해당 포스팅은 "만들면서 배우는 생성 AI 2탄" 을 참조했습니다.

 

1. 오토 인코더(Auto Encoder)

 

- 오토 인코더는 단순히 어떤 항목의 인코딩과 디코딩 작업을 수행하도록 훈련된 신경망을 말합니다.

- 이 과정을 통해 출력이 가능한 원본에 가까워지도록 하는 결과물을 만드는 신경망을 훈련시킵니다.

 

- 인코더(Encoder)

 * 역할 : 입력 데이터를 더 작은 차원의 표현으로 압축하는 역할을 합니다. 이를 통해 입력 데이터의 중요한 특징을 추출하고, 노이즈를 제거하거나 데이터의 구조적 특성을 반영할 수 있습니다.

 * 이때, 압축된 형태의 특징 벡터를, Latent Vector 또는 Latent Representation 이라고 합니다.

 

 - 디코더(Decoder)

 * 역할 : 인코더에 의해 생성된 압축된 표현을 원래의 데이터 형태로 복원하는 역할을 합니다. 즉, 압축된 데이터를 다시 고차원 데이터로 확장하여 원래의 입력 데이터와 최대한 유사하게 만드는 과정을 말합니다.

 

 

 

2. Fashion MNIST를 사용한 오토인코더 재현 (Colab Notebook)

 

먼저, 깃허브 저장소로부터 Utils.py를 다운로드 합니다.

import sys

# 코랩의 경우 깃허브 저장소로부터 utils.py를 다운로드 합니다.
if 'google.colab' in sys.modules:
    !wget https://raw.githubusercontent.com/rickiepark/Generative_Deep_Learning_2nd_Edition/main/notebooks/utils.py
    !mkdir -p notebooks
    !mv utils.py notebooks

 

 

다음은 필요한 라이브러리를 불러와줍니다.

코드 재현은 tensorflow를 바탕으로 진행할 예정입니다.

import numpy as np
import matplotlib.pyplot as plt

from tensorflow.keras import layers, models, datasets, callbacks
import tensorflow.keras.backend as K

from notebooks.utils import display

 

 

학습 전체에 활용할 파라미터(하이퍼 파라미터)를 정의해줍니다.

* 이미지의 크기 : 32

* 채널의 수: 1 (흑백 이미지 이므로 1) 

* 한번에 학습할 Batch의 크기 : 100

*버퍼 사이즈(대규모 데이터셋을 처리할 때, 메모리에 한 번에 모든 데이터를 로드하는 것이 비효율적일 수 있으므로, 데이터셋을 작은 청크(Chunk)로 나누어 처리하는데 이때 이 청크의 크기를 buffer size로 이야기함)

 : 1,000

* 검증 데이터의 비율 : 0.2

* 임베딩 할 차원 : 2차원

* Epochs (학습 횟수) : 3

IMAGE_SIZE = 32
CHANNELS = 1
BATCH_SIZE = 100
BUFFER_SIZE = 1000
VALIDATION_SPLIT = 0.2
EMBEDDING_DIM = 2
EPOCHS = 3

 

데이터는 다음과 같이 로드합니다.

# 데이터를 로드합니다.
(x_train, y_train), (x_test, y_test) = datasets.fashion_mnist.load_data()

 

이후 데이터를 전처리해 줍니다.

* 이미지 픽셀별 값이 0부터 255인데 255로 나누어 0과 1사이의 값으로 변환

* 이미지 가장자리에 2픽셀씩 0으로 패딩추가

* 이미지 배열의 마지막 차원에 새로운 축을 추가하여 채널 차원으로 사

# 데이터 전처리
def preprocess(imgs):
    """
    이미지를 정규화하고 크기를 변경합니다.
    """
    imgs = imgs.astype("float32") / 255.0
    imgs = np.pad(imgs, ((0, 0), (2, 2), (2, 2)), constant_values=0.0)
    imgs = np.expand_dims(imgs, -1)
    return imgs


x_train = preprocess(x_train)
x_test = preprocess(x_test)

 

# 훈련 세트에 있는 의류 아이템 일부를 출력합니다.
display(x_train)

 

 

다음은 오토 인코더 모델을 만드는 부분입니다.

* Input 이미지는 32x32의 1채널 이미지로 받아들이고

* Conv2D : 16x16으로 크기는 줄이지만 채널수는 32로 증가 (고수준 특성 추출) / Stride가 2 이므로 이미지의 크기는 절반이 됨)

* Conv2D_1 : 8x8으로 크기는 줄이지만 채널수는 64로 증가 (고수준 특성 추출)  / Stride가 2 이므로 이미지의 크기는 절반이 됨)

* Conv2D_2 : 4x4으로 크기는 줄이지만 채널수는 128로 증가 (고수준 특성 추출)  / Stride가 2 이므로 이미지의 크기는 절반이 됨)

* flatten : 마지막 합성곱층의 출력을 픽셀별로 펼칩니다 ( 4x4x128 = 2048)

* Dense : 2차원 잠재 공간에 해당하는 크기가 2인 Dense 층에 연결합니다. 

# 인코더
encoder_input = layers.Input(
    shape=(IMAGE_SIZE, IMAGE_SIZE, CHANNELS), name="encoder_input"
)
x = layers.Conv2D(32, (3, 3), strides=2, activation="relu", padding="same")(
    encoder_input
)
x = layers.Conv2D(64, (3, 3), strides=2, activation="relu", padding="same")(x)
x = layers.Conv2D(128, (3, 3), strides=2, activation="relu", padding="same")(x)
shape_before_flattening = K.int_shape(x)[1:]  # 디코더에 필요합니다!

x = layers.Flatten()(x)
encoder_output = layers.Dense(EMBEDDING_DIM, name="encoder_output")(x)

encoder = models.Model(encoder_input, encoder_output)
encoder.summary()

 

다음은 디코더 부분입니다.

*인코더의 과정을 반대로 한다고 생각하시면 됩니다

*인코더는 이미지의 크기를 줄이고 채널수를 늘려서 고수준 특성을 많이 추출한다면, 디코더는 이를 복원하는 과정입니다.

*그렇기에 줄어든 이미지를 늘리는 Conv2DTranspose를 사용해줍니다. 

  (이미지 크기와 채널개수는 인코더의 역순과 동일하게)

* 마지막 Conv2DTranspose를 이미지 크기를 원본 대로 32x32로 돌려놓고 채널의 수도 1개의 채널로 맞춰줍니다.

  이때, 마지막 값 도출된 값을 0과 1사이의 값으로 전환하기위해 Sigmoid 함수를 Activation Function으로 활용해줍니다.

# 디코더
decoder_input = layers.Input(shape=(EMBEDDING_DIM,), name="decoder_input")
x = layers.Dense(np.prod(shape_before_flattening))(decoder_input)
x = layers.Reshape(shape_before_flattening)(x)
x = layers.Conv2DTranspose(
    128, (3, 3), strides=2, activation="relu", padding="same"
)(x)
x = layers.Conv2DTranspose(
    64, (3, 3), strides=2, activation="relu", padding="same"
)(x)
x = layers.Conv2DTranspose(
    32, (3, 3), strides=2, activation="relu", padding="same"
)(x)
decoder_output = layers.Conv2D(
    CHANNELS,
    (3, 3),
    strides=1,
    activation="sigmoid",
    padding="same",
    name="decoder_output",
)(x)

decoder = models.Model(decoder_input, decoder_output)
decoder.summary()

 

이제 설정된 인코더와 디코더를 바탕으로 최종 오토 인코더 모델을 정의해줍니다.

# 오토인코더
autoencoder = models.Model(
    encoder_input, decoder(encoder_output)
)
autoencoder.summary()

 

 

이렇게 정의된 데이터와 모델을 가지고 훈련을 진행시켜보겠습니다.

 

*옵티마이저는 adam으로, loss는 일반적인 binary_crossentropy로 정의합니다.

# 오토인코더 컴파일
autoencoder.compile(optimizer="adam", loss="binary_crossentropy")

* 학습 과정에서 나온 결과물을 저장하기 위한 체크포인트에 관한 설정을 진행해줍니다.

 이때 validation loss가 최소일때 정의해줍니다.

# 모델 저장 체크포인트 생성
model_checkpoint_callback = callbacks.ModelCheckpoint(
    filepath="./checkpoint",
    save_weights_only=False,
    save_freq="epoch",
    monitor="loss",
    mode="min",
    save_best_only=True,
    verbose=0,
)
tensorboard_callback = callbacks.TensorBoard(log_dir="./logs")

* 이후 학습 루프를 진행해줍니다.

autoencoder.fit(
    x_train,
    x_train,
    epochs=EPOCHS,
    batch_size=BATCH_SIZE,
    shuffle=True,
    validation_data=(x_test, x_test),
    callbacks=[model_checkpoint_callback, tensorboard_callback],
)

*3번의 학습결과가 아래와 같습니다. train 및 validation loss가 점차 감소하는 것을 알 수 있습니다.

 

*학습된 최종 모델을 저장해줍니다.

# 최종 모델을 저장합니다.
autoencoder.save("./models/autoencoder")
encoder.save("./models/encoder")
decoder.save("./models/decoder")

 

* 자 이제, 오토인코더를 활용해 재구성해보겠습니다.

n_to_predict = 5000
example_images = x_test[:n_to_predict]
example_labels = y_test[:n_to_predict]
predictions = autoencoder.predict(example_images)

print("실제 의류 아이템")
display(example_images)
print("재구성 이미지")
display(predictions)

 

* 원본 이미지가 첫번째 행이고, 디코딩된 재구성 이미지가 두번째 행에 있습니다. 3번 밖에 학습하지 않았고 아직 학습이 충분히 이루어지지 않았음에도 불구하고 latent vector를 활용해 꽤나 괜찮은 재구성 결과를 만들었습니다.

 

* 다음은 몇 개의 임베딩을 출력해봅니다.

# 샘플 이미지를 인코딩합니다.
embeddings = encoder.predict(example_images)
# 몇 개의 임베딩을 출력합니다.
print(embeddings[:10])

 

* 2개의 차원으로 각 이미지들의 표현이 압축됨을 알 수 있습니다.

 

* 2D 공간에 인코딩 된 포인트를 출력해 시각화해보겠습니다.

# 2D 공간에 인코딩된 포인트를 출력합니다.
figsize = 8

plt.figure(figsize=(figsize, figsize))
plt.scatter(embeddings[:, 0], embeddings[:, 1], c="black", alpha=0.5, s=3)
plt.show()

* 32x32의 이미지가 다음과 같이 2차원 평면에서 1개의 점으로 압축된 잠재 벡터(latent vector)로 변환됨을 알 수 있습니다.

 

*레이블(의류 종류)에 따라 임베딩에 색을 입혀보면 아래와 같습니다.

# 레이블(의류 종류)에 따라 임베딩에 색을 입힙니다.
example_labels = y_test[:n_to_predict]

figsize = 8
plt.figure(figsize=(figsize, figsize))
plt.scatter(
    embeddings[:, 0],
    embeddings[:, 1],
    cmap="rainbow",
    c=example_labels,
    alpha=0.8,
    s=3,
)
plt.colorbar()
plt.show()

* 0번 레이블은 티셔츠/탑, 1번은 바지, 2번은 풀오버, ..... , 9번은 앵클부츠입니다. 각 레이블 별로 색깔을 입혀보면 아래와 같이 같은 레이블끼리 비슷하게 분포해있는 모습을 보이며 레이블 간에 다른 특성들로 인해 산재해 있는 모습도 보입니다.

 

*이렇게 잠재 공간의 특성을 활용해 기존에 없던 임의의 잠재공간의 점을 지칭하여 새로운 이미지를 만들어 보겠습니다.

# 기존의 임베딩 범위 구하기
mins, maxs = np.min(embeddings, axis=0), np.max(embeddings, axis=0)

# 잠재 공간에서 포인트를 샘플링합니다.
grid_width, grid_height = (6, 3)
sample = np.random.uniform(
    mins, maxs, size=(grid_width * grid_height, EMBEDDING_DIM)
)
# 샘플링된 포인트를 디코딩합니다.
reconstructions = decoder.predict(sample)

 

# 그래프로 그립니다.
figsize = 8
plt.figure(figsize=(figsize, figsize))

# ... 원본 임베딩 ...
plt.scatter(embeddings[:, 0], embeddings[:, 1], c="black", alpha=0.5, s=2)

# ... 잠재 공간에서 새로 생성된 포인트
plt.scatter(sample[:, 0], sample[:, 1], c="#00B0F0", alpha=1, s=40)
plt.show()

# 디코딩된 이미지 그리드 추가
fig = plt.figure(figsize=(figsize, grid_height * 2))
fig.subplots_adjust(hspace=0.4, wspace=0.4)

for i in range(grid_width * grid_height):
    ax = fig.add_subplot(grid_height, grid_width, i + 1)
    ax.axis("off")
    ax.text(
        0.5,
        -0.35,
        str(np.round(sample[i, :], 1)),
        fontsize=10,
        ha="center",
        transform=ax.transAxes,
    )
    ax.imshow(reconstructions[i, :, :], cmap="Greys")

 

* 다음과 같이 잠재공간상에 임의로 포인트를 선정한 결과는 아래와 같습니다.(하늘색 점)

 

* 이 잠재 벡터들을 디코더를 통해 재현한 결과는 아래와 같습니다. 즉, 기존에 없던 새로운 이미지들을 생성시킨 것입니다.

 

 

* 레이블(의류 종류)에 따라 임베딩에 색을 입히고, 2차원인 잠재 공간에서 개략적인 Grid 단위로 이미지가 나타내는 부분을 시각화 하면 아래와 같습니다.

# 레이블(의류 종류)에 따라 임베딩에 색을 입힙니다.
figsize = 12
grid_size = 15
plt.figure(figsize=(figsize, figsize))
plt.scatter(
    embeddings[:, 0],
    embeddings[:, 1],
    cmap="rainbow",
    c=example_labels,
    alpha=0.8,
    s=300,
)
plt.colorbar()

x = np.linspace(min(embeddings[:, 0]), max(embeddings[:, 0]), grid_size)
y = np.linspace(max(embeddings[:, 1]), min(embeddings[:, 1]), grid_size)
xv, yv = np.meshgrid(x, y)
xv = xv.flatten()
yv = yv.flatten()
grid = np.array(list(zip(xv, yv)))

reconstructions = decoder.predict(grid)
# plt.scatter(grid[:, 0], grid[:, 1], c="black", alpha=1, s=10)
plt.show()

fig = plt.figure(figsize=(figsize, figsize))
fig.subplots_adjust(hspace=0.4, wspace=0.4)
for i in range(grid_size**2):
    ax = fig.add_subplot(grid_size, grid_size, i + 1)
    ax.axis("off")
    ax.imshow(reconstructions[i, :, :], cmap="Greys")

 

반응형

댓글