Il Problema Fondamentale del Machine Learning
Il bias-variance tradeoff è il concetto più importante per comprendere perché un modello ML funziona o fallisce. Ogni modello ha due fonti di errore: il bias (quanto le assunzioni del modello si discostano dalla realtà) e la varianza (quanto il modello è sensibile alle fluttuazioni nei dati di training). L'obiettivo è trovare il punto di equilibrio ottimale.
L'overfitting si verifica quando il modello memorizza i dati di training incluso il rumore, ottenendo performance eccellenti sul training set ma scadenti su dati nuovi. L'underfitting si verifica quando il modello è troppo semplice per catturare i pattern reali nei dati. Riconoscere e risolvere questi problemi è una delle competenze più preziose nel ML.
Cosa Imparerai in Questo Articolo
- Bias-Variance tradeoff e come diagnosticarlo
- Segni di overfitting e underfitting
- Learning curves per la diagnosi
- Strategie di cross-validation
- L1 (Lasso) e L2 (Ridge) regularization
- Early stopping e data augmentation
Diagnosticare Overfitting e Underfitting
Il modo più diretto per diagnosticare overfitting e underfitting è confrontare le performance su training set e test set. Se il modello ha alta performance sul training ma bassa sul test, è overfitting. Se ha bassa performance su entrambi, è underfitting. Le learning curves visualizzano come le performance cambiano al variare della quantità di dati o della complessità del modello.
from sklearn.model_selection import learning_curve, validation_curve
from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import load_breast_cancer
import numpy as np
# Dataset
data = load_breast_cancer()
X, y = data.data, data.target
# Learning curve: performance vs dimensione training set
train_sizes, train_scores, val_scores = learning_curve(
DecisionTreeClassifier(random_state=42),
X, y,
train_sizes=np.linspace(0.1, 1.0, 10),
cv=5,
scoring='accuracy',
n_jobs=-1
)
print("Learning Curve (Albero Decisionale senza limiti):")
print(f"{'Train Size':<12s} {'Train Acc':<12s} {'Val Acc':<12s} {'Gap':<8s}")
for size, train, val in zip(
train_sizes,
train_scores.mean(axis=1),
val_scores.mean(axis=1)
):
gap = train - val
status = "OVERFIT" if gap > 0.05 else "OK"
print(f"{size:<12d} {train:.3f} {val:.3f} {gap:.3f} {status}")
# Validation curve: performance vs complessità' modello
param_range = range(1, 20)
train_scores_vc, val_scores_vc = validation_curve(
DecisionTreeClassifier(random_state=42),
X, y,
param_name='max_depth',
param_range=param_range,
cv=5,
scoring='accuracy',
n_jobs=-1
)
print("\nValidation Curve (max_depth):")
best_depth = 1
best_val = 0
for depth, train, val in zip(
param_range,
train_scores_vc.mean(axis=1),
val_scores_vc.mean(axis=1)
):
if val > best_val:
best_val = val
best_depth = depth
print(f" depth={depth:<3d} train={train:.3f} val={val:.3f}")
print(f"\nMiglior max_depth: {best_depth} (val accuracy: {best_val:.3f})")
Cross-Validation: Valutazione Robusta
La cross-validation è la tecnica standard per stimare in modo affidabile la performance di generalizzazione. Il K-Fold CV divide il dataset in K parti uguali: ad ogni iterazione, una parte viene usata come test e le restanti K-1 come training. Si ripete K volte e si calcola la media delle performance. Lo Stratified K-Fold mantiene la proporzione delle classi in ogni fold, essenziale per dataset sbilanciati.
from sklearn.model_selection import (
KFold, StratifiedKFold, RepeatedStratifiedKFold,
cross_val_score, cross_validate
)
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.datasets import load_breast_cancer
import numpy as np
data = load_breast_cancer()
X, y = data.data, data.target
pipeline = Pipeline([
('scaler', StandardScaler()),
('clf', RandomForestClassifier(n_estimators=100, random_state=42))
])
# Strategie di CV
strategies = {
'5-Fold': KFold(n_splits=5, shuffle=True, random_state=42),
'Stratified 5-Fold': StratifiedKFold(n_splits=5, shuffle=True, random_state=42),
'Repeated Strat 5x3': RepeatedStratifiedKFold(
n_splits=5, n_repeats=3, random_state=42
)
}
for name, cv in strategies.items():
scores = cross_val_score(pipeline, X, y, cv=cv, scoring='accuracy')
print(f"{name:<25s}: {scores.mean():.4f} (+/- {scores.std():.4f})")
# cross_validate per metriche multiple
results = cross_validate(
pipeline, X, y,
cv=StratifiedKFold(5, shuffle=True, random_state=42),
scoring=['accuracy', 'precision', 'recall', 'f1'],
return_train_score=True
)
print("\nDettaglio cross_validate:")
for metric in ['accuracy', 'precision', 'recall', 'f1']:
train = results[f'train_{metric}'].mean()
test = results[f'test_{metric}'].mean()
gap = train - test
print(f" {metric:<12s}: train={train:.3f} test={test:.3f} gap={gap:.3f}")
Regularization: L1 (Lasso) e L2 (Ridge)
La regularization aggiunge un termine di penalità alla cost function per scoraggiare modelli troppo complessi. L2 (Ridge) aggiunge la somma dei quadrati dei pesi: riduce tutti i pesi verso zero ma non li azzera mai. L1 (Lasso) aggiunge la somma dei valori assoluti dei pesi: può azzerare completamente alcuni pesi, effettuando implicitamente feature selection. Elastic Net combina L1 e L2, controllando il mix con il parametro l1_ratio.
Il parametro alpha (o C=1/alpha in LogisticRegression) controlla la forza della regularization: un alpha alto penalizza di più (modello più semplice, rischio underfitting), un alpha basso penalizza meno (modello più complesso, rischio overfitting).
from sklearn.linear_model import Ridge, Lasso, ElasticNet, LogisticRegression
from sklearn.model_selection import cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.datasets import load_breast_cancer
import numpy as np
data = load_breast_cancer()
X, y = data.data, data.target
# Confronto regularization per classificazione
regularizations = {
'No Reg (C=1e6)': LogisticRegression(C=1e6, max_iter=10000, random_state=42),
'L2 Weak (C=10)': LogisticRegression(C=10, penalty='l2', max_iter=10000, random_state=42),
'L2 Strong (C=0.01)': LogisticRegression(C=0.01, penalty='l2', max_iter=10000, random_state=42),
'L1 (C=1)': LogisticRegression(C=1, penalty='l1', solver='saga', max_iter=10000, random_state=42),
'ElasticNet': LogisticRegression(C=1, penalty='elasticnet', solver='saga',
l1_ratio=0.5, max_iter=10000, random_state=42)
}
print("Confronto Regularization:")
for name, model in regularizations.items():
pipeline = Pipeline([('scaler', StandardScaler()), ('clf', model)])
scores = cross_val_score(pipeline, X, y, cv=5, scoring='accuracy')
# Conta coefficienti non-zero (dopo fit)
pipeline.fit(X, y)
n_nonzero = np.sum(np.abs(pipeline.named_steps['clf'].coef_) > 1e-5)
print(f" {name:<22s}: acc={scores.mean():.3f} features_attive={n_nonzero}/{X.shape[1]}")
Early Stopping e Data Augmentation
L'early stopping è una tecnica di regularization per modelli addestrati iterativamente (gradient boosting, neural networks): si monitora la performance sul validation set ad ogni epoca e si ferma il training quando la performance smette di migliorare. Previene l'overfitting senza dover scegliere manualmente il numero di iterazioni.
La data augmentation è la strategia più efficace contro l'overfitting quando i dati sono pochi: generare nuovi campioni di training attraverso trasformazioni che preservano l'etichetta. Per le immagini: rotazioni, flip, crop, variazioni di colore. Per il testo: sinonimi, back-translation. Per i dati tabulari: SMOTE per dati sbilanciati o aggiunta di rumore gaussiano.
Regola pratica: Se il gap tra training accuracy e validation accuracy è superiore al 5%, il modello sta probabilmente overfittando. Se la validation accuracy è inferiore al 70% per un problema non troppo difficile, il modello sta probabilmente underfittando. Le learning curves sono lo strumento diagnostico più informativo.
Punti Chiave
- Bias alto = underfitting (modello troppo semplice); Varianza alta = overfitting (troppo complesso)
- Learning curves visualizzano il gap tra training e validation performance
- Cross-validation (Stratified K-Fold) è il gold standard per la valutazione
- L2 (Ridge) riduce tutti i pesi; L1 (Lasso) azzera alcuni pesi (feature selection implicita)
- Early stopping ferma il training quando il validation score smette di migliorare
- Più dati = meno overfitting: la data augmentation aiuta quando il dataset è piccolo







