Nell’ultimo articolo della nostra serie dedicata al ML, abbiamo avuto modo di approfondire uno dei primi esempi di neurone artificiale, il percettrone.
Oggi facciamo un passo avanti e andiamo a definire un’evoluzione del percettrone chiamata Logistic Regression.
Come dataset di esempio, continueremo a mantenere il set di nomi maschili e femminili che abbiamo costruito insieme nell’articolo dedicato alla generazione di un dataset per classificatori supervisionati.
Logistic Regression
A differenza di quanto il nome possa far pensare, la Logistic Regression (LR) è un algoritmo di classificazione supervisionato che, come il percettrone, funziona molto bene su classi linearmente separabili.
Sempre come nel caso del percettrone, si tratta di un modello lineare per la classificazione binaria, ma può essere facilmente esteso a classificare dataset multiclasse tramite l’impiego di tecniche quali la OvR.
Per poter comprendere il funzionamento della LR, è necessario innanzitutto introdurre un concetto statistico chiamato odds ration (rapporto di probabilità):
\frac{p}{1-p}
con p la probabilità che un evento accada.
Come spesso succede, per semplificare i calcoli matematici, si definisce la funzione logaritmica, in questo caso chiamata logit function:
log\frac{p}{1-p}
questa funzione ha una curiosa proprietà: prende in input un numero qualsiasi tra 0 e 1 e lo trasforma in un valore nel range dei numeri reali.
Partendo da questa sua proprietà, esprimiamo una relazione lineare tra i valori delle feature e il log-odds così definita:
logit(P(\textbf{y=1}|\textbf{x})) = w_0x_0 + w_1x_1 + ... + w_mx_m = \textbf{w}^{T}\textbf{x} = z
alla sinistra abbiamo la logit che prende in input la probabilità condizionata che un campione x appartenga alla classe y=1, dall’altra invece abbiamo il nostro predittore lineare che abbiamo avuto modo di incontrare nell’articolo dedicato al percettrone.
Sigmoide
Risolvendo la relazione precedente rispetto a P, che è la probabilità che vogliamo trovare per poter fare la nostra predizione, otteniamo la seguente funzione:
\phi (z) = \frac{1}{1 + e^{-z}}
Per via della sua caratteristica forma ad S, la funzione in questione prende il nome di sigmoide:
Al contrario della logit, com’è logico aspettarsi, la sigmoide trasforma qualsiasi valore reale in un numero tra 0 e 1.
A differenza di un percettrone, la LR usa la sigmoide come funzione di attivazione e sposta la funzione scalino al di fuori del loop di calcolo dell’errore di predizione.
Ciò lo si fa anche in virtù del fatto che la sigmoide è una funzione derivabile, ed è quindi possibile usare, come vedremo più avanti, delle tecniche efficienti di calcolo del minimo per ottimizzare i pesi w.
L’output della sigmoide è la probabilità di un campione x di appartenere alla classe 1.
Considerando ad esempio il dataset dei nomi maschili e femmili, costruito negli scorsi articoli, \phi (z) = 0.8 significa che abbiamo l’80% di probabilità che un nome appartenga alla classe (1) dei nomi maschili.
Per derivare la probabilità di appartenenza alla classe inversa, è sufficiente calcolare 1 - \phi (z) = 0.2.
La probabilità può essere ora convertita in un output binario, attraverso una funzione scalino che è posizionata al di fuori del loop di calcolo degli errori ed è così definito:
y = \left\{\begin{matrix} 1 & \phi(z) \geqslant 0.5) \\ 0 & altrimenti \end{matrix}\right.
La Logistic Regression, oltre a classificare un campione in ingresso, permette anche di calcolare la probabilità di appartenenza di quel determinato campione ad classe.
Log-Likelihood
Arrivati a questo punto, abbiamo visto come predire la probabilità e l’appartenenza ad una determinata classe di un campione. Non ci resta che discutere quindi come aggiornare i pesi w per addestrare il nostro modello al riconoscimento dei campioni.
Definiamo a tal proposito una funzione obiettivo che dovrà essere poi ottimizzata durante il processo di apprendimento.
Nel caso della LR, la funzione in questione si basa sulla così detta log-likelihood, la funzione di verosimiglianza utilizzata per la stima dei parametri di un modello statistico.
J(W) = - \sum y^ilog(\phi(z^i)) + (1 - y^i)log(1-\phi(z^i))
Per motivi di brevità non ci soffermeremo nel capire come ottenere questa funzione, ad ogni modo vale la pena spendere due parole sul grafico di questa funzione di costo:
Possiamo notare che il costo J(w) va a 0 nel caso in cui abbiamo predetto correttamente la classe di appartenenza (1 nel caso della linea continua, 0 nel caso della linea tratteggiata), viceversa il costo tende all’infinito. E’ possibile dunque penalizzare le predizioni errate con costi proporzionalmente crescenti, da 0 fino ad un valore infinito.
Ottimizziamo i Parametri
Come abbiamo detto, essendo la sigmoide, e quindi J(w), differenziabile, possiamo utilizzare la tecnica del gradiente discendente il quale è basata sull’utilizzo della derivata per trovare il minimo di una funzione.
Usando il gradiente discendente, quindi, possiamo adesso aggiornare i pesi w facendo un passo nella direzione opposta al gradiente \Delta J(w) della nostra funzione di costo. Più nello specifico, la regola di aggiornamento dei pesi sarà pari a:
w = w + \Delta w
dove il fattore \Delta w è definito come il gradiente negativo moltiplicato per un parametro denominato learning rate \eta :
\Delta w = - \eta \Delta J(w)
Per calcolare il gradiente della funzione di costo, dobbiamo calcolare le derivate parziali della stessa rispetto a ciascun peso w_j:
\Delta w = - \eta \frac{\partial J}{\partial w_j} = \eta \sum (y^i - \phi(z^i))x_j^i
Addestriamo una Logistic Regression
Sempre sulla falsariga di quanto fatto nello scorso articolo, vediamo ora come scrivere un semplice programma in Python che, facendo uso delle librerie scikit-learn ci permetta di addestrare una LR sul nostro dataset di prova.
Teniamo sempre di conto che il dataset che useremo non è linearmente separabile, è facile ad esempio trovare nomi di persona che terminano per ‘a’ ma che non sono per forza femminili. Quello che dovremo aspettarci dunque è un classificatore che riuscirà a predire il genere di un nome con una certa percentuale di errore dipendente da come il dataset è composto.
Con riferimento alle funzioni load_dataset e calculate_features ti rimando allo scorso articolo dove mostro il codice sorgente di ciascuna di quelle funzioni. Concentriamoci invece ora sullo script in sè:
Dopo aver caricato il dataset di partenza nella riga 5, che ti ricordo puoi scaricare dal mio repository, lo abbiamo diviso in una parte di training set ed in una di test set. Nelle righe successive, creiamo un’istanza della classe LogisticRegression (riga 7) dando in input alcuni parametri che avremo modo di approfondire più avanti nelle nostre trattazioni. Con la riga successiva, effettuiamo l’addestramento sul dataset di training.
Proseguiamo poi, riga 10, a stampare il punteggio raggiunto sul test set.
Nel mio caso, ho raggiunto immediatamente oltre il 96% di percentuale di riconoscimento.
A questo punto, come già fatto nello scorso articolo, non ci resta che restare in attesa di input da parte dell’utente per testare il nostro modello su parole a piacere.
Una volta recuperato l’input dell’utente (riga 13), procediamo calcolando le features discusse nello scorso articolo (lunghezza nome, numero vocali e consonanti, vocale di terminazione) e invochiamo la LR affinchè ci restituisca il risultato circa il genere della parola in ingresso (riga 14).
Oltre a questo, nella riga 15, stampiamo anche la probabilità di appartenenza della parola in questione ad entrambe le classi.
Conclusioni
In questo articolo, abbiamo approfondito il funzionamento di un altro classificatore lineare, naturale evoluzione del Percettrone.
La Logistic Regression, basandosi su uno stimatore a massima verosimiglianza, permette di classificare dataset linearmente separabili e, a differenza di altri algoritmi, restituisce anche la percentuale di appartenenza del campione in ingresso alle classi in gioco.
Scrivi un commento