Autoencoder variazionali (VAE): immagini nuove da uno spazio latente continuo
Modello generativo con variabile latente, un VAE scritto da zero con il trucco di riparametrizzazione, uno spazio latente ordinato, generazione di cifre dal rumore a confronto con un autoencoder ordinario, l’effetto di β e il collasso, l’interpolazione e dove si usano i VAE.
Serie Basi di IA Parte 44 di 52
- Mappa dell’intelligenza artificiale: che cos’è ciascuna cosa e come si incastra tutto
- Breve storia dell’intelligenza artificiale: da Turing ai modelli linguistici
- Dati, modello, addestramento, inferenza: i concetti di base su un unico esempio
- Come leggere la notazione matematica: simboli, indici, sommatorie e quantificatori
- Insiemi e relazioni: i mattoncini con cui si costruiscono le definizioni nell’IA
- Funzioni e applicazioni: perché ogni modello di IA è una funzione
- Logica e dimostrazioni: le regole con cui si ragiona nell’IA
- Grafi e alberi: la struttura su cui poggiano la ricerca e le reti neurali
- Algebra lineare per l’apprendimento automatico: vettori, matrici e perché l’IA soprattutto moltiplica
- Derivate, gradiente e regola della catena: come un modello capisce in quale direzione imparare
- Discesa del gradiente: come impara un modello
- Che cos’è la probabilità: dal lancio di un dado agli assiomi di Kolmogorov
- Probabilità condizionata e teorema di Bayes: come cambiare una stima quando scopri qualcosa di nuovo
- Variabile aleatoria e funzione di ripartizione: una sola funzione che dice tutto sulla distribuzione
- Funzione di densità e funzione di massa: perché una variabile continua ha probabilità zero in un punto
- Valore atteso, varianza e covarianza: dove si trova una distribuzione, quanto è larga e che cosa è legato a che cosa
- Panoramica delle distribuzioni di probabilità: Bernoulli, binomiale, Poisson, uniforme, normale ed esponenziale
- Legge dei grandi numeri e teorema del limite centrale: perché le medie si comportano in modo così prevedibile
- Statistica e stima dei parametri: massima verosimiglianza, MAP e intervalli di confidenza
- Teoria dell’informazione: entropia, entropia incrociata e divergenza KL — da dove vengono le funzioni di perdita
- Ricerca nello spazio degli stati: BFS, DFS, UCS e A* con animazioni
- Giochi e avversari: minimax, potatura alfa-beta e Monte Carlo tree search
- Soddisfacimento di vincoli: backtracking, propagazione dei vincoli e AC-3 su un sudoku e una mappa delle regioni ceche
- Logica e sistemi basati sulla conoscenza: concatenazione in avanti e all’indietro, risoluzione e sistemi esperti
- Pianificazione: STRIPS, il mondo dei blocchi, euristiche dal rilassamento e pianificazione negli agenti IA
- Ricerca locale e ricottura simulata: un commesso viaggiatore tra i capoluoghi di regione cechi
- Algoritmi genetici: popolazione, incrocio, mutazione e selezione su uno zaino per l’escursione
- Strategie evolutive e programmazione genetica: dalla regola di un quinto alla legge di Keplero
- Ottimizzazione con colonie di formiche: come una traccia di feromone trova un percorso breve
- Ottimizzazione a sciame di particelle: inerzia, esperienza propria ed esperienza dello sciame
- Neuroevoluzione: evoluzione dei pesi e della forma delle reti neurali e l’algoritmo NEAT
- Insiemi fuzzy e funzioni di appartenenza: come scrivere «un po’ caldo»
- Logica fuzzy e probabilità: perché «un po’ caldo» non significa «probabilmente caldo»
- Logica fuzzy: t-norme, implicazioni e regole SE–ALLORA
- Controllo fuzzy: Mamdani e Sugeno, dal ventilatore al pendolo inverso su un carrello
- Clustering fuzzy: l’algoritmo fuzzy c-means e i punti tra i cluster
- Sistemi neuro-fuzzy (ANFIS): regole fuzzy che imparano dai dati
- Catene di Markov: matrice di transizione, distribuzione stazionaria e PageRank
- Modelli di Markov nascosti: l’algoritmo di Viterbi e il casinò disonesto
- Reti bayesiane: perché l’erba è bagnata e che cosa significa per l’irrigatore
- Filtro di Kalman: come stimare da misure imprecise dove si trova un’auto e a che velocità va
- Tipi di apprendimento automatico: supervisionato, non supervisionato, semi-supervisionato e altri
- Prompt, contesto e RAG: come dare a un modello le informazioni giuste
- Autoencoder variazionali (VAE): immagini nuove da uno spazio latente continuo
- Reti generative avversarie (GAN): il falsario contro il detective
- Modelli di diffusione: come il rumore puro diventa un’immagine
- Processi decisionali di Markov: stati, azioni, ricompense ed equazioni di Bellman
- Q-learning e DQN: imparare il valore delle azioni, dalla tabella alla rete neurale
- Policy gradient e PPO: imparare direttamente la politica
- Come funziona un agente IA: un modello linguistico in un ciclo con strumenti
- Programmare con agenti IA: incarico, test e controllo
- Valutazione e sicurezza dei sistemi di IA: come misurare e come sorvegliare
Programma completo della serie 52 di 83 argomenti pubblicati
- Perché da un autoencoder ordinario non si riesce a generare bene.
- Come funziona il VAE: una distribuzione invece di un punto, la penalità KL e il trucco di riparametrizzazione.
- Quanto costa β, perché le immagini del VAE sono sfocate e dove si usano oggi i VAE.
Utile sapere prima: Teoria dell’informazione: entropia, entropia incrociata e divergenza KL — da dove vengono le funzioni di perdita
L’autoencoder della parte sulle rappresentazioni sa comprimere un’immagine in pochi numeri e ricomporla a partire da essi. Viene spontanea una domanda: se il decoder sa trasformare un codice in una cifra, non potremmo dargli un codice che ci siamo inventati e ottenere una cifra nuova che nessuno ha disegnato? Con un autoencoder ordinario questo quasi non funziona. L’autoencoder variazionale è una sua modifica che rende possibile proprio questo, ed è il primo dei modelli generativi di cui ci occuperemo in questa parte della serie.
Riprende le parti Autoencoder e rappresentazioni e Teoria dell’informazione (divergenza KL).
1. Dalla compressione alla generazione
Definizione — modello generativo
Un modello generativo impara dai dati x1, …, xn una distribuzione p(x) da cui si possa campionare, cioè produrre nuovi x che sembrano dati ma non ne sono copie. I modelli con variabile latente lo fanno in due passi: estraggono un codice z da una distribuzione semplice p(z) (per esempio quella normale) e il decoder calcola da esso x.
Un classificatore risponde a «che cos’è». Un modello generativo sa rispondere a «mostrami qualcosa che potrebbe stare nei dati». La ricetta: lancia i dadi per ottenere qualche numero, passalo a un pittore (il decoder) e lui ne ricaverà un’immagine.
p(x) = ∫ p(x | z) · p(z) dz
- p(z)
- distribuzione del codice (prior): la scegliamo noi, e semplice, qui N(0, I), perché poi in fase di generazione sappiamo estrarre da essa
- p(x | z)
- decoder: quanto è probabile l’immagine x se riceviamo il codice z; qui una rete che per ogni pixel restituisce la probabilità che sia acceso
- ∫ … dz
- somma su tutti i codici possibili: l’immagine può essere nata da qualsiasi codice; l’integrale su tutto lo spazio latente però non sappiamo calcolarlo, e per questo il VAE lo sostituisce con una stima (più avanti)
Il problema dell’autoencoder ordinario: non sappiamo da dove estrarre il codice. L’encoder colloca le cifre nello spazio latente dove gli pare. Nella nostra rete con codice bidimensionale, i codici delle cifre di test stanno attorno al punto (-3,9; -0,9) con deviazione standard 4,3 e 6,0, e tra i gruppi ci sono buchi in cui il decoder non ha mai visto nulla.
2. Autoencoder variazionale
Definizione — autoencoder variazionale (VAE)
Il VAE (Diederik Kingma e Max Welling, 2013) ha un encoder che per l’immagine x non restituisce un singolo punto, ma una distribuzione normale q(z | x) = N(μ(x), diag σ²(x)). Impara minimizzando la perdita
L = −Ez ~ q[log p(x | z)] + β · KL(q(z | x) ‖ N(0, I)),
dove il primo termine è l’errore di ricostruzione (qui l’entropia incrociata binaria dei pixel) e il secondo una penalità per quanto la distribuzione del codice si discosta dalla normale standard. Per la distribuzione normale, KL = ½ ∑j (μj² + σj² − log σj² − 1). Con β = 1, −L è un limite inferiore della log-verosimiglianza dei dati (ELBO).
L’encoder non può dire «questo sette abita esattamente qui», ma solo «da qualche parte in questa nuvoletta». Il decoder poi deve saper disegnare un sette da qualsiasi punto della nuvoletta. E la penalità KL spinge tutte le nuvolette verso l’origine e non permette loro di essere troppo piccole: così si sovrappongono, riempiono lo spazio e tra di loro non restano buchi.
L(x) = −Ez ~ q(z | x)[ log p(x | z) ] + β · KL( q(z | x) ‖ N(0, I) )
- −Ez ~ q[log p(x | z)]
- errore di ricostruzione: quanto male il decoder disegna x a partire da un codice estratto dalla nuvoletta. Il valore atteso su tutta la nuvoletta (non solo sul suo centro) costringe il decoder a disegnare bene in tutto l’intorno del codice; il meno c’è perché il logaritmo della probabilità lo vogliamo il più grande possibile e la perdita la più piccola possibile
- β
- peso della penalità: regola il compromesso tra precisione e ordine nello spazio; β = 1 è esattamente l’ELBO, altri valori li proviamo nella sezione su β
- KL(q(z | x) ‖ N(0, I))
- distanza della nuvoletta dalla distribuzione da cui estrarremo: senza di essa l’encoder ridurrebbe le nuvolette a punti e li sparpaglierebbe lontano l’uno dall’altro (la ricostruzione sarebbe migliore) e torneremmo a un autoencoder ordinario
KL = ½ ∑j=1k ( μj² + σj² − log σj² − 1 )
- KL
- la penalità della formula precedente, calcolata esattamente per la distribuzione normale; le coordinate del codice sono indipendenti, perciò è una somma su j
- μj²
- spostamento del centro della nuvoletta dall’origine: cresce quadraticamente, quindi i codici lontani costano molto e l’encoder li riporta verso lo zero, dove estrarremo
- σj² − log σj²
- larghezza della nuvoletta: la funzione s − log s ha minimo 1 proprio in s = 1; penalizza una nuvoletta troppo larga, ma soprattutto una troppo stretta (−log σ² → ∞ per σ → 0), perché la nuvoletta non diventi un punto
Teorema — l’ELBO è un limite inferiore della log-verosimiglianza
Per qualsiasi distribuzione q(z | x) vale
log p(x) = Eq[log p(x | z)] − KL(q(z | x) ‖ p(z)) + KL(q(z | x) ‖ p(z | x)) ≥ Eq[log p(x | z)] − KL(q(z | x) ‖ p(z)).
Dimostrazione. log p(x) non dipende da z, quindi è uguale al suo valore atteso rispetto a q. Per il teorema di Bayes p(x) = p(x | z) p(z) / p(z | x); moltiplichiamo la frazione per q(z | x) / q(z | x):
log p(x) = Eq[log p(x | z)] + Eq[log (p(z) / q(z | x))] + Eq[log (q(z | x) / p(z | x))].
Il secondo termine è −KL(q ‖ p(z)), il terzo KL(q ‖ p(z | x)). La divergenza KL è sempre non negativa (disuguaglianza di Gibbs dalla parte sulla teoria dell’informazione), quindi tralasciando il terzo termine otteniamo la disuguaglianza. ∎
Il membro destro è l’ELBO (evidence lower bound) e con β = 1 è esattamente −L. Minimizzando L alziamo quindi il limite inferiore di log p(x) senza dover calcolare l’integrale. L’uguaglianza vale quando q(z | x) coincide con la vera distribuzione a posteriori p(z | x).
Definizione — entropia incrociata binaria dei pixel
Il decoder restituisce per ciascuno dei 64 pixel un numero yj ∈ (0, 1) e lo interpreta come la probabilità che il pixel sia acceso. Per un’immagine con luminosità xj ∈ [0, 1] si ha
−log p(x | z) = −∑j=164 [ xj log yj + (1 − xj) log(1 − yj) ].
Esempio per un pixel: è acceso (x = 1) e il decoder dice 0,9 → −ln 0,9 ≈ 0,105; dice 0,5 → ln 2 ≈ 0,693; è spento (x = 0) e il decoder dice 0,9 → −ln 0,1 ≈ 2,303. L’errore commesso con sicurezza è quello che costa di più. Poiché l’uscita è una sigmoide, la derivata rispetto all’ingresso della sigmoide è semplicemente yj − xj, proprio come nella regressione logistica.
Definizione — trucco di riparametrizzazione (reparametrization trick)
L’estrazione z ~ N(μ, σ²) non si può derivare rispetto a μ e σ. Per questo la si scrive come z = μ + σ · ε, dove ε ~ N(0, 1) è un rumore indipendente dai parametri. Allora ∂z/∂μ = 1 e ∂z/∂σ = ε, e il gradiente arriva con la retropropagazione fino all’encoder.
Portiamo il caso «fuori»: invece di «estrai un punto dalla nuvoletta» diciamo «estrai un rumore generico e spostalo e allargalo secondo l’encoder». Spostare e allargare sono conti ordinari, e quelli si possono derivare.
zj = μj(x) + σj(x) · εj, σj = exp(½ · log σj²), εj ~ N(0, 1)
- μj(x)
- centro della nuvoletta, uscita dell’encoder: ∂z/∂μ = 1, il gradiente del decoder passa all’encoder senza modifiche
- σj(x)
- larghezza della nuvoletta: l’encoder non restituisce σ, ma log σ², che può essere qualsiasi numero reale; l’esponenziale ne fa sempre una larghezza positiva, così la rete non deve sorvegliare nessun vincolo. ∂z/∂σ = ε
- εj ~ N(0, 1)
- tutto il caso: non dipende da nessun parametro, quindi attraverso di esso non serve derivare nulla; in fase di generazione tralasciamo l’encoder ed estraiamo z direttamente da N(0, I)
Esempio — una cifra di test (0) nel VAE addestrato con k = 2
encoder: μ = (0,285; 1,587), log σ² = (−2,695; −1,931) → σ = (0,260; 0,381)
KL1 = ½ (0,081 + 0,068 + 2,695 − 1) ≈ 0,922
KL2 = ½ (2,518 + 0,145 + 1,931 − 1) ≈ 1,797 → KL ≈ 2,72 nat
riparametrizzazione con ε = (0,5; −1,0): z = (0,285 + 0,260 · 0,5; 1,587 − 0,381 · 1,0) = (0,415; 1,206)
Qui la maggior parte della penalità non viene dalla distanza dall’origine, ma dalla strettezza della nuvoletta: il termine −log σ² contribuisce con 2,695 e 1,931. L’encoder vorrebbe un codice il più preciso possibile, la KL lo mantiene più largo. La KL media sulle cifre di test è 3,42 nat. L’errore di ricostruzione di questa cifra (dal centro μ) è 34,6 nat su 64 pixel.
def krok_vae(davka, beta=1.0):
for x in davka:
mu, logvar = koder(x) # k + k numeri
sigma = exp(0.5 * logvar)
eps = normal(0, 1, size=len(mu)) # caso dall’esterno
z = mu + sigma * eps # trucco di riparametrizzazione
y = dekoder(z) # probabilità dei pixel
rek = -sum(x * log(y) + (1 - x) * log(1 - y))
kl = 0.5 * sum(mu**2 + exp(logvar) - logvar - 1)
zpetne_sireni(rek + beta * kl) # il gradiente passa z → mu, sigma → encoder
adam_krok() # gradiente medio del batch
La nostra rete: 64 pixel → 32 neuroni (tanh) → μ e log σ² (k numeri ciascuno) → 32 neuroni → 64 pixel (sigmoide), addestrata con Adam per 150 epoche su 600 cifre sintetiche. La stessa rete, solo senza il rumore ε e senza la penalità KL, è un autoencoder ordinario: il confronto è quindi equo.
3. Uno spazio latente ordinato
Entrambe le reti raggruppano insieme le cifre uguali. L’autoencoder però le sparpaglia su un’area ampia: le due circonferenze della distribuzione normale sono piccole al confronto. I codici del VAE stanno attorno all’origine con deviazione standard 1,14 e 1,04, cioè proprio dove estrarremo.
Com’è fatto tutto lo spazio lo mostra una griglia: punti dello spazio latente disposti regolarmente, ciascuno passato attraverso il decoder.
Nel VAE le cifre sfumano dolcemente l’una nell’altra e quasi ogni casella è una cifra leggibile. Onestamente: nel piano anche il decoder dell’autoencoder è piuttosto liscio e anche la sua griglia contiene soprattutto cifre. La differenza si vede appieno solo con un codice a più dimensioni.
4. Generazione: cifre nuove dal rumore
Estraiamo 200 codici e li facciamo decodificare: nel VAE da N(0, I), nell’autoencoder (per dargli una possibilità equa) da una distribuzione normale adattata ai suoi codici reali. La qualità la misuriamo con la distanza (l’errore quadratico medio) del campione dalla cifra di addestramento più vicina. Le cifre di test reali distano dalla cifra di addestramento più vicina in media 0,017, il rumore casuale 0,159.
Definizione — distanza dalla cifra di addestramento più vicina e distribuzione adattata
Per un campione y ∈ [0, 1]64 e le cifre di addestramento x(1), …, x(600) si ha
d(y) = mini (1/64) ∑j=164 (yj − x(i)j)²,
cioè l’errore quadratico medio (MSE) rispetto alla cifra più simile; la qualità del generatore è la media di d su 200 campioni. Un d piccolo significa «somiglia a una cifra reale»; attenzione, zero lo otterrebbe anche un generatore che si limitasse a copiare i dati di addestramento.
L’autoencoder estrae da zj = mj + sj · εj, dove mj e sj sono la media e la deviazione standard della j-esima coordinata dei codici delle cifre di test (per k = 2: m = (−3,9; −0,9), s = (4,3; 6,0)). Le coordinate si estraggono in modo indipendente: un’eventuale dipendenza tra di esse la distribuzione adattata non la coglie.
| dimensione del codice k | ricostruzione AE (MSE) | ricostruzione VAE (MSE) | campioni AE | campioni VAE |
|---|---|---|---|---|
| 2 | 0,051 | 0,063 | 0,042 | 0,044 |
| 8 | 0,025 | 0,036 | 0,090 | 0,054 |
| 16 | 0,017 | 0,039 | 0,112 | 0,055 |
Con un codice bidimensionale le due reti si comportano in modo simile (0,042 contro 0,044). Con otto dimensioni l’autoencoder crolla: i suoi campioni hanno distanza 0,090, oltre metà strada verso il rumore, mentre il VAE resta a 0,054. Con sedici è 0,112 contro 0,055. I codici dell’autoencoder in molte dimensioni stanno su una superficie sottile e contorta, e un punto casuale dalla distribuzione normale adattata non ci cade praticamente mai.
Il prezzo: il VAE ricostruisce peggio. Con k = 16 il suo errore di ricostruzione è 0,039 contro 0,017 dell’autoencoder, e le sue immagini sono più sfocate. Il codice è rumoroso, quindi il decoder impara a disegnare la «media» di ciò che potrebbe esserci nell’intorno del codice. La sfocatura è un noto punto debole del VAE.
5. Ricostruzione contro ordine: β
Il peso β determina con quanta forza la penalità KL spinge i codici verso la distribuzione normale. Cinque VAE con codice k = 2 e β diversi:
| β | KL (nat per immagine) | ricostruzione (MSE) | campioni (distanza) |
|---|---|---|---|
| 0,1 | 8,18 | 0,053 | 0,044 |
| 0,5 | 4,71 | 0,057 | 0,044 |
| 1 | 3,42 | 0,063 | 0,044 |
| 2 | 2,07 | 0,076 | 0,052 |
| 5 | 0,05 | 0,131 | 0,075 |
Un β piccolo lascia i codici più liberi (KL 8,2) e la ricostruzione è più precisa. Un β grande li schiaccia verso lo zero. Con β = 5 la KL scende a 0,05: l’encoder ha smesso di mettere qualsiasi cosa nel codice e il decoder disegna sempre più o meno la stessa cosa, qualunque codice riceva. Questo si chiama collasso della distribuzione a posteriori (posterior collapse).
Definizione — il nat e la KL come prezzo del codice
Il nat è l’unità di informazione con il logaritmo naturale: 1 nat = 1 / ln 2 ≈ 1,443 bit. La KL media sui dati limita dall’alto l’informazione mutua tra l’immagine e il codice:
Ex KL(q(z | x) ‖ p(z)) = I(x; z) + KL(q(z) ‖ p(z)) ≥ I(x; z),
dove q(z) = Ex q(z | x) è la distribuzione dei codici di tutte le immagini insieme. La KL è quindi il «prezzo» che l’encoder paga per ciò che mette nel codice sull’immagine.
Con β = 0,1 sono al massimo 8,18 nat ≈ 11,8 bit per immagine, con β = 5 solo 0,05 nat ≈ 0,07 bit: meno di un decimo di bit, quindi il codice non dice quasi nulla sull’immagine e al decoder non resta che disegnare sempre la stessa cosa.
Al contrario, un β maggiore di 1 lo usa di proposito il β-VAE (Irina Higgins e altri, 2017): una pressione più forte verso coordinate indipendenti del codice fa sì che le singole dimensioni corrispondano spesso a proprietà comprensibili (inclinazione, spessore del tratto).
6. Transizioni graduali
Prendiamo i codici di due cifre di test (dalle reti con codice a otto dimensioni; per ogni cifra, l’immagine che il VAE ricostruisce meglio) e percorriamo la retta tra di essi in otto passi: z(t) = (1 − t) · za + t · zb per t = 0, 1/7, 2/7, …, 1, dove za, zb sono i centri μ delle due cifre. Decodifichiamo ogni punto intermedio.
La transizione è graduale in entrambe le reti: i passi intermedi sono forme verosimili tra le due cifre. Lungo la retta tra due codici reali non ci si allontana molto dai dati, quindi qui l’autoencoder ordinario non resta indietro: il suo punto debole è solo l’estrazione alla cieca. Lo stesso principio, nei modelli grandi, permette di cambiare gradualmente per esempio l’espressione di un volto o lo stile di un’immagine.
7. Dove si usano i VAE
- Compressione per altri generatori. Stable Diffusion (diffusione latente, Robin Rombach e altri, 2022) non lavora con i pixel, ma con il codice di un autoencoder con una piccola penalità KL: comprime un’immagine 512 × 512 × 3 in 64 × 64 × 4 e la diffusione gira in quello spazio più piccolo. Della diffusione parlerà una delle prossime parti.
- Codici discreti. Il VQ-VAE (Aäron van den Oord e altri, 2017), invece di un codice continuo, sceglie voci da un dizionario appreso. L’immagine diventa così una sequenza di «token» che un transformer sa generare.
- Dati scientifici. Molecole, dati cellulari, suono: ovunque sia utile uno spazio liscio in cui si possa cercare e interpolare.
In nitidezza delle immagini i VAE perdono contro le GAN e i modelli di diffusione. Hanno però un addestramento stabile, una vera interpretazione probabilistica e un encoder in regalo.
8. Riepilogo
- Un modello generativo impara la distribuzione dei dati per poterne campionare; con una variabile latente estrae un codice e lo decodifica.
- Il VAE codifica in una distribuzione N(μ, σ²) e la perdita = ricostruzione + β · KL rispetto a N(0, I). Il trucco z = μ + σε permette di derivare attraverso l’estrazione.
- I codici del VAE stanno attorno all’origine e riempiono lo spazio; quelli dell’autoencoder sono sparpagliati e pieni di buchi.
- Con 8 dimensioni del codice: campioni dell’autoencoder 0,090, del VAE 0,054 (cifre reali 0,017, rumore 0,159).
- Il prezzo è una ricostruzione peggiore e la sfocatura; un β troppo grande porta al collasso (KL 0,05).
Vuoi provarlo con mano? Chiedi a un agente IA di addestrare un VAE con codice bidimensionale sul dataset MNIST e di disegnare la griglia delle immagini decodificate: vedrai l’intera «mappa» delle cifre scritte a mano.
La vedi diversamente? Scrivi a support@coden-app.com.