Home Fondamenti Storia dell'AI Reti Neurali Backpropagation Architetture Token Modelli AI Case Studies Tecniche RAG RAG Avanzato GraphRAG MCP Orchestrazione LangChain LangGraph Prompt Engineering Usare l'AI ChipsBot News

Reti Neurali — dal neurone artificiale al Multi-Layer Perceptron

Sotto ogni LLM moderno, sotto ChatGPT, Claude, Gemini, c'è lo stesso mattone elementare: un “neurone” artificiale che esegue una somma pesata seguita da una funzione non lineare. Capire bene questo mattone — e come stratificarlo in reti — è il prerequisito per capire tutto il resto. In questa pagina partiamo dalla biologia, arriviamo al teorema di approssimazione universale, e ti diamo gli strumenti per leggere qualunque architettura moderna.

Il neurone biologico — l'ispirazione

Il cervello umano contiene circa 86 miliardi di neuroni connessi in un reticolo da circa 100–1000 trilioni di sinapsi. Un singolo neurone è un'unità relativamente semplice: riceve segnali elettrici da migliaia di altri neuroni tramite i dendriti, somma i potenziali ricevuti nel soma, e se la somma supera una certa soglia produce un potenziale d'azione che si propaga lungo l'assone verso altri neuroni. L'intensità di ogni connessione (sinapsi) è modulabile — più o meno “forte” — e proprio in queste modulazioni vive l'apprendimento.

Per i pionieri dell'AI degli anni '40 questa è un'immagine illuminante. Se un neurone è così semplice — somma, soglia, output binario — e dal cervello emerge il pensiero, allora una rete artificiale di unità simili potrebbe in linea di principio fare la stessa cosa. Va detto subito: questa analogia è suggestiva ma fuorviante se presa troppo sul serio. Le reti neurali moderne sono ispirate al cervello come un Boeing 747 è ispirato a un albatro: l'astrazione è utile come punto di partenza, poi il sistema artificiale ha le sue regole.

Tre differenze enormi rispetto alla biologia, da tenere a mente quando senti dire “funziona come il cervello”: (1) i neuroni biologici comunicano via spike temporali, non valori continui; (2) il cervello impara senza backpropagation (l'algoritmo di training delle reti artificiali è biologicamente implausibile); (3) il cervello consuma 20 watt, GPT-4 ne consuma decine di megawatt per addestramento. Confronti come “quanti neuroni ha GPT-3?” sono giornalismo, non scienza.

Il neurone di McCulloch-Pitts (1943)

Il primo modello matematico di un neurone artificiale è del 1943. Warren McCulloch (neurofisiologo) e Walter Pitts (logico matematico) propongono un'unità che riceve N input binari x1, ..., xN ∈ {0,1}, li somma, applica una soglia θ, e produce 0 o 1:

output = 1 se (x₁ + x₂ + ... + x_N) ≥ θ, altrimenti 0

Con questa unità primitiva McCulloch e Pitts mostrano che reti opportunamente connesse possono calcolare qualsiasi funzione logica: AND, OR, NOT, e quindi qualsiasi circuito booleano. È un risultato di pura computabilità: il neurone artificiale è un'unità di calcolo universale.

Il limite enorme di questo modello: i pesi sono tutti uguali (somma pura) e non c'è apprendimento. La rete è progettata, non addestrata. Quindici anni dopo, Rosenblatt aggiungerà entrambe le cose.

Il perceptron di Rosenblatt (1958)

Frank Rosenblatt aggiunge tre ingredienti al modello McCulloch-Pitts che lo trasformano in un sistema di apprendimento. Primo, pesi associati a ogni input: invece di sommare cieco, sommi pesato. Secondo, un bias esplicito (equivalente a un input fisso che permette di traslare la soglia). Terzo, e cruciale, una regola di apprendimento per aggiornare i pesi quando il perceptron sbaglia.

L'unità perceptron prende N input x1, ..., xN (questa volta numerici, non solo binari), li moltiplica per i pesi w1, ..., wN, somma, aggiunge il bias b, applica una funzione di attivazione φ (storicamente uno step binario, oggi sigmoidi o ReLU):

z = w₁x₁ + w₂x₂ + ... + w_N x_N + b = w · x + b (notazione vettoriale) output = φ(z)

La regola di apprendimento di Rosenblatt è brillante per la sua semplicità. Mostri al perceptron un esempio (x, y) dove y è la classe corretta (0 o 1). Calcoli la sua predizione ŷ. Se ha sbagliato, aggiusti i pesi nella direzione che ridurrebbe l'errore: w ← w + η (y − ŷ) x, dove η (eta) è il learning rate. Ripeti su molti esempi finché non sbaglia più.

Rosenblatt dimostra il teorema di convergenza del perceptron: se i dati sono linearmente separabili (esiste un iperpiano che divide perfettamente le due classi), il perceptron converge a una soluzione in un numero finito di passi. È il primo algoritmo di machine learning con garanzia teorica.

# Perceptron in 15 righe di NumPy
import numpy as np

class Perceptron:
    def __init__(self, n_inputs, lr=0.1):
        self.w = np.zeros(n_inputs)
        self.b = 0.0
        self.lr = lr

    def predict(self, x):
        z = np.dot(self.w, x) + self.b
        return 1 if z >= 0 else 0

    def fit(self, X, y, epochs=100):
        for _ in range(epochs):
            for xi, yi in zip(X, y):
                pred = self.predict(xi)
                error = yi - pred
                self.w += self.lr * error * xi
                self.b += self.lr * error

Il muro dell'XOR — perché un singolo layer non basta

Il perceptron impara qualunque funzione linearmente separabile. Geometricamente: traccia un iperpiano in N dimensioni e dichiara classe 1 a destra, classe 0 a sinistra. Per AND e OR funziona perfettamente, perché sono linearmente separabili. Per XOR no.

XOR: input → output (0,0) → 0 (0,1) → 1 (1,0) → 1 (1,1) → 0 x₂ │ (0,1)● (1,1)○ │ │ (0,0)○ (1,0)● └──────────────────── x₁ ○ = classe 0 ● = classe 1 Non esiste nessuna linea retta che separi i due cerchi pieni dai due cerchi vuoti.

Questo è il risultato che Minsky e Papert pubblicano nel 1969 nel libro Perceptrons, e che — come abbiamo visto nella storia dell'AI — affossa per quasi vent'anni la ricerca sulle reti neurali. La critica era matematicamente corretta ma circoscritta a un caso specifico. La soluzione esiste, e si chiama aggiungere uno strato nascosto.

Il Multi-Layer Perceptron (MLP)

L'idea è semplice: invece di un singolo strato di neuroni che mappano direttamente input → output, metti uno o più strati nascosti in mezzo. Ogni strato nascosto prende come input gli output dello strato precedente e produce nuovi output che alimentano il successivo. L'ultimo strato è l'output.

[x₁ x₂ x₃ x₄] ← INPUT LAYER (4 unità) │ ┌────┴────┐ │ W₁, b₁ │ + φ ← HIDDEN LAYER 1 (8 unità, ad es.) └────┬────┘ │ ┌────┴────┐ │ W₂, b₂ │ + φ ← HIDDEN LAYER 2 (6 unità) └────┬────┘ │ ┌────┴────┐ │ W₃, b₃ │ + φ ← OUTPUT LAYER (es. 3 classi) └────┬────┘ │ [ŷ₁ ŷ₂ ŷ₃] ← OUTPUT

Ogni connessione tra due strati è una matrice di pesi. Se lo strato L ha n unità e lo strato L+1 ne ha m, la matrice di pesi W ha dimensioni m×n. La trasformazione completa è:

a⁽ᴸ⁺¹⁾ = φ( W⁽ᴸ⁾ a⁽ᴸ⁾ + b⁽ᴸ⁾ )

Dove a(L) è il vettore di attivazioni dello strato L, e φ è applicata elemento per elemento. Concatenando questa formula da strato 1 a strato L finale, ottieni la funzione complessiva della rete:

f(x) = φ( W⁽ᴸ⁾ φ( ... φ( W⁽¹⁾ x + b⁽¹⁾ ) ... ) + b⁽ᴸ⁾ )

Punto cruciale: se φ fosse lineare, la composizione di strati lineari sarebbe ancora lineare — equivalente a un singolo strato, niente di nuovo. La non-linearità di φ è ciò che rende il deep learning possibile. È il motivo per cui ogni rete neurale “reale” ha funzioni di attivazione non lineari, e perché la scelta della funzione di attivazione è così importante.

XOR risolto con un MLP a 2-2-1

Tornando al problema XOR: con un layer nascosto di due unità, la rete può risolverlo. La rete impara internamente due rappresentazioni intermedie (es. AND e OR), poi combina queste due con un secondo strato (AND NOT). È un esempio bellissimo di come la profondità aggiunga capacità rappresentativa: ogni layer crea nuove feature, e gli strati successivi operano su queste feature derivate, non sui dati grezzi.

# MLP per XOR a mano (pesi calcolati, non addestrati)
import numpy as np

def step(x): return (x >= 0).astype(int)

# Layer 1: due neuroni che imparano AND e OR
W1 = np.array([[1, 1],     # OR:  attivo se x1=1 OR x2=1
               [1, 1]])    # AND: attivo se x1=1 AND x2=1
b1 = np.array([-0.5, -1.5])  # soglie diverse: OR scatta a 1, AND scatta a 2

# Layer 2: XOR = OR AND NOT AND
W2 = np.array([[1, -1]])
b2 = np.array([-0.5])

for x in [[0,0], [0,1], [1,0], [1,1]]:
    a1 = step(W1 @ x + b1)         # attivazioni layer nascosto
    y  = step(W2 @ a1 + b2)         # output
    print(f"{x} -> hidden {a1} -> XOR {y[0]}")

# [0,0] -> hidden [0,0] -> XOR 0
# [0,1] -> hidden [1,0] -> XOR 1
# [1,0] -> hidden [1,0] -> XOR 1
# [1,1] -> hidden [1,1] -> XOR 0

Funzioni di attivazione

La funzione di attivazione è il piccolo dettaglio non lineare che separa una rete neurale da una pila di moltiplicazioni di matrici. Negli anni si sono provate molte varianti; ognuna ha trade-off precisi.

FunzioneFormulaRangeQuando usarla
Step1 se x ≥ 0, 0 altrimenti{0,1}Storia (perceptron classico). Non differenziabile, non usata oggi
Sigmoideσ(x) = 1 / (1 + e−x)(0,1)Output binario, gate (LSTM, GRU). Soffre di vanishing gradient
Tanh(ex − e−x) / (ex + e−x)(−1, 1)Hidden layer RNN, output centrato attorno a zero
ReLUmax(0, x)[0, ∞)Standard de facto per hidden layer dal 2012 a oggi
Leaky ReLUmax(0.01x, x)RRisolve il problema dei “neuroni morti” di ReLU
GELUx · Φ(x)RStandard nei Transformer (BERT, GPT)
Swish / SiLUx · σ(x)RSpesso usata in modelli recenti (Llama, Gemma)
Softmaxex_i / Σ ex_j(0,1), somma=1Output multi-classe (probabilità)

Perché ReLU ha cambiato tutto

Fino al 2010 la sigmoide era lo standard per i hidden layer. Era teoricamente elegante (continua, differenziabile ovunque, biologicamente plausibile come saturazione). Ma aveva un problema feroce: il vanishing gradient. La derivata della sigmoide è massima 0.25 (al centro) e tende a zero agli estremi; quando moltiplichi 0.25 per sé stesso 20 volte (come capita nel backward pass di una rete profonda), ottieni un numero piccolissimo, e gli strati più bassi non imparano. Le reti profonde erano addestrabili in teoria, in pratica no.

ReLU (Rectified Linear Unit, max(0, x)) risolve il problema in modo brutale: la derivata è 0 per x<0 e 1 per x>0. Non c'è saturazione nel range positivo, quindi i gradienti si propagano senza shrinking. AlexNet nel 2012 usa ReLU, è uno dei motivi non secondari della sua vittoria. Da allora ReLU e le sue varianti sono ovunque.

Analogia: la sigmoide è un volume che gira morbido e che a un certo punto si schiaccia in alto o in basso — più ruoti la manopola, meno succede. ReLU è un interruttore con un dimmer lineare sopra zero: ogni piccolo movimento sotto zero non fa niente, ogni piccolo movimento sopra zero risponde linearmente. La “risposta lineare” sopra zero è ciò che permette al gradiente di propagarsi anche attraverso 100 strati.

ReLU ha un suo difetto, i dying ReLU neurons: se un neurone riceve sempre input negativi, la sua derivata è sempre zero e non viene mai aggiornato — resta “morto”. Leaky ReLU e GELU sono fix moderni a questo problema, con piccola pendenza non zero a sinistra. GELU in particolare è lo standard nei Transformer e tende a dare risultati leggermente migliori in modelli grandi.

Quale scegliere oggi (2026)
  • Hidden layer in CNN o MLP generici: ReLU. È il default sicuro.
  • Hidden layer in Transformer: GELU (BERT, GPT) o Swish/SiLU (Llama, Gemma).
  • Output classificazione binaria: sigmoide.
  • Output classificazione multi-classe: softmax.
  • Output regressione: nessuna attivazione (lineare).
  • Gate interni di RNN/LSTM: sigmoide (interpretabile come probabilità di aprire il gate).

Il forward pass — un esempio numerico

Vediamo concretamente cosa succede quando un input attraversa una rete. Costruiamo una mini-rete con 2 input, 3 unità nascoste, 1 output, e tracciamo numericamente il calcolo.

# Mini MLP: 2 -> 3 -> 1, attivazione ReLU sull'hidden, sigmoide su output
import numpy as np

# Input
x = np.array([0.5, -0.3])

# Pesi e bias (inventati per esempio)
W1 = np.array([
    [0.2,  0.8],   # neurone 1
    [-0.5, 0.1],   # neurone 2
    [0.3, -0.7],   # neurone 3
])
b1 = np.array([0.1, -0.2, 0.05])

W2 = np.array([[0.4, -0.6, 0.9]])
b2 = np.array([0.0])

# Forward pass
z1 = W1 @ x + b1
print(f"z1 (pre-attivazione hidden): {z1}")
# z1 = [0.34, -0.48, -0.06]  (calcola tu)

a1 = np.maximum(0, z1)         # ReLU
print(f"a1 (post-ReLU):           {a1}")
# a1 = [0.34, 0, 0]

z2 = W2 @ a1 + b2
print(f"z2 (pre-attivazione out):  {z2}")
# z2 = [0.136]

y  = 1 / (1 + np.exp(-z2))     # sigmoide
print(f"y (output):                {y}")
# y = [0.534]  (interpretabile come probabilità ~53%)

Tre cose da notare nell'esempio. Primo, ReLU ha spento due neuroni su tre — quelli con pre-attivazione negativa diventano zero. Questa sparsità di attivazione è tipica di ReLU e ha implicazioni computazionali: solo i neuroni attivi contribuiscono. Secondo, l'output è 0.534 — vicino a 0.5, cioè il modello è molto incerto. In un classificatore binario soglieresti a 0.5 e diresti “classe 1”, ma con poca confidenza. Terzo, questo intero processo è deterministico dati pesi e input: la stocasticità arriva solo durante il training (mini-batch, dropout, inizializzazione casuale).

Il teorema di approssimazione universale

Nel 1989 George Cybenko e poi (in forma più generale) Kurt Hornik dimostrano un risultato sorprendente: un MLP con un singolo strato nascosto sufficientemente largo può approssimare qualsiasi funzione continua su un dominio compatto a qualsiasi precisione.

In formula informale: per ogni funzione continua f: [0,1]n → R e ogni ε>0, esiste un MLP con un layer nascosto di K(ε) unità tale che |f(x) − rete(x)| < ε per ogni x. Vale per attivazioni non polinomiali (sigmoide, tanh, ReLU vanno tutte bene).

Da un punto di vista filosofico, è sorprendente: le reti neurali sono approssimatori universali di funzioni. Non c'è una limitazione teorica di cosa possano rappresentare.

Da un punto di vista pratico, è un risultato quasi inutile. Il teorema dice che esiste un MLP con un singolo layer che approssima la tua funzione, ma non dice quanto largo deve essere quel layer (potrebbe essere astronomicamente grande), né come trovare i pesi giusti (NP-hard nel caso generale), né che quel modello generalizzi a dati nuovi (potrebbe overfittare in modo brutale).

Analogia: dire che gli MLP sono approssimatori universali è come dire che con LEGO si può costruire qualsiasi forma. Tecnicamente vero, ma non ti dice quanti mattoncini servono per costruire la Sagrada Familia, né quale sequenza di assemblaggio è pratica. La domanda interessante non è mai “si può?”, è “si può con risorse ragionevoli partendo da dati ragionevoli?”. Ed è in quella domanda che il deep learning moderno vive o muore.

Profondità vs ampiezza — perché le reti sono “deep”

Il teorema universale dice che basta un layer. La pratica dice il contrario: dal 2012 in poi le reti vincenti sono profonde, con decine o centinaia di strati. Perché?

Il motivo è l'efficienza rappresentativa. Esistono risultati teorici (Telgarsky 2016, Eldan-Shamir 2016) che mostrano funzioni esponenzialmente più difficili da rappresentare con un singolo strato largo che con più strati profondi. In parole povere: una rete profonda di N unità totali può rappresentare funzioni che una rete a un singolo strato richiederebbe 2N unità per rappresentare.

L'intuizione è che la profondità permette composizione gerarchica di feature. Pensa al riconoscimento di un volto in una foto:

Layer 1: pixels → linee, bordi, angoli Layer 2: bordi → texture, pattern semplici (occhi parziali, archi) Layer 3: pattern → parti di volto (occhio intero, naso, bocca) Layer 4: parti → volti interi, espressioni Layer 5: volti → identità di persone specifiche

Ogni strato compone gli output del precedente in concetti più astratti. Un singolo strato largo dovrebbe fare tutta questa gerarchia in un colpo solo, ed è molto meno efficiente. Questa è la giustificazione intuitiva (e parzialmente formale) del “deep learning”: la profondità non è un vezzo, è ciò che permette alle reti di estrarre rappresentazioni gerarchiche del mondo.

Inizializzazione dei pesi — il piccolo dettaglio decisivo

Da cosa partono i pesi prima dell'addestramento? Non da zero, perché allora tutti i neuroni di uno stesso strato calcolerebbero la stessa cosa (problema di simmetria) e il backprop non potrebbe differenziarli. Non da numeri grandi, perché le pre-attivazioni esploderebbero e le sigmoidi/tanh saturerebbero. Non da numeri piccolissimi uguali per tutti, perché le attivazioni svanirebbero strato dopo strato.

La risposta moderna sono due schemi di inizializzazione casuale calibrati sulla dimensione degli strati. Si tirano i pesi da una distribuzione normale (o uniforme) con varianza scelta in modo da preservare l'ampiezza delle attivazioni man mano che attraversano la rete.

Xavier / Glorot (2010)

Per sigmoide e tanh. Varianza = 2 / (nin + nout). L'idea: mantenere la varianza delle attivazioni e dei gradienti costante attraverso gli strati. Ha sbloccato l'addestramento di reti profonde con attivazioni sigmoidi.

He / Kaiming (2015)

Per ReLU. Varianza = 2 / nin. Adatta Xavier al fatto che ReLU spegne metà dei neuroni in media (le attivazioni negative diventano zero), quindi la varianza utile è dimezzata. Standard de facto per reti con ReLU/GELU.

Sembra un dettaglio implementativo, ma in pratica un'inizializzazione sbagliata può impedire l'addestramento di una rete profonda. PyTorch e TensorFlow scelgono lo schema corretto in automatico in base al tipo di layer, ma è importante sapere che il valore di default è una scelta progettuale, non magia.

Cosa una rete impara davvero

Una domanda a cui si può rispondere solo in modo parziale, e che vale la pena affrontare con onestà. Dopo l'addestramento, cosa sta succedendo dentro la rete?

Per le CNN su immagini abbiamo una buona idea grazie a tecniche di visualizzazione (filtri appresi, activation maximization, feature attribution). Studi seminali di Zeiler-Fergus (2014) e di Olah-Schubert-Mordvintsev (2017–2020, OpenAI Microscope) hanno mostrato che le CNN imparano una gerarchia abbastanza interpretabile: edge detector nei primi layer, texture e pattern nei layer intermedi, parti di oggetti e oggetti interi nei layer alti. Questo conferma l'intuizione gerarchica esposta sopra.

Per i Transformer su testo la situazione è più misteriosa. La comunità di mechanistic interpretability (Anthropic, Neel Nanda, Chris Olah) sta facendo progressi sorprendenti: identificazione di induction heads (circuiti che imparano a copiare pattern dal contesto), features monosemantiche estratte con sparse autoencoder, circuits dedicati a compiti specifici come l'aritmetica modulare o l'identificazione del soggetto di una frase. Ma rispetto alle CNN siamo ancora indietro: un LLM da 100 miliardi di parametri è in larga misura una scatola nera anche per chi lo ha costruito.

Il problema della scatola nera La scarsa interpretabilità non è un dettaglio accademico. In contesti medical, legal, financial, autonomous driving, “il modello ha deciso X” non è sufficiente: serve sapere perché. Tecniche come SHAP, LIME, attention visualization, contrastive explanations sono parziali ma utili. La mechanistic interpretability è una delle frontiere di ricerca più importanti del 2024–2026. Per ora, vale la regola: se devi giustificare la decisione a un giudice, un revisore o un cliente, un modello interpretabile (regressione logistica, decision tree) può essere preferibile a uno più accurato ma opaco.

Continua il percorso

Hai i mattoni. Adesso devi capire come si costruisce davvero, cioè come una rete impara i pesi giusti partendo da casuali. Tutto questo lo vedremo in: