오토인코더와 VAE(Autoencoder & Variational Autoencoder)

기본 오토인코더(plain autoencoder)는 입력을 압축한 뒤 다시 복원합니다. 외우기는 잘하지만, 새로운 데이터를 생성하지는 못합니다. 여기에 한 가지 요령 — 코드(code)가 가우시안(Gaussian)처럼 보이도록 강제하는 것 — 을 더하면 샘플러(sampler)가 생깁니다. z = μ + σ·ε로 표현되는 이 재매개변수화(reparameterization) 한 가지 덕분에, 2026년에 사용하는 모든 잠재 확산(latent-diffusion)과 흐름 매칭(flow-matching) 이미지 모델은 입력단에 VAE를 둡니다.

유형: Build 언어: Python 선수 지식: Phase 3 · 02 (Backprop), Phase 3 · 07 (CNNs), Phase 8 · 01 (생성 모델 — 분류와 역사) 예상 시간: 약 75분

문제

784픽셀 MNIST 숫자를 16개의 숫자로 이루어진 코드(code)로 압축한 뒤 다시 복원한다고 합시다. 기본 오토인코더는 재구성 평균제곱오차(reconstruction MSE)를 잘 맞추지만, 코드 공간(code space)은 울퉁불퉁한 덩어리가 됩니다. 코드 공간에서 임의의 점을 골라 디코딩(decoding)하면 잡음(noise)이 나옵니다. 샘플러가 없는 셈입니다. 결국 생성 모델인 척하는 압축 모델일 뿐입니다.

우리가 실제로 원하는 것은 세 가지입니다. (a) 코드 공간이 등방 가우시안(isotropic Gaussian) N(0, I)처럼 깨끗하고 매끄러운 분포여서 그곳에서 직접 표본을 뽑을 수 있어야 합니다. (b) 어떤 표본을 디코딩하더라도 그럴듯한 숫자(plausible digit)가 나와야 합니다. (c) 인코더(encoder)와 디코더(decoder)는 여전히 잘 압축해야 합니다. 세 가지 목표를 하나의 구조와 하나의 손실로 달성해야 합니다.

Kingma의 2013년 VAE는 인코더가 하나의 점이 아니라 분포(distribution) q(z|x) = N(μ(x), σ(x)²)를 출력하도록 학습합니다. 그리고 KL 패널티(KL penalty)로 이 분포를 사전 분포(prior) N(0, I) 쪽으로 끌어당긴 뒤, 디코딩 전에 q(z|x)에서 z를 뽑습니다. 추론(inference) 시점에는 인코더를 버리고 z ~ N(0, I)에서 표본을 뽑은 다음 디코딩합니다. KL 패널티가 바로 코드 공간을 구조화하는 힘입니다.

2026년의 VAE가 단독 생성기로 배포되는 경우는 드뭅니다. 원시 이미지 품질(raw image quality) 측면에서는 확산 모델(diffusion)에 밀렸기 때문입니다. 하지만 모든 잠재 확산 모델(SD 1/2/XL/3, Flux, AudioCraft)의 인코더로는 여전히 핵심입니다. VAE를 배우면 우리가 사용하는 모든 이미지 파이프라인(pipeline)의 보이지 않는 첫 층을 함께 배우는 셈입니다.

사전 테스트

2문제 · 이 강의를 시작하기 전에 얼마나 알고 있는지 확인해보세요

1.기본 오토인코더(plain autoencoder)가 MNIST에서 재구성 MSE를 잘 맞추지만, 코드 공간(code space)에서 임의의 점을 골라 디코딩하면 잡음(noise)이 나옵니다. 근본적인 이유는 무엇인가요?

2.VAE에서 재매개변수화 요령(reparameterization trick) z = mu + sigma * epsilon은 무엇을 달성하나요?

0/2 답변 완료

개념

autoencoder vs VAE — the one trick plain autoencoder x encoder z loss = ||x - x̂||² z-space: lumpy, not a distribution sample random z -> garbage out variational autoencoder x encoder μ(x) log σ²(x) z = μ + σ·ε reparameterize ε ~ N(0, I) decoder loss = reconstruction + β · KL ||x - x̂||² + ½ Σ( σ² + μ² - logσ² - 1 ) recon term: push x̂ → x KL term: push q(z|x) → N(0, I) β knob trades sharpness for well-shaped latent inference: sample z ~ N(0, I), forward through decoder → new x̂ one forward pass; no iteration; decoder is the whole generator

오토인코더(Autoencoder). z = encoder(x), x̂ = decoder(z), 손실은 ||x - x̂||²입니다. 코드 공간은 구조화되어 있지 않습니다.

VAE 인코더. 두 벡터 μ(x)log σ²(x)를 출력합니다. 이 둘이 q(z|x) = N(μ, diag(σ²))를 정의합니다.

재매개변수화 요령(Reparameterization trick). q(z|x)에서 직접 표본을 뽑는 연산은 미분 가능하지 않습니다. 표본을 z = μ + σ·ε로 다시 쓰면 됩니다. 여기서 ε ~ N(0, I)입니다. 이제 z(μ, σ)와 매개변수가 아닌 잡음의 결정론적 함수(deterministic function)이며, 기울기(gradient)는 μσ를 통해 흐를 수 있습니다.

손실(Loss). 증거 하한(Evidence Lower BOund; ELBO)은 두 항으로 구성됩니다.

loss = reconstruction + β · KL[q(z|x) || N(0, I)]
     = ||x - x̂||²  + β · Σ_i ( σ_i² + μ_i² - log σ_i² - 1 ) / 2

재구성 항은 x 쪽으로 밀고, KL 항은 q(z|x)를 사전 분포 쪽으로 밉니다. 둘은 서로 교환 관계(trade-off)입니다. 작은 β(<1)는 더 선명한 표본을 만들지만 코드 공간은 덜 가우시안적이게 됩니다. 큰 β(>1)는 더 깨끗한 코드 공간을 만들지만 표본은 더 흐릿해집니다. β-VAE(Higgins 2017)는 이 손잡이를 유명하게 만들면서 분리 학습(disentanglement) 연구를 촉발했습니다.

샘플링(Sampling). 추론 시점에는 z ~ N(0, I)에서 표본을 뽑아 디코더에 한 번 통과시킵니다. 순전파 한 번이면 끝이며, 확산 모델처럼 반복적인 샘플링(iterative sampling)을 하지 않습니다.

직접 만들기

code/main.py는 NumPy나 torch 없이 아주 작은 VAE를 구현합니다. 입력은 8차원 합성 데이터(synthetic data)이며, 8차원 공간에서 2-성분 가우시안 혼합(2-component Gaussian mixture)으로부터 표본을 뽑아 만듭니다. 인코더와 디코더는 모두 은닉층 한 개짜리 다층 퍼셉트론(single hidden-layer MLP)입니다. tanh 활성화, 순전파(forward pass), 손실, 직접 작성한 역전파(backward pass)를 차례로 구현합니다. 실서비스용이 아니라 학습 목적입니다.

Step 1: encoder forward

def encode(x, enc):
    h = tanh(add(matmul(enc["W1"], x), enc["b1"]))
    mu = add(matmul(enc["W_mu"], h), enc["b_mu"])
    log_sigma2 = add(matmul(enc["W_sig"], h), enc["b_sig"])
    return mu, log_sigma2

σ 대신 log σ²을 출력하면 신경망 출력이 제약 없는 값이 됩니다. σsoftplus를 씌우는 방식은 함정입니다. σ ≈ 0 근방에서 기울기가 죽기 쉽기 때문입니다.

Step 2: reparameterize and decode

def reparameterize(mu, log_sigma2, rng):
    eps = [rng.gauss(0, 1) for _ in mu]
    sigma = [math.exp(0.5 * lv) for lv in log_sigma2]
    return [m + s * e for m, s, e in zip(mu, sigma, eps)]

def decode(z, dec):
    h = tanh(add(matmul(dec["W1"], z), dec["b1"]))
    return add(matmul(dec["W_out"], h), dec["b_out"])

eps는 순수한 잡음이고, musigma는 학습 가능한 경로에 있습니다. 덕분에 샘플링을 포함하는 것처럼 보여도 역전파(backpropagation)는 그대로 가능합니다.

Step 3: ELBO

def elbo(x, x_hat, mu, log_sigma2, beta=1.0):
    recon = sum((a - b) ** 2 for a, b in zip(x, x_hat))
    kl = 0.5 * sum(math.exp(lv) + m * m - lv - 1 for m, lv in zip(mu, log_sigma2))
    return recon + beta * kl, recon, kl

두 분포가 모두 가우시안이므로 KL은 닫힌 형태(closed-form)로 정확하게 계산됩니다. 수치 적분이 필요 없습니다. 2026년에도 몬테카를로(Monte-Carlo) 방식으로 KL을 추정해 3배쯤 느려지는 코드가 실제로 돌아다니지만, 여기서는 그럴 이유가 없습니다.

