Study/Data Science

[딥러닝] AutoEncoder & Variational AutoEncoder 정리

Railly Linker 2025. 6. 7. 17:48

(AutoEncoder)

- AE란, 입력 데이터를 압축(인코딩) 한 후, 다시 원래대로 복원(디코딩) 하도록 학습하는 비지도 학습 신경망을 뜻합니다.

다른 지도 학습 모델과는 달리, 입력 데이터를 그대로 정답 데이터로 사용하기에 따로 정답 레이블링을 할 필요가 없는 비지도 학습 모델이죠.

 

- AutoEncoder 의 구조는,

AE 구조

이미지 출처

 

이렇습니다.

 

딥러닝을 아시는 분이라면 더이상 설명할 필요도 없을만큼 간단한 구조로,

입력 이미지를 받아서 점차 작은 벡터로 줄여나가는 부분을 Encoder 라고 하고,

인코더에서 인코딩되어 축약된 중간 벡터 부분을 Latent Vector 라고 하며,

Latent Vector 를 기반으로 원본 이미지를 복원하는 부분을 Decoder 라고 합니다.

 

- AutoEncoder 의 의미는 입력 데이터의 의미를 작은 벡터에 압축하는 인코더를 비지도 학습으로 학습하는 것입니다.

입력 데이터를 모종의 함수로 더 작은 공간에 투영하고, 작게 압축된 데이터를 기반으로 원본 데이터를 복원할 수 있다면, 압축된 데이터에는 원본의 모든 정보가 포함되어 있다는 것을 의미합니다.

 

- AE 의 출력값은 원본 데이터의 복원이기에 손실 함수는 회귀 모델에 주로 사용하는 MSE(Mean Squared Error) 가 사용됩니다.

 

- AE 의 장점은,

원본으로 복원 가능한 핵심 정보를 압축하고, 불필요한 정보는 제거하여 정리한다는 특성으로 인해,

PCA 를 대체하여 다른 모델에 입력값 전처리 용도로 사용이 가능하고,

원본에 큰 영향을 끼치지 않는 부분, 즉 노이즈를 제거할 수 있다는 점이 있습니다.

 

조금 더 창의적으로 활용하자면,

정상적인 패턴의 데이터만 학습한 모델의 경우, 비정상적인 데이터가 들어간다면 재구성시에는 학습한 적이 없는 데이터의 처리에 따른 에러가 발생하게 되므로, 에러가 큰 데이터를 이상 데이터로 간주하는 방식으로 이상 탐지 모델로 활용이 가능합니다.

 

- Pytorch 로 구현하면 아래와 같습니다.

import torch
import torch.nn as nn

class AutoEncoder(nn.Module):
    def __init__(self):
        super().__init__()
        self.encoder = nn.Sequential(
            nn.Linear(784, 128),
            nn.ReLU(),
            nn.Linear(128, 32)
        )
        self.decoder = nn.Sequential(
            nn.Linear(32, 128),
            nn.ReLU(),
            nn.Linear(128, 784),
            nn.Sigmoid()
        )

    def forward(self, x):
        z = self.encoder(x)
        x_hat = self.decoder(z)
        return x_hat

 

위와 같이 784 크기의 데이터를 받아서, 128로 축약하고, 또 32 로 축약하는 Encoder 와,

32 크기의 Latent Vector 를 128 로 확대하고, 최종적으로 784 라는 원본 크기로 복원하는 간단한 모델로 작성이 가능합니다.

 

- 추가로, 이미지 데이터의 경우 위와 같이 평면 차원에 Flatten 해서 입력하는 방식도 있지만, 일반적으로는 CNN 레이어를 통해 데이터를 받고, Deconvolution 레이어로 이미지를 복원하도록 처리합니다.

 

이에따라 모델을 수정하면,

import torch
import torch.nn as nn
import torch.nn.functional as F

class ConvAutoEncoder(nn.Module):
    def __init__(self):
        super().__init__()
        # Encoder: 마지막 Linear로 latent vector 생성
        self.encoder = nn.Sequential(
            nn.Conv2d(1, 16, kernel_size=3, stride=2, padding=1),  # [B, 16, 14, 14]
            nn.ReLU(),
            nn.Conv2d(16, 32, kernel_size=3, stride=2, padding=1),  # [B, 32, 7, 7]
            nn.ReLU()
        )
        self.flatten = nn.Flatten()              # [B, 32*7*7]
        self.fc_enc = nn.Linear(32 * 7 * 7, 32)   # -> [B, 32] latent vector

        # Decoder: latent vector -> feature map
        self.fc_dec = nn.Linear(32, 32 * 7 * 7)   # -> [B, 32*7*7]
        self.unflatten = nn.Unflatten(1, (32, 7, 7))

        self.decoder = nn.Sequential(
            nn.ConvTranspose2d(32, 16, kernel_size=3, stride=2, padding=1, output_padding=1),  # [B, 16, 14, 14]
            nn.ReLU(),
            nn.ConvTranspose2d(16, 1, kernel_size=3, stride=2, padding=1, output_padding=1),   # [B, 1, 28, 28]
            nn.Sigmoid()
        )

    def forward(self, x):
        x = self.encoder(x)
        x = self.flatten(x)
        z = self.fc_enc(x)              # latent vector (B, 32)

        x = self.fc_dec(z)
        x = self.unflatten(x)
        x_hat = self.decoder(x)
        return x_hat

 

위와 같이

Convolution -> Flatten -> Latent Vector -> unFlatten -> DeConvolution(ConvTranspose2d)

이렇게 이미지 특성을 추출하는 CNN 레이어를 AE 모델에 적용이 가능합니다.

 

(Variational AutoEncoder)

- VAE 는, 확률적 생성 모델로써, AE 를 기반으로 Latent Space 에 확률 분포를 부여하여 디코더의 생성 품질을 높인 모델이라 할 수 있습니다.

 

- AE 는 데이터의 압축과 복원은 잘 하지만 새로운 데이터 생성에는 약합니다.

이유는, Latent Vector 가 원본 데이터의 의미를 구조적으로 나타내지 못할 가능성이 있기 때문입니다.

 

무슨 말이냐면, AE 의 학습의

데이터 입력 -> 데이터 압축 -> 압축된 데이터 입력 -> 데이터 복원

위와 같은 프로세스에서 중요한 것, 학습에 반영되는 것은 오로지 "데이터의 복원" 뿐입니다.

 

원본이 잘 복원되기만 한다면 Latent Vector 가 어떻게 출력되어도 상관이 없다는 것으로,

이렇게 된다면 Latent Vector 에 의미가 부여되는 것이 아니라 단순히 값 매칭 형식으로 학습이 될 가능성이 크다는 것이죠.

 

예를들어 보겠습니다.

우리가 만약 데이터를 수동으로 인코딩한다고 하면 어떻게 할까요?

700 크기의 인적사항 데이터를 받아서 50으로 줄인다고 한다면,

첫 컬럼은 나이, 다음은 성별, 다음은 키, ... 이런식으로 컬럼별 의미를 부여하여 논리적으로 수치를 기록하는 방식이 합리적이겠죠?

 

하지만 AE 의 학습 방식은 이렇게 논리적으로 진행되도록 제약사항이 없습니다.

단순히 입력 데이터 1번은 "1125478897155616...." 입력 데이터 2번은 "156548945616514..." 이렇게 일련번호를 붙이는 방식으로 진행할 수도 있다는 것입니다.

 

이렇게 된다면 수치가 하나만 변해도 이를 기반으로 디코딩을 할 경우 나타나는 데이터의 품질을 장담할 수 없게 됩니다.

(반면, Latent Vector 이 의미 기반으로 잘 학습되기만 한다면, 특정 컬럼의 입력값을 바꾸는 것만으로 이미지 내의 인물의 성별을 바꾸거나 나이를 바꾸거나 하는 신기한 일을 할 수 있겠죠. 마치 게임 커스터마이징 기능 처럼요.)

 

- 그렇다면 VAE 는 어떨까요?

VAE 역시 학습의 구조와 목표는, 원본 입력 -> 압축 -> 원본 복원으로 동일합니다.

다만, 압축하여 Latent Space 를 구축하는 과정에 제약을 줍니다.

 

VAE 구조

 

구조는 위와 같으며,

아래의 VAE 코드로 설명하겠습니다.

import torch
import torch.nn as nn
import torch.nn.functional as F

class VAE(nn.Module):
    def __init__(self, input_dim=784, hidden_dim=400, latent_dim=20):
        super(VAE, self).__init__()
        
        # 인코더: x -> μ, log(σ²)
        self.fc1 = nn.Linear(input_dim, hidden_dim)
        self.fc_mu = nn.Linear(hidden_dim, latent_dim)        # 평균 μ
        self.fc_logvar = nn.Linear(hidden_dim, latent_dim)    # 로그 분산 log(σ²)
        
        # 디코더: z -> x'
        self.fc2 = nn.Linear(latent_dim, hidden_dim)
        self.fc3 = nn.Linear(hidden_dim, input_dim)

    def encode(self, x):
        h = F.relu(self.fc1(x))
        mu = self.fc_mu(h)
        logvar = self.fc_logvar(h)
        return mu, logvar

    def reparameterize(self, mu, logvar):
        std = torch.exp(0.5 * logvar)
        eps = torch.randn_like(std)  # 샘플링된 ε ~ N(0,1)
        return mu + eps * std

    def decode(self, z):
        h = F.relu(self.fc2(z))
        return torch.sigmoid(self.fc3(h))  # 픽셀값 (0~1)

    def forward(self, x):
        mu, logvar = self.encode(x.view(-1, 784))
        z = self.reparameterize(mu, logvar)
        x_recon = self.decode(z)
        return x_recon, mu, logvar

 

위에서 encode 시점에 반환하는 값은 '평균'과 '분산'값을 반환합니다.

 

만약 배치로 64장의 이미지(x.shape = [64, 784])가 입력되면,


mu.shape = [64, latent_dim]
logvar.shape = [64, latent_dim]


이런식으로, 64 장의 이미지에 대하여 각 이미지별 latent vector 크기의 평균과 분산값을 추론하는 것입니다.

(AE 에서는 컬럼별 단일값이지만, VAE 는 컬럼별 평균과 분산의 쌍이 추론됨)

 

평균과 분산이 있다면 확률 분포를 알 수 있죠.

학습시 바로 이 확률분포에 제약을 걸어주는 것이 VAE 입니다.

def loss_function(x_recon, x, mu, logvar):
    # 1. 재구성 손실
    recon_loss = F.binary_cross_entropy(x_recon, x.view(-1, 784), reduction='sum')
    
    # 2. KL divergence
    kl_div = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
    
    return recon_loss + kl_div

 

위와 같이 학습에 걸어주는 제약인 손실 함수를 볼 수 있습니다.

VAE 가 입출력값을 0~1 사이의 실수값으로 받고, 출력 역시 동일한 범위의 값을 출력하기 때문에 Binary CE 를 사용하여 결과와 정답 사이의 차이를 구하는 것은 당연한 일인데, 여기에 KL Divergence 라는 제약을 합치는 의미는,

각 컬럼별 확률 분포를 평이하게 만들기 위해서입니다.

 

KLD 에 대해 간략히 설명하자면,

확률 분포 Q 와 P 가 있을 때, D_KL(Q || P) 는 분포 Q 가 분포 P 에 비해 얼마나 다른지를 측정하는 것으로, 0 에 가까울 수록 두 분포가 비슷하다는 의미입니다.

 

즉, 위에서는 모델이 추론한 분포가 어떠한 다른 분포와 일치하는 것을 목표로 학습이 진행되는데,

VAE 에서는 모델의 추론 분포가 평균 0, 표준편차 1 인 정규분포와 유사할수록 KLD 에서 0의 값이 나오게 하므로,

즉 VAE 인코더의 확률분포 추론의 방향은 정규분포 값을 반환되는 방향으로 학습되는 것입니다.

 

이유가 뭘까요?

정규 분포는 자연적으로 나올 수 있는 가장 평이한 값의 분포입니다.

Latent Vector 의 모든 컬럼에 대하여 출력되는 값이 이러한 정규분포를 따르게 된다면, Latent Vector 의 값은 000001 이나 000111 과 같이 극단에 치우치는 값이 반환되어 단순 매칭 되는 상황을 막고, 0.1 0.02 0.12 0.42 0.003 0.33 이런식으로 모든 컬럼에 있어서 값의 표현이 잔잔하게 흩어지게 강제하기 때문에, 해당 강제 상황 속에서 어떻게든 데이터를 표현하기 위하여 앞서 말했듯 Latent Vector 의 각 컬럼에 합리적인 의미를 부여하는 등, 보다 합리적인 방식으로 데이터를 압축하는 법을 학습할 것입니다.

 

정리하자면, VAE 의 학습 목표는, 모델이 반환하는 확률 분포는 정규 분포와 가깝게, 그리고 모델이 반환하는 출력값은 입력값과 가깝게 하는 방향으로 학습이 되는 것이고, 이를 통하여 Latent Vector 에 구조적으로 의미를 부여하도록 하는 것이 최종적인 목적입니다.

 

- 이상입니다.

VAE 는 생성 모델로써 제법 좋은 성과를 거뒀지만 매력적일정도의 결과물을 보여주지는 못하는 모델이었습니다.

현 시점의 Diffusion 기반의 이미지 생성 모델과 같이 선명하면서도 예술적인 이미지를 생성하지는 못했고,

전반적으로는 '흐릿한 이미지가 생성된다'라고 평가되었습니다.

 

그 이유는, 위의 VAE 원리를 이해하시면 알 수 있듯, VAE 의 학습 목표가 안정적인 결과물의 출력이며, 그것을 위하여 인코더가 출력하는 출력값을 정규분포에 가깝도록 훈련을 시켰기 때문에, 출력 결과물에 디테일과 선명함이 사라지고 평균적이고 무난한 데이터가 출력되기 때문이죠.

 

안정성과 디테일 사이의 트레이드 오프가 일어난 것입니다.

이후 이러한 디테일 문제를 해결하기 위하여,

 

β-VAE : KL 항의 가중치를 조절하여 더 의미있는 Latent Space 형성

VQ-VAE : 연속 벡터 대신 백터 양자화를 통한 더 샤프한 표현

 

이런식으로 발전이 진행되었고,

 

반대로 디테일은 좋지만 안정성이 굉장히 좋지 못한 GAN 이라는 생성 모델이 나왔을 때에는 안정성을 높이기 위해,

 

VAE-GAN : VAE 의 재구성 능력을 GAN 의 고품질 이미지 생성 기능에 결합

 

이런식으로 활용되기도 했습니다.

 

결국 현 시점 이미지 생성 모델의 주류가 되어 발전중인 Diffusion 모델을 발전시킨 Stable Diffusion 에서는 연산량을 감소시키고, 노이즈를 제거하고 안정성을 높이기 위해 VAE 가 사용되게 되므로 VAE 는 오래된 기술임에도 배워둬야 할 기술로 여겨져 이렇게 게시글로 정리해 봤습니다.

 

다음에는 Diffusion 모델을 이해하기 위한 DDPM 모델을 이해하기 위하여, U-Net 을 공부하고 정리하겠습니다.