Introduzione: La Nuova Frontiera della Generazione
I Diffusion Models hanno superato le GAN come stato dell'arte nella generazione di immagini, alimentando sistemi come DALL-E, Stable Diffusion e Midjourney. L'idea alla base e sorprendentemente semplice: un processo forward aggiunge gradualmente rumore gaussiano a un'immagine fino a distruggerla completamente, poi una rete neurale impara il processo reverse, rimuovendo il rumore passo dopo passo per ricostruire l'immagine originale (o generarne una nuova).
A differenza delle GAN, i diffusion models offrono training stabile, maggiore diversità nei risultati e un framework probabilistico rigoroso. Il prezzo e un processo di generazione più lento (centinaia di step di denoising), mitigato da tecniche come DDIM e latent diffusion.
Cosa Imparerai
- Il processo forward: come il rumore distrugge progressivamente un'immagine
- Il processo reverse: come una rete neurale impara a rimuovere il rumore
- DDPM: Denoising Diffusion Probabilistic Models
- DDIM: generazione più veloce con meno step
- Text conditioning: generare immagini da descrizioni testuali con CLIP
- Stable Diffusion: diffusion nello spazio latente
- Implementazione pratica con Hugging Face Diffusers
Il Processo Forward: Aggiungere Rumore
Il processo forward e una catena di Markov che aggiunge progressivamente rumore gaussiano all'immagine in T step. Ad ogni step t, un piccolo quantitativo di rumore viene aggiunto secondo uno schedule predefinito (lineare o coseno). Dopo sufficienti step (tipicamente T=1000), l'immagine originale e completamente distrutta e diventa puro rumore gaussiano.
Una proprietà fondamentale e che possiamo saltare direttamente a qualsiasi step t senza calcolare tutti gli step intermedi, grazie alla formula chiusa:
import torch
import torch.nn as nn
import numpy as np
class DiffusionSchedule:
"""Schedule per il processo di diffusione"""
def __init__(self, num_timesteps=1000, beta_start=1e-4, beta_end=0.02):
self.num_timesteps = num_timesteps
# Schedule lineare dei beta
self.betas = torch.linspace(beta_start, beta_end, num_timesteps)
self.alphas = 1.0 - self.betas
# Alpha cumulativo: prodotto di tutti gli alpha fino a t
self.alpha_cumprod = torch.cumprod(self.alphas, dim=0)
self.sqrt_alpha_cumprod = torch.sqrt(self.alpha_cumprod)
self.sqrt_one_minus_alpha_cumprod = torch.sqrt(1.0 - self.alpha_cumprod)
def add_noise(self, x_0, t, noise=None):
"""Forward process: q(x_t | x_0) - aggiunge rumore a x_0"""
if noise is None:
noise = torch.randn_like(x_0)
sqrt_alpha = self.sqrt_alpha_cumprod[t].view(-1, 1, 1, 1)
sqrt_one_minus = self.sqrt_one_minus_alpha_cumprod[t].view(-1, 1, 1, 1)
# x_t = sqrt(alpha_cumprod_t) * x_0 + sqrt(1 - alpha_cumprod_t) * noise
return sqrt_alpha * x_0 + sqrt_one_minus * noise
# Demo: rumore progressivo
schedule = DiffusionSchedule()
image = torch.randn(1, 3, 64, 64) # Immagine originale
for t in [0, 250, 500, 750, 999]:
t_tensor = torch.tensor([t])
noisy = schedule.add_noise(image, t_tensor)
print(f"Step {t}: noise level = {schedule.sqrt_one_minus_alpha_cumprod[t]:.4f}")
Il Processo Reverse: Rimuovere il Rumore
Il processo reverse e dove avviene la magia. Una rete neurale (tipicamente una U-Net) impara a predire il rumore aggiunto ad ogni step. Partendo da puro rumore gaussiano, il modello rimuove gradualmente il rumore in T step, generando un'immagine coerente.
L'obiettivo del training e semplice: minimizzare la differenza tra il rumore reale aggiunto e il rumore predetto dalla rete. Questa loss MSE sul rumore si e rivelata estremamente efficace.
class SimpleUNet(nn.Module):
"""U-Net semplificata per noise prediction"""
def __init__(self, channels=3, time_emb_dim=256):
super().__init__()
# Time embedding: trasforma timestep in vettore
self.time_mlp = nn.Sequential(
nn.Linear(1, time_emb_dim),
nn.SiLU(),
nn.Linear(time_emb_dim, time_emb_dim)
)
# Encoder
self.enc1 = self._block(channels, 64)
self.enc2 = self._block(64, 128)
self.enc3 = self._block(128, 256)
# Bottleneck
self.bottleneck = self._block(256, 512)
# Decoder con skip connections
self.dec3 = self._block(512 + 256, 256)
self.dec2 = self._block(256 + 128, 128)
self.dec1 = self._block(128 + 64, 64)
self.final = nn.Conv2d(64, channels, 1)
self.pool = nn.MaxPool2d(2)
self.up = nn.Upsample(scale_factor=2, mode='bilinear')
def _block(self, in_ch, out_ch):
return nn.Sequential(
nn.Conv2d(in_ch, out_ch, 3, padding=1),
nn.GroupNorm(8, out_ch),
nn.SiLU(),
nn.Conv2d(out_ch, out_ch, 3, padding=1),
nn.GroupNorm(8, out_ch),
nn.SiLU()
)
def forward(self, x, t):
# t_emb = self.time_mlp(t.float().unsqueeze(-1))
e1 = self.enc1(x)
e2 = self.enc2(self.pool(e1))
e3 = self.enc3(self.pool(e2))
b = self.bottleneck(self.pool(e3))
d3 = self.dec3(torch.cat([self.up(b), e3], dim=1))
d2 = self.dec2(torch.cat([self.up(d3), e2], dim=1))
d1 = self.dec1(torch.cat([self.up(d2), e1], dim=1))
return self.final(d1)
# Training: predici il rumore
model = SimpleUNet()
schedule = DiffusionSchedule()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
def training_step(x_0):
t = torch.randint(0, 1000, (x_0.size(0),))
noise = torch.randn_like(x_0)
x_t = schedule.add_noise(x_0, t, noise)
noise_pred = model(x_t, t)
loss = nn.functional.mse_loss(noise_pred, noise)
return loss
DDPM e DDIM: Velocita vs qualità
DDPM
Il DDPM (Denoising Diffusion Probabilistic Models) usa tutti i T step nel processo di sampling, garantendo alta qualità ma richiedendo centinaia o migliaia di valutazioni della rete.
DDIM
Il DDIM (Denoising Diffusion Implicit Models) accelera il sampling usando un processo non-markoviano che permette di saltare step. Con soli 50-100 step ottiene qualità comparabile a DDPM con 1000 step, riducendo il tempo di generazione di 10-20 volte.
Stable Diffusion: Diffusion nello Spazio Latente
Stable Diffusion applica il processo di diffusione non nello spazio dei pixel ma in uno spazio latente compresso (tipicamente 64x64 invece di 512x512). Un autoencoder (VAE) comprime l'immagine nello spazio latente, la diffusione opera in questo spazio ridotto, e il decoder ricostruisce l'immagine finale. Questo riduce i requisiti computazionali di circa 50 volte, rendendo possibile la generazione su GPU consumer.
Text-to-Image: Condizionamento con CLIP
La generazione text-to-image utilizza CLIP (Contrastive Language-Image Pre-training) per codificare il prompt testuale in un embedding che guida il processo di denoising. La classifier-free guidance bilancia aderenza al prompt e diversità: valori più alti producono immagini più aderenti al testo ma meno varie.
from diffusers import StableDiffusionPipeline
import torch
# Caricare Stable Diffusion
pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype=torch.float16
)
pipe = pipe.to("cuda")
# Generare un'immagine da testo
prompt = "A serene Japanese garden with cherry blossoms, digital art"
image = pipe(
prompt,
num_inference_steps=50,
guidance_scale=7.5 # Classifier-free guidance
).images[0]
image.save("japanese_garden.png")
print(f"Immagine generata: {image.size}")
ControlNet e il Futuro
ControlNet aggiunge controllo spaziale preciso alla generazione: schizzi a mano, mappe di profondità, pose umane e bordi Canny possono guidare la generazione, mantenendo la composizione desiderata mentre il modello aggiunge dettagli e stile.
Il campo dei modelli generativi evolve rapidamente: consistency models promettono generazione in un singolo step, video diffusion genera video coerenti, e modelli multimodali combinano testo, immagini e audio in un unico framework.
Prossimi Passi nella Serie
- Nel prossimo articolo esploreremo il Reinforcement Learning
- Vedremo come agenti apprendono da ricompense: Q-Learning, DQN e PPO
- Implementeremo un agente che impara a giocare con OpenAI Gymnasium







