La classificazione è, insieme alla regressione di cui già abbiamo parlato qui, uno dei due problemi principali dell’apprendimento supervisionato, la branca del machine learning che si occupa di insegnare ai computer come risolvere determinati problemi mostrandogli esempi di come questi sono già stati risolti in passato.

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.

Esempio di modello di classificazione per classificare un tumore come benigno o maligno in base al numero di punti di concavità e valore medio del raggio, la retta che separa le classi è chiamata confine di decisione (decision boundary)

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:

A destra il decision boundary per il train set, a sinistra quello per il test set, i tumori maligni sono evidenziati in rosso, quelli benigni in verde.

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.


Ti interessa il Machine Learning ? Se vuoi approfondire più nel dettaglio la regressione e altri argomenti come classificazione e clustering allora dai uno sguardo al nostro Corso Pratico sul Machine Learning con Python, disponbile su Udemy a soli 9.90 € per un periodo di tempo limitato.


 

About Giuseppe Gullo

Cresciuto a pane e bit, ho cominciato a programmare a 13 anni, durante un periodo di convalescenza forzata dovuta ad un brutto incidente.

Durante la mia adolescenza ho utilizzato un mio approccio hacker all'apprendimento per passare da un argomento all'altro senza sosta, sviluppo web, programmazione software, sviluppo mobile per android ed iOS, sviluppo di videogame 2d e 3d con Unity.

Poco più che ventenne mi sono avvicinato all'intelligenza artificiale, ed è stato amore a prima vista.

Ho lavorato come sviluppatore indipendente e freelancer, creando diverse dozzine di servizi che hanno raggiunto centinaia di migliaia di persone in tutto il mondo.

Il mio life goal è riuscire a sfruttare le enormi potenzialità dell'AI per migliorare le condizioni di vita degli esseri umani.