Faza 04 · lecția 10

Generarea imaginilor — modele de difuzie

Scopul lecției: Un model de difuzie învață să elimine zgomotul. Antrenați-l să elimine o cantitate mică de zgomot dintr-o imagine zgomotoasă, repetați procesul invers de o mie de ori și obțineți un generator de imagini.

Versiunea curentă AlexBred.com: primele 100 de lecții ale programului în limba română.

Curs
AI Engineering from Scratch
Fază
Viziune computerizată
Lectură
17 min.
Verificat
Cuprinsul lecției
  1. Obiective de învățare
  2. Problema
  3. Conceptul
  4. Procesul direct
  5. Saltul în formă închisă
  6. Procesul invers
  7. Pierderea de antrenare
  8. Eșantionatorul (DDPM)
  9. De ce 1000 de pași
  10. DDIM: eșantionare de 20× mai rapidă
  11. Condiționarea de timp
  12. Construiți
  13. Pasul 1: Schema de zgomot
  14. Pasul 2: Difuzia directă (q_sample)
  15. Pasul 3: Un U-Net mic, condiționat de timp
  16. Pasul 4: Bucla de antrenare
  17. Pasul 5: Eșantionatorul (DDPM)
  18. Pasul 6: Eșantionatorul DDIM (determinist, ~20× mai rapid)
  19. Utilizați-l
  20. Livrați-l
  21. Exerciții
  22. Termeni-cheie
  23. Lecturi suplimentare

Un model de difuzie învață să elimine zgomotul. Antrenați-l să elimine o cantitate mică de zgomot dintr-o imagine zgomotoasă, repetați procesul invers de o mie de ori și obțineți un generator de imagini.

Tip: Construcție Limbaje: Python Cerințe prealabile: Faza 4 Lecția 07 (U-Net), Faza 1 Lecția 06 (Probabilități), Faza 3 Lecția 06 (Optimizatori) Timp: ~75 de minute

Obiective de învățare

  • Derivați procesul direct de adăugare a zgomotului x_0 -> x_1 -> ... -> x_T și explicați de ce forma închisă q(x_t | x_0) este valabilă pentru orice t
  • Implementați un obiectiv de antrenare în stil DDPM care face regresie asupra zgomotului adăugat la fiecare pas și un eșantionator care parcurge drumul înapoi de la zgomot pur la o imagine
  • Construiți un U-Net condiționat de timp (suficient de mic pentru a fi antrenat pe CPU) care prezice zgomotul pentru orice pas temporal
  • Explicați diferența dintre eșantionarea DDPM și DDIM și când este potrivită fiecare (Lecția 23 tratează în profunzime flow matching și rectified flow)

Problema

GAN-urile generează dintr-un singur pas: zgomot la intrare, imagine la ieșire, o singură trecere înainte. Sunt rapide și dificil de antrenat. Modelele de difuzie generează iterativ: pornesc de la zgomot pur, elimină zgomotul în pași mici, iar imaginea apare treptat. Sunt lente și ușor de antrenat. În ultimii cinci ani, această din urmă proprietate a dominat: orice echipă mică poate antrena un model de difuzie și poate obține eșantioane rezonabile; antrenarea GAN-urilor este o meserie învățată de-a lungul anilor de rulări eșuate.

Dincolo de stabilitatea antrenării, structura iterativă a difuziei deblochează tot ce face generarea modernă de imagini: condiționare pe text, inpainting, editarea imaginilor, super-rezoluție, stil controlabil. Fiecare pas al buclei de eșantionare este un loc unde poate fi injectată o constrângere nouă. Acest punct de inserție explică de ce Stable Diffusion, Imagen, DALL-E 3, Midjourney și fiecare model de imagini controlabil pe care îl veți utiliza se bazează pe difuzie.

Notă tehnică a traducerii: Comparația dintre GAN-uri și difuzie este o orientare didactică, nu o regulă universală. Costul de antrenare și de inferență, stabilitatea, latența și calitatea depind de arhitectură, rezoluție, condiționare, date, hardware și scheduler; există modele generative controlabile care nu sunt modele de difuzie.

Această lecție construiește DDPM-ul minimal: adăugarea directă a zgomotului, eliminarea inversă a zgomotului, bucla de antrenare. Lecția următoare (Stable Diffusion) îl conectează la un sistem de producție cu un VAE, un codificator de text și classifier-free guidance.

Conceptul

Procesul direct

Luați o imagine x_0. Adăugați o cantitate mică de zgomot gaussian pentru a obține x_1. Adăugați puțin mai mult zgomot pentru a obține x_2. Continuați timp de T pași până când x_T devine aproape imposibil de distins de zgomotul gaussian pur.

q(x_t | x_{t-1}) = N(x_t; sqrt(1 - beta_t) * x_{t-1},  beta_t * I)

beta_t este o schemă de varianțe mici, de obicei liniară de la 0.0001 la 0.02 pe parcursul a T=1000 de pași. Fiecare pas micșorează puțin semnalul și injectează zgomot proaspăt.

Saltul în formă închisă

Adăugarea zgomotului pas cu pas este un lanț Markov, însă matematica se compactează: puteți eșantiona x_t direct din x_0 într-un singur pas.

Definim alpha_t = 1 - beta_t
Definim alpha_bar_t = prod_{s=1..t} alpha_s

Apoi:
  q(x_t | x_0) = N(x_t; sqrt(alpha_bar_t) * x_0,  (1 - alpha_bar_t) * I)

Echivalent:
  x_t = sqrt(alpha_bar_t) * x_0 + sqrt(1 - alpha_bar_t) * epsilon
  unde epsilon ~ N(0, I)

Această singură ecuație este motivul pentru care difuzia este practică. În timpul antrenării alegeți un t aleator, eșantionați x_t direct din x_0 și antrenați într-un singur pas — nu este necesară simularea întregului lanț Markov.

Procesul invers

Procesul direct este fix. Procesul invers p(x_{t-1} | x_t) este ceea ce învață rețeaua neuronală. Modelele de difuzie nu prezic direct x_{t-1}; ele prezic zgomotul epsilon adăugat la pasul t, iar matematica deduce x_{t-1} din acesta.

Диаграмма к уроку «Generarea imaginilor — modele de difuzie»

Pierderea de antrenare

Pentru fiecare pas de antrenare:

  1. Eșantionați o imagine reală x_0.
  2. Eșantionați uniform un pas temporal t din [1, T].
  3. Eșantionați zgomot epsilon ~ N(0, I).
  4. Calculați x_t = sqrt(alpha_bar_t) * x_0 + sqrt(1 - alpha_bar_t) * epsilon.
  5. Preziceți epsilon_theta(x_t, t) cu rețeaua.
  6. Minimizați || epsilon - epsilon_theta(x_t, t) ||^2.

Atât. Rețeaua neuronală învață să prezică zgomotul la orice pas temporal. Pierderea este MSE. Nu există joc adversarial, colaps sau oscilație.

Notă tehnică a traducerii: MSE este forma simplificată uzuală pentru parametrizarea prin predicția zgomotului, însă obiectivul DDPM provine dintr-o bornă variațională. Există și ponderări ale pierderii, predicția lui x_0 sau parametrizarea v; nu rezultă că orice model de difuzie folosește exact acest unic apel MSE.

Eșantionatorul (DDPM)

Pentru a genera: porniți de la x_T ~ N(0, I) și parcurgeți înapoi câte un pas.

pentru t = T, T-1, ..., 1:
    eps = model(x_t, t)
    x_{t-1} = (1 / sqrt(alpha_t)) * (x_t - (beta_t / sqrt(1 - alpha_bar_t)) * eps) + sqrt(beta_t) * z
    unde z ~ N(0, I) dacă t > 1, altfel 0
returnează x_0

Ideea-cheie este că, deși condiționala inversă nu este cunoscută în formă închisă în general, ea este cunoscută pentru acest proces direct gaussian specific. Coeficienții cu aspect complicat sunt cei obținuți prin regula lui Bayes.

Notă tehnică a traducerii: Condiționala posterioară exactă a procesului direct, q(x_{t-1} | x_t, x_0), are varianța tilde_beta_t = beta_t * (1 - alpha_bar_{t-1}) / (1 - alpha_bar_t). Procesul invers învățat p_theta(x_{t-1} | x_t) nu cunoaște x_0: îi estimează media din predicția epsilon, iar DDPM poate alege o varianță fixă, inclusiv beta_t ca în acest fragment, sau tilde_beta_t. Prin urmare, sqrt(beta_t) nu este universal greșit; alegerea varianței trebuie să corespundă parametrizării și schedulerului folosite.

De ce 1000 de pași

Schema de zgomot direct este aleasă astfel încât fiecare pas să adauge suficient de puțin zgomot încât pasul invers să fie aproape gaussian. Prea puțini pași și pasul invers este departe de gaussian, iar rețeaua nu îl poate modela bine. Prea mulți pași și eșantionarea devine costisitoare, cu câștiguri descrescătoare. T=1000 cu o schemă liniară este valoarea implicită DDPM.

Notă tehnică a traducerii: T=1000 și schema liniară sunt alegerea de referință a DDPM original, nu o cerință teoretică. Schemele, parametrizările și solvere moderne pot folosi alte numere de pași; compromisul trebuie măsurat pentru modelul și obiectivul concret.

DDIM: eșantionare de 20× mai rapidă

Antrenarea este aceeași. Eșantionarea se schimbă. DDIM (Song et al., 2020) definește un proces invers determinist care sare peste pași temporali fără reantrenare. Eșantionarea în 50 de pași cu DDIM oferă o calitate apropiată de DDPM cu 1000 de pași. Fiecare sistem de producție utilizează DDIM sau o variantă și mai rapidă (DPM-Solver, Euler ancestral).

Notă tehnică a traducerii: „20×” este o valoare ilustrativă; lucrarea DDIM raportează experimental intervalul 10–50× în anumite configurații. Câștigul de viteză și apropierea calității depind de model, de schema de zgomot, de discretizare și de metrică; 50 de pași nu garantează calitate comparabilă pentru orice model și nici toate sistemele de producție nu utilizează DDIM. DDIM este o familie de procese implicite ne-Markoviene; eta controlează stocasticitatea, iar afirmația că o anumită valoare „recuperează DDPM” trebuie evaluată pentru parametrizarea și planificarea concretă.

Condiționarea de timp

Rețeaua epsilon_theta(x_t, t) trebuie să știe ce pas temporal elimină zgomotul. Modelele moderne de difuzie injectează t prin reprezentări temporale sinusoidale (aceeași idee ca poziționarea în transformere), care se adaugă la hărțile de caracteristici la fiecare nivel U-Net.

t_embedding = sinusoidal(t)
feature_map += MLP(t_embedding)

Fără condiționarea de timp, rețeaua trebuie să ghicească nivelul zgomotului din imaginea însăși, ceea ce funcționează, dar este mult mai puțin eficient din punctul de vedere al eșantioanelor.

Construiți

Pasul 1: Schema de zgomot

import torch

def linear_beta_schedule(T=1000, beta_start=1e-4, beta_end=2e-2):
    return torch.linspace(beta_start, beta_end, T)


def precompute_schedule(betas):
    alphas = 1.0 - betas
    alphas_cumprod = torch.cumprod(alphas, dim=0)
    return {
        "betas": betas,
        "alphas": alphas,
        "alphas_cumprod": alphas_cumprod,
        "sqrt_alphas_cumprod": torch.sqrt(alphas_cumprod),
        "sqrt_one_minus_alphas_cumprod": torch.sqrt(1.0 - alphas_cumprod),
        "sqrt_recip_alphas": torch.sqrt(1.0 / alphas),
    }

schedule = precompute_schedule(linear_beta_schedule(T=1000))

Precalculați o singură dată, apoi selectați după indice în timpul antrenării și al eșantionării.

Pasul 2: Difuzia directă (q_sample)

def q_sample(x0, t, noise, schedule):
    sqrt_a = schedule["sqrt_alphas_cumprod"][t].view(-1, 1, 1, 1)
    sqrt_one_minus_a = schedule["sqrt_one_minus_alphas_cumprod"][t].view(-1, 1, 1, 1)
    return sqrt_a * x0 + sqrt_one_minus_a * noise

Formă închisă într-o singură linie. t este un lot de pași temporali, câte unul pentru fiecare imagine din lot.

Notă tehnică a traducerii: Dacă antrenarea se face pe un accelerator, tensorii din schedule, indicii t și imaginile trebuie să fie pe dispozitive compatibile; fragmentul creează inițial schema pe dispozitivul implicit. Într-o implementare de producție, înregistrați schema ca buffer al modulului sau mutați-o explicit împreună cu modelul.

Pasul 3: Un U-Net mic, condiționat de timp

import torch.nn as nn
import torch.nn.functional as F
import math

def timestep_embedding(t, dim=64):
    half = dim // 2
    freqs = torch.exp(-math.log(10000) * torch.arange(half, device=t.device) / half)
    args = t[:, None].float() * freqs[None]
    emb = torch.cat([args.sin(), args.cos()], dim=-1)
    return emb


class TinyUNet(nn.Module):
    def __init__(self, img_channels=3, base=32, t_dim=64):
        super().__init__()
        self.t_mlp = nn.Sequential(
            nn.Linear(t_dim, base * 4),
            nn.SiLU(),
            nn.Linear(base * 4, base * 4),
        )
        self.t_dim = t_dim
        self.enc1 = nn.Conv2d(img_channels, base, 3, padding=1)
        self.enc2 = nn.Conv2d(base, base * 2, 4, stride=2, padding=1)
        self.mid = nn.Conv2d(base * 2, base * 2, 3, padding=1)
        self.dec1 = nn.ConvTranspose2d(base * 2, base, 4, stride=2, padding=1)
        self.dec2 = nn.Conv2d(base * 2, img_channels, 3, padding=1)
        self.time_proj = nn.Linear(base * 4, base * 2)

    def forward(self, x, t):
        t_emb = timestep_embedding(t, self.t_dim)
        t_emb = self.t_mlp(t_emb)
        t_proj = self.time_proj(t_emb)[:, :, None, None]

        h1 = F.silu(self.enc1(x))
        h2 = F.silu(self.enc2(h1)) + t_proj
        h3 = F.silu(self.mid(h2))
        d1 = F.silu(self.dec1(h3))
        d2 = torch.cat([d1, h1], dim=1)
        return self.dec2(d2)

U-Net cu două niveluri, cu condiționarea de timp injectată la gâtul de sticlă. Măriți adâncimea și lățimea pentru imagini reale.

Notă tehnică a traducerii: În TinyUNet din fragment, condiționarea de timp este injectată numai după enc2, la gâtul de sticlă, nu la fiecare nivel U-Net cum afirmă explicația generală. Arhitecturile de producție pot injecta reprezentarea temporală în mai multe blocuri, dar poziția și mecanismul sunt alegeri de arhitectură.

Pasul 4: Bucla de antrenare

def train_step(model, x0, schedule, optimizer, device, T=1000):
    model.train()
    x0 = x0.to(device)
    bs = x0.size(0)
    t = torch.randint(0, T, (bs,), device=device)
    noise = torch.randn_like(x0)
    x_t = q_sample(x0, t, noise, schedule)
    pred = model(x_t, t)
    loss = F.mse_loss(pred, noise)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    return loss.item()

Aceasta este întreaga buclă de antrenare. Fără joc GAN, fără pierdere specializată, un singur apel MSE.

Pasul 5: Eșantionatorul (DDPM)

@torch.no_grad()
def sample(model, schedule, shape, T=1000, device="cpu"):
    model.eval()
    x = torch.randn(shape, device=device)
    betas = schedule["betas"].to(device)
    sqrt_one_minus_a = schedule["sqrt_one_minus_alphas_cumprod"].to(device)
    sqrt_recip_alphas = schedule["sqrt_recip_alphas"].to(device)

    for t in reversed(range(T)):
        t_batch = torch.full((shape[0],), t, dtype=torch.long, device=device)
        eps = model(x, t_batch)
        coef = betas[t] / sqrt_one_minus_a[t]
        mean = sqrt_recip_alphas[t] * (x - coef * eps)
        if t > 0:
            x = mean + torch.sqrt(betas[t]) * torch.randn_like(x)
        else:
            x = mean
    return x

1000 de treceri înainte pentru a produce un lot de eșantioane. În cod real, ați înlocui acest lucru cu un eșantionator DDIM în 50 de pași.

Pasul 6: Eșantionatorul DDIM (determinist, ~20× mai rapid)

@torch.no_grad()
def sample_ddim(model, schedule, shape, steps=50, T=1000, device="cpu", eta=0.0):
    model.eval()
    x = torch.randn(shape, device=device)
    alphas_cumprod = schedule["alphas_cumprod"].to(device)

    ts = torch.linspace(T - 1, 0, steps + 1).long()
    for i in range(steps):
        t = ts[i]
        t_prev = ts[i + 1]
        t_batch = torch.full((shape[0],), t, dtype=torch.long, device=device)
        eps = model(x, t_batch)
        a_t = alphas_cumprod[t]
        a_prev = alphas_cumprod[t_prev] if t_prev >= 0 else torch.tensor(1.0, device=device)
        x0_pred = (x - torch.sqrt(1 - a_t) * eps) / torch.sqrt(a_t)
        sigma = eta * torch.sqrt((1 - a_prev) / (1 - a_t) * (1 - a_t / a_prev))
        dir_xt = torch.sqrt(1 - a_prev - sigma ** 2) * eps
        noise = sigma * torch.randn_like(x) if eta > 0 else 0
        x = torch.sqrt(a_prev) * x0_pred + dir_xt + noise
    return x

eta=0 este complet determinist (același zgomot de intrare produce mereu aceeași ieșire). eta=1 recuperează DDPM.

Notă tehnică a traducerii: În fragment, eta=1 introduce zgomot în traiectoria DDIM, însă nu trebuie interpretat drept echivalență automată cu orice eșantionator DDPM. Mai mult, ts se termină la 0, astfel că perechile din buclă nu produc niciodată t_prev=-1, iar ramura cu a_prev=1 nu este atinsă. Cu indexare de la zero, alphas_cumprod[0] este încă mai mic decât 1; pentru a parcurge explicit pasul final până la imaginea curată, programați tranziția t=0 -> -1 cu a_prev=1 sau definiți consecvent o convenție cu un pas inițial fără zgomot, fără a dubla pasul zero. Echivalența depinde de variația aleasă, de pașii selectați și de parametrizarea modelului; comparați cu formularea DDIM și cu schedulerul concret utilizat.

Utilizați-l

Pentru lucru în producție, utilizați diffusers:

from diffusers import DDPMScheduler, UNet2DModel

unet = UNet2DModel(sample_size=32, in_channels=3, out_channels=3, layers_per_block=2)
scheduler = DDPMScheduler(num_train_timesteps=1000)

Biblioteca oferă schedulere gata pregătite (DDPM, DDIM, DPM-Solver, Euler, Heun), U-Net-uri configurabile, pipeline-uri pentru text-la-imagine și imagine-la-imagine, precum și ajutoare pentru ajustare fină LoRA.

Pentru cercetare, k-diffusion (Katherine Crowson) are implementările de referință cele mai fidele și cele mai bune variante de eșantionare.

Livrați-l

Această lecție produce:

  • outputs/prompt-diffusion-sampler-picker.md — un prompt care alege DDPM / DDIM / DPM-Solver / Euler pe baza țintei de calitate, a bugetului de latență și a tipului de condiționare.
  • outputs/skill-noise-schedule-designer.md — o abilitate care produce o schemă beta liniară, cosinusoidală sau sigmoidă pentru T și nivelul-țintă de corupere, plus grafice de diagnostic ale raportului semnal-zgomot în timp.

Exerciții

  1. (Ușor) Vizualizați procesul direct: luați o imagine și reprezentați x_t pentru t in [0, 100, 250, 500, 750, 1000]. Verificați că x_1000 arată ca zgomot gaussian pur.
  2. (Mediu) Antrenați TinyUNet pe setul de date synthetic-circles timp de 20 de epoci și eșantionați 16 cercuri. Comparați eșantionarea DDPM (1000 de pași) cu DDIM (50 de pași) — produc imagini similare cu aceeași sămânță de zgomot?
  3. (Dificil) Implementați o schemă cosinusoidală de zgomot (Nichol & Dhariwal, 2021): alpha_bar_t = cos^2((t/T + s) / (1 + s) * pi / 2). Antrenați același model cu scheme liniare și cosinusoidale și arătați că schema cosinusoidală oferă eșantioane mai bune la număr mic de pași.

Notă tehnică a traducerii: Cu linear_beta_schedule(T=1000), indicii valizi ai tensorilor programați sunt de la 0 la 999. Pentru codul din lecție, înlocuiți t=1000 din primul exercițiu cu t=999; altfel indexarea depășește lungimea schemei. Formula cosinusoidală dată pentru alpha_bar_t necesită și normalizare la valoarea inițială, derivarea raporturilor succesive pentru beta_t și, de regulă, limitarea valorilor; rezultatul „mai bun” la puțini pași este o ipoteză experimentală, nu o garanție.

Termeni-cheie

Termen Ce se spune de obicei Ce înseamnă de fapt
Proces direct „Adăugați zgomot în timp” Lanț Markov fix care corupe o imagine în zgomot gaussian în T pași
Proces invers „Eliminați zgomotul pas cu pas” Distribuție învățată care parcurge drumul înapoi de la zgomot la imagine
Predicția epsilon „Preziceți zgomotul” Ținta de antrenare: epsilon_theta(x_t, t) prezice zgomotul adăugat la pasul t
Schema beta „Cantitățile de zgomot” Secvență de T varianțe mici care definesc cât zgomot intră la fiecare pas
alpha_bar_t „Factor cumulativ de păstrare” Produsul lui (1 - beta_s) până la timpul t; t mai mare înseamnă mai puțin semnal rămas
Eșantionator DDPM „Ancestral, stocastic” Eșantionează fiecare x_{t-1} din condiționala sa gaussiană; 1000 de pași
Eșantionator DDIM „Determinist, rapid” Rescrie eșantionarea ca ODE deterministă; 20–100 de pași cu o calitate similară
Condiționarea de timp „Spuneți modelului ce t este” Reprezentare sinusoidală a lui t injectată în U-Net pentru ca acesta să cunoască nivelul zgomotului

Lecturi suplimentare

Notă tehnică a traducerii: Afirmațiile din descrierile lecturilor despre „depășirea GAN-urilor la FID”, „inferență în timp real” și „cea mai bună referință actuală” sunt dependente de moment, metrică, set de date și protocol. Ele trebuie citite ca descrieri istorice sau editoriale ale lucrărilor, nu ca rezultate universale și permanente.

Sursă: Originalul în limba engleză

Navigare: ← Lecția 04.09 — Generarea imaginilor — GAN-uri · Faza 4 — Viziune computerizată · Lecția 04.11 — Stable Diffusion — arhitectură și ajustare fină → · Catalog complet