Cross-Validation in Python: K-Fold, Stratificata, LOOCV
Impara la cross-validation in Python: K-Fold, Stratificata, Leave-One-Out, CV annidata e pipeline con scikit-learn — con esempi eseguibili.
La cross-validation è il metodo standard per stimare quanto bene un modello di machine learning si comporterà su dati mai visti. Invece di affidarsi a una singola suddivisione train/test — che può produrre un punteggio eccessivamente ottimistico o pessimistico a seconda di quali campioni finiscono in quale parte — la cross-validation addestra e valuta il modello più volte su diverse partizioni dei dati e ne calcola la media.
Questa pagina tratta:
- Perché una singola suddivisione train/test non è sufficiente
- La cross-validation K-Fold e come scegliere
k - La K-Fold Stratificata per distribuzioni di classi sbilanciate
- La cross-validation Leave-One-Out (LOO) per dataset di piccole dimensioni
- La valutazione di metriche multiple con
cross_validate - L'uso di una
Pipelineall'interno della cross-validation per prevenire la perdita di dati - La cross-validation annidata per la regolazione imparziale degli iperparametri
Tutti gli esempi usano scikit-learn e il dataset Iris integrato, quindi puoi eseguirli immediatamente senza scaricare nulla.
Perché la Cross-Validation è Importante
Un flusso di valutazione naive divide i dati una volta sola, addestra su una parte e testa sull'altra. Il punteggio ottenuto dipende fortemente da quali campioni sono finiti in ciascuna parte — una suddivisione fortunata può far sembrare buono un modello debole; una sfortunata può far sembrare scarso un modello forte.
La cross-validation risolve questo problema ripetendo il processo train/test k volte, ogni volta usando una porzione diversa dei dati come insieme di test. Il punteggio finale è la media su tutti i fold, che è molto più stabile di una singola misurazione.
La cross-validation sfrutta al massimo i dati limitati: ogni campione viene usato sia per l'addestramento che per la valutazione nell'intero esperimento.
Consulta la pagina train/test split per la tecnica di base più semplice che la cross-validation migliora.
Cross-Validation K-Fold
K-Fold è la strategia di cross-validation più diffusa. I dati vengono divisi in k fold di uguale dimensione. In ognuna delle k iterazioni:
- Un fold viene tenuto da parte come insieme di test.
- I restanti
k - 1fold formano l'insieme di addestramento. - Il modello viene addestrato da zero e valutato sul fold di test.
Dopo k iterazioni si hanno k punteggi. La loro media è la stima delle prestazioni con cross-validation; la loro deviazione standard indica quanto siano consistenti le prestazioni su diverse sezioni di dati.
Per k = 5 e un dataset di 150 campioni, ogni fold contiene 30 campioni (20%) per il test e 120 campioni (80%) per l'addestramento.
Esempio Base di K-Fold
from sklearn.model_selection import KFold, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris
# Load the built-in Iris dataset (150 samples, 4 features, 3 classes)
iris = load_iris()
X, y = iris.data, iris.target
model = LogisticRegression(max_iter=200)
kfold = KFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(model, X, y, cv=kfold, scoring='accuracy')
print("Fold scores:", scores.round(4))
# Fold scores: [1. 1. 0.9333 0.9667 0.9667]
print("Mean accuracy: %.4f Std: %.4f" % (scores.mean(), scores.std()))
# Mean accuracy: 0.9733 Std: 0.0249La funzione cross_val_score gestisce internamente tutte le iterazioni. Parametri chiave:
| Parametro | Scopo |
|---|---|
estimator | Qualsiasi modello scikit-learn (o Pipeline) |
X, y | Matrice delle feature e vettore target |
cv | Oggetto cross-validator o intero (es. cv=5) |
scoring | Stringa della metrica — 'accuracy', 'f1_macro', 'roc_auc', ecc. |
Scegliere k
- k = 5 o k = 10 è consigliato per la maggior parte dei dataset. Questi valori offrono un buon compromesso bias-varianza nella stima.
- k più grande (es. 10) produce un bias inferiore ma una varianza maggiore nella stima, ed è più costoso da calcolare.
- k più piccolo (es. 3) è più veloce ma la stima è più sensibile al modo in cui i dati sono stati suddivisi.
- Per dataset molto piccoli (meno di ~100 campioni) considera di usare Leave-One-Out.
Ispezione Manuale dei Fold
Puoi iterare sui fold manualmente quando hai bisogno di ispezionare cosa entra in ogni suddivisione o quando vuoi eseguire una logica personalizzata per fold:
from sklearn.model_selection import KFold
from sklearn.datasets import load_iris
import numpy as np
iris = load_iris()
X, y = iris.data, iris.target
kfold = KFold(n_splits=5, shuffle=True, random_state=42)
for fold, (train_idx, test_idx) in enumerate(kfold.split(X), start=1):
X_train, X_test = X[train_idx], X[test_idx]
y_train, y_test = y[train_idx], y[test_idx]
print(f"Fold {fold}: train={len(train_idx)} samples, test={len(test_idx)} samples")Output:
Fold 1: train=120 samples, test=30 samples
Fold 2: train=120 samples, test=30 samples
Fold 3: train=120 samples, test=30 samples
Fold 4: train=120 samples, test=30 samples
Fold 5: train=120 samples, test=30 samplesCross-Validation K-Fold Stratificata
La K-Fold semplice divide i dati in base all'ordine degli indici. Con distribuzioni di classi sbilanciate, alcuni fold possono contenere pochissimi esempi di una classe minoritaria, rendendo il punteggio inaffidabile.
La K-Fold Stratificata garantisce che ogni fold contenga approssimativamente la stessa proporzione di ciascuna classe presente nell'intero dataset. Usa StratifiedKFold ogni volta che il tuo target è categorico:
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris
iris = load_iris()
X, y = iris.data, iris.target
model = LogisticRegression(max_iter=200)
skfold = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(model, X, y, cv=skfold, scoring='accuracy')
print("Fold scores:", scores.round(4))
# Fold scores: [1. 0.9667 0.9333 1. 0.9333]
print("Mean accuracy: %.4f Std: %.4f" % (scores.mean(), scores.std()))
# Mean accuracy: 0.9667 Std: 0.0298StratifiedKFold è il cross-validator predefinito usato internamente da GridSearchCV e RandomizedSearchCV per i problemi di classificazione — la stratificazione viene applicata automaticamente in questi contesti.
Cross-Validation Leave-One-Out
La cross-validation Leave-One-Out (LOO) è il caso estremo: k è uguale al numero di campioni. In ogni iterazione, un campione è l'insieme di test e tutti i restanti campioni formano l'insieme di addestramento. Per un dataset di 150 campioni ciò significa 150 cicli di addestramento/valutazione.
from sklearn.model_selection import LeaveOneOut, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris
iris = load_iris()
X, y = iris.data, iris.target
model = LogisticRegression(max_iter=200)
loocv = LeaveOneOut()
scores = cross_val_score(model, X, y, cv=loocv, scoring='accuracy')
print(f"Number of folds: {len(scores)}") # 150
print(f"Mean accuracy: {scores.mean():.4f}") # 0.9667
print(f"Std deviation: {scores.std():.4f}") # 0.1795Quando usare LOO:
- Il tuo dataset ha meno di ~100 campioni e non puoi permetterti di riservarne alcuno per il test.
- Vuoi la stima a bias minimo delle prestazioni del modello.
Svantaggi di LOO:
- Costo computazionale molto elevato — il modello viene riaddesstrato
nvolte. - Alta varianza nella stima: il punteggio di test di ogni fold è 0 o 1 (classificazione binaria) o un singolo punto, quindi la deviazione standard non è significativa per i singoli fold.
Per la maggior parte dei dataset, la K-Fold con k=5 o k=10 offre un compromesso migliore.
Valutare Più Metriche Contemporaneamente
cross_val_score può calcolare solo una metrica per chiamata. Usa cross_validate per calcolare più metriche simultaneamente e recuperare anche i punteggi di addestramento per verificare l'overfitting:
from sklearn.model_selection import KFold, cross_validate
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris
import numpy as np
iris = load_iris()
X, y = iris.data, iris.target
model = LogisticRegression(max_iter=200)
kfold = KFold(n_splits=5, shuffle=True, random_state=42)
cv_results = cross_validate(
model, X, y,
cv=kfold,
scoring=['accuracy', 'f1_macro'],
return_train_score=True,
)
print("Test accuracy: ", cv_results['test_accuracy'].round(4))
# Test accuracy: [1. 1. 0.9333 0.9667 0.9667]
print("Train accuracy:", cv_results['train_accuracy'].round(4))
# Train accuracy: [0.975 0.9583 0.9833 0.975 0.9833]
print("Test F1-macro: ", cv_results['test_f1_macro'].round(4))
# Test F1-macro: [1. 1. 0.9259 0.9691 0.971 ]Confrontare i punteggi di addestramento e di test tra i fold è un modo rapido per individuare l'overfitting: se l'accuratezza di addestramento è costantemente molto più alta di quella di test, il modello sta memorizzando i dati di addestramento. Consulta la discussione su bias e varianza per ulteriori informazioni.
Usare una Pipeline all'Interno della Cross-Validation
Un errore comune è adattare i passi di preprocessing (come la scalatura delle feature o l'imputazione) sull'intero dataset prima della cross-validation. Questo fa trapelare informazioni dal fold di test nel processo di addestramento, portando a un punteggio eccessivamente ottimistico.
Il pattern corretto è racchiudere il preprocessing e il modello insieme in una Pipeline e passare la pipeline a cross_val_score. scikit-learn riadatta l'intera pipeline — scaler incluso — in modo indipendente all'interno di ogni fold:
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.datasets import load_iris
iris = load_iris()
X, y = iris.data, iris.target
# Correct: preprocessing is fitted only on training folds
pipe = Pipeline([
('scaler', StandardScaler()),
('clf', LogisticRegression(max_iter=200)),
])
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(pipe, X, y, cv=skf, scoring='accuracy')
print("Fold scores:", scores.round(4))
# Fold scores: [1. 0.9667 0.9 1. 0.9 ]
print("Mean accuracy: %.4f Std: %.4f" % (scores.mean(), scores.std()))
# Mean accuracy: 0.9533 Std: 0.0452Usa sempre una Pipeline quando il tuo flusso di lavoro include qualsiasi passo che apprende dai dati (scalatura, PCA, codifica, imputazione).
Cross-Validation Annidata
Quando usi la cross-validation sia per regolare gli iperparametri che per valutare le prestazioni del modello sugli stessi dati, rischi di fare overfitting sui fold di validazione — gli iperparametri scelti sono quelli che hanno ottenuto il punteggio migliore su quelle particolari partizioni, quindi il punteggio riportato è ottimistico.
La cross-validation annidata separa le due preoccupazioni:
- Loop interno: seleziona gli iperparametri tramite grid search sui fold di addestramento.
- Loop esterno: valuta il modello migliore trovato dal loop interno su un fold di test tenuto da parte.
from sklearn.model_selection import GridSearchCV, StratifiedKFold, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris
iris = load_iris()
X, y = iris.data, iris.target
# Inner CV: hyperparameter selection
inner_cv = StratifiedKFold(n_splits=3, shuffle=True, random_state=2)
param_grid = {'C': [0.01, 0.1, 1, 10]}
gs = GridSearchCV(
LogisticRegression(max_iter=300),
param_grid,
cv=inner_cv,
scoring='accuracy',
)
# Outer CV: unbiased performance estimation
outer_cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=1)
nested_scores = cross_val_score(gs, X, y, cv=outer_cv, scoring='accuracy')
print("Nested CV fold scores:", nested_scores.round(4))
# Nested CV fold scores: [0.9667 1. 0.9333 1. 0.9 ]
print("Mean accuracy: %.4f" % nested_scores.mean())
# Mean accuracy: 0.9600L'approccio annidato fornisce una stima imparziale delle prestazioni del modello finale distribuito. Usalo ogni volta che stai riportando risultati in un contesto di ricerca o confrontando algoritmi. Per un flusso di lavoro di deployment semplice, in cui riaddestrerai comunque su tutti i dati disponibili, un singolo loop esterno con GridSearchCV è di solito sufficiente. Consulta la pagina grid search per una guida dettagliata sulla regolazione degli iperparametri.
Errori Comuni
Preprocessing fuori dal fold
Adattare uno scaler sull'intero dataset prima di chiamare cross_val_score — invece di farlo all'interno di una Pipeline — fa trapelare le statistiche del fold di test nell'addestramento. La soluzione è usare sempre una Pipeline.
Uso scorretto di random_state
Se imposti shuffle=True senza un random_state, ogni esecuzione produce una suddivisione diversa e i tuoi risultati non sono riproducibili. Imposta sempre random_state su un intero fisso quando riporti i risultati.
Interpretare la deviazione standard
Una deviazione standard elevata tra i fold non è sempre negativa — può riflettere una genuina variabilità nel dataset (es. alcuni fold sono più facili di altri). Esamina i punteggi dei singoli fold prima di trarre conclusioni.
Cross-validation su dati di serie temporali
K-Fold mescola i dati in modo casuale, il che miscelerebbe informazioni future nelle finestre di addestramento passate per problemi di serie temporali. Usa invece TimeSeriesSplit di scikit-learn, che rispetta l'ordine temporale.
Riferimento Rapido
| Tecnica | Quando usarla | Classe scikit-learn |
|---|---|---|
| K-Fold | Scelta predefinita per la maggior parte dei task di regressione/classificazione | KFold |
| K-Fold Stratificata | Classificazione con classi sbilanciate | StratifiedKFold |
| Leave-One-Out | Dataset molto piccoli (< ~100 campioni) | LeaveOneOut |
| CV Annidata | Riportare punteggi imparziali con la regolazione degli iperparametri | GridSearchCV dentro cross_val_score |
| CV Serie Temporali | Dati con ordinamento temporale | TimeSeriesSplit |
Argomenti Correlati
- Train/Test Split — la tecnica di base più semplice che la cross-validation migliora
- Grid Search — regolazione degli iperparametri, comunemente abbinata alla cross-validation
- Logistic Regression — uno dei classificatori usati negli esempi precedenti
- Linear Regression — controparte di regressione, anch'essa valutata con la cross-validation
- Confusion Matrix — analisi delle prestazioni per classe a complemento dei punteggi di accuratezza
- AUC-ROC Curve — un'altra metrica di valutazione che puoi passare a
scoringincross_val_score