Corso
Implementerai KNN sul famoso dataset Iris.
Nota: Potresti prendere in considerazione il corso Machine Learning with Python oppure, per avere un contesto su come si è evoluto l’ML e molto altro, leggere questo post.
Introduzione
Il Machine Learning nasce dall’informatica e studia principalmente la progettazione di algoritmi in grado di apprendere dall’esperienza. Per imparare hanno bisogno di dati con determinati attributi, in base ai quali gli algoritmi cercano di trovare pattern predittivi significativi. In generale, i compiti di ML si possono classificare in apprendimento di concetti, clustering, modellazione predittiva, ecc. L’obiettivo finale degli algoritmi di ML è riuscire a prendere decisioni corrette senza intervento umano. La previsione dei titoli azionari o del meteo sono due esempi di applicazioni degli algoritmi di machine learning.
Esistono vari algoritmi di machine learning come alberi decisionali, Naive Bayes, Random Forest, Support Vector Machine, K-nearest neighbor, K-means clustering, ecc.
Dalla famiglia degli algoritmi di machine learning, quello che userai oggi è il k-nearest neighbor.
Ora, la domanda è: che cos’è esattamente l’algoritmo K-Nearest Neighbor? Scopriamolo!
Che cos’è un k-Nearest Neighbor?
KNN, o algoritmo dei k vicini più prossimi, è un algoritmo di apprendimento supervisionato; “supervisionato” significa che utilizza le etichette di classe dei dati di addestramento durante la fase di apprendimento. È un algoritmo di machine learning basato su istanze, in cui i nuovi punti dati vengono classificati in base a istanze (punti dati) archiviate e già etichettate. KNN può essere usato sia per classificazione sia per regressione; tuttavia, è più diffuso per la classificazione.
Il k in KNN è una variabile cruciale, detta anche iperparametro, che aiuta a classificare con precisione un punto dati. Più precisamente, k è il numero di vicini più prossimi da cui vuoi raccogliere un voto quando classifichi un nuovo punto dati.

Si vede che, al crescere di k da 1 a 7, il confine decisionale tra due classi con alcuni punti dati diventa più liscio.
La domanda ora è: come avviene tutta questa “magia”, per cui ogni volta che arriva un nuovo punto dati esso viene classificato in base ai punti già memorizzati?
Vediamolo rapidamente in questo modo:
- Per prima cosa carichi tutti i dati e inizializzi il valore di k,
- poi calcoli la distanza tra i punti dati memorizzati e un nuovo punto che vuoi classificare usando vari metriche di similarità o distanza come distanza Manhattan (L1), distanza Euclidea (L2), similarità del coseno, distanza di Bhattacharyya, distanza di Chebyshev, ecc.
- Successivamente ordini i valori di distanza in ordine crescente o decrescente e determini i top o lower k vicini più prossimi.
- Raccogli le etichette dei k vicini più prossimi e usi un voto di maggioranza o un voto pesato per classificare il nuovo punto dati. Al nuovo punto viene assegnata un’etichetta di classe in base al punto che ha il punteggio più alto tra tutti quelli memorizzati.
- Infine, restituisci la classe predetta per la nuova istanza.
La predizione può essere di due tipi: classificazione, in cui a un nuovo punto dati si assegna un’etichetta di classe, oppure regressione, in cui si assegna un valore al nuovo punto. A differenza della classificazione, nella regressione si assegna al nuovo punto la media dei valori dei k vicini più prossimi.
Svantaggi di KNN: in primo luogo, la complessità nella ricerca dei vicini più prossimi per ogni nuovo punto dati. In secondo luogo, determinare il valore di k a volte diventa un compito tedioso. Infine, non è sempre chiaro quale metrica di distanza usare per calcolare i vicini.
Basta teoria, giusto? Allora carichiamo, analizziamo e capiamo i dati che userai in questo piccolo tutorial.
Caricare i dati Iris
Il dataset Iris è composto da 150 campioni con tre classi, cioè Iris-Setosa, Iris-Versicolor e Iris-Virginica. Quattro caratteristiche/attributi che contribuiscono a identificare univocamente una delle tre classi sono sepal-length, sepal-width, petal-length e petal-width.
Sentiti libero di usare un altro dataset pubblico o uno tuo privato.
Sklearn è una libreria di machine learning per Python ampiamente usata in data science. Include vari algoritmi di classificazione, regressione e clustering, tra cui support vector machines, random forests, gradient boosting, k-means, KNN, ecc. In sklearn trovi una libreria chiamata datasets con vari dataset per diversi compiti, incluso Iris, tutti caricabili “out of the box”. È piuttosto intuitivo e diretto. Quindi carichiamo subito il dataset iris.
from sklearn.datasets import load_iris
load_iris contiene sia i dati sia le etichette di classe per ogni campione. Estraiamoli rapidamente.
data = load_iris().data
La variabile data sarà un array numpy di forma (150,4) con 150 campioni, ciascuno con quattro attributi. Ogni classe ha 50 campioni.
data.shape
(150, 4)
Estraiamo le etichette di classe.
labels = load_iris().target
labels.shape
(150,)
Ora devi combinare dati ed etichette; per farlo userai un’ottima libreria Python chiamata NumPy. NumPy aggiunge supporto per array e matrici di grandi dimensioni e multidimensionali, insieme a un’ampia collezione di funzioni matematiche di alto livello per operare su questi array. Importiamola subito!
import numpy as np
Poiché data è un array 2D, dovrai rimodellare anche labels in un array 2D.
labels = np.reshape(labels,(150,1))
Ora userai la funzione concatenate disponibile in numpy e imposterai axis=-1 per concatenare sulla seconda dimensione.
data = np.concatenate([data,labels],axis=-1)
data.shape
(150, 5)
Poi importerai la libreria di analisi dati di Python chiamata pandas, utile quando vuoi organizzare i dati in forma tabellare ed eseguire operazioni e manipolazioni. In particolare, offre strutture dati e operazioni per manipolare tabelle numeriche e serie temporali.
In questo tutorial userai pandas in modo piuttosto esteso.
import pandas as pd
names = ['sepal-length', 'sepal-width', 'petal-length', 'petal-width', 'species']
dataset = pd.DataFrame(data,columns=names)
Ora hai il data frame dataset che contiene sia i dati sia le etichette di classe che ti servono!
Prima di andare oltre, ricorda che la variabile labels ha etichette di classe come valori numerici, ma le convertirai nei nomi dei fiori, ovvero nelle specie.
Per farlo selezionerai solo la colonna class e sostituirai ognuno dei tre valori numerici con la specie corrispondente. Userai inplace=True per modificare direttamente il data frame dataset.
dataset['species'].replace(0, 'Iris-setosa',inplace=True)
dataset['species'].replace(1, 'Iris-versicolor',inplace=True)
dataset['species'].replace(2, 'Iris-virginica',inplace=True)
Stampiamo le prime cinque righe di dataset per vedere com’è!
dataset.head(5)
| sepal-length | sepal-width | petal-length | petal-width | species | |
|---|---|---|---|---|---|
| 0 | 5.1 | 3.5 | 1.4 | 0.2 | Iris-setosa |
| 1 | 4.9 | 3.0 | 1.4 | 0.2 | Iris-setosa |
| 2 | 4.7 | 3.2 | 1.3 | 0.2 | Iris-setosa |
| 3 | 4.6 | 3.1 | 1.5 | 0.2 | Iris-setosa |
| 4 | 5.0 | 3.6 | 1.4 | 0.2 | Iris-setosa |
Analizza i tuoi dati
Scopriamo rapidamente come appaiono le tre specie quando vengono visualizzate e quanto sono diverse tra loro non solo nei numeri ma anche nella realtà!
(Fonte)Visualizziamo i dati caricati sopra con uno scatterplot per capire quanto una variabile influenzi l’altra, ovvero quanto siano correlate due variabili.
Userai la libreria matplotlib per visualizzare i dati con uno scatterplot.
import matplotlib.pyplot as plt
Consiglio: Vuoi imparare diversi modi per visualizzare i dati in Python? Dai un’occhiata al corso Introduction to data visualization with matplotlib.
plt.figure(4, figsize=(10, 8))
plt.scatter(data[:50, 0], data[:50, 1], c='r', label='Iris-setosa')
plt.scatter(data[50:100, 0], data[50:100, 1], c='g',label='Iris-versicolor')
plt.scatter(data[100:, 0], data[100:, 1], c='b',label='Iris-virginica')
plt.xlabel('Sepal length',fontsize=20)
plt.ylabel('Sepal width',fontsize=20)
plt.xticks(fontsize=20)
plt.yticks(fontsize=20)
plt.title('Sepal length vs. Sepal width',fontsize=20)
plt.legend(prop={'size': 18})
plt.show()

Dal grafico sopra è evidente che c’è un’alta correlazione per i fiori di Iris setosa rispetto a lunghezza e larghezza del sepalo. Al contrario, c’è meno correlazione tra Iris versicolor e Iris virginica. I punti dati di versicolor e virginica sono più dispersi rispetto a quelli di setosa, che sono più densi.
Tracciamo rapidamente anche il grafico per petal-length e petal-width.
plt.figure(4, figsize=(8, 8))
plt.scatter(data[:50, 2], data[:50, 3], c='r', label='Iris-setosa')
plt.scatter(data[50:100, 2], data[50:100, 3], c='g',label='Iris-versicolor')
plt.scatter(data[100:, 2], data[100:, 3], c='b',label='Iris-virginica')
plt.xlabel('Petal length',fontsize=15)
plt.ylabel('Petal width',fontsize=15)
plt.xticks(fontsize=15)
plt.yticks(fontsize=15)
plt.title('Petal length vs. Petal width',fontsize=15)
plt.legend(prop={'size': 20})
plt.show()

Anche per petal-length e petal-width, il grafico indica una forte correlazione per i fiori setosa, che risultano densamente raggruppati.
Per validare ulteriormente quanto petal-length e petal-width siano correlate, tracciamo una matrice di correlazione su tutte e tre le specie.
dataset.iloc[:,2:].corr()
| petal-length | petal-width | |
|---|---|---|
| petal-length | 1.000000 | 0.962865 |
| petal-width | 0.962865 | 1.000000 |
La tabella sopra indica una forte correlazione di 0,96 tra petal-length e petal-width quando si combinano le tre specie.
Analizziamo anche la correlazione separatamente per ciascuna specie.
dataset.iloc[:50,:].corr() #setosa
| sepal-length | sepal-width | petal-length | petal-width | |
|---|---|---|---|---|
| sepal-length | 1.000000 | 0.742547 | 0.267176 | 0.278098 |
| sepal-width | 0.742547 | 1.000000 | 0.177700 | 0.232752 |
| petal-length | 0.267176 | 0.177700 | 1.000000 | 0.331630 |
| petal-width | 0.278098 | 0.232752 | 0.331630 | 1.000000 |
dataset.iloc[50:100,:].corr() #versicolor
| sepal-length | sepal-width | petal-length | petal-width | |
|---|---|---|---|---|
| sepal-length | 1.000000 | 0.525911 | 0.754049 | 0.546461 |
| sepal-width | 0.525911 | 1.000000 | 0.560522 | 0.663999 |
| petal-length | 0.754049 | 0.560522 | 1.000000 | 0.786668 |
| petal-width | 0.546461 | 0.663999 | 0.786668 | 1.000000 |
dataset.iloc[100:,:].corr() #virginica
| sepal-length | sepal-width | petal-length | petal-width | |
|---|---|---|---|---|
| sepal-length | 1.000000 | 0.457228 | 0.864225 | 0.281108 |
| sepal-width | 0.457228 | 1.000000 | 0.401045 | 0.537728 |
| petal-length | 0.864225 | 0.401045 | 1.000000 | 0.322108 |
| petal-width | 0.281108 | 0.537728 | 0.322108 | 1.000000 |
Dalle tre tabelle sopra è chiaro che la correlazione tra petal-length e petal-width di setosa e virginica è rispettivamente 0,33 e 0,32. Per versicolor è 0,78.
Ora visualizziamo la distribuzione delle feature tracciando gli istogrammi:
fig = plt.figure(figsize = (8,8))
ax = fig.gca()
dataset.hist(ax=ax)
plt.show()

petal-length, petal-width e sepal-length mostrano una distribuzione unimodale, mentre sepal-width mostra una sorta di distribuzione Gaussiana. Tutto ciò è utile per decidere un algoritmo che lavori bene con questo tipo di distribuzioni.
Ora analizzerai se tutti e quattro gli attributi sono sulla stessa scala; questo è un aspetto essenziale dell’ML. Il data frame pandas ha una funzione integrata, describe, che ti dà count, mean, max, min dei dati in formato tabellare.
dataset.describe()
| sepal-length | sepal-width | petal-length | petal-width | |
|---|---|---|---|---|
| count | 150.000000 | 150.000000 | 150.000000 | 150.000000 |
| mean | 5.843333 | 3.057333 | 3.758000 | 1.199333 |
| std | 0.828066 | 0.435866 | 1.765298 | 0.762238 |
| min | 4.300000 | 2.000000 | 1.000000 | 0.100000 |
| 25% | 5.100000 | 2.800000 | 1.600000 | 0.300000 |
| 50% | 5.800000 | 3.000000 | 4.350000 | 1.300000 |
| 75% | 6.400000 | 3.300000 | 5.100000 | 1.800000 |
| max | 7.900000 | 4.400000 | 6.900000 | 2.500000 |
Si vede che tutti e quattro gli attributi hanno una scala simile tra 0 e 8 e sono in centimetri; volendo puoi ridimensionarli ulteriormente tra 0 e 1.
Anche se sappiamo che ci sono 50 campioni per classe, cioè ~33,3% della distribuzione totale, ricontrolliamo!
print(dataset.groupby('species').size())
species
Iris-setosa 50
Iris-versicolor 50
Iris-virginica 50
dtype: int64
Preprocessare i tuoi dati
Dopo aver caricato e analizzato a fondo i dati, è il momento di prepararli per alimentarli al tuo modello di ML. In questa sezione pre-processerai i dati in due modi: normalizzandoli e suddividendoli in set di addestramento e di test.
Normalizzare i dati
Ci sono due modi per normalizzare i dati:
- Normalizzazione per esempio, in cui normalizzi ogni campione individualmente,
- Normalizzazione per feature, in cui normalizzi ogni caratteristica allo stesso modo su tutti i campioni.
Perché o quando devi normalizzare i dati? E serve standardizzare i dati di Iris?
La risposta è: quasi sempre. È buona pratica normalizzare i dati perché porta tutti i campioni sulla stessa scala e nello stesso intervallo. La normalizzazione è cruciale quando i dati non sono coerenti. Puoi controllare l’incoerenza usando la funzione describe() vista sopra, che ti darà i valori max e min. Se i max e min di una feature sono significativamente più grandi di quelli di un’altra, normalizzare entrambe le feature sulla stessa scala è molto importante.
Poniamo che X sia una feature con un intervallo ampio e Y una seconda feature con un intervallo ridotto. L’influenza di Y può essere sovrastata da quella di X. In tal caso conviene normalizzare sia X sia Y.
Nei dati Iris, la normalizzazione non è necessaria.
Stampiamo di nuovo describe() per capire perché non serve normalizzare.
dataset.describe()
| sepal-length | sepal-width | petal-length | petal-width | |
|---|---|---|---|---|
| count | 150.000000 | 150.000000 | 150.000000 | 150.000000 |
| mean | 5.843333 | 3.057333 | 3.758000 | 1.199333 |
| std | 0.828066 | 0.435866 | 1.765298 | 0.762238 |
| min | 4.300000 | 2.000000 | 1.000000 | 0.100000 |
| 25% | 5.100000 | 2.800000 | 1.600000 | 0.300000 |
| 50% | 5.800000 | 3.000000 | 4.350000 | 1.300000 |
| 75% | 6.400000 | 3.300000 | 5.100000 | 1.800000 |
| max | 7.900000 | 4.400000 | 6.900000 | 2.500000 |
L’attributo sepal-length ha valori da 4,3 a 7,9, sepal-width contiene valori da 2 a 4,4, mentre petal-length varia da 1 a 6,9 e petal-width da 0,1 a 2,5. I valori di tutte le feature sono nell’intervallo 0,1–7,9, che puoi considerare accettabile. Quindi non è necessario applicare alcuna normalizzazione al dataset Iris.
Suddividere i dati
Questo è un altro aspetto fondamentale del machine learning, poiché l’obiettivo è creare un modello capace di prendere decisioni o classificare i dati in un ambiente di test senza intervento umano. Pertanto, prima di distribuire il tuo modello ML in produzione, devi assicurarti che generalizzi bene sui dati di test.
A questo scopo ti servono un set di addestramento e uno di test. Tornando ai dati Iris, hai 150 campioni: allenerai il modello sull’80% dei dati e userai il restante 20% per il test.
In data science incontrerai spesso il termine Overfitting, che indica che il modello ha imparato molto bene i dati di addestramento ma fallisce sui dati di test. Quindi suddividere i dati in addestramento e test (o validazione) ti aiuta a capire se il modello sta andando in overfitting.
Per la suddivisione tra training e test userai la libreria sklearn, che ha una funzione integrata chiamata train_test_split. Suddividiamo i dati.
from sklearn.model_selection import train_test_split
train_data, test_data, train_label, test_label = train_test_split(dataset.iloc[:,:3], dataset.iloc[:,3], test_size=0.2, random_state=42)
Nota che random_state è un seed che accetta un random_state in input: se cambi il numero, cambia anche la suddivisione dei dati. Se invece mantieni lo stesso random_state e riesegui la cella più volte, la suddivisione rimarrà invariata.
Stampiamo rapidamente le forme dei dati di addestramento e di test insieme alle loro etichette.
train_data.shape,train_label.shape,test_data.shape,test_label.shape
((120, 3), (120,), (30, 3), (30,))
Finalmente è il momento di alimentare i dati all’algoritmo dei k vicini più prossimi!
Il modello KNN
Dopo aver caricato, analizzato e preprocessato i dati, è il momento di inserirli nel modello KNN. Per farlo userai la funzione integrata di sklearn neighbors, che include una classe chiamata KNeigborsClassifier.
Iniziamo importando il classificatore.
from sklearn.neighbors import KNeighborsClassifier
Nota: il parametro k (n_neighbors) è spesso un numero dispari per evitare parità nei punteggi di voto.
Per decidere il valore migliore dell’iperparametro k, farai una sorta di grid-search. Allenerai e testerai il modello su 10 diversi valori di k e infine userai quello che ti dà i risultati migliori.
Inizializziamo una variabile neighbors(k) con valori da 1 a 9 e due matrici di zeri numpy, train_accuracy e test_accuracy, per le accuratezze di training e test. Ti serviranno poi per tracciare un grafico e scegliere il valore migliore di neighbor.
neighbors = np.arange(1,9)
train_accuracy =np.zeros(len(neighbors))
test_accuracy = np.zeros(len(neighbors))
Nel prossimo blocco avverrà tutta la magia. Farai l’enumerate su tutti e nove i valori dei vicini e, per ciascuno, effettuerai la predizione sia sui dati di addestramento sia di test. Infine, memorizzerai l’accuratezza negli array numpy train_accuracy e test_accuracy.
for i,k in enumerate(neighbors):
knn = KNeighborsClassifier(n_neighbors=k)
#Fit the model
knn.fit(train_data, train_label)
#Compute accuracy on the training set
train_accuracy[i] = knn.score(train_data, train_label)
#Compute accuracy on the test set
test_accuracy[i] = knn.score(test_data, test_label)
Ora traccerai le accuratezze di training e test usando matplotlib; con il grafico accuracy vs. varying number of neighbors potrai scegliere il valore di k a cui il modello performa meglio.
plt.figure(figsize=(10,6))
plt.title('KNN accuracy with varying number of neighbors',fontsize=20)
plt.plot(neighbors, test_accuracy, label='Testing Accuracy')
plt.plot(neighbors, train_accuracy, label='Training accuracy')
plt.legend(prop={'size': 20})
plt.xlabel('Number of neighbors',fontsize=20)
plt.ylabel('Accuracy',fontsize=20)
plt.xticks(fontsize=20)
plt.yticks(fontsize=20)
plt.show()

Osservando il grafico, sembra che con n_neighbors=3 il modello dia il meglio. Quindi restiamo su n_neighbors=3 e rieseguiamo l’addestramento.
knn = KNeighborsClassifier(n_neighbors=3)
#Fit the model
knn.fit(train_data, train_label)
#Compute accuracy on the training set
train_accuracy = knn.score(train_data, train_label)
#Compute accuracy on the test set
test_accuracy = knn.score(test_data, test_label)
Valutare il tuo modello
Nell’ultimo segmento di questo tutorial valuterai il modello sui dati di test usando alcune tecniche come confusion_matrix e classification_report.
Controlliamo prima l’accuratezza del modello sui dati di test.
test_accuracy
0.9666666666666667
Voilà! Sembra che il modello sia riuscito a classificare correttamente il 96,66% dei dati di test. Non è fantastico? Con poche righe di codice sei riuscito ad addestrare un modello di ML che ora sa dirti il nome del fiore usando solo quattro feature, con un’accuratezza del 96,66%. Chissà, magari ha fatto meglio di un essere umano.
Matrice di confusione
Una matrice di confusione serve principalmente a descrivere le prestazioni del modello sui dati di test per i quali sono note le etichette vere.
Scikit-learn fornisce una funzione che calcola per te la matrice di confusione.
prediction = knn.predict(test_data)
La seguente funzione plot_confusion_matrix() è stata modificata e ripresa da questa fonte.
import itertools
def plot_confusion_matrix(cm, classes,
normalize=False,
title='Confusion matrix',
cmap=plt.cm.Blues):
plt.imshow(cm, interpolation='nearest', cmap=cmap)
plt.title(title)
plt.colorbar()
tick_marks = np.arange(len(classes))
plt.xticks(tick_marks, classes, rotation=45)
plt.yticks(tick_marks, classes)
fmt = '.2f' if normalize else 'd'
thresh = cm.max() / 2.
for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):
plt.text(j, i, format(cm[i, j], fmt),
horizontalalignment="center",
color="white" if cm[i, j] > thresh else "black")
plt.ylabel('True label',fontsize=30)
plt.xlabel('Predicted label',fontsize=30)
plt.tight_layout()
plt.xticks(fontsize=18)
plt.yticks(fontsize=18)
class_names = load_iris().target_names
# Compute confusion matrix
cnf_matrix = confusion_matrix(test_label, prediction)
np.set_printoptions(precision=2)
# Plot non-normalized confusion matrix
plt.figure(figsize=(10,8))
plot_confusion_matrix(cnf_matrix, classes=class_names)
plt.title('Confusion Matrix',fontsize=30)
plt.show()

Dal grafico della confusion_matrix sopra, puoi osservare che il modello ha classificato correttamente tutti i fiori tranne un virginica, classificato come versicolor.
Report di classificazione
Il report di classificazione ti aiuta a individuare le classi misclassificate in modo più dettagliato fornendo precision, recall e F1 score per ciascuna classe. Userai la libreria sklearn per visualizzare il report di classificazione.
from sklearn.metrics import classification_report
print(classification_report(test_label, prediction))
precision recall f1-score support
Iris-setosa 1.00 1.00 1.00 10
Iris-versicolor 0.90 1.00 0.95 9
Iris-virginica 1.00 0.91 0.95 11
micro avg 0.97 0.97 0.97 30
macro avg 0.97 0.97 0.97 30
weighted avg 0.97 0.97 0.97 30
Vai oltre!
Per prima cosa, complimenti a chi è arrivato fino in fondo! Ma questo era solo l’inizio. C’è ancora molta strada da fare!
Questo tutorial ha trattato principalmente le basi del machine learning e l’implementazione di un tipo di algoritmo di ML noto come KNN con Python. Il dataset Iris che hai usato è piuttosto piccolo e relativamente semplice.
Se questo tutorial ha acceso in te l’interesse a saperne di più, prova a usare altri dataset o ad approfondire altri algoritmi di ML, magari applicandoli al dataset Iris per osservare l’effetto sull’accuratezza. In questo modo imparerai molto di più che con la sola teoria!
Se hai sperimentato abbastanza con le basi presentate qui e con altri algoritmi di machine learning, potresti voler approfondire Python e l’analisi dei dati.

