Chapter sebelumnya menutup dengan satu kejanggalan: autoencoder mengompresi dengan baik, tetapi ruang latennya penuh wilayah yang tidak pernah dilatih — menarik z acak di wilayah itu membuat decoder menghasilkan keluaran yang tidak menyerupai data apa pun. Variational autoencoder (VAE) mengubah struktur latent itu secara sistematis: encoder tidak lagi menghasilkan satu titik z, melainkan sebuah distribusi, dan sebuah regularizer KL memaksa seluruh distribusi tersebut menempel pada prior N(0,1) yang kita pilih sendiri. Hasil akhirnya adalah model yang benar-benar bisa di-sampling: tarik z dari prior, decoder menghasilkan data baru yang masuk akal. Chapter ini membangun VAE mini di PyTorch, membuktikan reparameterization trick secara numerik, lalu membandingkan angka demi angka dengan autoencoder biasa pada data yang sama.
Masalah: Ruang Laten yang Berlubang #
Mengapa autoencoder gagal sebagai model generatif? Karena loss-nya hanya peduli pada satu hal: data latih harus bisa direkonstruksi lewat z miliknya sendiri. Tidak ada tekanan apa pun pada wilayah z yang tidak dikunjungi data. Encoder bebas menempatkan cluster di mana saja di sumbu z — hasilnya rentang z yang asimetris, bersela, dan penuh lubang. Ketika kita menarik z dari distribusi sederhana seperti N(0,1), sebagian sampel jatuh di lubang, dan decoder yang tidak pernah dilatih di sana menghasilkan keluaran tak bermakna.
VAE menutup lubang ini lewat tiga komponen yang bekerja bersama:
- Distribusi posterior
q(z|x)— encoder mengeluarkan dua angka per data:mu(x)dansigma(x). Satu titik data tidak lagi punya satuz, melainkan sebuah awan peluang. - Reparameterization trick — pengambilan sampel ditulis sebagai
z = mu + sigma · epsdenganeps ~ N(0,1), sehingga keacakan pindah ke variabel masukanepsdan gradien tetap mengalir kemusertasigma. - Regularizer KL — jarak Kullback-Leibler antara
q(z|x)dan priorN(0,1), yang untuk Gaussian diagonal berbentuk ringkas:
KL = 0.5 · Σ (mu² + sigma² − 1 − ln sigma²)
Loss total VAE adalah penjumlahan keduanya: error rekonstruksi menarik model ke ketepatan, KL menahan posterior di sekitar prior. Kombinasi ini adalah bentuk negatif dari Evidence Lower Bound (ELBO) — kerangka yang diperkenalkan makalah asli VAE, Auto-Encoding Variational Bayes (arXiv:1312.6114) .
Demo: VAE Mini pada Data Tiga Cluster #
Data kita: tiga cluster 2D yang duduk pada satu busur — struktur berlubang yang sempurna untuk menguji ruang laten. Berkas vae_mini.py melatih autoencoder biasa sebagai pembanding, lalu VAE pada data yang sama:
"""Variational autoencoder mini pada data 2D tiga cluster (PyTorch).
Pembuka: verifikasi reparameterization trick (gradien mengalir lewat
z = mu + sigma*eps). Pembanding: autoencoder biasa yang rekonstruksinya
bagus tetapi ruang latennya berlubang -- decoder pada z di luar wilayah
terlatih menghasilkan titik jauh dari data. Pemain utama: VAE dengan
q(z|x) = N(mu(x), sigma(x)^2), sampling lewat reparameterization, dan
regularizer KL ke prior N(0,1), sehingga sampling z dari prior
menghasilkan titik dekat data. Deterministik (seed tetap), CPU.
"""
import math
import numpy as np
import torch
import torch.nn as nn
torch.manual_seed(0)
rng = np.random.default_rng(5)
# ---------- 1. Data: tiga cluster pada satu busur ----------
sudut = np.deg2rad([45.0, 90.0, 135.0])
pusat = np.stack(
[3.0 + 3.0 * np.cos(sudut), 0.5 + 3.0 * np.sin(sudut)], axis=1
)
X = np.vstack([c + rng.normal(0, 0.30, size=(150, 2)) for c in pusat])
Xt = torch.tensor(X, dtype=torch.float32)
print("pusat cluster:", pusat.round(2).tolist())
print(f"data: {len(X)} titik 2D (tiga cluster, std 0.30)")
print()
# ---------- 2. Reparameterization trick: verifikasi ----------
mu_c = torch.tensor(1.5, requires_grad=True)
sigma_c = torch.tensor(0.5, requires_grad=True)
eps_c = torch.tensor(0.3)
(mu_c + sigma_c * eps_c).backward()
print("=== Reparameterization: z = mu + sigma * eps ===")
print(f"eps = 0.3 -> dz/dmu = {mu_c.grad.item():.1f},"
f" dz/dsigma = {sigma_c.grad.item():.1f} (= eps)")
z_banyak = 2.0 + 0.5 * torch.randn(20000)
print(f"20000 sampel mu=2.0, sigma=0.5 -> mean {z_banyak.mean().item():.3f},"
f" std {z_banyak.std().item():.3f}")
print()
def jarak_ke_data(x_gen):
"""Jarak tiap titik generasi ke data latih terdekat."""
return np.sqrt(((x_gen[:, None, :] - X[None, :, :]) ** 2).sum(-1)).min(1)
def jarak_ke_pusat(x_gen):
"""Jarak tiap titik generasi ke pusat cluster terdekat."""
return np.sqrt(((x_gen[:, None, :] - pusat[None, :, :]) ** 2).sum(-1)).min(1)
# ---------- 3. Pembanding: autoencoder biasa ----------
class Autoencoder(nn.Module):
def __init__(self):
super().__init__()
self.encoder = nn.Sequential(nn.Linear(2, 16), nn.Tanh(), nn.Linear(16, 1))
self.decoder = nn.Sequential(nn.Linear(1, 16), nn.Tanh(), nn.Linear(16, 2))
def forward(self, x):
return self.decoder(self.encoder(x))
ae = Autoencoder()
opt = torch.optim.Adam(ae.parameters(), lr=1e-2)
for ep in range(800):
loss = ((ae(Xt) - Xt) ** 2).sum(1).mean()
opt.zero_grad()
loss.backward()
opt.step()
with torch.no_grad():
z_latih = ae.encoder(Xt).squeeze(1)
zmin, zmax = z_latih.min().item(), z_latih.max().item()
p_luar = 0.5 * (1 + math.erf(zmin / math.sqrt(2))) + 0.5 * (1 - math.erf(zmax / math.sqrt(2)))
print("=== Autoencoder biasa (pembanding) ===")
print(f"MSE rekonstruksi : {loss.item():.4f} (bagus sebagai kompresor)")
for k in range(3):
zk = z_latih[k * 150:(k + 1) * 150]
print(f" z cluster {k}: [{zk.min():.2f}, {zk.max():.2f}]")
print(f"z total [{zmin:.2f}, {zmax:.2f}];"
f" P(sampel N(0,1) jatuh di luar rentang) = {p_luar:.3f}")
z_prior = rng.normal(0, 1, size=(1000, 1))
x_ae = ae.decoder(torch.tensor(z_prior, dtype=torch.float32)).detach().numpy()
print(f"sampling z~N(0,1): jarak ke data terdekat {jarak_ke_data(x_ae).mean():.3f},"
f" dekat pusat {(jarak_ke_pusat(x_ae) < 0.9).mean():.3f}")
print()
# ---------- 4. VAE: q(z|x), reparameterization, KL ----------
class VAE(nn.Module):
def __init__(self):
super().__init__()
self.enc_fc = nn.Sequential(nn.Linear(2, 16), nn.Tanh())
self.mu = nn.Linear(16, 1)
self.logvar = nn.Linear(16, 1)
self.decoder = nn.Sequential(nn.Linear(1, 16), nn.Tanh(), nn.Linear(16, 2))
def encode(self, x):
h = self.enc_fc(x)
return self.mu(h), self.logvar(h)
def forward(self, x):
mu, logvar = self.encode(x)
eps = torch.randn_like(mu)
z = mu + torch.exp(0.5 * logvar) * eps
return self.decoder(z), mu, logvar
vae = VAE()
opt = torch.optim.Adam(vae.parameters(), lr=1e-2)
print("=== Pelatihan VAE (rekon deterministik z=mu + KL) ===")
for ep in range(1600):
x_rekon, mu, logvar = vae(Xt)
rekon = ((x_rekon - Xt) ** 2).sum(1).mean()
kl = (0.5 * (mu ** 2 + logvar.exp() - 1 - logvar)).sum(1).mean()
loss = rekon + kl
opt.zero_grad()
loss.backward()
opt.step()
if ep % 400 == 0 or ep == 1599:
with torch.no_grad():
rekon_det = ((vae.decoder(mu) - Xt) ** 2).sum(1).mean().item()
print(f" epoch {ep:4d} rekon = {rekon_det:.3f} KL = {kl.item():.3f}"
f" total = {rekon_det + kl.item():.3f}")
print()
with torch.no_grad():
mu_latih, logvar_latih = vae.encode(Xt)
print("posterior q(z|x) per cluster:")
for k in range(3):
muk = mu_latih[k * 150:(k + 1) * 150]
sigmak = logvar_latih[k * 150:(k + 1) * 150].exp().sqrt()
print(f" cluster {k}: mu {muk.mean():+.2f}, sigma {sigmak.mean():.2f}")
print()
# ---------- 5. Sampling dari prior N(0,1) ----------
z_prior2 = rng.normal(0, 1, size=(1000, 1))
x_vae = vae.decoder(torch.tensor(z_prior2, dtype=torch.float32)).detach().numpy()
print("=== Sampling z ~ N(0,1) ===")
print(f"model AE : jarak ke data {jarak_ke_data(x_ae).mean():.3f},"
f" dekat pusat {(jarak_ke_pusat(x_ae) < 0.9).mean():.3f}")
print(f"model VAE: jarak ke data {jarak_ke_data(x_vae).mean():.3f},"
f" dekat pusat {(jarak_ke_pusat(x_vae) < 0.9).mean():.3f}")
print()
print("decoder pada tiga nilai z yang sama:")
for zv in (-1.5, 0.0, 1.5):
zt = torch.tensor([[zv]], dtype=torch.float32)
with torch.no_grad():
xa = ae.decoder(zt).numpy()[0]
xv = vae.decoder(zt).numpy()[0]
xa = np.round(xa.astype(np.float64), 2).tolist()
xv = np.round(xv.astype(np.float64), 2).tolist()
dp = jarak_ke_pusat(np.array([xv]))[0]
print(f" z = {zv:+.1f} AE -> {str(xa):16s} VAE -> {str(xv):16s}"
f" (jarak VAE ke pusat {dp:.2f})")Output yang Dihasilkan:
pusat cluster: [[5.12, 2.62], [3.0, 3.5], [0.88, 2.62]]
data: 450 titik 2D (tiga cluster, std 0.30)
=== Reparameterization: z = mu + sigma * eps ===
eps = 0.3 -> dz/dmu = 1.0, dz/dsigma = 0.3 (= eps)
20000 sampel mu=2.0, sigma=0.5 -> mean 1.994, std 0.502
=== Autoencoder biasa (pembanding) ===
MSE rekonstruksi : 0.0887 (bagus sebagai kompresor)
z cluster 0: [-2.56, -1.00]
z cluster 1: [-0.44, 0.18]
z cluster 2: [0.45, 0.91]
z total [-2.56, 0.91]; P(sampel N(0,1) jatuh di luar rentang) = 0.188
sampling z~N(0,1): jarak ke data terdekat 0.360, dekat pusat 0.654
=== Pelatihan VAE (rekon deterministik z=mu + KL) ===
epoch 0 rekon = 20.069 KL = 0.109 total = 20.178
epoch 400 rekon = 0.131 KL = 0.940 total = 1.071
epoch 800 rekon = 0.138 KL = 0.992 total = 1.130
epoch 1200 rekon = 0.121 KL = 0.938 total = 1.059
epoch 1599 rekon = 0.124 KL = 0.993 total = 1.117
posterior q(z|x) per cluster:
cluster 0: mu +1.15, sigma 0.42
cluster 1: mu -0.02, sigma 0.29
cluster 2: mu -1.14, sigma 0.46
=== Sampling z ~ N(0,1) ===
model AE : jarak ke data 0.360, dekat pusat 0.654
model VAE: jarak ke data 0.091, dekat pusat 0.880
decoder pada tiga nilai z yang sama:
z = -1.5 AE -> [5.07, 2.7] VAE -> [0.78, 2.55] (jarak VAE ke pusat 0.12)
z = +0.0 AE -> [2.62, 3.45] VAE -> [2.98, 3.46] (jarak VAE ke pusat 0.04)
z = +1.5 AE -> [-1.25, 1.3] VAE -> [5.18, 2.53] (jarak VAE ke pusat 0.11)Mari kita telaah kode di atas baris per baris:
- Baris 22-27: data dibangun dari tiga pusat pada busur setengah lingkaran; setiap cluster berisi 150 titik dengan simpangan 0,30 — celah antar cluster inilah yang nanti menguji ruang laten.
- Baris 33-42: verifikasi reparameterization — turunan
zterhadapmutepat 1,0 dan terhadapsigmatepat 0,3 (sama denganeps); secara statistik, 20000 sampel menghasilkan mean 1,994 dan std 0,502, sesuaimu = 2.0dansigma = 0.5yang dipakai. - Baris 57-64: kelas
Autoencoderpembanding — arsitektur dan ukuran layer sama dengan VAE agar perbandingannya adil. - Baris 67-88: diagnosis ruang laten AE — MSE rekonstruksi 0,0887 (kompresor yang bagus), tetapi tiga cluster menempati rentang
zyang bersela:[-2.56, -1.00],[-0.44, 0.18],[0.45, 0.91]; 18,8% sampelN(0,1)jatuh di luar rentang terlatih, dan sampling menghasilkan titik berjarak rata-rata 0,360 dari data terdekat. - Baris 92-108: kelas
VAE— encoder berhenti dimudanlogvar(log varians untuk menjaga kenumerikan), laluforwardmenulis sampel sebagaiz = mu + exp(0.5·logvar) · eps— reparameterization persis seperti diverifikasi di poin 2. - Baris 114-126: training loop — loss = rekonstruksi + KL; total turun dari 20,178 ke 1,117. Angka rekonstruksi dicetak untuk
z = mu(deterministik) supaya lintasan loss tidak bergetar karena keacakaneps. - Baris 129-135: hasil struktural yang paling penting — posterior tiga cluster menempel rapi pada prior:
mudi +1,15, −0,02, −1,14 dengansigma0,29-0,46; distribusi-distribusi itu saling menutupi sepanjang sumbuz. - Baris 139-157: uji akhir — sampling 1000 titik
z ~ N(0,1)lalu membandingkan decoder kedua model pada tiga nilaizyang sama.
Tiga kelompok angka menutup argumen. Pertama, loss VAE turun dari 20,178 ke 1,117 dengan komponen rekonstruksi 0,124 — sedikit lebih buruk dari 0,0887 milik AE, harga yang dibayar untuk regularisasi KL. Kedua, sampling dari prior: 88,0% sampel VAE mendarat dekat pusat cluster dengan jarak rata-rata ke data 0,091 — empat kali lebih dekat daripada AE (0,360, hanya 65,4% valid). Ketiga, tabel tiga nilai z: pada z = +1.5 (di luar rentang z AE), decoder AE mengeluarkan [-1.25, 1.3] — jauh dari busur — sedangkan decoder VAE mengeluarkan [5.18, 2.53], hanya 0,11 dari pusat cluster 0. Perhatikan juga bahwa AE menempatkan cluster 0 pada z negatif dan VAE pada z positif: arah sumbu laten AE murni kebetulan pelatihan, sedangkan VAE terpaku pada prior yang kita tentukan.
Autoencoder vs Variational Autoencoder #
| Aspek | Autoencoder | VAE |
|---|---|---|
Keluaran encoder untuk satu x | satu titik z | distribusi N(mu, sigma²) |
| Bentuk ruang laten | bebas, cenderung berlubang | dipaksa mendekati prior N(0,1) |
| Sampling dari model | tidak terjamin bermakna | z ~ N(0,1) lalu decoder |
| Loss | hanya rekonstruksi | rekonstruksi + KL (negatif ELBO) |
| Kualitas rekonstruksi | lebih tajam | sedikit lebih kabur |
| Status probabilistik | bukan model peluang P(x) eksplisit | aproksimasi P(x) terkontrol |
VAE memperbaiki struktur ruang laten dengan mengikatnya pada distribusi — tetapi seluruh latihannya tetap satu jaringan yang mengejar satu loss. Keluarga pendekatan berikutnya meninggalkan formula peluang sama sekali: dua jaringan yang saling menyaingi, generator dan discriminator — pembuka dunia GAN pada chapter berikutnya.