Salta al contenuto principale

01 - Introduzione al Machine Learning: tassonomia e workflow

Come riconoscere il tipo di problema, impostare una baseline e individuare il data leakage, con esempi in scikit-learn eseguiti due volte.

Cosa ne pensi di questo articolo?

01 - Introduzione al Machine Learning: tassonomia e workflow
ML Fundamentalsfedericocalo.dev

In Breve

Come riconoscere il tipo di problema, impostare una baseline e individuare il data leakage, con esempi in scikit-learn eseguiti due volte.

Un programma tradizionale applica regole che qualcuno ha scritto; un modello di machine learning ricava la regola dagli esempi. La differenza si vede subito con un filtro antispam: scrivere a mano le regole che separano la posta utile dalla pubblicità è un lavoro senza fine, mentre dare al sistema diecimila messaggi già etichettati e lasciare che trovi da solo i segnali che li distinguono funziona, a patto di saper controllare che abbia imparato qualcosa di vero. Questo articolo apre la serie ML Fundamentals e ti serve per due decisioni: a quale famiglia appartiene il problema che hai davanti, e quali passi separano un taccuino che «funziona» da un risultato di cui ci si può fidare. Si presuppone solo un po' di Python e l'idea di media e proporzione; il resto lo definiamo man mano.

Gli esempi girano tutti su dati inclusi in scikit-learn (scikit-learn 1.9.0, numpy 2.4.6, Python 3.12), con seme fisso, senza scaricare nulla. Ogni uscita riportata è stata ottenuta due volte con lo stesso risultato. Per l'implementazione di un modello da zero passa al capitolo successivo, 02 - Regressione lineare from scratch.

Che cosa significa «imparare dai dati»

Un modello è una funzione con dei parametri regolabili: riceve in ingresso delle feature (le grandezze misurate, per esempio metratura, piano e quartiere di un appartamento) e restituisce una previsione (il prezzo). Addestrare significa scegliere i parametri che rendono piccoli gli errori sugli esempi disponibili, misurati da una funzione di perdita. Fin qui la statistica descrive la stessa cosa; ciò che il machine learning aggiunge è l'insistenza su un'unica domanda: come va il modello su dati che non ha mai visto? Un modello che ricorda a memoria gli esempi di addestramento ha errore zero e non serve a niente: è il tema del capitolo 04 - Bias-Variance Tradeoff.

La tassonomia: quale tipo di problema ho

La distinzione che organizza il resto è se hai in mano la risposta giusta per gli esempi. Il manuale di James, Witten, Hastie e Tibshirani (An Introduction to Statistical Learning, seconda edizione) la tratta nel paragrafo 2.1.4, «Supervised Versus Unsupervised Learning», e la riprende nel capitolo 12 per l'apprendimento non supervisionato.

Apprendimento supervisionato: regressione e classificazione

Nell'apprendimento supervisionato ogni esempio arriva con la sua risposta, detta etichetta o target. Se il target è un numero (il prezzo di un appartamento, la temperatura di domani) il problema è di regressione; se è una categoria (tumore benigno o maligno, messaggio spam o no) è di classificazione. I capitoli 02 e 03 costruiscono un esempio di ciascuna.

Apprendimento non supervisionato

Senza etichette si può solo cercare struttura: raggruppare gli esempi simili (clustering) oppure comprimere le feature mantenendo l'informazione (riduzione di dimensionalità). Qui la «risposta giusta» non esiste, quindi il giudizio sul risultato è meno netto, come mostra il confronto che segue sul dataset Iris, 150 fiori di tre specie misurati con quattro lunghezze. Lo stesso dataset è usato una volta con le etichette e una volta ignorandole.

import numpy as np
from sklearn.datasets import load_iris
from sklearn.cluster import KMeans
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, adjusted_rand_score
from sklearn.model_selection import train_test_split

