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, 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).
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…
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).
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.
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”
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
Articolo molto interessante, soprattutto per un novizio come me. Mi chiedevo se il dataset fosse scaricabile.