Indice articolo
From “Dancing in the dark” To “It’s a kind of magic
“ML” sta per Machine Learning che è una branca del’Intelligenza Artificiale.
“For dummies” è una locuzione gergale spesso utilizzata in ambito informatico per indicare che quel materiale informativo/divulgativo è rivolto ad i principianti, quindi parliamo di Intelligenza Artificiale per principianti.
“15 min” è il tempo medio di lettura ignorando le porzioni di codice pyton (riquadri grigi).
Spesso si accosta all’Intelligenza Artificiale il concetto di macchine “pensanti”, non è esattamente così, tuttavia il Machine Learning consente alle macchine di risolvere i problemi imparando dall’esperienza, che è esattamente quello che fa il nostro cervello quindi, se la cosa vi “stuzzica” e volete capirci qualcosa di più vi invito a continuare la lettura.
Cosa dovete sapere per capirci qualcosa
“Assolutamente nulla” è più che sufficiente un po’ di “curiosità”, ma se siete esperti tanto meglio, essendo questo documento rivolto ai “dummies” ma redatto da un poco più di un dummy, sarò lieto di ascoltare qualsiasi tipo di suggerimento.
Ah dimenticavo il sottotitolo. Vi chiederete cosa c’entrano i titoli di due canzoni con il Machine Learning… “forse nulla”, ma se siete curiosi e non vi frega niente del Machine Learning, magari vi “stuzzica” cercare di capire dove voglio andare a parare.
Cosa vedremo
Analizzeremo l’approccio che dovrebbe essere seguito per la realizzazione di un modello di apprendimento automatico, utilizzato nell’ambito dello sviluppo di un ipotetico software che consente di prevedere se una massa rilevata durante un accertamento mammografico è benigna o maligna.
La traccia che seguiremo si svilupperà nei seguenti punti:
- Reperimento del dataset
- Preparazione dei dati
- Training e test dei seguenti modelli
- Decision tree
- Random forest
- KNN
- Naive Bayes
- SVM
- Logistic Regression
- Neural network
Reperimento del dataset
Cosa è un dataset
Il dataset è una sorgente dati in formato tabellare – esempio excel – che utilizzeremo per il training e per il test dei modelli.
Per training intendiamo la fase in cui istruiamo il modello, per test la fase in cui lo valutiamo.
Il modello invece è quella entità che una volta istruita fa le previsioni.
Durante la trattazione quando vengono citati i classifiers, fate conto che sono la stessa cosa del modello.
Le colonne (attributi) di un dataset sono suddivise logicamente per:
- features – le variabili che condizionano il risultato, generalmente la notazione utilizzata per rappresentarle è:
X
; - labels – il risultato in funzione degli attributi, in questa circostanza la nomenclatura prevede l’utilizzo della lettera:
y
.
Per chi si ricorda un po’ di algebra gli verrà in mente come si definisce una funzione: y=f(x)
ed in effetti parliamo sostanzialmente di questa cosa, il ML ci consente di determinare f (il modello), ovvero quella funzione che è in grado di rappresentare y in funzione di X.
Riepiloghiamo
Training data – Sono le righe del dataset che utilizzeremo esclusivamente per il training: al modello gli spieghiamo per ogni riga che a fronte di quelle features (caratteristiche della massa) sarà associata quella label (massa maligna o benigna).
Test data – Sono le righe del dataset che il modello non conosce e vengono utilizzate esclusivamente per il test: al modello gli chiediamo per ogni riga quale è la label (se la massa è maligna o benigna) a fronte di quelle features (caratteristiche della massa), la percentuale di risposte corrette viene detta “accuratezza”.
Un esempio visuale
Immaginate il training dataset come dei puntini in uno spazio bidimensionale, in cui sull’asse delle ordinate (y) abbiamo la label e su quello delle ascisse (x) la feature, il modello è quella linea che mette assieme questi puntini. Se aggiungiamo altri puntini – il dataset di test – e questi si trovano in prossimità della nostra linea siamo stati bravi ed il nostro modello sarà in grado di fare previsioni in quanto abbiamo trovato una funzione che collega tutti i nostri puntini.
Dove si trova il dataset
Il dataset si può scaricare dal seguente indirizzo: Mammographic Mass
Ogni riga di questo dataset classifica una particolare massa, quindi per chi “ragiona ad oggetti” una riga è una istanza rappresentata dai seguenti attributi (chi ha competenze mediche perdoni la traduzione letterale dei possibili valori di alcuni attributi):
- BI-RADS assessment – Indice di gravità da 1 to 5
- Age – Età del paziente in anni
- Shape – circolare=1 ovale=2 lobulare=3 irregolare=4
- Margin – circoscritto=1 microlobulato=2 oscurato=3 non-definito=4 spicolato=5
- Density – elevata=1 iso=2 bassa=3 contente-grasso=4
- Severity – benigna=0 or maligna=1
BI-RADS lo scartiamo in quanto non è attributo predittivo (dovete fidarvi…).
Gli attributi “age”, “shape”, “margin” e “density” sono le features mentre “severity” è quello che vogliamo prevedere, quindi la label.
Come potete notare il valore degli attributi è espresso in numeri, questo è un prerequisito in quanto il machine learning utilizza modelli di tipo matematico, quindi i dati devono essere necessariamente espressi in formato numerico.
Preparazione dei dati
Download del dataset
import pandas as pd
#Indichiamo le colonne che vogliamo importare
col_names = ["bi-rads", "age", "shape", "margin", "density", "severity"]
#Url del dataset
dataset_url = "https://archive.ics.uci.edu/ml/machine-learning-databases/mammographic-masses/mammographic_masses.data"
#Il parametro na_values serve per dire a pandas che se l'attributo contiene il valore ?
#deve essere considerato come non impostato (null value)
df = pd.read_csv(dataset_url, na_values = ['?'], names=col_names)
Analisi e pulizia dei dati
Intanto vediamo cosa c’è dentro:
df.head()
Possiamo notare ad esempio che in queste prime cinque righe ce ne sono due con la “density” non impostata (NaN).
Ora vediamo di fare una analisi statistica del dataset:
df.describe()
La riga “count” mostra in quante righe si trova quell’attributo.
Su 961 righe che hanno l’attributo “severity” impostato ne abbiamo solo 885 che hanno impostato la “density”, 913 il “margin”, ecc…
Questo significa che alcune righe sono prive di attributi, estraiamole (non perdete troppo tempo ad analizzare il tabulato)
df.loc[(df['age'].isnull()) |
(df['shape'].isnull()) |
(df['margin'].isnull()) |
(df['density'].isnull())]
Visto che le righe con attributi non impostati sono distribuite in modo casuale possiamo ipotizzare che anche eliminandole non dovremmo inficiare i dati (fidatevi questo sarà più chiaro fra qualche secondo):
#Eliminiamo le righe con campi non impostati
df = df.dropna()
#Estraiamo nuovamente le statistiche
df.describe()
Le statistiche del nuovo tabulato sono rimaste inalterate, questo significa che non abbiamo compromesso la valenza statistica del dataset.
La fase di cleaning del dataset deve prevedere, oltre all’eliminazione di righe con attributi non impostati (o quanto meno è un aspetto da valutare), anche l’eliminazione delle colonne (features) che non influenzano la previsione in quanto appesantirebbero inutilmente il training del modello.
#estraggo le features
feature_cols = ["age", "shape", "margin", "density"]
#Matrice delle features (variabili)
X = df[feature_cols]
#Matrice delle labels, in questo caso è la colonna "severity" (colonna dei risultati)
y = df.severity
#importo l'oggetto che fa questo lavoro
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
#Gli do in pasto la matrice delle features
scaler.fit(X)
#Applico la trasformazione
X = scaler.transform(X)
Training e test dei modelli
results = []
#Funzione per mostrare il riepilogo dei risultato
def show_results(classifier, scores):
results.append([classifier, format("%0.2f (+/- %0.2f)" % (scores.mean(), scores.std() * 2))])
return pd.DataFrame(data=results, columns=['Classifier', 'Accuracy'])
Split
#importiamo le librerie necessarie per lo split
import numpy
from sklearn.model_selection import train_test_split
#questo serve solo per rendere reiterabili i risultati
numpy.random.seed(1234)
# X_train - features di train (attributi delle istanze di massa che condizionano il risultato)
# X_test - features di test
# y_train - label di train (maligna|benigna)
# y_test - label di train
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=1)
Decision Trees
Il primo modello su cui lavoriamo è DecisionTreeClassifier:
#Importiamo il classificatore
from sklearn.tree import DecisionTreeClassifier
#Lo creiamo
clf = DecisionTreeClassifier(random_state=0)
#Lo istruiamo
clf.fit(X_train, y_train)
Utilizzeremo il dataset di test (“X_test” ed “y_test”) per misurare l’accuratezza:
#Pass al metodo score features e labels di test
accuracy = clf.score(X_test, y_test)
#stampo il risultato
print("Accuracy: ",accuracy)
from sklearn.model_selection import cross_val_score
scores = cross_val_score(clf, X, y, cv=10)
#stampo risultato
results_df = show_results('DecisionTreeClassifier', scores)
results_df
RandomForestClassifier
Ora valutiamo RandomForestClassifier.
La cosa interessante è che si procede esattamente come nel caso del DecisionTreeClassifier, con la differenza ovvia, che a codice indichiamo il nuovo modello:
#Importo il classificatore
from sklearn.ensemble import RandomForestClassifier
#Lo creo
rcf = RandomForestClassifier(n_estimators=100, max_depth=2, random_state=0)
#Lo valuto
scores = cross_val_score(rcf, X, y, cv=10)
#stampo risultato
results_df = show_results('RandomForestClassifier', scores)
results_df
SVM
from sklearn.svm import SVC
svc = SVC(kernel='linear')
scores = cross_val_score(svc, X, y, cv=10)
#stampo risultato
results_df = show_results('SVC', scores)
results_df
SVC è analogo al RandomForestClassifier
KNN
from sklearn.neighbors import KNeighborsClassifier
knc = KNeighborsClassifier(n_neighbors=10)
scores = cross_val_score(knc, X, y, cv=10)
#stampo risultato
results_df = show_results('KNeighborsClassifier', scores)
results_df
Tuning 1
Ora vediamo come possiamo provare a modificare gli iperparametri per verificare se riusciamo a migliorarne l’accuratezza, visto che KNN prevede l’impostazione di “n_neighbors” (più tantissimi altri) facciamo fare queste verifiche a pyton:
#Misuro l'accuratezza di knc per il parametro n_neighbors che va da 1 a 20
for k in range(1, 20):
knc = KNeighborsClassifier(n_neighbors=k)
scores = cross_val_score(knc, X, y, cv=10)
print("K: %s Accuracy: %0.2f (+/- %0.2f)" % (k, scores.mean(), scores.std() * 2))
Mi rendo conto che con “n_neighbors=10” ottengo i risultati migliori ma aumentandone ulteriormente il valore non si ha alcun beneficio. Questo è un aspetto importante in quanto si è portati a pompare il modello con la convinzione di aumentarne la precisione, tuttavia in certe circostanze non serve a nulla.
Naive Bayes
Naive Bayes
Rispetto agli altri modelli questo accetta tra i valori del dataset solo numeri positivi che vanno da 0 ad 1 quindi devo fare una ulteriore normalizzazione:
from sklearn.naive_bayes import MultinomialNB
from sklearn.preprocessing import MinMaxScaler
#Creo il normalizzatore
scaler = MinMaxScaler()
#Gli passo le features
scaler.fit(X)
#Normalizzo
X_nb = scaler.transform(X)
mmnb = MultinomialNB()
scores = cross_val_score(mmnb, X_nb, y, cv=10)
results_df = show_results('MultinomialNB', scores)ff
results_df
Tuning 2
In questa circostanza iteriamo per i possibili valori dell’iperparametro “kernel” nel caso di SVC:
#L'iperparametro che voglio modificare è il kernel
#ne proverò tre
kernels = ['linear', 'rbf', 'poly']
for kernel in kernels:
svc = SVC(kernel=kernel, gamma='scale')
scores = cross_val_score(svc, X, y, cv=10)
print("Kernel: %s, Accuracy: %0.2f (+/- %0.2f)" % (kernel, scores.mean(), scores.std() * 2))
Vedo che non cambia quasi nulla.
Logistic Regression
Logistic Regression
Ora proviamo con quello che è uno dei modelli più elementari:
from sklearn.linear_model import LogisticRegression
logisticRegression = LogisticRegression(solver="lbfgs")
scores = cross_val_score(logisticRegression, X, y, cv=10)
results_df = show_results('LogisticRegression', scores)
results_df
Constatiamo che è un po’ meglio di tutti gli altri. L’insegnamento che dobbiamo trarne è che anche nel Machine Learning: semplice non fa rima con inutile.
Neural Networks
Per finire utilizziamo una rete neurale, qui la cosa si fa leggermente più complessa in quanto la rete neurale ha una sua topologia, livelli, neuroni, ecc…
Imposto alcune variabili di ambiente ed importo le varie librerie:
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE'
from tensorflow.keras.layers import Dense, Dropout
from tensorflow.keras.models import Sequential
from tensorflow.keras.wrappers.scikit_learn import KerasClassifier
Creo il modello di rete neurale che sarà costituito da:
- In input layer di 4 neuroni
input_dim=4
(la regola è un neurone per ogni feature); - Un layer intermedio di 6 neuroni
model.add(Dense(6...)
, (in realtà maggiore è il numero di neuroni di questo layer migliore dovrebbe essere la capacità della rete neurale di fare previsioni esatte, tuttavia esiste un limite fisiologico oltre il quale non ha senso andare, comunque molto dipende dalla complessità del problema che vogliamo risolvere); - Per finire un output layer con 1 neurone
model.add(Dense(1...)
, in questo caso ne utilizziamo 1 in quanto la classificazione è binaria (maligna|beningna).
def create_model():
model = Sequential()
#Aggiungo layer di 6 neuroni attivati da un input layer di 4 neuroni
model.add(Dense(6, input_dim=4, kernel_initializer='normal', activation='relu'))
#Aggiungo l'output layer
model.add(Dense(1, kernel_initializer='normal', activation='sigmoid'))
#Compilo il modello
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
return model
estimator = KerasClassifier(build_fn=create_model, epochs=100, verbose=0)
scores = cross_val_score(estimator, X, y, cv=10)
results_df = show_results('Neural networks', scores)
results_df
Chi ha vinto la sfida?
“DecisionTreeClassifier” è ultimo della classifica, “LogisticRegression” (media 0.81) ha vinto dimostrando che in certe circostanze, anche in questo “astruso mondo dell’intelligenza artificiale”, Davide (LogisticRegression) può vincere ancora contro Golia (Neural networks).
Conclusioni
Il Machine Learning mi affascina da sempre, ma chi ha voglia di approfondire si trova di fronte ad un universo di cose, e di certo non è facilitato dalla terminologia: anaconda, pandas, numpy, keras, tensorflow, rnn, cnn, reinforcement learning, data cleaning, hyper parameters, ecc… che è tanto variegata quanto fantasiosa, e la prima sensazione che si prova è di essere in balia di qualcosa che è troppo più grande di noi.
Nonostante ciò, spinto dalla curiosità mi sono messo a fare qualche tutorial on line con l’aspettativa, visto che spesso su altri argomenti ha funzionato, di ottenere buoni risultati, ma l’aspettativa era sistematicamente disattesa e l’unico risultato a conclusione del tutorial era: “wow” che bello!!!
Ma se cercavo di ripercorrere il percorso era tutto piuttosto oscuro, mi sentivo come uno che sta ballando al buio, una sensazione piacevole ma che svanisce nel momento in cui ti rendi conto che non hai la più pallida idea di dove sei finito, né tanto meno come ci sei arrivato.
Poi ho avuto la possibilità di seguire un corso on line: “Machine Learning, Data Science and Deep Learning with Pyton” con la certezza di non capirci nulla.
A fine corso era previsto un test, quanto vi ho presentato in questo articolo.
Mettersi alla prova su questioni così delicate utilizzando un approccio che sino a qualche mese fa per me era “arabo” era decisamente stimolante, e sapere di poter procedere con calma senza la “mannaia” dell’esame andato male rendeva la cosa anche divertente.
Mano a mano che procedevo mi rendevo conto che tutte quelle “fancy words” (come si divertiva a definirle il docente) cominciavano ad assumere un significato, inoltre il quadro si faceva sempre un po’ più nitido, poi, quando ho iniziato a fare il mio lavoro – che è scrivere codice – appurando che con “così poco codice python” si riusciva a risolvere un problema così complesso, mi sono detto: “wow” ma questa è una specie di magia!!!
Anche perché se un dummy – io – con due righe di codice riesce a risolvere questo problema (che è un po’ di più dell’inflazionato:System.out.println("Hello world!!!")
, cosa possono fare quelli bravi?
Per tale ragione, spinto da raptus di esaltazione ho sentito la necessità di condividere questa esperienza, ed i titoli delle due canzoni citate nel sottotitolo sintetizzano in modo perfetto il mio pensiero sul Machine Learning all’inizio ed alla fine di questo percorso, e per tale ragione “Dancing in the dark” e “It’s a kind of magic” si trovano esattamente dove ha senso che siano…
PS
Per i pochi che non hanno mai ascoltato questi due pezzi meravigliosi e per i più che hanno voglia di riascoltarli, mi congedo mettendovi a disposizione i links:
PS2
Ah per la cronaca, proprio per rimanere sul tema delle “fancy words”, lo strumento che ho utilizzato per fare questa cosa si chiama Jupyter notebook!!!
PS3 non è la playstation, ma cinque per nove fa 47!!!