X, y = load_iris(return_X_y=True)
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3, random_state=0, stratify=y)
clf = LogisticRegression(max_iter=1000).fit(Xtr, ytr)
print("supervisionato, accuratezza sul test:", round(accuracy_score(yte, clf.predict(Xte)), 3))
km = KMeans(n_clusters=3, n_init=10, random_state=0).fit(X)
print("non supervisionato, indice di Rand corretto:", round(adjusted_rand_score(y, km.labels_), 3))
print("dimensioni dei cluster:", sorted(np.bincount(km.labels_).tolist()))

# Output:
# supervisionato, accuratezza sul test: 1.0
# non supervisionato, indice di Rand corretto: 0.73
# dimensioni dei cluster: [38, 50, 62]

Il classificatore vede le etichette in addestramento e sui 45 fiori messi da parte (test_size=0.3, quindi il 30% di 150) non sbaglia nulla. stratify=y mantiene le tre specie in proporzione uguale nei due blocchi, altrimenti uno dei due potrebbe averne troppo poche. Un'accuratezza di 1.0 su 45 esempi non è una garanzia: con un altro seme di partizione potresti vedere un errore, ed è un motivo per cui il capitolo 05 ripete la valutazione su più partizioni. K-means, che non ha mai visto le specie, trova tre gruppi di 38, 50 e 62 fiori contro i 50, 50, 50 reali: l'indice di Rand corretto (adjusted_rand_score) vale 1 per due partizioni identiche e circa 0 per partizioni casuali, e 0.73 dice che il raggruppamento coincide in buona parte ma non del tutto con le specie. Il metodo ha ritrovato quasi la struttura senza sapere nulla di botanica, e questo è ciò che si può chiedere a un algoritmo non supervisionato.

Le altre famiglie, in una riga ciascuna

Nell'apprendimento semi-supervisionato solo una piccola parte degli esempi ha l'etichetta. Nell'apprendimento auto-supervisionato l'etichetta si ricava dai dati stessi, per esempio nascondendo una parola di una frase e chiedendo al modello di indovinarla. Nell'apprendimento per rinforzo un agente sceglie azioni e riceve un premio, senza che nessuno gli dica la mossa giusta. Questa serie si concentra sulle prime due famiglie, che coprono la maggior parte del lavoro su dati tabellari.

Per orientarti, la domanda è quale dato hai davvero a disposizione:

  • Esempi con un target numerico: regressione.
  • Esempi con una categoria da prevedere: classificazione.
  • Esempi senza alcun target: clustering o riduzione di dimensionalità.
  • Pochi esempi etichettati e molti non etichettati: semi-supervisionato.
  • Una sequenza di decisioni con un premio finale: rinforzo.

Le voci sono alternative, non una sequenza, ma la scelta non è sempre libera: spesso il tuo problema ammette due formulazioni, e conviene preferire quella per cui l'etichetta si può davvero ottenere. Prevedere se un cliente lascerà il servizio è una classificazione; prevedere fra quanti giorni lo farà è una regressione che richiede però di aspettare che il cliente se ne vada, quindi cambia il costo di raccogliere le etichette.

Il workflow, dalla domanda al monitoraggio

I passi hanno un ordine che non si può invertire senza danno, in particolare il terzo rispetto agli altri:

  1. Formula la domanda e la metrica con cui sapresti di aver risposto bene (per esempio «meno del 5% di messaggi utili finiti nello spam»).
  2. Guarda i dati: quante righe, quante feature, quanti valori mancanti, che proporzione hanno le classi.
  3. Metti da parte un insieme di test prima di prendere qualunque decisione guardando i dati, e non toccarlo più fino alla fine.
  4. Costruisci una baseline banale (per esempio rispondere sempre con la classe più frequente) per sapere quanto vale «non fare niente».
  5. Addestra il modello vero e confrontalo con la baseline su dati di validazione.
  6. Mettilo in uso e controlla che i dati in ingresso non cambino nel tempo (capitolo 12).

L'elenco è una sequenza vera: se il passo 3 viene dopo il 5, il test ha già influenzato le tue scelte e non misura più nulla di indipendente. Il passo più facile da saltare è il 4, ed è quello che dà senso a ogni numero successivo, come si vede nel prossimo esperimento sul dataset Wisconsin Breast Cancer, 569 tumori descritti da 30 misure e classificati in benigni (classe 1) o maligni (classe 0).

import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.dummy import DummyClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

X, y = load_breast_cancer(return_X_y=True)
print("esempi, feature:", X.shape, "- classe 1 (benigno):", round(y.mean(), 3))
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)
base = DummyClassifier(strategy="most_frequent").fit(Xtr, ytr)
model = make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000)).fit(Xtr, ytr)
print("baseline (sempre la classe piu' frequente):", round(base.score(Xte, yte), 3))
print("regressione logistica con standardizzazione:", round(model.score(Xte, yte), 3))
print("accuratezza sul training:", round(model.score(Xtr, ytr), 3))

# Output:
# esempi, feature: (569, 30) - classe 1 (benigno): 0.627
# baseline (sempre la classe piu' frequente): 0.629
# regressione logistica con standardizzazione: 0.986
# accuratezza sul training: 0.988

La baseline DummyClassifier(strategy="most_frequent") risponde sempre «benigno» e ottiene 0.629 sull'insieme di test (90 casi giusti su 143): è la soglia da battere, e coincide quasi con la proporzione di benigni nel dataset, 0.627. Il modello vero arriva a 0.986 (141 su 143). La make_pipeline unisce StandardScaler e regressione logistica in un oggetto solo: lo scaler calcola media e deviazione standard dai soli dati di addestramento e le applica al test, che è esattamente la separazione che il passo 3 chiede. L'accuratezza sul training (0.988) è appena sopra quella sul test (0.986): la differenza è piccola, quindi qui non c'è sovradattamento visibile. Senza la baseline, «0.986» sembrerebbe un risultato e basta; con la baseline si legge come un salto da 0.63 a 0.99.

Data leakage: quando la metrica mente

Il data leakage è l'uso, durante l'addestramento, di informazione che in produzione non avresti; la documentazione di scikit-learn («Common pitfalls and recommended practices») lo tratta con esempi di pre-elaborazione eseguita prima della divisione. La versione più insidiosa è la selezione delle feature fatta guardando tutte le etichette. Per vederla con i numeri costruiamo un caso in cui la risposta corretta è nota: 50 esempi, 5000 feature di puro rumore, etichette scelte a caso. Nessun modello può fare meglio del 50%.

import numpy as np
from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score, StratifiedKFold
from sklearn.pipeline import make_pipeline

rng = np.random.default_rng(0)
X = rng.standard_normal((50, 5000))      # 50 esempi, 5000 feature di puro rumore
y = np.repeat([0, 1], 25)                # etichette che non dipendono da X
cv = StratifiedKFold(5, shuffle=True, random_state=0)

# modo SBAGLIATO: scelgo le 20 feature migliori guardando TUTTE le etichette, poi valido
X_sel = SelectKBest(f_classif, k=20).fit_transform(X, y)
sbagliato = cross_val_score(LogisticRegression(), X_sel, y, cv=cv).mean()

# modo GIUSTO: la selezione sta dentro la pipeline, rifatta in ogni fold
pipe = make_pipeline(SelectKBest(f_classif, k=20), LogisticRegression())
giusto = cross_val_score(pipe, X, y, cv=cv).mean()
print("selezione fuori dal fold:", round(sbagliato, 3))
print("selezione dentro il fold:", round(giusto, 3))

# Output:
# selezione fuori dal fold: 0.96
# selezione dentro il fold: 0.52

Con la selezione fuori dalla validazione incrociata l'accuratezza stimata è 0.96, un risultato che sembrerebbe eccezionale su dati che sono rumore puro. Il motivo: scegliendo le 20 feature su 5000 più correlate con le etichette di tutti gli esempi, qualche feature coincide per caso con le etichette anche negli esempi poi usati per il test, e il modello «scopre» una regolarità che non esiste. Mettendo SelectKBest dentro la pipeline la selezione viene rifatta in ogni fold usando solo gli esempi di addestramento di quel fold, e la stima scende a 0.52, cioè al caso. Il difetto non sta nel modello ma nel punto in cui nasce ogni decisione che usa le etichette: va cercato lì. Qui cross_val_score è usato come strumento già pronto; come funziona e quando falsa la stima lo spiega il capitolo 05.

Esercizio svolto: la precisione ingannevole

Un reparto vuole un modello che segnali i pazienti con una certa malattia: su 1000 pazienti ne sono malati 20 (2%). Qualcuno propone un modello che risponde sempre «sano» e dichiara «accuratezza del 98%». Calcoliamo che cosa significa davvero, con dati sintetici costruiti per l'occasione.

import numpy as np
from sklearn.metrics import accuracy_score, recall_score

y_vero = np.array([1] * 20 + [0] * 980)      # 20 pazienti malati su 1000 (dati sintetici)
y_pred = np.zeros(1000, dtype=int)           # il modello dice sempre "sano"
print("accuratezza:", accuracy_score(y_vero, y_pred))
print("recall sui malati:", recall_score(y_vero, y_pred))

# Output:
# accuratezza: 0.98
# recall sui malati: 0.0

Accuratezza 0.98 e recall 0.0: la recall è la quota di malati effettivamente trovati (veri positivi divisi per tutti i positivi reali), e qui nessuno dei 20 viene segnalato. Un modello inutile ha ottenuto un voto alto perché la classe interessante è rara. La lezione operativa è doppia: confronta sempre il modello con la baseline, e scegli la metrica in base al costo degli errori e non per abitudine. Il capitolo 11 - Metriche e valutazione le sviluppa tutte.

Quando non usare il machine learning

Un modello è una scommessa statistica; quando si può evitare, si evita. Tre situazioni in cui non conviene:

  • La regola esiste ed è esatta: il calcolo di un'imposta o di una scadenza si scrive come codice, e un modello ci aggiungerebbe errori.
  • Non hai abbastanza esempi per il fenomeno che vuoi descrivere, in particolare per le classi rare.
  • Un errore del sistema ha un costo che nessuno è disposto a pagare e nessuno controlla l'output.

Il criterio comune è che il machine learning serve dove la regola è troppo complessa da scrivere ma gli esempi abbondano e l'errore occasionale è tollerabile; negli altri casi una tabella o un'istruzione condizionale sono più economiche, più spiegabili e più facili da controllare.

FAQ - Domande frequenti

Qual è la differenza tra machine learning e intelligenza artificiale?

L'intelligenza artificiale è il campo più vasto, che include anche sistemi a regole scritte a mano; il machine learning ne è la parte in cui il comportamento si ricava da esempi. Ogni modello di questa serie è machine learning, non tutta l'intelligenza artificiale lo è.

Serve la matematica avanzata per cominciare?

Per usare scikit-learn bastano Python e le nozioni di media e proporzione; per capire perché un metodo funziona servono algebra lineare e derivate, e i capitoli successivi le introducono quando servono, a partire dal gradiente nel capitolo 02.

Posso fare machine learning senza GPU?

Sì: tutti gli esempi di questo articolo girano su CPU in pochi secondi. La GPU diventa utile per le reti neurali grandi, trattate dal capitolo 10 in avanti.

Che cosa sai fare adesso

Sai distinguere un problema supervisionato da uno non supervisionato guardando i dati che hai, sai impostare un confronto onesto con una baseline, e sai riconoscere il punto in cui la selezione delle feature fa trapelare le etichette nella validazione. Il passo successivo è costruire un modello da zero per vederne i parametri: 02 - Regressione lineare from scratch in Python.

Fonti

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti