Chapter sebelumnya memperlihatkan diffusion sebagai gagasan: data dideraui bertahap, lalu prosesnya dibalik oleh jaringan. Kini kita formalisasikan kedua arah tersebut dengan notasi Gaussian yang menjadi bahasa standar seluruh literatur diffusion — notasi beta, alpha, dan alpha_bar yang sama dipakai DDPM, Stable Diffusion, hingga implementasi di library modern. Hasil formalisasi ini adalah satu rumus tertutup yang memungkinkan kita melompat dari x_0 ke x_t mana pun tanpa menjalankan t langkah satu per satu, dan satu persamaan reverse yang menjadi target pembelajaran jaringan. Chapter ini menutup semuanya dengan implementasi NumPy yang membuktikan kesetaraan kedua bentuk secara numerik.

Notasi: β, α, dan ᾱ #

Tiga simbol ini muncul di setiap rumus diffusion, jadi kita tempatkan lebih dulu:

SimbolDefinisiMakna
β_tvariance derau pada langkah tseberapa banyak derau baru ditambahkan
α_t1 − β_tporsi sinyal yang bertahan satu langkah
ᾱ_tα_1 · α_2 · … · α_tporsi sinyal yang bertahan dari x_0 sampai x_t
εsampel N(0, I)derau yang menjadi target prediksi jaringan
ε_θ(x_t, t)keluaran denoisertebakan jaringan atas derau di dalam x_t

ᾱ_t adalah hasil kali beruntun sehingga selalu menurun ke arah nol — itulah meteran penyusutan sinyal yang kita pakai pada chapter sebelumnya (SNR = ᾱ_t / (1 − ᾱ_t)). Jadwal derau (noise schedule) adalah pilihan nilai β_1 … β_T; jadwal linear DDPM menaikkan β_t dari 1e-4 ke 0.02 selama T = 1000 langkah.

Forward Process: Rantai Gaussian dan Bentuk Tertutup #

Satu langkah forward didefinisikan sebagai distribusi kondisional Gaussian:

q(x_t | x_{t-1}) = N(sqrt(1 − β_t) · x_{t-1}, β_t I)

Artinya: kalikan x_{t-1} dengan sqrt(1 − β_t) (sinyal menyusut sedikit), lalu tambahkan derau Gaussian baru ber-variance β_t. Derivasinya memakai satu sifat Gaussian dua kali: jika x ~ N(μ, σ²), maka a·x + b·ε berdistribusi N(a·μ, a²σ² + b²). Dengan aturan itu, tiga langkah pertama dapat diuraikan menjadi:

x_3 = sqrt(ᾱ_3) · x_0 + sqrt(α_3α_2β_1) · ε_1 + sqrt(α_3β_2) · ε_2 + sqrt(β_3) · ε_3

Tiga suku derau adalah jumlah variabel Gaussian independen, sehingga digabung menjadi satu Gaussian dengan variance α_3α_2β_1 + α_3β_2 + β_3 = 1 − ᾱ_3 (aljabar sederhana: α(1−β) + β = 1 berlaku berulang). Generalisasi untuk langkah ke-t menghasilkan bentuk tertutup (closed-form):

x_t = sqrt(ᾱ_t) · x_0 + sqrt(1 − ᾱ_t) · ε, ε ~ N(0, I)

Intuisinya: pada langkah t, amplitudo sinyal asli tersisa sqrt(ᾱ_t), amplitudo derau total mencapai sqrt(1 − ᾱ_t), dan keduanya berbagi satu sumber derau ε. Bentuk ini juga adalah reparameterisasi — trik yang sama dengan VAE pada chapter Variational Autoencoder : sampel acak dikeluarkan dari rumus sehingga gradien dapat mengalir ke parameter.

Membuktikan Kedua Bentuk Cocok Secara Numerik #

Berkas forward_proses.py membuktikan kesetaraan kedua bentuk dalam dua bagian. Bagian A membuktikan pada satu titik bahwa tiga langkah iteratif sama persis dengan satu rumus tertutup memakai derau gabungan. Bagian B menjalankan T = 1000 langkah pada dua cluster 2D (512 titik) dan membandingkan statistik jalur iteratif terhadap sampel closed-form yang dibangun langsung dari x_0:

python
"""Forward diffusion: bentuk iteratif vs bentuk tertutup (closed-form).

Bagian A membuktikan pada satu titik bahwa tiga langkah kecil sama
persis dengan satu rumus tertutup yang memakai derau gabungan, serta
memeriksa identitas varians derau yang mendasarinya.
Bagian B menjalankan T = 1000 langkah pada dua cluster 2D dan
membandingkan statistik jalur iteratif terhadap sampel closed-form.
"""

import numpy as np

rng = np.random.default_rng(7)

T = 1000
beta = np.linspace(1e-4, 0.02, T)
alpha = 1.0 - beta
alpha_bar = np.cumprod(alpha)

# ---------- A. Tiga langkah kecil == satu rumus tertutup ----------
x0 = np.array([2.0, -2.0])
e1, e2, e3 = rng.standard_normal((3, 2))

x1 = np.sqrt(alpha[0]) * x0 + np.sqrt(beta[0]) * e1
x2 = np.sqrt(alpha[1]) * x1 + np.sqrt(beta[1]) * e2
x3 = np.sqrt(alpha[2]) * x2 + np.sqrt(beta[2]) * e3

derau_gabungan = (np.sqrt(alpha[2] * alpha[1] * beta[0]) * e1
                  + np.sqrt(alpha[2] * beta[1]) * e2
                  + np.sqrt(beta[2]) * e3)
x3_tertutup = np.sqrt(alpha_bar[2]) * x0 + derau_gabungan

print("=== A. Tiga langkah iteratif vs satu rumus tertutup ===")
print("x_3 iteratif           :", np.round(x3, 6))
print("x_3 tertutup           :", np.round(x3_tertutup, 6))
print("selisih maksimum       :",
      f"{np.max(np.abs(x3 - x3_tertutup)):.1e}")
varians_gabungan = (alpha[2] * alpha[1] * beta[0]
                    + alpha[2] * beta[1] + beta[2])
print(f"varians derau gabungan : {varians_gabungan:.12f}")
print(f"1 - alpha_bar_3        : {1.0 - alpha_bar[2]:.12f}")

# ---------- B. Dua cluster 2D, T = 1000 langkah ----------
n_per = 256
cluster_a = 1.5 + 0.3 * rng.standard_normal((n_per, 2))
cluster_b = -1.5 + 0.3 * rng.standard_normal((n_per, 2))
x0_semua = np.vstack([cluster_a, cluster_b])
jarak_awal = np.linalg.norm(cluster_a.mean(axis=0) - cluster_b.mean(axis=0))
std_awal = x0_semua.std()

def jarak_pusat(x):
    a = x[:n_per].mean(axis=0)
    b = x[n_per:].mean(axis=0)
    return np.linalg.norm(a - b)

# Jalur iteratif: benar-benar menjalankan 1000 langkah
x = x0_semua.copy()
snapshot = {0: x.copy()}
for t in range(1, T + 1):
    eps = rng.standard_normal(x.shape)
    x = np.sqrt(1.0 - beta[t - 1]) * x + np.sqrt(beta[t - 1]) * eps
    if t in (250, 500, 1000):
        snapshot[t] = x.copy()

# Jalur closed-form: x_t dibangun langsung dari x_0 dengan derau segar
def closed_form(t):
    if t == 0:
        return x0_semua.copy()
    ab = alpha_bar[t - 1]
    eps = rng.standard_normal(x0_semua.shape)
    return np.sqrt(ab) * x0_semua + np.sqrt(1.0 - ab) * eps

print()
print("=== B. Dua cluster 2D (512 titik), T = 1000 ===")
print(f"jarak pusat awal : {jarak_awal:.3f} | std awal : {std_awal:.3f}")
print()
print("   t   | mean iter | mean clsd | std iter | std clsd | std teori | jarak iter")
print("-------+-----------+-----------+----------+----------+-----------+-----------")
for t in (0, 250, 500, 1000):
    xi = snapshot[t]
    xc = closed_form(t)
    ab = 1.0 if t == 0 else alpha_bar[t - 1]
    std_teori = np.sqrt(ab * std_awal**2 + (1.0 - ab))
    print(f" {t:5d} | {xi.mean():9.3f} | {xc.mean():9.3f} | "
          f"{xi.std():8.3f} | {xc.std():8.3f} | {std_teori:9.3f} | "
          f"{jarak_pusat(xi):10.3f}")

print()
print(f"jarak pusat pada t=1000 (teori sqrt(alpha_bar)*{jarak_awal:.3f} "
      f"= {np.sqrt(alpha_bar[-1]) * jarak_awal:.3f})")
print(f"alpha_bar_1000 = {alpha_bar[-1]:.2e} -> x_1000 hampir murni N(0, I)")

Output yang Dihasilkan:

text
=== A. Tiga langkah iteratif vs satu rumus tertutup ===
x_3 iteratif           : [ 1.991274 -2.018132]
x_3 tertutup           : [ 1.991274 -2.018132]
selisih maksimum       : 8.9e-16
varians derau gabungan : 0.000359717016
1 - alpha_bar_3        : 0.000359717016

=== B. Dua cluster 2D (512 titik), T = 1000 ===
jarak pusat awal : 4.201 | std awal : 1.512

   t   | mean iter | mean clsd | std iter | std clsd | std teori | jarak iter
-------+-----------+-----------+----------+----------+-----------+-----------
     0 |    -0.023 |    -0.023 |    1.512 |    1.512 |     1.512 |      4.201
   250 |     0.008 |    -0.019 |    1.285 |    1.327 |     1.294 |      3.037
   500 |     0.033 |    -0.019 |    1.020 |    1.055 |     1.049 |      1.023
  1000 |    -0.031 |    -0.014 |    1.012 |    0.976 |     1.000 |      0.038

