Salta al contenuto principale

71 - Variational AutoEncoder (1): dalla verosimiglianza all'ELBO, derivato per intero

La teoria del VAE senza salti: due derivazioni dell'ELBO, la KL fra gaussiane e il trucco di riparametrizzazione, ogni passaggio verificato con un numero.

Cosa ne pensi di questo articolo?

71 - Variational AutoEncoder (1): dalla verosimiglianza all'ELBO, derivato per intero
Machine Learning Engineerfedericocalo.dev

In Breve

La teoria del VAE senza salti: due derivazioni dell'ELBO, la KL fra gaussiane e il trucco di riparametrizzazione, ogni passaggio verificato con un numero.

Alla fine della lezione sugli autoencoder avevamo un problema preciso: il decoder sa trasformare codici in immagini, ma non sappiamo da quale distribuzione pescare i codici. Il Variational AutoEncoder (VAE), proposto da Kingma e Welling nel 2013, risolve il problema cambiando prospettiva: smette di essere una macchina che comprime e diventa un modello probabilistico delle immagini, con una variabile latente z di cui si fissa la distribuzione a priori (una normale standard) e di cui si impara come ogni immagine ne dipenda. Questa lezione è la teoria, e la faccio per intero: nessuna formula viene dichiarata senza essere ricavata, con l'unica eccezione della disuguaglianza di Jensen (di cui do una definizione e un esempio numerico). Ogni passaggio numerico è verificato con un programma, che ho eseguito due volte con uscite identiche. Nella lezione successiva costruiremo il VAE in PyTorch; se capisci questa pagina, il codice sarà pochissimo.

Le parole di questa lezione

Questa è la lezione con più vocabolario di tutto il modulo, quindi le definizioni vanno lette una per volta. Un modello generativo descrive come i dati vengono «prodotti»: qui prima si estrae un codice z da una distribuzione semplice, poi da z si genera l'immagine x. La variabile latente z non è nei dati ma postuliamo che ci sia e che li «spieghi», come le dieci parole della descrizione del volto. Il prior p(z) è la distribuzione che scegliamo noi per z prima di vedere qualunque dato: la normale standard N(0, I), media zero, varianza uno, componenti indipendenti.

La verosimiglianza p(x | z) è la probabilità di un'immagine dato il codice, ed è ciò che il decoder calcola. L'evidenza p(x) = ∫ p(x | z) p(z) dz è la probabilità dell'immagine in assoluto, sommata su tutti i codici possibili, ed è la quantità che vorremmo massimizzare sui dati veri. Il posterior p(z | x) è la distribuzione dei codici che avrebbero potuto produrre x; per la regola di Bayes vale p(x | z) p(z) / p(x), ed è l'«inverso» del decoder.

La divergenza KL KL(q ‖ p) misura quanto una distribuzione q differisca da p: è E_q[log q(z) − log p(z)], la media secondo q della differenza dei logaritmi; è sempre ≥ 0, vale 0 solo se q = p e non è simmetrica. La disuguaglianza di Jensen dice che per una funzione concava come il logaritmo log(E[Y]) ≥ E[log Y]: se Y vale 1 o 9 con probabilità ½, log(5) = 1.609 mentre ½·log(1) + ½·log(9) = 1.099. L'inferenza variazionale approssima una distribuzione esatta intrattabile (qui il posterior) con una più semplice q, scelta in una famiglia e ottimizzata. L'ELBO (Evidence Lower BOund) è un limite inferiore, calcolabile, di log p(x): massimizzarlo spinge in alto la verosimiglianza. L'inferenza ammortizzata, infine, usa una rete (l'encoder) che data x produce subito i parametri di q, invece di ottimizzare una q separata per ogni immagine.

L'analogia: un pasticciere e il suo apprendista

Un pasticciere (il modello) prepara torte scegliendo un «bigliettino di ricetta» z (poco dolce, molto alta, con frutta…), pescato da un barattolo che contiene le ricette che usa di solito: il prior. Dato il bigliettino, la torta x che esce non è mai identica: c'è un po' di variabilità (la verosimiglianza p(x|z)). Ora ti mostrano una torta e ti chiedono: «quale bigliettino l'ha prodotta?». È il problema inverso, il posterior, e per un pasticciere con mille ingredienti non si può calcolare esattamente: dovresti provare tutte le ricette. Allora si assume un apprendista (l'encoder) che, guardando la torta, risponde: «secondo me il bigliettino è intorno a questo valore, con questa incertezza»: un valore centrale μ e una larghezza σ. Poi c'è una regola ulteriore: all'apprendista si chiede di non proporre mai bigliettini che il pasticciere non userebbe mai (lontani dal barattolo). Quella regola è il termine KL. Il risultato: il pasticciere può ora essere usato da solo, pescando un bigliettino dal barattolo, e le torte saranno sensate.

Il problema: massimizzare p(x) è intrattabile

Vogliamo che il modello assegni alta probabilità alle immagini vere, cioè massimizzare log p(x). Per definizione:

p(x) = ∫ p(x | z) p(z) dz

Il decoder p(x | z) è una rete neurale. L'integrale, su tutti i possibili z in uno spazio di decine di dimensioni, di una funzione definita da una rete non ha forma chiusa e un calcolo numerico diretto (campionare tanti z dal prior e mediare) ha una varianza enorme: quasi tutti i z casuali non producono nulla di simile a x, e servirebbero un numero astronomico di campioni. Quindi log p(x) non si può né calcolare né derivare in modo diretto.

L'idea: una famiglia q e un limite inferiore

Introduciamo una distribuzione q(z | x), che chiamiamo approssimazione variazionale del posterior, e che è proprio ciò che l'encoder produce. Nel VAE si sceglie una gaussiana con matrice di covarianza diagonale: q(z | x) = N(μ(x), diag(σ²(x))), dove μ(x) e σ²(x) sono due vettori calcolati dall'encoder. Ora ricaviamo un limite inferiore di log p(x), in due modi.

Derivazione 1: con la disuguaglianza di Jensen

Partiamo da log p(x) e moltiplichiamo e dividiamo per q dentro l'integrale (non cambia nulla):

log p(x) = log ∫ p(x, z) dz
         = log ∫ q(z|x) · [ p(x, z) / q(z|x) ] dz
         = log E_{z~q} [ p(x, z) / q(z|x) ]

L'ultimo passaggio riconosce l'integrale come un valore atteso rispetto a q. Ora applichiamo Jensen (log E[Y] ≥ E[log Y]) con Y = p(x, z) / q(z|x):

log p(x) ≥ E_{z~q} [ log p(x, z) − log q(z|x) ]  =: ELBO

Poiché p(x, z) = p(x | z) p(z), si può spezzare in due pezzi:

ELBO = E_q[ log p(x|z) ]  +  E_q[ log p(z) − log q(z|x) ]
     = E_q[ log p(x|z) ]  −  KL( q(z|x) ‖ p(z) )

L'ultima uguaglianza è la definizione di KL (con segno cambiato, perché E_q[log p − log q] = −KL(q‖p)).

Derivazione 2: con la scomposizione esatta

La seconda derivazione è più istruttiva perché dice quanto vale il divario fra ELBO e log p(x). Si parte da log p(x), che non dipende da z, quindi è uguale alla sua media rispetto a q. Poi si usa la regola di Bayes p(x) = p(x, z) / p(z | x):

log p(x) = E_q[ log p(x) ]
         = E_q[ log ( p(x, z) / p(z|x) ) ]
         = E_q[ log ( p(x, z) / q(z|x) ) ]  +  E_q[ log ( q(z|x) / p(z|x) ) ]
         =           ELBO                  +  KL( q(z|x) ‖ p(z|x) )

Nel terzo passaggio abbiamo moltiplicato e diviso per q(z|x) dentro il logaritmo, spezzando un logaritmo di un rapporto in due. Il secondo pezzo è esattamente la definizione di KL fra q e il vero posterior. Poiché ogni KL è ≥ 0:

log p(x) = ELBO + KL( q(z|x) ‖ p(z|x) )   ≥   ELBO

Tre conseguenze importanti. (1) L'ELBO è davvero un limite inferiore, e per due strade diverse arriviamo allo stesso risultato. (2) Il divario è esattamente la KL fra q e il vero posterior: più q approssima bene p(z|x), più l'ELBO è vicino a log p(x); se q = p(z|x) il divario è zero. (3) Massimizzare l'ELBO rispetto ai parametri dell'encoder riduce quel divario, mentre massimizzarlo rispetto al decoder alza log p(x). Con un solo obiettivo si fanno insieme le due cose.

I due termini dell'ELBO e il loro significato

ELBO(x) =   E_{z~q(z|x)} [ log p(x | z) ]     −   KL( q(z|x) ‖ p(z) )
              termine di ricostruzione            termine di regolarizzazione

Il termine di ricostruzione misura, per ogni z tirato dall'encoder, quanto il decoder trova probabile l'immagine vera. Se il decoder è una gaussiana con varianza fissa s², log p(x | z) = −‖x − x̂‖² / (2s²) + costante: è l'errore quadratico di sempre; se i pixel sono considerati binari (Bernoulli) è l'opposto dell'entropia incrociata binaria. Il termine di regolarizzazione, la KL al prior, misura quanto la distribuzione dei codici di x si discosta dalla normale standard: più l'encoder è «specifico» su x (media lontana da 0, deviazione piccola), più la KL è alta, ed è il costo, in nat di informazione, del codice.

Questo equilibrio è l'anima del VAE. Un autoencoder normale ha solo il primo termine, e perciò usa lo spazio latente come gli pare. Qui il secondo termine costringe i codici a restare in una zona vicina alla normale standard, e questo rende lo spazio latente campionabile.

La KL fra una gaussiana e la normale standard, ricavata a mano

Nel VAE servono KL( N(μ, σ²) ‖ N(0, 1) ) componente per componente. Si ricava in poche righe, e conviene farlo: è la formula che userai nel codice. Per una componente, con q = N(μ, σ²) e p = N(0, 1):

log q(z) = −½ log(2π) − ½ log σ² − (z − μ)² / (2σ²)
log p(z) = −½ log(2π) − z² / 2

KL = E_q[ log q(z) − log p(z) ]
   = E_q[ −½ log σ² − (z − μ)² / (2σ²) + z² / 2 ]

Servono tre medie secondo q. Per definizione di varianza, E_q[(z − μ)²] = σ², quindi E_q[(z − μ)²/(2σ²)] = ½. Per un'altra proprietà di base, E_q[z²] = μ² + σ² (media al quadrato più varianza). Sostituendo:

KL = −½ log σ² − ½ + ½ (μ² + σ²)
   = ½ ( μ² + σ² − log σ² − 1 )

Per un vettore latente con k componenti indipendenti, le KL si sommano. Verifichiamo la formula confrontandola con una stima a campione (Monte Carlo): estraiamo un milione di z da q, calcoliamo log q(z) − log p(z) e ne facciamo la media.

import numpy as np

rng = np.random.default_rng(0)

# 1) KL( N(mu, sigma^2) || N(0, 1) ): closed form versus a Monte Carlo estimate
def kl_closed(mu, log_var):
    return 0.5 * (mu ** 2 + np.exp(log_var) - log_var - 1.0)

def kl_monte_carlo(mu, log_var, n=1_000_000):
    sigma = np.exp(0.5 * log_var)
    z = mu + sigma * rng.standard_normal(n)                              # samples from q
    log_q = -0.5 * np.log(2 * np.pi) - 0.5 * log_var - (z - mu) ** 2 / (2 * sigma ** 2)
    log_p = -0.5 * np.log(2 * np.pi) - 0.5 * z ** 2
    return np.mean(log_q - log_p)                                        # E_q[ log q - log p ]

for mu, lv in [(0.0, 0.0), (1.0, 0.0), (0.5, -1.0), (2.0, -3.0)]:
    print(f"mu={mu:4.1f} log_var={lv:5.1f}   closed {kl_closed(mu, lv):.4f}   Monte Carlo {kl_monte_carlo(mu, lv):.4f}")
mu= 0.0 log_var=  0.0   closed 0.0000   Monte Carlo 0.0000
mu= 1.0 log_var=  0.0   closed 0.5000   Monte Carlo 0.5008
mu= 0.5 log_var= -1.0   closed 0.3089   Monte Carlo 0.3087
mu= 2.0 log_var= -3.0   closed 3.0249   Monte Carlo 3.0243

