← AI 놀이터 홈으로

🌱 VAE — 생성을 위한 오토인코더 (Variational Autoencoder)

오토인코더의 잠재 공간엔 데이터가 없는 '구멍'이 있어요 — 노이즈 훈련(β)으로 골짜기를 메워서, 정규분포에서 뽑기만 하면 새 데이터가 나오는 생성 모델로 바꿔 봅시다.

📖 따라하기 가이드 — 1 / 6 단계

Step 1. 복습 — 그런데 잠재 공간에 구멍이?

지난 오토인코더 페이지에서 점 (x, y)를 잠재값 z 하나로 압축했죠. 이번 데이터는 축을 따라 두 덩어리로 뭉쳐 있어요 (축은 이미 최적으로 학습된 상태). 아래 히스토그램을 보세요 — z 값이 두 봉우리로 갈라지고 가운데는 텅 빈 골짜기예요. AE의 잠재 공간엔 이런 구멍이 생겨요. 구멍에서 디코딩하면? 학습한 적 없는 이상한 결과가 나오죠. 이게 “AE는 복원용이지 생성용은 아니다”의 이유예요.

● 청록 점 = 원본 데이터 (축을 따라 두 덩어리로 뭉쳐 있어요) · — 파란 직선 = 이미 학습이 끝난 잠재 축 · ● 파란 작은 점 = 투영(복원)된 점 · ✚ = 데이터 중심 · ⭐ = z로 디코딩해 만든 점 (보라 링 = z 슬라이더). 두 덩어리 사이의 빈 구간이 이번 페이지의 주인공이에요.

📊 잠재값 z의 분포 — 이 그림이 핵심!

가로축 = 잠재값 z, 세로축 = 밀도. 파란 막대 = 데이터의 z에 훈련 노이즈(σ = β×0.6)를 섞은 분포 · 회색 곡선 = 표준정규 N(0,1) · ┊ 보라 점선 = 지금 z 슬라이더 위치. β = 0이면 두 봉우리 + 가운데 빈 골짜기(AE), β를 올리면 봉우리가 뭉개지며 회색 곡선 같은 종 모양에 가까워지는지 보세요.

🎛️ 노이즈 훈련 강도 β

정직한 단순화: 실제 VAE는 인코더가 평균·분산을 내고 재매개변수화(reparameterization)로 학습해요. 여기선 잠재값 z에 노이즈를 주입하는 것으로 그 효과만 체험합니다.

✨ 생성 데모 — 잠재 공간에서 뽑기

디코딩 결과: x̂ = (6.64, 6.15)

z ~ N(0, 1)에서 10개를 뽑아 디코딩한 ⭐들 — 정규분포에서 뽑기만 하면 새 데이터가 나온다는 게 VAE의 생성 방식이에요.

📊 지금 상태

복원 오차 (β 반영)
—
골짜기 채움 비율
—
β
0.0

복원 오차 = 축과 수직 방향의 기본 잔차 + 노이즈로 생기는 오차(σ² 환산). 골짜기 채움 비율 = 가운데(z≈0) 밀도 ÷ 봉우리 밀도 — 0%면 깊은 골짜기, 100%에 가까우면 매끈한 분포예요. β를 올리면 골짜기는 메워지지만 오차가 커지는 트레이드오프를 확인하세요.

🧮 수식과 파이썬

손실 = 복원 오차 + β × KL(잠재분포 ‖ N(0,1))
잘 복원하기 vs 잠재 공간을 정규분포처럼 매끄럽게 — 둘 사이 저울이 β예요
🐍 파이썬 코드로 보면? (펼치기)
import torch
import torch.nn as nn

class VAE(nn.Module):
    def __init__(self):
        super().__init__()
        self.enc = nn.Sequential(nn.Linear(784, 128), nn.ReLU())
        self.mu = nn.Linear(128, 32)       # 잠재분포의 평균 μ
        self.logvar = nn.Linear(128, 32)   # 잠재분포의 로그분산
        self.dec = nn.Sequential(nn.Linear(32, 128), nn.ReLU(),
                                 nn.Linear(128, 784), nn.Sigmoid())

    def reparameterize(self, mu, logvar):
        eps = torch.randn_like(mu)         # 노이즈를 섞어 z를 뽑기
        return mu + eps * torch.exp(0.5 * logvar)

    def forward(self, x):
        h = self.enc(x)
        mu, logvar = self.mu(h), self.logvar(h)
        z = self.reparameterize(mu, logvar)
        return self.dec(z), mu, logvar

# 손실 = 복원 오차 + β × KL(잠재분포 ‖ N(0,1))
recon = ((x_hat - x) ** 2).sum()
kl = -0.5 * torch.sum(1 + logvar - mu ** 2 - logvar.exp())
loss = recon + beta * kl