Programmazione Scientifica++: Lezione 3

Deep Learning — Loss, Backpropagation e Ottimizzatori

Docente

Argomenti di Oggi

  • La Funzione Costo (MSE)
  • Gradient Descent
  • Backpropagation: la regola della catena, i \(\boldsymbol{\delta}\) e le due formule
  • Il Batching
  • Ottimizzatori: dalla discesa semplice ad Adam

Minimizzare l’Errore: Loss Function

L’intelligenza artificiale, in realtà, non è poi così intelligente. Non “pensa”. Quello che fa è risolvere un immenso problema di Minimizzazione Matematica.

Per far sì che una rete neurale impari, dobbiamo dirle quanto sta sbagliando in modo rigoroso e misurabile. La funzione che calcola questa “distanza” tra la risposta data dalla rete e la risposta vera (Target) si chiama Funzione Costo (Loss Function).

Esistono molte Loss (es. Cross-Entropy per la classificazione), ma la più intuitiva è la MSE.

La Mean Squared Error (MSE)

L’Errore Quadratico Medio calcola la differenza tra la Verità (\(y\)) e la Predizione della rete (\(a\)), la eleva al quadrato (per punire severamente gli errori grandi ed eliminare il problema del segno negativo), e ne fa la media.

\[ E = \frac{1}{n} \sum_{i=1}^{n} (y_{target, i} - a_{output, i})^2 \]

Il risultato \(E\) è un singolo, banalissimo numero scalare (es. \(E = 12.4\)). Se \(E=0\), la rete è onnisciente. Se \(E\) è grande, la rete è stupida.

L’Obiettivo Matematico

Il nostro obiettivo diventa: trovare i pesi \(\mathbf{W}^{(\ell)}\) e i bias \(\mathbf{b}^{(\ell)}\) che rendono \(E\) il più piccolo possibile. (Non zero: una rete che azzera l’errore sui dati di addestramento di solito ha solo imparato a memoria — ne riparleremo col test set.)

Una rete con 100 strati può avere \(10^7\) pesi, e \[ E = E\!\left(\mathbf{W}^{(1)}, \dots, \mathbf{W}^{(100)}\right) \] è una superficie in uno spazio a dieci milioni di dimensioni.

Come troviamo il punto più basso di questa immensa vallata?

L’Escursionista Bendato

Immaginate di essere bendati in cima a una catena montuosa e di voler scendere a valle. L’unico modo che avete per farcela è appoggiare il piede per “tastare” la pendenza del terreno sotto di voi, capire in quale direzione la pendenza scende, e fare un piccolo passo.

Ripetete questo processo 10.000 volte, e alla fine arriverete a valle.

Questo processo, in matematica analitica, si chiama Gradient Descent (Discesa del Gradiente).

Gradient Descent

La pendenza del terreno è la Derivata (il Gradiente) dell’Errore \(E\) rispetto ai pesi \(\mathbf{W}\). Per addestrare la rete, “aggiorniamo” ogni singolo peso sottraendogli una frazione del gradiente:

\[ \mathbf{W}_{new} = \mathbf{W}_{old} - \eta \frac{\partial E}{\partial \mathbf{W}} \]

Dove \(\eta\) è il Learning Rate (quanto è lungo il passo dell’escursionista bendato). Se il passo è troppo lungo, cadrete in un burrone. Se è troppo corto, ci metterete secoli ad arrivare a valle.

Gradient Descent — Visualizzazione

Il Paradosso della Rete Profonda

Calcolare il gradiente \(\partial E/\partial \mathbf{W}\) per l’ultimo strato è facile: basta la derivata della MSE. Ma prendete un peso del primo strato — per esempio \(W^{(1)}_{57,\,432}\), quello con cui il neurone nascosto 57 legge il pixel 432. Quanto vale \(\partial E / \partial W^{(1)}_{57,\,432}\), se \(E\) si misura solo all’uscita, uno strato più in là? E in una rete di cento strati, cento strati più in là?

La soluzione è il Teorema della Funzione Composta (Regola della Catena) di Leibniz.

L’errore finale è causato dall’output del Layer 100. Ma l’output del Layer 100 è causato dal Layer 99. A sua volta causato dal 98… giù fino al Layer 1.

