Neural Network: Introduzione e Forward Propagation
Parte (1/4)
Potrebbe interessarti:
- Neural Network: Backpropagation nell’Output Layer - (2/4)
- Neural Network: Backpropagation nell’Hidden Layer - (3/4)
- Neural Network: Implementazione del Modello e Classificazione - (4/4)
Il progetto Python, che comprende gli script mostrati in questo post, e quelli relativi alla creazione dei grafici Matplotlib e Plotly, è disponibile assieme al corso Introduzione al Machine Learning.
Cos’è una Rete Neurale?
Una Rete Neurale è un modello computazionale progettato per apprendere pattern e relazioni, anche molto complessi, presenti nei dati di input. Essa è costituita da unità interconnesse, chiamate nodi o neuroni, dove ogni nodo agisce come un’unità computazionale il cui funzionamento ricorda, nella forma più semplice, quello utilizzato nella Regressione Logistica.
I nodi sono organizzati in layer: un layer di input, uno o più hidden layer, e un layer di output. In generale, ogni nodo riceve uno o più valori in ingresso, li combina mediante una somma pesata, aggiunge un termine di bias e applica al risultato una funzione di attivazione. La presenza di molti nodi organizzati in più layer permette alla rete di apprendere progressivamente rappresentazioni e relazioni complesse nei dati, rendendo le Reti Neurali adatte a problemi quali il riconoscimento delle immagini, l’elaborazione del linguaggio naturale e numerosi problemi di classificazione e regressione.
Per fare un confronto, la Regressione Logistica applica una funzione di attivazione non-lineare, la Sigmoide, a una combinazione lineare delle feature. Nonostante la non-linearità della Sigmoide, il suo confine decisionale rimane però lineare, poiché è determinato dalla combinazione lineare:
$$ \mathbf{w}\cdot\mathbf{x}+b=0 $$Le Reti Neurali hanno invece una capacità di modellazione molto maggiore, grazie alla composizione di più trasformazioni non-lineari distribuite su uno o più hidden layer. Ogni layer utilizza l’output del layer precedente come proprio input, applicando una nuova trasformazione ai dati.
La composizione di queste trasformazioni permette alle Reti Neurali di rappresentare relazioni e confini decisionali non-lineari anche molto complessi, consentendo di classificare dati che non sono linearmente separabili nello spazio delle feature originale.
Sotto opportune condizioni, una Rete Neurale dotata di un numero sufficiente di nodi può inoltre approssimare un’ampia classe di funzioni. Questa proprietà, formalizzata dai cosiddetti Universal Approximation Theorems, aiuta a comprendere l’elevata capacità espressiva delle Reti Neurali.
Forward Propagation e Backpropagation
Nelle Reti Neurali, il processo di apprendimento prevede due fasi: Forward Propagation e Backpropagation (o Forward Phase e Backward Phase).
Nella fase di Forward Propagation, i dati in ingresso attraversano la rete procedendo dal layer di input verso gli hidden layer e, infine, verso il layer di output. Ogni layer riceve i valori prodotti dal layer precedente, li elabora e genera nuovi valori, chiamati attivazioni, che vengono passati al layer successivo. Alla fine del processo, il layer di output produce la previsione del modello. Nel caso di un problema di classificazione, ad esempio, questi valori possono rappresentare le probabilità associate alle diverse classi. Durante l’addestramento, la previsione viene confrontata con il valore reale attraverso una funzione di Loss, che misura l’errore commesso dalla rete.
Nella fase di Backpropagation, la rete calcola come la Loss varia al variare dei suoi parametri. Il processo parte dal layer di output e procede all’indietro attraverso i layer precedenti, determinando progressivamente il contributo dei diversi pesi e bias all’errore complessivo. L’obiettivo è calcolare i gradienti, cioè le derivate parziali della funzione di costo rispetto a ciascun parametro della rete. Questi valori vengono successivamente utilizzati da un algoritmo di ottimizzazione, come la Discesa del Gradiente, per aggiornare i parametri del modello e ridurre la Loss.
Forward Propagation
La Forward Propagation è il processo con cui i dati attraversano la Rete Neurale, dal layer di input fino al layer di output, layer dopo layer, per calcolare la previsione del modello. Per capire come funziona questo processo, iniziamo considerando il funzionamento di una singola unità simile a quella utilizzata nel modello di Regressione Logistica. Essa può essere rappresentata mediante un input layer e un output layer, che formano la semplice struttura illustrata in Figura 1:

Per una data osservazione in ingresso, $\mathbf{x}^{(i)}$, il layer di input contiene un nodo per ciascuna delle sue $n$ feature, $x^{(i)}_1, x^{(i)}_2, \dots, x^{(i)}_n$. Ciascuna feature è collegata al nodo del layer di output attraverso una connessione alla quale è associato un peso $w_j$. Il nodo di output calcola innanzitutto la combinazione lineare degli input:
$$ \begin{align} z^{(i)} & = w_1x^{(i)}_1 + w_2x^{(i)}_2 + \dots + w_nx^{(i)}_n + b \label{logit-1}\tag{1}\\[6pt] & = \mathbf{w}\cdot\mathbf{x}^{(i)} + b \label{logit-2}\tag{2} \end{align} $$dove $\mathbf{w}$ rappresenta il vettore dei pesi e $b$ il termine di bias. Il bias svolge un ruolo analogo all’intercetta di un modello lineare: permette di traslare la funzione rispetto all’origine e rende quindi il modello più flessibile rispetto al caso $b=0$.
Nel caso della Regressione Logistica, il valore reale $z^{(i)}$ rappresenta anche il logit, cioè il logaritmo degli odds associati alla probabilità prevista. Questo valore viene passato alla funzione di attivazione Sigmoide, $\sigma$:
$$ \begin{align} \hat{p}^{(i)} &= \sigma\left(z^{(i)}\right)\\[6pt] &= \frac{1}{1 + e^{-z^{(i)}}} = \frac{1}{1 + e^{-\left(\mathbf{w}\cdot\mathbf{x}^{(i)}+b\right)}}\label{sigmoid}\tag{3}\\[6pt] \end{align} $$La Sigmoide trasforma quindi lo score $z^{(i)}$, che può assumere qualsiasi valore reale, in un valore compreso nell’intervallo $(0,1)$. Nella Regressione Logistica tale valore può essere interpretato come la probabilità stimata che l’osservazione $\mathbf{x}^{(i)}$ appartenga alla classe positiva:
$$ \hat{p}^{(i)} = P\left(y^{(i)}=1\mid\mathbf{x}^{(i)}\right) $$Ecco un esempio che utilizza solamente tre feature, $x_1$, $x_2$ e $x_3$:

Classificazione Multi-Label
Il modello precedente utilizza un singolo nodo di output ed è quindi in grado di stimare la probabilità associata a una singola classe positiva, come avviene nella classificazione binaria. Possiamo però estendere questa architettura aggiungendo più nodi al layer di output, in modo che ciascun nodo stimi indipendentemente la probabilità associata a una diversa label. Questa struttura è simile all’idea di utilizzare più modelli di Regressione Logistica, uno per ciascuna label, e costituisce una semplice forma di classificazione multi-label.
Consideriamo la seguente immagine, nella quale il layer di output contiene due nodi, $a_1$ e $a_2$, ciascuno associato a una diversa funzione di attivazione Sigmoide:

A differenza della classificazione binaria con un singolo nodo di output, in questo caso i due nodi producono due probabilità indipendenti. L’appartenenza a una label non esclude quindi necessariamente l’appartenenza all’altra.
Generalizzando, un layer di output con $n_o$ nodi $a_1, a_2, \cdots, a_{n_o}$ permette di stimare le probabilità associate a $n_o$ label distinte. Ogni nodo di output possiede un proprio insieme di pesi e un proprio bias. Indichiamo con $w_{t,s}$ il peso associato alla connessione che va dal nodo sorgente $s$ del layer precedente al nodo destinazione $t$ del layer corrente. Considerando un’osservazione $\mathbf{x}$ con $n$ feature di input (non utilizzando temporaneamente la notazione “superscript”, per migliorare la leggibilità delle formule), abbiamo:
$$ \begin{align} z_1 & = w_{1,1}x_1 + w_{1,2}x_2 + \cdots + w_{1,n}x_n + b_1\\[6pt] z_2 & = w_{2,1}x_1 + w_{2,2}x_2 + \cdots + w_{2,n}x_n + b_2\\[6pt] \cdots\\[6pt] z_{n_o} & = w_{n_o,1}x_1 + w_{n_o,2}x_2 + \cdots + w_{n_o,n}x_n + b_{n_o} \label{logit-k}\tag{4}\\[6pt] a_1 & = \sigma\left(z_1\right)\\[6pt] a_2 & = \sigma\left(z_2\right)\\[6pt] \cdots\\[6pt] a_{n_o} & = \sigma\left(z_{n_o}\right) \label{sigma-k}\tag{5}\\[6pt] \end{align} $$In relazione all’immagine precedente, la notazione $w_{2,3}$ indica quindi il peso della connessione che va dal terzo nodo del layer di input, $x_3$, al secondo nodo del layer di output, $a_2$. Per questa semplice rete, abbiamo:
$$ \begin{align} z_1 & = w_{1,1}x_1 + w_{1,2}x_2 + w_{1,3}x_3 + b_1\\[6pt] z_2 & = w_{2,1}x_1 + w_{2,2}x_2 + w_{2,3}x_3 + b_2\\[6pt] a_1 & = \sigma\left(z_1\right)\\[6pt] a_2 & = \sigma\left(z_2\right)\\[6pt] \end{align} $$dove $a_1$ rappresenta la probabilità stimata che l’esempio in ingresso, $\mathbf{x}$, appartenga alla label associata al primo nodo di output, mentre $a_2$ rappresenta la probabilità stimata che appartenga alla label associata al secondo nodo. Poiché utilizziamo una funzione Sigmoide indipendente per ciascun nodo, $a_1$ e $a_2$ non sono vincolati a sommare a $1$: ad esempio, potremmo ottenere contemporaneamente $a_1=0.8$ e $a_2=0.7$.
Possiamo codificare queste operazioni utilizzando la seguente notazione matriciale:
$$ \begin{align} \mathbf{a} & = \sigma \left(\mathbf{W} \mathbf{x} + \mathbf{b}\right) \label{sigma-matrix-1}\tag{6}\\[6pt] & = \sigma\left\{ \begin{bmatrix} w_{1,1} & w_{1,2} & \dots & w_{1,n} \\[6pt] w_{2,1} & w_{2,2} & \dots & w_{2,n} \\[6pt] \vdots & \vdots & \ddots & \vdots \\[6pt] w_{n_o,1} & w_{n_o,2} & \dots & w_{n_o,n}\\[6pt] \end{bmatrix} \begin{bmatrix} x_1\\[6pt] x_2\\[6pt] \vdots\\[6pt] x_n\\[6pt] \end{bmatrix} + \begin{bmatrix} b_1\\[6pt] b_2\\[6pt] \vdots\\[6pt] b_{n_o}\\[6pt] \end{bmatrix} \right\} \label{sigma-matrix-2}\tag{7}\\[6pt] & = \sigma\left\{ \begin{bmatrix} w_{1,1}x_1 + w_{1,2}x_2 + \dots + w_{1,n}x_n + b_1\\[6pt] w_{2,1}x_1 + w_{2,2}x_2 + \dots + w_{2,n}x_n + b_2\\[6pt] \vdots\\[6pt] w_{n_o,1}x_1 + w_{n_o,2}x_2 + \dots + w_{n_o,n}x_n + b_{n_o}\\[6pt] \end{bmatrix} \right\} = \begin{bmatrix} a_1\\[6pt] a_2\\[6pt] \vdots\\ a_{n_o}\\[6pt] \end{bmatrix} \label{sigma-matrix-3}\tag{8}\\[6pt] \end{align} $$dove $\mathbf{W}$ è una matrice di dimensione $n_o \times n$. La sua $j$-ma riga contiene il vettore dei pesi associato al $j$-mo nodo del layer di output:
$$ \mathbf{w}_j = \begin{bmatrix} w_{j,1} & w_{j,2} & \dots & w_{j,n} \end{bmatrix} $$mentre $\mathbf{b}$ è un vettore di $n_o$ elementi contenente un bias per ciascun nodo di output.
L’uso delle matrici permette di rappresentare in modo compatto ed efficiente i calcoli eseguiti dalle Reti Neurali. Le operazioni matriciali possono inoltre essere fortemente parallelizzate, sfruttando hardware come le GPU, e questo può accelerare notevolmente sia l’addestramento sia l’inferenza del modello. La rappresentazione matriciale rende inoltre naturale l’estensione a reti più grandi: aggiungere nuovi nodi significa aumentare le dimensioni delle matrici e dei vettori coinvolti, senza dover definire individualmente ogni singola equazione.
L’equazione precedente, $\eqref{sigma-matrix-1}$, mostra il calcolo applicato a una singola osservazione, $\mathbf{x}$. Possiamo estendere la stessa operazione a un insieme di $m$ osservazioni, eseguendo la Forward Propagation dell’intero insieme contemporaneamente. Reintroducendo la notazione “superscript”, abbiamo:
$$ \begin{align} \mathbf{A} &= \sigma \left(\mathbf{X} \mathbf{W}^{\intercal} + \mathbf{B}\right) \label{sigma-matrix-4}\tag{9}\\[6pt] &= \sigma\left\{ \begin{bmatrix} x^{(1)}_1 & x^{(1)}_2 & \dots & x^{(1)}_n\\[6pt] x^{(2)}_1 & x^{(2)}_2 & \dots & x^{(2)}_n\\[6pt] \vdots & \vdots & \ddots & \vdots \\[6pt] x^{(m)}_1 & x^{(m)}_2 & \dots & x^{(m)}_n\\[6pt] \end{bmatrix} \begin{bmatrix} w_{1,1} & w_{2,1} & \dots & w_{n_o,1}\\[6pt] w_{1,2} & w_{2,2} & \dots & w_{n_o,2}\\[6pt] \vdots & \vdots & \ddots & \vdots \\[6pt] w_{1,n} & w_{2,n} & \dots & w_{n_o,n}\\[6pt] \end{bmatrix} + \begin{bmatrix} b_1 & b_2 & \dots & b_{n_o}\\[6pt] b_1 & b_2 & \dots & b_{n_o}\\[6pt] \vdots & \vdots & \ddots & \vdots \\[6pt] b_1 & b_2 & \dots & b_{n_o}\\[6pt] \end{bmatrix} \right\} \label{sigma-matrix-5}\tag{10}\\[6pt] & = \sigma\left\{ \begin{bmatrix} \mathbf{w}_1\cdot\mathbf{x}^{(1)}+b_1 & \mathbf{w}_2\cdot\mathbf{x}^{(1)}+b_2 & \dots & \mathbf{w}_{n_o}\cdot\mathbf{x}^{(1)}+b_{n_o}\\[6pt] \mathbf{w}_1\cdot\mathbf{x}^{(2)}+b_1 & \mathbf{w}_2\cdot\mathbf{x}^{(2)}+b_2 & \dots & \mathbf{w}_{n_o}\cdot\mathbf{x}^{(2)}+b_{n_o}\\[6pt] \vdots & \vdots & \ddots & \vdots\\[6pt] \mathbf{w}_1\cdot\mathbf{x}^{(m)}+b_1 & \mathbf{w}_2\cdot\mathbf{x}^{(m)}+b_2 & \dots & \mathbf{w}_{n_o}\cdot\mathbf{x}^{(m)}+b_{n_o}\\[6pt] \end{bmatrix} \right\} \label{sigma-matrix-6}\tag{11}\\[6pt] & = \begin{bmatrix} a^{(1)}_1 & a^{(1)}_2 & \dots & a^{(1)}_{n_o}\\[6pt] a^{(2)}_1 & a^{(2)}_2 & \dots & a^{(2)}_{n_o}\\[6pt] \vdots & \vdots & \ddots & \vdots \\[6pt] a^{(m)}_1 & a^{(m)}_2 & \dots & a^{(m)}_{n_o}\\[6pt] \end{bmatrix} \label{sigma-matrix-7}\tag{12}\\[6pt] \end{align} $$La matrice risultante, $\mathbf{A}$, ha quindi dimensione $m \times n_o$, dove ogni riga corrisponde a un’osservazione e ogni colonna a una label. In particolare, l’elemento $a^{(i)}_j$ rappresenta la probabilità stimata che l’osservazione $\mathbf{x}^{(i)}$ appartenga alla $j$-ma label. Ad esempio, $a^{(1)}_1$ rappresenta la probabilità associata alla prima label per l’osservazione $\mathbf{x}^{(1)}$, mentre $a^{(1)}_2$ rappresenta la probabilità associata alla seconda label per la stessa osservazione.
Poiché ogni nodo applica indipendentemente la propria Sigmoide, le probabilità presenti sulla stessa riga di $\mathbf{A}$ non devono necessariamente sommare a $1$. Questo rende l’architettura adatta a problemi multi-label, nei quali una stessa osservazione può appartenere contemporaneamente a più classi. Nel caso di una vera classificazione multiclasse a classi mutuamente esclusive, invece, sarà più opportuno sostituire le Sigmoidi indipendenti con una funzione Softmax, in modo da ottenere una distribuzione di probabilità sulle classi la cui somma sia uguale a $1$.
Uso degli Hidden Layer
Finora ci siamo occupati esclusivamente di reti senza hidden layer: inseriamo i dati nel layer di input e calcoliamo direttamente i valori del layer di output. La capacità delle Reti Neurali di rappresentare relazioni complesse emerge quando tra il layer di input e quello di output vengono introdotti uno o più layer intermedi, chiamati hidden layer. L’esempio seguente presenta una semplice Rete Neurale con un layer di input, un hidden layer e un layer di output:

Per semplicità, il diagramma precedente contiene due nodi per ogni layer, ma layer diversi possono contenere un numero diverso di nodi. Indicando con $n$ il numero di feature del layer di input, possiamo calcolare l’output del $j$-esimo nodo dell’hidden layer come:
$$ \begin{align} z^h_j & = w^h_{j,1}x_1 + w^h_{j,2}x_2 + \cdots + w^h_{j,n}x_n + b^h_j \label{z^h_j-1}\tag{13}\\[6pt] & = \mathbf{w}^{h\intercal}_j\mathbf{x} + b^h_j \label{z^h_j-2}\tag{14}\\[6pt] a^h_j & = \sigma\left(z^h_j\right) \label{a^h_j}\tag{15}\\[6pt] \end{align} $$Il valore $z^h_j$ rappresenta quindi la combinazione lineare degli input ricevuti dal nodo, mentre $a^h_j$ rappresenta la sua attivazione, ottenuta applicando la funzione di attivazione $\sigma$. Ad esempio, considerando il diagramma precedente, per calcolare l’output del primo nodo dell’hidden layer, $a^h_1$, abbiamo:
$$ \begin{align} z^h_1 & = w^h_{1,1}x_1 + w^h_{1,2}x_2 + b^h_1\\[6pt] & = \mathbf{w}^{h\intercal}_1\mathbf{x} + b^h_1\\[6pt] a^h_1 & = \sigma\left(z^h_1\right)\\[6pt] \end{align} $$Le attivazioni prodotte dall’hidden layer diventano quindi gli input del layer successivo. Indicando con $n_h$ il numero di nodi dell’hidden layer, possiamo calcolare l’output del $j$-esimo nodo dell’output layer come:
$$ \begin{align} z^o_j & = w^o_{j,1}a^h_1 + w^o_{j,2}a^h_2 + \cdots + w^o_{j,n_h}a^h_{n_h} + b^o_j \label{z^o_j-1}\tag{16}\\[6pt] & = \mathbf{w}^{o\intercal}_j\mathbf{a}^h + b^o_j \label{z^o_j-2}\tag{17}\\[6pt] a^o_j & = \sigma\left(z^o_j\right) \label{a^o_j}\tag{18}\\[6pt] \end{align} $$Ad esempio, per calcolare l’output del secondo nodo dell’output layer, $a^o_2$, abbiamo:
$$ \begin{align} z^o_2 & = w^o_{2,1}a^h_1 + w^o_{2,2}a^h_2 + b^o_2\\[6pt] & = \mathbf{w}^{o\intercal}_2\mathbf{a}^h + b^o_2\\[6pt] a^o_2 & = \sigma\left(z^o_2\right)\\[6pt] \end{align} $$Lo stesso meccanismo può essere generalizzato a un numero arbitrario di layer. Più in generale, possiamo calcolare l’output del $j$-esimo nodo del layer $l$ come:
$$ \begin{align} z^l_j & = w^l_{j,1}a^{l-1}_1 + w^l_{j,2}a^{l-1}_2 + \cdots + w^l_{j,n_{l-1}}a^{l-1}_{n_{l-1}} + b^l_j \label{z^l_j-1}\tag{19}\\[6pt] & = \mathbf{w}^{l\intercal}_j\mathbf{a}^{l-1} + b^l_j \label{z^l_j-2}\tag{20}\\[6pt] a^l_j & = \sigma\left(z^l_j\right) \label{a^l_j}\tag{21}\\[6pt] \end{align} $$dove:
- $w^l_{t,s}$ è il peso associato alla connessione che va dal nodo sorgente $s$ del layer precedente $(l-1)$ al nodo destinazione $t$ del layer corrente $l$.
- $b^l_t$ è il bias associato al nodo $t$ del layer corrente $l$.
- $a^l_q$ è l’attivazione del $q$-esimo nodo del layer corrente $l$.
- $n_l$ indica il numero di nodi del layer $l$.
- $n_{l-1}$ indica il numero di nodi del layer precedente.
Nel caso del primo layer computazionale possiamo considerare direttamente:
$$ \mathbf{a}^{0}=\mathbf{x} $$in modo che la stessa notazione possa essere utilizzata per tutti i layer della rete. Per rendere i calcoli più compatti ed efficienti, possiamo utilizzare le operazioni matriciali.
Analisi Dimensionale
- La matrice dei pesi, $\mathbf{W}^l$, ha un numero di righe uguale al numero di nodi del layer corrente, $n_l$, e un numero di colonne uguale al numero di nodi del layer precedente, $n_{l-1}$.
- Il vettore delle attivazioni, $\mathbf{a}^l$, e quello dei bias, $\mathbf{b}^l$, contengono entrambi $n_l$ elementi.
- Il vettore delle attivazioni del layer precedente, $\mathbf{a}^{l-1}$, contiene $n_{l-1}$ elementi.
Avendo un’unica osservazione di input, $\mathbf{x}$, possiamo calcolare $\mathbf{a}^{l}$ come:
$$ \begin{align} \mathbf{a}^l & = \sigma \left(\mathbf{W}^{l} \mathbf{a}^{l-1} + \mathbf{b}^l\right) \label{a^l}\tag{22}\\[6pt] & = \sigma\left\{ \begin{bmatrix} w^l_{1,1} & w^l_{1,2} & \dots & w^l_{1,n_{l-1}}\\[6pt] w^l_{2,1} & w^l_{2,2} & \dots & w^l_{2,n_{l-1}}\\[6pt] \vdots & \vdots & \ddots & \vdots\\[6pt] w^l_{n_l,1} & w^l_{n_l,2} & \dots & w^l_{n_l,n_{l-1}}\\[6pt] \end{bmatrix} \begin{bmatrix} a^{l-1}_1\\[6pt] a^{l-1}_2\\[6pt] \vdots\\[6pt] a^{l-1}_{n_{l-1}}\\[6pt] \end{bmatrix} + \begin{bmatrix} b^l_1\\[6pt] b^l_2\\[6pt] \vdots\\[6pt] b^l_{n_l}\\[6pt] \end{bmatrix} \right\}\\[6pt] & = \sigma\left\{ \begin{bmatrix} w^l_{1,1}a^{l-1}_1 + w^l_{1,2}a^{l-1}_2 + \dots + w^l_{1,n_{l-1}}a^{l-1}_{n_{l-1}} + b^l_1\\[6pt] w^l_{2,1}a^{l-1}_1 + w^l_{2,2}a^{l-1}_2 + \dots + w^l_{2,n_{l-1}}a^{l-1}_{n_{l-1}} + b^l_2\\[6pt] \vdots\\[6pt] w^l_{n_l,1}a^{l-1}_1 + w^l_{n_l,2}a^{l-1}_2 + \dots + w^l_{n_l,n_{l-1}}a^{l-1}_{n_{l-1}} + b^l_{n_l}\\[6pt] \end{bmatrix} \right\} = \begin{bmatrix} a^l_1\\[6pt] a^l_2\\[6pt] \vdots\\[6pt] a^l_{n_l}\\[6pt] \end{bmatrix} \end{align} $$Possiamo ora estendere lo stesso calcolo a $m$ osservazioni di input, $\mathbf{x}^{(i)}$. La matrice $\mathbf{A}^l$, di dimensione $m \times n_l$, contiene le attivazioni calcolate dal layer $l$ per tutte le osservazioni: ogni $i$-ma riga contiene gli $n_l$ valori di attivazione prodotti per l’osservazione $\mathbf{x}^{(i)}$. In questo caso:
Analisi Dimensionale
- La matrice delle attivazioni, $\mathbf{A}^l$, ha $m$ righe e $n_l$ colonne.
- la matrice delle attivazioni del layer precedente, $\mathbf{A}^{l-1}$, ha $m$ righe e $n_{l-1}$ colonne.
- la matrice trasposta dei pesi ha dimensione $\mathbf{W}^{l\intercal}\in\mathbb{R}^{n_{l-1}\times n_l}$.
- la matrice $\mathbf{B}^l$ contiene una copia del vettore dei bias per ciascuna delle $m$ osservazioni e ha quindi dimensione $\mathbf{B}^l\in\mathbb{R}^{m\times n_l}$.
Abbiamo quindi:
$$ \begin{align} \mathbf{A}^l & = \sigma \left(\mathbf{A}^{l-1} \mathbf{W}^{l\intercal} + \mathbf{B}^l\right) \label{A^l}\tag{23}\\[6pt] & = \sigma\left\{ \begin{bmatrix} a^{l-1,(1)}_1 & a^{l-1,(1)}_2 & \dots & a^{l-1,(1)}_{n_{l-1}}\\[6pt] a^{l-1,(2)}_1 & a^{l-1,(2)}_2 & \dots & a^{l-1,(2)}_{n_{l-1}}\\[6pt] \vdots & \vdots & \ddots & \vdots\\[6pt] a^{l-1,(m)}_1 & a^{l-1,(m)}_2 & \dots & a^{l-1,(m)}_{n_{l-1}}\\[6pt] \end{bmatrix} \begin{bmatrix} w^l_{1,1} & w^l_{2,1} & \dots & w^l_{n_l,1}\\[6pt] w^l_{1,2} & w^l_{2,2} & \dots & w^l_{n_l,2}\\[6pt] \vdots & \vdots & \ddots & \vdots\\[6pt] w^l_{1,n_{l-1}} & w^l_{2,n_{l-1}} & \dots & w^l_{n_l,n_{l-1}}\\[6pt] \end{bmatrix} + \begin{bmatrix} b^l_1 & b^l_2 & \dots & b^l_{n_l}\\[6pt] b^l_1 & b^l_2 & \dots & b^l_{n_l}\\[6pt] \vdots & \vdots & \ddots & \vdots\\[6pt] b^l_1 & b^l_2 & \dots & b^l_{n_l}\\[6pt] \end{bmatrix} \right\}\\[6pt] & = \sigma\left\{ \begin{bmatrix} \mathbf{w}^{l\intercal}_1\mathbf{a}^{l-1,(1)}+b^l_1 & \mathbf{w}^{l\intercal}_2\mathbf{a}^{l-1,(1)}+b^l_2 & \dots & \mathbf{w}^{l\intercal}_{n_l}\mathbf{a}^{l-1,(1)}+b^l_{n_l}\\[6pt] \mathbf{w}^{l\intercal}_1\mathbf{a}^{l-1,(2)}+b^l_1 & \mathbf{w}^{l\intercal}_2\mathbf{a}^{l-1,(2)}+b^l_2 & \dots & \mathbf{w}^{l\intercal}_{n_l}\mathbf{a}^{l-1,(2)}+b^l_{n_l}\\[6pt] \vdots & \vdots & \ddots & \vdots\\[6pt] \mathbf{w}^{l\intercal}_1\mathbf{a}^{l-1,(m)}+b^l_1 & \mathbf{w}^{l\intercal}_2\mathbf{a}^{l-1,(m)}+b^l_2 & \dots & \mathbf{w}^{l\intercal}_{n_l}\mathbf{a}^{l-1,(m)}+b^l_{n_l}\\[6pt] \end{bmatrix} \right\}\\[6pt] & = \sigma\left\{ \begin{bmatrix} z^{l(1)}_1 & z^{l(1)}_2 & \dots & z^{l(1)}_{n_l}\\[6pt] z^{l(2)}_1 & z^{l(2)}_2 & \dots & z^{l(2)}_{n_l}\\[6pt] \vdots & \vdots & \ddots & \vdots\\[6pt] z^{l(m)}_1 & z^{l(m)}_2 & \dots & z^{l(m)}_{n_l}\\[6pt] \end{bmatrix} \right\} = \begin{bmatrix} a^{l(1)}_1 & a^{l(1)}_2 & \dots & a^{l(1)}_{n_l}\\[6pt] a^{l(2)}_1 & a^{l(2)}_2 & \dots & a^{l(2)}_{n_l}\\[6pt] \vdots & \vdots & \ddots & \vdots\\[6pt] a^{l(m)}_1 & a^{l(m)}_2 & \dots & a^{l(m)}_{n_l}\\[6pt] \end{bmatrix} \end{align} $$dove $a^{l(i)}_j$ indica l’output del $j$-esimo nodo del layer $l$, calcolato per l’$i$-ma osservazione di input. La Forward Propagation consiste quindi nel ripetere questa stessa operazione layer dopo layer:
$$ \mathbf{a}^{l-1} \longrightarrow \mathbf{z}^{l} = \mathbf{W}^{l}\mathbf{a}^{l-1}+\mathbf{b}^{l} \longrightarrow \mathbf{a}^{l} = \sigma(\mathbf{z}^{l}) $$Le attivazioni $\mathbf{a}^{l}$ prodotte da un layer diventano gli input del layer successivo, fino a raggiungere il layer di output della rete.
Implementazione di Forward Propagation
La classe MLPClassifier
Dopo aver definito le equazioni vettoriali che ci permettono di calcolare le attivazioni dei nodi di un dato layer, implementiamo un semplice modello di Rete Neurale con un singolo hidden layer. Poiché per ora non sappiamo ancora come eseguire il training del modello, utilizziamo valori dei parametri già ottimizzati, che caricheremo da alcuni file.
Nel costruttore della classe utilizziamo i parametri num_features, num_hidden e num_classes
per inizializzare le matrici dei pesi e i vettori dei bias dell’hidden layer e dell’output layer.
Per un generico layer $l$, la matrice $\mathbf{W}^l$ ha un numero di righe
uguale al numero di nodi del layer corrente e un numero di colonne uguale
al numero di nodi del layer precedente.
Il vettore $\mathbf{b}^l$ contiene invece un bias per ciascun nodo del layer corrente.
Per l’hidden layer:
- la matrice dei pesi,
self.weight_h, ha dimensione (num_hidden,num_features); - il vettore dei bias contiene
num_hiddenelementi e, nell’implementazione seguente, viene rappresentato come una matrice riga di dimensione (1,num_hidden).
Per l’output layer:
self.weight_oha dimensione (num_classes,num_hidden);self.bias_oha dimensione (1,num_classes).
Quando inizializziamo una matrice dei pesi, dobbiamo evitare di assegnare lo stesso valore a tutti i pesi, ad esempio $0$, perché ciò può impedire ai diversi neuroni di apprendere comportamenti differenti. Approfondiremo questo problema dopo aver studiato il funzionamento della Backpropagation. Per ora ci basta osservare che, se tutti i neuroni di uno stesso layer vengono inizializzati con gli stessi pesi e ricevono gli stessi input, produrranno gli stessi output. Durante l’addestramento riceveranno inoltre gli stessi aggiornamenti e continueranno quindi a comportarsi nello stesso modo. Questo fenomeno viene comunemente indicato come problema della simmetria: i diversi neuroni non riescono a specializzarsi nell’apprendimento di caratteristiche differenti dei dati. Una semplice strategia per evitare tale problema consiste nell’inizializzare i pesi con valori casuali. Nel nostro esempio utilizziamo il metodo Generator.normal per estrarre i pesi da una distribuzione normale con media $0$ e deviazione standard $0.1$:
import numpy as np
class MLPClassifier:
"""A Multi Layer Perceptron (MLP) with a single Hidden Layer."""
def __init__(self, num_features, num_hidden, num_classes, random_state=123):
"""
Initialize the weight matrices and bias vectors for the hidden and output layers.
Parameters
----------
num_features: int
the number of dataset features.
num_hidden : int
the number of nodes for the hidden layer.
num_classes : int
the number of nodes for the output layer.
"""
self.num_classes = num_classes
self.rng = np.random.default_rng(random_state)
# initialize hidden layer
self.weight_h = self.rng.normal(loc=0.0, scale=0.1, size=(num_hidden, num_features))
self.bias_h = np.zeros(num_hidden).reshape(1, -1)
# initialize output layer
self.weight_o = self.rng.normal(loc=0.0, scale=0.1, size=(num_classes, num_hidden))
self.bias_o = np.zeros(num_classes).reshape(1, -1)
Il Metodo Fit
Per ora, non sapendo ancora come eseguire l’addestramento del modello, ci limitiamo a caricare da alcuni file i valori dei parametri precedentemente ottimizzati:
def fit(self, X, y):
"""
Fit the data, computing the optimal values for the model parameters.
For now, just read the fitted parameters from some files.
"""
self.__load_params()
def __load_params(self, data_dir="data"):
self.weight_h = np.load(data_dir + "/weight_h.npy")
self.weight_o = np.load(data_dir + "/weight_out.npy")
self.bias_h = np.load(data_dir + "/bias_h.npy")
self.bias_o = np.load(data_dir + "/bias_out.npy")
In questa prima implementazione, quindi, fit non utilizza ancora direttamente X e y:
i due parametri vengono mantenuti nell’interfaccia del metodo perché, quando implementeremo
l’algoritmo di training, saranno proprio i dati X e le relative label y
a essere utilizzati per ottimizzare pesi e bias.
Il Metodo Predict
Il metodo predict chiama __forward per eseguire la Forward Propagation
e ottenere i valori continui prodotti dalle funzioni Sigmoidi dei nodi dell’output layer.
Per ottenere una singola classe prevista per ogni osservazione utilizziamo np.argmax,
che restituisce l’indice del nodo con il valore di attivazione maggiore:
def predict(self, X: np.ndarray) -> np.ndarray:
"""
Classify the input examples.
Parameters
----------
X : ndarray with shape (n_examples, n_features)
the examples to classify.
Returns
-------
ndarray with shape (n_examples, )
an array containing the target class label for each example.
"""
_, out = self.__forward(X) # shape: (n_examples, n_classes)
y_hat = np.argmax(out, axis=1) # shape: (n_examples, )
return y_hat
In questa architettura utilizziamo ancora una funzione Sigmoide indipendente
per ciascun nodo dell’output layer.
I valori prodotti dai diversi nodi non sono quindi vincolati a sommare a $1$.
L’uso di argmax impone comunque una decisione a classe singola,
selezionando il nodo che ha prodotto il valore maggiore.
Più avanti vedremo come utilizzare la funzione Softmax
per costruire un output più adatto a una classificazione multiclasse a classi mutuamente esclusive.
Il Metodo di Forward Propagation
Il metodo __forward implementa la fase di Forward Propagation,
applicando l’equazione $\eqref{A^l}$ prima all’hidden layer
e successivamente all’output layer.
Per l’hidden layer calcoliamo:
La matrice A_h ha dimensione (n_examples, n_hidden).
Ogni sua riga contiene le attivazioni dei neuroni dell’hidden layer
calcolate per una determinata osservazione.
Questi valori non rappresentano, in generale, probabilità di appartenenza alle classi:
sono rappresentazioni intermedie dei dati apprese dalla rete
e costituiscono gli input del layer successivo.
Per l’output layer abbiamo invece:
La matrice A_o ha dimensione (n_examples, n_classes).
In questa particolare architettura, ogni elemento dell’output può essere interpretato
come il valore di probabilità stimato dal corrispondente classificatore Sigmoide.
L’implementazione è quindi:
def __forward(self, X):
"""
Take in one or more training examples, and for each one of them,
get the class-membership probabilities returned from the hidden and output layers.
Parameters
----------
X : ndarray with shape (n_examples, n_features)
the examples to classify.
Returns
-------
A_h : ndarray with shape (n_examples, n_hidden)
hidden layer's activation matrix.
For each example, it contains the class probability returned by every node of the layer.
A_o : ndarray with shape (n_examples, n_classes)
output layer's activation matrix.
For each example, it contains the class probability returned by every node of the layer.
"""
# Hidden Layer:
# Z^(h) = X @ W^(h)^t + b^(h)
# A^(h) = σ(Z^(h))
Z_h = (X @ self.weight_h.T) + self.bias_h # shape: (n_examples, n_hidden)
A_h = self.__sigmoid(Z_h) # shape: (n_examples, n_hidden)
# Output Layer:
# Z^(o) = A^(h) @ W^(o)^t + b^(o)
# A^(o) = σ(Z^(o))
Z_o = (A_h @ self.weight_o.T) + self.bias_o # shape: (n_examples, n_classes)
A_o = self.__sigmoid(Z_o) # shape: (n_examples, n_classes)
return A_h, A_o
def __sigmoid(self, z):
return 1. / (1. + np.exp(-z))
Dal punto di vista dimensionale, per l’hidden layer abbiamo:
$$ (m\times n)(n_h\times n)^\intercal = (m\times n)(n\times n_h) = (m\times n_h) $$mentre per l’output layer:
$$ (m\times n_h)(n_o\times n_h)^\intercal = (m\times n_h)(n_h\times n_o) = (m\times n_o) $$Le dimensioni delle matrici utilizzate nel codice risultano quindi coerenti con le equazioni definite in precedenza.
Esempi di Classificazione
Confrontiamo ora i risultati ottenuti da un modello di Regressione Logistica
con quelli della nostra implementazione MLPClassifier,
utilizzando un hidden layer composto da $6$ nodi:
from sklearn.datasets import make_moons
from sklearn.linear_model import LogisticRegression
# config
num_hidden = 6
# get data from a non-linear dataset
X, y = make_moons(n_samples=100, noise=0.05, random_state=42)
# instance and train the models
log_reg = LogisticRegression(random_state=42)
log_reg.fit(X, y)
mlpc = MLPClassifier(num_features=2, num_hidden=num_hidden, num_classes=2)
mlpc.fit(X, y)
# plot the decision regions
classifiers = [log_reg, mlpc]
classifiers_name = ["Logistic Regression", "Multi-Layer Perceptron"]
plot_decision_regions_2D(X, y, classifiers, classifiers_name)

Dal grafico precedente possiamo osservare come MLPClassifier,
grazie alla presenza dell’hidden layer e delle relative funzioni di attivazione non-lineari,
sia in grado di rappresentare un confine decisionale non-lineare
e di adattarsi molto bene alla struttura del dataset make_moons.
Vediamo ora cosa accade ripetendo lo stesso esperimento con diverse istanze della rete,
ognuna caratterizzata da un numero crescente di nodi nell’hidden layer:
Con un hidden layer formato da un solo neurone, la capacità del modello di costruire un confine decisionale rimane fortemente limitata e, in questo esempio, otteniamo un risultato analogo a quello della Regressione Logistica. Aumentando il numero di neuroni aumenta invece la capacità della rete di combinare più trasformazioni non-lineari degli input. Con $3$ nodi possiamo già osservare come il confine decisionale inizi a seguire la forma delle due “mezze lune” presenti nel dataset. Con $5$ nodi, nel caso mostrato, il classificatore riesce ad adattarsi alla distribuzione dei dati fino a ottenere un errore di classificazione pari a $0$ sul dataset visualizzato.
La Rete Neurale come Approssimatore Universale di Funzioni
L’Hidden Layer
Consideriamo una rete con un hidden layer costituito da un singolo nodo. Il suo output viene calcolato come:
$$ \begin{align} a^h_1 & = \sigma\left(w^h_{1,1}x_1 + w^h_{1,2}x_2 + b^h_1\right) \label{a^h_1}\tag{24}\\[6pt] a^o_1 & = \sigma\left(w^o_{1,1}a^h_1 + b^o_1\right) \label{hlo-1}\tag{25}\\[6pt] & = \sigma\biggl[w^o_{1,1}\cdot{\sigma\left(w^h_{1,1}x_1 + w^h_{1,2}x_2 + b^h_1\right)} + b^o_1\biggr]\\[6pt] \end{align} $$La stessa rete, con un hidden layer formato da due nodi, restituisce:
$$ \begin{align} a^h_1 & = \sigma\left(w^h_{1,1}x_1 + w^h_{1,2}x_2 + b^h_1\right)\\[6pt] a^h_2 & = \sigma\left(w^h_{2,1}x_1 + w^h_{2,2}x_2 + b^h_2\right) \label{a^h_2}\tag{26}\\[6pt] a^o_1 & = \sigma\left(w^o_{1,1}a^h_1 + w^o_{1,2}a^h_2 + b^o_1\right) \label{hlo-2}\tag{27}\\[6pt] & = \sigma\biggl[w^o_{1,1}\cdot{\sigma\left(w^h_{1,1}x_1 + w^h_{1,2}x_2 + b^h_1\right)}\\ & \hspace{12pt} + w^o_{1,2}\cdot{\sigma\left(w^h_{2,1}x_1 + w^h_{2,2}x_2 + b^h_2\right)} + b^o_1\biggr]\\ \end{align} $$Se invece abbiamo un hidden layer con tre nodi:
$$ \begin{align} a^h_1 & = \sigma\left(w^h_{1,1}x_1 + w^h_{1,2}x_2 + b^h_1\right)\\[6pt] a^h_2 & = \sigma\left(w^h_{2,1}x_1 + w^h_{2,2}x_2 + b^h_2\right)\\[6pt] a^h_3 & = \sigma\left(w^h_{3,1}x_1 + w^h_{3,2}x_2 + b^h_3\right) \label{a^h_3}\tag{28}\\[6pt] a^o_1 & = \sigma\left(w^o_{1,1}a^h_1 + w^o_{1,2}a^h_2 + w^o_{1,3}a^h_3 + b^o_1\right) \label{hlo-3}\tag{29}\\[6pt] & = \sigma\biggl[w^o_{1,1}\cdot{\sigma\left(w^h_{1,1}x_1 + w^h_{1,2}x_2 + b^h_1\right)}\\ & \hspace{12pt} + w^o_{1,2}\cdot{\sigma\left(w^h_{2,1}x_1 + w^h_{2,2}x_2 + b^h_2\right)}\\ & \hspace{12pt} + w^o_{1,3}\cdot{\sigma\left(w^h_{3,1}x_1 + w^h_{3,2}x_2 + b^h_3\right)} + b^o_1\biggr]\\ \end{align} $$Possiamo generalizzare le equazioni $\eqref{hlo-1}$, $\eqref{hlo-2}$ e $\eqref{hlo-3}$ come:
$$ \begin{align} a^o_1 & = \sigma\left[\sum_{k=1}^{n_h}\left(w^o_{1,k}a^h_k\right) + b^o_1 \right] \label{hlo-gen}\tag{30}\\ \end{align} $$dove $n_h$ è il numero di nodi dell’hidden layer.
Le Funzioni Base
Riguardando il grafico di Figura 6, possiamo osservare come, aumentando il numero di nodi $a^h_j$ dell’hidden layer, aumenti anche la capacità della rete di rappresentare funzioni complesse. Ciascun nodo $a^h_j$ calcola una trasformazione non-lineare dei propri input, utilizzando un insieme specifico di pesi e un bias. Possiamo interpretare queste trasformazioni come funzioni base $\phi_j$, ovvero funzioni che possono essere combinate per costruire rappresentazioni più complesse.
Il termine “funzione base” viene qui utilizzato in senso intuitivo: le funzioni apprese dai neuroni non devono necessariamente costituire una base nel rigoroso significato dell’algebra lineare, né essere linearmente indipendenti. L’analogia è però utile per comprendere come la rete possa costruire funzioni complesse combinando trasformazioni più semplici.
Nell’algebra lineare, una base di uno spazio vettoriale è costituita da un insieme di vettori linearmente indipendenti che generano lo spazio. Ogni vettore appartenente allo spazio può quindi essere rappresentato come una combinazione lineare dei vettori della base. In uno spazio bidimensionale possiamo, ad esempio, utilizzare i vettori base:
$$ \mathbf{e}_1=(1,0), \qquad \mathbf{e}_2=(0,1) $$e rappresentare qualsiasi vettore $\mathbf{v}=(x,y)$ come:
$$ \mathbf{v}=x\mathbf{e}_1+y\mathbf{e}_2 $$Ad esempio, $\mathbf{v}=2\mathbf{e}_1+1.5\mathbf{e}_2$ può essere visualizzato come:

