인공지능/생성형 AI

오토인코더

mino28 2025. 8. 21. 09:33

1. 오토인코더

오토인코더(Autoencoder)는 입력 데이터를 효율적으로 압축하고 다시 복원하는 것을 목표로 하는 인공신경망 기반의 비지도 학습 모델입니다. 인코더(encoder)라는 신경망 구조를 통해 입력 데이터를 저차원(latent space)의 잠재 표현으로 변환하고, 디코더(decoder)를 통해 이를 다시 원래의 입력 데이터로 복원합니다. 학습은 원본 입력과 복원된 출력 간의 재구성 오류(reconstruction error)를 최소화하는 방식으로 이루어지며, 이를 통해 데이터의 핵심 특징을 추출하거나 노이즈 제거, 차원 축소 등에 활용됩니다. 오토인코더는 생성 모델의 기초가 되는 구조로서, 이후 변분 오토인코더(VAE)나 GAN과 같은 발전된 모델에도 큰 영향을 주었습니다.

 

1. 인코더 (Encoder): 데이터를 요약하는 압축기

인코더는 고차원 데이터를 받아서, 그 핵심적인 특징만을 담은 저차원 잠재 표현(latent vector)으로 바꿔줍니다.

  • 입력 이미지가 28x28 픽셀이라면, 총 784개의 숫자가 들어옵니다.
  • 인코더는 이 784개의 숫자 중에서 정말 중요한 특징만 뽑아내서, 예를 들어 32차원 벡터로 요약합니다. 이걸 보통 잠재 공간(latent space)이라고 부릅니다.

 

2. 디코더 (Decoder): 요약 정보를 복원하는 재생기

디코더는 인코더가 만든 요약 정보를 보고, 최대한 원래 데이터와 비슷하게 복원하려고 합니다.

  • 인코더가 만든 32차원의 벡터를 받아서,
  • 다시 784개의 숫자 (28x28 이미지)로 복원하려고 합니다.
  • 이 때, 인코더에서 잃은 정보를 복원하기 때문에, 완전히 같진 않을 수 있지만 비슷하게 재현하려고 합니다.

 

2. 오토인코더 구현하기

from tensorflow.keras.datasets import mnist
from tensorflow.keras.models import Sequential, Model
from tensorflow.keras.layers import Input, Dense, Conv2D, MaxPooling2D, UpSampling2D, Flatten, Reshape

import matplotlib.pyplot as plt
import numpy as np

# MNIST 데이터셋을 불러옵니다.

(X_train, _), (X_test, _) = mnist.load_data()
X_train = X_train.reshape(X_train.shape[0], 28, 28, 1).astype('float32') / 255
X_test = X_test.reshape(X_test.shape[0], 28, 28, 1).astype('float32') / 255

# 생성자 모델을 만듭니다.
autoencoder = Sequential()

# 인코딩 부분입니다.
autoencoder.add(Conv2D(16, kernel_size=3, padding='same', input_shape=(28,28,1), activation='relu'))
autoencoder.add(MaxPooling2D(pool_size=2, padding='same'))
autoencoder.add(Conv2D(8, kernel_size=3, activation='relu', padding='same'))
autoencoder.add(MaxPooling2D(pool_size=2, padding='same'))
autoencoder.add(Conv2D(8, kernel_size=3, strides=2, padding='same', activation='relu'))

# 디코딩 부분입니다.
autoencoder.add(Conv2D(8, kernel_size=3, padding='same', activation='relu'))
autoencoder.add(UpSampling2D())
autoencoder.add(Conv2D(8, kernel_size=3, padding='same', activation='relu'))
autoencoder.add(UpSampling2D())
autoencoder.add(Conv2D(16, kernel_size=3, activation='relu'))
autoencoder.add(UpSampling2D())
autoencoder.add(Conv2D(1, kernel_size=3, padding='same', activation='sigmoid'))

# 전체 구조를 확인합니다.
autoencoder.summary()

 

# 컴파일 및 학습을 하는 부분입니다.
autoencoder.compile(optimizer='adam', loss='binary_crossentropy')
autoencoder.fit(X_train, X_train, epochs=50, batch_size=128, validation_data=(X_test, X_test))

# 학습된 결과를 출력하는 부분입니다.
random_test = np.random.randint(X_test.shape[0], size=5)  # 테스트할 이미지를 랜덤하게 불러옵니다.
ae_imgs = autoencoder.predict(X_test)                     # 앞서 만든 오토인코더 모델에 집어 넣습니다.

plt.figure(figsize=(7, 2))                         # 출력될 이미지의 크기를 정합니다.

for i, image_idx in enumerate(random_test):        # 랜덤하게 뽑은 이미지를 차례로 나열합니다.
   ax = plt.subplot(2, 7, i + 1)
   plt.imshow(X_test[image_idx].reshape(28, 28))   # 테스트할 이미지를 먼저 그대로 보여줍니다.
   ax.axis('off')
   ax = plt.subplot(2, 7, 7 + i +1)
   plt.imshow(ae_imgs[image_idx].reshape(28, 28))  # 오토인코딩 결과를 다음열에 출력합니다.
   ax.axis('off')
plt.show()

 

 

3. Sparse Autoencoder

Sparse Autoencoder는 입력 데이터를 압축된 형태로 표현하는 오토인코더의 한 종류로, 잠재 공간(latent space)에서 대부분의 뉴런이 0에 가깝고 일부만 활성화되도록 강제하는 구조입니다. 이렇게 희소성을 주기 위해 L1 정규화나 KL Divergence 기반의 제약을 추가하여, 모델이 단순히 모든 뉴런을 다 쓰는 대신 중요한 특징만 선택적으로 사용하도록 유도합니다. 그 결과, Sparse Autoencoder는 데이터의 핵심적인 특징을 더 해석 가능하고 압축된 방식으로 표현할 수 있으며, 차원 축소, 특징 추출, 이상 탐지 등 다양한 분야에서 활용됩니다.

 

4. Denoising Autoencoder

Denoising Autoencoder는 입력 데이터에 일부러 노이즈를 추가한 뒤, 그 손상된 데이터를 원래의 깨끗한 데이터로 복원하도록 학습하는 오토인코더입니다. 이렇게 학습하면 모델은 단순히 입력을 복사하는 대신 데이터의 본질적 패턴과 구조를 더 잘 학습하게 되며, 잡음에 강인한 표현을 얻을 수 있습니다. 그 결과, Denoising Autoencoder는 특징 추출, 데이터 전처리, 이상 탐지 등에서 활용되며, 노이즈 제거뿐 아니라 일반적인 강건한 표현 학습(robust representation learning) 방법으로 널리 사용됩니다.

import torch.nn as nn

class Encoder(nn.Module):
    def __init__(self, num_input_channels, base_channel_size, latent_dim):
        super().__init__()

        self.net = nn.Sequential(
            nn.Conv2d(num_input_channels, base_channel_size, kernel_size=3, padding=1, stride=2),  # 32x32 => 16x16
            nn.GELU(),
            nn.Conv2d(base_channel_size, base_channel_size, kernel_size=3, padding=1),
            nn.GELU(),
            nn.Conv2d(base_channel_size, 2 * base_channel_size, kernel_size=3, padding=1, stride=2),  # 16x16 => 8x8
            nn.GELU(),
            nn.Conv2d(2 * base_channel_size, 2 * base_channel_size, kernel_size=3, padding=1),
            nn.GELU(),
            nn.Conv2d(2 * base_channel_size, 2 * base_channel_size, kernel_size=3, padding=1, stride=2),  # 8x8 => 4x4
            nn.GELU(),
            nn.Flatten(),
            nn.Linear(2 * 16 * base_channel_size, latent_dim),
        )

    def forward(self, x):
        return self.net(x)

GELU를 사용하는 이유

  • 부드러움(Smoothness): 0 부근에서 미분이 연속이라 기울기 소실/폭주 완화에 유리하고, 학습이 안정적입니다. (ReLU는 0에서 미분이 뚝 끊깁니다.)
  • 정보 보존: 작은 음수도 조금은 통과시켜서, ReLU처럼 음수 영역 뉴런이 “죽어버리는(dead)” 현상을 줄입니다.
  • 경험적 성능: NLP의 Transformer(BERT, GPT 계열), 비전의 최신 아키텍처에서 GELU가 ReLU보다 성능이 더 좋은 경우가 자주 보고되었습니다. CNN에도 무난히 잘 맞습니다.
class Decoder(nn.Module):
    def __init__(self, num_input_channels, base_channel_size, latent_dim):
        super().__init__()

        self.linear = nn.Sequential(nn.Linear(latent_dim, 2 * 16 * base_channel_size), nn.GELU())
        self.net = nn.Sequential(
            nn.ConvTranspose2d(
                2 * base_channel_size, 2 * base_channel_size, kernel_size=3, output_padding=1, padding=1, stride=2
            ),  # 4x4 => 8x8
            nn.GELU(),
            nn.Conv2d(2 * base_channel_size, 2 * base_channel_size, kernel_size=3, padding=1),
            nn.GELU(),
            nn.ConvTranspose2d(2 * base_channel_size, base_channel_size, kernel_size=3, output_padding=1, padding=1, stride=2),  # 8x8 => 16x16
            nn.GELU(),
            nn.Conv2d(base_channel_size, base_channel_size, kernel_size=3, padding=1),
            nn.GELU(),
            nn.ConvTranspose2d(
                base_channel_size, num_input_channels, kernel_size=3, output_padding=1, padding=1, stride=2
            ),  # 16x16 => 32x32
            nn.Tanh(),
        )

    def forward(self, x):
        x = self.linear(x)
        x = x.reshape(x.shape[0], -1, 4, 4)
        x = self.net(x)
        return x

 

class Autoencoder(nn.Module):
    def __init__(self, num_input_channels, base_channel_size, latent_dim):
        super().__init__()

        self.encoder = Encoder(num_input_channels, base_channel_size, latent_dim)
        self.decoder = Decoder(num_input_channels, base_channel_size, latent_dim)

    def forward(self, x):
        latent = self.encoder(x)
        output = self.decoder(latent)
        return latent, output

 

model = Autoencoder(num_input_channels=3, base_channel_size=64, latent_dim=256)

 

import torch
from torchvision.transforms import v2

trn_transforms = v2.Compose([
    v2.ToImage(),
    v2.RandomResizedCrop(size=(32, 32), antialias=True),
    v2.RandomHorizontalFlip(p=0.5),
    v2.RandomVerticalFlip(p=0.5),
    v2.ToDtype(torch.float32, scale=True),
    v2.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]),
])
test_transforms = v2.Compose([
    v2.ToImage(),
    v2.Resize(size=(32, 32), antialias=True),
    v2.ToDtype(torch.float32, scale=True),
    v2.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]),
])

 

from torchvision.datasets import CIFAR10
trn_dataset = CIFAR10(".", train=True, download=True, transform=trn_transforms)
test_dataset = CIFAR10(".", train=False, download=True, transform=test_transforms)

 

import matplotlib.pyplot as plt
import numpy as np
import torchvision.utils as vutils

def imshow(inputs, title):
    mean = 0.5
    std = 0.5
    inputs = std * inputs + mean
    inputs = torch.clip(inputs, 0, 1)
    grid = vutils.make_grid(inputs, padding=2, normalize=True)

    plt.figure(figsize=(8,8))
    plt.axis("off")
    plt.title(title)
    plt.imshow(np.transpose(grid, (1,2,0)))
    plt.show()
    plt.close()

 

import torch

trn_loader = torch.utils.data.DataLoader(trn_dataset, batch_size=64, shuffle=True, num_workers=2)
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=64, shuffle=False, num_workers=2)

 

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

 

from tqdm import tqdm

def train(model, criterion, optimizer, trn_loader, test_loader, device, num_epochs):
  for epoch in range(num_epochs):

    model.train()
    trn_loss = 0.0
    for inputs, _ in tqdm(trn_loader):
      inputs = inputs.to(device)

      _, outputs = model(inputs)
      loss = criterion(outputs, inputs)

      optimizer.zero_grad()
      loss.backward()
      optimizer.step()

      trn_loss += loss.item() * inputs.size(0)

    trn_epoch_loss = trn_loss / len(trn_loader.dataset)
    print(f"[Train] Loss: {trn_epoch_loss:.4f}")

    with torch.no_grad():
      model.eval()
      test_loss = 0.0
      for inputs, _ in tqdm(test_loader):
        inputs = inputs.to(device)

        _, outputs = model(inputs)
        loss = criterion(outputs, inputs)

        test_loss += loss.item() * inputs.size(0)

      test_epoch_loss = test_loss / len(test_loader.dataset)
      print(f"[Test] Loss: {test_epoch_loss:.4f}")
      imshow(inputs.cpu(), "Inputs")
      imshow(outputs.cpu(), "outputs")

 

import torch.optim as optim

model = model.to(device)
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

 

train(model, criterion, optimizer, trn_loader, test_loader, device, num_epochs=10)

 

def get_embed(model, data_loader):
    image_list, embed_list = [], []
    model.eval()
    with torch.no_grad():
        for inputs, _ in tqdm(data_loader):
            inputs = inputs.to(device)
            latents = model.encoder(inputs)
            image_list.append(inputs.cpu())
            embed_list.append(latents)

    image_list = torch.cat(image_list, dim=0)
    embed_list =  torch.cat(embed_list, dim=0)
    return image_list, embed_list

 

def find_similar_images(query_image, query_embed, key_images, key_embeds, k=7):
    dist = torch.cdist(query_embed[None, :], key_embeds, p=2)
    dist = dist.squeeze(dim=0)

    _, topk_indices = torch.topk(dist, k, largest=False)
    topk_images = torch.cat([query_image[None], key_images[topk_indices.cpu()]], dim=0)
    imshow(topk_images, f"Top-{k} images")

    _, bottomk_indices = torch.topk(dist, k, largest=True)
    bomttomk_images = torch.cat([query_image[None], key_images[bottomk_indices.cpu()]], dim=0)
    imshow(bomttomk_images, f"Bottom-{k} images")

 

test_images, test_embeds = get_embed(model, test_loader)

 

for i in range(8):
    find_similar_images(test_images[i], test_embeds[i], test_images[i+1:], test_embeds[i+1:])

 

def add_noise(inputs):
    noise = torch.randn(inputs.size()) * 0.2
    noisy_inputs = inputs + noise
    return noisy_inputs

 

def train_with_noise(model, criterion, optimizer, trn_loader, test_loader, device, num_epochs):
  for epoch in range(num_epochs):

    model.train()
    trn_loss = 0.0
    for inputs, _ in tqdm(trn_loader):
      inputs_with_noise = add_noise(inputs)
      inputs_with_noise = inputs_with_noise.to(device)
      inputs = inputs.to(device)

      _, outputs = model(inputs_with_noise)
      loss = criterion(outputs, inputs)

      optimizer.zero_grad()
      loss.backward()
      optimizer.step()

      trn_loss += loss.item() * inputs.size(0)

    trn_epoch_loss = trn_loss / len(trn_loader.dataset)
    print(f"[Train] Loss: {trn_epoch_loss:.4f}")

    with torch.no_grad():
      model.eval()
      test_loss = 0.0
      for inputs, _ in tqdm(test_loader):
        inputs_with_noise = add_noise(inputs)
        inputs_with_noise = inputs_with_noise.to(device)
        inputs = inputs.to(device)

        _, outputs = model(inputs_with_noise)
        loss = criterion(outputs, inputs)

        test_loss += loss.item() * inputs.size(0)

      test_epoch_loss = test_loss / len(test_loader.dataset)
      print(f"[Test] Loss: {test_epoch_loss:.4f}")
      imshow(inputs.cpu(), "Inputs")
      imshow(outputs.cpu(), "outputs")

 

model = Autoencoder(num_input_channels=3, base_channel_size=64, latent_dim=256)
model = model.to(device)

optimizer = optim.Adam(model.parameters(), lr=0.001)

train_with_noise(model, criterion, optimizer, trn_loader, test_loader, device, num_epochs=10)

 

!pip install medmnist

 

import medmnist

pathmnist_info = medmnist.INFO['pathmnist']
DataClass = getattr(medmnist, pathmnist_info['python_class'])

pathmnist_trn_datset = DataClass(split='train', download=True, transform=trn_transforms)
pathmnist_test_dataset = DataClass(split='test', download=True, transform=test_transforms)

pathmnist_trn_loader = torch.utils.data.DataLoader(pathmnist_trn_datset, batch_size=64, shuffle=True, num_workers=2)
pathmnist_test_loader = torch.utils.data.DataLoader(pathmnist_test_dataset, batch_size=64, shuffle=False, num_workers=2)

 

model = Autoencoder(num_input_channels=3, base_channel_size=64, latent_dim=256)
model = model.to(device)

optimizer = optim.Adam(model.parameters(), lr=0.001)

train(model, criterion, optimizer, pathmnist_trn_loader, pathmnist_test_loader, device, num_epochs=3)

 

test_images, test_embeds = get_embed(model, pathmnist_test_loader)
for i in range(8):
    find_similar_images(test_images[i], test_embeds[i], test_images[i+1:], test_embeds[i+1:])

 

model = Autoencoder(num_input_channels=3, base_channel_size=64, latent_dim=256)
model = model.to(device)

optimizer = optim.Adam(model.parameters(), lr=0.001)

train_with_noise(model, criterion, optimizer, pathmnist_trn_loader, pathmnist_test_loader, device, num_epochs=3)
 

'인공지능 > 생성형 AI' 카테고리의 다른 글

GAN  (4) 2025.08.21
비지도 학습  (0) 2025.08.21