Questo articolo ha l'obiettivo di comunicarti come funzionano le reti neurali nell'ambito del machine e deep learning.

Le reti neurali sono un particolare tipo di tecnologia che ha completamente rivoluzionato gli ultimi tempi.

Infatti, modelli come GPT e Falcon sono proprio basati su diverse reti neurali che comunicano tra di loro in una architettura particolare chiamata transformer.

Spesso sentiamo dire che le reti neurali artificiali sono delle rappresentazioni dei neuroni cerebrali umani all'interno di un computer.

Questi insiemi di neuroni formano reti interconnesse, ma i loro processi che scatenano eventi e attivazioni sono alquanto diversi da quello di un cervello vero.

Un neurone, preso singolarmente, è relativamente inutile, ma se unito a centinaia o migliaia di altri neuroni formano un rete interconnessa che spesso supera le performance di qualsiasi altro algoritmo di machine learning.

💡
Essendo questo articolo relativo ad un argomento introduttivo all'apprendimento automatico, ti condivido la pagina "Inizia Qui" dove puoi orientarti meglio tra il contenuto di questo blog.
Inizia qui con il Machine Learning e Analisi Dati
Stai iniziando il tuo percorso con Python, Machine Learning e Data Analytics? 👌 Questa pagina ti fornirà il percorso ideale in base al contenuto presente in questo blog. Ogni sezione riporterà gli articoli più idonei per il livello e gli obiettivi del lettore. Ogni sezione sarà inoltre aggiornat…

Breve background storico

Il concetto di rete neurale è alquanto antico - i primi pensieri di modellare un software prendendo ispirazione dal cervello umano risalgono agli inizi del 1940, da parte di Donald Hebb, McCulloch e Pitts.

Per oltre 20 anni il concetto è rimasto sul piano della teoria, poiché l'addestramento delle reti neurali è stato possibile solo attraverso una maggiore potenza computazione e alla creazione dell'algoritmo di backpropagation da parte di Paul Werbos, un efficiente meccanismo che permette alla rete di imparare propagando il feedback di un neurone a quello che lo precede.

Spicca il lavoro di Geoffrey Hinton, Andrew Ng e Jeff Dean che, insieme ad altri ricercatori, hanno reso il paradigma delle reti neurali popolare e efficace per tutta una serie di problemi.

Oggi le reti neurali vengono utilizzate in una miriade di compiti grazie alla loro abilità di risolvere problemi prima considerati impossibili da risolvere come la traduzione simultanea tra lingue, sintesi di video e audio e guida autonoma.

Neurone naturale e neurone artificiale - quali sono le differenze?

Anche se è vero che le reti neurali si ispirano ai neuroni naturali, questo paragone è quasi fuorviante poiché le loro anatomie e comportamenti sono diversi.

Non andrò molto nell'aspetto neuroscientifico, ma i neuroni naturali sembrano preferire una attivazione basata su "switch", on oppure off.

Attività oppure nessuna attività. In seguito al periodo di attività, tra l'altro, i neuroni naturali mostrano un periodo refrattario, cioè dove la loro abilità di attivarsi nuovamente è soppressa.

Questo comportamento viene descritto nel concetto di potenziale d'azione.

Visione generale dell'anatomia di un neurone

Anatomia di un neurone artificiale

‍Reti neurali come "black box"

Le reti neurali sono considerate delle black box (scatola chiusa)- non sappiamo perché raggiungano queste performance, ma sappiamo come lo fanno.

I cosiddetti dense layers (strati densi), che sono gli strati più comuni in una rete neurale, creano interconnessioni tra i vari strati della rete.

Ogni neurone è connesso ad ogni altro neurone dello strato successivo, il che significa che il suo valore di output diventa l'input per i prossimi neuroni.

Ogni connessione tra neuroni possiede un peso (weight) che è uno dei fattori che viene modificato durante l'addestramento. Il peso della connessione influenza quanto input viene passato tra un neurone all'altro. Questo comportamento segue la formula \( inputs \times weights \).

Una volta che un neurone riceve gli input da tutti gli altri neuroni connessi ad esso, viene aggiunto un bias, un valore costante che va sommato al calcolo che coinvolge il peso menzionato. Anche il bias è un fattore che viene modificato durante l'addestramento.

💡
Una rete neurale è in grado di generalizzare e modellare un problema nel mondo reale (che non è altro che una funzione matematica) proprio grazie al costante aggiustamento di pesi e bias, che modulano l'output e l'input di ogni singolo neurone finché non la rete non approccia ad una soluzione accettabile.

L'output di un neurone è espresso dalla formula \( output = inputs \times weights + bias \).

L'aggiustamento di pesi e bias viene fatto nelle hidden layers (strati nascosti), che sono gli strati presenti tra lo strato di input e quello di output. Sono detti "nascosti" proprio perché non vediamo il comportamento di aggiustamento di pesi e bias.

Ecco perché le reti neurali sono delle black box.

Come apprende una rete neurale

La caratteristica che rende complesse le reti neurali è proprio la enorme mole di calcoli che avviene a livello sia di rete che di singolo neurone.

Insieme ai pesi e bias ci sono le funzioni di attivazione che aggiungono una ulteriore complessità matematica ma influenzano enormemente la performance di una rete neurale.

💡
Impari efficacemente leggendo? Ho il libro perfetto da consigliarti per introdurti al Deep Learning in Python.

Si tratta di Deep Learning with Python di François Chollet. È un libro completo, dettagliato, matematicamente complesso ma ricco di esempi sia teorici che pratici.

Il deep learning non è argomento semplice, ma se sei interessato a questo percorso, questo manuale (e molti altri) non può mancare nella tua libreria.

Deep Learning with Python (seconda edizione) da F. Chollet

Una bibbia del deep learning in Python da uno degli esponenti del settore

Compralo su Amazon

Pesi e bias

Pesi e bias possono essere interpretati come un sistema di manopole che possiamo ruotare per ottimizzare il nostro modello - come quando cerchiamo di sintonizzare la nostra radio ruotando le manopole per cercare la frequenza gradita.

La differenza sostanziale è che in una rete neurale, abbiamo centinaia se non migliaia di manopole da girare per raggiungere il risultato finale.

Poiché pesi e bias sono dei parametri della rete, questi saranno oggetto del cambiamento generato dalla rotazione della manopola immaginaria.

Visto che i pesi sono moltiplicati all'input, questi influenzano la magnitudine dell'input. Il bias, invece, poiché è sommato all'espressione \( inputs \times weights \), sposterà la funzione nel piano dimensionale. Vediamo degli esempi.

Ricordiamo che la formula è \( output = inputs \times weights + bias \)

Come cambia l'output di un neurone in base a peso e bias

Com'è possibile notare, pesi e bias impattano il comportamento di ogni neurone artificiale, ma lo fanno in maniera rispettivamente diversa. I pesi sono solitamente inizializzati randomicamente mentre il bias a 0.

Il comportamento di un neurone è anche influenzato dalla sua funzione di attivazione che, parallela al potenziale d'azione per un neurone naturale, definisce le condizioni di attivazione e relativi valori dell'output finale.

Funzioni di attivazione

Il tema delle funzioni di attivazione merita un articolo a sé, ma qui presenterò una overview generale.

Se ricordate, ho menzionato come un neurone naturale abbia una attivazione a switch. In gergo informatico/matematico, chiamiamo questa funzione una step function (funzione gradino).

Comportamento di una step function (funzione gradino)

Seguendo la logica

\( 1 \ x > 0; 0 \ x \leq 0 \)

la funzione gradino permette al neurone di restituire 1 se l'input è maggiore di 0 oppure 0 se l'input è minore o uguale a 0. Questo comportamento simula il comportamento di un neurone naturale e segue la formula

\( output = sum(inputs \times weights) + bias \)

La step function è però molto semplice, e nel settore si tende ad usare delle funzioni di attivazione più complesse, come l'unità lineare rettificata (ReLU) e SoftMax.

Come scrivere una piccola rete in Python

Creemo una piccola rete neurale con 4 input e 3 neuroni per comprendere come funziona il calcolo di pesi e bias.

Architettura della piccola rete che andremo a scrivere in Python

Iniziamo dal definire questi parametri manualmente a scopo d'esempio

inputs = [1, 2, 3, 4] # quattro input
# weights è un array 3x4 -> 3 neuroni, 4 pesi associati ad ogni connessione
weights = [[ 0.74864643, -1.00722027,  1.45983017,  1.34236011],
           [-1.20116017, -0.08884298, -0.46555646,  0.02341039],
           [-0.30973958,  0.89235565, -0.92841053,  0.12266543]]
biases = [0, 0.3, -0.5] # ogni neurone ha un bias

Ora creiamo il loop che andrà a creare la nostra piccola rete neurale

layer_outputs = [] # creiamo la lista che conterrà i risultati dell'elaborazione dei neuroni dello strato 
# per ogni neurone
for neuron_weights, neuron_bias in zip(weights, biases):
    # inizializziamo l'output a 0
    neuron_output = 0
    # per ogni input e peso
    for n_input, weight in zip(inputs, neuron_weights):
        # moltiplicare input e peso e aggiungerlo all'output
        neuron_output += n_input * weight
    # aggiungere il bias all'output
    neuron_output += neuron_bias
    # aggiungere il risultato del neurone allo strato
    layer_outputs.append(neuron_output)

print(layer_outputs) # stampiamo il risultato

L'output finale è questo

Il risultato della rete neurale

Codice completo

inputs = [1, 2, 3, 4] # quattro input
# weights è un array 3x4 -> 3 neuroni, 4 pesi associati ad ogni connessione
weights = [[ 0.74864643, -1.00722027,  1.45983017,  1.34236011],
           [-1.20116017, -0.08884298, -0.46555646,  0.02341039],
           [-0.30973958,  0.89235565, -0.92841053,  0.12266543]]
biases = [0, 0.3, -0.5] # ogni neurone ha un bias

layer_outputs = [] # creiamo la lista che conterrà i risultati dell'elaborazione dei neuroni dello strato 
# per ogni neurone
for neuron_weights, neuron_bias in zip(weights, biases):
    # inizializziamo l'output a 0
    neuron_output = 0
    # per ogni input e peso
    for n_input, weight in zip(inputs, neuron_weights):
        # moltiplicare input e peso e aggiungerlo all'output
        neuron_output += n_input * weight
    # aggiungere il bias all'output
    neuron_output += neuron_bias
    # aggiungere il risultato del neurone allo strato
    layer_outputs.append(neuron_output)

print(layer_outputs) # stampiamo il risultato