Spiegazione. kl_closed è la formula ricavata (con log σ² come parametro, exp(log_var) è σ²). kl_monte_carlo estrae n campioni z = μ + σ·ε, calcola le due densità logaritmiche scritte esplicitamente e ne media la differenza. I due metodi coincidono fino alla terza o quarta cifra (la piccola differenza è rumore di campionamento). Per leggere i casi: μ = 0, σ = 1 è proprio il prior, KL = 0; spostare la media a 1 costa 0.5 nat (½·1²); ridurre la varianza a e⁻³ ≈ 0.05 con media 2 costa 3.02 nat.

Cosa significa il costo. La KL misura quanta informazione il codice porta su x. Un encoder che vuole essere molto preciso (σ piccola) e usare un codice lontano dall'origine paga caro: con μ = 3 e σ = 0.01 la KL vale 8.61 nat; con μ = 3 e σ = 1 solo 4.5; con μ = 0 e σ = 0.1 vale 1.81. L'encoder deve convincere il decoder che quella precisione (quei nat) vale il miglioramento della ricostruzione.

Il trucco di riparametrizzazione

Per addestrare con la discesa del gradiente dobbiamo derivare l'ELBO rispetto ai parametri dell'encoder (μ, σ). Ma il termine di ricostruzione è un valore atteso rispetto a z ~ q(z|x), cioè rispetto a un campionamento casuale la cui distribuzione dipende da μ e σ. Come si deriva un'estrazione casuale? Ci sono due strade.

Stimatore a score-function (REINFORCE). Si deriva la densità: ∇_μ E_q[f(z)] = E_q[ f(z) · ∇_μ log q(z) ]. Funziona con qualunque f, anche non derivabile, ma la stima ha una varianza grande.

Riparametrizzazione. Si scrive z = μ + σ ⊙ ε con ε ~ N(0, I). Ora il caso è tutto in ε, che non dipende da nessun parametro, mentre z è una funzione deterministica e derivabile di μ, σ e ε. Si può quindi derivare dentro il valore atteso: ∇_μ E[f(μ + σ ε)] = E[f'(z)]. Confrontiamo i due stimatori su un caso dove conosciamo la risposta esatta: f(z) = z² con z ~ N(1.5, 0.7²). Qui E[z²] = μ² + σ², quindi la derivata rispetto a μ vale esattamente 2μ = 3.

# 2) Why the reparametrisation trick: two ways to estimate  d/d(mu) E_{z~N(mu,s^2)} [ f(z) ]   with f(z) = z^2
mu, sigma, n = 1.5, 0.7, 200_000
true_grad = 2 * mu                                      # E[z^2] = mu^2 + sigma^2  ->  derivative 2*mu

eps = rng.standard_normal(n)
z = mu + sigma * eps

# (a) pathwise / reparametrised: z = mu + sigma*eps, so d f(z)/d mu = f'(z) * 1 = 2 z
g_reparam = 2 * z

# (b) score-function (REINFORCE): d/dmu E[f] = E[ f(z) * d log q(z)/d mu ] = E[ f(z) * (z - mu) / sigma^2 ]
g_score = (z ** 2) * (z - mu) / sigma ** 2

print(f"true gradient            : {true_grad:.3f}")
print(f"reparametrised estimator : mean {g_reparam.mean():.3f}   std of one sample {g_reparam.std():.3f}")
print(f"score-function estimator : mean {g_score.mean():.3f}   std of one sample {g_score.std():.3f}")
print(f"variance ratio score/reparam: {g_score.var() / g_reparam.var():.1f}x")
true gradient            : 3.000
reparametrised estimator : mean 2.997   std of one sample 1.398
score-function estimator : mean 2.982   std of one sample 6.979
variance ratio score/reparam: 24.9x

Spiegazione. Si estraggono 200.000 campioni di ε. Lo stimatore riparametrizzato è semplicemente f'(z) = 2z calcolato su ogni campione; quello a score-function è f(z) · (z − μ)/σ². Entrambi sono corretti in media (2.997 e 2.982 contro 3.000: la differenza è rumore). Ma la deviazione standard di un singolo campione è 1.40 per il riparametrizzato e 6.98 per l'altro: la varianza è 24.9 volte più grande. In pratica, con un solo campione per immagine (come si fa nei VAE), lo stimatore a score-function darebbe gradienti così rumorosi da rendere l'addestramento inutilizzabile; il riparametrizzato funziona. È la ragione per cui il trucco è «il» ingrediente del VAE. Condizione: il trucco richiede che f sia derivabile rispetto a z (qui lo è, il decoder è una rete), e che si possa scrivere z come funzione derivabile dei parametri più un rumore indipendente (vero per le gaussiane).

Un ELBO su un caso giocattolo, calcolato in tre modi

Per vedere ogni pezzo funzionare insieme, un caso con una sola variabile osservata e una latente, in cui tutto si può calcolare esattamente. Immagina che l'«immagine» sia un solo pixel x = 0.8. Il prior è z ~ N(0, 1). Il decoder è p(x | z) = N(x; z, 0.3²) (il pixel è z più un po' di rumore). L'encoder dice, per questo x, q(z | x) = N(0.5, e⁻¹), cioè μ = 0.5 e log σ² = −1.

