2 / 5

Reti Neurali Artificiali e Deep Learning

Architettura e training delle reti neurali artificiali: percettrone, multilayer perceptron (MLP), funzioni di attivazione (ReLU, sigmoidea, softmax), loss function, gradient descent e back propagation.

Reti neurali

Le reti neurali, o ANN (Artificial Neural Network) è un metodo di machine learning che si ispira alla struttura e funzionamento di reti neurali biologiche presenti nei cervelli degli animali.

Una rete neurale è formata da unità connesse fra loro, chiamati nodi, che simulano il compito di un neurone. Sono connessi fra di loro tramite degli archi che simulano le sinapsi tra neuroni biologici.

Ogni neurone artificiale riceve dei segnali dai neuroni connessi ad esso, processa questi input e invia dei segnali ai neuroni collegati ad esso.

Il "segnale" è rappresentato da un numero reale, e l'output di ogni neurone è calcolato da una funzione non lineare della somma dei suoi input, chiamata la "funzione di attivazione".

La "forza" della connessione del segnale è determinata dai "pesi", ed è la parte che viene aggiustata durante il processo di learning

Pasted image 20241219213557.png

Percettrone

Il percettrone utilizza vari "neuroni" collegati ad un neurone finale per calcolare il valore dell'output. è visto come un modello a "sigolo strato", dato che contiene solo uno strato di neuroni:

Pasted image 20241219213844.png

Modelliamo un neurone in maniera semplice:

Un neurone può rievere segnali da altri neuroni, questi segnali sono combinati insieme, e se il risultato di questa combinazione supera una certa soglia, allora il neurone stesso lancerà un segnale, in caso che non superi la soglia, non fa nulla.

Possiamo modellare ciò vedendo come input numeri x1,...,xnx_1, ..., x_n a questo punto il neurone calcola y=w1x1+...+wnxny = w_1 x_1 + ... + w_nx _n dove w1,...,wnw_1, ..., w_n sono i valori dei "pesi".

Se y>ty > t, dove tt è un valore di "soglia", allora il neurone emette un segnale di "1" ai neuroni a cui è collegato, altrimenti il segnale avrà valore "0"

Il percettrone aggiunge un bias bRb \in \mathbb{R} che viene aggiunto alla lista pesata dei neuroni, serve per aggiustare il "confine" del nodo finale.

  • Con un bias =0= 0, il modello classificherà l'input solo in base al risultato dei neuroni
  • Con un bias >0>0, il modello ha bisogno di un valore maggiore dai neuroni per potersi attivare
  • Con un bias <0<0, il modello ha bisogno di un valore minore dai neuroni per potersi attivare

A questo punto, la funzione g(x)g(x), ritorna 0 se x<=x <= 0 e 1 se x>0x > 0.

Per trovare il valore del percettrone, usiamo y=g(wx+b)y = g(w \cdot x + b) dove y{0,1}y \in \{ 0, 1\}. Possiamo semplificare l'equazione rimuovendo il bias dalla equazione, e creando un nuovo neurone che ha come valore quello del bias

Training del percettrone

Abbiamo un training set DD di mm esempi e(1),...,e(m)e^{(1)}, ..., e^{(m)} dove ogni e(i)e^{(i)} ha associato la label output y(i)y^{(i)}. Fare il training del percettrone su DD vuol dire derivare i pesi w1,...wnw_1, ... w_n che rendono le predizioni y^(i)=g(we)\hat{y}^{(i)} = g(w \cdot e) il più vicino possibile a y(i)y^{(i)} per i=1,...,ni = 1, ..., n.

Euristica di Rosenblatt

è una tecnica usata per fare training di un percettrone.

Si parte da w1,...,wnw_1, ..., w_n inizializzati con valori randomici, a questo punto, per ogni esempio del training set con indice ii calcoliamo y^(i)\hat{y}^{(i)} come visto precedentemente, e aggiustiamo i pesi in questo modo:

wj=wj+λ(y(i)y^(i))xj(i)w_j = w_j + \lambda(y^{(i)} - \hat{y}^{(i)})x_j^{(i)}

dove λ>0\lambda > 0 è un iperparametro chiamato il "learning rate".

Assumendo che xj(i)>0x_j^{(i)} > 0, allora questo training funziona perchè:

  • Se y(i)>y^(i)y^{(i)} > \hat{y}^{(i)}, allora wjw_j incrementa, dato che λ>0\lambda > 0. Questo causerà y^(i)\hat{y}^{(i)} di aumentare il proprio valore, avvicinandosi al valore y(i)y^{(i)}
  • Se y(i)<y^(i)y^{(i)} < \hat{y}^{(i)}, allora wjw_j decresce. Questo causerà y^(i)\hat{y}^{(i)} di diminuire il proprio valore, avvicinandosi al valore y(i)y^{(i)}

In entrambi i casi, il valore del percettrone si avvicina a quello desiderato (i)^{(i)}.


Questo ciclo viene eseguito più volte, dove ogni iterazione è chiamata "epoch" (epoca). L'algoritmo terminerà se arriva ad un numero massimo di iterazioni, o quando l'errore è abbastanza piccolo.


Se abbiamo una funzione di attivazione lineare g()g(\cdot), il percettrone corrisponde ad un modello lineare che separa linearmente i dati. Questo funziona bene se i dati sono linearmente separabili.

Pasted image 20241219222143.png

Però, il percettrone, come anche altri modelli, possono usare funzioni di attivazioni non lineari.

Percettrone multi livello

Il percettrone a singolo livello è molto limitato, ma diventa molto più interessante quando mettiamo in sieme (uno dopo l'altro), più strati.

A questo punto vediamo il percettrone in 3 strati principali

  • L'input layer, che sono i neuroni iniziali con cui ci si interfaccia quando si da l'input da analizzare
  • L'output layer è il neurone finale dove viene comunicato l'output
  • L'hidden layer sono tutti gli strati intermedi tra l'input e l'ouput layer

Pasted image 20241220124314.png

Questo percettrone è chiamato "multi level perceptron" o MLP.

Ha un numero fissato di kk strati, ed ogni strato LpL_p ha un numero fisso di neuroni dpd_p. Ogni strato può avere un numero arbitrario di neuroni, eccetto l'ultimo che ne ha uno solo

Ogni neurone ηu(p)\eta_u^{(p)}, dello strato LpL_p, u=1,,dpu = 1, \dots, d_p, riceve come input, gli output y1(p1),,ydp1(p1)y_1^{(p-1)}, \dots, y_{d_{p-1}}^{(p-1)} di tutti i neuroni η1(p1),,ηdp1(p1)\eta_1^{(p-1)}, \dots, \eta_{d_{p-1}}^{(p-1)} dello strato precedente Lp1L_{p-1}.

L'output yu(p)y_u^{(p)} del neurone ηu(p)\eta_u^{(p)} viene calcolata come una combinazione lineare dei suoi input y1(p1),,ydp1(p1)y_1^{(p-1)}, \dots, y_{d_{p-1}}^{(p-1)} con i pesi w1,u(p),,wdp1,u(p)w_{1,u}^{(p)}, \dots, w_{d_{p-1},u}^{(p)}, seguita dall'applicazione di una funzione di attivazione g()g(\cdot):

yu(p)=g(vwv,u(p)yv(p1))y_u^{(p)} = g\left( \sum_v w_{v,u}^{(p)} y_v^{(p-1)} \right)

Pasted image 20241220125543.png

La matematica di un perceptrone multistrato può essere espressa in una forma più compatta e forse più comprensibile adottando una notazione matriciale.

Per semplificare la spiegazione di un layer con indice pp, usiamo "in\text{in}" per rappresentare l'input di questo strato (lo strato con indice p1p - 1), ed usiamo "out\text{out}" per rappresentare l'output dello strato corrente (lo strato con indice pp)

Sia yin=(y1in,,ydinin)Rdin\mathbf{y}^\text{in} = (y_1^\text{in}, \dots, y_{d_\text{in}}^\text{in}) \in \mathbb{R}^{d_\text{in}} un vettore (colonna) dind_\text{in}-dimensionale contenente gli output dello strato di input LinL_\text{in}.

Sia WRdout×din\mathbf{W} \in \mathbb{R}^{d_\text{out} \times d_\text{in}} una matrice (dout×din)(d_\text{out} \times d_\text{in})-dimensionale che raccoglie tutti i pesi dallo strato di input (dimensione dind_\text{in}) allo strato di output (dimensione doutd_\text{out}), ovvero:

Wout=[w1,1outwdin,1outw1,doutoutwdin,doutout]\mathbf{W^\text{out}} = \begin{bmatrix} w_{1,1}^\text{out} & \dots & w_{d_\text{in},1}^\text{out} \\ \vdots & \ddots & \vdots \\ w_{1,d_\text{out}}^\text{out} & \dots & w^\text{out}_{d_\text{in},d_\text{out}} \end{bmatrix}

Le uscite yout=(y1out,,ydoutout)Rdout\mathbf{y}^\text{out} = (y_1^\text{out}, \dots, y_{d_\text{out}}^\text{out}) \in \mathbb{R}^{d_\text{out}} possono essere espresse come:

yout=g(Woutyin)\mathbf{y}^\text{out} = g\left( \mathbf{W}^\text{out} \mathbf{y}^\text{in} \right)

dove g()g(\cdot) è applicata elemento per elemento a tutte le componenti del vettore Woutyin\mathbf{W}^\text{out} \mathbf{y}^\text{in}.

Pasted image 20241220131810.png

A questo punto denotiamo f(p)f^{(p)} come la funzione y(p)=g(W(p)y(p1))\mathbf{y}^{(p)} = g\left( \mathbf{W}^{(p)} \mathbf{y}^{(p-1)} \right) dello strato LpL_p i un MLP a kk strati. Abbiamo xx l'input dato all'MLP, allora la funzione finale che rappresenta l'intero MLP sarà del tipo:

MLP(x)=(f(k)f(k1)f(2)f(1))(x)=f(k)(f(k1)((f(2)(f(1)(x)))))\begin{aligned} M L P(\boldsymbol{x}) &= \left(f^{(k)} \circ f^{(k-1)} \circ \cdots \circ f^{(2)} \circ f^{(1)}\right)(\boldsymbol{x}) \\ &=f^{(k)}\left(f^{(k-1)}\left(\cdots\left(f^{(2)}\left(f^{(1)}(\boldsymbol{x})\right)\right)\right)\right) \end{aligned}

Nel caso in cui tutte le funzioni di attivazione siano lineari, le funzioni effettuerebbero semplicemente una moltiplicazione, a questo punto potremmo rappresentare il tutto tramite una produttoria dei pesi, ma a questo punto stiamo creando l'equivalente di un percettrone a singolo strato.


Gli MLP sono considerati come "feed forward neural networks", cioè che i dati fluiscono da sinistra verso destra (da input ad output)

Funzione di attivazione

Si può modificare le funzioni di attivazione all'interno di un MLP in maniera da migliorare il learning del modello e gestire task che non sono solo quelli di classificazione binaria.

Di solito la funzione di attivazione è definita come un iperparametro e scelto durante la fase di training tramite lo step di validazione.

ReLU

La funzione di attivazione ReLU è quella più consigliata per la maggior parte dei MLP. é una funzione non lineare ma che rimane molto vicina ad essere lineare, infatti è "lineare in parti", perchè è una funzione che continene due "parti" lineari.

Dato che è quasi lineare, conserva molte proprietà dei modelli lineari, ed è facile da ottimizzare tramite metodi del gradiente, spesso usati per allenare gli MLP

r(x)=max{0,x}={0,x<0x,x0r(x)=\max \{0, x\}= \begin{cases}0, & x<0 \\ x, & x \geq 0\end{cases}

Pasted image 20241220135419.png

Funzione sigmoidea

è la più comune approssimazione alla funzione "step", è monotona crescente ed ha limite 0 quando tende a -\infty e ad 1 quando tende a ++\infty

sigm(x)=11+exsigm(x) = \frac{1}{1+e^{-x}}

Pasted image 20241220135645.png

Funzione iperbolica

è monotona crescente, ed ha limite -1 quando tende a -\infty e ad 1 quando tende a ++\infty

Pasted image 20241220135736.png

Arctan

è l'inverso della funzione tangente, è monotona crescente, ed ha limite π2-\frac{\pi}{2} quando tende a -\infty e ad π2\frac{\pi}{2} quando tende a ++\infty

Pasted image 20241220135853.png

Softmax

è una funzione che converte un vettore dd-dimensionale in un vettore di valori reali che contiene la distribuzione di probabilità, cioè normalizzarle ad un valore compreso fra 0 e 1, e che sommano tutti ad 1.

σ:Rd(0,1)d\sigma:\mathbb{R}^d \rightarrow (0,1)^d

Softmax:

σ(x)i=exij=1dexj\sigma(\boldsymbol{x})_i=\frac{e^{x_i}}{\sum_{j=1}^d e^{x_j}}

è principalmente usato nell'output layer

Loss function

Fare il training di un modello richiede trovare i migliori pesi W={W(1),...,W(k)}W = \{W^{(1)}, ..., W^{(k)}\} che minimizzano la "distanza" fra l'esempio in DD e il proprio label.

Questa funzione che calcola la distanza è chiamata la "loss function", formalmente espressa come l:Rn×RnRl: \mathbb{R}^n \times R^{n^\prime} \rightarrow \mathbb{R} che calcola la distanza l(fw(xi),yi)l(f_w(x_i), y_i) fra la label fw(xi)f_w(x_i) calcolata dall'MLP e la vera label yiy_i. Se la differenza è un valore grande, allora vuol dire che sia numericamente che "concettualmente", i due risultati sono molto differenti.

Questo è applicato su un singolo esempio del dataset, possiamo definire la funzione che calcola l'errore complessivo semplicementte effettuando una somma pesata di ogni esempio nel dataset

L(W,fW,D)=1D(xi,yi)D(fW(xi),yi).\mathcal{L}\left(\boldsymbol{W}, f_W, D\right)=\frac{1}{|D|} \sum_{\left(\boldsymbol{x}_i, y_i\right) \in D} \ell\left(f_W\left(\boldsymbol{x}_i\right), \boldsymbol{y}_i\right) .

e quindi possiamo definire il problema di ML come un problema di ottimizzazione, dove dobbiamo minimizzare:

argminw L(W,fw,D)argmin_w \ \mathcal{L}(W, f_w, D)

Gradient descent

Questo problema di ottimizzazione può essere risolto tramite una tecnica di ottimizzazione chiamata "gradient descent", che è l'opposto dell'algoritmo di hill climbing.

è una funzione definita come ϕ:RdR\phi: \mathbb{R}^d \rightarrow \mathbb{R}, la "direzione" dello steepest descent in un punto zRdz \in \mathbb{R}^d corrisponde al negativo del gradiente di ϕ\phi in zz, denotato come:

zϕ=(ϕx1(z),,ϕxd(z)).\nabla_z \phi=\left(\frac{\partial \phi}{\partial x_1}(\mathbf{z}), \ldots, \frac{\partial \phi}{\partial x_d}(\mathbf{z})\right) .

che contiene le derivate parziali di ϕ\phi su zz

Il gradient descent inizia ad un punto casuale zRdz \in \mathbb{R}^d, ed applica iterativamente:

  • Calcola la direzione del deepest descent in zz, cioè zϕ-\nabla_z \phi
  • Aggiorna zz facendo un passo verso la direzione del steepest descent, cioè: z=zλzϕz = z - \lambda\nabla_z \phi dove lambda>0lambda > 0 è un iper parametro chiamato il "learning rate" che dice "quanto grande" è il passo da fare verso lo steepest descent fino a quando zz non decresce più la funzione obiettivo, e quindi si è arrivati ad un minimo locale

Pasted image 20241220143619.png

Nel contesto del MLP, il gradient descent è applicato durante la fase di training, applicandola ai pesi WW per minimizzare la funzione obiettivo ϕ=L(W,fw,D)\phi = \mathcal{L}(W, f_w, D)

Ora lo step chiave è di calcolare il gradiente della funzione ϕ\phi, che può essere complesso, dato che la funzione fwf_w implementata nell'MLP potrebbe essere molto complessa, dato che può essere composizione di molte funzioni, una per ogni layer.

Un modo per risolvere ciò sarebbe di applicare la "chain rule". Possiamo vedere la funzione da analizzare dell'MLP come:

MLP(x)=(f(k)f(k1)f(2)f(1)),M L P(\boldsymbol{x})=\left(f^{(k)} \circ f^{(k-1)} \circ \cdots \circ f^{(2)} \circ f^{(1)}\right),

La chain rule ci dice che la derivata D(f1f2)=(Df1f2)Df2D(f_1 \circ f_2) = (D f_1 \circ f_2)D f_2, dove DfDf indica la derivata di ff. A questo punto possiamo applicare ricorsivamente la chain rule per ottenere:

D(f1...fn)=k=1n(Dfkf(k+1)..n)D(f_1 \circ ... \circ f_n) = \prod_{k=1}^n\left(D f_k \circ f_{(k+1) .. n}\right)

dove fa..b=fafbf_{a . . b}=f_a \circ \cdots \circ f_b

Back propagation

Eseguendo l'applicazione ricorsiva della chain rule ci potrebbe portare ad una duplicazione della stessa derivata, ripetendo inutilmente dei calcoli.

Per evitare ciò possiamo usare un approccio di programmazione dinamica, chiamato "back propagation". è chiamata in questo modo perchè l'algoritmo prima effettua un passo in "avanti" dove le derivate di ogni strato sono calcolate e salvate, per poi effettuare un passo "indietro" dove i gradienti vengono calcolati, muovendosi dall'ultimo al primo strato, combinando tramite programmazione dinamica, le derivate intermedie usate durante il forward pass


Il gradient descent ha delle limitazioni:

  • funziona solo se la funzione da minimizzare è differenziabile, cosi da poter calcolare il gradiente
  • ci si potrebbe fermare ad un minimo locale
  • scegliere un λ\lambda corretto è difficile, se usiamo un λ\lambda troppo grande, potremmo saltare il minimo, se λ\lambda è piccolo, potremmo convergere troppo lentamente
  • aumentare il numero degli strati potrebbe causare i gradienti di diventare molto piccoli o molto grandi, causando problemi di rappresentazione numerica
    • Durante lo step backward, durante il calcolo dei gradienti dall'output layer all'input layer, i gradienti diventano man mano più piccoli più andiamo avanti, convergendo a 0. Questo causa i pesi ad essere aggiornati in maniera minima, e quindi mai convergere al minimo locale
    • Allo stesso modo, i gradienti potrebbero essere molto grandi, causando grandi cambiamenti nei pesi, causando il modello a divergere.

Gradient descent stocastico

Calcolare l'errore complessivo del dataset ad ogni step del gradient descent può essere molto dispendioso. Per risolvere questo problema, usiamo una variante del gradient descent che approssima L(W,fw,D)\mathcal{L}(W, f_w, D) calcolando la loss function solo su un subset (piccolo) di MDM \subseteq D, che chiamiamo "minibatch", e quindi calcolando la somma della loss function complessiva solo su questo subset.

  • Usiamo l'intero training set D che è mischiato "randomicamente" e suddiviso in minibatch.
  • Ogni minibatch è usato come singolo step del gradient descent
  • Una volta aver processato tutti i minibatch, ci ritroviamo al punto in cui abbiamo analizzato tutti i dati nel nostro dataset, quindi consideriamo questo come una singola epoca di training
  • Ripetiamo questo proccesso (randomizzando DD ogni volta) per un certo numero di epoche.

Pasted image 20241220151127.png

Loss functions

Elenchiamo alcune loss function più usate. Indicheremo con y^=fw(x)\hat{y} = f_w(x) è la predizione, mentre yy la label corretta.

LpL_p distance

è una metrica utile per problemi di regressione, la dimensionalità di y^\hat y e yy deve essere uguale.

(y^,y)=y^yp\ell(\widehat{\boldsymbol{y}}, \boldsymbol{y})=\|\widehat{\boldsymbol{y}}-\boldsymbol{y}\|^p

NLL (Negative log-likelihood)

Assumiamo che ogni elemento di y^\hat y rappresenti la probabilità che quell'esempio faccia parte di una certa classe, e che la classe corretta sia il qq-esimo elemento.

(y^,y)=log y^q\ell(\widehat{\boldsymbol{y}}, \boldsymbol{y})=- log \ \hat y_q

CE (Cross entropy)

La funzione NLL richiede che i valori y^\hat y rappresentino una distribuzione di probabilità. il CE è semplicemente la funzione NLL ma che trasforma, tramite uno strato softmax, qualsiasi vettore numerico in una distribuzione di probabilità.

Softmax:

SOFTMAX(y^)=ey^ij=1ney^j\operatorname{SOFTMAX}(\widehat{\boldsymbol{y}})=\frac{e^{\hat{y}_i}}{\sum_{j=1}^n e^{\hat{y}_j}}

CE:

(y^,y)=logey^qj=1ney^j=y^q+logj=1ney^j\ell(\widehat{\boldsymbol{y}}, \boldsymbol{y})=-\log \frac{e^{\widehat{\boldsymbol{y}}_q}}{\sum_{j=1}^n e^{\hat{\boldsymbol{y}}_j}}=-\widehat{\boldsymbol{y}}_q+\log \sum_{j=1}^n e^{\hat{\boldsymbol{y}}_j}

BCE (Binary cross entropy)

Corrisponde a NLL però nel caso di classificazione binaria. Assume che y^[0,1]\hat y \in [0,1] sia uno scalare e che y{0,1}y \in \{0,1\} sia uno scalare. y^\hat y rappresenta la probabilità che y=1y = 1, e 1y^1-\hat y rappresenta la probabilità che y=0y = 0

(y,y^)=(ylogy^+(1y)log(1y^))\ell(\mathbf{y}, \hat{\mathbf{y}}) = - \left( \mathbf{y} \log \hat{\mathbf{y}} + (1 - \mathbf{y}) \log(1 - \hat{\mathbf{y}}) \right)

Deep learning vs Machine learning vs AI

Il termine deep learning si riferisce a un insieme di tecniche di machine learning in cui i modelli sono rappresentati come circuiti algebrici complessi, con connessioni i cui pesi (o "forze") possono essere modificati durante l'addestramento.

La parola "deep" ("profondo") indica che questi circuiti sono organizzati in molti strati: ciò implica che i percorsi di calcolo dagli input agli output attraversano numerosi passaggi.

Questi circuiti algebrici corrispondono a reti neurali artificiali. Di conseguenza, il deep learning utilizza reti neurali con molti strati di neuroni artificiali connessi tra loro.

Pasted image 20241220163348.png Pasted image 20241220163411.png

Teorema dell'approssimazione universale

è un teorema molto importante sulla rappresentazione e potenza approssimativa delle reti neurali di funzioni arbitrarie.

Il teorema dice che un MLP con solo 1 output layer lineare e un hidden layer con qualsiasi funzione di attivazione non lineare, può approssimare qualsiasi funzione Borel misurabile, cioè qualsiasi funzione continua su un sottoinsieme chiuso e delimitato di Rn\mathbb{R}^n, con qualsiasi errore non zero desiderato, a condizione che siano dati abbastanza hidden units.

Quindi ci dice che un MLP con un singolo hidden layer (ma non ci dice quanto grande questo hidden layer sia) riesce ad approssimare qualsiasi funzione Borel misurabile con un arbitrario livello di precisione.


Di solito, in pratica, nelle deep MLP, rappresentazioni più complesse sono costruite sopra rappresentazioni più semplici, arrivando ad una rappresentazione man mano più astratta e significativa.

Pasted image 20241220164232.png