Step 4: generate

def sample(dec, z_dim, rng):
    z = [rng.gauss(0, 1) for _ in range(z_dim)]
    return decode(z, dec)

이 다섯 줄이 곧 생성 모델입니다.

함정(Pitfalls)

  • 사후 붕괴(Posterior collapse). KL 항이 q(z|x) → N(0, I)을 너무 강하게 밀어붙여, zx에 관한 정보를 전혀 담지 못하게 됩니다. 해결책으로는 β 점진 증가(β-annealing; β=0에서 시작해 1까지 천천히 키우기), 자유 비트(free bits), 또는 비활성 차원에는 KL을 적용하지 않는 방법이 있습니다.
  • 흐릿한 표본(Blurry samples). 가우시안 디코더 가능도(Gaussian decoder likelihood)는 사실상 MSE 재구성을 의미합니다. MSE는 L2 기준으로 베이즈 최적(Bayes-optimal) 값, 곧 평균을 선택합니다. 여러 그럴듯한 숫자의 평균은 결국 흐릿한 숫자가 됩니다. 해결책은 이산 디코더(discrete decoder; VQ-VAE, NVAE)를 쓰거나, VAE를 인코더로만 사용하고 그 잠재(latent) 위에 확산 모델을 쌓는 것입니다. Stable Diffusion이 바로 이 방식을 사용합니다.
  • β가 너무 크고 너무 일찍 들어가는 경우. 다시 사후 붕괴를 만나게 됩니다. β≈0.01에서 시작해 서서히 키워 가는 것이 좋습니다.
  • 잠재 차원(latent dim)이 너무 작은 경우. MNIST에는 16차원이 잘 동작하지만, ImageNet 256²에는 256차원, ImageNet 1024²에는 2048차원이 필요할 수 있습니다. Stable Diffusion의 VAE는 512×512×3 → 64×64×4로 압축합니다. 공간 면적 기준 32배, 채널 기준 32배 다운샘플(downsample) 비율입니다.

사용해보기

2026년 VAE 스택은 다음처럼 선택합니다.

상황선택
확산 모델용 이미지 잠재(image-latent) 인코더Stable Diffusion VAE(sd-vae-ft-ema) 또는 Flux VAE
오디오 잠재(audio-latent) 인코더Encodec(Meta), SoundStream, DAC(Descript)
비디오 잠재(video latents)Sora의 시공간 패치(spatiotemporal patches), Latte VAE, WAN VAE
분리된 표현 학습(disentangled representation learning)β-VAE, FactorVAE, TCVAE
트랜스포머(transformer) 모델링용 이산 잠재VQ-VAE, RVQ(ResidualVQ)
생성용 연속 잠재(continuous latents)일반 VAE를 만든 뒤, 그 잠재 공간 위에서 흐름/확산 모델을 조건부로 학습

잠재 확산 모델(latent-diffusion model)은 인코더와 디코더 사이에 확산 모델을 끼워 둔 VAE입니다. VAE가 거친 압축(coarse compression)을 맡고, 확산 모델이 무거운 작업을 담당합니다. 비디오에서는 VAE + 비디오 확산 DiT, 오디오에서는 Encodec + MusicGen 트랜스포머라는 동일한 패턴이 반복됩니다.

산출물 만들기

outputs/skill-vae-trainer.md를 저장된 산출물로 둡니다.

이 스킬(skill)은 데이터셋 프로파일(dataset profile), 목표 잠재 차원(latent-dim target), 그리고 후속 용도(재구성 전용, 샘플링, 잠재 확산 입력)를 입력으로 받아 다음을 출력합니다.

  • 아키텍처(architecture) 선택(plain / β / VQ / RVQ)
  • β 일정(schedule)
  • 잠재 차원
  • 디코더 가능도(decoder likelihood; Gaussian 대 categorical)
  • 평가 계획(recon MSE, 차원별 KL, q(z|x)N(0, I) 사이의 Fréchet 거리)

연습문제

  1. 쉬움. code/main.py에서 β0.01, 0.1, 1.0, 5.0으로 바꿔 가며 실행합니다. 최종 재구성 MSE와 KL을 기록합니다. 우리 합성 데이터에서 어떤 β가 파레토 최적(Pareto-best)인지 확인합니다.
  2. 중간. 가우시안 디코더 가능도를 베르누이 가능도(Bernoulli likelihood; 교차 엔트로피 손실)로 바꿉니다. 같은 합성 데이터를 이진화(binarize)한 뒤, 표본 품질을 비교합니다.
  3. 어려움. code/main.py를 작은 VQ-VAE로 확장합니다. 연속 z를 K=32개의 항목을 가진 코드북(codebook)에서 최근접 이웃(nearest-neighbour) 조회로 대체합니다. 재구성 MSE를 비교하고, 코드북 항목이 몇 개나 실제로 사용되는지 보고합니다. 코드북 붕괴(codebook collapse)는 실제로 자주 발생하는 문제입니다.

