Dua pertanyaan dari chapter sebelumnya masih terbuka. Pertama, bagaimana memilih jadwal β_1 … β_T — noise schedule — karena pilihan itu menentukan seberapa cepat sinyal mati dan seberapa halus tiap langkah pembalikan. Kedua, mengapa jaringan cukup dilatih dengan MSE biasa untuk memprediksi derau, tanpa loss adversarial maupun istilah regularizer apa pun. Chapter ini menjawab keduanya lalu menggabungkan seluruh potongan menjadi pipeline utuh: denoiser mini di atas data cincin 2D, dilatih seribu iterasi, lalu menghasilkan sampel baru lewat loop sampling reverse — dari derau murni menjadi titik-titik yang menempel pada cincin.

Noise Schedule: Linear vs Cosine #

Jadwal DDPM asli bersifat linear: β_t naik dari 1e-4 ke 0.02 selama T = 1000. Persoalannya terlihat pada ᾱ_t: linear menjatuhkan sisa sinyal terlalu cepat di paruh awal, sehingga langkah-langkah tengah bekerja pada data yang hampir habis. Jadwal cosine dari Nichol & Dhariwal (Improved DDPM, arxiv.org/abs/2102.09672 ) membentuk ᾱ_t langsung sebagai kurva kosinus yang mulus turun ke nol. Berkas noise_schedule.py membandingkan keduanya:

python
"""Jadwal derau (noise schedule): linear vs cosine untuk T = 1000.

Kedua jadwal dibandingkan lewat alpha_bar_t dan SNR-nya pada beberapa
timestep, lalu kurvanya disimpan sebagai berkas noise_schedule.png.
"""

import numpy as np
import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt

T = 1000


def alpha_bar_linear(T, beta_awal=1e-4, beta_akhir=0.02):
    """Jadwal DDPM asli: beta naik linear, alpha_bar hasil kali beruntun."""
    beta = np.linspace(beta_awal, beta_akhir, T)
    return np.cumprod(1.0 - beta)


def alpha_bar_cosine(T, s=0.008):
    """Jadwal cosine (Nichol & Dhariwal 2021)."""
    def f(t):
        return np.cos((t / T + s) / (1.0 + s) * np.pi / 2.0) ** 2
    return f(np.arange(T + 1)) / f(0.0)


ab_linear = alpha_bar_linear(T)
ab_cosine = alpha_bar_cosine(T)


def snr_db(ab):
    """SNR (dB) dari alpha_bar; tangani kasus tepat 0 dan 1."""
    if ab <= 0.0:
        return float("-inf")
    if ab >= 1.0:
        return float("inf")
    return 10.0 * np.log10(ab / (1.0 - ab))


print(f"Jadwal derau untuk T = {T} langkah")
print()
print("   t   | alpha_bar linear | alpha_bar cosine | SNR lin (dB) | SNR cos (dB)")
print("-------+------------------+------------------+--------------+-------------")
for t in (1, 100, 250, 500, 750, 900, 1000):
    lin = ab_linear[t - 1]
    cos = ab_cosine[t]
    print(f" {t:5d} | {lin:16.4f} | {cos:16.4f} | "
          f"{snr_db(lin):12.1f} | {snr_db(cos):12.1f}")

print()
print("Selisih terbesar alpha_bar (t di 1..1000):",
      f"{np.max(ab_cosine[1:] - ab_linear):.3f}",
      "pada t =", int(np.argmax(ab_cosine[1:] - ab_linear)) + 1)

# ---------- Kurva alpha_bar kedua jadwal ----------
fig, ax = plt.subplots(figsize=(6.5, 3.8))
langkah = np.arange(1, T + 1)
ax.plot(langkah, ab_linear, label="linear (DDPM)", color="#1f77b4")
ax.plot(langkah, ab_cosine[1:], label="cosine", color="#d62728")
ax.set_yscale("log")
ax.set_xlabel("timestep t")
ax.set_ylabel("alpha_bar_t (skala log)")
ax.set_title("Sisa sinyal: jadwal linear vs cosine")
ax.legend()
ax.grid(True, which="both", alpha=0.3)
fig.savefig("noise_schedule.png", dpi=110, bbox_inches="tight")
print("\nKurva disimpan ke noise_schedule.png")

Output yang Dihasilkan:

text
Jadwal derau untuk T = 1000 langkah

   t   | alpha_bar linear | alpha_bar cosine | SNR lin (dB) | SNR cos (dB)
-------+------------------+------------------+--------------+-------------
     1 |           0.9999 |           1.0000 |         40.0 |         43.8
   100 |           0.8970 |           0.9721 |          9.4 |         15.4
   250 |           0.5241 |           0.8470 |          0.4 |          7.4
   500 |           0.0786 |           0.4938 |        -10.7 |         -0.1
   750 |           0.0034 |           0.1443 |        -24.7 |         -7.7
   900 |           0.0003 |           0.0241 |        -35.6 |        -16.1
  1000 |           0.0000 |           0.0000 |        -43.9 |       -324.3

Selisih terbesar alpha_bar (t di 1..1000): 0.453 pada t = 409

Kurva disimpan ke noise_schedule.png

Kurva alpha_bar jadwal linear vs cosine

Angka kuncinya: pada t = 250, linear hanya menyisakan 0.524 sinyal sedangkan cosine masih 0.847; selisih terbesar 0.453 terjadi di t = 409. Cosine menjaga langkah-langkah awal-tengah tetap kaya sinyal dan menunda proses hilangnya sinyal hingga ujung rantai — SNR cosine masih -0.1 dB di t = 500, saat linear sudah -10.7 dB. Pada model gambar nyata, perbedaan ini berdampak langsung pada kualitas sampel. Mari kita telaah kodenya baris per baris:

  1. Baris 17-20 (alpha_bar_linear): jadwal linear persis DDPM — np.linspace untuk β lalu np.cumprod untuk ᾱ.
  2. Baris 23-27 (alpha_bar_cosine): cosine justru mendefinisikan ᾱ_t lebih dulu lewat f(t)/f(0); nilai β jika perlu diperoleh dengan membalik relasi β_t = 1 − ᾱ_t/ᾱ_{t-1} (pola yang dipakai lagi di skrip kedua). Parameter s = 0.008 menggeser kurva agar β_1 tidak nol.
  3. Baris 33-39 (snr_db): SNR dihitung dari ᾱ; dua penjaga if menangani tepi ᾱ = 0 dan ᾱ = 1 agar logaritma tetap aman.
  4. Baris 41-60 (tabel dan plot): tujuh timestep cukup untuk membaca karakter kurva; plot memakai skala log agar rentang 1e-5 … 1 terlihat sekaligus, lalu disimpan ke bundle sebagai PNG.

Objectif Training: Mengapa MSE pada ε Sudah Cukup #

Objectif DDPM tampak mencurigakan sederhana:

L = E_{t, x_0, ε} [ || ε − ε_θ( sqrt(ᾱ_t)·x_0 + sqrt(1−ᾱ_t)·ε , t ) ||² ]

Pilih t acak, bangun x_t lewat bentuk tertutup, minta jaringan menebak ε, dan hukum kesalahan dengan MSE. Intuisinya: pada setiap (x_t, t), mengetahui derau ε sama dengan mengetahui x_0 (rumus pembalikan dari chapter sebelumnya) sekaligus mean reverse μ_θ. MSE antara ε dan ε_θ karenanya adalah pengukur langsung kualitas setiap langkah pembalikan — dan secara formal dapat diturunkan sebagai batas atas (bound) log-likelihood data dengan bobot tetap dari jadwal. Tidak ada dua jaringan yang saling menekan seperti GAN; gradien datang dari satu istilah kuadrat yang mulus.

Pilihan memprediksi ε (bukan langsung x_0) juga bukan kebetulan:

ParametrisasiTargetSifat
ε_θ (DDPM)derau N(0, I)target berskala seragam di semua t — stabil
x̂_0data asliskala bergantung data; galat kecil di t besar terpelintir besar saat dibalik
v (v-prediction)gabungan keduanyakompromi untuk t besar, dipakai model modern

Karena ε selalu berdistribusi sama (N(0, I)) di semua timestep, loss MSE tidak bergeser masalah skala antar t — salah satu sumber kestabilan training diffusion.

Denoiser Mini: Training dan Sampling Reverse dalam Satu Berkas #

Berkas denoiser_mini.py adalah pipeline lengkap skala meja. Data berupa 400 titik cincin radius 2.0; denoiser MLP dengan embedding timestep sinusoidal (pola sepupu positional embedding pada section Attention dan Transformer ) dilatih 1000 iterasi penuh-batch dengan Adam; sampling reverse lalu membalik T = 150 langkah dari derau murni menjadi 200 sampel baru:

python
"""Denoiser mini: melatih epsilon_theta lalu sampling reverse penuh.

Data 2D berbentuk cincin radius 2.0 (400 titik). Denoiser MLP menerima
(x_t, t) dan memprediksi derau epsilon. Training memakai bentuk tertutup
forward; sampling membalik T = 150 langkah dari derau murni menuju
sampel baru yang statistiknya dibandingkan dengan data.
Diuji pada CPU dengan PyTorch 2.14.
"""

import math

import numpy as np
import torch
import torch.nn as nn

torch.manual_seed(0)
rng = np.random.default_rng(0)

# ---------- 1. Data sintetis: cincin radius 2.0 ----------
N = 400
sudut = rng.uniform(0.0, 2.0 * np.pi, N)
r = 2.0 + 0.05 * rng.standard_normal(N)
data = np.stack([r * np.cos(sudut), r * np.sin(sudut)], axis=1)
x0 = torch.tensor(data, dtype=torch.float32)
radius_data = torch.linalg.norm(x0, dim=1)
print(f"data   : {N} titik cincin | radius rata-rata "
      f"{radius_data.mean():.3f} | std radius {radius_data.std():.3f}")

# ---------- 2. Jadwal derau cosine untuk T = 150 ----------
T = 150
s = 0.008


def f_cos(t):
    return np.cos((t / T + s) / (1.0 + s) * np.pi / 2.0) ** 2


ab_np = f_cos(np.arange(T + 1)) / f_cos(0.0)          # alpha_bar 0..T
beta_np = np.clip(1.0 - ab_np[1:] / ab_np[:-1], 1e-5, 0.999)
beta = torch.tensor(beta_np, dtype=torch.float32)
alpha = 1.0 - beta
alpha_bar = torch.cumprod(alpha, dim=0)
print(f"jadwal : cosine | beta {beta[0]:.5f}..{beta[-1]:.3f} | "
      f"alpha_bar_{T} = {alpha_bar[-1]:.2e}")

# ---------- 3. Denoiser: MLP dengan embedding timestep sinusoidal ----------
DIM_EMB = 32


def embedding_t(t):
    """Embedding sinusoidal timestep, seperti positional embedding."""
    setengah = DIM_EMB // 2
    frekuensi = torch.exp(
        -math.log(10000.0)
        * torch.arange(setengah, dtype=torch.float32) / setengah
    )
    sudut = t.float().unsqueeze(1) * frekuensi
    return torch.cat([torch.sin(sudut), torch.cos(sudut)], dim=1)


class Denoiser(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(2 + DIM_EMB, 256),
            nn.SiLU(),
            nn.Linear(256, 256),
            nn.SiLU(),
            nn.Linear(256, 2),
        )

    def forward(self, x, t):
        return self.net(torch.cat([x, embedding_t(t)], dim=1))


model = Denoiser()
n_param = sum(p.numel() for p in model.parameters())
print(f"model  : MLP [2+{DIM_EMB}, 256, 256, 2] | {n_param} parameter")

# ---------- 4. Training: prediksi epsilon dari (x_t, t) ----------
optimizer = torch.optim.Adam(model.parameters(), lr=3e-3)
loss_fn = nn.MSELoss()
ITERASI = 1000

for i in range(1, ITERASI + 1):
    t = torch.randint(1, T + 1, (N,))
    ab = alpha_bar[t - 1].unsqueeze(1)
    eps = torch.randn_like(x0)
    xt = torch.sqrt(ab) * x0 + torch.sqrt(1.0 - ab) * eps
    loss = loss_fn(model(xt, t), eps)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    if i in (1, 250, 500, 1000):
        print(f"iter {i:4d} | loss MSE {loss.item():.4f}")

# ---------- 5. Sampling: balik T langkah dari derau murni ----------
N_SAMPEL = 200
model.eval()
with torch.no_grad():
    x = torch.randn(N_SAMPEL, 2)                    # x_T ~ N(0, I)
    for t in range(T, 0, -1):
        t_batch = torch.full((N_SAMPEL,), t, dtype=torch.long)
        eps_pred = model(x, t_batch)                # tebakan derau
        ab = alpha_bar[t - 1]
        beta_t = beta[t - 1]
        koef = beta_t / torch.sqrt(1.0 - ab)
        mean = (x - koef * eps_pred) / torch.sqrt(alpha[t - 1])
        if t > 1:
            x = mean + torch.sqrt(beta_t) * torch.randn_like(x)
        else:
            x = mean                                # langkah terakhir tanpa derau

radius_sampel = torch.linalg.norm(x, dim=1)
acak = torch.randn(N_SAMPEL, 2)                    # pembanding derau murni
radius_acak = torch.linalg.norm(acak, dim=1)
pita = (radius_sampel > 1.8) & (radius_sampel < 2.2)
pita_acak = (radius_acak > 1.8) & (radius_acak < 2.2)

print(f"\nsampel : {N_SAMPEL} titik dari sampling reverse")
print(f"radius rata-rata sampel : {radius_sampel.mean():.3f} "
      f"(data: {radius_data.mean():.3f})")
print(f"std radius sampel       : {radius_sampel.std():.3f} "
      f"(data: {radius_data.std():.3f})")
print(f"di pita radius 1.8-2.2  : sampel {pita.float().mean():.1%} | "
      f"derau murni {pita_acak.float().mean():.1%}")
print("lima sampel pertama     :")
print(np.round(x[:5].numpy(), 3))

Output yang Dihasilkan:

text
data   : 400 titik cincin | radius rata-rata 2.000 | std radius 0.050
jadwal : cosine | beta 0.00037..0.999 | alpha_bar_150 = 1.08e-07
model  : MLP [2+32, 256, 256, 2] | 75266 parameter
iter    1 | loss MSE 0.9783
iter  250 | loss MSE 0.4678
iter  500 | loss MSE 0.4678
iter 1000 | loss MSE 0.4138

sampel : 200 titik dari sampling reverse
radius rata-rata sampel : 1.994 (data: 2.000)
std radius sampel       : 0.125 (data: 0.050)
di pita radius 1.8-2.2  : sampel 98.5% | derau murni 10.5%
lima sampel pertama     :
[[ 1.391 -1.658]
 [ 2.069 -0.042]
 [ 1.904  0.875]
 [ 0.532  1.884]
 [ 1.336  1.318]]

Hasilnya tegas: radius rata-rata sampel 1.994 terhadap data 2.000, dan 98.5% sampel jatuh di pita radius 1.8–2.2 — bandingkan dengan 10.5% jika titik ditarik langsung dari derau murni. Jaringan yang tidak pernah melihat proses pembalikan secara eksplisit berhasil merekonstruksi distribusi cincin dari sinyal x_t saja. Mari kita telaah kode di atas baris per baris:

  1. Baris 16-17 (seed): torch.manual_seed mengunci inisialisasi bobot dan seluruh derau PyTorch; default_rng mengunci pembuatan data — keluaran identik di setiap eksekusi.
  2. Baris 20-25 (data): sudut seragam + gangguan radius 0.05 menghasilkan cincin; torch.linalg.norm(x0, dim=1) menghitung radius tiap titik sebagai statistik pembanding.
  3. Baris 34-44 (jadwal cosine): sama seperti skrip pertama, β diperoleh dengan membalik ᾱ lalu dijepit (np.clip) ke [1e-5, 0.999] agar β tetap valid saat ᾱ mengecil drastis di ujung; alpha_bar_150 = 1.08e-07 memastikan x_T praktis murni derau.
  4. Baris 50-58 (embedding_t): timestep integer diubah menjadi vektor 32 dimensi lewat sinus-kosinus multi-frekuensi — jaringan menerima informasi t yang kaya, bukan satu skalar; SiLU dipilih karena gradiennya halus di daerah negatif.
  5. Baris 61-73 (Denoiser): forward(x, t) menyambung koordinat x (2 dimensi) dengan embedding t (32 dimensi) menjadi input 34 dimensi — satu jaringan melayani seluruh timestep.
  6. Baris 85-95 (loop training): per iterasi, torch.randint memilih t acak per titik; xt dibangun sekali lewat bentuk tertutup (tanpa loop t langkah); target adalah eps yang benar-benar dipakai. Loss naik-turun antar iterasi (0.4678 muncul dua kali) karena setiap iterasi mengestimasi ekspektasi pada t berbeda — tren menurunnya tetap jelas: 0.9783 → 0.4138.
  7. Baris 99-101 (persiapan sampling): model.eval() membekukan perilaku layer (wajib dibiasakan, lihat chapter Training Loop ); torch.no_grad() mematikan pencatatan gradien; x = torch.randn(...) adalah titik awal x_T ~ N(0, I) — sama persis dengan distribusi tujuan forward.
  8. Baris 102-112 (loop sampling reverse — inti proses): untuk t dari T turun ke 1: (a) eps_pred adalah tebakan derau jaringan pada kondisi saat ini; (b) koef = β_t / sqrt(1−ᾱ_t) dan mean = (x − koef·eps_pred) / sqrt(α_t) persis rumus μ_θ dari chapter sebelumnya — kode dan rumus saling mengunci baris demi baris; (c) untuk t > 1, derau segar sqrt(β_t)·z ditambahkan kembali sesuai σ_t = sqrt(β_t); (d) di langkah terakhir (t = 1) derau tidak ditambahkan agar sampel tidak menyimpan sisa gangguan.
  9. Baris 114-118 (statistik): radius sampel dibandingkan radius data; pembanding acak menarik titik dari N(0, I) untuk menunjukkan seberapa jauh sampling reverse mengubah distribusi.

Membaca Trade-off Jumlah Langkah #

Loop sampling mengevaluasi jaringan T kali per sampel — biaya inference bawaan diffusion. Memangkas T dari 150 ke 50 mempercepat sampling tiga kali, tetapi tiap langkah kini membuang derau lebih besar sehingga kualitas sampel menurun. Riset decoding modern (DDIM, sampler konsistensi) memangkas ratusan langkah menjadi belasan dengan mengubah struktur loop-nya; loop ancestral di atas tetap rujukan paling mudah dipahami.

Pipeline kita bekerja pada ruang data asli berdimensi rendah. Pada gambar 512×512×3, ratusan evaluasi denoiser pada tensor piksel sebesar itu terasa berat — mahal saat training maupun sampling. Chapter Latent Diffusion menyelesaikannya dengan memindahkan seluruh proses diffusion ke ruang laten terkompresi.