CI2 : Régularisation, optimisation et métriques
Modalités de rendu : L'évaluation se fera sur la base de votre rapport au format Markdown (rapport.md). Dans le même dépôt Git que le TP1, créez un répertoire TP2, et placez-y votre rapport ainsi que vos scripts Python. Mettez à jour votre dépôt au fur et à mesure de votre progression.
Création d'un dataset personnalisé (∼30mn, – moyen)
Dans la pratique, vos données ne viendront pas toujours de torchvision.datasets. Vous devez savoir intégrer vos propres données. Nous allons utiliser le Cardiovascular Disease dataset disponible sur Kaggle.
Note : Le prétraitement avec Pandas vous est fourni pour gagner du temps. Concentrez-vous sur l'interface PyTorch.
✍️ À mettre dans votre rapport :import torch import pandas as pd import numpy as np from torch.utils.data import Dataset, DataLoader, random_split from sklearn.preprocessing import StandardScaler class CardioDataset(Dataset): def __init__(self, csv_path): # 1. Chargement et nettoyage df = pd.read_csv(csv_path, sep=";") df = df.drop_duplicates().drop("id", axis=1) # 2. One-hot encoding des variables catégorielles categorical_features = ["gender", "cholesterol", "gluc"] for feature in categorical_features: one_hot = pd.get_dummies(df[feature], dtype=np.float32) one_hot.columns = [f"{feature}_{c}" for c in one_hot.columns] df = df.drop(feature, axis=1) df = pd.concat([df, one_hot], axis=1) features_names = list(df.columns) features_names.remove("cardio") # 3. Normalisation (StandardScaler) self.features = df[features_names] scaler = StandardScaler() self.features = scaler.fit_transform(self.features).astype(np.float32) # 4. Labels self.labels = df["cardio"].to_numpy().astype(np.float32).reshape(-1, 1) def __len__(self): # Retourne la taille du dataset return len(________) def __getitem__(self, idx): if torch.is_tensor(idx): idx = idx.tolist() # Récupérer les features et le label à l'index 'idx' local_features = ________[idx] local_labels = ________[idx] return {"features": local_features, "labels": local_labels} # Test du Dataset et création des DataLoaders if __name__ == '__main__': dataset = CardioDataset("data/cardio_train.csv") print(f"Taille totale du dataset : {len(dataset)}") # Découpage 80% train, 10% val, 10% test generator = torch.Generator().manual_seed(42) train_set, val_set, test_set = random_split(dataset, [0.8, 0.1, 0.1], generator=generator) # Création des DataLoaders train_loader = DataLoader(train_set, batch_size=64, shuffle=________) val_loader = DataLoader(val_set, batch_size=64, shuffle=________) test_loader = DataLoader(test_set, batch_size=64, shuffle=False) # Test de lecture batch = next(iter(train_loader)) print(f"Shape features: {batch['features'].shape}, Shape labels: {batch['labels'].shape}")
- Dans le code fourni, nous utilisons StandardScaler() sur l'ensemble du dataset (avant le split). Pourquoi est-ce une mauvaise pratique en apprentissage automatique (concept de data leakage) ?
- Si le dataset était trop volumineux pour tenir dans la RAM (ex: 500 Go de données tabulaires), quelle classe PyTorch utiliseriez-vous à la place de Dataset ?
MLP et Régularisation L1 / L2 (∼40mn, – moyen)
Nous allons créer un modèle de classification binaire et implémenter la régularisation manuellement pour bien en comprendre la mécanique mathématique.
✍️ À mettre dans votre rapport :import torch.nn as nn import torch.optim as optim # (Importez votre CardioDataset et vos loaders ici) class MLP(nn.Module): def __init__(self, input_size, hidden_size): super().__init__() self.net = nn.Sequential( nn.Linear(input_size, hidden_size), nn.ReLU(), nn.Linear(hidden_size, hidden_size), nn.ReLU(), nn.Linear(hidden_size, 1), nn.Sigmoid() # Sortie binaire [0, 1] ) def forward(self, x): return self.net(x) # Initialisation device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = MLP(input_size=batch['features'].shape[1], hidden_size=128).to(device) criterion = nn.BCELoss() optimizer = optim.SGD(model.parameters(), lr=0.01) l1_lambda = 1e-4 l2_lambda = 1e-3 for epoch in range(10): model.train() for batch in train_loader: inputs, targets = batch["features"].to(device), batch["labels"].to(device) optimizer.________() outputs = model(inputs) base_loss = criterion(outputs, targets) # Calcul de la pénalité L1 (somme des valeurs absolues des poids) l1_penalty = sum(p.abs().sum() for p in model.parameters()) # Calcul de la pénalité L2 (somme des carrés des poids) l2_penalty = sum(________ for p in model.parameters()) # Loss totale loss = base_loss + l1_lambda * l1_penalty + l2_lambda * l2_penalty loss.________() optimizer.________()
- Réglez l1_lambda = 0.1 et l2_lambda = 0. Qu'observez-vous sur l'apprentissage (loss et précision) ? Pourquoi une régularisation trop forte produit-elle cet effet (comment appelle-t-on ce phénomène) ?
- Dans PyTorch, il n'est généralement pas nécessaire d'ajouter la pénalité L2 à la main dans la boucle. Quel argument de l'optimiseur (ex: optim.SGD) permet d'appliquer cette régularisation L2 automatiquement ?
- Quelle est la différence conceptuelle sur les poids du réseau entre une régularisation L1 et L2 ?
Comparaison des Optimiseurs et TensorBoard (∼40mn, – moyen)
Nous allons comparer l'efficacité de plusieurs optimiseurs de la littérature sur notre dataset et suivre leurs courbes avec TensorBoard.
✍️ Action : Lancez le script, puis démarrez TensorBoard (tensorboard --logdir=runs).from torch.utils.tensorboard import SummaryWriter def train_model(opt_name, learning_rate=0.001, epochs=30): model = MLP(input_size=12, hidden_size=128).to(device) criterion = nn.BCELoss() # Choix de l'optimiseur if opt_name == "SGD": optimizer = optim.SGD(model.parameters(), lr=learning_rate) elif opt_name == "Momentum": optimizer = optim.SGD(model.parameters(), lr=learning_rate, momentum=0.9) elif opt_name == "RMSprop": optimizer = optim.________(model.parameters(), lr=learning_rate) elif opt_name == "Adam": optimizer = optim.________(model.parameters(), lr=learning_rate) writer = SummaryWriter(f'runs/cardio_{opt_name}_lr{learning_rate}') for epoch in range(epochs): model.train() running_loss = 0.0 for batch in train_loader: inputs, targets = batch["features"].to(device), batch["labels"].to(device) optimizer.zero_grad() loss = criterion(model(inputs), targets) loss.backward() optimizer.step() running_loss += loss.item() # Logging de la loss moyenne par epoch writer.add_scalar('Training Loss', running_loss / len(train_loader), epoch) writer.close() # Lancement des expériences for opt in ["SGD", "Momentum", "RMSprop", "Adam"]: train_model(opt, learning_rate=0.001)
✍️ À mettre dans votre rapport :
- Insérez une capture d'écran de TensorBoard superposant les courbes de perte des 4 optimiseurs.
- Quel optimiseur converge le plus rapidement initialement ?
- Comparez la courbe de SGD simple et Momentum. Quel est l'effet de l'ajout du moment sur la descente de gradient ?
Analyse des métriques (Précision, Rappel, F1, AUC) (∼30mn, – facile)
La précision (accuracy) n'est pas toujours la bonne métrique, surtout en médecine. Nous allons calculer des métriques plus robustes sur l'ensemble de test.
✍️ À mettre dans votre rapport :from sklearn.metrics import precision_score, recall_score, f1_score, roc_auc_score def evaluate_model(model, test_loader): model.eval() all_targets = [] all_preds_probs = [] with torch.________(): # Désactiver le calcul des gradients for batch in test_loader: inputs, targets = batch["features"].to(device), batch["labels"].to(device) outputs = model(inputs) # Stocker les probabilités et les cibles pour sklearn all_targets.extend(targets.cpu().numpy()) all_preds_probs.extend(outputs.cpu().numpy()) # Conversion en numpy array all_targets = np.array(all_targets) all_preds_probs = np.array(all_preds_probs) # Prédictions binaires (seuil à 0.5) all_preds_classes = (all_preds_probs > 0.5).astype(int) # Calcul des métriques precision = precision_score(________, ________) recall = recall_score(________, ________) f1 = f1_score(all_targets, all_preds_classes) auc = roc_auc_score(all_targets, all_preds_probs) print(f"Precision: {precision:.4f} | Recall: {recall:.4f} | F1: {f1:.4f} | AUC: {auc:.4f}")
- Rappelez la définition de la précision (Precision) et du rappel (Recall).
- Dans le contexte médical (détecter une maladie cardiovasculaire), vaut-il mieux privilégier un modèle avec une forte Précision ou un modèle avec un fort Rappel ? Justifiez votre réponse.
- À quoi sert l'aire sous la courbe ROC (AUC) par rapport aux autres métriques calculées à un seuil fixe de 0.5 ?
Ce TP vous a permis de manipuler des données tabulaires "réelles", de comprendre l'impact crucial de la régularisation pour éviter le surapprentissage, et d'observer comment les optimiseurs modernes accélèrent la convergence.
N'oubliez pas de commiter vos dernières modifications (rapport et code) dans votre dossier TP2, et de faire un push sur votre dépôt Git.