# 3) The ELBO on a toy problem with ONE number x and a 1-D latent variable, computed three ways
x = 0.8                      # the observed "image" (a single pixel)
mu, log_var = 0.5, -1.0      # what the ENCODER says about z given x:  q(z|x) = N(mu, exp(log_var))
s = 0.3                      # the DECODER's fixed noise:  p(x|z) = N(x; z, s^2)
sigma = np.exp(0.5 * log_var)

# (i) closed form: reconstruction term  E_q[log N(x; z, s^2)]  and KL to the prior
recon_closed = -0.5 * np.log(2 * np.pi * s ** 2) - ((x - mu) ** 2 + sigma ** 2) / (2 * s ** 2)
kl = kl_closed(mu, log_var)
print(f"closed form : reconstruction {recon_closed:.4f}   KL {kl:.4f}   ELBO {recon_closed - kl:.4f}")

# (ii) Monte Carlo with the reparametrisation trick
z = mu + sigma * rng.standard_normal(1_000_000)
recon_mc = np.mean(-0.5 * np.log(2 * np.pi * s ** 2) - (x - z) ** 2 / (2 * s ** 2))
print(f"Monte Carlo : reconstruction {recon_mc:.4f}   KL {kl_monte_carlo(mu, log_var):.4f}   ELBO {recon_mc - kl_monte_carlo(mu, log_var):.4f}")

# (iii) the quantity the ELBO bounds: log p(x) = log integral p(x|z) p(z) dz  (here computable: x ~ N(0, 1 + s^2))
log_px = -0.5 * np.log(2 * np.pi * (1 + s ** 2)) - x ** 2 / (2 * (1 + s ** 2))
print(f"exact log p(x) = {log_px:.4f}   (ELBO must be <= this; the gap is KL(q(z|x) || p(z|x)))")

# the best possible q for this x gives a gap of zero: the exact posterior p(z|x) = N( x/(1+s^2), s^2/(1+s^2) )
mu_star, var_star = x / (1 + s ** 2), s ** 2 / (1 + s ** 2)
recon_star = -0.5 * np.log(2 * np.pi * s ** 2) - ((x - mu_star) ** 2 + var_star) / (2 * s ** 2)
elbo_star = recon_star - kl_closed(mu_star, np.log(var_star))
print(f"ELBO with the exact posterior: {elbo_star:.4f}  (equals log p(x): the bound is tight)")
closed form : reconstruction -2.2587   KL 0.3089   ELBO -2.5677
Monte Carlo : reconstruction -2.2541   KL 0.3088   ELBO -2.5631
exact log p(x) = -1.2556   (ELBO must be <= this; the gap is KL(q(z|x) || p(z|x)))
ELBO with the exact posterior: -1.2556  (equals log p(x): the bound is tight)

