Inseriamo in questo articolo un interessantissimo contributo esterno sul Machine Learning e la libreria scikit-learn .

Autore del post è Gianluca Emireni, laureato in Statistica e tecnologie informatiche,g_emireni consulente per Enti di ricerca ed imprese private su progetti data-driven: data modeling e progettazione di Datawarehouse, implementazione di strumenti per la Business Intelligence.  Si occupa di analisi statistiche che variano dal forecasting di serie storiche alle varie declinazioni della machine learning (regressioni, classificazioni, cluster, market basket, ecc.). Amante degli stack software open source come la suite Python (numpy, scipy, pandas, anaconda) ed R (Rstudio ed Rstudio server).


machine_learning

La prima definizione formale di Machine learning venne coniata nel 1959 da Arthur Samuel, uno dei pionieri nel campo dell’intelligenza artificiale: “Campo del sapere che dà ai computer la capacità di imparare senza essere esplicitamente programmati”. Il concetto odierno di machine learning spesso assume contorni poco definiti, tanto da confondersi con altre discipline come il data mining, sebbene a quest’ultima vengano più spesso associati problemi di analisi esplorativa dei dati e classificazione/unsupervised learning.

 Ultimamente l’hype verso questi temi è fortissimo, e parole come rete neurale o support vector machine sono entrate nel lessico di molti ricercatori, non tutti di formazione statistica. Chi si è avvicinato alla statistica in ambito accademico sa che la maggior parte degli algoritmi utilizzati nei processi di machine learning non sono altro che classici strumenti dell’inferenza statistica.

 La sensazione di poter dominare processi di analisi, a volte complessi, è alimentata anche dal fiorire di software sempre più automatizzati che offrono processi black box e restituiscono sempre un risultato, indipendentemente dalla qualità dei dati o dell’analisi condotta. Da statistico non posso che essere contento del rinnovato interesse suscitato da questi temi, tuttavia ritengo che il ruolo dell’analista rimanga centrale per la corretta formulazione del problema e per l’interpretazione dei risultati, anche quelli forniti dal miglior software di advanced analytics.

Il sito http://www.tylervigen.com fornisce una serie di esempi simpatici, come questo, che ingannerebbero anche il miglior software del mondo…

 

divorce_maine_correlation

R vs Python

Di seguito non citerò la gettonatissima figura del data scientist, ma parlerò di un team di analisti che si occupa della data science, dato che a mio avviso il data scientist è approssimabile ad una figura mitologica, in possesso di una quantità di skill difficilmente reperibili in un singolo individuo. Questo articolo motiva la mia scelta meglio di quanto non possa farlo con le mie parole: http://www.cio.com/article/3011648/analytics/dont-look-for-unicorns-build-a-data-science-team.html.

 Il nostro team di scienziati del dato, ad un certo punto del percorso di analisi, si troverà a compiere una scelta: a quale software affidarsi per l’analisi dei dati? Al momento, i due competitor più gettonati sono R e Python (o meglio, alcune sue librerie).

r_vs_python

Senza voler entrare nel merito della discussione, peraltro già trattata in molti contesti (un esempio a caso: https://www.pycon.it/media/conference/slides/r-you-ready-for-python.pdf, oppure http://blog.datacamp.com/wp-content/uploads/2015/05/R-vs-Python-216-2.png), R (https://www.r-project.org/) si è imposto nel corso dell’ultimo ventennio come standard di fatto per l’analisi statistica: ha dalla sua il vantaggio di essere stato plasmato nell’ambiente accademico ed è (o meglio era) rivolto all’ambiente accademico. Ha alle spalle una lunga storia, una community di altissimo livello ed un repository di librerie vastissimo e sempre aggiornato, il CRAN (https://cran.r-project.org/mirrors.html).

Esistono diverse librerie che permettono di sfruttare l’architettura client-server, sia per il deploy di applicazioni (RStudio Server: https://www.rstudio.com/products/rstudio/ ) che per la consultazione di dashboard via browser (Shiny: http://shiny.rstudio.com/), che lo rendono adatto a configurazioni di tipo cloud. 

Tuttavia, anche Python si sta affermando come un competitor sempre più agguerrito, visto che unisce l’efficacia del linguaggio di programmazione alla comodità di librerie come pandas (http://pandas.pydata.org/) per la creazione di strutture dati analitiche e scikit-learn (http://scikit-learn.org/stable/) per l’adozione di funzioni di machine learning per l’analisi.

Io, per non fare un torto a nessuno, ho deciso di usarli entrambi.

In questo post esploreremo alcune funzionalità di Python, esaminando un caso di classificazione binaria. Si tratta di un vero use case, con dati veri anche se opportunamente anonimizzati. Tanto per capirci, non sarà il classico esempio di classificazione su iris dataset, con percentuali bulgare di successo.

Per i profani, l’iris dataset è un stato introdotto da Ronald Fisher in un paper del 1936 e viene spesso utilizzato in problemi di tipo supervised (regressione e classificazione) e unsupervised (cluster analysis) a causa delle sue dimensioni contenute (150 osservazioni e 5 features) e per il fatto che garantisce ottime performance, qualunque sia il l’approccio utilizzato.

Il dataset

Il dataset sul quale condurremo l’esperimento di classificazione proviene da una base dati di una società che ha punti vendita/retailers come propri clienti.

Conta all’incirca 410.000 record, ognuno dei quali descrive un cliente con le sue caratteristiche anagrafiche ed economiche, le abitudini d’acquisto per categoria merceologica e nel tempo.

La variabile dipendente/outcome binaria che cercheremo di prevedere è la reazione da parte del cliente ad uno stimolo di marketing, per farlo useremo le circa 120 variabili/features a nostra disposizione, alcune numeriche, altre categoriali.

Nel codice che vedrete di seguito, la variabile target verrà chiamata target, le features vengono chiamate feature1, feature2, …, featureN.

 Gli strumenti

Personalmente rimango affezionato ai costrutti di Python 2.x e sto aspettando il punto di non ritorno prima di adottare la versione 3. Per comodità, ho installato la distribuzione Anaconda di Continuum Analytics (https://www.continuum.io/downloads), che contiene già numerosissime librerie per l’analisi dei dati ed il machine learning; in particolare, tutte le istruzioni che vedrete nel post non hanno richiesto l’installazione di nessuna libreria aggiuntiva. Anaconda porta con sé quel bellissimo strumento chiamato Jupyter notebook ( http://jupyter.org/ ) che mi trovo ad utilizzare sempre più spesso: si tratta di un editor di codice web-based che permette di creare e condividere documenti contententi codice, testo, equazioni e grafici.

jupyter

 

Lo use case

Entriamo nel vivo e diamo un’occhiata al codice che ci permetterà di condurre la nostra analisi.

Caricamento delle librerie

In fase di import, carichiamo tutte le librerie di cui avremo bisogno; di fatto, si tratta dei moduli numpy, sklearn e pandas. Ogni singolo classifier viene caricato come modulo tramite l’opportuna libreria.

import numpy as np
import sklearn as sk
import pandas as pd
from sklearn import preprocessing as pp, cross_validation
from sklearn.metrics import confusion_matrix
from sklearn.preprocessing import StandardScaler

# classifiers                                                                                                              
from sklearn.neighbors import KNeighborsClassifier
from sklearn.svm import SVC
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, AdaBoostClassifier, GradientBoostingClassifier, VotingClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis, QuadraticDiscriminantAnalysis

 Lettura dei dati

Carico il dataset come un pandas DataFrame. Per motivi di “portabilità” ho estratto un file csv, ma è decisamente più frequente il caso in cui l’origine dati sarà un database. In questo caso, abbiamo nella libreria sqlalchemy (http://www.sqlalchemy.org/ ) un ottimo alleato: ci offre un layer per la connessione e l’interrogazione di svariati DBMS.

data = pd.read_csv("/path/to/data.csv")

Preprocessing

Nella fase di preprocessing, ii DataFrame pandas trasformano tutti gli oggetti non numerici, stringhe incluse, nel dtype “object”. Nel file csv le nostre variabili categoriali vengono memorizzate come stringhe di testo e, dato che i modelli di classificazione di sklearn richiedono che tutte le variabili siano numeriche, utilizziamo il modulo preprocessing.LabelEncoder di sklearn per codificarle in numeri interi.

## Preprocess                                                                                                              

newdata = dict()
for key in data.keys():
   if data[key].dtypes == 'object':
       le = pp.LabelEncoder()
       le.fit(data[key])
       newdata[key] = le.transform(data[key])
   else:
       newdata[key] = data[key]

Trasformo il dizionario così ottenuto in un DataFrame pandas, separando la variabile target (che voglio determinare con il mio modello) da tutte le altre features.

## Result to pandas dict                                                                                                   
res = pd.DataFrame.from_dict(newdata, orient='columns', dtype=None)

# extracting target var to a different variable                                                                            
target = res['target']
res = res.drop('target', 1) 

Training e validation set

La fase di preprocessing procede con lo split del dataset originale in due dataset di training per la stima del modello e di validation per la verifica della bontà della classificazione ottenuta. Anche la variabile target viene divisa allo stesso modo.

La suddivisione dei record nei due dataset è casuale e in questo caso prevede una divisione tra training e validation set usando il 70% dei record originali per il primo ed il 30% per il secondo.

I dati vengono poi scalati in modo da ottenere variabili numeriche di media 0 e varianza 1. 

## Splitting training/validation set, training/validation target                                                           
nrows = len(newdata['feature1'])
indexes = np.arange(nrows)
np.random.shuffle(indexes)
train_indexes = indexes[:int(nrows * 0.70)]
train_rows = np.zeros(nrows, dtype=np.bool)
train_rows[train_indexes] = True
validation_rows = np.logical_not(train_rows)
train_set = res.loc[train_rows>0,:]
validation_set = res.loc[validation_rows>0,:]
train_target = target.loc[train_rows>0]
validation_target = target.loc[validation_rows>0]

# scaling                                                                                                                  
train_set = StandardScaler().fit_transform(train_set)
validation_set = StandardScaler().fit_transform(validation_set)

Model training

Il training del modello è l’aspetto più interessante dell’esercizio. In questo caso il classificatore utilizzato è una Random Forest, generalizzazione del classification tree.

## Training the model                                                                                                      
clf = RandomForestClassifier(n_estimators=100, max_depth=15, max_features=20, min_samples_leaf=20, n_jobs=10)
clf.fit(train_set, train_target)

 Ottengo i risultati predetti dal mio classificatore:

## Predict results                                                                                                         
predicted = clf.predict(validation_set)
conf_matrix = confusion_matrix(validation_target*1.00, predicted*1.00)

conf_matrix è la cosiddetta “matrice di confusione” che si ottiene incrociando i veri valori della variabile target con quelli stimati dal modello di classificazione.

 conf_matrix

array([[114371,   1748],
[  3873,   4028]]) 
Stimati 0 Stimati 1 Totale
Veri 0 114.371 1.748 116.119
Veri 1 3.873 4.028 7.901
Totale 118.244 5.776 124.020

 

 A partire dalla matrice di confusione, si valuta la bontà della classificazione del nostro modello, usando il validation set, che non entra nelle operazioni di stima del modello. Valutare il modello sul set di dati utilizzato per stimarlo (il training set) è un errore da non fare, visto che dà luogo ad overfitting e fornisce (spesso) performance di classificazione “dopate”.

L’indicatore più semplice ed intuitivo è la accuracy, ovvero la percentuale di previsioni indovinate dal mio modello di classificazione – quelle sulla diagonale – rispetto al totale. Nel nostro caso otteniamo un buon 95%.

La precision indica la quota degli 1 classificati correttamente su tutti quelli che il modello indica come 1.

La sensitivity indica la quota dei veri 1 stimati sul totale dei veri 1.

La specificity è l’equivalente della sensitivity per i valori 0.

F1 è un indicatore di sintesi dell’intera classificazione.

Determino questi indicatori con una UDF:

conf_matrix_stats(conf_matrix)

Il risultato ottenuto è:

{'F1': 0.59,
'accuracy': 0.95,
'sensitivity': 0.51,
'precision': 0.70,
'specificity': 0.97}

In pratica, otteniamo ottime performance globali di classificazione, con alcuni problemi nel prevedere i “veri positivi” (bassa sensitività). Questi valori degli Indicatori sono tipici in tutti i casi di classificazione con valori della variabile target molto sbilanciati: nel nostro caso circa il 6% dei clienti ha reagisce allo stimolo di marketing (target=1), la conseguenza è che è molto difficile prevedere chi parteciperà.

Al contrario, uno stimatore “stupido” che classifica tutte le osservazioni come 0 otterrà comunque una accuracy pari al 92%, ma avrà sensitivity e precision pari a 0.

Proviamo diversi modelli

Visto che scikit-learn mette a disposizione molti modelli diversi di classificazione, possiamo decidere di non limitarci ad usarne uno soltanto, e di metterli tutti alla prova sui nostri dati. Lo possiamo fare con un semplice ciclo iterando su una lista di oggetti/classificatori.

# Cycling over different classifiers                                                                                          

names = ["Nearest Neighbors", "Linear SVM", "RBF SVM", "Decision Tree",

        "Random Forest", "AdaBoost", "Naive Bayes", "Gradient Boosting"]

classifiers = [

   KNeighborsClassifier(3),
   SVC(kernel="linear", C=0.025),
   SVC(gamma=2, C=1),
   DecisionTreeClassifier(max_depth=5),
   RandomForestClassifier(max_depth=5, n_estimators=10, max_features=1),
   AdaBoostClassifier(),
   GaussianNB(),
   GradientBoostingClassifier(n_estimators=30, learning_rate=1.0, max_depth=1, random_state=0)]



for name, clf in zip(names, classifiers):
   print name
   clf.fit(train_set, train_target)
   predicted = clf.predict(validation_set)
   conf_matrix = confusion_matrix(validation_target*1.00, predicted*1.00)
   print conf_matrix_stats(conf_matrix)

 

Ovviamente, parametri diversi utilizzati nelle chiamate ai metodi possono portare a classificazioni più o meno precise. In questo caso non ci siamo spinti nell’ottimizzazione, ma abbiamo cercato di confrontare le performance di diversi metodi usando dei parametri molto vicini a quelli di default. 

Majority vote

Le performance di classificazione dei modelli vengono confrontate tra di loro per determinare un “vincitore”: in questo caso il GradientBoostingClassifier è quello che sembra fornire le performance migliori. 

Una volta individuati gruppi di classificatori che si comportano bene, è possibile utilizzare il metodo del “voto di maggioranza”: ogni metodo crea il proprio modello e classifica le osservazioni del validation set.

A questo punto, la categoria “votata” da più modelli è quella che viene associata all’osservazione.

Ecco un esempio che utilizza un decision tree, una random forest ed il gradient boosting:

clf1 = DecisionTreeClassifier(max_depth=5)
clf2 = RandomForestClassifier(max_depth=5, n_estimators=10, max_features=1)
clf3 = GradientBoostingClassifier(n_estimators=30, learning_rate=1.0, max_depth=1, random_state=0)

eclf = VotingClassifier(estimators=[('lr', clf1), ('rf', clf2), ('gnb', clf3)], voting='hard')
eclf.fit(train_set, train_target)
predicted = eclf.predict(validation_set)

 

Approfondimenti

Spero che quanto raccontato in questo post non sia stato troppo noioso e che allo stesso tempo vi possa essere utile a trarre qualche spunto. Io per primo ho attinto a piene mani dalla documentazione ufficiale di scikit-learn: http://scikit-learn.org/stable/documentation.html, davvero di ottimo livello. 

Se volete mettervi in contatto con me, sono presente sui social

Twitter: https://twitter.com/gianlucaemireni

LinkedIn: https://it.linkedin.com/in/gianluca-emireni-57280927

———————

Sempre su questo argomento vi segnaliamo un precedente articolo pubblicato su questo blog: “Machine Learning: i postini e gli algoritmi di classificazione” 

CC BY-NC-SA 4.0
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.