Deep Learning
Introduzione
Il deep learning è una branca del machine learning. Esso si basa sull’idea che anziché costruire modelli di machine learning ad hoc per risolvere determinate classi di problemi, sia possibile costruire modelli matematici generici che siano in grado di apprendere a partire da soli input numerici, senza una pre-elaborazione dei dati da parte del data scientist.
Si chiama deep learning perché nei modelli più complessi, ovvero le reti neurali profonde, le memorie di questi modelli hanno parecchi strati nascosti che contengono diversi livelli di rappresentazione della conoscenza appresa.
In questa lezione vedremo come funziona il percettrone, il modello base su cui si sono evolute le reti neurali.
Percettrone
I neuroni biologici sono cellule nervose interconnesse presenti nel cervello e coinvolte nell’elaborazione e trasmissione di segnali chimici ed elettrici. Il neurone in base alla propria “memoria interna” e in base ai segnali che riceve emette a sua volta un segnale di attivazione che viene trasmesso ad altri neuroni. La memoria dei neuroni si addestra tramite l’esperienza (i segnali di input ed il feedback dal segnale di output) e permette di controllare l’apparato sensoriale, motorio e la memoria stessa nel corpo umano (e degli animali).
A partire da queste considerazioni è stato sviluppato quindi un modello matematico/computazionale che vuole simulare questo comportamento (“pensare umanamente”).
Un percettrone o neurone digitale è un componente software che riceve in input un vettore ed emette come output un valore. Si tratta del primo esempio in assoluto di macchina ad apprendimento automatico (ideatam nel 1958 da Rosenblatt). E’ il punto di partenza di tutto il deep learning, le reti neurali e la IA come la conosciamo oggi.
Il percettrone è chiamato neurone digitale perché con questo modello matematico è nato con l’intento originale di emulare un neurone biologico, con alcune analogie con le sinapsi (i pesi V), i dendriti (il vettore X di input), e l’assone (l’attivazione), anche se con un modello digitale: l’output è strettamente binario e digitale. Va quindi visto solo come una approssimazione ad alto livello.
Vediamo come funziona. Il percettrone è una applicazione software dotata di un input, un output, una memoria interna ed un algoritmo che funziona in questo modo:
- la memoria interna è di norma un vettore V (ma può essere una matrice), i cui valori sono detti pesi o parametri. E’ cioè V = [v1,v2,…vn]. E’ inoltre presente un valore b, detto bias.
- Questi parametri sono utilizzati da una funzione di trasformazione che applica ad un input numerico, ovvero un vettore X di pari dimensione n, un calcolo che coinvolge i valori del vettore dei pesi V ed il bias, e produce un valore scalare Essa ha quindi la forma T(X,V,b) -> y
Un esempio di trasformazione potrebbe essere x0v0+x1v1+…+b - Il risultato di questo calcolo viene passato ad una funzione, detta di attivazione, A(y) che restituisce un valore che può essere 0 o 1. Vi sono diverse funzioni di attivazione1 la più semplice è quella a scalino:
- In altri termini avremo una funzione complessiva A(T(X,V,b)) -> 0 | 1
Graficamente:

A cosa serve il percettrone? Sostanzialmente a due scopi:
- riprodurre le funzioni logiche di base: AND, OR, NOT
- classificazione lineare:
- identificazione di un punto sopra o sotto una certa retta (regressione lineare)
- classificazione di un insieme di dati (diagnostica di un problema, approvazione di un documento, analisi di una recensione, ecc.)
Vediamo due esempi pratici:
Porta logica
Facciamo un esempio pratico. Poniamo di voler simulare un AND Logico tra due valori A e B.
Un percettrone valido potrebbe essere questo: V = [1,1] e b=-1,5. La funzione T è:
La funzione di attivazione è quella già vista sopra:
Il percettrone accetta vettori x di numeri binari. Facciamo un test sui 4 valori:
| Input | T(X,V,b) | A(y) |
|---|---|---|
| [0,0] | -1.5 | 0 |
| [0,1] | -0.5 | 0 |
| [1,0] | -0.5 | 0 |
| [1,1] | +0.5 | 1 |
Classificatore lineare
Ipotizziamo ora di voler classificare un punto in base alla sua posizione rispetto ad una retta. Ipotizziamo che la retta sia:
E vogliamo ottenere qualcosa come questo:

Un percettrone valido potrebbe essere questo: V = [-2,1] e b=+1. La funzione T è:
La funzione di attivazione è quella già vista sopra:
Se facciamo un test su qualche punto di esempio abbiamo:
| X | T(X,V,b) | A(y) |
|---|---|---|
| [2,5] | 2*-2+5*1+1=2 | 1 |
| [3,1] | 3*-2+1*1+1=-4 | 0 |
| [0,2] | 0*-2+2*1+1=3 | 1 |
Addestramento tramite discesa del gradiente
Il percettrone per funzionare, deve ricevere un addestramento supervisionato come in altri modelli di Machine Learning. L’obiettivo è quello di trovare in modo automatico i parametri V e b corretti, sulla base di un dataset di esempi composti da input ed output noti. Una volta individuati, è in grado di rispondere automaticamente agli input con un output corretto.
Il set di esempi conosciuto deve essere sufficientemente ampio, ed inoltre serve un algoritmo di addestramento che consenta di calcolare i pesi corretti del vettore V insieme al valore del bias b, in modo che il modello possa “imparare”.
L’algoritmo è il seguente:
1. Inizialmente si sceglie un insieme di pesi e bias casuale.
2. Ad ogni iterazione si ha un vettore Xk (k è il k-esimo esempio) e relativo valore y’ di output atteso. Si passano i dati di input al vettore V e si calcola quindi un valore y calcolato. Ipotizziamo ad esempio che la funzione T sia come quella vista sopra:
3. Il risultato generato sarà diverso dal risultato atteso. L’addestramento consiste nel calcolare l’errore, e nel modificare opportunamente i pesi vi ed il bias b in modo tale che ad un nuovo ricalcolo l’output yk si avvicini al risultato atteso y’k. Occorre quindi quantificare questo errore. Per farlo si usa una funzione di costo dell’errore, che dipende dallo scostamento tra il valore atteso (y’k) e il valore calcolato (yk). Una funzione molto efficace è la seguente:
Il costo è proporzionale al quadrato dell’errore, sia per evitare che errori positivi e negativi si annullino, sia perchè errori maggiori aumentino il costo dello scostamento. Essa ha una forma di parabola, e ci aiuta a comprendere che il nostro obiettivo è minimizzare il costo dell’errore, cioè trovare il valore minimo della parabola.2

Per trovare il mimimo di una funzione è sufficiente calcolare la derivata, o gradiente, che ci da la pendenza. Rispetto al singolo peso e per ogni esempio k è (svolgendo la derivata della parabola):
Il valore della derivata è quello che va portato a 0. Ovvero per quei valori dei pesi v di V per cui la derivata vale 0 sono quelli che sono in grado di risolvere il problema. La derivata ci dice la direzione verso cui ridurre l’errore, la pendenza cioè per un certo valore di errore. L’obiettivo dell’algoritmo di aggiustamento dei pesi quindi coincide con la riduzione della derivata, ovvero la discesa del gradiente.
4. Con la pendenza, possiamo calcolare di quanto dobbiamo modificare ogni peso:
dove m è un numero che indica la velocità di apprendimento, che non deve essere troppo grande (perché ad ogni iterazione il peso deve cambiare di poco, se troppo grande il peso si sposta troppo e supera il valore minimo, cominciando ad oscillare) nè troppo piccola (l’algoritmo diventa troppo lento). In altri termini: la correzione sarà tanto più veloce quanto sarà grande l’errore, e tanto più piccola mano a mano che ci avviciniamo al valore desiderato.
Aggiorniamo quindi il peso applicando il delta:
4. Si rieffettua questa operazione per tutti gli N esempi, aggiornando ogni volta i pesi.
5. Si ripete di nuovo da capo per un certo numero di volte, dette epoche.
In pratica, il vettore V aggiusta i propri pesi per adattarsi ai dati, anzi ai singoli esempi di dati, cercando di memorizzare una “forma” che sia in grado di dare una risposta che minimizza l’errore.
E’ importante capire che tutti i pesi, cioè tutti gli elementi del vettore V, sono aggiornati ad ogni iterazione. Questo significa che si hanno tanti aggiustamenti indipendenti per soddisfare input differenti, e quindi questa operazione può essere pensata un po’ come lo stendere un lenzuolo spiegazzato su un letto: se si tira troppo da una parte, si deve poi aggiustare dalla parte opposta, poi si procede sempre più di fino fino ad avere un lenzuolo ben steso.
Esempi di addestramento
Porta logica AND
Per capire bene come funziona vediamo un esempio pratico, l’addestramento di un neurone che simula una porta logica AND, cioè quella dell’esempio sopra riportato.
esempi:
| k | x1 | x2 | y’ |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 1 | 0 | 1 | 0 |
| 2 | 1 | 0 | 0 |
| 3 | 1 | 1 | 1 |
Poniamo poi V iniziale = [0.1, 0.2] bias b = 0.1 e m = 0.1
Ora addestriamo.
Epoca 1
Pesi di partenza: v1 = -0.1, v2 = 0.2, b = 0.1
| Step | Input (x1, x2) | Somma (v1·x1 + v2·x2 + b) | Output y | Target y’ | Errore (y’-y) | Δv1, Δv2, Δb | Nuovi Pesi (v1, v2, b) |
|---|---|---|---|---|---|---|---|
| 1 | (0, 0) | 0.1 | 1 | 0 | -1 | (0.0, 0.0, -0.1) | (-0.1, 0.2, 0.0) |
| 2 | (0, 1) | 0.2 | 1 | 0 | -1 | (0.0, -0.1, -0.1) | (-0.1, 0.1, -0.1) |
| 3 | (1, 0) | -0.2 | 0 | 0 | 0 | (0.0, 0.0, 0.0) | (-0.1, 0.1, -0.1) |
| 4 | (1, 1) | -0.1 | 0 | 1 | +1 | (+0.1, +0.1, +0.1) | (0.0, 0.2, 0.0) |
Epoca 2
Pesi di partenza: v1 = 0.0, v2 = 0.2, b = 0.0
| Step | Input (x1, x2) | Somma (v1·x1 + v2·x2 + b) | Output y | Target y’ | Errore (y’-y) | Δv1, Δv2, Δb | Nuovi Pesi (v1, v2, b) |
|---|---|---|---|---|---|---|---|
| 1 | (0, 0) | 0.0 | 0 | 0 | 0 | (0.0, 0.0, 0.0) | (0.0, 0.2, 0.0) |
| 2 | (0, 1) | 0.2 | 1 | 0 | -1 | (0.0, -0.1, -0.1) | (0.0, 0.1, -0.1) |
| 3 | (1, 0) | -0.1 | 0 | 0 | 0 | (0.0, 0.0, 0.0) | (0.0, 0.1, -0.1) |
| 4 | (1, 1) | 0.0 | 0 | 1 | +1 | (+0.1, +0.1, +0.1) | (0.1, 0.2, 0.0) |
Epoca 3
Pesi di partenza: v1 = 0.1, v2 = 0.2, b = 0.0
| Step | Input (x1, x2) | Somma (v1·x1 + v2·x2 + b) | Output y | Target y’ | Errore (y’-y) | Δv1, Δv2, Δb | Nuovi Pesi (v1, v2, b) |
|---|---|---|---|---|---|---|---|
| 1 | (0, 0) | 0.0 | 0 | 0 | 0 | (0.0, 0.0, 0.0) | (0.1, 0.2, 0.0) |
| 2 | (0, 1) | 0.2 | 1 | 0 | -1 | (0.0, -0.1, -0.1) | (0.1, 0.1, -0.1) |
| 3 | (1, 0) | 0.0 | 0 | 0 | 0 | (0.0, 0.0, 0.0) | (0.1, 0.1, -0.1) |
| 4 | (1, 1) | 0.1 | 1 | 1 | 0 | (0.0, 0.0, 0.0) | (0.1, 0.1, -0.1) |
Epoca 4 (Verifica Finale)
Pesi di partenza: v1 = 0.1, v2 = 0.1, b = -0.1
| Step | Input (x1, x2) | Somma (v1·x1 + v2·x2 + b) | Output y | Target y’ | Errore (y’-y) | Esito |
|---|---|---|---|---|---|---|
| 1 | (0, 0) | -0.1 | 0 | 0 | 0 | OK |
| 2 | (0, 1) | 0.0 | 0 | 0 | 0 | OK |
| 3 | (1, 0) | 0.0 | 0 | 0 | 0 | OK |
| 4 | (1, 1) | +0.1 | 1 | 1 | 0 | OK |
Ovviamente si tratta di un esempio molto semplice, ma ci consente di capire a fondo come funziona la discesa del gradiente.
Classificazione lineare
Vediamolo ora per il secondo esempio, la retta f(x) = 2m-1. Il vettore di esempi sarà:
| k | x1 | x2 | y’ |
|---|---|---|---|
| 0 | 0 | 2 | 1 |
| 1 | 1 | 3 | 1 |
| 2 | 2 | 2 | 0 |
| 3 | 3 | 0 | 0 |
Poniamo V iniziale = [0.1, -0.1] bias b = 0.1 e m = 0.1
Epoca 1
Pesi di partenza: v1 = 0.1, v2 = -0.1, b = 0.1
| Step | Input (x1, x2) | Somma (v1·x1 + v2·x2 + b) | Output y | Target y’ | Errore (y’-y) | Δv1, Δv2, Δb | Nuovi Pesi (v1, v2, b) |
|---|---|---|---|---|---|---|---|
| 1 | (0, 2) | -0.1 | 0 | 1 | +1 | (0.0, +0.2, +0.1) | (0.1, 0.1, 0.2) |
| 2 | (1, 3) | 0.6 | 1 | 1 | 0 | (0.0, 0.0, 0.0) | (0.1, 0.1, 0.2) |
| 3 | (2, 2) | 0.6 | 1 | 0 | -1 | (-0.2, -0.2, -0.1) | (-0.1, -0.1, 0.1) |
| 4 | (3, 0) | -0.2 | 0 | 0 | 0 | (0.0, 0.0, 0.0) | (-0.1, -0.1, 0.1) |
Epoca 2
Pesi di partenza: v1 = -0.1, v2 = -0.1, b = 0.1
| Step | Input (x1, x2) | Somma (v1·x1 + v2·x2 + b) | Output y | Target y’ | Errore (y’-y) | Δv1, Δv2, Δb | Nuovi Pesi (v1, v2, b) |
|---|---|---|---|---|---|---|---|
| 1 | (0, 2) | -0.1 | 0 | 1 | +1 | (0.0, +0.2, +0.1) | (-0.1, 0.1, 0.2) |
| 2 | (1, 3) | 0.4 | 1 | 1 | 0 | (0.0, 0.0, 0.0) | (-0.1, 0.1, 0.2) |
| 3 | (2, 2) | 0.2 | 1 | 0 | -1 | (-0.2, -0.2, -0.1) | (-0.3, -0.1, 0.1) |
| 4 | (3, 0) | -0.8 | 0 | 0 | 0 | (0.0, 0.0, 0.0) | (-0.3, -0.1, 0.1) |
Epoca 3
Pesi di partenza: v1 = -0.3, v2 = -0.1, b = 0.1
| Step | Input (x1, x2) | Somma (v1·x1 + v2·x2 + b) | Output y | Target y’ | Errore (y’-y) | Δv1, Δv2, Δb | Nuovi Pesi (v1, v2, b) |
|---|---|---|---|---|---|---|---|
| 1 | (0, 2) | -0.1 | 0 | 1 | +1 | (0.0, +0.2, +0.1) | (-0.3, 0.1, 0.2) |
| 2 | (1, 3) | 0.2 | 1 | 1 | 0 | (0.0, 0.0, 0.0) | (-0.3, 0.1, 0.2) |
| 3 | (2, 2) | -0.2 | 0 | 0 | 0 | (0.0, 0.0, 0.0) | (-0.3, 0.1, 0.2) |
| 4 | (3, 0) | -0.7 | 0 | 0 | 0 | (0.0, 0.0, 0.0) | (-0.3, 0.1, 0.2) |
Epoca 4 (Verifica Finale)
Pesi di partenza: v1 = -0.3, v2 = 0.1, b = 0.2
| Step | Input (x1, x2) | Somma (v1·x1 + v2·x2 + b) | Output y | Target y’ | Errore (y’-y) | Esito |
|---|---|---|---|---|---|---|
| 1 | (0, 2) | 0.4 | 1 | 1 | 0 | OK |
| 2 | (1, 3) | 0.2 | 1 | 1 | 0 | OK |
| 3 | (2, 2) | -0.2 | 0 | 0 | 0 | OK |
| 4 | (3, 0) | -0.7 | 0 | 0 | 0 | OK |
Conclusioni
Il percettrone, ideato da Rosenblatt nel 1958, ed evoluto nel neurone ADALINE, è il punto di partenza di tutta la linea evolutiva delle reti neurali. Esso è il primo modello che ha introdotto l’idea che la macchina possa imparare correggendo automaticamente la propria configurazione in base agli errori commessi.
L’algoritmo di discesa del gradiente, un algoritmo del 1960 elaborato da Widrow e Hoff, pensato in un’epoca in cui non esisteva la potenza di calcolo per risolvere problemi molto più complessi di una classificazione lineare, è stato ed è tutt’oggi una pietra miliare dell’informatica, insieme all’algoritmo di retropropagazione dell’errore, che si usa nelle reti neurali.
Sono stati necessari 50 anni perché le conseguenze di questo algoritmo avessero delle applicazioni realmente usabili nel mondo reale, grazie all’incremento della potenza di calcolo dei computer.
- Le funzioni di attivazione possibili sono molte, tra queste RELU e Sigmoid, in ogni caso il loro output è sempre tra 0 e 1 ↩︎
- questa procedura è stata introdotta con il percettrone ADALINE (Adaptive Linear Neuron) introdotto da Widrow e Hoff nel 1960, e che esegue gli aggiustamenti non dopo aver eseguito la funzione di attivazione. Il neurone di Rosenblatt, invece, applicava l’aggiustamento solo dopo aver eseguito anche la funzione di attivazione, con un procedimento enormemente meno efficiente, perché la funzione di costo non era lineare. ↩︎