Seguiamo i tre pezzi. (i) Forma chiusa. Per un decoder gaussiano, E_q[log N(x; z, s²)] = −½ log(2π s²) − ((x − μ)² + σ²) / (2 s²) (si ricava come la KL, usando E_q[(x − z)²] = (x − μ)² + σ²). Il codice dà −2.2587; con la KL 0.3089 si ottiene ELBO = −2.5677. (ii) Monte Carlo con riparametrizzazione. Un milione di campioni z = μ + σ ε, media di log p(x | z) e della KL stimata: ELBO ≈ −2.5631, uguale alla forma chiusa entro il rumore. (iii) Il valore vero. In questo caso x è la somma di due gaussiane indipendenti, quindi p(x) = N(0, 1 + s²) e log p(x) = −1.2556 si calcola esattamente: l'ELBO (−2.57) è sotto −1.26, come deve. Il divario, 1.31 nat, è la KL fra l'q dell'encoder e il vero posterior p(z | x).

L'ultima riga mostra che il divario si può chiudere: per questo modello giocattolo il vero posterior è N(x/(1 + s²), s²/(1 + s²)) (media 0.734, varianza 0.0826). Se l'encoder producesse esattamente quella q, l'ELBO varrebbe −1.2556, identico a log p(x): il limite è stretto. Nel VAE vero l'encoder sbaglia un po' (è una rete con capacità limitata e una famiglia gaussiana diagonale), quindi l'ELBO sta sempre sotto log p(x); il suo errore è proprio il divario che l'addestramento cerca di ridurre.

Schema del VAE: l'encoder produce media e log-varianza, il codice z si ottiene come media più deviazione per un rumore epsilon indipendente, il decoder ricostruisce; la perdita è ricostruzione più KL verso la normale standard Variational autoencoder: the encoder outputs a distribution, z is sampled with the reparametrisation trick x ENCODER q(z | x) μ(x) log σ²(x) z μ σ ε ~ N(0, I) z = μ + σ ⊙ ε (ε is noise, not learned: gradients flow through μ and σ) DECODER p(x | z) x̂ loss = reconstruction + KL( q(z | x) || N(0, I) ) reconstruction: how well x̂ matches x (MSE or cross-entropy) KL: keeps every q(z | x) close to the standard normal, so that sampling z ~ N(0, I) at generation time makes sense At generation time: drop the encoder, draw z ~ N(0, I), run the decoder.
Schema del VAE: l'encoder produce media e log-varianza, il codice z si ottiene come media più deviazione per un rumore epsilon indipendente, il decoder ricostruisce; la perdita è ricostruzione più KL verso la normale standard

Il disegno riassume tutto. L'encoder non produce un codice ma due vettori, μ(x) e log σ²(x) (si usa il logaritmo della varianza perché può essere qualunque numero reale, mentre la varianza deve essere positiva: σ² = exp(log σ²) lo garantisce). Il nodo giallo applica la riparametrizzazione con un rumore ε estratto fuori dal grafo di calcolo (linea tratteggiata: nessun gradiente deve attraversarlo). Il decoder produce x̂. In basso, la perdita da minimizzare è l'opposto dell'ELBO: ricostruzione + KL. In generazione si butta via l'encoder.

La perdita da implementare