La Backpropagation prende il Delta dell’errore finale e lo Propaga all’indietro, strato per strato, calcolando quanta colpa ha avuto ogni singolo neurone. È l’algoritmo che ha reso possibile il Deep Learning: ora lo deriviamo.

Backpropagation — Computation Graph

Notazione: tre vettori per ogni strato

Numeriamo gli strati \(\ell = 1, \dots, L\) e chiamiamo \(\mathbf{a}^{(0)} = \mathbf{x}\) l’ingresso. Ogni strato produce due vettori:

\[ \mathbf{z}^{(\ell)} = \mathbf{W}^{(\ell)}\mathbf{a}^{(\ell-1)} + \mathbf{b}^{(\ell)}, \qquad \mathbf{a}^{(\ell)} = \sigma\!\left(\mathbf{z}^{(\ell)}\right) \]

\(\mathbf{z}\) è la pre-attivazione, \(\mathbf{a}\) l’attivazione; per componenti, con la convenzione della Lezione 2 (\(j\) = arrivo, \(k\) = ingresso), \(z^{(\ell)}_j = \sum_k W^{(\ell)}_{jk}\, a^{(\ell-1)}_k + b^{(\ell)}_j\). Il terzo vettore è la definizione su cui poggia tutto:

\[ \delta^{(\ell)}_j \;\equiv\; \frac{\partial E}{\partial z^{(\ell)}_j} \]

cioè quanto cambia l’errore se sposto di poco la pre-attivazione del neurone \(j\): la sua colpa. La backpropagation è un modo efficiente di calcolare tutti i \(\boldsymbol{\delta}^{(\ell)}\). (Nel framework \(\mathbf{b} = 0\): le formule valgono togliendolo.)

Passo 1: il gradiente dei pesi

Il peso \(W^{(\ell)}_{jk}\) influenza \(E\) solo attraverso \(z^{(\ell)}_j\): è l’unica pre-attivazione in cui compare. Regola della catena:

\[ \frac{\partial E}{\partial W^{(\ell)}_{jk}} = \frac{\partial E}{\partial z^{(\ell)}_j}\,\frac{\partial z^{(\ell)}_j}{\partial W^{(\ell)}_{jk}} = \delta^{(\ell)}_j\, a^{(\ell-1)}_k , \]

perché nella somma \(z^{(\ell)}_j = \sum_k W^{(\ell)}_{jk} a^{(\ell-1)}_k\) il peso \(W_{jk}\) moltiplica solo \(a_k\). In forma matriciale è un prodotto colonna per riga:

\[ \frac{\partial E}{\partial \mathbf{W}^{(\ell)}} = \boldsymbol{\delta}^{(\ell)} \left(\mathbf{a}^{(\ell-1)}\right)^{T}, \qquad \frac{\partial E}{\partial \mathbf{b}^{(\ell)}} = \boldsymbol{\delta}^{(\ell)} . \]

Le dimensioni tornano: \((N_\ell \times 1)(1 \times N_{\ell-1})\), la forma di \(\mathbf{W}^{(\ell)}\). Ogni elemento \((j,k)\) è “colpa del neurone \(j\)” per “quanto era acceso l’ingresso \(k\)”: un ingresso spento non può far cambiare il suo peso. Il problema si è ridotto a calcolare i \(\boldsymbol{\delta}\), uno strato alla volta.

Passo 2: l’ultimo strato

All’uscita \(E\) dipende da \(z^{(L)}_j\) solo attraverso \(a^{(L)}_j = \sigma(z^{(L)}_j)\):

\[ \delta^{(L)}_j = \frac{\partial E}{\partial a^{(L)}_j}\,\sigma'\!\left(z^{(L)}_j\right) \qquad\Longrightarrow\qquad \boldsymbol{\delta}^{(L)} = \nabla_{\mathbf{a}} E \odot \sigma'\!\left(\mathbf{z}^{(L)}\right) \]

dove \(\odot\) è il prodotto componente per componente. Con la MSE, \(E = \sum_j (a^{(L)}_j - y_j)^2\), il primo fattore è \(2\,(\mathbf{a}^{(L)} - \mathbf{y})\): l’errore, con il segno che dice in che verso correggere.

