Matematicamente il problema dell’apprendimento supervisionato può essere ridotto a:
“ho una o più variabili indipendenti x e una variabile dipendente y, devo trovare una funzione f tale che f(x) sia uguale a y”.
Niente di più, niente di meno.
In gergo tecnico l’insieme delle varibili indipendenti x vengono chiamate features e la varibile y viene chiamata target.
La differenza principale tra regressione e classificazione è:
- In un problema di regressione la variabile y è continua, cioè un numero.
- In un problema di classificazione la variabile y è categoria, ovvero un insieme finito di valori, i cosidetti labels, che solitamente rappresentano l’appartenenza ad una classe.
Alcuni problemi che possono essere affrontati con un modello di classificazione sono ad esempio:
- Riconoscere oggetti nelle foto.
- Effettuare diagnosi cliniche.
- Comprendere lo stato d’animo di una persona da un tweet.
Un semplice problema di classificazione con due features e due classi può essere rappresentato geometricamente come trovare l’equazione della retta che meglio riesce a dividere le due classi.
Quando le classi da predire sono solo due si parla di classificazione binaria, in questi casi il modello non fa altro che rispondere con un si o con un no
- E’ un gatto nella foto ? SI/NO
- Ha il paziente la varicella ? SI/NO
- La persona che ha scritto questo tweet è incazzata ? SI/NO
Il si equivale ad un valore 1 e viene considerato come la classe positiva, al contrario il no equivale ad un valore 0 ed è la classe negativa.
Usare il Machine Learning per riconoscere un tumore maligno
In questo articolo proveremo a eseguire una classificazione binaria per classificare tumori al seno come benigni o maligni, basandoci su alcune proprietà estratte da immagini digitalizzate. A tale scopo utilizzeremo il Wisconsis Breast Cancer Dataset. Partiamo !
Per prima cosa importiamo i vari moduli che ci serviranno, abbiamo creato dei video tutorial completi per ognuna di queste librerie, li trovi cliccando sui link.
- Numpy: la libreria Python per il calcolo numerico.
- Pandas: libreria Python per analisi e manipolazione di dati.
- Matplotlib: una libreria Python che permette di creare grafici e visualizzazioni con poche righe di codice.
- Scikit-learn: una libreria Python che contiene classi per il preprocessing dei dati e la creazione di modelli di machine learning.
import pandas as pd import numpy as np import matplotlib.pyplot as plt from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.preprocessing import LabelEncoder from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score from sklearn.metrics import log_loss
Utilizzando Pandas importiamo il dataset direttamente dalla repo all’interno di un DataFrame, una speciale struttura dati che permette di immagazzinare dati in maniera tabulare.
Purtroppo ci tocca definire manualmente i nomi di tutte le colonne.
columns = ["id","diagnosis","radius_mean","texture_mean","perimeter_mean","area_mean","smoothness_mean","compactness_mean","concavity_mean","concave points_mean","symmetry_mean","fractal_dimension_mean","radius_se","texture_se","perimeter_se","area_se","smoothness_se","compactness_se","concavity_se","concave points_se","symmetry_se","fractal_dimension_se","radius_worst","texture_worst","perimeter_worst","area_worst","smoothness_worst","compactness_worst","concavity_worst","concave points_worst","symmetry_worst","fractal_dimension_worst"] breast_cancer = pd.read_csv("https://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/wdbc.data",names=columns) breast_cancer.head()
Il metodo head mostra le prime 5 righe del DataFrame, una cosa del genere:
Tramite il metodo info del DataFrame possiamo visualizzare diverse informazioni sulle varie colonne.
breast_cancer.info()
Il dataset contiene 32 colonne, 1 di queste, diagnosis, è il nostro target, mentre le altre sono potenziali features, tranne id, di questo non ce ne facciamo nulla.
Osserva bene che il tipo di diagnosis è object, mentre tutte le altre colonne contengono numeri.
breast_cancer["diagnosis"].unique()
Utilizzando il metodo unique possiamo osservare gli elementi unici contenuti in diagnosis.
Il risultato mostrerà che la colonna è composta da due valori: M e B, che sono dei label indicanti rispettivamente un tumore maligno e uno benigno.
Classificazione con due features
Siccome il nostro obiettivo è sviluppare un modello semplice, selezioniamo solo due colonne e promuoviamole a features del nostro modello, optiamo per le seguenti:
- radius_se: l’errore standard del raggio del tumore
- concave points_worst: il numero peggiore di punti di concavità nel contorno del tumore.
Adesso creiamo due array numpy, uno contenente le features e l’altro il target.
X = breast_cancer[["radius_se","concave points_worst"]].values Y = breast_cancer['diagnosis'].values
Per poter valutare la qualità del nostro modello dobbiamo suddividere il nostro dataset in 2 set differenti:
- Train set: che ci servirà per addestrare il modello.
- Test set: che ci servirà per testarlo.
Questa suddivisione è necessaria per verificare che il modello abbia effettivamente imparato qualcosa e che sia in grado di generalizzare su dati non visti durante l’addestramento.
Possiamo eseguire la suddivisione utilizzando la funzione train_test_split, assegnando il 70% dei dati al train set e il 30% al test set.
X_train, X_test, Y_train, Y_test = train_test_split(X,Y, test_size=0.3, random_state=0)
Ricorderai che la colonna diagnosis contiene dei caratteri, ma a noi servono dei numeri !
Una tecnica da adoperare in questi casi è codificare gli oggetti sotto forma di numeri, nel nostro caso la codifica che andremo a fare è la seguente:
- M => Classe positiva = 1
- B => Classe negativa = 0
Per farlo possiamo utilizzare la classe LabelEncoder.
LabelEncoder appartiene alla categoria dei trasformatori di scikit-learn, che si occupano di eseguire trasformazioni i dati.
Un trasformatore viene inizializzato con il metodo fit, che si occupa di effettuare tutti i calcoli necessari per poi poter eseguire la trasformazione utilizzando il metodo transform.
le = LabelEncoder() Y_train = le.fit_transform(Y_train) Y_test = le.transform(Y_test)
La classe fit_transform è una scorciatoia che equivale a chiamare prima fit e poi transform sui dati passati come parametro.
Prima di costruire il modello dobbiamo essere sicuri che i dati siano sulla stessa scala, altrimenti si rischia che questo attribuisca maggiore importanza alle features con magnitudine maggiore.
Riprendiamo un momento il nostro DataFrame, selezioniamo solo le nostre due features e utilizziamo il metodo describe per ottenere le informazioni statistiche (in alternativa avremmo potuto utilizzare le funzioni np.min e np.max sui nostri array numpy)
breast_cancer[["radius_se","concave points_worst"]].describe()
L’output sarà questo:
Osservando i valori minimi e massimi delle due colonne puoi vedere che la colonna radius_se si trova in un range di valore 10 volte superiore rispetto a concave points_worst.
Per portare le features su di una scala comune abbiamo due possibilità:
- Normalizzare: portare tutti i dati in un range compreso tra 0 e 1.
- Standardizzare: creare una distribuzione normale, cioè con media 0 e deviazione standard 1.
Optiamo per la standardizzazione, possiamo eseguirla utilizzando la classe trasformatore StandardScaler di scikit-learn.
ss = StandardScaler() X_train = ss.fit_transform(X_train) X_test = ss.transform(X_test)
Adesso siamo pronti per creare il nostro modello di classificazione.
Il modello che implementeremo è la regressione logistica, che oltre alla classe di appartenenza ritorna anche la probabilità di quanto la classificazione sia corretta.
from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score lr = LogisticRegression() lr.fit(X_train, Y_train)
Abbiamo il nostro modello ! E ora ?
Adesso dobbiamo valutare quanto effettivamente sia buono, per farlo possiamo utilizzare il test set e due metriche:
- Accuracy: semplicemente conta quante delle classificazioni fatte dal modello sono corrette, ritorna un valore tra 0 e 1, maggiore è meglio
- Negative Log-likelihood (log loss): tiene conto della probabilità, ritorna un valore tra 0 ed 1, minore è meglio.
Entrambe le funzioni sono implementate in scikit-learn
Y_pred = lr.predict(X_test) Y_pred_proba = lr.predict_proba(X_test) print("ACCURACY: "+str(accuracy_score(Y_test, Y_pred))) print("LOG LOSS: "+str(log_loss(Y_test, Y_pred_proba)))
Se hai fatto tutto come me, i risultati dovrebbero essere:
- ACCURACY = 0.9064
- LOG LOSS = 0.215
Trattandosi di un modello estremamente semplice, con sole due features, i risultati sono molto buoni.
Visualizziamo graficamente il decision boundary all’interno di uno scatter plot.
def showBounds(X,Y,model,title=None): h = .02 x_min, x_max = X[:, 0].min(), X[:, 0].max() y_min, y_max = X[:, 1].min(), X[:, 1].max() xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h)) Z = model.predict(np.c_[xx.ravel(), yy.ravel()]) Z = Z.reshape(xx.shape) plt.contourf(xx, yy, Z, cmap=plt.cm.Paired) X_m = X[Y==1] X_b = X[Y==0] plt.title(title) plt.scatter(X_b[:, 0], X_b[:, 1], c="green", edgecolor='white') plt.scatter(X_m[:, 0], X_m[:, 1], c="red", edgecolor='white') plt.show() showBounds(X_train, Y_train, lr, title="Train set") showBounds(X_test, Y_test, lr, title="Test set")
Ed eccolo qui:
La funzione showBounds è abbastanza complessa e spiegarla riga per riga porterebbe via troppo tempo, in sostanza quello che fa è:
- Creare uno scatterplot con gli esempi corrispondenti a tumori maligni (in rosso).
- Creare uno scatterplot con gli esempi corrispondenti a tumori benigni (in verde)
- Eseguire una predizione per ogni punto del grafico, ricorda che l’asse delle x corrisponde alla feature radius_se, mentre quella delle y alla feature concave points_worst, se il punto viene classificato come maligno allora lo colora di marroncino, se invece viene classificato come benigno lo colora di azzurro, il confine che separa i due colori è il decision boundary.
Classificazione con tutte le features
Vediamo un po’ cosa accade sfruttando tutte le colonne del dataset originale come features.
X = breast_cancer.drop('diagnosis',axis=1).values Y = breast_cancer['diagnosis'].values X_train, X_test, Y_train, Y_test = train_test_split(X,Y, test_size=0.3, random_state=0) le = LabelEncoder() Y_train = le.fit_transform(Y_train) Y_test = le.transform(Y_test) ss = StandardScaler() X_train = ss.fit_transform(X_train) X_test = ss.transform(X_test) lr = LogisticRegression() lr.fit(X_train, Y_train) Y_pred = lr.predict(X_test) Y_pred_proba = lr.predict_proba(X_test) print("ACCURACY: "+str(accuracy_score(Y_test, Y_pred))) print("LOG LOSS: "+str(log_loss(Y_test, Y_pred_proba)))
I risultati sono:
- ACCURACY = 0.965
- LOG LOSS = 0.11
Avendo più informazioni a disposizione, il modello addestrato con tutte le features è notevolmente migliore, questo dimostra il ruolo fondamentale che hanno i dati nel machine learning.
Trovi il notebook eseguibile con il tutto il codice presente nell’articolo sulla repository Github dei tutorial di ProfessionAI.