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

인코더는 고차원 데이터를 받아서, 그 핵심적인 특징만을 담은 저차원 잠재 표현(latent vector)으로 바꿔줍니다.
디코더는 인코더가 만든 요약 정보를 보고, 최대한 원래 데이터와 비슷하게 복원하려고 합니다.
노이즈 없이 단순히 입력 이미지를 자기 자신으로 복원하는 기본 오토인코더 구조
from tensorflow.keras.datasets import mnist
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Conv2D, MaxPooling2D, UpSampling2D
import matplotlib.pyplot as plt
import numpy as np
# 1) 데이터 로드 & 전처리
(X_train, _), (X_test, _) = mnist.load_data()
X_train = X_train.reshape(-1, 28, 28, 1).astype('float32') / 255.0
X_test = X_test.reshape(-1, 28, 28, 1).astype('float32') / 255.0
# 2) 오토인코더 구성 (인코더 2회 다운샘플 ↔ 디코더 2회 업샘플 대칭)
autoencoder = Sequential()
# Encoder: 28x28x1 -> 14x14x16 -> 7x7x8
autoencoder.add(Conv2D(16, kernel_size=3, padding='same', activation='relu', input_shape=(28, 28, 1)))
autoencoder.add(MaxPooling2D(pool_size=2, padding='same'))
autoencoder.add(Conv2D(8, kernel_size=3, padding='same', activation='relu'))
autoencoder.add(MaxPooling2D(pool_size=2, padding='same'))
# Decoder: 7x7x8 -> 14x14x8 -> 28x28x16 -> 28x28x1
autoencoder.add(Conv2D(8, kernel_size=3, padding='same', activation='relu'))
autoencoder.add(UpSampling2D())
autoencoder.add(Conv2D(16, kernel_size=3, padding='same', activation='relu')) # padding='same' 추가
autoencoder.add(UpSampling2D())
autoencoder.add(Conv2D(1, kernel_size=3, padding='same', activation='sigmoid'))
# 3) 구조 확인
autoencoder.summary()
autoencoder.compile(optimizer='adam', loss='binary_crossentropy')
history = autoencoder.fit(
X_train, X_train,
epochs=50, batch_size=128,
validation_data=(X_test, X_test),
verbose=1
)
random_idx = np.random.randint(X_test.shape[0], size=5)
recons = autoencoder.predict(X_test)
plt.figure(figsize=(10, 4)) # 2x5
for i, idx in enumerate(random_idx):
# 원본
ax = plt.subplot(2, 5, i + 1)
plt.imshow(X_test[idx].squeeze(), cmap='gray')
ax.axis('off')
if i == 0:
ax.set_title("Original")
# 복원
ax = plt.subplot(2, 5, 5 + i + 1)
plt.imshow(recons[idx].squeeze(), cmap='gray')
ax.axis('off')
if i == 0:
ax.set_title("Reconstructed")
plt.tight_layout()
plt.show()
Sparse Autoencoder는 입력 데이터를 압축된 형태로 표현하는 오토인코더의 한 종류로, 잠재 공간(latent space)에서 대부분의 뉴런이 0에 가깝고 일부만 활성화되도록 강제하는 구조입니다. 이렇게 희소성을 주기 위해 L1 정규화나 KL Divergence 기반의 제약을 추가하여, 모델이 단순히 모든 뉴런을 다 쓰는 대신 중요한 특징만 선택적으로 사용하도록 유도합니다. 그 결과, Sparse 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)
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)