핵심 용어

용어흔한 설명실제 의미
오토인코더(Autoencoder)인코딩-디코딩 신경망x → z → x̂ 구조에서 MSE를 학습한다. 자체로는 생성 모델이 아니다.
VAE샘플러가 있는 AE인코더가 분포를 출력하고, KL 패널티가 코드 공간을 구조화한다.
ELBO(증거 하한)evidence lower bound`log p(x) ≥ recon - KL[q(z
재매개변수화(Reparameterization)z = μ + σ·ε확률적 노드를 결정론적 부분 + 순수 잡음으로 다시 써, 샘플링을 통과하는 역전파를 가능하게 한다.
사전 분포(Prior)p(z)잠재의 목표 분포. 보통 N(0, I)를 사용한다.
사후 붕괴(Posterior collapse)"KL 항이 이긴다"인코더가 x를 무시하고 사전 분포만 출력하며, 디코더가 환각으로 채워야 한다.
β-VAE조절 가능한 KL 가중치loss = recon + β·KL. β가 클수록 분리 학습이 잘되지만 표본은 흐릿해진다.
VQ-VAE이산 잠재연속 z를 가장 가까운 코드북 벡터로 치환한다. 트랜스포머 기반 모델링이 가능해진다.

프로덕션 메모: VAE는 확산 서버에서 가장 뜨거운 경로입니다

Stable Diffusion / Flux / SD3 파이프라인에서 VAE는 요청마다 두 번 호출됩니다. img2img나 인페인팅(inpainting) 시에는 한 번 인코딩하고, 마지막에 한 번 디코딩합니다. 1024² 해상도에서는 디코더 통과(decoder pass)가 파이프라인 전체에서 가장 큰 활성화 메모리 정점(activation-memory peak)을 만드는 경우가 많습니다. 128×128×16 잠재를 1024×1024×3으로 업샘플(upsample)해야 하기 때문입니다. 실무적으로 두 가지 결론이 따라옵니다.

  • 디코딩을 슬라이스(slice)하거나 타일(tile)로 처리합니다. diffuserspipe.vae.enable_slicing()pipe.vae.enable_tiling()을 제공합니다. 타일링은 작은 이음매(seam) 아티팩트를 감수하는 대신, 메모리를 O(H·W)에서 O(tile²)로 줄입니다. 소비자용 GPU에서 1024² 이상을 다루려면 거의 필수입니다.
  • 디코더는 bf16, 마지막 리사이즈(resize)의 수치 계산은 fp32로 둡니다. SD 1.x VAE는 fp32로 공개되었고, 1024² 이상에서 fp16으로 변환하면 조용히 NaN을 만들어 냅니다. SDXL에는 madebyollin/sdxl-vae-fp16-fix가 있습니다. fp16-fix 계열을 우선 사용하거나 bf16을 사용하세요.

더 읽을거리

실습 코드

이 강의의 실습 코드 1개

main
Code

산출물

이 강의에서 생성된 프롬프트, 스킬, 코드 산출물 1개

vae-trainer

Specify VAE architecture, latent size, beta schedule, and eval plan for a given dataset and downstream use.

Skill

확인 문제

3문제 · 모두 맞추면 완료 표시가 가능합니다

1.VAE 학습 중 KL 발산이 매우 일찍 거의 0에 수렴하고 재구성 손실은 높은 상태입니다. 어떤 현상이며 어떻게 해결하나요?

2.MNIST에서 VAE 표본이 GAN 표본보다 흐릿한(blurry) 경향이 있습니다. VAE 손실 함수의 어떤 측면이 근본 원인인가요?

3.Stable Diffusion의 VAE는 512x512x3 이미지를 64x64x4 잠재로 압축합니다. 1024x1024에서 SD를 실행하면 VAE 디코더가 가장 큰 활성화 메모리 정점(activation-memory peak)을 만듭니다. 표준적인 완화 방법은 무엇인가요?

0/3 답변 완료

추가 문제 풀기

AI가 강의 내용을 바탕으로 새로운 문제를 생성합니다