Un’idea simile può essere applicata agli spazi di funzioni: anziché combinare vettori, possiamo combinare delle funzioni. Ad esempio, possiamo considerare le funzioni:
$$ \begin{align} \phi_1(x) & = x\\[6pt] \phi_2(x) & = x^2\\[6pt] \phi_3(x) & = x^3\\[6pt] \end{align} $$e costruire una funzione più complessa mediante una loro combinazione lineare. Utilizzando $\phi_1$ e $\phi_2$, possiamo definire:
$$ \begin{align*} f(x) &= 3\phi_1(x)-2\phi_2(x)\\ &= 3x-2x^2 \end{align*} $$
In Figura 11 possiamo osservare come funzioni relativamente semplici possano essere combinate, ciascuna scalata da un proprio coefficiente, per costruire una funzione più complessa.
Un principio analogo viene sfruttato dalle Reti Neurali, nelle quali le attivazioni dei neuroni dell’hidden layer possono essere interpretate come funzioni non-lineari apprese direttamente dai dati.
Ritornando alla nostra rete neurale, possiamo utilizzare $\eqref{a^h_1}$, $\eqref{a^h_2}$ e $\eqref{a^h_3}$ per definire:
$$ \begin{align} \phi_1(\mathbf{x}) & = a^h_1(\mathbf{x}) = \sigma\left(w^h_{1,1}x_1 + w^h_{1,2}x_2 + b^h_1\right)\\[6pt] \phi_2(\mathbf{x}) & = a^h_2(\mathbf{x}) = \sigma\left(w^h_{2,1}x_1 + w^h_{2,2}x_2 + b^h_2\right)\\[6pt] \phi_3(\mathbf{x}) & = a^h_3(\mathbf{x}) = \sigma\left(w^h_{3,1}x_1 + w^h_{3,2}x_2 + b^h_3\right)\\[6pt] \end{align} $$Possiamo quindi riscrivere $\eqref{hlo-3}$ come una combinazione delle funzioni $\phi_j(\mathbf{x})$, ciascuna scalata dal proprio peso $w^o_{1,j}$:
$$ \begin{align} a^o_1 & = \sigma\left( w^o_{1,1}\phi_1(\mathbf{x}) + w^o_{1,2}\phi_2(\mathbf{x}) + w^o_{1,3}\phi_3(\mathbf{x}) + b^o_1 \right) \label{a^o_1-base}\tag{31}\\[6pt] \end{align} $$Generalizzando:
$$ \begin{align} a^o_1 & = \sigma\left[ \sum_{k=1}^{n_h} w^o_{1,k}\phi_k(\mathbf{x}) + b^o_1 \right] \label{a^o_1-gen-base}\tag{32}\\[6pt] & = \sigma\left( w^o_{1,1}\phi_1(\mathbf{x}) + w^o_{1,2}\phi_2(\mathbf{x}) + \cdots + w^o_{1,n_h}\phi_{n_h}(\mathbf{x}) + b^o_1 \right)\\[6pt] \end{align} $$L’Output Layer
In una Rete Neurale con un singolo hidden layer, l’output di ciascun nodo nascosto $a^h_j$ può quindi essere interpretato come una feature non-lineare appresa a partire dalle feature di input. L’output layer combina queste feature trasformate per costruire la previsione finale del modello.
Un aspetto fondamentale è che la rete non si limita ad apprendere i coefficienti utilizzati per combinare le feature trasformate. Essa apprende contemporaneamente:
- la forma delle trasformazioni prodotte dall’hidden layer, attraverso i suoi pesi e bias;
- il modo in cui queste trasformazioni vengono combinate, attraverso i pesi e i bias dell’output layer.
Entrambi gli insiemi di parametri vengono quindi appresi direttamente dai dati durante l’addestramento.
Dopo la trasformazione non-lineare effettuata dall’hidden layer, ogni osservazione viene rappresentata in un nuovo spazio delle feature latenti1:
$$ \pmb{\phi}(\mathbf{x}) = \left(\phi_1(\mathbf{x}), \dots, \phi_{n_h}(\mathbf{x})\right) $$In questo spazio, il $j$-esimo nodo dell’output layer calcola la combinazione lineare:
$$ \begin{align} z^o_j &= \sum_{k=1}^{n_h} \left( w^o_{j,k}\phi_k(\mathbf{x}) \right) + b^o_j \label{z^o_1-gen-base}\tag{33}\\[6pt] \end{align} $$L’equazione:
$$ z^o_j=0 $$definisce un iperpiano nello spazio delle feature latenti, che separa questo spazio in due regioni:
$$ z^o_j>0 \qquad\text{e}\qquad z^o_j<0 $$Applicando la funzione di attivazione Sigmoide:
$$ a^o_j=\sigma(z^o_j) $$il valore reale $z^o_j$ viene trasformato in un valore appartenente all’intervallo $(0,1)$. Nel caso di un output Sigmoide utilizzato per un problema di classificazione, questo valore può essere interpretato come la probabilità stimata associata alla $j$-esima classe o label.
Sebbene il nodo di output costruisca un confine lineare nello spazio delle feature latenti, lo stesso confine può risultare non-lineare nello spazio delle feature originali, perché lo spazio latente è stato ottenuto mediante trasformazioni non-lineari.
Riprendiamo l’esempio già visto nel contesto delle feature polinomiali, in cui consideriamo un dataset monodimensionale (1D) che non è linearmente separabile:

In questo esempio, una trasformazione non-lineare delle feature genera un nuovo spazio bidimensionale (2D) nel quale le osservazioni diventano linearmente separabili:

Aggiungendo neuroni all’hidden layer, la rete può costruire uno spazio delle feature latenti con un numero maggiore di dimensioni e con trasformazioni non-lineari più ricche. In modo intuitivo, possiamo immaginare queste trasformazioni come operazioni che “piegano” e “deformano” lo spazio delle feature originale. In uno spazio trasformato opportunamente, osservazioni che non erano linearmente separabili nello spazio originale possono diventarlo.
La non-linearità viene introdotta dall’applicazione delle funzioni di attivazione alle combinazioni lineari degli input. In assenza di funzioni di attivazione non-lineari, la composizione di più layer lineari rimarrebbe complessivamente una trasformazione lineare (o affine): aggiungere ulteriori layer non aumenterebbe quindi la capacità del modello di rappresentare relazioni non-lineari.
Questa capacità è collegata a un importante risultato teorico: il Teorema di Approssimazione Universale. Sotto opportune condizioni sulla funzione di attivazione, una Rete Neurale feed-forward con un singolo hidden layer e un numero sufficientemente grande di neuroni può approssimare con precisione arbitraria qualsiasi funzione continua definita su un dominio compatto. Questo significa che, almeno dal punto di vista della capacità rappresentativa, anche una rete con un singolo hidden layer può rappresentare funzioni estremamente complesse. Il teorema, tuttavia, non stabilisce quanti neuroni siano necessari, né garantisce che il processo di addestramento riesca a trovare i parametri necessari per ottenere una determinata approssimazione. All’aumentare del numero di neuroni nell’hidden layer aumenta generalmente anche il numero dei parametri apprendibili e quindi la capacità espressiva del modello. Questo permette alla rete di rappresentare strutture progressivamente più complesse, ma aumenta anche il costo computazionale e, se la capacità del modello diventa eccessiva rispetto ai dati disponibili, può aumentare il rischio di overfitting.
In una Rete Neurale con hidden layer, le feature latenti sono rappresentazioni interne dei dati apprese automaticamente dal modello. Non sono presenti direttamente nei dati originali, ma vengono costruite attraverso le trasformazioni applicate dai layer della rete. ↩︎



