DL Appunti Discesa del gradiente

DL Appunti per la Discesa del Gradiente

Come “Apprendono” le Reti Neurali

Foto di Rohit Tandon / Unsplash

Le Reti Neurali Artificiali (ANN) sono approssimatori di funzioni universali. Possono approssimare qualsiasi funzione complessa se fornite abbastanza dati, hanno un’architettura appropriata e vengono addestrate per un periodo sufficiente.

Ma cosa significa “addestrare” una rete?

In un articolo precedente sul processo di feedforward, ho menzionato che addestrare una rete significa regolare il valore dei suoi pesi per ottenere una migliore approssimazione della funzione che stiamo cercando di approssimare.

In questo articolo, descriverò l’algoritmo della discesa del gradiente, che viene utilizzato per regolare i pesi di una ANN.

Cominciamo con i concetti di base.

Scendendo da una montagna

Immaginati di essere in cima a una montagna e di dover raggiungere il punto più basso di una valle vicina ad essa.

Non abbiamo una mappa, c’è nebbia e sta facendo buio, abbiamo perso le strade e dobbiamo arrivare in fondo velocemente. Non è una situazione piacevole, ma mostra i “confini” del problema.

Per la nostra sicurezza, supponiamo che non ci siano dirupi ripidi nella montagna, quindi è simile a una funzione differenziabile.

Discesa dalla vetta del Monviso. Una piccola valle vicino a Oncino, Cuneo. Immagine dell'autore.

Quando fa buio, non possiamo vedere in quale direzione stiamo andando. L’unico modo per scendere è fare piccoli passi e controllare se ci troviamo a un’altezza inferiore o meno.

Se ci accorgiamo di essere saliti, andiamo nella direzione opposta. Se siamo scesi, continuiamo in quella direzione. Ripetiamo il processo fino a quando, alla fine, raggiungeremo il fondo.

Come puoi vedere, questa non è necessariamente l’approccio migliore. Potremmo finire in una piccola valle, non in fondo alla montagna, o potremmo impiegare molto tempo su un altopiano.

Questo illustra il principio di base della discesa del gradiente e anche le sue principali sfide. Torniamo a questo esempio, ma vediamo prima una spiegazione più formale.

Cos’è un gradiente?

Un gradiente è una rappresentazione della velocità di cambiamento di una funzione. Indica la direzione dell’aumento o della diminuzione più grande. In modo intuitivo, ciò significa che il gradiente è zero in un massimo locale o in un minimo locale.

Per una funzione che dipende da diverse variabili (o assi di coordinate), il gradiente è un vettore le cui componenti sono le derivate parziali della funzione, valutate in un punto dato. Questo è indicato con il simbolo ∇ (nabla) che rappresenta l’operatore differenziale vettoriale.

Vediamo questo nella notazione matematica. Supponiamo di avere una funzione n-dimensionale f:

Il gradiente di questa funzione nel punto p (che è determinato da n coordinate) è dato da:

Tornando all’esempio della montagna, ci sono zone della montagna in cui il terreno è ripido, come le pendici della montagna, e altre zone in cui il terreno è quasi piatto, come una valle o un altopiano. Le valli e gli altopiani rappresentano minimi locali, che di solito sono punti critici.

Il metodo della discesa del gradiente

Per molti problemi di ottimizzazione, il nostro obiettivo è minimizzare una funzione di perdita per ottenere il risultato più accurato.

Nell’apprendimento profondo e nelle reti neurali artificiali (ANN), le funzioni di perdita che utilizziamo sono differenziabili: non presentano discontinuezze e sono uniformi in tutto il loro dominio.

Ciò ci permette di utilizzare la derivata della funzione di perdita rispetto alle variabili indipendenti come un’indicazione su se stiamo procedendo verso una soluzione (un minimo globale).

Quanto grandi sono i passi che facciamo in proporzione alla derivata? Questo viene determinato da un parametro di dimensione del passo, η (possiamo chiamarlo tasso di apprendimento quando si parla di apprendimento profondo). Esso moltiplica il gradiente, ridimensionandolo per determinare la dimensione del passo.

In questo modo, gradienti più ripidi produrranno passi più grandi. Man mano che ci avviciniamo a un minimo locale, la pendenza (gradiente) tenderà a zero.

Per capire come funziona l’ottimizzazione di una funzione 1D, diamo un’occhiata alla figura seguente, ad esempio:

Esempio semplificato di discesa del gradiente in un problema 1D. Immagine dell'autore.

