Esplora E-book
Categorie
Esplora Audiolibri
Categorie
Esplora Riviste
Categorie
Esplora Documenti
Categorie
• DEVTALKS
• DESIGN PATTERN
O
O
O
▪
O
O
▪
O
▪
▪
▪
O
O
▪
O
O
• EXTRA
• PART NER
non puoi usare il Deep Learning se non sai come funziona una Rete Neurale Artificiale.
Quando ci si approccia per la prima volta a Deep Learning e Reti Neurali si viene subito
intimoriti dalla matematica che c’è dietro e si finisce a guardare le API di Tensorflow o a fare
copia-incolla di pezzi di codice presi in giro per il web aggiungendo strati su strati, come
dicevano gli scienziati dell’antica Roma, ad mentula di canis.
Lo scopo di questo articolo è demistificare la complessità delle Reti Neurali e mostrare che in
fondo, se si sa come interpretarla, la matematica che c’è dietro non è nulla di terrificante.
PREMESSE
• In questo tutorial creeremo una Shallow Neural Network, cioè una Rete Neurale con
un’unico strato nascosto. Ampliare la Rete Neurale al caso di strati nascosti multipli
non è per nulla complesso, a livello matematico non cambia nulla, a livello di codice
bisogna lavorare un po’ di più. Prova a farlo come esercizio, oppure se vuoi vedere una
mia implementazione fammelo sapere nei commenti.
• La rete che andremo a creare è per problemi di classificazione binaria, quindi quando
dobbiamo distinguere un caso positivo da uno negativo (es. distinguere uomo da
donna), se vogliamo utilizzarla per altri tipi di problemi, come la classificazione
multiclasse o la regressione, dobbiamo soltanto modificare la funzione di attivazione
sullo strato di output e la funzione di costo che utilizziamo. Anche qui, se vuoi vedere
degli esempi ad-hoc, fammelo sapere.
PREREQUISITI
• In questo tutorial implementeremo una Rete Neurale da Zero con Python usando
soltanto Numpy, popolare libreria per il calcolo numerico che ci permette di operare su
matrici e vettori. Se sei un programmatore ma non conosci Python dai uno sguardo al
mio articolo Programmazione con Python: le 10 Cose da Sapere. Se non sai proprio
nulla di programmazione puoi partire con il mio corso gratuito di un’ora e
mezza Programmazione con Python in 90 minuti.
• In questo tutorial darò per scontato che tu sappia cosa è e a cosa serve il Machine
Learning, se non lo sai dai uno sguardo al mio articolo Cosa è il Machine Learning.
• Conoscere come funziona una Rete Neurale Artificiale, anche ad un livello molto
intuitivo, ti può agevolare a seguire questo tutorial, dai uno sguardo all’articolo Deep
Learning Svelato: ecco come funziona una Rete Neurale Artificiale.
• Un po’ di matematica la devi sapere, sennò dove vuoi andare eh ?
CREIAMO UNA CLASSE PER LA NOSTRA RETE NEURALE
Creiamo una classe NeuralNetwork, nel costruttore passeremo soltanto un parametro
hidden_layer_size, che conterrà il numero di nodi (o neuroni) dello strato nascosto.
class NeuralNetwork:
def __init__(self, hidden_layer_size=100):
self.hidden_layer_size=hidden_layer_size
Una metrica comune per problemi di classificazione è l’accuracy che in sostanza indica
semplicemente la percentuale di predizioni che il nostro modello ha azzeccato e, senza voler
utilizzare probabilità condizionali, possiamo definire così:
1N∑i=1N(yi==yi^)
dove 𝑦̂ è il vettore con le predizioni del modello mentre 𝑦̂ è il vettore con i valori reali.
∑ è il simbolo della sommatoria, ad esempio ∑i=1N(vi) ci tonerà la somma di tutti gli N valori
all’interno del vettore v.
L’accuracy è una funzione di scoring che ritorna un valore compreso tra 0 e 1, dove un valore
maggiore indica una qualità migliore del modello (eccezion fatta per casi di overfitting, ma di
questo parleremo più avanti).
Aggiungiamo alla classe un metodo per calcolare l’accuracy.
Uno dei limiti di questa metrica è che non tiene conto della probabilità che una predizione sia
corretta, quindi un’errore grossolano assume lo stesso peso di un’errore minore. Per questo
motivo è sempre una buona idea affiancare l’accuracy ad un altra metrica che tiene conto di
questa informazione, cioè la Cross Entropy anche conosciuta come Log Loss, che è definita in
questo modo:
J(y,y^))=−1N∑i=1N(yi⋅log(ai)+(1−yi)⋅(1−ai)
dove a sono le probabilità di appartenenza alla classe positiva ritornate dal modello,
mentre y sono sempre i valori reali.
A differenza dell’accuracy, la log loss è una funzione di costo, quindi un suo valore minore
indica una migliore qualità del modello.
LA PREDIZIONE
La predizione è la fase in cui utilizziamo i coefficienti appresi dal modello per classificare una
data osservazione. In un modello lineare, come la regressione logistica, la classificazione
avviene semplicemente moltiplicando le features dell’osservazione con i rispettivi pesi e
sommando il bias, per poi far passare tale valore attraverso una funzione di attivazione (della
quale parleremo dopo).
In una Rete Neurale il discorso è più complesso, abbiamo più coefficienti disposti su più strati
disposti in sequenza, nel caso di una Shallow Neural Network abbiamo tre strati, uno di input,
uno nascosto e uno di output, 2 matrici di pesi, una che collega ogni nodo dello strato input ad
ogni neurone dello strato nascosto e una che collega ogni nodo dello strato nascosto all’unico
nodo dello strato di output, oltre ovviamente ai due rispettivi vettori con i bias, che vengono
sempre trascurati :(.
In una rete neurale la predizione avviene a cascata, l’input della rete arriva allo strato di input,
viene moltiplicato per i pesi dello strato e viene sommato il bias in questo modo:
z1=W1x+b1
In questo caso abbiamo fatto uso di matrici e vettori, il prodotto tra W1 e x è un prodotto
scalare tra matrici (in realtà tra una matrice e un vettore in questo caso, ma il discorso è lo
stesso) ed è così definito:
Ora l’output dello strato di input diventerà l’input dello strato nascosto, questo processo è
conosciuto come Forward Propagation (Propagazione in Avanti) e queste sono le sue
equazioni:
z1=W1x+b1a1=ϕ(z1)z2=W2a1+b2a2=ϕ(z2)
i vari vettori z sono gli output lineari dello strato, anche chiamati net input, come vedi a
questi vettori viene applicata una funzione ϕ, questa funzione è la funzione di attivazione di
cui parlavamo prima e ci permette di aggiungere la non linearità alla nostra rete, senza di essa
una rete neurale, anche una molto profonda, porterebbe agli stessi risultati di un semplice
modello lineare come la regressione logistica.
Esistono diverse funzioni di attivazione e variano in base al tipo di strato, per gli strati
nascosti la più utilizzata e la Rectified Linear Unit (ReLu) che è così definita:
Per gli strati di output la funzione di attivazione da utilizzare dipende dal tipo di problema che
stiamo affrontando, per una classificazione binaria bisogna usare la sigmoide, che è così
definita.
σ(z)=11+e−z
z1=W1x+b1a1=g(z1)z2=W2a1+b2a2=σ(z2)
Che è quella cache dove salviamo i risultati intermedi della rete ? Di questo parleremo più
avanti, per adesso fidati.
Ora, l’ultimo strato ci ritorna la probabilità che l’osservazione in input appartenga alla classe
positiva:
Questo nel caso standard, poi possiamo anche variare in base ai nostri obiettivi di precision e
recall, ma questo merita un’articolo a parte.
Per standard, qualora la probabilità fosse esattamente del 50% classifichiamola come
positiva, anche se non è attendibile, in ogni caso insieme ad una classificazione dobbiamo
sempre prendere in considerazione la probabilità della sua correttezza.
L’ADDESTRAMENTO
L’addestramento della maggior parte dei modelli di machine learning si basa sull’utilizzo di un
algoritmo di ottimizzazione, il più comune è il Gradient Descent.
Okay, detto così potrebbe non sembrare facile affatto, specialmente se non ricordi cosa è una
derivata, quindi facciamo un piccolo ripasso di analisi matematica.
La derivata di una funzione è un’altra funzione derivata da essa (e da qui il nome) che indica
quanto velocemente la funzione sta crescendo/decrescendo in un determinato punto.
Se in un dato punto la funzione sta crescendo in maniera molto rapida, la sua derivata sarà un
valore positivo grande, al contrario se la funzione sta decrescendo in maniera molto rapida la
sua derivata sarà un valore negativo molto grande. Se invece la funzione è costante, quindi
mantiene lo stesso valore, allora la derivata varrà 0.
Esistono 3 diversi tipi di notazioni per indicare una derivata: la notazione di Lagrange, quella
di Newton e quella di Leibniz.
In questo tutorial useremo quella di Leibniz, che poi è quella più utilizzata nel caso di funzioni
a più variabili.
Parlando di funzioni a più variabili, se una funzione ha più variabili allora ha più derivate, dato
che ogni variabile può contribuire alla variazione della funzione in maniera differente, in
questo caso si parla di derivate parziali, che messe insieme formano il gradiente della
funzione, che si indica con il simbolo nabla (il triangolo sotto sopra che vedi qui sotto)
∇f(x,y,z)=[dfdx,dfdydfdz]
In parole povere il Gradient Descent funziona così: i valori ‘ideali’ dei coefficienti sono quelli
che ci permettono di ottenere il valore minore per la funzione di costo, cioè quelli che la
minimizzano, sommando iterativamente il valore delle rispettive derivate parziali della
funzione di costo tendiamo a ‘spingere’ i coefficienti verso tale punto di minimo.
Il learning rate ci permette di impostare la forza di tale spinta, o meglio la dimensione di ogni
step (epoca) del Gradient Descent.
Implementiamo il Gradient Descent per la nostra Rete Neurale, per adesso ignora la funzione
per calcolare le derivate parziali, ci arriveremo tra pochissimo:
Learning Rate e numero di Epoche sono due dei tanti iperparametri di una rete neurale, cioè
quei valori che tocca a noi definire manualmente.
Per una rete neurale il numero di epoche andrebbe sempre impostato almeno a 100, mentre il
Learning Rate va cercato in uno spazio di potenze di 10 che va da 10−4 (0.0001) a 10.
Come facciamo a calcolare il gradiente, cioè le derivate parziali della funzione di costo rispetto
ai vari coefficienti ? Come si dice da me “e qua casca lo scecco” !
Se si fosse trattato di una regressione logistica, differenziare la funzione di costo sarebbe stato
un gioco da ragazzi della quinta liceo classico rimandati per tre anni di fila in matematica, ma
nel caso di una rete neurale è molto più complesso, infatti una rete neurale è formata da più
funzioni composte, cioè funzioni che hanno come argomento altre funzioni, se non ci credi
pensa che le equazioni della forward propagation possono anche essere espresse come
un’unica equazione incomprensibile, questa qui nel caso di una Shallow Neural Network
a2=σ(W2⋅g(W1x+b1)+b2)
Ora noi dobbiamo riuscire a sapere quanto ogni coefficiente di ogni strato contribuisce
all’errore della rete e questo problema non è per nulla banale ! Infatti gli scienziati ci si sono
arrovellati sopra per 50 anni, fino al 1984, quando si arrivò ad una soluzione,
la Backpropagation (propagazione all’indietro o retropropagazione).
Ma come ? Ma perché ?
L’algoritmo si basa su una proprietà delle derivate, chiamata Chain Rule (Regola della
Catena), che ci dice che la derivata di una funzione composta è pari al prodotto della derivata
più esterna, avente come argomento la funzione interna, per la derivata della funzione
interna.
f(x)=f(g(x))
che quindi è una funzione composta, possiamo calcolare la sua derivata come
dfdx=dfdgdgdx
Applicando la chain rule, otteniamo le seguenti equazioni per il calcolo delle derivate parziali
dJdz2=A2−Y^dJdW2=1N(A2T⋅dJdz2)dJdb2=1N∑i=1N(dJdz2)dJdz1=dJdz2⋅W2T∗g′(z1)dJdW1
=1N(XT⋅dJdz1)dJdb1=1N∑i=1N(dJdz1)
Ricordi la cache con i risultati intermedi della forward propagation ? Bene è qui che ci serve !
Infatti come vedi per poter eseguire la backpropation, e quindi per poter applicare la chain
rule, abbiamo bisogno di questi valori.
• X è la matrice con gli esempi che utilizziamo per addestrare la rete neurale, ogni
colonna rappresenta una feature e ogni riga contiene un’esempio.
• La T all’apice indica la matrice trasposta, cioè la matrice ottenuta invertendo le righe
con le colonne.
• g′(z1) è la derivata della funzione ReLu rispetto a z1 (in questo caso abbiamo usato la
notazione di Lagrange) che è la seguente:
Abbiamo tutto ciò che ci serve per implementare la backpropagation, mettiamoci all’opera
Esistono tecniche sofisticate per l’inizializzazione intelligente dei pesi, ma nel nostro caso
stiamo realizzando una rete neurale con un solo strato nascosto, quindi non dovremmo
preoccuparci di questi problemi, selezioniamo i pesi da una semplice distribuzione normale,
cioè una distribuzione con media pari a 0 e deviazione standard pari a 1.
import numpy as np
class NeuralNetwork:
def __init__(self, hidden_layer_size=100):
self.hidden_layer_size=hidden_layer_size
def _init_weights(self, input_size, hidden_size):
self._W1 = np.random.randn(input_size, hidden_size)
self._b1 = np.zeros(hidden_size)
self._W2 = np.random.randn(hidden_size,1)
self._b2 = np.zeros(1)
def _accuracy(self, y, y_pred):
return np.sum(y==y_pred)/len(y)
def _log_loss(self, y_true, y_proba):
return -np.sum(np.multiply(y_true,np.log(y_proba))+np.multiply((1-y_true),np.log(1-y_proba)))/len(y_true)
def _relu(self, Z):
return np.maximum(Z, 0)
def _sigmoid(self, Z):
return 1/(1+np.power(np.e,-Z))
def _relu_derivative(self, Z):
dZ = np.zeros(Z.shape)
dZ[Z>0] = 1
return dZ
def _forward_propagation(self, X):
Z1 = np.dot(X,self._W1)+self._b1
A1 = self._relu(Z1)
Z2 = np.dot(A1,self._W2)+self._b2
A2 = self._sigmoid(Z2)
self._forward_cache = (Z1, A1, Z2, A2)
return A2.ravel()
def predict(self, X, return_proba=False):
proba = self._forward_propagation(X)
y = np.zeros(X.shape[0])
y[proba>=0.5]=1
y[proba<0.5]=0
if(return_proba):
return (y, proba)
else:
return proba
def _back_propagation(self, X, y):
Z1, A1, Z2, A2 = self._forward_cache
m = A1.shape[1]
dZ2 = A2-y.reshape(-1,1)
dW2 = np.dot(A1.T, dZ2)/m
db2 = np.sum(dZ2, axis=0)/m
dZ1 = np.dot(dZ2, self._W2.T)*self._relu_derivative(Z1)
dW1 = np.dot(X.T, dZ1)/m
db1 = np.sum(dZ1, axis=0)/m
return dW1, db1, dW2, db2
def fit(self, X, y, epochs=200, lr=0.01):
self._init_weights(X.shape[1], self.hidden_layer_size)
for _ in range(epochs):
Y = self._forward_propagation(X)
dW1, db1, dW2, db2 = self._back_propagation(X, y)
self._W1-=lr*dW1
self._b1-=lr*db1
self._W2-=lr*dW2
self._b2-=lr*db2
def evaluate(self, X, y):
y_pred, proba = self.predict(X, return_proba=True)
accuracy = self._accuracy(y, y_pred)
log_loss = self._log_loss(y, proba)
return (accuracy, log_loss)
Utilizziamo lo stesso dataset per testare la nostra rete neurale, questa volta facendo
totalmente a meno di scikit-learn.
Importiamo il dataset direttamente dalla Repository Github dei Tutorial di ProfessionAI, per
farlo possiamo utilizzare Pandas, una popolare libreria Python per l’analisi dati.
import pandas as pd
CSV_URL =
"https://raw.githubusercontent.com/ProfAI/tutorials/master/Come%20Creare%20una%20Rete%20Neurale%20da%20Ze
ro/breast_cancer.csv"
breast_cancer = pd.read_csv(CSV_URL)
Il risultato sarà un DataFrame, una struttura dati che Pandas usa per rappresentare dati
tabulari, possiamo avere una preview del suo contenuto usando il metodo .head().
Il nostro dataset contiene in totale 563 righe (e quindi esempi) e 31 colonne, cioè 30 features
e un target, che è la colonna “malignant”.
X = breast_cancer.drop("malignant", axis=1).values
y = breast_cancer["malignant"].values
Ora dobbiamo dividere ogni array in due array distinti, uno per l’addestramento e uno per il
test. Questa divisione serve per poter verificare le reali capacità predittive del modello,
testandolo su dati che non ha già visto durante la fase di addestramento.
E’ buona norma portare le features in un range di valori comune, questo può velocizzare
anche di tanto la fase di addestramento.
Xnorm=X–XminXmax−Xmin
X_max = X_train.max(axis=0)
X_min = X_train.min(axis=0)
X_train = (X_train - X_min)/(X_max-X_min)
X_test = (X_test - X_min)/(X_max-X_min)
Perfetto ! Adesso creiamo la nostra rete con 10 nodi sullo strato nascosto, addestriamola sul
set di addestramento per 500 epoche e valutiamola sul set di test.
model = NeuralNetwork()
model.fit(X_train, y_train, epochs=500, lr=0.01)
model.evaluate(X_test, y_test)
I risultati che ho ottenuto io sono circa 0.981 di accuracy e circa 0.089 di log loss, dato che i
pesi vengono inizializzati a valori casuali il risultato può lievemente variare tra diverse
esecuzioni della rete.
exams_df =
pd.read_csv("https://raw.githubusercontent.com/ProfAI/tutorials/master/Come%20Creare%20una%20Rete%20Neurale%
20da%20Zero/exam%20results.csv")
X_new = exams_df.values
X_new = (X_new - X_min)/(X_max-X_min)
Ora utilizziamo il metodo predict per classificare i risultati di tali esami, in modo da
identificare eventuali tumori maligni, ottenendo anche la probabilità
y_pred, y_proba = model.predict(X_new, return_proba=True)
Stampiamo il risultato.
Quando la probabilità associata non è alta andrebbero eseguiti ulteriori esami di verifica,
specialmente nel caso di un tumore classificato come benigno, dato che classificare
erroneamente un tumore maligno come benigno è molto più grave che classificare un tumore
benigno come maligno.
Come abbiamo visto in questo tutorial, in questi casi è opportuno utilizzare anche la matrice
di confusione come metrica per valutare il modello.
Realizzare una Rete Neurale da zero in questo modo può essere davvero utile a fini didattici,
per riuscire a comprendere appieno come questa funziona al suo interno e l’effetto che hanno
i vari parametri e iperparametri, però per progetti che devono andare in produzione non
dovresti mai, per nessun motivo, fare una cosa dal genere, ma piuttosto puoi affidarti a
framework affermati, aggiornati e supportati dalle grosse aziende del Tech, l’esempio per
eccellenza è Tensorflow.
A PROPOSITO DI ME
I NOSTRI PARTNER
CERCA
GLI ARTICOLI PIÙ LETTI
ARTICOLI RECENTI
COMMENTI RECENTI