Questo è il punto di partenza. È anche l’unico posto in cui la loss entra nel calcolo: da qui in giù la backpropagation non sa più quale loss stiamo usando — ed è per questo che nel framework la loss è una classe separata, con il suo backward che restituisce proprio \(\nabla_{\mathbf{a}} E\).

Note

La slide sulla MSE aveva un fattore \(1/n\); il framework lo omette. Un fattore costante moltiplica tutto il gradiente e si riassorbe nel learning rate \(\eta\).

Il Problema dell’Inversione

Abbiamo \(\boldsymbol{\delta}^{(L)}\). Ci serve \(\boldsymbol{\delta}^{(L-1)}\), poi \(\boldsymbol{\delta}^{(L-2)}\), fino al primo strato. Nel forward pass l’informazione ha attraversato \(\mathbf{W}\) in avanti: \[ \mathbf{z} = \mathbf{W} \cdot \mathbf{x} + \mathbf{b} \]

Ora l’errore deve fluire al contrario, attraversando la stessa matrice. L’istinto (sbagliato) sarebbe usare l’inversa \(\mathbf{W}^{-1}\).

Sarebbe un disastro. Le inverse sono lentissime da calcolare, numericamente instabili, e semplicemente non esistono se \(\mathbf{W}\) non è quadrata — e \(\mathbf{W}\) quasi mai lo è: \(784 \to 200\) non si inverte.

Per fortuna non serve invertire nulla: basta fare il conto.

Passo 3: il passo ricorsivo

\(z^{(\ell)}_j\) influenza \(E\) attraverso tutte le pre-attivazioni dello strato successivo, \(z^{(\ell+1)}_k\). Regola della catena con una somma:

\[ \delta^{(\ell)}_j = \sum_k \frac{\partial E}{\partial z^{(\ell+1)}_k}\,\frac{\partial z^{(\ell+1)}_k}{\partial z^{(\ell)}_j} = \sum_k \delta^{(\ell+1)}_k\,\frac{\partial z^{(\ell+1)}_k}{\partial z^{(\ell)}_j} . \]

Da \(z^{(\ell+1)}_k = \sum_m W^{(\ell+1)}_{km}\,\sigma(z^{(\ell)}_m) + b^{(\ell+1)}_k\), derivando rispetto a \(z^{(\ell)}_j\) sopravvive solo \(m = j\): resta \(\partial z^{(\ell+1)}_k/\partial z^{(\ell)}_j = W^{(\ell+1)}_{kj}\,\sigma'(z^{(\ell)}_j)\), e quindi \(\delta^{(\ell)}_j = \sigma'(z^{(\ell)}_j)\sum_k W^{(\ell+1)}_{kj}\,\delta^{(\ell+1)}_k\). Si somma sul primo indice di \(W\), e sommare sul primo indice è moltiplicare per la trasposta:

\[ \boxed{\;\boldsymbol{\delta}^{(\ell)} = \left(\left(\mathbf{W}^{(\ell+1)}\right)^{T}\boldsymbol{\delta}^{(\ell+1)}\right) \odot \sigma'\!\left(\mathbf{z}^{(\ell)}\right)\;} \]

Nessuna inversa: la trasposta riporta le colpe dei neuroni d’arrivo al neurone di partenza, con lo stesso peso che aveva portato il segnale in avanti.

L’algoritmo completo

  1. Forward: calcola e conserva \(\mathbf{z}^{(\ell)}\) e \(\mathbf{a}^{(\ell)}\), per \(\ell = 1 \dots L\).
  2. Ultimo strato: \(\;\boldsymbol{\delta}^{(L)} = \nabla_{\mathbf{a}}E \odot \sigma'(\mathbf{z}^{(L)})\).
  3. Backward, per \(\ell = L, L-1, \dots, 1\): prima il gradiente dello strato, poi la colpa da consegnare a quello prima:

\[ \begin{aligned} \partial E/\partial\mathbf{W}^{(\ell)} &= \boldsymbol{\delta}^{(\ell)}\left(\mathbf{a}^{(\ell-1)}\right)^T \\[2pt] \boldsymbol{\delta}^{(\ell-1)} &= \big((\mathbf{W}^{(\ell)})^T\boldsymbol{\delta}^{(\ell)}\big)\odot\sigma'\!\left(\mathbf{z}^{(\ell-1)}\right) \qquad (\ell > 1) \end{aligned} \]

  1. Aggiornamento: \(\;\mathbf{W}^{(\ell)} \leftarrow \mathbf{W}^{(\ell)} - \eta\,\partial E/\partial\mathbf{W}^{(\ell)}\), per ogni \(\ell\).

Costo: due prodotti matrice-vettore per strato, lo stesso ordine del forward. Perturbare un peso alla volta costerebbe un forward per ogni peso: con \(10^5\) pesi, \(10^5\) volte tanto.

Attenzione al libro

Nel libro di Rashid il passo 3 è semplificato: per riportare l’errore dallo strato di uscita (2) a quello nascosto (1) usa l’errore grezzo \(\mathbf{e} = \mathbf{y} - \mathbf{a}^{(2)}\),

\[ \begin{aligned} \text{libro:}\quad & \left(\mathbf{W}^{(2)T}\,\mathbf{e}\right)\odot\sigma'\!\left(\mathbf{z}^{(1)}\right) \\[2pt] \text{esatto:}\quad & \left(\mathbf{W}^{(2)T}\big[\,\mathbf{e}\odot\sigma'\!\left(\mathbf{z}^{(2)}\right)\big]\right)\odot\sigma'\!\left(\mathbf{z}^{(1)}\right) \end{aligned} \]

Manca il fattore \(\sigma'(\mathbf{z}^{(2)})\) dello strato di uscita — cioè il passaggio da \(\mathbf{e}\) a \(\boldsymbol{\delta}^{(2)}\) (segno e fattore 2 si riassorbono in \(\eta\)). La rete del libro impara comunque, perché \(\sigma' > 0\) e ogni componente dell’errore conserva il verso, ma quello che scende non è il gradiente di \(E\): è una direzione vicina, con i pesi delle colpe alterati. Il framework del corso implementa la formula esatta — il layer sigmoide di uscita applica il suo \(\sigma'\) prima di passare il vettore indietro — e nel Lab 13 il gradient check numerico lo verificherà alla sesta cifra.

Vettorizzazione: Il Batching

Nelle lezioni precedenti abbiamo detto che la Rete “impara” elaborando un’immagine alla volta (Vettore Colonna \(\mathbf{x}\)). Calcola la predizione, calcola l’errore, aggiorna i pesi (SGD).

Ma elaborare \(60.000\) immagini una per una lascia inutilizzata gran parte della potenza della CPU: ogni prodotto matrice-vettore è troppo piccolo per riempire le unità vettoriali.

Possiamo mandare 64 immagini contemporaneamente alla rete neurale per l’addestramento?

Passare le Matrici invece dei Vettori

La risposta è l’ennesimo miracolo dell’Algebra Lineare, chiamato Batching.

Invece di passare alla Forward Pass un vettore colonna singolo \(\mathbf{x}\) (dimensione: 784 pixel \(\times\) 1 immagine), passiamo una Gigantesca Matrice \(\mathbf{X}\) (dimensione: 784 pixel \(\times\) 64 immagini)!

Le equazioni rimangono identiche! \[ \mathbf{Z} = \mathbf{W} \mathbf{X} + \mathbf{B} \]

Librerie come Eigen macinano le 64 colonne con le estensioni vettoriali (AVX), riducendo di molto il tempo per epoca.

Note

Nel nostro framework il batch avrà una sola colonna: aggiorniamo i pesi dopo ogni immagine, come fa Rashid nel libro. È lo Stochastic Gradient Descent puro, e su MNIST basta per superare il 90% in tre epoche. Scriveremo comunque tutto in forma matriciale: le equazioni sono già quelle giuste, e passare al mini-batch richiederebbe di adattare solo il dataset e le metriche.

Limiti dell’SGD, e l’Inerzia

Lo Stochastic Gradient Descent nudo e crudo ha due difetti. Tornando all’analogia della montagna: se finisce in una vallata “secondaria” (minimo locale) resta bloccato lì, pensando di aver risolto il problema quando il minimo globale è a 10 km. E se la vallata è stretta, rimbalza fra le due pareti (oscillazione) invece di scendere.

Il trucco è imitare la fisica classica: immaginiamo l’escursionista come una palla da bowling pesante. L’SGD con Momentum aggiunge l’inerzia: se stiamo scendendo velocemente in una direzione, memorizza questa “velocità”. Davanti a un piccolo dosso, l’inerzia accumulata lo fa scavalcare — e l’addestramento diventa più veloce e più fluido.

Il Re dell’Industria: ADAM

Oggi, praticamente chiunque (da OpenAI a Google) usa l’algoritmo Adam (Adaptive Moment Estimation).

A differenza di SGD, che usa un solo Learning Rate \(\eta\) per tutta l’immensa matrice dei pesi, Adam è furbo. Tiene in memoria la media dei gradienti passati (Momentum) e adatta un Learning Rate personalizzato per ogni singolo peso della rete!.

Se un peso viene aggiornato poco, Adam accelera il suo Learning Rate. Se sbalza troppo, lo frena. È un ottimizzatore plug-and-play che non necessita quasi mai di calibrazione.

Nel Laboratorio 10 implementerete SDOptimizer, la discesa più semplice — una riga, param -= lr * grad. Momentum e Adam sono la traccia 1 dei progetti finali: l’interfaccia Optimizer è fatta perché si possano aggiungere senza toccare né i layer né la rete.

I Learning Rate Scheduler

Anche con Adam, tenere lo stesso passo dall’inizio alla fine è sconsigliato: in fondo alla vallata, passi troppo lunghi scavalcano il centro. Serve un Learning Rate Scheduler, che diminuisca \(\eta\) col tempo. Due politiche classiche:

  • ReduceLROnPlateau — reattivo: se la Loss smette di scendere per 5 epoche, divide \(\eta\) per 10. Ottimo per spremere l’ultimo grammo di precisione.
  • Cosine Annealing — proattivo: segue la curva del coseno, veloce all’inizio e dolce verso la fine, senza gli scalini del plateau.

Nel Laboratorio 10 scriverete l’interfaccia Scheduler a \(\eta\) costante; queste due sono la traccia 2 dei progetti. Ma come uniamo Layer, Ottimizzatori, Scheduler e Backpropagation nello stesso software C++ senza impazzire?

Quesiti finali

1. Un passo troppo lungo. Loss \(L(x) = x^2\), partenza \(x_0 = 1\), learning rate \(\eta = 1\). Dove siete dopo un passo? E dopo due? Per quali \(\eta\) il metodo converge, e qual è il valore che centra il minimo in un solo passo?

2. (aperta) Il tetto della sigmoide. La derivata della sigmoide è \(\sigma' = \sigma(1-\sigma)\). Qual è il suo valore massimo? La backpropagation moltiplica questo fattore una volta per strato: cosa arriva al primo strato di una rete di dieci? E allora come fanno ad addestrarsi le reti da cinquanta strati di cui leggete sui giornali?

Quesiti finali: le risposte

1. \(L'(x) = 2x\), quindi il passo è \(x \leftarrow x - \eta\,2x = (1 - 2\eta)\,x\). Con \(\eta = 1\) il fattore vale \(-1\): \(x_1 = -1\), \(x_2 = +1\), e si rimbalza fra i due per sempre senza che la loss cali di un millesimo. Si converge per \(|1 - 2\eta| < 1\), cioè \(0 < \eta < 1\); e \(\eta = 0{,}5\) annulla il fattore e centra il minimo al primo passo. Un learning rate grande non è “la stessa discesa, più veloce”: è un metodo diverso, che puo’ non convergere affatto.

2. Traccia: \(\sigma(1-\sigma)\) è massima dove \(\sigma = 1/2\) e vale \(0{,}25\). Dieci strati la moltiplicano dieci volte: \(0{,}25^{10} \approx 10^{-6}\), e siamo nel caso migliore. Il primo strato riceve un gradiente un milione di volte più piccolo dell’ultimo e di fatto non impara: è il vanishing gradient, già incontrato nella Lezione 02. Le reti profonde di oggi lo aggirano cambiando i tre ingredienti che qui diamo per scontati: la funzione di attivazione (ReLU, Lezione 02), l’inizializzazione dei pesi (He e Xavier, Laboratorio 08) e il percorso che l’errore fa per tornare indietro.