5 분 소요


image


VAE : Encoder, Auto Encoder 그리고 Variational Auto Encoder

현대 딥러닝의 핵심 패러다임 중 하나는 “데이터를 어떻게 효율적으로 표현할 것인가” 입니다. 이 질문에서 출발한 일련의 아키텍처들이 있습니다. 단순히 입력을 압축하는 인코더(Encoder) 에서 시작해, 압축과 복원을 동시에 학습하는 오토인코더(Autoencoder), 그리고 잠재 공간을 확률적으로 모델링하여 새로운 데이터를 생성할 수 있는 변분 오토인코더(Variational Autoencoder, VAE) 까지. 이 글에서는 각 개념을 단계적으로 설명하며, 각 모델이 왜 등장했고 무엇을 해결했는지를 직관적인 비유와 함께 중점적으로 다룹니다.


1. Encoder (인코더)

1.1 인코더란 무엇인가?

인코더(Encoder)는 고차원의 입력 데이터를 저차원의 잠재 표현(latent representation)으로 변환하는 신경망 컴포넌트입니다. 본질적으로, 인코더는 데이터에서 가장 중요한 특징(feature)만을 추출하여 압축된 형태로 저장하는 역할을 합니다.

  • 입력 데이터 $x$ (예: 이미지, 텍스트, 오디오)
  • 잠재 벡터 $z$ (latent vector) — 저차원의 압축된 표현
  • 인코더 함수 $f_\phi(x) = z$ ($\phi$는 학습 가능한 파라미터)

1.2 인코더의 본질: 데이터의 ‘가정(Assumption)’ 찾기

정보를 압축한다는 것은 무작위로 데이터를 버리는 것이 아닙니다. 데이터가 가진 어떤 ‘규칙’이나 ‘가정’을 찾아내는 과정입니다.

예를 들어, 2차원 좌표 평면(X, Y) 위에 점들이 하나의 곡선(예: 사인 곡선) 형태를 띠며 모여 있다고 가정해 봅시다. 이 데이터의 규칙을 안다면 우리는 X와 Y라는 두 개의 숫자 대신, ‘곡선 위의 어느 위치인가’를 나타내는 하나의 숫자(1차원) 로 데이터를 압축할 수 있습니다.

1.3 인코더의 한계

하지만 현실의 데이터(예: 3072차원($32\times32\times3$)의 고양이 이미지)는 2차원 점들처럼 사람이 직접 규칙을 찾아 수동으로 인코딩 공식을 짜는 것이 불가능에 가깝습니다. 또한, 인코더만으로는 압축된 벡터 $z$로부터 원본 데이터를 재구성(디코딩)할 수 없습니다. 이것이 오토인코더가 등장하게 된 배경입니다.


2. Autoencoder (오토인코더)

2.1 오토인코더와 ‘오토(Auto)’의 의미

오토인코더(Autoencoder, AE)는 인코더와 디코더를 결합한 신경망 구조입니다. 입력 $x$를 압축한 뒤, 다시 원본 $\hat{x}$로 복원(reconstruct)하는 비지도 학습(unsupervised learning) 방식을 취합니다.

image

여기서 ‘오토(Auto)’ 라는 이름이 붙은 이유는, 사람이 직접 찾기 불가능한 복잡한 데이터의 규칙(가정)을 딥러닝 모델이 ‘자동으로’ 찾아내기 때문입니다. 입력과 출력을 똑같이 맞추도록(Self-supervised) 학습시키고 중간에 병목(bottleneck)을 두면, 네트워크는 살아남기 위해 데이터의 핵심 패턴을 스스로 압축하고 복원하는 공식을 만들어냅니다.

image

아키텍처 구조:

  • Encoder: 입력 $x \rightarrow$ 잠재 벡터 $z$ (고차원 $\rightarrow$ 저차원)
  • Latent Space: 압축된 표현이 존재하는 저차원 공간 (병목)
  • Decoder: 잠재 벡터 $z \rightarrow$ 재구성된 출력 $\hat{x}$ (저차원 $\rightarrow$ 고차원)

2.2 손실 함수 (Loss Function)

오토인코더는 재구성 손실(Reconstruction Loss) 을 최소화하며 학습됩니다. 주로 평균 제곱 오차(MSE)를 사용합니다.

