00 Generative Model Ailesi
Diffusion modeller, generative model ailesi içinde training stabilitesi ve çeşitlilik açısından en güçlü konuma oturmuştur.
Üretici yapay zeka tarihi boyunca birkaç temel paradigma birbiriyle rekabet etmiştir. Generative Adversarial Network (GAN), 2014 yılında Ian Goodfellow tarafından tanıtıldı: bir üretici (generator) ile bir ayırt edici (discriminator) birbiriyle "rekabet" ederek görüntü üretmeyi öğrenir. GAN'lar birkaç yıl boyunca görüntü üretiminde fiili standart oldu, ancak training instability, mode collapse ve değerlendirme zorluğu gibi kronik sorunlar hiçbir zaman tam olarak çözülemedi.
Variational Autoencoder (VAE) aynı dönemde ortaya çıktı ve olasılıksal bir latent uzay öğrenerek yeniden örnekleme imkânı sağladı. Eğitimi stabildir, ancak üretilen görüntüler çoğu zaman bulanık kalır çünkü ELBO kaybı piksel düzeyinde ortalama almaya eğilimlidir. Normalizing Flow modelleri ise tam olasılık hesabı yapar, ancak mimari kısıtları nedeniyle ölçeklenmesi zordur.
Diffusion modeller, 2020 sonrasında DALL-E 2, Imagen ve Stable Diffusion ile ana akıma girdi. Temel fikir son derece sezgisel: gerçek bir görüntüyü adım adım Gaussian gürültüyle boz (forward process), ardından bu bozulmayı tersine çevirmeyi öğren (reverse process). Model eğitim sırasında, verilen gürültülü görüntüden eklenen gürültüyü tahmin etmeyi öğrenir — bu görev şaşırtıcı biçimde basit ve stabiltir.
| Model Ailesi | Eğitim Stabilitesi | Çeşitlilik | Görüntü Kalitesi | Metin Koşullandırma | Örnekleme Hızı |
|---|---|---|---|---|---|
| GAN | Düşük (mode collapse) | Sınırlı | Yüksek (keskin) | Zor | Hızlı (tek geçiş) |
| VAE | Yüksek | İyi | Düşük (bulanık) | Orta | Hızlı |
| Normalizing Flow | Yüksek | İyi | Orta | Zor | Orta |
| Diffusion | Çok Yüksek | Çok İyi | Çok Yüksek | Mükemmel | Yavaş (T adım) |
Diffusion modellerin GAN'ı geçmesinin birkaç kritik nedeni vardır. Birincisi, eğitim kaybı basit bir MSE (Mean Squared Error) türevidir; GAN'ın adversarial oyunundan çok daha stabittir. İkincisi, model aynı anda binlerce farklı gürültü seviyesinde eğitildiği için veri dağılımını çok daha kapsamlı öğrenir — mode collapse riski minimumdur. Üçüncüsü ve belki en önemlisi, Classifier-Free Guidance tekniği diffusion modellerini metin koşullandırmasına son derece uyumlu hale getirdi; bu sayede DALL-E 2, Imagen ve Stable Diffusion gibi modeller ortaya çıkabildi.
2021: DALL-E (OpenAI) — dVAE + transformer. 2022: DALL-E 2 — CLIP + diffusion. Imagen (Google) — T5 + diffusion. Stable Diffusion (Stability AI + LMU München) — LDM, ilk büyük açık kaynak model. 2023-2024: SDXL, SD3, Flux ile mimari evrimi devam etti.
01 Gerçek görüntü x_0 → Forward process → Saf gürültü x_T 02 Model eğitimi: x_t verildiğinde ε_θ(x_t, t) ≈ ε tahmin et 03 Inference: x_T (gürültü) → Reverse process → Üretilen görüntü x_0 04 Koşullandırma: metin prompt → CLIP/T5 embedding → cross-attention
01 Forward Process — Gürültü Ekleme
Forward process, gerçek veri dağılımını T adımda saf Gaussian gürültüye dönüştüren sabit bir Markov zinciridir.
Forward process (ya da diffusion process), modelin öğreneceği şey değildir — tamamen deterministik ve önceden tanımlıdır. Her adımda mevcut görüntüye az miktarda Gaussian gürültü eklenir. Markov zinciri özelliği gereği, x_t sadece x_{t-1}'e koşulludur. T adım sonunda (genellikle T = 1000), orijinal görüntü tamamen gürültüye dönüşmüş olur; yani dağılım standart bir Gaussian N(0, I)'ya yaklaşmıştır.
Tek adım geçişi şu şekilde yazılır:
q(x_t | x_{t-1}) = N(x_t; √(1-β_t) · x_{t-1}, β_t · I) — Her adımda önceki görüntü biraz küçülür (√(1-β_t) faktörü) ve β_t kadar gürültü eklenir. β_t ∈ (0, 1) noise schedule tarafından belirlenen küçük değerlerdir.
Forward process'in kritik özelliği, reparameterization sayesinde herhangi bir t adımı için x_t'yi doğrudan x_0'dan örnekleyebilmektir. ᾱ_t = ∏_{s=1}^{t} (1 - β_s) (kümülatif çarpım) tanımlanırsa:
q(x_t | x_0) = N(x_t; √ᾱ_t · x_0, (1-ᾱ_t) · I) — Bu, eğitim sırasında rastgele bir t seçip doğrudan x_t örneklememizi sağlar. Her seferinde t adım ileri simüle etmek yerine tek formülle hesaplanır.
Reparameterization trick ile bu örnekleme şu şekilde yazılır: x_t = √ᾱ_t · x_0 + √(1-ᾱ_t) · ε, burada ε ~ N(0, I). Bu formül eğitimin merkezinde yer alır: verilen x_0 ve t için, x_t'yi anında üretebiliriz.
import numpy as np
import matplotlib.pyplot as plt
# ── Noise schedule (linear DDPM) ─────────────────────────────
T = 1000
beta_start, beta_end = 1e-4, 0.02
betas = np.linspace(beta_start, beta_end, T) # (T,)
alphas = 1.0 - betas
alpha_bar = np.cumprod(alphas) # ᾱ_t
def forward_sample(x0: np.ndarray, t: int) -> tuple:
"""x_t = sqrt(ᾱ_t) * x0 + sqrt(1 - ᾱ_t) * ε"""
ab = alpha_bar[t]
eps = np.random.randn(*x0.shape)
x_t = np.sqrt(ab) * x0 + np.sqrt(1.0 - ab) * eps
return x_t, eps
# ── 5 adım görselleştirme ────────────────────────────────────
np.random.seed(42)
x0 = np.random.randn(64, 64) # sahte 64×64 görüntü
timesteps = [0, 100, 250, 500, 750, 999]
for t in timesteps:
x_t, _ = forward_sample(x0, t)
signal_ratio = alpha_bar[t]
noise_ratio = 1.0 - alpha_bar[t]
print(f"t={t:4d} | ᾱ_t={signal_ratio:.4f} | "
f"sinyal: {signal_ratio*100:.1f}% | gürültü: {noise_ratio*100:.1f}%")
# t= 0 | ᾱ_t=0.9999 | sinyal: 99.9% | gürültü: 0.1%
# t= 100 | ᾱ_t=0.9486 | sinyal: 94.9% | gürültü: 5.1%
# t= 250 | ᾱ_t=0.7939 | sinyal: 79.4% | gürültü: 20.6%
# t= 500 | ᾱ_t=0.4647 | sinyal: 46.5% | gürültü: 53.5%
# t= 750 | ᾱ_t=0.1534 | sinyal: 15.3% | gürültü: 84.7%
# t= 999 | ᾱ_t=0.0001 | sinyal: 0.0% | gürültü:100.0%
Çıktıdan görüleceği üzere t=500 civarında sinyal ve gürültü dengede bulunmaktadır; t=999'da ise görüntü neredeyse tamamen standart Gaussian gürültüye dönüşmüştür. Bu progression, modelin farklı "kir seviyeleri"nde görüntüyü tanımayı öğrenmesini sağlar.
02 Reverse Process — Gürültü Çıkarma
Reverse process, gürültüden görüntüye giden öğrenilen bir Markov zinciridir ve her adım sinir ağı tarafından parametrize edilir.
Eğer forward process veriyi bozuyorsa, reverse process onu onarır. Gerçek reverse posterior q(x_{t-1} | x_t, x_0) biliniyor olsa bunu kullanırdık, ancak x_0'ı bilmiyoruz — onu üretmeye çalışıyoruz. Dolayısıyla reverse transition'ı bir sinir ağıyla parametrize ederiz:
p_θ(x_{t-1} | x_t) = N(x_{t-1}; μ_θ(x_t, t), σ_t² · I) — Model μ_θ'yı (ve isteğe bağlı olarak σ_t'yi) öğrenir. Sabit varyans σ_t² = β_t ya da β̃_t = (1-ᾱ_{t-1})/(1-ᾱ_t) · β_t kullanılabilir.
DDPM makalesinin önemli bir katkısı, ağın doğrudan μ_θ'yı tahmin etmek yerine ε_θ — yani eklenen gürültüyü — tahmin etmesinin daha iyi sonuç verdiğini göstermesidir (ε-prediction). Bu tercih, eğitim kaybını büyük ölçüde sadeleştirir. Ortalama μ_θ, tahmin edilen ε'dan türetilebilir:
μ_θ(x_t, t) = (1/√α_t) · (x_t − β_t/√(1-ᾱ_t) · ε_θ(x_t, t)) — x_t'den tahmin edilen gürültü çıkarılır, sonuç normalize edilir.
import torch
import numpy as np
# ── Basitleştirilmiş DDPM sampling loop ──────────────────────
def ddpm_sample(model, shape, betas, device="cuda"):
"""T adım geri giderek x_T'den x_0 üret."""
T = len(betas)
alphas = 1.0 - betas
alpha_bar = torch.cumprod(torch.tensor(alphas), dim=0).to(device)
# x_T: saf gürültüden başla
x = torch.randn(*shape).to(device)
model.eval()
with torch.no_grad():
for t in reversed(range(T)): # T-1, T-2, ..., 0
t_tensor = torch.full((shape[0],), t, dtype=torch.long).to(device)
# Model gürültüyü tahmin eder
eps_pred = model(x, t_tensor) # ε_θ(x_t, t)
# μ_θ hesapla
alpha_t = alphas[t]
alpha_bar_t = alpha_bar[t]
beta_t = betas[t]
coef = beta_t / torch.sqrt(1.0 - alpha_bar_t)
mu = (1.0 / torch.sqrt(torch.tensor(alpha_t))) * (x - coef * eps_pred)
# t=0 adımında gürültü ekleme
if t > 0:
z = torch.randn_like(x)
sigma_t = torch.sqrt(torch.tensor(beta_t))
x = mu + sigma_t * z
else:
x = mu # son adımda gürültüsüz
return x # x_0: üretilen görüntü
# ── Kullanım örneği ──────────────────────────────────────────
# model = UNet(...) # eğitilmiş denoising U-Net
# betas = np.linspace(1e-4, 0.02, 1000)
# images = ddpm_sample(model, shape=(4, 3, 64, 64), betas=betas)
# # images: (4, 3, 64, 64) tensor, 4 üretilen görüntü
Bu sampling döngüsü T=1000 adım gerektirir; her adımda bir UNet forward pass yapılır. Bu nedenle DDPM örneklemesi yavaştır. Daha hızlı sampling için DDIM ve DPM-Solver gibi deterministic veya daha az adımla çalışan yöntemler geliştirilmiştir (bkz. s8).
03 DDPM Matematiği
DDPM'nin eğitim hedefi, karmaşık olasılıksal türetimden başlayarak놀랍도록 basit bir MSE kaybına indirgenir.
DDPM (Denoising Diffusion Probabilistic Models, Ho et al. 2020), modeli olasılıksal bir çerçevede eğitmek için ELBO (Evidence Lower Bound) maksimizasyonunu kullanır. Tam log-likelihood log p_θ(x_0)'ı doğrudan maksimize etmek imkânsızdır; bunun yerine aşağıdaki alt sınırı maksimize ederiz:
log p_θ(x_0) ≥ ELBO = E_q[ log p_θ(x_0|x_1) ] − KL(q(x_T|x_0) ‖ p(x_T)) − Σ KL(q(x_{t-1}|x_t,x_0) ‖ p_θ(x_{t-1}|x_t)) — İlk terim reconstruction, son terim denoising eşleşmesidir. Orta KL sabit (öğrenilmiş parametre içermez).
DDPM makalesinin sürpriz katkısı, bu KL terimlerinin kapalı formda hesaplanabilir olduğunu göstermesidir çünkü hem q hem de p Gaussian'dır. KL'yi açtığımızda eğitim hedefi şu şekilde sadeleşir:
L_simple = E_{t, x_0, ε} [ ‖ε − ε_θ(√ᾱ_t · x_0 + √(1-ᾱ_t) · ε, t)‖² ] — Rastgele bir t seç, rastgele bir ε çek, gürültülü x_t oluştur, modelin tahmin ettiği gürültü ile gerçek gürültü arasındaki MSE'yi minimize et.
Bu sadeleştirme neden işe yarar? Çünkü forward process Gaussian zincirinin analitik özellikleri (reparameterization) sayesinde, herhangi bir t için gürültülü örneği anında üretebilir ve modelin tahminini tek bir adımda değerlendirebiliriz. Bu, GAN'ın iki ağ ve narin bir minimax dengesine kıyasla son derece basit ve stabil bir eğitim döngüsü anlamına gelir.
import torch
import torch.nn.functional as F
def train_step(model, optimizer, x0_batch, alpha_bar, device):
"""DDPM L_simple: tek eğitim adımı."""
batch_size = x0_batch.size(0)
# 1. Her örnek için rastgele t seç: t ∈ [0, T-1]
t = torch.randint(0, len(alpha_bar), (batch_size,), device=device)
# 2. Gerçek gürültü örnekle: ε ~ N(0, I)
eps = torch.randn_like(x0_batch)
# 3. x_t oluştur: reparameterization
ab = alpha_bar[t].view(-1, 1, 1, 1) # broadcast için reshape
x_t = torch.sqrt(ab) * x0_batch + torch.sqrt(1.0 - ab) * eps
# 4. Model gürültüyü tahmin eder
eps_pred = model(x_t, t)
# 5. L_simple: MSE(ε_pred, ε_gerçek)
loss = F.mse_loss(eps_pred, eps)
# 6. Backprop + optimizer adımı
optimizer.zero_grad()
loss.backward()
optimizer.step()
return loss.item()
# ── Eğitim döngüsü ───────────────────────────────────────────
# for epoch in range(num_epochs):
# for x0_batch in dataloader:
# x0_batch = x0_batch.to(device)
# loss = train_step(model, optimizer, x0_batch, alpha_bar, device)
# # Tipik kayıp: başlangıçta ~1.0, yakınsayınca ~0.05-0.15
Eğitim algoritması şu kadar basittir: (1) veri setinden x_0 örnekle, (2) rastgele t seç, (3) rastgele ε örnekle, (4) x_t hesapla, (5) modelin ε tahminini değerlendir, (6) MSE kaybını backprop et. Yakınsama, GAN'a kıyasla çok daha güvenilirdir ve loss değeri doğrudan model kalitesini yansıtır.
04 Noise Schedule
β değerlerinin hangi hıza göre artacağını belirleyen noise schedule, örnekleme kalitesini ve eğitim dinamiklerini doğrudan etkiler.
Forward process'in kalitesi büyük ölçüde β_t değerlerinin seçimine, yani noise schedule'a bağlıdır. Orijinal DDPM'de doğrusal (linear) bir schedule kullanılmış ve sonraki çalışmalar bunun optimum olmadığını göstermiştir. Temel problem şudur: linear schedule'da, yüksek t değerlerinde görüntü zaten tamamen gürültüye dönmüştür; modelin bu adımlarda öğrenecek anlamlı yapı kalmaz ve kapasite israf olur.
Signal-to-Noise Ratio (SNR)
SNR(t) = ᾱ_t / (1 - ᾱ_t) — Bu oran, t'nin fonksiyonu olarak sinyal ve gürültü dengesini gösterir. İyi bir schedule, SNR'ın geniş bir aralığı düzgün biçimde kapsamasını sağlar: çok erken sıfıra düşmemeli (kapasite israfı), çok geç sıfıra düşmemelidir (yetersiz gürültüleme).
| Schedule | Tanım | Avantaj | Dezavantaj | Kullanım |
|---|---|---|---|---|
| Linear | β = linspace(1e-4, 0.02, T) | Basit, orijinal DDPM | Yüksek t'de görüntü çok erken biter | DDPM, SD 1.x |
| Cosine | ᾱ_t = cos²(π/2 · t/T) gibi düşer | Düzgün SNR düşüşü, daha kaliteli | Biraz daha karmaşık | IDDPM, DALL-E 2 |
| Karras (EDM) | Sürekli gürültü, log-normal örnekleme | En az adımda en yüksek kalite | Farklı formülasyon, uyarlama gerekir | EDM, SDXL Turbo |
import numpy as np
T = 1000
t_arr = np.arange(T)
# ── 1. Linear schedule (DDPM orijinal) ──────────────────────
beta_linear = np.linspace(1e-4, 0.02, T)
alpha_bar_linear = np.cumprod(1.0 - beta_linear)
snr_linear = alpha_bar_linear / (1.0 - alpha_bar_linear)
# ── 2. Cosine schedule (IDDPM / Improved DDPM) ──────────────
s = 0.008 # offset sabiti
f_t = np.cos((t_arr / T + s) / (1 + s) * np.pi / 2) ** 2
f_0 = np.cos(s / (1 + s) * np.pi / 2) ** 2
alpha_bar_cosine = f_t / f_0
alpha_bar_cosine = np.clip(alpha_bar_cosine, 0, 0.9999)
snr_cosine = alpha_bar_cosine / (1.0 - alpha_bar_cosine)
# ── 3. Karras (EDM) sigma schedule ──────────────────────────
sigma_min, sigma_max = 0.002, 80.0
rho = 7.0 # EDM önerilen
step = t_arr / (T - 1)
sigma_karras = (
sigma_max ** (1 / rho) + step *
(sigma_min ** (1 / rho) - sigma_max ** (1 / rho))
) ** rho
# ── Karşılaştırma: SNR @ t=[0, 250, 500, 750, 999] ──────────
checkpoints = [0, 250, 500, 750, 999]
print(f"{'t':<6} {'SNR-Linear':<14} {'SNR-Cosine':<14}")
for t in checkpoints:
print(f"{t:<6} {snr_linear[t]:<14.4f} {snr_cosine[t]:<14.4f}")
# t=0 SNR ~ 10000 (neredeyse temiz görüntü)
# t=500 Linear~0.86, Cosine~1.0 (daha dengeli)
# t=999 SNR ~ 0.0001 (tamamen gürültü)
Cosine schedule'ın temel avantajı, linear schedule'ın yüksek t'de SNR'ı çok hızlı düşürmesinin aksine, geçişi çok daha pürüzsüz yapmasıdır. Bu, modele daha geniş bir gürültü aralığında anlamlı gradyan sinyali sağlar ve özellikle düşük adımlı (50-100 step) inference için daha iyi görüntü kalitesine yol açar.
05 Denoising U-Net
Diffusion modelinin kalbi olan U-Net, hem yerel detayları hem de küresel yapıyı aynı anda işleyecek şekilde tasarlanmıştır.
U-Net mimarisi, tıbbi görüntü segmentasyonu için 2015 yılında Ronneberger ve arkadaşları tarafından önerildi. Diffusion modellerinde ise gürültü tahmini için kusursuz bir uyum göstermektedir. Temel yapı şöyledir: bir encoder (inen yol) görüntüyü giderek küçülen feature map'lere sıkıştırır, bir bottleneck en sıkıştırılmış temsili işler, bir decoder (çıkan yol) orijinal boyuta geri açar. Skip connections, encoder'ın her seviyesini karşılık gelen decoder katmanına doğrudan bağlar; bu sayede ince spatial detaylar bottleneck'te kaybolmaz.
Time Embedding
U-Net'in diffusion versiyonundaki kilit fark, ağın kaçıncı gürültü adımında olduğunu bilmesidir. Time embedding, t değerini ağa enjekte eder. İşlem şu sırayı izler: (1) Transformer positional encoding ile aynı sinüzoidal formülle t'yi yüksek boyutlu vektöre çevir, (2) iki linear katman + aktivasyon ile dönüştür, (3) her ResNet bloğunda bu embedding'i feature map'e toplama yoluyla ekle. Bu, aynı ağın T farklı "denoising modu"nda davranmasını sağlar.
Attention Layers
Stable Diffusion'ın U-Net'i (UNet2DConditionModel), her resolution level'da ResNet blokları ve Transformer blokları içerir. Transformer blokları self-attention ve cross-attention içerir: self-attention görüntünün kendi içindeki ilişkileri yakalar, cross-attention ise metin gömme vektörlerini görüntüye entegre eder (bkz. s7).
from diffusers import UNet2DConditionModel
# SD 1.5 UNet yapısını yükle ve incele
unet = UNet2DConditionModel.from_pretrained(
"runwayml/stable-diffusion-v1-5",
subfolder="unet"
)
print("--- UNet Yapısı ---")
print(f"In channels: {unet.config.in_channels}") # 4 (latent)
print(f"Out channels: {unet.config.out_channels}") # 4 (gürültü tahmini)
print(f"Cross-attn dim: {unet.config.cross_attention_dim}") # 768
print(f"Block channels: {unet.config.block_out_channels}")
# (320, 640, 1280, 1280) — her encoder seviyesinin kanal sayısı
# Parametre sayısı
params = sum(p.numel() for p in unet.parameters())
print(f"Toplam parametre: {params/1e6:.1f}M") # ~860M
# Down blocks: encoder
for i, block in enumerate(unet.down_blocks):
print(f"Down {i}: {type(block).__name__}")
# Down 0: CrossAttnDownBlock2D (resnet + cross-attention)
# Down 1: CrossAttnDownBlock2D
# Down 2: CrossAttnDownBlock2D
# Down 3: DownBlock2D (sadece resnet)
# Mid block: bottleneck
print(f"Mid: {type(unet.mid_block).__name__}")
# Mid: UNetMidBlock2DCrossAttn
U-Net'in kanal boyutları (320, 640, 1280, 1280) encoder'ın her seviyesinde iki katına çıkar. Bu, ağın daha derin katmanlarda daha soyut özellikleri daha yüksek kapasiteyle temsil etmesini sağlar. Skip connection'lar sayesinde decoder, hem bottleneck'teki global anlam hem de encoder'ın erken katmanlarındaki ince texture ve kenar bilgilerini birleştirebilir.
06 Latent Diffusion — VAE + Diffusion
Piksel uzayında diffusion aşırı hesaplama gerektirir; Latent Diffusion, bu maliyeti sıkıştırılmış latent uzayda çalışarak dramatik biçimde düşürür.
512×512 piksel boyutunda bir görüntü üzerinde doğrudan T=1000 adım diffusion yapmak son derece pahalıdır: her adımda 512×512×3 = ~786K boyutunda tensörler işlenir. Latent Diffusion Model (LDM), Rombach ve arkadaşları tarafından 2022'de önerildi ve Stable Diffusion'ın temelini oluşturur. Çözüm şudur: önce bir VAE ile görüntüyü çok daha küçük bir latent uzaya sıkıştır, diffusion'ı bu latent uzayda yap, sonuçta latent'i tekrar görüntüye decode et.
Stable Diffusion'da kullanılan VAE 8× sıkıştırma uygular: 512×512×3 piksel görüntü → 64×64×4 latent vektörü. Bu, 786K yerine sadece ~16K boyutunda tensörlerle çalışmak anlamına gelir — yaklaşık 49× daha az hesaplama. Tüm diffusion süreci bu küçük latent uzayda gerçekleşir. Sadece son adımda VAE decoder latent'i tekrar görüntüye çevirir.
KL-Regularized VAE
Latent Diffusion'daki VAE, standart bir Variational Autoencoder'dır ancak küçük bir KL katsayısı (λ ≈ 10⁻⁶) ile eğitilmiştir. Bu, latent uzayın düzenli (yaklaşık Gaussian) kalmasını sağlar — diffusion için gerekli bir özellik. Bunun yanı sıra perceptual loss ve adversarial (GAN) loss da eklenerek görüntü kalitesi iyileştirilmiştir.
import torch
from diffusers import AutoencoderKL
from PIL import Image
from torchvision import transforms
# ── VAE yükle ────────────────────────────────────────────────
vae = AutoencoderKL.from_pretrained(
"runwayml/stable-diffusion-v1-5",
subfolder="vae",
torch_dtype=torch.float16
).to("cuda")
# ── Görüntüyü latent uzaya encode et ────────────────────────
transform = transforms.Compose([
transforms.Resize((512, 512)),
transforms.ToTensor(),
transforms.Normalize([0.5], [0.5]) # [-1, 1] aralığına normalize
])
img = Image.open("input.jpg").convert("RGB")
pixel_tensor = transform(img).unsqueeze(0).half().to("cuda")
print(f"Piksel tensör: {pixel_tensor.shape}") # (1, 3, 512, 512)
with torch.no_grad():
latent_dist = vae.encode(pixel_tensor)
latent = latent_dist.latent_dist.sample()
latent = latent * vae.config.scaling_factor # SD'de 0.18215
print(f"Latent tensör: {latent.shape}") # (1, 4, 64, 64)
# ── Latent'i tekrar görüntüye decode et ─────────────────────
with torch.no_grad():
latent_unscaled = latent / vae.config.scaling_factor
decoded = vae.decode(latent_unscaled).sample # (1, 3, 512, 512)
# Tensor → PIL Image
decoded = (decoded.clamp(-1, 1) + 1) / 2 # [0, 1] aralığına
decoded_pil = transforms.ToPILImage()(decoded[0].cpu().float())
decoded_pil.save("reconstructed.jpg")
# Encode → Decode süreci görüntüde minimal kayıp oluşturur
Scaling factor (0.18215): VAE latent'lerinin standart sapması yaklaşık 1'e normalize etmek için kullanılır. Bu değer, diffusion modelinin latent uzayı beklenen değer aralığında görmesini sağlar. SDXL'de farklı bir scaling factor kullanılır.
07 Text Conditioning
Metin prompt'unu görüntüye dönüştürmek için CLIP text encoder ve cross-attention, U-Net'in her bloğuna anlam enjekte eder.
Stable Diffusion'da metin koşullandırmasının ilk adımı text encoding'dir. CLIP ViT-L/14 text encoder'ı, token dizisini 77 token × 768 boyutlu embedding matrisine çevirir. Bu matris, U-Net'teki her Transformer bloğunun cross-attention katmanlarına key ve value olarak enjekte edilir; latent görüntü feature'ları ise query olarak kullanılır. Bu sayede denoising her adımda metin içeriğine "bakabilir".
Classifier-Free Guidance (CFG)
Classifier-Free Guidance (Ho & Salimans, 2022), metin koşullandırmasının kalitesini dramatik biçimde artıran bir tekniktir. Eğitim sırasında rastgele olarak prompt yerine boş string kullanılır; bu, ağın hem conditional hem de unconditional gürültü tahmini yapmayı öğrenmesini sağlar. Inference'da iki tahmin birleştirilir:
ε_final = ε_uncond + w · (ε_cond − ε_uncond) — w: guidance scale (cfg_scale). w=1 koşullanmasız, w=7-8 dengeli, w=15+ metnin daha sıkı takibi ama daha az çeşitlilik. Tipik kullanım: w=7.5.
| cfg_scale | Metin Uyumu | Çeşitlilik | Görüntü Kalitesi | Öneri |
|---|---|---|---|---|
| 1.0 | Yok (rastgele) | Çok yüksek | Orta | — |
| 3–5 | Zayıf | Yüksek | İyi | Yaratıcı keşif |
| 7–8 | Dengeli | Orta | Çok iyi | Genel kullanım |
| 12–15 | Güçlü | Düşük | İyi (oversaturation riski) | Kesin prompt takibi |
| 20+ | Çok güçlü | Çok düşük | Düşük (artefact) | Kaçının |
import torch
from diffusers import StableDiffusionPipeline, DDIMScheduler
pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype=torch.float16
).to("cuda")
pipe.scheduler = DDIMScheduler.from_config(pipe.scheduler.config)
# ── CFG'nin dahili çalışması (konseptüel) ───────────────────
def cfg_step(unet, latents, t, cond_emb, uncond_emb, cfg_scale):
"""Tek bir CFG denoising adımı."""
# Her iki embeddingyi birleştir → tek forward pass
latents_doubled = torch.cat([latents, latents])
emb_doubled = torch.cat([uncond_emb, cond_emb])
noise_pred = unet(latents_doubled, t, encoder_hidden_states=emb_doubled).sample
noise_uncond, noise_cond = noise_pred.chunk(2)
# CFG formülü: ε = ε_uncond + w * (ε_cond - ε_uncond)
noise_guided = noise_uncond + cfg_scale * (noise_cond - noise_uncond)
return noise_guided
# ── Basit kullanım (pipeline'ı kullanarak) ───────────────────
prompt = "a majestic snow leopard on a mountain peak, photorealistic"
negative = "blurry, low quality, cartoon, deformed"
for cfg in [3.0, 7.5, 12.0]:
image = pipe(
prompt,
negative_prompt=negative,
guidance_scale=cfg,
num_inference_steps=30,
generator=torch.Generator().manual_seed(42)
).images[0]
image.save(f"cfg_{cfg}.png")
08 Stable Diffusion Pipeline
diffusers kütüphanesi, birden fazla bileşeni tek bir pipeline altında birleştirerek üretim-hazır inference sağlar.
HuggingFace diffusers kütüphanesi, Stable Diffusion'ın tüm bileşenlerini tek bir StableDiffusionPipeline nesnesi altında yönetir. Bu bileşenlerin her biri bağımsız olarak değiştirilebilir, güncellenebilir ve ince ayar yapılabilir.
Scheduler Seçimi
Scheduler, diffusion adımlarının nasıl atlanacağını belirler. DDIM deterministik, 20-50 adımda iyi sonuç. DPMSolverMultistepScheduler daha az adımda (15-25) yüksek kalite için tercih edilir. UniPC benzer kalitede. Scheduler değişikliği için sadece bir satır yeterlidir.
import torch
from diffusers import (
StableDiffusionPipeline,
DPMSolverMultistepScheduler,
DDIMScheduler,
)
# ── Pipeline yükle ───────────────────────────────────────────
model_id = "runwayml/stable-diffusion-v1-5"
pipe = StableDiffusionPipeline.from_pretrained(
model_id,
torch_dtype=torch.float16,
variant="fp16"
).to("cuda")
# ── Scheduler değiştir: DPMSolver (daha hızlı, kaliteli) ────
pipe.scheduler = DPMSolverMultistepScheduler.from_config(
pipe.scheduler.config
)
# Bellek optimizasyonu (opsiyonel)
pipe.enable_attention_slicing() # düşük VRAM için
# pipe.enable_model_cpu_offload() # 6GB VRAM için
# pipe.enable_xformers_memory_efficient_attention() # hız için
# ── Temel parametrelerle görüntü üret ───────────────────────
prompt = "cinematic photo of a futuristic city at dusk, neon lights, "
prompt += "ultra detailed, 8k, photorealistic"
negative = "ugly, blurry, low resolution, watermark, text, deformed"
generator = torch.Generator(device="cuda").manual_seed(2024)
result = pipe(
prompt=prompt,
negative_prompt=negative,
num_inference_steps=25, # DPMSolver ile 25 yeterli
guidance_scale=7.5, # CFG scale
width=512, height=512,
num_images_per_prompt=4, # tek seferde 4 görüntü
generator=generator,
)
for i, img in enumerate(result.images):
img.save(f"output_{i}.png")
print(f"Kaydedildi: output_{i}.png")
fp16 kullanımı VRAM'ı yaklaşık yarıya indirir (~3.4GB yerine ~6.5GB). Ancak bazı modellerde NaN/Inf sorunları oluşabilir; bu durumda fp32 veya bfloat16 deneyin. 4GB VRAM için enable_model_cpu_offload() ile bileşenler sırayla GPU'ya yüklenir.
09 ControlNet — Koşullu Üretim
ControlNet, mevcut görüntülerin yapısını (kenar, poz, derinlik) koruyarak difusyon sürecini hassas biçimde kontrol etmeyi sağlar.
ControlNet (Zhang et al., 2023), standart text-to-image pipeline'ına ek kontrol sinyalleri eklemenin zarif bir yolunu sunar. Temel fikir, U-Net encoder'ının eğitilebilir bir kopyasını (trainable copy) oluşturmak ve bu kopyanın çıktılarını orijinal U-Net'in decoder katmanlarına "sıfır konvolüsyon" (zero convolution) aracılığıyla eklemektir. Zero convolution, başlangıçta tamamen sıfır ağırlıklıdır; bu, eğitim başında ControlNet'in orijinal modeli hiç bozmadığı ve kademeli olarak etki kazandığı anlamına gelir.
Conditioning Tipleri
import torch
import cv2
import numpy as np
from PIL import Image
from diffusers import ControlNetModel, StableDiffusionControlNetPipeline
from diffusers import UniPCMultistepScheduler
# ── Canny edge map üret ──────────────────────────────────────
def make_canny(image_path: str, low: int = 100, high: int = 200) -> Image:
img = cv2.imread(image_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray, low, high) # ikili kenar haritası
edges_rgb = cv2.cvtColor(edges, cv2.COLOR_GRAY2RGB)
return Image.fromarray(edges_rgb)
# ── ControlNet + SD pipeline ─────────────────────────────────
controlnet = ControlNetModel.from_pretrained(
"lllyasviel/sd-controlnet-canny",
torch_dtype=torch.float16
)
pipe = StableDiffusionControlNetPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
controlnet=controlnet,
torch_dtype=torch.float16
).to("cuda")
pipe.scheduler = UniPCMultistepScheduler.from_config(pipe.scheduler.config)
pipe.enable_attention_slicing()
# ── Inference ────────────────────────────────────────────────
control_image = make_canny("reference.jpg")
result = pipe(
prompt="a beautiful oil painting of a medieval castle, dramatic lighting",
negative_prompt="blurry, modern, low quality",
image=control_image,
num_inference_steps=20,
guidance_scale=7.5,
controlnet_conditioning_scale=1.0, # ControlNet etki gücü [0-2]
generator=torch.Generator().manual_seed(99)
)
control_image.save("canny_map.png") # Canny kenar haritası
result.images[0].save("controlled_output.png")
# Çıktı: referans görüntünün yapısını koruyarak yeni stilde üretim
controlnet_conditioning_scale parametresi, ControlNet'in etki gücünü belirler. 0.5: hafif rehberlik, yapı korunur ama daha özgür. 1.0: standart, kompozisyon sıkı korunur. 1.5+: yapıya aşırı bağlı, yaratıcılık azalır. Çoğu kullanım için 0.8–1.0 aralığı optimaldır.
10 LoRA ile Stil Adaptasyonu
LoRA, büyük diffusion modellerini çok az veri ve parametre ile belirli bir stil veya konuya uyarlamanın en verimli yoludur.
LoRA (Low-Rank Adaptation), dil modellerinde geliştirilen ve diffusion modellerine başarıyla uyarlanan bir fine-tuning tekniğidir. Temel fikir şudur: büyük ağırlık matrislerini doğrudan güncellemeye çalışmak yerine, her matrisin güncellemesini iki küçük düşük ranklı matrisin çarpımı olarak modelle. W_new = W_frozen + α · (BA), burada A ∈ ℝ^(r×d) ve B ∈ ℝ^(d×r), r << d.
Diffusion LoRA'sı genellikle U-Net'in attention katmanlarına (Q, K, V, Out projection matrisleri) uygulanır. Tüm modeli eğitmek yerine sadece bu küçük ek matrisler (toplam ~3–10M parametre) güncellenir. Bu sayede 10–20 görüntü ve birkaç saat GPU süresiyle yeni bir stil öğretilebilir; ve farklı LoRA'lar birleştirilebilir.
Dreambooth ile Karşılaştırma
Dreambooth tüm modeli fine-tune eder ve bir konsepti (örn. belirli bir nesne veya kişi) öğrenmek için 3–5 görüntü kullanır, ancak saatlerce eğitim ve tüm model ağırlıklarının kaydedilmesi gerekir (~4GB). LoRA ise sadece adapter ağırlıklarını kaydeder (~10–100MB), çok daha hızlıdır ve birden fazla LoRA aynı anda uygulanabilir.
| Yöntem | Gerekli Görüntü | Eğitim Süresi | Model Boyutu | Esneklik |
|---|---|---|---|---|
| Full Fine-tune | 1000+ | Günler | Tam model (~4GB) | Düşük (tek stil) |
| Dreambooth | 3–5 | 1–4 saat | Tam model (~4GB) | Orta |
| LoRA | 10–20 | 30–90 dk | ~10–150MB | Yüksek (birleştirilebilir) |
| Textual Inversion | 5–10 | 1–2 saat | ~5KB (embedding) | Sınırlı |
import torch
from diffusers import StableDiffusionPipeline
# ── Base model + LoRA yükle ──────────────────────────────────
pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype=torch.float16
).to("cuda")
# HuggingFace Hub'dan LoRA yükle
pipe.load_lora_weights(
"CiroN2022/toy-face", # HF repo veya yerel yol
weight_name="toy_face_v2.safetensors"
)
# LoRA etki gücünü ayarla [0.0 - 1.0]
pipe.set_adapters(["default"], adapter_weights=[0.8])
image = pipe(
"portrait of a toyface astronaut, soft lighting",
num_inference_steps=30,
guidance_scale=7.5,
generator=torch.Generator().manual_seed(7)
).images[0]
image.save("lora_output.png")
# ── Birden fazla LoRA birleştirme (style mixing) ─────────────
pipe.load_lora_weights("lora_style_A.safetensors", adapter_name="styleA")
pipe.load_lora_weights("lora_style_B.safetensors", adapter_name="styleB")
# İki LoRA'yı farklı ağırlıklarla karıştır
pipe.set_adapters(
["styleA", "styleB"],
adapter_weights=[0.6, 0.4] # %60 A, %40 B
)
mixed = pipe(
"a warrior in an enchanted forest, dramatic composition",
num_inference_steps=35,
guidance_scale=8.0,
).images[0]
mixed.save("style_mix.png")
# LoRA'yı kaldır (base model'e geri dön)
pipe.unload_lora_weights()
kohya_ss GUI ve HuggingFace PEFT kütüphanesi, LoRA eğitimi için en yaygın araçlardır. Eğitim için önerilen parametreler: rank r=4–32 (daha yüksek rank → daha fazla kapasite ama daha büyük dosya), alpha=r (rank'a eşit), lr=1e-4, 1000–2000 adım. Görüntüler 512×512 veya 768×768 olarak hazırlanmalıdır.