CSC 8607 – Introduction au deep learning

Portail informatique

CI2 : Régularisation, optimisation et métriques

Le but de cette séance est d'explorer les techniques de régularisation (L1/L2), de tester des optimiseurs avancés (Adam, RMSProp, etc.) et d'implémenter des métriques de classification pertinentes. Vous apprendrez également à créer votre propre dataset personnalisé en PyTorch et à comparer vos expériences avec TensorBoard.

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.

Téléchargez le dataset cardio_train.csv et placez-le dans votre dossier TP2/data/. Complétez ensuite la classe CardioDataset ci-dessous dans un fichier dataset.py.
Note : Le prétraitement avec Pandas vous est fourni pour gagner du temps. Concentrez-vous sur l'interface PyTorch.
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}")
✍️ À mettre dans votre rapport :
  1. 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) ?
  2. 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.

Créez un script train.py. Définissez l'architecture du modèle et complétez la boucle d'entraînement avec les pénalités L1 et L2.
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.________()
✍️ À mettre dans votre rapport :
  1. 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) ?
  2. 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 ?
  3. 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.

Modifiez votre script train.py pour encapsuler l'entraînement dans une fonction train_model(optimizer_name, lr). Utilisez SummaryWriter pour logguer la perte d'entraînement.
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)
✍️ Action : Lancez le script, puis démarrez TensorBoard (tensorboard --logdir=runs).

✍️ À mettre dans votre rapport :
  1. Insérez une capture d'écran de TensorBoard superposant les courbes de perte des 4 optimiseurs.
  2. Quel optimiseur converge le plus rapidement initialement ?
  3. 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.

Complétez la fonction d'évaluation suivante en utilisant sklearn.metrics. Cette fonction doit être appelée à la toute fin de l'entraînement de votre meilleur modèle.
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}")
✍️ À mettre dans votre rapport :
  1. Rappelez la définition de la précision (Precision) et du rappel (Recall).
  2. 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.
  3. À 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.