\[\mathcal{L}_{MSE} = \frac{1}{N} \sum_{i=1}^{N} (x_i - \hat{x}_i)^2\]

image

2.3 오토인코더의 한계점: 생성 모델이 될 수 없는 이유

⚠️ 중요한 한계 오토인코더는 복원을 기가 막히게 잘하지만, 새로운 이미지를 ‘생성’할 수는 없습니다. 잠재 공간이 불연속적(discontinuous) 이기 때문입니다.

학습 데이터는 잠재 공간의 아주 좁은 특정 지점들에만 매핑됩니다. 만약 우리가 임의의 가우시안 분포에서 아무 숫자나 샘플링하여 디코더에 넣는다면, 디코더는 학습해 본 적 없는 낯선 숫자를 받아 의미 없는 쓰레기 값(garbage output)을 만들어냅니다. 즉, 점과 점 사이의 ‘빈 공간’에서는 정상적인 데이터가 생성되지 않습니다.

이 치명적인 한계를 극복하기 위해 등장한 것이 바로 Variational Autoencoder입니다.


3. Variational Autoencoder (VAE)

3.1 VAE란?

변분 오토인코더(Variational Autoencoder, VAE)는 오토인코더의 구조를 기반으로 하되, 잠재 공간을 확률 분포(probability distribution) 로 모델링하여 연속적이고 매끄러운 잠재 공간을 학습하는 진짜 ‘생성 모델’입니다.

3.2 VAE의 핵심: ‘노이즈(Noise)’를 더하는 잡기술(?)

VAE가 잠재 공간을 연속적으로 만드는 방법은 직관적으로 보면 ‘인코딩된 결과에 노이즈를 섞는 것’ 입니다. 기존 AE가 입력 데이터를 정확히 하나의 ‘점’으로 압축했다면, VAE는 압축된 점 주변에 가우시안 노이즈(오차 범위)를 흩뿌립니다.

디코더 입장에서는 정확한 점이 아니라 약간씩 흔들린(Variation) 노이즈 낀 숫자들을 받아도 원본을 복원해 내야 합니다. 이 가혹한 훈련을 거치면, 디코더는 특정 점뿐만 아니라 그 주변 영역(연속된 공간)의 숫자들을 모두 유의미한 데이터로 복원할 수 있는 능력을 갖추게 됩니다. 이제 우리는 빈 공간 없이 꽉 찬 잠재 공간에서 마음껏 샘플링을 할 수 있습니다!

3.3 아키텍처 및 Reparameterization Trick (재매개변수화)

image

VAE의 인코더는 잠재 벡터를 직접 출력하는 대신, 잠재 공간의 확률 분포를 정의하는 파라미터($\mu, \sigma$)를 출력합니다.

  • Encoder: $\mu$ (평균), $\log \sigma^2$ (로그 분산)
  • Sampling: $z \sim \mathcal{N}(\mu, \sigma^2)$ (여기서 노이즈가 추가됨)
  • Decoder: $z \rightarrow \hat{x}$ (재구성)

하지만 무작위로 샘플링을 하면 미분이 불가능하여 인코더로 학습 신호(Gradient)를 보낼 수 없습니다. 이를 해결하는 수학적 기법이 Reparameterization Trick입니다. 무작위성은 외부의 노이즈 $\epsilon$으로 빼내어 결정론적 경로를 만듭니다.

\[z = \mu + \sigma \odot \epsilon \quad (\text{where } \epsilon \sim \mathcal{N}(0, I))\]

3.4 손실 함수: ELBO (Evidence Lower BOund)

image

VAE의 손실 함수는 복원 능력과 잠재 공간의 연속성을 동시에 잡기 위해 두 가지 항을 더합니다.

\[\mathcal{L}_{VAE} = \text{Reconstruction Loss} + \text{KL Divergence}\]
  1. Reconstruction Loss: 디코더가 원본 데이터를 잘 복원하도록 강제합니다.
  2. KL Divergence (KLD): 인코더가 만든 잠재 분포 $q_\phi(z x)$가 사전 확률 분포인 표준 정규분포 $\mathcal{N}(0, I)$에 가까워지도록 규제합니다. 이를 통해 잠재 공간이 극단으로 퍼지지 않고 0 근처에 예쁘게 모이게 됩니다.
\[\mathcal{D}_{KL} = -\frac{1}{2} \sum_{j=1}^{J} (1 + \log(\sigma_j^2) - \mu_j^2 - \sigma_j^2)\]

3.5 PyTorch 모델 구현 및 학습 코드

import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

# 1. VAE 모델 아키텍처 정의
class VAE(nn.Module):
    def __init__(self, input_dim=784, hidden_dim=400, latent_dim=20):
        super(VAE, self).__init__()
        
        # Encoder: 자동으로 데이터의 규칙(분포)을 찾음
        self.fc1 = nn.Linear(input_dim, hidden_dim)
        self.fc_mu = nn.Linear(hidden_dim, latent_dim)
        self.fc_var = nn.Linear(hidden_dim, latent_dim)
        
        # Decoder: 노이즈가 낀 잠재 벡터에서도 원본을 복원해냄
        self.fc3 = nn.Linear(latent_dim, hidden_dim)
        self.fc4 = nn.Linear(hidden_dim, input_dim)
        
    def encode(self, x):
        h = torch.relu(self.fc1(x))
        return self.fc_mu(h), self.fc_var(h)
    
    def reparameterize(self, mu, log_var):
        std = torch.exp(0.5 * log_var)
        # 외부에서 끌어오는 무작위 노이즈 epsilon
        eps = torch.randn_like(std) 
        # Reparameterization Trick: 미분 가능한 결정론적 경로 형성
        return mu + eps * std  
        
    def decode(self, z):
        h = torch.relu(self.fc3(z))
        return torch.sigmoid(self.fc4(h))
        
    def forward(self, x):
        mu, log_var = self.encode(x)
        z = self.reparameterize(mu, log_var)
        return self.decode(z), mu, log_var

# 손실 함수 정의
def vae_loss(x, x_hat, mu, log_var):
    # 1. Reconstruction Loss (BCE)
    recon_loss = nn.functional.binary_cross_entropy(x_hat, x, reduction='sum')
    # 2. KL Divergence: 잠재 공간을 표준 정규분포(0 근처)로 묶어줌
    kl_loss = -0.5 * torch.sum(1 + log_var - mu.pow(2) - log_var.exp())
    return recon_loss + kl_loss

# 2. 데이터 로더 준비 (MNIST 데이터셋 예시)
transform = transforms.Compose([
    transforms.ToTensor(), 
    transforms.Lambda(lambda x: torch.flatten(x))
])
dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
dataloader = DataLoader(dataset, batch_size=128, shuffle=True)

# 3. 학습 루프 (Training Loop)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = VAE().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)

epochs = 10
model.train()

for epoch in range(epochs):
    train_loss = 0
    for batch_idx, (data, _) in enumerate(dataloader):
        data = data.to(device)
        
        optimizer.zero_grad()
        
        # Forward pass
        reconstructed_batch, mu, log_var = model(data)
        
        # Loss 계산 및 최적화
        loss = vae_loss(data, reconstructed_batch, mu, log_var)
        loss.backward()
        optimizer.step()
        
        train_loss += loss.item()
        
    print(f"Epoch {epoch+1}, Average Loss: {train_loss / len(dataloader.dataset):.4f}")

마무리

Encoder부터 VAE까지를 요약하면 다음과 같습니다.

  1. Encoder: 데이터를 저차원 표현으로 압축하며, 그 본질은 데이터에 숨겨진 규칙(가정) 을 찾는 것입니다.
  2. Autoencoder: 인간이 찾기 힘든 복잡한 규칙을 모델이 자동(Auto) 으로 학습하지만, 잠재 공간의 빈틈 때문에 새로운 데이터를 생성하지는 못합니다.
  3. VAE: 잠재 공간에 의도적인 노이즈(Variation) 를 섞어 빈틈을 메우고 매끄럽게 만듦으로써, 마침내 강력한 생성 능력(Generative Capability) 을 획득했습니다.

그 다음은? VAE는 현대 생성 AI의 뼈대가 되었습니다. VAE의 철학과 수식을 탄탄히 이해했다면, 이후 등장하는 Diffusion 모델이나 VQ-VAE 같은 최신 아키텍처를 이해하는 것도 훨씬 수월해질 것입니다.


참고 자료 (References)

댓글남기기