jarak pusat pada t=1000 (teori sqrt(alpha_bar)*4.201 = 0.027)
alpha_bar_1000 = 4.04e-05 -> x_1000 hampir murni N(0, I)

Bagian A menghasilkan pembuktian eksak: kedua jalur memberi x_3 yang sama hingga selisih 8.9e-16 (batas presisi float64), dan identitas varians derau gabungan cocok sampai 12 digit desimal. Bagian B memperlihatkan konvergensi menuju N(0, I) dari dua jalur berbeda: mean mengendap ke sekitar nol, std menyusut dari 1.512 menuju 1.012 (iteratif) dan 0.976 (closed-form) dengan prediksi teori tepat 1.000, sementara jarak dua pusat cluster runtuh dari 4.201 menjadi 0.038 — teori sqrt(ᾱ) · 4.201 = 0.027. Selisih kecil antar jalur wajar karena kedua jalur memakai realisasi derau berbeda; distribusinya yang identik. Mari kita telaah kode di atas baris per baris:

  1. Baris 12 (default_rng(7)): seluruh derau ditarik dari satu generator ber-seed; urutan penarikan tetap sehingga keluaran reproducible.
  2. Baris 14-17 (jadwal): np.cumprod menghitung ᾱ_t sekaligus untuk semua t — hasil kali beruntun α_1 … α_t, diindeks dari nol.
  3. Baris 20-25 (Bagian A): satu titik x_0 = (2, −2) dideraui tiga langkah secara eksplisit, satu baris per langkah, persis mengikuti definisi q(x_t | x_{t-1}).
  4. Baris 27-30 (derau gabungan): tiga suku derau dijumlahkan dengan bobot sqrt(α_3α_2β_1), sqrt(α_3β_2), sqrt(β_3) — bobot yang muncul saat ketiga update disubstitusikan bertahap. x3_tertutup lalu dibangun dengan bentuk sqrt(ᾱ_3)·x_0 + derau gabungan.
  5. Baris 37-40 (identitas varians): jumlah kuadrat bobot derau dibandingkan langsung dengan 1 − ᾱ_3; keduanya 0.000359717016 — bukti angka bahwa derau terakumulasi tepat menjadi satu Gaussian ber-variance 1 − ᾱ_t.
  6. Baris 43-48 (data): dua cluster 2D masing-masing 256 titik di sekitar (1.5, 1.5) dan (−1.5, −1.5); jarak antar pusat empirisnya 4.201.
  7. Baris 56-62 (jalur iteratif): loop T = 1000 mengimplementasikan definisi forward mentah-mentah — derau segar per langkah untuk seluruh 512 titik sekaligus lewat broadcasting.
  8. Baris 65-70 (closed_form): jalur pembanding melompat langsung dari x_0 ke x_t memakai bentuk tertutup dengan derau segar; tanpa np.cumprod dan bentuk ini, kita harus menjalankan t langkah untuk setiap t yang ingin diperiksa.
  9. Baris 78-85 (tabel): std_teori = sqrt(ᾱ·var_data + (1−ᾱ)) adalah prediksi deterministik dari bentuk tertutup; nilai tercetak pada baris t = 500 (1.049) jatuh di antara 1.020 dan 1.055 dari kedua jalur empiris.

Reverse Process yang Dipelajari Jaringan #

Membalik forward secara analitik mustahil: q(x_{t-1} | x_t) saja tidak menentukan jawaban unik, dan bentuk posterior q(x_{t-1} | x_t, x_0) yang memang tertutup membutuhkan x_0 — persis hal yang tidak kita ketahui saat sampling. Diffusion model karenanya mempelajari reverse sebagai rantai Gaussian bertparameter jaringan:

p_θ(x_{t-1} | x_t) = N(μ_θ(x_t, t), σ_t² I)

Jaringan tidak memprediksi μ_θ secara langsung; jaringan memprediksi derau ε_θ(x_t, t), lalu mean dihitung dari rumus:

μ_θ(x_t, t) = (1 / sqrt(α_t)) · (x_t − (β_t / sqrt(1 − ᾱ_t)) · ε_θ(x_t, t))

Dua pilihan σ_t yang lazim: sqrt(β_t) (mudah) atau variance posterior (lebih ketat). Sampling lengkapnya: tarik x_T ~ N(0, I), terapkan rumus di atas dari t = T turun ke t = 1, dan tambahkan derau kecil z ~ N(0, I) pada tiap langkah kecuali langkah terakhir.

Mengapa jaringan cukup memprediksi ε dengan loss MSE sederhana, dan bagaimana jadwal β memengaruhi kualitas sampel? Chapter berikutnya, Denoising dan Noise Schedule , menjawab keduanya sekaligus melatih denoiser mini pertama kita dan menjalankan sampling reverse yang utuh.