Come puoi vedere, iniziamo la “ricerca” del minimo da un punto arbitrario (ho rappresentato due esempi, A e B). Prendiamo gradualmente dei passi verso i minimi più vicini, apportando cambiamenti a θ in proporzione alla pendenza.

L’illustrazione rappresenta ciò che fa l’algoritmo seguente (pseudocodice) [1]:

θ(0) = θ_init       # Inizializzazione della variabile di ottimizzazioneη = 0.02            # Parametro di dimensione del passopϵ = 0.01            # Precisione dell'ottimizzazionek = 0               # Contatore delle iterazionifinché |f(θ(k+1) - f(θ(k))| > ϵ: θ(k+1) = θ(k) - η ∇f(θ(k)) k = k + 1

La funzione di perdita qui è simile al nostro esempio della montagna al buio, senza una mappa: non sappiamo com’è fatta. Vogliamo conoscere il valore di θ per cui J è minimizzato, ma l’algoritmo di ottimizzazione non sa quale sarebbe il valore di J per tutti i possibili input θ.

Per questo motivo, iniziamo il nostro algoritmo di ottimizzazione con un valore arbitrario di θ. Ad esempio, i punti A e B nella figura rappresentano due diverse inizializzazioni.

Problemi potenziali con la discesa del gradiente

L’algoritmo della discesa del gradiente è efficace in quanto ci può aiutare a ottenere una soluzione approssimata per qualsiasi funzione convessa.

Se la funzione che stiamo cercando di ottimizzare è convessa, per qualsiasi valore di ϵ esiste un certo valore di dimensione del passo η tale per cui la discesa del gradiente convergerà a θ* entro ϵ dall’ottimo vero θ. [1]

Tuttavia, come potresti immaginare, questo non è perfetto. L’algoritmo potrebbe convergere, ma ciò non garantisce che troveremo un minimo globale.

Le principali sfide della discesa del gradiente sono le seguenti:

L’inizializzazione arbitraria ha un impatto sui risultati

Utilizzando valori di inizializzazione diversi, potremmo trovarci con minimi locali invece di minimi globali.

Ad esempio, iniziando dal punto B invece del punto A nella figura precedente.

O, un caso meno evidente, convergendo verso un plateau (problema del gradiente che scompare) come mostrato dalla linea blu nella figura sottostante.

Discesa del gradiente su una superficie a sella. L'animazione mostra come punti di inizializzazione diversi possano produrre risultati diversi. Immagine dell'autore.

La scelta delle dimensioni del passo adeguate richiede un compromesso tra la velocità di convergenza e la stabilità

C’è una interazione tra la dimensione del passo o il tasso di apprendimento e il numero di epoche che dovremmo utilizzare per ottenere risultati accurati.

Prendi ad esempio i risultati di un esperimento parametrico, mostrati di seguito. Le immagini provengono dal corso online A Deep Understanding of Deep Learning, di Mike X Cohen, che consiglio vivamente a chiunque sia interessato al Deep Learning e all’utilizzo di PyTorch, seguendo un approccio scientifico.

In questo caso, Mike ha mostrato come testare i risultati per un’ottimizzazione della discesa del gradiente quando si cambia il tasso di apprendimento e il numero di epoche di allenamento in modo indipendente (un parametro nel tempo, per una griglia di valori diversi). Possiamo vedere come entrambi i parametri influenzano i risultati per questo caso particolare.

Le immagini provengono dal corso online A Deep Understanding of Deep Learning, di Mike X Cohen.

Il vero minimo globale della funzione si trova intorno a -1,4. Si può vedere che per tassi di apprendimento più piccoli, sono necessarie un maggior numero di epoche di allenamento per convergere a quel risultato. Quindi sembrerebbe che utilizzare un tasso di apprendimento più grande possa aiutarci a ridurre il tempo di calcolo.

Ma nella pratica, non si tratta solo della velocità di convergenza.

Grandi dimensioni del passo possono portare a una convergenza molto lenta, impedire all’algoritmo di convergere del tutto (oscillando intorno ai minimi per sempre) o provocare un comportamento divergente.

La figura successiva mostra come diversi tassi di apprendimento influenzano i risultati dell’ottimizzazione, anche se inizializziamo l’algoritmo nella stessa posizione x = 2.

Esempio di discesa del gradiente stocastico applicato a f(x)=x^2 con diverse dimensioni dei passi. In tutti i casi, l'algoritmo è inizializzato a x = 2. Immagine dell'autore, con base di codice adattata dal notebook di Saraj Rival.

Qui è evidente che avere dimensioni del passo grandi migliora la velocità di convergenza, ma solo fino a un certo punto.

Aumentare il tasso di apprendimento di un ordine di grandezza ha causato il blocco dell’algoritmo. I risultati per η = 1 oscillano tra x = 2 e x = -2, e questo viene mostrato solo dalla linea orizzontale blu nella figura di sinistra.

In alcuni casi, una dimensione del passo grande potrebbe effettivamente “sparare” i risultati all’infinito, causando un overflow numerico del nostro programma.

D’altra parte, dimensioni del passo troppo piccole possono creare una convergenza molto lenta o nessuna convergenza affatto.

Discesa del gradiente per l’allenamento di una Rete Neurale

Nel contesto del Deep Learning, la funzione che stiamo cercando di ottimizzare è la nostra funzione di perdita J. Definiamo le nostre perdite di allenamento come la media delle perdite per tutto il nostro set di dati di allenamento:

Dove Dtrain è il numero di campioni nel nostro set di dati di allenamento.

Così, possiamo implementare la discesa del gradiente basandoci sul seguente algoritmo, in cui calcoliamo il gradiente delle perdite di allenamento rispetto ai pesi per un certo numero di epoche per effettuare un aggiornamento dei pesi del modello [2]

w = [0, ... ,0]    # Inizializza i pesifor k = 1,..., num_epoche:       # Ripeti per il numero desiderato di iterazioni  grad = ∇w TrainLoss(w)         # Gradiente delle perdite di allenamento   w[k] <-- w[k-1] - η * grad     # Aggiorna i pesi del modello

Il problema con la discesa del gradiente come mostrato nel codice pseudocodice sopra è la sua bassa velocità.

Per un esempio giocattolo con pochi punti e una semplice funzione potrebbe funzionare bene, ma immagina che stiamo sviluppando una ANN e abbiamo un milione di punti dati per allenarla.

Se vogliamo aggiornare i pesi del modello utilizzando la discesa del gradiente, dovremmo guardare tutti i campioni dei dati di allenamento, solo per fare un aggiornamento dei pesi del modello. Poi ripeti, ancora e ancora, fino a quando non raggiungiamo la convergenza. Un aggiornamento del peso per epoca.

Per superare questo problema, potremmo utilizzare l’algoritmo della cosiddetta discesa del gradiente stocastico

Discesa del gradiente stocastico

Per superare il problema della lenta convergenza della “vanilla” discesa del gradiente, possiamo eseguire un aggiornamento dei pesi del modello basato su ogni campione dell’insieme di allenamento. Il vantaggio è che non dobbiamo aspettare finché non abbiamo attraversato l’intero set per eseguire un solo aggiornamento dei pesi.

Possiamo aggiornare i pesi più volte per epoca, poiché utilizziamo la funzione di perdita per ogni campione individuale, anziché le perdite di allenamento complete.

Ecco com’è l’algoritmo Discesa del gradiente stocastico (SGD)

w = [0, ... ,0]    # Inizializza i pesi per k = 1,..., num_epochs:       for (x, y) ∈ Dtrain:                 grad = ∇w J(x,y,w)                w[k] <--  w[k-1] - η(k) * grad 

Si noti che la dimensione del passo è una funzione delle iterazioni di allenamento. Ciò perché, affinché l’algoritmo converga, η deve diminuire man mano che il numero di iterazioni progredisce. Nelle dispense delle conferenze del MIT 6.036 [1], viene menzionato il seguente teorema:

Teorema 4.1. Se J è convessa e η(t) è una sequenza che soddisfa

Allora SGD converge con probabilità uno all’ottimo θ.

Le persone adottano approcci diversi per ridurre il valore di η durante l’avanzamento dell’allenamento, e questo viene spesso chiamato “Annealing”:

  • Cambiare il tasso di apprendimento in proporzione all’epoca di allenamento (ad esempio, η(t) = 1/t), o impostarlo su un valore più basso una volta raggiunta una certa epoca di apprendimento. Questo metodo offre buoni risultati, ma non è correlato alle prestazioni del modello. Questo viene chiamato “decadimento del tasso di apprendimento” ed è lo standard industriale per gestire questo problema.
  • Moltiplicando il tasso di apprendimento per il gradiente della funzione di perdita: Questo metodo è buono perché si adatta al problema, ma richiede una scalatura accurata. Questo è incorporato nelle varianti RMSprop e Adam della discesa del gradiente.
  • Moltiplicando il tasso di apprendimento per le perdite: Il vantaggio è che è anche adattativo al problema, ma richiede anche una scalatura.

SGD può funzionare bene dopo aver visitato solo alcuni dei dati. Questo comportamento può essere utile per set di dati relativamente grandi, perché possiamo ridurre la quantità di memoria necessaria e il tempo totale di esecuzione rispetto all’implementazione “vanilla” della discesa del gradiente.

Potremmo dire che l’implementazione “vanilla” è più lenta perché deve attraversare l’intero “lotto” di campioni per eseguire un singolo aggiornamento dei pesi. SGD esegue un aggiornamento su ogni campione, ma la qualità degli aggiornamenti è inferiore. Potremmo avere dati rumorosi o una funzione davvero complessa che stiamo cercando di adattare con la nostra ANN. È come usare un batch di dimensione Dtrain è lento, ma preciso, e usare un batch di dimensione 1 è veloce, ma un po’ meno preciso.

C’è un termine intermedio, ed è chiamato discesa del gradiente “mini-batch”.

Discesa del gradiente mini-batch

Foto di Sebastian Herrmann su Unsplash

Se suddividiamo i nostri dati in batch più piccoli di dimensioni uguali, potremmo eseguire la discesa del gradiente “vanilla” per ciascuno di quei mini-batch.

Diciamo che dividiamo i dati in 100 parti più piccole. Potremmo attraversare i nostri dati in 100 passaggi. Ad ogni passaggio, guardiamo le perdite di allenamento solo per i dati contenuti nel mini-batch corrente e miglioriamo i parametri del nostro modello. Ripetiamo questo fino a quando iniziamo il ciclo di nuovo. Ogni ciclo è conosciuto come epoca, anche se ho usato il termine in modo più vago in precedenza solo per riferirmi al numero di iterazioni durante l’ottimizzazione.

“`html

Il vantaggio di ciò è che aggiorniamo i parametri del nostro modello su ogni mini-batch, piuttosto che dopo aver esaminato l’intero set di dati. L’approccio che ho definito come “vanilla” gradiente discendente, è anche conosciuto come gradiente discendente a lotti, dove la dimensione del lotto è il numero totale di campioni nel set di dati di training. Per il gradiente discendente a mini-batch, i mini-batch sono solitamente potenze di due: 32 campioni, 64, 128, 256, e così via.

SGD sarebbe un caso estremo quando la dimensione del mini-batch viene ridotta a un singolo esempio nel set di dati di allenamento.

Lo svantaggio nell’uso del gradiente discendente a mini-batch nel nostro processo di ottimizzazione è che incorporiamo un livello di variabilità. Non è garantito che ogni passo ci avvicini ai valori dei parametri ideali, ma la direzione generale è comunque verso il minimo.

Questo metodo è uno degli standard dell’industria perché, trovando la dimensione ottimale del lotto, possiamo scegliere un compromesso tra velocità e precisione quando si lavora con set di dati molto grandi.

Grazie per la lettura! Spero che questo articolo sia stato interessante e ti abbia aiutato a chiarire alcuni concetti. Condivido anche le fonti che ho utilizzato durante la stesura di questo articolo, nel caso in cui tu sia interessato/a ad approfondire con materiale più approfondito e formale.

In un futuro articolo, scriverò su metodi più avanzati di gradiente discendente (quelli utilizzati nelle applicazioni reali) e su come effettivamente aggiorniamo i pesi del modello durante l’allenamento, utilizzando la retropropagazione, poiché il gradiente discendente è solo una parte del quadro completo.

Nel frattempo, potresti essere interessato/a a leggere il mio articolo precedente, sugli Artificial Neural Networks feedforward:

Artificial Neural Networks feedforward

Il concetto di base, spiegato

VoAGI.com

Riferimenti

[1] MIT Open Learning Library: 6.036 Introduzione all’Apprendimento Automatico. Capitolo 6: Gradiente Discendente

[2] Standford Online: Intelligenza Artificiale e Apprendimento Automatico 4 — Gradiente Discendente Stocastico | Stanford CS221 (2021)

[3] Corso online Una Profonda Comprensione del Deep Learning, di Mike X Cohen ( sincxpress.com)

[4] Standford Online: CS231 Reti Neurali Convoluzionali per il Riconoscimento Visivo

Pubblicato originariamente su https://www.makerluis.com il 4 novembre 2023.

“`