Mettendo insieme: per un batch di immagini, si minimizza (opposto dell'ELBO, mediato sul batch):

loss = E_ε [ ‖x − D(μ + σ ⊙ ε)‖² / (2 s²) ]  +  ½ Σ_j ( μ_j² + σ_j² − log σ_j² − 1 )

Il valore atteso su ε si stima con un solo campione per immagine: gli autori del VAE riportano che un campione per punto basta se il mini-batch è abbastanza grande (per esempio 100). Il rapporto fra i due termini è controllato da s²: scegliere s equivale a pesare ricostruzione e regolarizzazione (lo vedremo, come β, nella lezione 73).

Trade-off

La famiglia gaussiana diagonale è semplice e rende la KL calcolabile in forma chiusa, ma limita ciò che q può rappresentare: se il vero posterior ha più picchi o correlazioni fra componenti, il divario non si chiude mai. Un solo campione per immagine è economico e, secondo gli autori, sufficiente con mini-batch grandi; un ELBO più stretto (importance weighting) esiste ma costa di più. E il peso fra ricostruzione e regolarizzazione è una scelta: più peso alla KL dà uno spazio latente ordinato e campionabile ma ricostruzioni peggiori; meno peso dà ricostruzioni nitide ma uno spazio che torna a somigliare a quello di un autoencoder.

Trappole reali

  1. Somme e medie incoerenti. La KL si somma sulle dimensioni latenti e la ricostruzione sui pixel: se in una usi la somma e nell'altra la media, il rapporto dei due termini è sbagliato di un fattore pari al numero di pixel.
  2. Parametrizzare σ direttamente. Può diventare negativo o zero; si usa log σ² (o softplus).
  3. Campionare z senza riparametrizzare. Se z non è derivabile rispetto a μ e σ, i gradienti non arrivano all'encoder: nessun errore, ma l'encoder non impara.
  4. Confondere ELBO e verosimiglianza. Un ELBO più alto non implica automaticamente un log p(x) più alto nello stesso modo per due modelli diversi: il divario può cambiare.
  5. Usare la KL «a mano» con un'altra convenzione. Alcune implementazioni usano log σ invece di log σ², e la formula cambia di un fattore 2 nel termine del logaritmo.

Le cinque trappole sono tutte errori di bilancio: un fattore di scala, un segno o una convenzione cambiati senza accorgersene spostano silenziosamente il rapporto fra ricostruzione e KL. Il rimedio è lo stesso del caso giocattolo di questa lezione: ripetere il calcolo su un esempio dove conosci la risposta esatta.

Per verificarti

Prova a rispondere prima di leggere il conto. KL(N(2, 1) ‖ N(0, 1)) vale ½(4 + 1 − 0 − 1) = 2 nat, e KL(N(1, 0.25) ‖ N(0, 1)) vale ½(1 + 0.25 + 1.386 − 1) = 0.818 nat. Con μ = 2, σ = 0.5 ed ε = −1, il codice è z = 2 + 0.5·(−1) = 1.5. Se l'ELBO di un'immagine è −100 e log p(x) vale −90, la KL fra q e il vero posterior è 10 nat, perché log p(x) = ELBO + KL.

Se l'encoder produce sempre μ = 0 e σ = 1 la KL vale 0, il codice non porta nessuna informazione su x e il decoder non può ricostruire altro che l'immagine media: è il caso estremo che nella lezione 73 chiameremo collasso del posterior.

Lo stimatore a score-function ha varianza maggiore perché moltiplica il valore della funzione per la derivata del logaritmo della densità, che può essere grande e variabile, mentre il riparametrizzato usa la derivata della funzione, molto più regolare (24.9 volte meno varianza nel nostro esempio). I conti numerici di questo paragrafo sono fatti a mano, non con il programma.

Che cosa sai fare adesso

Sai ricavare l'ELBO per due strade, E_q[log p(x|z)] − KL(q(z|x) ‖ p(z)), e sai che il suo divario da log p(x) è esattamente KL(q(z|x) ‖ p(z|x)). Sai che la KL fra una gaussiana e la normale standard vale ½(μ² + σ² − log σ² − 1) per componente (verificata con Monte Carlo: 3.0249 contro 3.0243). Sai perché serve la riparametrizzazione: dà un gradiente corretto con una varianza 24.9 volte minore dello stimatore a score-function (misurata su un caso con risposta nota). E hai visto, su un caso giocattolo, l'ELBO −2.57 stare sotto log p(x) = −1.26 e pareggiarlo quando l'encoder produce il vero posterior. Nella lezione successiva trasformiamo tutto questo in poche righe di PyTorch e generiamo cifre nuove.

Nella serie, la lezione precedente è Autoencoder: comprimere, ricostruire e capire cos'è uno spazio latente.

Fonti

  • Kingma e Welling, «Auto-Encoding Variational Bayes» (arXiv 1312.6114) - l'ELBO, la riparametrizzazione, il VAE come encoder gaussiano diagonale e la nota che un campione per punto basta con mini-batch di circa 100; PDF letto, consultata il 2026-11-12
  • NumPy, numpy.random.Generator - il generatore con seme usato per i campionamenti Monte Carlo; pagina raggiunta, consultata il 2026-11-12
  • Misure: i tre listati di questa lezione, eseguiti due volte come un unico script con Python 3.14.4 e NumPy 2.3.5, seme fisso, uscite identiche e uguali a quelle riportate

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti