Reti neurali
Un po’ di storia
Il percettrone è uno strumento per certi versi rivoluzionario: si tratta della prima macchina che è in grado di risolvere un problema imparando dai propri errori. Si distingue rispetto all’algoritmo tradizionale perché calcola il risultato sulla base della propria modellizzazione: non ha logica interna, esegue una semplice trasformazione lineare da A a B. Ma si distingue anche dal machine learning perché non serve pre analizzare i dati, come si fa con i classificatori bayesiani o con KNN.
Esso, con la sua struttura, però è in grado di dare una risposta solo ad input rappresentabili in insiemi linearmente separati. Ovvero, se rappresentiamo tutti gli input su un piano, o uno spazio, o uno spazio a n dimensioni, essi devono essere classificabili sempre da una retta, o piano o spazio a n-1 dimensioni. Inoltre il neurone è davvero limitato perché risolve problemi semplici di classificazione, 0 o 1. La realtà invece è molto più complessa e richiede di costruire risposte articolate e complesse, non semplici classificazioni.
Per questo a partire dagli anni 60 viene introdotta come soluzione la rete neurale, ovvero un insieme di neuroni organizzati a strati, dove l’output di uno strato è l’input dello strato successivo. La rete neurale consente di memorizzare pesi su più livelli di conoscenza e non solo memorizzare molta più informazione, ma avere una maggiore capacità di adattamento a problemi complessi. Ogni livello interno, infatti, memorizza una parte del modello, scomponendo un problema complesso in più problemi più semplici. Questo permette di di individuare correlazioni non immediate tra i dati di input e generare output in cui classificazioni e previsioni possono essere anche linearmente scomponibili, oltre ad essere in grado di dare una risposta su insiemi di dati più ampi.

La rete neurale è alla base di un nuovo tipo di Machine Learning, chiamato Deep Learning, ed ha le seguenti caratteristiche:
- rientra nella categoria del “pensare umanamente” (i modelli di Machine Learning rientrano spesso nell’agire umanamente);
- I modelli per essere usati, vanno addestrati, ma non viene prevista, a differenza del Machine Learning, una fase iniziale di predisposizione dei dati: essi vengono semplicemente codificati in numeri e passati alla rete neurale
- prevede apprendimento supervisionato, ma i modelli più avanzati hanno apprendimento non supervisionato o con rinforzo;
Negli anni ’80 viene progettato infatti l’algoritmo di retropropagazione dell’errore, che consente di addestrare l’intera rete neurale in modo efficace da ogni singolo esempio. Negli anni ’90 poi vengono inventate reti neurali ancora più complesse, come le convoluzionali (CNN) e le temporalizzate (RNN): queste reti memorizzano concetti astratti in forma numerica, e ricordano le passate iterazioni, creando un contesto di lavoro.
Resta però un limite fondamentale. le reti neurali, almeno fino al 2000, restano quasi solo un esercizio teorico. Il problema è di velocità di calcolo: come abbiamo già visto solo con un percettrone occorrono 16 esempi per creare una porta AND, un semplice riconoscitore di testo richiede milioni di calcoli, una immagine miliardi. La vera intelligenza artificiale era il machine learning classico (KNN, regressioni, Bayes, ecc.).
Finalmente gli anni 2000 che cominciano ad essere realizzate reti neurali in grado di risolvere problemi di maggiore complessità, ma è solo dal 2012, grazie alle GPU, processori speciali progettati per i videogiochi, che si arriva a superare in qualità i classificatori bayesiani. Il Deep Learning ha smesso di essere un esercizio teorico, ed è diventata una tecnologia reale, con utilità pratica, attraendo investimenti enormi e facendo fare passi da gigante a quella che ha smesso di essere una teoria matematica ed è diventata una tecnologia.
L’ultima grande invenzione sono i Transformer (2017), una evoluzione di CNN e RNN, che sono alla base dei moderni LLM (2022 – Large Language Models) in grado di memorizzare enormi quantità di informazione di qualsiasi forma, e rispondere a domande su qualsiasi argomento.
In questa lezione ci concentreremo sul principio di funzionamento delle reti neurali, approfondendo altri argomenti in lezioni successive.
Funzionamento
Le reti neurali sono normalmente disposte su più strati (detti layer) per permettere una più forte interazione tra loro e memorizzare le correlazioni su più di un livello, in modo da gestire applicazioni di grande complessità. La rete neurale agisce come una memoria che salva al suo interno relazioni anche molto complesse tra i dati di input in modo tale da riuscire a restituire un output in grado di gestire anche dati di input strutturati o destrutturati. Ogni layer è rappresentato da un vettore o matrice multidimensionale, chiamati anche tensori (dove la loro dimensione è chiamata rango);
La geometria della rete indica la sua struttura complessiva:
- il livello di input: come nel percettrone questo livello ha la stessa struttura del dato di input (ad esempio un tensore di rango 1 e lunghezza N);
- quanti sono i layer profondi;
- come è strutturato il tensore di ogni layer (rango e dimensione);
- il livello di output, ovvero rango e dimensione del tensore di output.
- è sempre presente una funzione di attivazione finale, che però è una funzione continua, come il sigmoid o RELU , perché deve essere derivabile.
Qui un esempio di rete:

Il funzionamento estende il concetto di percettrone. Il vettore di input viene trasformato dal primo strato, che lo passa al secondo (se c’è aumento di dimensioni viene passato a tutte le dimensioni, altrimenti viene compresso in minor numero di dimensioni), che a sua volta viene trasformato coi pesi del secondo layer al terzo, e così via, fino allo strato finale. Infine viene eseguita la funzione di attivazione e viene emesso l’output.
Il risultato della trasformazione è quindi una funzione composta, che produce un risultato che dipende da tutti i layers:
Addestramento
L’addestramento è concettualmente simile a quello del percettrone:
1) La rete viene inizializzata con pesi casuali.
2) Il dataset di apprendimento viene suddiviso in esempi (o gruppi di esempi) che vengono sottoposti alla rete (con il valore atteso) uno alla volta.
3) Ad ogni iterazione i pesi di ogni layer della rete vengono aggiornati e si procede con l’esempio successivo. Gli esempi possono essere migliaia o anche milioni. L’operazione di aggiornamento dei pesi segue un algoritmo di retropropagazione, che si basa a sua volta sulla discesa del gradiente.
3) Quando tutti gli esempi sono processati, si conclude un ciclo di addestramento, detto “epoca“.
4) Si ripete l’addestramento per un certo numero significativo di epoche (che possono essere migliaia o milioni) fino a quando la funzione di costo (detta “loss“) raggiunge il minimo prefissato. E’ prevista anche una funzione di accuratezza che indica la percentuale di successo rispetto agli esempi.
5) Alla fine dell’addestramento si blocca l’aggiornamento dei pesi. Viene quindi passato alla rete neurale un nuovo dataset, detto di validazione, mai visto dalla rete neurale, e si ricalcola l’accuratezza e il loss per questo nuovo dataset. Questo test di validazione serve per comprendere se in effetti la rete neurale funziona o soffre di bias o overfitting.
Qui un esempio di andamento delle funzioni loss e accuratezza in un caso reale.

Se le cose non vanno bene si ricomincia da capo modificando eventualmente struttura della rete, set di esempi, numero di epoche, modalità di calcolo dell’errore, ecc. Tutti questi elementi vengono chiamati iperparametri della rete.
E’ fondamentale capire che non esiste ad oggi una “teoria” che permette di definire un modello generale di rete e di addestramento che sarà sicuramente efficace. La storia dell’invenzione delle reti neurali è partita da importanti teoremi matematici, ma per raggiungere un risultato realmente efficace la ricerca procede per esperimenti, tentativi ed ottimizzazioni, secondo una pratica più tipica dell’ingegneria che della matematica.
Retropropagazione
Col percettrone abbiamo visto che i pesi sono aggiornati creando una funzione di costo:
e poi con la derivata (il gradiente) calcoliamo di quanto dobbiamo correggere il peso:
Nelle reti profonde, V non è un vettore, ma un insieme di tensori collegati tra loro, e la funzione di costo è ottenuta solo alla fine di tutta l’elaborazione. Così, se c’è un errore, come si fa a distribuire la correzione nei singoli livelli? il peso dei primi strati infatti non è collegato però direttamente all’output finale.
Per risolvere questo problema dobbiamo immaginare la rete neurale come una “catena di montaggio”, dove cioè i layer sono degli ingranaggi che insieme a partire da un input producono un output. Ogni ingranaggio ha un ruolo nella produzione del risultato finale, ma ogni ingranaggio ha le sue caratteristiche. Quel che dobbiamo fare è quindi smontare gli ingranaggi ed aggiustarli separatamente.
In termini matematici il percettrone è una funzione di trasformazione (un solo ingranaggio):
La rete neurale invece ha molti ingranaggi, ad esempio ne ipotizziamo due:
Questo ha un impatto sulla funzione di costo. La funzione di costo J che per il percettrone ricordiamo era questa:
Nella rete neurale questa funzione di costo è estremamente più complessa, perché dipende da due o più trasformazioni indipendenti, e di norma ha una forma che può essere molto complessa. Ad esempio potrebbe essere questa:

Tuttavia noi possiamo scomporre T in più funzioni composte (in questo esempio due):
Dove T2 è la trasformazione del secondo layer, e T1 la trasformazione del primo layer, e quindi calcolare due funzioni di costo J1 e J2. Siccome la derivata di una funzione composta è il prodotto delle derivate:
In generale per n layer abbiamo n trasformazioni, e la derivata della funzione di costo finale, altro non è che il prodotto delle derivate delle singole funzioni di costo di ogni layer, più semplici da gestire e calcolare.
Si parte quindi dall’ultimo layer a ritroso in modo ricorsivo:
- nell’ultimo layer si calcola il gradiente locale della funzione di costo come se fosse l’unico layer (cioè si usa come input l’output del layer precedente) si aggiornano i pesi e si passa al layer precedente il gradiente trovato;
- per ogni layer precedente, si calcola il gradiente locale come se fosse l’ultimo e lo si moltiplica per il gradiente ricevuto dal layer successivo, si aggiornano i pesi e si richiama il layer precedente col gradiente calcolato;
- se è il primo, dopo il calcolo e l’aggiornamento, termina l’operazione.
In altri termini, ogni gradiente dipende dal gradiente locale, e dal gradiente calcolato dei layer successivi: si tratta di un problema di programmazione dinamica (si memorizzano i risultati parziali) non troppo complesso. E si spiega anche perché si usano le GPU, ovvero processori con set di istruzioni di calcolo molto semplici e veloci, progettate per generare velocemente immagini, cioè rappresentazioni vettoriali di numeri.
Come si vede il gradiente di ogni layer mano a mano che si procede a ritroso diventa sempre più piccolo, e quindi tende a fare aggiustare meno i pesi dei primi layer. Per questa ragione una rete con 10 layer è più difficile da addestrare di una con 5. Questo problema è stato poi aggirato con alcuni artifici matematici, ad esempio calcolando il gradiente non sulla singola trasformazione f(x) ma sulla trasformazione f(x)+ x dove x è l’input iniziale: questo ha reso una parte del gradiente indipendente dalla retropropagazione ed incrementando quindi il peso dei primi layers.
Conclusioni
Le reti neurali artificiali sono modelli matematici composti da un insieme di strati di tensori multidimensionali, in grado di memorizzare nei propri pesi l’esperienza accumulata nell’addestramento, ed essere quindi in grado di gestire set di dati variegati e complessi, come ad esempio immagini, testo in linguaggio naturale, audio, video, ecc. ed essere in grado di fare classificazioni o predizioni.
Per l’addestramento supervisionato, una rete neurale riceve un set di esempi che viene utilizzato per aggiustare i pesi di tutti i layer della rete. Sono necessari set di dati consistenti e l’addestramento viene ripetuto per molte epoche. Il cuore dell’addestramento è l’algoritmo di retropropagazione, che consente di calcolare separatamente l’errore per ogni layer e procedere all’aggiustamento graduale dei pesi per migliorare le performance della rete.
