Se quando si parla del concetto di backpropagation e gradient descent ti sembra di non avere le idee chiare, sei nel posto giusto. In questo articolo partiremo innanzitutto con una breve spiegazione di cosa sono e a cosa servono, per poi passare ad un esempio di backpropagation e gradient descent con tanto di calcoli che spero chiarirà una volta per tutte eventuali dubbi.
Backpropagation e Gradient Descent
La caratteristica fondamentale di una rete neurale è rappresentata dalla sua capacità di apprendimento. Ciò è reso possibile da un’ottimizzazione continua dei pesi che la compongono. Questo tipo di ottimizzazioni si realizza con l’applicazione dell’algoritmo del gradient descent il quale, essendo basato sul calcolo della derivata, permette di aggiustare i pesi in modo da ridurre l’errore di predizione.
Nelle reti particolarmente grandi non è facile poter calcolare in forma analitica il gradiente della funzione di costo che descrive la rete, ecco quindi la necessità di algoritmi numerici.
La backpropagation è proprio un algoritmo numerico per il calcolo del gradiente di reti feedforward. Si basa sulla chain rule, tale per cui sappiamo che per calcolare la derivata di una funzione composta è possibile suddividere il calcolo in più step:
\frac{d}{dx}[f(g(x))] = \frac{df}{dg} \cdot \frac{dg}{dx}
Dunque la derivata di una funzione composta, anche se complessa, può essere calcolata come una composizione di derivate parziali. Ciò lo si fa, come vedremo, partendo dall’uscita della rete e proseguendo a ritroso fino al layer di input.
Una Semplice Rete Neurale
Non c’è nulla di meglio per chiarirsi le idee che partire da un esempio.
Immaginiamo quindi di avere una rete neurale composta da un layer di input, un layer hidden e un layer di output.

Per motivi di semplicità, la rete è stata appositamente ridotta all’osso con 3 livelli, ciascuno composto da un nodo soltanto. Ogni nodo ha una sua funzione di attivazione \phi(.) e un peso w che lega i nodi ad esso adiacenti.
Immaginiamo di inizializzare questi pesi nel seguente modo:
\begin{bmatrix}w_{i,h} = 0.15 \\w_{h,o} = 0.3\end{bmatrix}
Affinchè una rete neurale possa imparare anche in situazioni complesse, è necessario definire le funzioni di attivazione di ogni neurone in modo che siano funzioni non lineari. Solitamente la sigmoide, che abbiamo ad esempio già incontrato nell’articolo sulla Logistic Regression, è una buona scelta:
\phi(x) = \frac{1}{1+e^{-x}}
In questo esempio, per semplificare al massimo i calcoli e poterci concentrare unicamente sull’algoritmo in sè, sceglieremo una funzione di attivazione lineare così fatta:
\phi(x) = x
Adesso, immaginiamo di voler addestrare la rete in modo che inverta il parametro in ingresso, ovvero:
X = 1 \rightarrow Y = -1
L’idea finale sarebbe imparare ad invertire qualsiasi numero:
f(x) = -1 \cdot x
Calcoliamo l’Output della Rete
Definita la morfologia e i parametri iniziali, è ora di calcolare la così detta forward propagation, ovvero partendo dall’input X, seguiamo il flusso della rete per ottenere l’output Y.
Il suddetto calcolo è molto semplice da eseguire. Da sinistra verso destra abbiamo:
\phi(a_i) = X
\phi(a_h) = \phi(a_i) \cdot w_{i,h}
\phi(a_o) = \phi(a_h) \cdot w_{h,o}
Se adesso sostituiamo i valori dei pesi definiti in precedenza, ci accorgiamo velocemente che la rete determina un risultato molto diverso da quello voluto:
\phi(a_i) = X = 1
\phi(a_h) = \phi(a_i) \cdot w_{i,h} = 1 \cdot 0.15 = 0.15
\phi(a_o) = \phi(a_h) \cdot w_{h,o} = 0.15 \cdot 0.3 = 0.045
Guardando al valore di \phi(a_o), risulta chiaro come siamo ben lontani dal restituire l’output corretto (o.045 invece di -1). E’ necessario quindi ottimizzare i pesi.
Esempio Analitico di Backpropagation
Quando si parla di ottimizzazione, come abbiamo visto, la backprop ci viene in aiuto. Per prima cosa dobbiamo trovare una rappresentazione dell’errore che sia rigorosa.
A tal proposito, in questo esempio, faremo uso dell’errore quadratico medio o MSE:
E = \frac{1}{2} \left ( \phi (a_o) - Y\right )^{2}
Ovviamente esistono varie funzioni di costo per determinare l’errore di predizione. In questo esempio facciamo uso dell’errore quadratico medio che è comunque uno strumento utilizzato abbastanza di frequente.
L’errore quadratico meglio calcola il quadrato della distanza tra la nostra uscita desiderata Y e l’effettivo risultato prodotto dalla rete \phi(a_o).
La costante a moltiplicare è stata aggiunta solo perchè ci tornerà utile durante il calcolo della derivata che faremo a breve.
Avendo ottenuto una funzione che ci dice quanto la rete sta sbagliando, possiamo passare ad ottimizzarla. Ciò che vogliamo fare è capire come i pesi w_{i,h} e w_{h,o} interferiscano nel risultato finale, in modo da cambiarne il valore per minimizzare l’errore totale finale.
Ciò che facciamo è propagare l’errore della rete da destra verso sinistra, cercando di capire come modificarne i parametri per ottimizzarne il comportamento.
In termini matematici, vogliamo calcolare:
\frac{\delta E}{\delta w_{i,h}} e \frac{\delta E}{\delta w_{h,o}}
Per calcolare queste due quantità, interviene a questo punto la chain rule:
\frac{\delta E}{\delta w_{i,h}} = \frac{\delta E}{\delta \phi(a_o)} \cdot \frac{\delta \phi(a_o)}{\delta \phi(a_h)} \cdot \frac{\delta \phi(a_h)}{\delta w_{i,h}} = (\phi(a_o) - Y) \cdot w_{h,o} \cdot X
\frac{\delta E}{\delta w_{h,o}} = \frac{\delta E}{\delta \phi(a_o)} \cdot \frac{\delta \phi(a_o)}{\delta w_{h,o}} = (\phi(a_o) - Y) \cdot \phi(a_h)
Queste due quantità, che altro non sono che il gradiente della funzione di costo, sono fondamentali per l’aggiornamento dei pesi:
w_{i,h}^+ = w_{i,h} - \eta \cdot \frac{\delta E}{\delta w_{i,h}}
w_{i,o}^+ = w_{i,o} - \eta \cdot \frac{\delta E}{\delta w_{i,o}}
Quello che facciamo non è altro che andare a sottrarre ai pesi originari la derivata della funzione di costo rispetto al peso in questione. In questo modo riusciamo ad ottenere un nuovo valore del peso che ridurrà l’errore in uscita della rete.
Il parametro \eta è la cosìdetta learning rate ed è un valore che determina quanto vogliamo dare peso al gradiente. Più è alta, più l’aggiornamento dei pesi sarà rilevante.
Esempio di Backpropagation e Gradient Descent in Python
Ottimo, adesso che abbiamo tutte le formule che ci servono, passiamo a fare qualche calcolo numerico.
L’apprendimento della rete si divide per epoche. In ciascuna epoca:
- calcoliamo il valore di output \phi(a_o) della rete;
- si determinano i nuovi valori dei pesi w_{i,h}^+, w_{h,o}^+;
- si ricomincia dallo step 1.
Aiutiamoci con Python:
Lanciando lo script vedremo un output di questo tipo:
Conclusioni
In questo articolo abbiamo discusso un esempio di backpropagation e gradient descent.
Come possiamo vedere dall’output del nostro script Python, la nostra rete ha effettivamente imparato a invertire l’input iniziale. Non solo, siamo addirittura riusciti a generalizzare tant’è che nelle ultime 2 istruzioni inverte correttamente sia 0.4 che -200.
L’algoritmo della backpropagation insieme al gradient descent, ci permette di ottimizzare i pesi della rete e di realizzare un processo di apprendimento. Al termine di quest’ultimo, i pesi della rete saranno stati ottimizzati in modo da ridurre l’errore della funzione di costo.
Scrivi un commento