Dopo aver definito il nostro dataset di riferimento, passiamo a valutare l’algoritmo più semplice disponibile in letteratura.

Il Percettrone

La prima definizione di neurone artificiale nasce dagli studiosi Warren McCullock e Walter Pitts i quali nel loro lavoro “A logical Calculus of the Ideas Immanent in Nervous Activity” (1943), fornirono il primo modello semplificato di cellula cerebrale.

Fu Frank Rosenblatt, pochi anni dopo, a definire la prima regola di apprendimento del percettrone, capace di calcolare autonomamente i coefficienti ottimi di classificazione.

Grazie a questi coefficienti, il percettrone è in grado di predire la classe di appartenenza di un campione.

Per entrare un po’ nel dettaglio, immagina le feature che ho raccolto nello scorso articolo per descrivere la distribuzione dei nomi maschili e femminili. Come ti ho raccontato, possiamo determinare 8 caratteristiche principali:

  • lunghezza nome : x^{nome}_1
  • numero vocali : x^{nome}_2
  • numero consonanti : x^{nome}_3
  • vocale di terminazione
    • vocale di terminazione ‘a’ : x^{nome}_4
    • vocale di terminazione ‘e’ : x^{nome}_5
    • vocale di terminazione ‘i’  : x^{nome}_6
    • vocale di terminazione ‘o’ : x^{nome}_7
    • vocale di terminazione ‘u’ : x^{nome}_8

Il percettrone assegna di base una variabile peso a ciascuna di queste caratteristiche, per semplicità chiamate w_1 , …, w_8 . Questi pesi vengono inizializzati a valori prossimi allo 0.

A questo punto, per ciascun campione del nostro dataset, ovvero per ciascun nome, viene calcolato il così detto net input:

z = w_1 * x_1 + w_2 * x_2 + ... w_8 * x_8

Se il risultato del calcolo raggiunge o supera una certa soglia θ si dice che il percettrone ha classificato il campione in questione come di tipo 1, altrimenti il risultato sarà -1.

In termini matematici dunque possiamo dire che la funzione di decisione Φ() è così definita:

Bias Unit

In letteratura normalmente si usa una forma leggermente diversa da quella che ti ho appena presentato; in particolare si tende a portare la soglia di decisione θ alla sinistra dell’equazione andando a definire un peso w_0 = -θ e una feature x_0 = 1 .

Le equazioni sopra riportate assumono questa forma:

z = w_0* x_0 + w_1* x_1 + ... w_8 * x_8

Il peso w_0 prende il nome di bias unit.

La regola di apprendimento

Una volta determinato il net input è necessario verificare se la classe predetta sia quella esatta o meno e in relazione a questo aggiornare i pesi.

Si definisce quindi la regola di apprendimento del percettrone nel seguente modo:

\Delta w_j = \eta (y^i - \hat{y}^i)* x_j^i

dove:

  • η : parametro di apprendimento (learning rate) che può variare tipicamente tra 0.0 e 1.0.
  • \mathbf{y^i} : la classe di appartenenza del nome di riferimento (ad es. “Luca”, essendo nome maschile, avrà y^i = 1 )
  • \mathbf{\hat{y}^i} : il risultato della predizione sulla base del risultato di z.
  • \mathbf{x_j^i } : la j-esima feature del campione (ad es. nel caso della lunghezza del nome di “Luca” avremo, x_1^{Luca} = 4 )

Al termine di questa valutazione avremo in \Delta w_j lo scostamento necessario affinché il classificatore sia in grado di predire la classe corretta del campione considerato. In particolare possiamo avere due casi:

  • \Delta w_j  = 0 con predizione corretta (ad es. nel caso di “Luca” dovremmo avere  y^i = 1 e \hat{y}^i = 1 )
  • \Delta w_j  \neq 0 con predizione errata.

Sulla base di questo risultato si procede infine con l’aggiornamento dei pesi, seguendo questa semplice regola:

w_j = w_j + \Delta w_j  

Un Esempio

Ripartiamo dal nostro dataset di nomi maschili e femminili. Consideriamo le feature x_i con relativi pesi w_i viste poco fa.

Assumiamo come campione di riferimento il nome “Erica” con y^{Erica} = -1  ovvero nome di tipo femminile; avremo quindi:

  • lunghezza nome : x^{Erica}_1  = 5
  • numero vocali : x^{Erica}_2 = 3
  • numero consonanti : x^{Erica}_3 = 2
  • vocale di terminazione
    • vocale di terminazione a : x^{Erica}_4  = 1
    • vocale di terminazione e : x^{Erica}_5 = 0
    • vocale di terminazione i  : x^{Erica}_6 = 0
    • vocale di terminazione o : x^{Erica}_7  = 0
    • vocale di terminazione u : x^{Erica}_8 = 0

Ammettiamo che il nostro classificatore riconosca inizialmente il nome come maschile ( \hat{y}^{Erica} = 1 ) la regola di apprendimento darà il seguente risultato:

\Delta w_1 = \eta (y^{Erica} - \hat{y}^{Erica})* x^{Erica}_1  = \eta (-1 - 1) * 5 = - 10 \eta

Come possiamo notare, maggiore è il valore della feature x^{Erica}_1 , più alta sarà la correzione da applicare durante l’aggiornamento dei pesi.

Qualcuno potrebbe obiettare che dare maggior enfasi ad un nome semplicemente perché più lungo di altri può essere sbagliato. In effetti questa può essere un’obiezione corretta che sottolinea come nell’analisi dei dati i risultati migliori si ottengono solo sperimentando.

Se il classificatore avesse invece predetto il genere corretto, ovvero \hat{y}^{Erica} = -1 , la regola di apprendimento sarebbe stata nulla:

\Delta w_1 = \eta (y^{Erica} - \hat{y}^{Erica})* x^{Erica}_1  = \eta (-1 - -1) * 5 = 0

Mano a mano che l’algoritmo viene iterato sui vari campioni disponibili nel training set, si ottiene quindi una taratura dei pesi sui dati presentati con conseguente apprendimento del modello che li descrive.

Vale la pena notare che la convergenza del percettrone, e dunque la sua capacità di predire correttamente la tipologia di un campione, sono strettamente legati alla struttura del dataset stesso. E’ necessario infatti che le classi siano linearmente separabili, ovvero che sia possibile dividere geometricamente le due classi tracciando una linea di separazione. Se ciò non fosse possibile, l’algoritmo non potrà mai giungere ad una stabilità, continuando di fatto ad aggiornare i suoi pesi senza soluzione di continuità.

Addestriamo un Percettrone!

Adesso che abbiamo capito cosa c’è dietro questo classificatore, è giunto il momento di provarlo sul nostro dataset.

Teniamo 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.

Python possiede un nutrito numero di librerie per il machine learning, in particolare la libreria scikit-learn, che ovviamente ha anche un’implementazione del percettrone.

Scikit-learn è una libreria open source di apprendimento automatico per il linguaggio di programmazione Python. Contiene algoritmi di classificazione, regressione e clustering (raggruppamento) e macchine a vettori di supporto, regressione logistica, classificatore bayesiano, k-mean e DBSCAN, ed è progettato per operare con le librerie NumPy e SciPy. scikit-learn è attualmente sponsorizzato da INRIAe talvolta daGoogle.

Se ti interessa approfondire la tua conoscenza sull’uso di questa libreria ti  consiglio vivamente di dare un’occhiata a questo libro di Sebastian Raschka:

Avendo sempre come riferimento quanto discusso nel nostro precedente articolo, una volta scaricato il dataset_names.txt e salvato nella cartella di progetto dove andrai a salvare lo script Python, è necessario dividerlo in 2 gruppi principali: quello di training e quello di test:

Copy to Clipboard
Caricare il dataset

Puoi trovare maggiori informazioni sulla funzione load_dataset() sempre in questo articolo.

A questo punto è necessario importare la libreria scikit insieme all’oggetto Perceptron e crearne un’istanza che useremo poi per addestrare il classificatore:

Copy to Clipboard

Il comportamento del percettrone può essere personalizzato con diversi parametri resi disponibili da scikit. Di seguito te ne propongo alcuni presi dalla guida ufficiale:

  • eta0 (double) : rappresenta il parametro di apprendimento η visto in precedenza.
  • shuffle (bool) : parametro che definisce se il training set debba essere mescolato casualmente ad ogni epoca.
  • alpha (float) : costante moltiplicata per il termine di regolarizzazione, se usato.
  • max_iter (int) : numero massimo di passi (epoche) da fare per considerare l’apprendimento terminato.

Per ottenere il massimo del risultato è necessario variare il comportamento del classificatore caratterizzandolo con valori diversi di questi parametri. Non esistono metodi matematici se non la semplice sperimentazione. Sta nella bravura dell’analista di dati intuire quale sarà la conformazione migliore da dare al classificatore per ottenere il risultato migliore.

Detto ciò, possiamo passare adesso ad addestrare il modello e a verificarne i risultati:

Copy to Clipboard

Il metodo fit() esegue l’addestramento del percettrone sul training set passatogli come argomento.

Per valutare le performance di quanto fatto, è possibile invece utilizzare il metodo score()  con il quale possiamo rapidamente testare il nostro modello sui dati precedentemente recuperati:

Copy to Clipboard

Ciò che andiamo a stampare sarà un valore compreso tra 0.0 e 1.0 che esprimerà quanto il modello è stato bravo a determinare le classi di appartenenza dei dati di test. Più il valore si avvicinerà ad 1.0, più il modello sarà preciso.

Miglioriamo i risultati

Lanciando lo script appena fatto senza aggiungere altro, otteniamo una precisione dell’82%. Niente male per essere il nostro primo tentativo! Vediamo se cambiando qualche parametro riusciamo a far meglio.

Partiamo dall’aumentare il numero di iterazioni che l’algoritmo usa per addestrare il classificatore:

Copy to Clipboard

In questo caso siamo stati fortunati, il risultato mostra una precisione di oltre il 96%.

Analizzando quanto fatto è possibile rendersi conto che una forte influenza è data anche dal random_state con cui si selezionano i dati di training e test nella funzione load_dataset(). Variare quel parametro può generare forti variazioni del risultato finale. Questo mostra bene come rappresenti un passaggio cruciale scegliere correttamente i dati e le caratteristiche che li descrivono.

Puoi scaricare il codice completo del progetto a questo  indirizzo o far riferimento a questo repository.

Conclusioni

A questo punto, non resta che usare il nostro classificatore su dati casuali inseriti da noi. Come abbiamo avuto modo di discutere, il percettrone ci restituirà risultato pari a 1 se interpreterà quanto passatogli in input come nome maschile, viceversa stamperà l’etichetta -1.

Copy to Clipboard

Inseriamo adesso delle parole italiane casuali:

Copy to Clipboard

Possiamo notare come il percettrone sia riuscito ad imparare abbastanza bene la regola che descrive come determinare se un nome è maschile o femminile estendendo questo concetto anche su nomi di cosa e animali.

Pur trattandosi di un algoritmo semplice nel suo complesso, è riuscito ad evincere che normalmente i nomi che terminano per ‘a’ sono femminili, mentre i restanti sono maschili.

Ovviamente questo non è sempre vero, come nel caso di “Luca” o “Ape”, vedremo a tal proposito nei prossimi articoli se saremo in grado di migliorare la resa del risultato cambiando algoritmo di classificazione.

Per terminare proviamo a stampare i pesi che il percettrone ha calcolato al termine dell’addestramento, per fare questo possiamo utilizzare l’attributo coef_ disponibile nella classe Perceptron.

Copy to Clipboard

Più chiaramente in termini di pesi avremo:

  • peso lunghezza nome : w_1  = 5
  • peso numero vocali : w_2 = 7
  • peso numero consonanti : w_3 = -2
  • pesi vocale di terminazione
    • peso vocale di terminazione a : w_4  = -88
    • peso vocale di terminazione e : w_5 = -12
    • peso vocale di terminazione i  : w_6 = 9
    • peso vocale di terminazione o : w_7  = 81
    • peso vocale di terminazione u : w_8 = 0

Il percettrone quindi, partendo dal training set usato, ha capito che:

  • la lunghezza del nome è una variabile composta dal numero di vocali e da quello delle consonanti, non è un caso se 5 = 7 - 2 ;
  • le vocali di terminazione più importanti sono la ‘a’ e la ‘o’
Download progetto