수달이네 기술 블로그

3. AutoEncoder2(Sparse AE, Denoising AE, AutoEncoder + CIFAR10) 본문

AI공부/Vision

3. AutoEncoder2(Sparse AE, Denoising AE, AutoEncoder + CIFAR10)

슬픈 수달이 2026. 3. 11. 12:05

Sparse Autoencoder

sparseAutoencoder.pdf

입력 데이터를 압축된 형태로 표현하는 오토인코더의 한 종류.

  • 잠재 공간에서 대부분의 뉴런이 0에 가깝고, 일부만 활성화하도록 강제.
    • 평균 활성도가 특정 목표값보다 낮도록 함.
    • 활성된 뉴런 = 특정 결과에만 반응하는 뉴런(역할을 부여)
    • 비활성된 뉴런 = 결과에 연관이 거의 없는 뉴런
  • 희소성을 주기 위해 L1정규화, KL Divergence 기반의 제약을 추가
    • L1 정규화: 가중치를 0에 가깝게 만들어 모델을 희소하도록 만듦.
    • KLD: 뉴런의 평균 활성도가 목표와 얼마나 다른지를 파악(Loss function)
  • 결과적으로 데이터의 핵심적인 특징을 더 해석하고, 압축된 방식으로 표현한다.
  • 차원축소, 특징추출, 이상 탐지등의 분야에서 사용.

KL Divergence: VAE같은 곳에서의 KLD는 분포간의 차이의 유사도를 측정

Denoising Autoencoder

입력 데이터에 일부러 노이즈를 추가한 후, 손상된 데이터를 원래의 클린 데이터로 복원하도록 학습하는 오토인코더의 종류

  • 입력을 복사하는 것이 아니므로 데이터의 본질적 패턴, 구조등을 더 잘 학습할 수 있다.
  • 테스트 데이터에 노이즈가 끼어있어도 기존 오토인코더보다 더 잘 표현할 수 있다.
  • 따라서, 특징 추출, 데이터 전처리, 이상 탐지 등에 활용

더 성능좋은 AutoEncoder구현

인코더

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.neㅇ
  • 이전 AE와는 다르게 MaxPooling을 사용하지 않고, Conv2d(stride=2)를 이용해 데이터를 압축했다.
    • MaxPooling연산은 복원하기 어려운 정보손실을 만든다. (영역 내의 최대값만 남기고 버리기 때문 = 영구손상)
    • 그러나 Conv연산은 영역내 모든 값을 사용함. = 복구가능
  • 이전 AE에서는 ReLU연산을 사용했지만 여기선 GELU연산을 사용했다.

  • 부드러움(Smoothness): 0 부근에서 미분이 연속이라 기울기 소실/폭주 완화에 유리. 학습이 안정적(ReLU는 0에서 끊김)
  • 0에서 안 끊기므로 음수영역 뉴런이 죽지 않음.
  • NLP의 Transformer나, 비전의 최신 아키텍쳐가 GELU가 성능이 더 좋은 경우가 보고됨.
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
  • Upsampling연산 대신 ConvTranspose2d연산을 사용함
    • UpSample: 학습이 되지 않고, 단순이 이미지만 키움
[1 2]          [1 1 2 2]
[3 4]    ->    [1 1 2 2]
               [3 3 4 4]
               [3 3 4 4]
  • x --Upsample--> 크기 증가 ------> Conv --> 특징 생성
  • ConvTranspose2d: 엄샘플 과정에서 학습되며, 필터링, 패턴을 생성한다.
    • 즉, 일단 입력값 사이에 0을 채우고 conv수행해 패턴 생성
    • x --ConvTranspose--> 크기 증가 + 특징 생성 동시에

AutoEncoder

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]),
])

CIFAR10데이터 로드

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)
  • 이번엔 MNIST가 아닌 CIFAR10을 불러왔다.

데이터로더 정의

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)

# epoch 1
# 100%|██████████| 782/782 [02:31<00:00,  5.15it/s]
# [Train] Loss: 0.0419
# 100%|██████████| 157/157 [00:33<00:00,  4.76it/s]
# [Test] Loss: 0.0374

#epoch 6
# 100%|██████████| 782/782 [03:08<00:00,  4.14it/s]
# [Train] Loss: 0.0051
# 100%|██████████| 157/157 [00:28<00:00,  5.54it/s]
# [Test] Loss: 0.0147

원본
epoch 1
epoch 6

  • 위 결과처럼 점차 선명해지는것을 알 수 있다.

잠재 벡터 추출

def get_embed(model, data_loader):
    image_list, embed_list = [], []
    model.eval()                         # 1) 모델을 평가 모드로 전환 (Dropout/BatchNorm 고정)
    with torch.no_grad():                # 2) 추론 모드 (그래디언트 계산/메모리 사용 줄임)
        for inputs, _ in tqdm(data_loader):   # 3) 데이터로더에서 배치 반복 (라벨은 사용 안 함)
            inputs = inputs.to(device)        # 4) 배치를 장치(GPU/CPU)로 이동
            latents = model.encoder(inputs)   # 5) 인코더 통과 → 잠재표현(latent/embedding) 추출
            image_list.append(inputs.cpu())   # 6) 입력 배치를 CPU로 옮겨 누적
            embed_list.append(latents)        # 7) 임베딩 배치를 누적 (GPU에 둔 상태)

    image_list = torch.cat(image_list, dim=0) # 8) 배치들을 샘플 차원으로 이어붙임
    embed_list = torch.cat(embed_list, dim=0)
    return image_list, embed_list             # 9) (이미지_전체, 임베딩_전체) 반환

이미지 출력(유사 이미지)

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:])

 

'AI공부 > Vision' 카테고리의 다른 글

2. AutoEncoder(오토인코더)  (0) 2026.03.10
1. ViT(Vision Transformer  (0) 2026.03.09