Corso Machine Learning Engineer · Modulo 02 · Matematica e statistica · Lezione 12
Obiettivo. Derivare l'entropia dalla nozione di sorpresa e riconoscere la stessa formula sotto tre nomi diversi. Alla fine sai calcolare un'entropia in bit e in nat, sai dimostrare con Jensen che KL(p‖q) ≥ 0, sai mostrare in due righe che la verosimiglianza negativa di un classificatore è la sua entropia incrociata, e sai riconoscere nell'information gain di un albero un'informazione mutua.
Falso mito da sfatare. «La divergenza KL misura la distanza fra due distribuzioni.» Non è una distanza: non è simmetrica e non rispetta la disuguaglianza triangolare. Con tre distribuzioni su un foglio si costruisce il controesempio a entrambe le proprietà in un minuto, e non è pedanteria: è la differenza che decide quale dei due versi stai minimizzando, e quindi che forma avrà il modello che ottieni.
Introduzione
Tre lezioni che hai già letto usano la stessa formula senza mai nominarla. Nella lezione sugli alberi decisionali si sceglie la domanda da mettere in un nodo confrontando entropie. Nella lezione sulla regressione logistica si addestra minimizzando la verosimiglianza negativa. Nella lezione sul Variational AutoEncoder compare un termine KL dentro l'ELBO, dato per non negativo senza prova. Tre nomi per una quantità sola.
Qui chiudiamo il cerchio, costruendo la teoria dal basso: la sorpresa di un evento, la sua media, il costo di sbagliare distribuzione. Non è eleganza fine a sé stessa: quando sai che NLL = H(p, q) = H(p) + KL(p‖q), sai anche quale pezzo della perdita può scendere e quale no, e smetti di aspettarti lo zero quando i dati non lo consentono.
Nota sul perimetro. La teoria dell'informazione di Shannon in senso stretto — codifica di sorgente, capacità di canale, codici correttori — ha sul sito una serie dedicata. Qui guardiamo solo il lato funzioni di costo.
Risultato finale di questa lezione. Saprai passare a memoria fra «verosimiglianza negativa», «entropia incrociata» ed «entropia più divergenza KL», e saprai dire, davanti a una perdita che non scende più, se il colpevole è il modello o sono i dati.
Roadmap della lezione.
- La sorpresa di un evento, e perché deve essere un logaritmo.
- L'entropia come sorpresa media; bit contro nat.
- Il massimo dell'entropia, dimostrato con i moltiplicatori di Lagrange.
- L'entropia incrociata, e l'algebra che la identifica con la verosimiglianza negativa.
- La divergenza KL come spreco, e la disuguaglianza di Jensen dimostrata per intero.
- Perché KL non è una distanza, e che cosa cambia fra i suoi due versi.
- L'informazione mutua, e la scoperta che è l'information gain di un albero.
- Come implementare tutto questo senza produrre
nan.
Le otto tappe hanno un filo solo: ogni quantità è la media o lo scarto della precedente. L'entropia è la sorpresa media, l'entropia incrociata è la sorpresa media con la credenza sbagliata, la divergenza KL è la differenza fra le due, l'informazione mutua è una KL particolare. Tenere a mente questa catena rende superfluo memorizzare le formule.
La sorpresa e la sua media
L'analogia: il telegramma
Immagina di spedire un telegramma, dove si paga a carattere, per dire che tempo farà domani a Lecce. «Domani sorge il sole» non vale un carattere: chi legge lo sapeva già. «Domani nevica» ne vale molti: è raro, non era prevedibile, e cambia davvero i piani di chi riceve.
In termini di ingegneria: l'informazione di un messaggio non dipende dal messaggio, ma da quanto era improbabile. Chiamiamo sorpresa (surprisal, o self-information) questa quantità, e cerchiamo la funzione S(p) che la misura a partire dalla probabilità p.
Perché proprio il logaritmo
Il logaritmo non si sceglie per gusto: lo si ottiene da due requisiti che chiunque accetterebbe prima di vedere la formula.
- Requisito 1 — un evento certo non sorprende.
S(1) = 0, eSè decrescente inp. - Requisito 2 — additività sugli indipendenti. Se due eventi indipendenti hanno probabilità
peq, osservarli entrambi deve sorprendere quanto la somma:S(p·q) = S(p) + S(q).
I due requisiti determinano la funzione a meno di una costante. Poni u = −ln p (quindi p = e^{−u}, u ≥ 0) e definisci g(u) = S(e^{−u}). Il requisito 2 diventa g(u + v) = g(u) + g(v), l'equazione funzionale di Cauchy; il requisito 1 rende g monotona con g(0) = 0. Una soluzione monotona di Cauchy è necessariamente lineare, g(u) = c·u con c > 0. Tornando indietro:
S(p) = −c · ln p.
La regola da portare via. La costante c non è un grado di libertà: è la scelta dell'unità, cioè della base. Con c = 1/ln 2 si ha S(p) = −log₂ p in bit, con c = 1 si ha S(p) = −ln p in nat. La sorpresa è −log p non per convenzione, ma perché è l'unica funzione che azzera il certo e somma gli indipendenti.
L'entropia: la sorpresa media
Definizione. L'entropia di una distribuzione discreta p = (p₁, …, pₙ) è la sorpresa attesa, cioè la media delle sorprese pesata con le probabilità degli eventi stessi:
H(p) = E_p[ −log p ] = −Σᵢ pᵢ log pᵢ.
Leggila come una frase: quanto sono mediamente sorpreso, se il mondo si comporta davvero come dice p? È l'unica media in cui i pesi e la quantità pesata vengono dalla stessa distribuzione, ed è la ragione per cui l'entropia è una proprietà della distribuzione, non di un esito. Se pᵢ = 0 il termine si pone a zero: il limite di p log p per p → 0 vale 0, quindi non è una scorciatoia ma il valore corretto.
Bit o nat: la stessa quantità in due unità
Cambiare base moltiplica l'entropia per una costante, e basta. Vale log₂ x = ln x / ln 2, quindi H_bit = H_nat / ln 2, con ln 2 ≈ 0,693147. Non è una differenza concettuale: è una differenza di unità, come fra metri e piedi.
| Base del logaritmo | Unità | Dove la incontri |
|---|---|---|
2 |
bit (binary digit) | teoria della codifica; il criterio entropy di scikit-learn; l'information gain degli alberi |
e |
nat (natural unit) | le funzioni di costo di PyTorch e SciPy; tutta la statistica basata sulla verosimiglianza |
10 |
hartley (o dit) | letteratura storica; oggi quasi solo in tabelle di conversione |
La libreria che usi sceglie i nat per una ragione pratica: la derivata di ln x è 1/x, senza il fattore 1/ln 2 che comparirebbe in ogni gradiente. Scikit-learn lavora in bit perché negli alberi la quantità si legge invece di derivarla, e «0,456 bit di guadagno» è più interpretabile di «0,316 nat».
import math
def sorpresa_bit(p):
# sorpresa di un singolo esito di probabilità p
return -math.log2(p)
def entropia_bit(distribuzione):
# sorpresa media: i pesi sono le probabilità stesse
return -sum(p * math.log2(p) for p in distribuzione if p > 0)
moneta_truccata = [0.9, 0.1]
dado_equo = [1 / 6] * 6
print(f"sorpresa di 'esce testa' (p = 0.9): {sorpresa_bit(0.9):.6f} bit")
print(f"sorpresa di 'esce croce' (p = 0.1): {sorpresa_bit(0.1):.6f} bit")
print(f"entropia della moneta truccata : {entropia_bit(moneta_truccata):.6f} bit")
print(f"entropia del dado equo : {entropia_bit(dado_equo):.6f} bit")
print(f"la stessa moneta, in nat : {entropia_bit(moneta_truccata) * math.log(2):.6f} nat")
Output ottenuto (Python 3.14.4):
sorpresa di 'esce testa' (p = 0.9): 0.152003 bit
sorpresa di 'esce croce' (p = 0.1): 3.321928 bit
entropia della moneta truccata : 0.468996 bit
entropia del dado equo : 2.584963 bit
la stessa moneta, in nat : 0.325083 nat
Cosa fa il codice. Il filtro if p > 0 implementa la convenzione 0 · log 0 = 0 ed evita l'eccezione di math.log2(0); l'ultima riga converte in nat moltiplicando per ln 2. Il numero da guardare è il terzo: un esito raro sorprende molto (3,32 bit per p = 0,1), ma proprio perché è raro pesa poco nella media, e la moneta truccata si ferma a 0,47 bit. Il dado equo, con sei esiti equiprobabili, arriva a log₂ 6 ≈ 2,58 bit: a parità di esiti, l'incertezza è massima quando nessuno è privilegiato.
Il massimo dell'entropia, dimostrato con Lagrange
Teorema. Per una distribuzione su n esiti vale 0 ≤ H(p) ≤ log n. Il minimo si raggiunge se e solo se la distribuzione è deterministica, il massimo se e solo se è uniforme. Il minimo è immediato: ogni termine −pᵢ log pᵢ è non negativo e la somma si annulla solo se ogni pᵢ vale 0 oppure 1. Il massimo è un problema di ottimizzazione vincolata, cioè il caso d'uso dei moltiplicatori di Lagrange della lezione precedente del modulo. Scriviamo la lagrangiana, con λ associato al vincolo di normalizzazione (lavoriamo in nat; la base non cambia nulla):
L(p, λ) = −Σᵢ pᵢ ln pᵢ − λ · (Σᵢ pᵢ − 1)
La derivata di −p ln p è −ln p − 1, quindi la stazionarietà è ∂L/∂pᵢ = −ln pᵢ − 1 − λ = 0, cioè pᵢ = e^{−1−λ}. Il punto cruciale: il membro di destra non dipende da i. Tutte le pᵢ sono uguali; il vincolo Σᵢ pᵢ = 1 dà pᵢ = 1/n e quindi H = ln n. Che sia un massimo segue dalla concavità stretta: −p ln p ha derivata seconda −1/p < 0, e una funzione strettamente concava sul simplesso ha un solo punto stazionario, che è il massimo globale. ∎
Più avanti otterremo lo stesso teorema in una riga e senza derivate: KL(p‖u) = ln n − H(p) ≥ 0 con u uniforme. Vale la pena dimostrarlo due volte: la prima spiega perché il massimo è l'uniforme, la seconda lo deduce da una disuguaglianza più generale.
import math
def entropia_bit(distribuzione):
return -sum(p * math.log2(p) for p in distribuzione if p > 0)
uniforme = [0.25, 0.25, 0.25, 0.25]
sbilanciata = [0.7, 0.1, 0.1, 0.1]
deterministica = [1.0, 0.0, 0.0, 0.0]
print(f"H(uniforme) : {entropia_bit(uniforme):.6f} bit")
print(f"H(sbilanciata) : {entropia_bit(sbilanciata):.6f} bit")
print(f"H(deterministica) : {entropia_bit(deterministica):.6f} bit")
print(f"limite superiore : {math.log2(4):.6f} bit (= log2 n, con n = 4)")
Output ottenuto:
H(uniforme) : 2.000000 bit
H(sbilanciata) : 1.356780 bit
H(deterministica) : -0.000000 bit
limite superiore : 2.000000 bit (= log2 n, con n = 4)
Cosa fa il codice. Tre distribuzioni sugli stessi quattro esiti, scritte a mano e non campionate, così che i numeri siano esatti: l'uniforme tocca il limite log₂ 4 = 2, la sbilanciata perde circa 0,64 bit, la deterministica non informa mai. Nota il -0.000000 della terza riga: è zero negativo, il valore IEEE 754 che esce da -(1.0 * 0.0), non un errore di arrotondamento (lo stesso fenomeno della lezione sui numeri in virgola mobile). Vale 0.0 == -0.0, quindi un confronto non se ne accorge, ma una stampa sì. L'entropia misura quanto è difficile indovinare, non quanto l'esito sia importante: fra due sezioni questo numero diventerà il pavimento invalicabile di qualunque funzione di costo.
Esercizio 1
Il pavimento di una distribuzione diadica
Considera quattro esiti con probabilità p = (1/2, 1/4, 1/8, 1/8). Lavora in bit, carta e penna, senza eseguire codice.
- Calcola
H(p). - Un codice assegna agli esiti parole lunghe rispettivamente 1, 2, 3 e 3 bit. Qual è la lunghezza media di un messaggio?
- Un secondo codice, costruito pensando che i quattro esiti fossero equiprobabili, usa 2 bit per ciascuno. Qual è ora la lunghezza media, e quanto hai sprecato?
Le tre domande preparano la sezione successiva senza nominarla: la prima è l'entropia, la seconda mostra che un codice costruito sulla sorpresa raggiunge esattamente l'entropia, la terza misura lo spreco di un codice tarato sulla distribuzione sbagliata. Quei 0,25 bit di scarto sono la divergenza KL, calcolata prima di averle dato il nome.
Mostra la soluzione
1. Le probabilità sono potenze di 1/2, quindi i logaritmi sono interi: H(p) = 1/2·1 + 1/4·2 + 1/8·3 + 1/8·3 = 0,5 + 0,5 + 0,375 + 0,375 = 1,75 bit.
2. La lunghezza media è 1/2·1 + 1/4·2 + 1/8·3 + 1/8·3 = 1,75 bit: identica all'entropia. Non è un caso. Quel codice assegna a ogni esito esattamente −log₂ pᵢ bit, cioè la sua sorpresa, e l'entropia è la media di quelle lunghezze. È il senso operativo della formula.
3. Con 2 bit per tutti la media è 2 bit esatti, perché le probabilità sommano a 1. Lo spreco è 2 − 1,75 = 0,25 bit per messaggio: il 14% di traffico in più, per aver usato il codice ottimale per la distribuzione sbagliata. Nelle prossime due sezioni quei 2 bit si chiameranno entropia incrociata e quei 0,25 bit divergenza KL: li hai appena calcolati senza saperlo.
Entropia incrociata: il costo del codice sbagliato
Definizione. Date due distribuzioni p e q sullo stesso insieme di esiti, l'entropia incrociata di q rispetto a p è
H(p, q) = −Σᵢ pᵢ log qᵢ.
Leggila come una frase: la sorpresa media che provo se il mondo si comporta come p ma io mi aspettavo q. I pesi vengono dalla realtà, le sorprese dalla mia credenza; nell'immagine del telegramma, è la lunghezza media quando il codice è tarato su q e i messaggi arrivano con frequenze p. Se q = p si ricade sull'entropia. Due proprietà da fissare subito: non è simmetrica (H(p, q) ≠ H(q, p)) e non è mai minore dell'entropia (H(p, q) ≥ H(p), con uguaglianza solo se q = p) — quest'ultima la dimostreremo fra poco, è la disuguaglianza di Gibbs.
La verosimiglianza negativa è l'entropia incrociata
Qui avviene l'identificazione che dà il titolo alla lezione, in quattro righe di algebra. Hai n esempi (xᵢ, yᵢ) con yᵢ ∈ {1, …, K} e un classificatore che produce una distribuzione q_θ(· | xᵢ) sulle K classi. La verosimiglianza negativa media — la perdita che si minimizza addestrando una regressione logistica o una rete neurale — è
NLL(θ) = −(1/n) · Σᵢ ln q_θ(yᵢ | xᵢ)
Introduciamo la distribuzione one-hot dell'etichetta vera, p⁽ⁱ⁾_k = 1 se k = yᵢ e 0 altrimenti: somma a 1, quindi è una distribuzione legittima. Allora ln q_θ(yᵢ | xᵢ) = Σ_k p⁽ⁱ⁾_k · ln q_θ(k | xᵢ), perché nella somma sopravvive un solo addendo. Sostituendo:
NLL(θ) = −(1/n) · Σᵢ Σ_k p⁽ⁱ⁾_k ln q_θ(k | xᵢ) = (1/n) · Σᵢ H(p⁽ⁱ⁾, q_θ(· | xᵢ))
La verosimiglianza negativa media è la media delle entropie incrociate per esempio. Non «è legata a»: è la stessa somma riscritta. La perdita della lezione sulla regressione logistica e quella della lezione sulla rete da zero sono la stessa riga di codice, ed è il motivo per cui PyTorch chiama CrossEntropyLoss ciò che uno statistico chiamerebbe verosimiglianza negativa.
import math
# quattro esempi, tre classi; previsioni scritte a mano per avere numeri esatti
etichette = [0, 1, 2, 0]
previsioni = [
[1 / 2, 1 / 4, 1 / 4], # quasi giusta
[1 / 8, 3 / 4, 1 / 8], # giusta e sicura
[1 / 4, 1 / 4, 1 / 2], # quasi giusta
[1 / 8, 1 / 8, 3 / 4], # sbagliata E sicura: è questa che costa
]
def una_calda(classe, k):
# la distribuzione one-hot dell'etichetta vera
return [1.0 if i == classe else 0.0 for i in range(k)]
nll, entropia_incrociata = 0.0, 0.0
for y, q in zip(etichette, previsioni):
p = una_calda(y, 3)
termine_nll = -math.log2(q[y]) # guarda solo la classe vera
termine_ce = -sum(p_k * math.log2(q_k) for p_k, q_k in zip(p, q) if p_k > 0)
print(f"classe vera {y}: -log2 q[y] = {termine_nll:.6f} bit")
nll += termine_nll / 4
entropia_incrociata += termine_ce / 4
print(f"verosimiglianza negativa media: {nll:.6f} bit = {nll * math.log(2):.6f} nat")
print(f"entropia incrociata media : {entropia_incrociata:.6f} bit = {entropia_incrociata * math.log(2):.6f} nat")
print(f"sono lo stesso numero: {abs(nll - entropia_incrociata) < 1e-12}")
Output ottenuto:
classe vera 0: -log2 q[y] = 1.000000 bit
classe vera 1: -log2 q[y] = 0.415037 bit
classe vera 2: -log2 q[y] = 1.000000 bit
classe vera 0: -log2 q[y] = 3.000000 bit
verosimiglianza negativa media: 1.353759 bit = 0.938354 nat
entropia incrociata media : 1.353759 bit = 0.938354 nat
sono lo stesso numero: True
Cosa fa il codice. Il ciclo calcola la stessa quantità per due strade: termine_nll guarda solo la probabilità della classe vera, termine_ce somma su tutte e tre pesando con il vettore one-hot, che azzera ogni addendo tranne uno. Guarda il quarto esempio: il modello mette 3/4 sulla classe sbagliata e 1/8 su quella giusta, e paga 3 bit, più del doppio della media. L'entropia incrociata punisce la sicurezza sbagliata molto più di quanto premi quella giusta, perché −log q cresce senza limite quando q → 0: una sola previsione quasi-zero sulla classe corretta può dominare la perdita di un intero minibatch.
La divergenza KL: lo spreco, non il costo totale
Definizione. La divergenza di Kullback-Leibler da p a q è la differenza fra entropia incrociata ed entropia:
KL(p‖q) = H(p, q) − H(p) = Σᵢ pᵢ log (pᵢ / qᵢ)
Leggila come una frase: quanto pago in più per aver usato q al posto di p. Non è il costo totale, è lo spreco. Il costo H(p, q) contiene un pezzo che non dipende da me (H(p), l'incertezza intrinseca dei dati) e uno che dipende solo dal modello. Nell'esercizio di poche righe fa: 2 bit erano l'entropia incrociata, 1,75 l'entropia, 0,25 la divergenza KL.
Quando addestri,
H(p)non si muove. Scende soltantoKL(p‖q).
L'entropia dei dati è una proprietà del dataset, non dei parametri: la sua derivata rispetto a θ è identicamente nulla. Minimizzare l'entropia incrociata e minimizzare la KL sono quindi lo stesso problema, con obiettivi che differiscono per una costante additiva. E se i dati hanno etichette rumorose — lo stesso input con etichette diverse — allora H(p) > 0 e la perdita non arriverà mai a zero, per quanto grande sia la rete. Non è un bug: è il pavimento.
La disuguaglianza di Jensen, dimostrata
Abbiamo affermato due volte che KL ≥ 0. Lo strumento per dimostrarlo è la disuguaglianza di Jensen, lo stesso attrezzo che la lezione sul Variational AutoEncoder usa per ricavare l'ELBO: lì viene usata, qui viene dimostrata. Enunciato: sia φ convessa e X una variabile aleatoria discreta con valori x₁, …, xₙ e probabilità w₁, …, wₙ; allora φ(Σᵢ wᵢxᵢ) ≤ Σᵢ wᵢ φ(xᵢ), e se φ è strettamente convessa l'uguaglianza vale solo per X costante.
Passo base (n = 2). È la definizione di convessità: φ(t·x₁ + (1−t)·x₂) ≤ t·φ(x₁) + (1−t)·φ(x₂) per ogni t ∈ [0, 1]. Geometricamente, la corda fra due punti del grafico sta sopra il grafico.
Passo induttivo. Vera per n−1 punti, prendiamone n con pesi che sommano a 1. Se wₙ = 1 non c'è nulla da dimostrare; altrimenti poni s = 1 − wₙ > 0 e y = Σ_{i<n} (wᵢ/s)·xᵢ, combinazione convessa di n−1 punti perché i pesi wᵢ/s sommano a 1. Allora Σᵢ wᵢxᵢ = s·y + wₙ·xₙ, e applicando prima il caso a due punti e poi l'ipotesi induttiva:
φ(Σᵢ wᵢxᵢ) = φ(s·y + wₙ·xₙ) ≤ s·φ(y) + wₙ·φ(xₙ) ≤ s·Σ_{i<n}(wᵢ/s)·φ(xᵢ) + wₙ·φ(xₙ) = Σᵢ wᵢ·φ(xᵢ)
dove la s si semplifica nell'ultimo passaggio. ∎
Applicazione: la disuguaglianza di Gibbs. La funzione φ(x) = −ln x è strettamente convessa su x > 0, perché φ″(x) = 1/x² > 0. Prendiamo come variabile aleatoria il rapporto X = qᵢ/pᵢ, con i estratto secondo p:
−KL(p‖q) = Σᵢ pᵢ ln(qᵢ/pᵢ) = E_p[ln X] ≤ ln E_p[X] = ln ( Σᵢ pᵢ · qᵢ/pᵢ ) = ln ( Σ_{i: pᵢ > 0} qᵢ ) ≤ ln 1 = 0
La prima disuguaglianza è Jensen applicata a −ln, la seconda vale perché una somma parziale delle qᵢ non supera 1. Cambiando segno: KL(p‖q) ≥ 0. L'uguaglianza richiede entrambe le relazioni con il segno di uguale: la stretta convessità impone qᵢ/pᵢ costante dove pᵢ > 0, e il fatto che entrambe sommino a 1 forza quella costante a valere 1, cioè q = p. ∎
La regola da portare via. KL ≥ 0 non è un assioma: è Jensen applicata al logaritmo. E siccome KL(p‖q) = H(p, q) − H(p), lo stesso argomento dimostra che nessun modello può costare meno dell'entropia dei dati. È il risultato più importante della lezione.
KL non è una distanza: asimmetria, triangolare e i due versi
Il nome «divergenza» invece di «distanza» non è una sfumatura. Una distanza deve essere simmetrica e rispettare la triangolare; la KL fallisce entrambe, e si vede con tre distribuzioni su tre esiti scelte con numeri comodi: p = (3/4, 1/8, 1/8), q = (1/2, 1/4, 1/4), r = (1/8, 3/4, 1/8).
import math
def divergenza_kl_bit(p, q):
totale = 0.0
for p_i, q_i in zip(p, q):
if p_i == 0.0:
continue # 0 · log(0/q) = 0, per continuità
if q_i == 0.0:
return math.inf # il modello esclude ciò che i dati osservano
totale += p_i * math.log2(p_i / q_i)
return totale
p = [0.75, 0.125, 0.125]
q = [0.5, 0.25, 0.25]
r = [0.125, 0.75, 0.125]
print(f"KL(p || q) = {divergenza_kl_bit(p, q):.6f} bit")
print(f"KL(q || p) = {divergenza_kl_bit(q, p):.6f} bit <- diverso: non è simmetrica")
print(f"KL(q || r) = {divergenza_kl_bit(q, r):.6f} bit")
print(f"KL(p || q) + KL(q || r) = {divergenza_kl_bit(p, q) + divergenza_kl_bit(q, r):.6f} bit")
print(f"KL(p || r) = {divergenza_kl_bit(p, r):.6f} bit <- più della somma")
print(f"KL(p || p) = {divergenza_kl_bit(p, p):.6f} bit")
Output ottenuto:
KL(p || q) = 0.188722 bit
KL(q || p) = 0.207519 bit <- diverso: non è simmetrica
KL(q || r) = 0.853759 bit
KL(p || q) + KL(q || r) = 1.042481 bit
KL(p || r) = 1.615602 bit <- più della somma
KL(p || p) = 0.000000 bit
Cosa fa il codice. I due casi limite sono gestiti esplicitamente: dove p è nulla il termine si salta, dove q è nulla ma p no la divergenza è infinita e la funzione lo dichiara invece di produrre un nan silenzioso. I numeri dicono due cose. KL(p‖q) = 0,188722 contro KL(q‖p) = 0,207519: nessuna simmetria. E il percorso diretto da p a r costa 1,615602 bit mentre passando per q se ne spendono 1,042481: la triangolare è violata di oltre mezzo bit. Resta solo ciò che abbiamo dimostrato, KL ≥ 0 e KL(p‖p) = 0: basta per usarla come funzione obiettivo, non basta per chiamarla distanza.
Esercizio 2
Quale dei due versi stai minimizzando?
Due soli esiti. p = (1/2, 1/2) è la realtà, q = (9/10, 1/10) è il modello. Lavora in bit, carta e penna. Ti servono log₂ 5 ≈ 2,321928 e log₂ 3 ≈ 1,584963.
- Calcola
KL(p‖q)eKL(q‖p). - Quale dei due è maggiore, e perché il risultato è quello che ci si aspetta guardando dove ciascuna distribuzione mette massa?
- Quando addestri un classificatore con l'entropia incrociata, quale dei due versi stai minimizzando?
Le tre domande servono a toccare con mano l'asimmetria: il conto dei due versi mostra che i risultati differiscono, la seconda chiede di spiegare la differenza guardando dove ciascuna distribuzione mette massa, la terza collega il risultato all'addestramento di un classificatore. Rispondi alla terza solo dopo aver letto la definizione di entropia incrociata.
Mostra la soluzione
1. KL(p‖q) = 1/2·log₂(0,5/0,9) + 1/2·log₂(0,5/0,1). Il primo rapporto è 5/9, il secondo è 5, quindi = 1/2·(log₂5 − 2·log₂3) + 1/2·log₂5 = log₂5 − log₂3 = 2,321928 − 1,584963 = 0,736966 bit. Nell'altro verso: KL(q‖p) = 0,9·log₂(1,8) + 0,1·log₂(0,2) = 0,9·0,847997 − 0,1·2,321928 = 0,763197 − 0,232193 = 0,531004 bit.
2. È maggiore KL(p‖q) = 0,736966. La media è presa secondo la distribuzione di sinistra: in KL(p‖q) pesiamo con p, che mette metà della massa sul secondo esito, dove q vale solo 0,1, e quel rapporto da 5 a 1 viene pagato con peso 0,5. In KL(q‖p) il rapporto grosso è nello stesso punto, ma lì q pesa 0,1. Il verso decide dove si guarda: la distribuzione di sinistra sceglie i punti in cui l'errore conta.
3. Il verso KL(dati ‖ modello), cioè il forward: l'entropia incrociata è H(p, q) con p la distribuzione empirica delle etichette, quindi la media è presa sui dati. Nell'inferenza variazionale si minimizza il verso opposto, ed è il motivo per cui i due metodi producono approssimazioni di forma diversa.
Forward KL e reverse KL: coprire oppure cercare una moda
Dato che i due versi non coincidono, scegliere quale minimizzare è una decisione di progetto. Il modo più nitido per vederla è adattare una sola gaussiana a una distribuzione bimodale, nei due sensi. Bersaglio: la mistura p(x) = ½·N(x; −2, 1) + ½·N(x; +2, 1), due colline identiche separate da una valle.
Forward, min_q KL(p‖q). La media è presa secondo p: ovunque p metta massa il modello paga ln(p/q), e se q è quasi zero dove p non lo è il costo esplode. È zero-avoiding, e la soluzione «copre» tutto il supporto. Per una famiglia esponenziale come le gaussiane la soluzione è in forma chiusa: minimizzare KL(p‖q) equivale a massimizzare E_p[ln q], le cui condizioni di stazionarietà impongono di uguagliare i momenti. Quindi μ = 0 e σ² = ½(1 + 4) + ½(1 + 4) − 0² = 5, cioè σ = √5 ≈ 2,236068: una campana larga, centrata proprio nella valle.
Reverse, min_q KL(q‖p). Qui la media è presa secondo q: il modello paga solo dove lui mette massa, e non viene punito per ignorare una regione che non visita mai. È zero-forcing, e la soluzione si incolla su una moda. Nel limite di mode ben separate il calcolo si fa a mano: vicino alla collina di destra p(x) ≈ ½·N(x; 2, 1), quindi KL(q‖p) ≈ KL(q‖N(2,1)) + ln 2, minimizzata da q = N(2, 1) con residuo ln 2 ≈ 0,693147 nat. La soluzione simmetrica su −2 è ugualmente ottima: il problema ha due minimi, e quale si trova dipende da dove parte l'ottimizzatore.
import math
# mistura bimodale, parametri scritti a mano: due gaussiane di peso 1/2
pesi = [0.5, 0.5]
medie = [-2.0, 2.0]
varianze = [1.0, 1.0]
# forward KL: la soluzione è in forma chiusa, uguaglia media e varianza
media_mistura = sum(w * m for w, m in zip(pesi, medie))
secondo_momento = sum(w * (v + m * m) for w, m, v in zip(pesi, medie, varianze))
varianza_mistura = secondo_momento - media_mistura ** 2
print(f"forward -> N(mu = {media_mistura:.6f}, sigma = {math.sqrt(varianza_mistura):.6f})")
print(f"reverse -> N(mu = {medie[1]:.6f}, sigma = {math.sqrt(varianze[1]):.6f})")
print(f"KL(q || p) residua nel limite di mode separate: {math.log(2):.6f} nat")
print(f"rapporto fra le due larghezze: {math.sqrt(varianza_mistura) / math.sqrt(varianze[1]):.6f}")
Output ottenuto:
forward -> N(mu = 0.000000, sigma = 2.236068)
reverse -> N(mu = 2.000000, sigma = 1.000000)
KL(q || p) residua nel limite di mode separate: 0.693147 nat
rapporto fra le due larghezze: 2.236068
Cosa fa il codice. Il forward non richiede nessuna ottimizzazione numerica: media e secondo momento della mistura hanno formule esatte, e la varianza è E[x²] − E[x]². Il reverse è riportato nel suo valore limite analitico, valido quando le mode sono separate abbastanza da trascurare la coda dell'altra: con quattro deviazioni standard di separazione, come qui, l'ottimizzazione numerica sposterebbe i parametri solo nella terza cifra decimale. Il rapporto fra le larghezze è 2,236068: la soluzione forward è più di due volte più larga di quella reverse.
| Criterio | Forward KL(p‖q) |
Reverse KL(q‖p) |
|---|---|---|
| Chi pesa gli errori | i dati | il modello |
| Comportamento | zero-avoiding: copre tutto il supporto | zero-forcing: si concentra su una moda |
| Difetto tipico | mette massa dove non c'è niente (la valle) | ignora intere regioni del bersaglio |
| Dove lo incontri | massima verosimiglianza, entropia incrociata | inferenza variazionale, ELBO, distillazione inversa |
Trade-off. Nessuno dei due versi è «quello giusto». Se stimi una densità e vuoi che il modello non dichiari impossibile nulla di ciò che accade, ti serve il forward: paga caro gli zeri sbagliati. Se costruisci un'approssimazione da cui dovrai campionare, spesso è preferibile il reverse: una gaussiana larga centrata nella valle genererebbe campioni in una regione che il bersaglio non visita mai. La domanda è sempre la stessa: mi serve copertura o mi serve precisione?
Informazione mutua
Definizione (due modi). L'informazione mutua fra X e Y è la riduzione di incertezza su una quando si osserva l'altra, I(X; Y) = H(Y) − H(Y | X), dove H(Y | X) = Σ_x p(x) · H(Y | X = x) è la media delle entropie dei «rami». Ma è anche la divergenza KL fra la congiunta e il prodotto delle marginali, I(X; Y) = KL( p(x, y) ‖ p(x)·p(y) ), cioè quanto la realtà si discosta dall'indipendenza. Le due definizioni coincidono; partiamo dalla seconda e usiamo p(x, y) = p(x)·p(y|x):
KL( p(x,y) ‖ p(x)p(y) ) = Σ_{x,y} p(x,y) log ( p(x,y) / (p(x)p(y)) ) = Σ_{x,y} p(x,y) log ( p(y|x) / p(y) )
= Σ_{x,y} p(x,y) log p(y|x) − Σ_{x,y} p(x,y) log p(y) = −H(Y|X) + H(Y)
Nell'ultimo passaggio il primo addendo è per definizione −H(Y|X), e nel secondo la somma su x marginalizza p(x,y) in p(y). ∎ Da questa forma discendono due proprietà gratis: l'informazione mutua è simmetrica, perché la seconda definizione lo è in modo evidente, ed è nulla se e solo se le variabili sono indipendenti, perché è una KL e una KL si annulla solo quando le due distribuzioni coincidono. Due corollari di KL ≥ 0 che dalla prima definizione avremmo faticato a ottenere.
import math
# sedici esempi sintetici, scritti a mano. "canale" è informativo, "rumore" no.
canale = [0] * 8 + [1] * 8
etichetta = [0, 0, 0, 0, 0, 0, 0, 1] + [1, 1, 1, 1, 1, 1, 1, 0]
rumore = [0, 0, 0, 1, 1, 1, 1, 0] * 2
def entropia_bit(frequenze):
totale = sum(frequenze)
return -sum((c / totale) * math.log2(c / totale) for c in frequenze if c > 0)
def conta(valori, maschera=None):
coppie = valori if maschera is None else [v for v, m in zip(valori, maschera) if m]
return [coppie.count(0), coppie.count(1)]
def mutua_per_riduzione(x, y):
# I(X; Y) = H(Y) - H(Y | X)
h_y = entropia_bit(conta(y))
h_condizionata = 0.0
for valore in (0, 1):
maschera = [v == valore for v in x]
peso = sum(maschera) / len(x)
h_condizionata += peso * entropia_bit(conta(y, maschera))
return h_y, h_condizionata, h_y - h_condizionata
def mutua_per_kl(x, y):
# I(X; Y) = KL( congiunta || prodotto delle marginali )
n = len(x)
totale = 0.0
for a in (0, 1):
for b in (0, 1):
congiunta = sum(1 for u, v in zip(x, y) if u == a and v == b) / n
if congiunta == 0.0:
continue
marginale_x = sum(1 for u in x if u == a) / n
marginale_y = sum(1 for v in y if v == b) / n
totale += congiunta * math.log2(congiunta / (marginale_x * marginale_y))
return totale
h_y, h_cond, mutua = mutua_per_riduzione(canale, etichetta)
print(f"H(etichetta) : {h_y:.6f} bit")
print(f"H(etichetta | canale) : {h_cond:.6f} bit")
print(f"I(etichetta; canale) : {mutua:.6f} bit")
print(f"I via KL(congiunta||prod.): {mutua_per_kl(canale, etichetta):.6f} bit")
print(f"le due vie coincidono entro 1e-12: {abs(mutua - mutua_per_kl(canale, etichetta)) < 1e-12}")
print(f"I(canale; etichetta) : {mutua_per_kl(etichetta, canale):.6f} bit <- simmetrica")
print(f"I(etichetta; rumore) : {mutua_per_kl(rumore, etichetta):.6f} bit <- indipendenti")
Output ottenuto:
H(etichetta) : 1.000000 bit
H(etichetta | canale) : 0.543564 bit
I(etichetta; canale) : 0.456436 bit
I via KL(congiunta||prod.): 0.456436 bit
le due vie coincidono entro 1e-12: True
I(canale; etichetta) : 0.456436 bit <- simmetrica
I(etichetta; rumore) : 0.000000 bit <- indipendenti
Cosa fa il codice. I dati sono sedici esempi inventati e scritti esplicitamente: dove canale vale 0 l'etichetta è 0 sette volte su otto e viceversa, mentre rumore spezza il dataset in due metà con la stessa identica composizione di etichette. mutua_per_riduzione implementa H(Y) − H(Y|X) pesando le entropie dei rami con la loro frequenza; mutua_per_kl ricostruisce la tabella 2×2 della congiunta. Le due strade danno lo stesso numero fino all'ultima cifra, e rumore dà esattamente zero — non «quasi zero per caso», ma zero perché la congiunta è il prodotto delle marginali. Osservare canale porta l'incertezza da 1 bit a 0,543564: il guadagno è 0,456436 bit. Tienilo a mente per trenta secondi.
L'information gain di un albero è informazione mutua
Un albero decisionale con criterio entropy sceglie la variabile su cui splittare massimizzando l'information gain, cioè l'entropia del padre meno la media pesata delle entropie dei figli:
IG(Y, X) = H(Y) − Σ_v (n_v / n) · H(Y | X = v)
Confrontalo con l'entropia condizionata H(Y|X) = Σ_v p(v)·H(Y|X=v), dove p(v) stimata sui dati è proprio n_v/n: le due espressioni coincidono termine a termine, quindi IG(Y, X) = I(Y; X). L'information gain non «assomiglia» all'informazione mutua: è l'informazione mutua sulla distribuzione empirica. Un albero è allora un algoritmo greedy che a ogni nodo sceglie la domanda con la massima informazione mutua con l'etichetta — il che ne spiega anche il limite, perché greedy significa «massima a un passo», e una coppia di variabili può essere informativa quando nessuna delle due lo è da sola. Verifichiamolo contro scikit-learn, sugli stessi sedici esempi.
import numpy as np
from sklearn.tree import DecisionTreeClassifier
# riusa canale, etichetta, rumore e mutua_per_kl definiti sopra
X = np.column_stack([canale, rumore]).astype(float)
y = np.array(etichetta)
albero = DecisionTreeClassifier(criterion="entropy", max_depth=1, random_state=0).fit(X, y)
impurita = albero.tree_.impurity # entropia di ogni nodo, in bit
numerosita = albero.tree_.n_node_samples
print(f"colonna scelta per lo split: {albero.tree_.feature[0]} (0 = canale, 1 = rumore)")
print(f"soglia : {albero.tree_.threshold[0]:.1f}")
print(f"entropia della radice : {impurita[0]:.6f} bit ({numerosita[0]} esempi)")
print(f"entropia del figlio sinistro: {impurita[1]:.6f} bit ({numerosita[1]} esempi)")
print(f"entropia del figlio destro : {impurita[2]:.6f} bit ({numerosita[2]} esempi)")
guadagno = impurita[0] - sum(
(numerosita[figlio] / numerosita[0]) * impurita[figlio] for figlio in (1, 2)
)
print(f"information gain dell'albero : {guadagno:.6f} bit")
print(f"informazione mutua a mano : {mutua_per_kl(canale, etichetta):.6f} bit")
Output ottenuto (NumPy 2.3.5, scikit-learn 1.8.0):
colonna scelta per lo split: 0 (0 = canale, 1 = rumore)
soglia : 0.5
entropia della radice : 1.000000 bit (16 esempi)
entropia del figlio sinistro: 0.543564 bit (8 esempi)
entropia del figlio destro : 0.543564 bit (8 esempi)
information gain dell'albero : 0.456436 bit
informazione mutua a mano : 0.456436 bit
Cosa fa il codice. Addestriamo un albero di profondità 1 — un solo split — con il criterio entropia: tree_.impurity contiene l'entropia di ogni nodo nell'ordine radice, figlio sinistro, figlio destro, e tree_.n_node_samples le numerosità. Lo stesso numero, 0,456436 bit, esce tre volte per tre strade indipendenti: come riduzione di entropia, come divergenza KL fra congiunta e prodotto, e come guadagno interno a una libreria che non sapeva nulla delle altre due. L'albero sceglie canale e ignora rumore, il cui guadagno è esattamente nullo. È anche la ragione per cui scikit-learn lavora in bit: quel numero finisce in un grafico che un essere umano deve leggere.
Implementarle senza farsi male
Il logaritmo di zero
Il modo più rapido per riempire un addestramento di nan è calcolare log(0). Succede con una probabilità che va in underflow, con una classe mai osservata in un minibatch, o con un vettore già normalizzato a cui si riapplica la softmax. Il risultato non è un errore: è un -inf che, moltiplicato per zero, diventa nan, si propaga nel gradiente e azzera i pesi in un passo.
import math
try:
math.log(0.0)
except ValueError as errore:
print("math.log(0.0) ->", errore)
print("0.0 * (-inf) ->", 0.0 * float("-inf"))
print(f"con il clip a 1e-12, la sorpresa si ferma a {-math.log(1e-12):.6f} nat")
print(f"cioè {-math.log2(1e-12):.6f} bit: grande, ma finita")
Output ottenuto (Python 3.14.4):
math.log(0.0) -> expected a positive input, got 0.0
0.0 * (-inf) -> nan
con il clip a 1e-12, la sorpresa si ferma a 27.631021 nat
cioè 39.863137 bit: grande, ma finita
Cosa fa il codice. math.log(0.0) solleva un'eccezione — il testo del messaggio è cambiato nelle versioni recenti di Python, ma ciò che conta è che sia un'eccezione e non un valore — mentre NumPy e PyTorch restituiscono -inf con un avviso; in entrambi i casi 0 · (−inf) dà nan, ed è lì che nasce il guaio. L'ultima parte mostra il rimedio più diffuso, il clipping: limitare le probabilità a [ε, 1−ε] con ε = 1e-12 tiene la sorpresa massima a 27,6 nat, grande ma finita, e il gradiente resta calcolabile.
Trappola reale: il clipping maschera il sintomo, non la causa. Se il modello assegna 1e-12 alla classe corretta, il problema non è il logaritmo: è il modello. Il clipping serve a non perdere un addestramento per una riga, non a convivere per sempre con previsioni impossibili. Metti un contatore sulle volte in cui interviene: se cresce, la diagnosi è altrove.
logsumexp, derivato
Il secondo scoglio è l'opposto: l'esponenziale che esplode. Per passare da punteggi grezzi (logit) a probabilità si usa la softmax, softmax(z)ᵢ = e^{zᵢ} / Σⱼ e^{zⱼ}, e con logit dell'ordine delle centinaia e^z esce dal campo dei float64, che si ferma attorno a 1,8 · 10³⁰⁸ (lezione sui numeri in virgola mobile).
Il rimedio si deriva in una riga. Per ogni costante c vale log Σⱼ e^{zⱼ} = log ( e^c · Σⱼ e^{zⱼ−c} ) = c + log Σⱼ e^{zⱼ−c}: aggiungere e togliere c non cambia il risultato matematico, ma cambia radicalmente quello numerico. Scegliendo c = max_j zⱼ il massimo degli esponenziali diventa e⁰ = 1 e gli altri stanno in (0, 1]: nessun overflow possibile, e gli underflow sui termini piccolissimi sono innocui perché contribuiscono alla somma in modo trascurabile. Questa è logsumexp, e log_softmax(z) = z − logsumexp(z) ne discende.
import math
def logsumexp(valori):
massimo = max(valori)
return massimo + math.log(sum(math.exp(v - massimo) for v in valori))
logit = [1000.0, 1001.0, 1002.0]
try:
[math.exp(z) for z in logit]
except OverflowError as errore:
print("softmax ingenua, math.exp(1000.0) ->", errore)
lse = logsumexp(logit)
log_prob = [z - lse for z in logit]
print(f"logsumexp stabile : {lse:.6f}")
print("log_softmax :", " ".join(f"{v:.6f}" for v in log_prob))
print("softmax :", " ".join(f"{math.exp(v):.6f}" for v in log_prob))
# il fallimento opposto: una probabilità che va in underflow
logit_estremo = [0.0, -800.0]
lse2 = logsumexp(logit_estremo)
probabilita = [math.exp(z - lse2) for z in logit_estremo]
print("softmax(x) :", " ".join(f"{v:.6e}" for v in probabilita))
print("log(softmax(x))[1] : -inf (la probabilità è diventata 0.0: informazione persa)")
print(f"log_softmax(x)[1] : {logit_estremo[1] - lse2:.6f} (esatto, senza mai esponenziare)")
Output ottenuto:
softmax ingenua, math.exp(1000.0) -> math range error
logsumexp stabile : 1002.407606
log_softmax : -2.407606 -1.407606 -0.407606
softmax : 0.090031 0.244728 0.665241
softmax(x) : 1.000000e+00 0.000000e+00
log(softmax(x))[1] : -inf (la probabilità è diventata 0.0: informazione persa)
log_softmax(x)[1] : -800.000000 (esatto, senza mai esponenziare)
Cosa fa il codice. La prima metà mostra l'overflow e il rimedio: con logit intorno a 1000 l'esponenziale diretto fallisce, mentre logsumexp restituisce 1002,407606 e le probabilità corrette, che dipendono solo dalle differenze fra i logit. La seconda metà mostra il fallimento simmetrico e più insidioso: con logit [0, −800] la softmax produce uno zero esatto e log(softmax(x)) perde l'informazione restituendo -inf, mentre log_softmax, che non esponenzia mai, dà il valore giusto, −800.
La regola da portare via. torch.nn.CrossEntropyLoss non è pigrizia di API: fonde log_softmax e verosimiglianza negativa in un'unica operazione proprio per non materializzare mai le probabilità. Da qui l'errore più comune del deep learning applicato: passarle un tensore su cui hai già chiamato softmax. Il codice non protesta, la perdita scende, il modello impara qualcosa — ma stai minimizzando l'entropia incrociata di una softmax di una softmax, che è una funzione diversa e molto più piatta. Se la perdita parte attorno a ln K e scende lentissima, controlla prima questo.
from scipy.special import logsumexp
from scipy.stats import entropy
p = [0.75, 0.125, 0.125]
q = [0.5, 0.25, 0.25]
print(f"entropy(p, base=2) : {entropy(p, base=2):.6f} bit")
print(f"entropy(p, q, base=2) = KL: {entropy(p, q, base=2):.6f} bit")
print(f"entropy(p) in nat : {entropy(p):.6f} nat")
print(f"logsumexp([1000,1001,1002]): {logsumexp([1000.0, 1001.0, 1002.0]):.6f}")
Output ottenuto (SciPy 1.16.3):
entropy(p, base=2) : 1.061278 bit
entropy(p, q, base=2) = KL: 0.188722 bit
entropy(p) in nat : 0.735622 nat
logsumexp([1000,1001,1002]): 1002.407606
Cosa fa il codice. scipy.stats.entropy sorprende chi legge solo il nome: con un argomento calcola l'entropia, con due la divergenza KL. In SciPy non esiste una funzione per H(p, q): se ti serve l'entropia incrociata la ottieni sommando le due chiamate, 1,061278 + 0,188722 = 1,250000 bit. Il logsumexp della libreria coincide con la nostra implementazione a mano fino all'ultima cifra stampata, e il 0,188722 della seconda riga è lo stesso numero dello snippet sull'asimmetria: due implementazioni indipendenti della stessa definizione.
L'esperimento: tre modi, un solo numero
Mettiamo tutto insieme su un problema sintetico a tre classi, costruito in modo che l'entropia dei dati non sia zero. Dodici esempi in tre gruppi di quattro; dentro ogni gruppo gli input sono identici ma le etichette no, quindi la distribuzione condizionata è genuinamente incerta: nel gruppo A due esempi su quattro sono di classe 0 e uno ciascuno delle altre due, e così via ruotando. È la situazione di qualunque dataset con etichettatura rumorosa o classi sovrapposte, cioè il caso in cui H(p, q) = H(p) + KL(p‖q) smette di essere accademica.
import numpy as np
# tre gruppi di quattro esempi; dentro un gruppo l'input è lo stesso,
# l'etichetta no. Numeri scritti a mano per avere valori esatti.
P = np.array([[0.50, 0.25, 0.25],
[0.25, 0.50, 0.25],
[0.25, 0.25, 0.50]])
etichette = np.array([[0, 0, 1, 2],
[1, 1, 0, 2],
[2, 2, 0, 1]])
def interpola(P, t):
# modello: una miscela fra l'uniforme (t = 0) e la verità (t = 1)
return (1 - t) / 3 + t * P
Q = interpola(P, 0.5)
# (a) verosimiglianza negativa media, esempio per esempio
nll = -np.mean([np.log(Q[g, y]) for g in range(3) for y in etichette[g]])
# (b) entropia incrociata media, gruppo per gruppo
entropia_incrociata = np.mean(-np.sum(P * np.log(Q), axis=1))
# (c) entropia dei dati più divergenza KL
entropia_dati = np.mean(-np.sum(P * np.log(P), axis=1))
divergenza = np.mean(np.sum(P * np.log(P / Q), axis=1))
scarti = [abs(nll - entropia_incrociata), abs(nll - entropia_dati - divergenza)]
print(f"(a) verosimiglianza negativa media : {nll:.9f} nat")
print(f"(b) entropia incrociata media : {entropia_incrociata:.9f} nat")
print(f"(c) H(p) + KL(p || q) : {entropia_dati + divergenza:.9f} nat")
print(f"scarto massimo sotto 1e-12: {max(scarti) < 1e-12}")
print(f" H(p), dipende solo dai dati : {entropia_dati:.9f} nat")
print(f" KL(p || q), dipende dal modello: {divergenza:.9f} nat")
Output ottenuto (NumPy 2.3.5):
(a) verosimiglianza negativa media : 1.053806209 nat
(b) entropia incrociata media : 1.053806209 nat
(c) H(p) + KL(p || q) : 1.053806209 nat
scarto massimo sotto 1e-12: True
H(p), dipende solo dai dati : 1.039720771 nat
KL(p || q), dipende dal modello: 0.014085438 nat
Cosa fa il codice. Tre strade per lo stesso numero: la prima scorre i dodici esempi e somma −ln q[y], come farebbe una perdita in addestramento; la seconda lavora sulle tre distribuzioni empiriche di gruppo; la terza somma entropia e divergenza calcolate separatamente. I tre valori coincidono fino alla nona cifra, con scarto sotto 1e-12: l'identità non è un'approssimazione, è algebra. Il dato interessante è la scomposizione: dei 1,053806 nat di perdita, 1,039721 sono entropia dei dati e solo 0,014085 sono colpa del modello. Un addestramento perfetto porterebbe la perdita a 1,039721, non a zero: se stai aspettando lo zero, stai aspettando una cosa che non può succedere.
Che cosa scende davvero quando scende la perdita
Un modello a tre classi che produce logit z e li passa per una softmax ha un gradiente dell'entropia incrociata di una semplicità sospetta: ∂H(p, q)/∂z = q − p, la previsione meno la verità. Dato che H(p) non dipende da z, quello è anche il gradiente della divergenza KL: la discesa del gradiente, quale che sia il passo e quale che sia l'ottimizzatore, muove soltanto la componente KL. Verifichiamolo misurando la scomposizione su quattro modelli di qualità crescente, invece di affidarci a una traiettoria che dipenderebbe dal passo scelto.
import numpy as np
# riusa P e interpola dallo snippet precedente
def scomposizione(Q):
h_p = np.mean(-np.sum(P * np.log2(P), axis=1))
h_pq = np.mean(-np.sum(P * np.log2(Q), axis=1))
return h_p, h_pq, h_pq - h_p
modelli = {
"scambiato": P[[2, 0, 1]], # predice la distribuzione di un altro gruppo
"uniforme": interpola(P, 0.0),
"intermedio": interpola(P, 0.5),
"perfetto": interpola(P, 1.0),
}
print(f"{'modello':<12}{'H(p)':>10}{'H(p,q)':>11}{'KL(p||q)':>11}")
for nome, previsioni in modelli.items():
h_p, h_pq, kl = scomposizione(previsioni)
print(f"{nome:<12}{h_p:>10.6f}{h_pq:>11.6f}{kl:>11.6f}")
gradiente = interpola(P, 0.0)[0] - P[0]
print("gradiente q - p sul gruppo A, modello uniforme:", " ".join(f"{g:+.6f}" for g in gradiente))
print(f"somma del gradiente: {gradiente.sum():+.6f} (resta sul simplesso)")
Output ottenuto:
modello H(p) H(p,q) KL(p||q)
scambiato 1.500000 1.750000 0.250000
uniforme 1.500000 1.584963 0.084963
intermedio 1.500000 1.520321 0.020321
perfetto 1.500000 1.500000 0.000000
gradiente q - p sul gruppo A, modello uniforme: -0.166667 +0.083333 +0.083333
somma del gradiente: -0.000000 (resta sul simplesso)
Cosa fa il codice. I quattro modelli vanno dal peggiore — che predice la distribuzione di un altro gruppo — al perfetto. La colonna H(p) è una colonna di costanti, 1,5 bit per tutti e quattro; H(p,q) scende da 1,75 a 1,5 e KL da 0,25 a 0: le due colonne scendono della stessa identica quantità, perché differiscono per una costante. Quando guardi una curva di perdita che scende stai guardando una curva di KL traslata verso l'alto di un valore che nessun ottimizzatore può toccare. Il gradiente somma a -0.000000, di nuovo uno zero negativo: la correzione sposta massa fra le classi senza mai uscire dal simplesso.
Errori tipici (sintomo, causa, rimedio)
nandopo poche epoche. Sintomo: la perdita è sensata per qualche centinaio di passi, poi diventanane i pesi la seguono. Causa:log(0), per underflow di una probabilità o per un logaritmo applicato a una softmax già calcolata. Rimedio: usa la versione fusa della perdita sui logit, mai sulle probabilità, e in codice tuo resta inlog_softmax.- Bit confusi con nat. Sintomo: la tua implementazione e la libreria danno perdite diverse, ma con rapporto sempre uguale. Causa: tu usi
log₂, la librerialn; differiscono per il fattoreln 2 ≈ 0,693147. Rimedio: dichiara l'unità nel nome della variabile (perdita_nat,guadagno_bit) e converti una volta sola, al confine. - KL usata come distanza simmetrica. Sintomo: due misure di «distanza fra distribuzioni» che dovrebbero coincidere e non coincidono, o un clustering che cambia risultato scambiando gli argomenti. Causa:
KL(p‖q) ≠ KL(q‖p)e la triangolare non vale. Rimedio: se ti serve una metrica usa Jensen-Shannon (la sua radice lo è) o Wasserstein; altrimenti scegli il verso consapevolmente. - Probabilità passate a una perdita che rifà la softmax. Sintomo: la perdita parte attorno a
ln Ke scende con lentezza inspiegabile, l'accuratezza migliora ma resta sotto le attese. Causa: il tensore in ingresso è già normalizzato e la softmax interna lo riappiattisce. Rimedio: controlla che i valori in ingresso possano essere negativi e non sommino a 1; se sommano a 1, hai trovato il bug. - La perdita sale mentre l'accuratezza sale. Sintomo: due curve che divergono a fine addestramento, e la tentazione di fidarsi della più rassicurante. Causa: non è un paradosso — l'accuratezza guarda quale classe ha il massimo, l'entropia incrociata quanta probabilità c'è sulla classe giusta, e un modello che diventa più sicuro sia delle risposte giuste sia di quelle sbagliate migliora la prima e peggiora la seconda. Rimedio: misura la calibrazione, e decidi in anticipo quale metrica sceglie il modello da salvare.
I cinque errori sono la stessa famiglia di guasti visti da angoli diversi: unità, argomenti, normalizzazioni applicate due volte. Quattro producono numeri sbagliati ma plausibili, e solo il primo si annuncia con un nan. Per questo conviene stampare accanto alla perdita il pavimento H(p) e l'unità: un valore fuori scala si nota subito.
Best practice
- Dichiara l'unità ovunque. Bit o nat va scritto nel nome della variabile e nell'etichetta del grafico: è la fonte più banale di numeri che non tornano fra due implementazioni.
- Non materializzare mai le probabilità. Resta in
log_softmaxdalla rete fino alla perdita: più stabile, più veloce, e nessuna occasione di chiamarelogsu uno zero. - Stampa il pavimento prima di addestrare. Calcola
H(p)della distribuzione empirica delle etichette: un modello non addestrato parte attorno aln K, e la perdita non scenderà sotto l'entropia condizionata dei dati. - Scegli il verso della KL per iscritto. Quando una specifica dice «minimizza la divergenza fra le due distribuzioni», chiedi quale va a sinistra: forward e reverse danno modelli diversi, e sui dati multimodali la differenza si vede a occhio.
- Verifica ogni implementazione con un secondo metodo. Una formula scritta a mano si controlla in trenta secondi contro
scipy.stats.entropy: in questa lezione lo stesso numero è uscito tre volte per tre strade indipendenti. - Usa probabilità razionali nei test. Potenze di
1/2e frazioni semplici danno entropie esatte (1,75 bit, 2 bit) verificabili a mente: un test con numeri casuali dice che il codice gira, non che è giusto.
Le sei regole sono tutte controlli a basso costo contro numeri che non tornano: la prima e la sesta evitano confusioni di unità e di verifica, la seconda e la terza stabilizzano e interpretano la perdita, la quarta fissa il verso della KL, la quinta chiede un secondo metodo indipendente. Se devi sceglierne una, scegli la quinta.
Glossario minimo
- Sorpresa (surprisal, self-information):
−log p, l'informazione portata da un singolo esito di probabilitàp. - Entropia (entropy):
H(p) = −Σ pᵢ log pᵢ, la sorpresa media di una distribuzione. Vale fra 0 elog n. - Entropia incrociata (cross-entropy):
H(p, q) = −Σ pᵢ log qᵢ, il costo medio di usare il codice ottimale perqquando la realtà èp. - Divergenza KL (Kullback-Leibler divergence):
KL(p‖q) = H(p, q) − H(p) ≥ 0, lo spreco dovuto al modello sbagliato. Non è una distanza. - Informazione mutua (mutual information):
I(X; Y) = H(Y) − H(Y|X) = KL(congiunta ‖ prodotto), quanto osservare una variabile riduce l'incertezza sull'altra. - Disuguaglianza di Jensen (Jensen's inequality): per
φconvessa,φ(E[X]) ≤ E[φ(X)]. È lo strumento che dimostraKL ≥ 0e che genera l'ELBO. - Disuguaglianza di Gibbs (Gibbs' inequality):
H(p) ≤ H(p, q)con uguaglianza solo seq = p. ÈKL ≥ 0riscritta. logsumexp:log Σ e^{zᵢ}calcolato sottraendo il massimo, per evitare l'overflow dell'esponenziale. È il cuore dilog_softmax.
Gli otto termini si leggono in sequenza come una catena di definizioni: ciascuno è costruito con il precedente, e dal quarto in poi tutti si misurano nella stessa unità scelta all'inizio. Se una voce non ti torna, risali alla sorpresa: tutto il glossario discende da −log p.
Mini-progetto
Costruisci uno script che misura il «pavimento» di un dataset con etichette rumorose e verifica che una perdita reale non lo attraversi.
- Genera a mano un dataset sintetico con
K = 4classi eGgruppi di input identici, assegnando a ciascun gruppo una distribuzione di etichette diversa e con probabilità razionali; calcola e stampa l'entropia condizionata mediaH(p)in nat; - implementa una perdita di entropia incrociata che accetta logit, usando
logsumexpe senza mai calcolare esplicitamente le probabilità; verificala contro una seconda implementazione ingenua su valori piccoli, e stampa la differenza massima; - ottimizza i logit (un vettore per gruppo) con qualunque metodo, stampando a ogni passo la perdita e la quantità
perdita − H(p); documenta che cosa succede quando un gruppo contiene una sola etichetta — è il caso limite in cuiH(p) = 0e la perdita può davvero tendere a zero, con i logit che divergono.
Le tre richieste vanno eseguite nell'ordine, perché ciascuna produce l'oracolo per la successiva: prima il pavimento teorico, poi la perdita scritta con logsumexp e verificata contro quella ingenua, infine l'ottimizzazione, dove la differenza fra perdita e pavimento deve scendere verso zero senza mai attraversarlo.
Consegna: uno script che stampa il pavimento teorico, la perdita finale e la divergenza KL residua, più due righe di commento che spiegano perché la differenza fra le ultime due è esattamente il pavimento.
Challenge
- Easy. Calcola a mano, in bit, l'entropia di una moneta con
p = 0,9e quella di un dado equo a sei facce. Quale delle due sorgenti «sorprende» di più in media, e di quanto? Poi di': esiste un singolo esito della moneta che sorprende più di qualunque esito del dado? - Medium. Dimostra che
H(p) ≤ log nper ogni distribuzione sunesiti, con uguaglianza solo sull'uniforme, usando la disuguaglianza di Gibbs invece dei moltiplicatori di Lagrange. Suggerimento: scriviKL(p‖u)conuuniforme e semplifica. - Hard. Dimostra la disuguaglianza di Jensen per
npunti per induzione, scrivendo tutti i passaggi, e deducineKL(p‖q) ≥ 0. Poi caratterizza il caso di uguaglianza: dove serve esattamente la stretta convessità? - Expert. Mostra numericamente che per
qvicina apvaleKL(p‖q) ≈ ½·(p−q)ᵀ F (p−q), conFla matrice di informazione di Fisher della famiglia categorica, cioèF = diag(1/pᵢ). Prendip = (0,5, 0,3, 0,2), perturbala diε·dcond = (1, −1, 0)edε ∈ {10⁻¹, 10⁻², 10⁻³}, e verifica che il rapporto fra KL esatta e forma quadratica tenda a 1 con ordineε. - Enterprise. Prendi un modello di classificazione in produzione nella tua organizzazione. Calcola la distribuzione delle classi predette nel periodo di addestramento e quella dell'ultimo mese, e misura la KL fra le due in bit. Scegli una soglia di allarme documentando come l'hai scelta: la via difendibile è stimare la distribuzione della KL su finestre storiche in cui non è successo nulla e mettere la soglia a un quantile alto, non scegliere un numero tondo. Scrivi anche che cosa non rileva questo indicatore: la deriva che lascia invariate le frequenze delle classi.
I cinque livelli salgono dal calcolo a mano alla verifica di una proprietà sul sistema che gestisci tu: i primi due consolidano entropia e Gibbs, il terzo esige la dimostrazione per induzione, il quarto collega la KL alla geometria dell'informazione di Fisher, l'ultimo misura una deriva reale. Fai prima quelli che ti fanno sbagliare i conti.
Gli esercizi di codice di questa lezione sono in un solo file, esercizi.py (circa 12 KB): l'entropia in bit e in nat, la KL che non è una distanza e l'informazione mutua che coincide con l'information gain di uno split. Richiede solo Python 3.12 o successivo, senza dipendenze esterne. Eseguilo con python3 esercizi.py: gira anche con le parti da completare lasciate vuote e ti dice quali mancano.
Recap
Mappa mentale. Sorpresa = −log p, unica funzione additiva che azzera il certo. Entropia H(p) = sorpresa media, fra 0 e log n. Entropia incrociata H(p, q) = costo con il codice sbagliato. KL(p‖q) = H(p,q) − H(p) ≥ 0 per Jensen: lo spreco. Verosimiglianza negativa = H(p, q) con p one-hot. Information gain = I(Y; X) = H(Y) − H(Y|X).
Cheat sheet.
| Simbolo | Significato |
|---|---|
−log p |
sorpresa di un esito di probabilità p |
H(p) |
entropia: sorpresa media, il pavimento di ogni perdita |
H(p, q) |
entropia incrociata: la perdita che minimizzi davvero |
KL(p‖q) |
divergenza: H(p,q) − H(p), l'unica parte che scende |
I(X; Y) |
informazione mutua: l'information gain di uno split |
ln 2 ≈ 0,693 |
il fattore fra nat e bit |
3 concetti: la sorpresa è −log p perché è l'unica funzione additiva che azzera il certo; l'entropia incrociata è la verosimiglianza negativa, non le somiglia; la KL è uno spreco non negativo, non una distanza.
3 errori: log(0) che produce nan; bit scambiati per nat con un fattore 0,693 di differenza; probabilità già normalizzate passate a una perdita che rifà la softmax.
3 best practice: dichiara sempre l'unità; resta in log_softmax fino alla perdita; stampa il pavimento H(p) prima di cominciare ad addestrare.
1 esercizio: il mini-progetto di questa lezione.
Prossima lezione: Bayes applicato ai parametri, dove si scopre che la regolarizzazione che hai già usato era una credenza scritta in formule.
Domande per andare avanti
- Se la KL non è una distanza, esiste una quantità simmetrica che lo sia e che si costruisca a partire da lei? (La divergenza di Jensen-Shannon e la sua radice: Cover e Thomas, capitolo 2.)
- L'entropia incrociata è la perdita giusta anche quando le classi sono fortemente sbilanciate, o conviene pesarla? (Lezione sulle metriche di classificazione, modulo 03.)
- Che cosa succede alla divergenza KL quando le distribuzioni sono continue e i loro supporti non si sovrappongono? (È il problema che motiva la distanza di Wasserstein nelle reti generative avversarie, modulo 07.)
Le tre domande sono ganci verso le lezioni successive: la prima porta alla Jensen-Shannon, la seconda alle metriche per le classi sbilanciate, la terza alle distribuzioni continue con supporti disgiunti. Non vanno risolte subito; servono a decidere che cosa leggere dopo.
Nella stessa serie trovi anche: 26 - Ottimizzazione vincolata: moltiplicatori di Lagrange, dualità e condizioni KKT (la lezione precedente), 28 - Statistica bayesiana: prior, posterior, coniugati e perché la regolarizzazione è una credenza (la lezione successiva), 95 - L'attenzione, derivata per intero: query, chiave, valore e il blocco Transformer scritto da zero (che richiama questa lezione).
Fonti
- C. E. Shannon, A Mathematical Theory of Communication, Bell System Technical Journal 27, 1948 - la definizione di entropia e la sua caratterizzazione per requisiti, nella sezione 6 «Choice, Uncertainty and Entropy», verificata sul testo del PDF, consultata il 2026-11-12
- SciPy, scipy.stats.entropy - l'entropia e la divergenza KL calcolate da libreria, con il logaritmo naturale come base di default, usate come secondo metodo di verifica, consultata il 2026-11-12
- SciPy, scipy.special.logsumexp - il calcolo stabile del logaritmo di una somma di esponenziali, consultata il 2026-11-12
- PyTorch, torch.nn.CrossEntropyLoss - la perdita si aspetta logit non normalizzati, non probabilità, consultata il 2026-11-12
Riferimenti bibliografici a cui rimanda la lezione e che non sono stati raggiunti online: S. Kullback e R. A. Leibler, On Information and Sufficiency, Annals of Mathematical Statistics 22(1), 1951, pp. 79-86, e T. M. Cover e J. A. Thomas, Elements of Information Theory, 2ª edizione, Wiley, 2006 (capitolo 2 per entropia, entropia relativa e informazione mutua). Tutti i listati sono stati eseguiti due volte con Python 3.14.4, NumPy 2.3.5, SciPy 1.16.3 e scikit-learn 1.8.0: le uscite riportate coincidono con quelle reali.
Commenti
Caricamento commenti...
Accedi per commentare