Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Un modèle linéaire ne peut pas ajuster tous les jeux de données. Nous introduisons la non-linéarité en empilant des couches cachées de neurones entre l’entrée et la sortie.

Multi Layer Perceptron

Un MLP (perceptron multicouche) à une couche cachée entièrement connectée : 4 entrées, 5 unités cachées, 3 sorties.

Une couche entièrement connectée ou dense connecte chaque neurone à tous les neurones de la couche précédente. Sa sortie est

h(x)=ϕ(Wx+b)h(\mathbf{x}) = \phi (\mathbf{W} \mathbf{x} + \mathbf{b}),

ϕ est la fonction d’activation, W\mathbf{W} la matrice de poids, b\mathbf{b} le vecteur de biais et x\mathbf{x} le vecteur d’entrée. Entraîner un MLP, c’est trouver les poids et les biais qui minimisent une fonction de perte. Avec assez d’unités cachées, un MLP peut approximer une large classe de fonctions : c’est pourquoi on qualifie les MLP d’approximateurs universels.

Dans ce carnet, nous construisons un MLP multi-classes en PyTorch sur le jeu de données de sources sismiques du carnet 4.1, ajoutons le dropout (extinction aléatoire de neurones) et la normalisation par lots (batch normalization), apprenons à sauvegarder et restaurer les modèles, et comparons avec le MLP intégré de scikit-learn.

🖥️ Diapositives du cours — Séance 21 (mer. 18 nov.)

1. Fonctions d’activation

Les fonctions d’activation appliquent une transformation non linéaire à la somme pondérée des entrées d’un neurone. Sans elles, une pile de couches linéaires se réduit à une seule couche linéaire. Trois choix courants :

  • Unité linéaire rectifiée (ReLU) : ReLU(x)=max(x,0)\mathrm{ReLU}(x)=\max(x,0). Sa dérivée vaut 0 ou 1, ce qui facilite l’optimisation. ReLU est le choix par défaut pour les couches cachées.
  • Sigmoïde : σ(x)=11+exp(x)\sigma(x) = \frac{1}{1+\exp(-x)} envoie tout nombre réel dans (0, 1). C’est une approximation lisse, « en S », d’une unité à seuil. Elle sert encore sur les unités de sortie quand la sortie doit être une probabilité, en classification binaire.
  • Tanh : tanh(x)=2σ(2x)1\tanh(x) = 2 \sigma(2x) -1 envoie dans (-1, 1) et est presque linéaire près de zéro. Son comportement se situe entre la sigmoïde et ReLU.
import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-2, 2, 100)

def sigm(x):   # sigmoid
    return 1 / (1 + np.exp(-x))

def relu(x):   # rectified linear unit
    return np.maximum(x, 0)

fig, ax = plt.subplots(figsize=(5, 3.5))
ax.plot(x, sigm(x), label='sigmoid')
ax.plot(x, relu(x), label='ReLU')
ax.plot(x, np.tanh(x), label='tanh')
ax.grid(True)
ax.legend()
ax.set_xlim([-2, 2])
ax.set_xlabel('x')
ax.set_ylabel(r'$\phi(x)$')
ax.set_title('Activation functions')
plt.tight_layout()
<Figure size 500x350 with 1 Axes>

2. Structures typiques de MLP

Un MLP de régression produit des valeurs scalaires. Le nombre de neurones de sortie égale le nombre de valeurs prédites. Pour prédire des coordonnées géospatiales 2D, par exemple, il faut 2 neurones de sortie : latitude et longitude. Pour des sorties réelles non bornées, la couche de sortie n’a pas d’activation. Pour contraindre les sorties, ajoutez une activation sur la couche de sortie : ReLU ou softplus pour des valeurs strictement positives, sigmoïde ou tanh pour des valeurs bornées entre 0 (ou -1) et 1.

Regression MLP

Un MLP de classification produit un score par classe. Pour les problèmes binaires, un seul neurone de sortie avec une sigmoïde donne la probabilité de la classe positive. Pour les problèmes multi-classes, la couche de sortie a un neurone par classe et un softmax convertit les scores en probabilités dont la somme vaut 1. En PyTorch, le modèle sort généralement les scores bruts (appelés logits) et la fonction de perte nn.CrossEntropyLoss applique le softmax en interne.

Classification MLP

3. Entraîner les réseaux de neurones

L’entraînement procède par petits lots de données appelés mini-lots (minibatches).

La passe avant (forward pass) envoie un mini-lot de la couche d’entrée, à travers les couches cachées, jusqu’à la couche de sortie, en calculant et en stockant les valeurs intermédiaires au passage, et produit des prédictions.

L’algorithme mesure ensuite l’erreur avec une fonction de perte.

La rétropropagation applique la règle de dérivation en chaîne à rebours dans le réseau, pour calculer le gradient de la perte par rapport à chaque poids et chaque biais. Comme la règle de dérivation en chaîne a besoin des valeurs intermédiaires de la passe avant, l’entraînement demande plus de mémoire que la prédiction.

Enfin, l’optimiseur (la descente de gradient ou une variante comme Adam) met à jour les poids à l’aide de ces gradients. Un passage sur l’ensemble d’entraînement complet s’appelle une époque. Répéter cette boucle pendant de nombreuses époques réduit progressivement la perte.

Un modèle séquentiel est un MLP à une seule branche : les données traversent les couches l’une après l’autre.

4. Un MLP multi-classes en PyTorch

Nous réutilisons le jeu de données de sources sismiques du carnet 4.1 : 62 caractéristiques physiques extraites de sismogrammes de quatre types de sources (séismes, explosions, bruit, événements de surface), 1 000 événements par classe, depuis une archive Zenodo. Comme en 4.1, une colonne de caractéristiques entièrement NaN est supprimée au nettoyage, ce qui en laisse 61. En 4.1, nous avons entraîné un MLP peu profond. Ici, nous en construisons un plus profond, à trois couches cachées, avec dropout et normalisation par lots.

import os
import pandas as pd
import pooch
import torch
import torch.nn as nn
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score, classification_report

# device-agnostic setup: GPU if available, else Apple silicon, else CPU
device = torch.device("cuda" if torch.cuda.is_available()
                      else "mps" if torch.backends.mps.is_available()
                      else "cpu")
print(f"Using device: {device}")

torch.manual_seed(42)
np.random.seed(42)
Using device: mps

4.1 Charger et nettoyer les données

Nous téléchargeons les quatre fichiers CSV (un par type de source) avec pooch, les concaténons, et créons une étiquette entière à partir du nom de la classe. Nous supprimons les colonnes d’intendance, supprimons une colonne de caractéristiques entièrement NaN, et retirons les éventuelles lignes restantes contenant des NaN ou des valeurs infinies.

base = "https://zenodo.org/api/records/14025693/files"
class_files = {"earthquake": "1000_earthquakes_physical_features.csv",
               "explosion": "1000_explosion_physical_features.csv",
               "noise": "1000_noise_physical_features.csv",
               "surface event": "1000_surface_physical_features.csv"}

frames = []
for class_name, fname in class_files.items():
    path = pooch.retrieve(f"{base}/{fname}/content", known_hash=None, fname=fname)
    d = pd.read_csv(path)
    d["label"] = class_name
    frames.append(d)
df = pd.concat(frames, ignore_index=True)

# drop bookkeeping columns
df = df.drop(columns=["Unnamed: 0", "serial_no", "source"])

# clean: replace inf with NaN, drop all-NaN columns, then drop rows with NaN
df = df.replace([np.inf, -np.inf], np.nan)
df = df.dropna(axis=1, how="all").dropna()

class_names = list(class_files.keys())
y = df["label"].map({name: i for i, name in enumerate(class_names)}).to_numpy()
X = df.drop(columns=["label"]).to_numpy(dtype=np.float32)
print(f"{X.shape[0]} samples, {X.shape[1]} features, {len(class_names)} classes")
4000 samples, 61 features, 4 classes

4.2 Découper et normaliser

Nous mettons de côté 20 % des données pour le test final, et découpons le reste en ensembles d’entraînement et de validation. L’ensemble de validation surveille le surapprentissage pendant l’entraînement ; l’ensemble de test n’est touché qu’une seule fois, à la fin. Nous standardisons les caractéristiques avec un StandardScaler ajusté sur l’ensemble d’entraînement seulement, pour qu’aucune information des ensembles de validation ou de test ne fuie dans le prétraitement.

X_trainval, X_test, y_trainval, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y)
X_train, X_val, y_train, y_val = train_test_split(
    X_trainval, y_trainval, test_size=0.2, random_state=42, stratify=y_trainval)

scaler = StandardScaler().fit(X_train)   # fit on train only
X_train = scaler.transform(X_train).astype(np.float32)
X_val = scaler.transform(X_val).astype(np.float32)
X_test = scaler.transform(X_test).astype(np.float32)
print(f"train: {len(X_train)}, validation: {len(X_val)}, test: {len(X_test)}")
train: 2560, validation: 640, test: 800

4.3 Datasets et DataLoaders

PyTorch alimente le modèle via un Dataset (un stockage indexable des échantillons) et un DataLoader (constitution des lots et mélange). Nos tableaux sont déjà prêts à devenir des tenseurs : TensorDataset suffit. Nous mélangeons les lots d’entraînement à chaque époque ; les chargeurs de validation et de test n’ont pas besoin de mélange.

from torch.utils.data import TensorDataset, DataLoader

batch_size = 64
train_ds = TensorDataset(torch.from_numpy(X_train), torch.from_numpy(y_train))
val_ds = TensorDataset(torch.from_numpy(X_val), torch.from_numpy(y_val))
test_ds = TensorDataset(torch.from_numpy(X_test), torch.from_numpy(y_test))

train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(val_ds, batch_size=batch_size)
test_loader = DataLoader(test_ds, batch_size=batch_size)

4.4 Un MLP plus profond, avec dropout et normalisation par lots

Le modèle a trois couches cachées (128, 64 et 32 unités) au lieu de la couche cachée unique de 4.1. Deux nouveaux types de couches apparaissent :

Le dropout (nn.Dropout(p)) met aléatoirement à zéro une fraction p des activations à chaque pas d’entraînement et remet à l’échelle les survivantes par 1/(1p)1/(1-p). Comme un neurone ne peut compter sur la présence d’aucun autre neurone en particulier, le réseau apprend des caractéristiques plus redondantes et plus générales, ce qui réduit le surapprentissage. Le dropout n’est actif qu’en mode entraînement et il est désactivé à l’évaluation.

La normalisation par lots (nn.BatchNorm1d) normalise les activations de chaque unité à moyenne nulle et variance unité sur le mini-lot courant, puis applique une échelle et un décalage appris. Cela maintient les activations dans une plage stable à mesure que les poids changent, ce qui stabilise et accélère l’entraînement. À l’évaluation, elle utilise des estimations glissantes de la moyenne et de la variance collectées pendant l’entraînement.

Ces deux couches se comportent différemment à l’entraînement et à l’évaluation. Appelez model.train() avant l’entraînement et model.eval() avant la prédiction pour changer de mode. La couche de sortie renvoie des logits bruts ; nn.CrossEntropyLoss se chargera du softmax.

class SourceMLP(nn.Module):
    def __init__(self, n_features, n_classes, p_drop=0.3):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(n_features, 128),
            nn.BatchNorm1d(128),
            nn.ReLU(),
            nn.Dropout(p_drop),
            nn.Linear(128, 64),
            nn.BatchNorm1d(64),
            nn.ReLU(),
            nn.Dropout(p_drop),
            nn.Linear(64, 32),
            nn.BatchNorm1d(32),
            nn.ReLU(),
            nn.Linear(32, n_classes),   # logits, no activation
        )

    def forward(self, x):
        return self.net(x)

n_features, n_classes = X_train.shape[1], len(class_names)
model = SourceMLP(n_features, n_classes).to(device)
print(model)
SourceMLP(
  (net): Sequential(
    (0): Linear(in_features=61, out_features=128, bias=True)
    (1): BatchNorm1d(128, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
    (2): ReLU()
    (3): Dropout(p=0.3, inplace=False)
    (4): Linear(in_features=128, out_features=64, bias=True)
    (5): BatchNorm1d(64, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
    (6): ReLU()
    (7): Dropout(p=0.3, inplace=False)
    (8): Linear(in_features=64, out_features=32, bias=True)
    (9): BatchNorm1d(32, eps=1e-05, momentum=0.1, affine=True, bias=True, track_running_stats=True)
    (10): ReLU()
    (11): Linear(in_features=32, out_features=4, bias=True)
  )
)

4.5 Entraînement par mini-lots avec Adam

La boucle d’entraînement ci-dessous suit la recette de la section 3 : passe avant, perte, rétropropagation, mise à jour des poids, un mini-lot à la fois. Nous utilisons l’optimiseur Adam, une variante de la descente de gradient à taux d’apprentissage adaptatif par paramètre (voir la liste des optimiseurs sur https://pytorch.org/docs/stable/optim.html). Après chaque époque, nous évaluons la perte et l’exactitude sur l’ensemble de validation.

La boucle sauvegarde aussi un checkpoint (point de sauvegarde) chaque fois que la perte de validation s’améliore. Nous revenons aux checkpoints à la section 5 ; pour l’instant, notez l’appel à torch.save. Les checkpoints vont dans un répertoire local checkpoints/.

Nous entraînons pendant 40 époques, ce qui suffit pour ce petit modèle. Vous pouvez augmenter le nombre d’époques sur votre propre machine.

def evaluate(model, loader, criterion):
    """Average loss and accuracy of the model over a DataLoader."""
    model.eval()   # evaluation mode: dropout off, batchnorm uses running stats
    total_loss, correct, n = 0.0, 0, 0
    with torch.no_grad():   # no gradients needed for evaluation
        for xb, yb in loader:
            xb, yb = xb.to(device), yb.to(device)
            logits = model(xb)
            total_loss += criterion(logits, yb).item() * len(yb)
            correct += (logits.argmax(dim=1) == yb).sum().item()
            n += len(yb)
    return total_loss / n, correct / n


def train(model, train_loader, val_loader, n_epochs=40, lr=1e-3,
          ckpt_path="checkpoints/best_mlp.pt"):
    os.makedirs("checkpoints", exist_ok=True)
    criterion = nn.CrossEntropyLoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=lr)
    history = {"train_loss": [], "val_loss": [], "train_acc": [], "val_acc": []}
    best_val_loss = float("inf")

    for epoch in range(n_epochs):
        model.train()   # training mode: dropout and batchnorm active
        running_loss, correct, n = 0.0, 0, 0
        for xb, yb in train_loader:
            xb, yb = xb.to(device), yb.to(device)
            optimizer.zero_grad()             # reset gradients
            logits = model(xb)                # forward pass
            loss = criterion(logits, yb)      # loss
            loss.backward()                   # backpropagation
            optimizer.step()                  # weight update
            running_loss += loss.item() * len(yb)
            correct += (logits.argmax(dim=1) == yb).sum().item()
            n += len(yb)

        history["train_loss"].append(running_loss / n)
        history["train_acc"].append(correct / n)
        val_loss, val_acc = evaluate(model, val_loader, criterion)
        history["val_loss"].append(val_loss)
        history["val_acc"].append(val_acc)

        # checkpoint: save the model each time the validation loss improves
        if val_loss < best_val_loss:
            best_val_loss = val_loss
            torch.save({"epoch": epoch + 1,
                        "state_dict": model.state_dict(),
                        "optimizer": optimizer.state_dict(),
                        "val_loss": val_loss}, ckpt_path)

        if (epoch + 1) % 5 == 0 or epoch == 0:
            print(f"[Epoch {epoch + 1:2d}] train loss: {history['train_loss'][-1]:.3f} "
                  f"- val loss: {val_loss:.3f} - val accuracy: {val_acc:.3f}")
    return history
history = train(model, train_loader, val_loader, n_epochs=40)
[Epoch  1] train loss: 1.042 - val loss: 0.724 - val accuracy: 0.809
[Epoch  5] train loss: 0.452 - val loss: 0.396 - val accuracy: 0.878
[Epoch 10] train loss: 0.353 - val loss: 0.346 - val accuracy: 0.895
[Epoch 15] train loss: 0.302 - val loss: 0.340 - val accuracy: 0.891
[Epoch 20] train loss: 0.273 - val loss: 0.334 - val accuracy: 0.886
[Epoch 25] train loss: 0.276 - val loss: 0.330 - val accuracy: 0.903
[Epoch 30] train loss: 0.246 - val loss: 0.326 - val accuracy: 0.891
[Epoch 35] train loss: 0.235 - val loss: 0.324 - val accuracy: 0.902
[Epoch 40] train loss: 0.217 - val loss: 0.327 - val accuracy: 0.908

4.6 Courbes d’apprentissage

Nous traçons les pertes d’entraînement et de validation côte à côte avec les exactitudes. L’écart entre les deux courbes est une lecture directe du surapprentissage : une perte d’entraînement qui continue de baisser pendant que la perte de validation stagne ou remonte signifie que le modèle mémorise l’ensemble d’entraînement.

epochs = np.arange(1, len(history["train_loss"]) + 1)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9, 3.5))

ax1.plot(epochs, history["train_loss"], label="train")
ax1.plot(epochs, history["val_loss"], label="validation")
ax1.set_xlabel("Epoch")
ax1.set_ylabel("Cross-entropy loss")
ax1.set_title("Loss")
ax1.legend()
ax1.grid(True)

ax2.plot(epochs, history["train_acc"], label="train")
ax2.plot(epochs, history["val_acc"], label="validation")
ax2.set_xlabel("Epoch")
ax2.set_ylabel("Accuracy")
ax2.set_title("Accuracy")
ax2.legend()
ax2.grid(True)
plt.tight_layout()
<Figure size 900x350 with 2 Axes>

4.7 Performance en test

Nous évaluons maintenant, une seule fois, sur l’ensemble de test mis de côté : l’exactitude globale et, par classe, la précision, le rappel et le score F1.

def predict(model, loader):
    model.eval()
    preds = []
    with torch.no_grad():
        for xb, _ in loader:
            logits = model(xb.to(device))
            preds.append(logits.argmax(dim=1).cpu().numpy())
    return np.concatenate(preds)

y_pred = predict(model, test_loader)
print(f"Test accuracy: {accuracy_score(y_test, y_pred):.3f}\n")
print(classification_report(y_test, y_pred, target_names=class_names))
Test accuracy: 0.890

               precision    recall  f1-score   support

   earthquake       0.90      0.85      0.88       200
    explosion       0.86      0.81      0.83       200
        noise       0.90      0.96      0.93       200
surface event       0.90      0.94      0.92       200

     accuracy                           0.89       800
    macro avg       0.89      0.89      0.89       800
 weighted avg       0.89      0.89      0.89       800

Exercice : reconstruisez le modèle avec p_drop=0.0 et réentraînez-le. Comment l’écart entre les courbes d’entraînement et de validation change-t-il ?

5. Sauvegarder et restaurer un modèle

5.1 Sauvegarder les poids avec state_dict

Les paramètres apprenables d’un modèle PyTorch résident dans son state_dict, un dictionnaire qui associe à chaque couche ses tenseurs de poids et de biais. La façon standard de sauvegarder un modèle entraîné est de sauvegarder ce dictionnaire, et non l’objet modèle lui-même :

  1. torch.save(model.state_dict(), path) écrit les poids sur le disque.
  2. Pour restaurer, construisez une nouvelle instance de la même classe de modèle et appelez load_state_dict.
  3. Appelez model.eval() avant la prédiction, pour que le dropout soit désactivé et que la normalisation par lots utilise ses statistiques glissantes.

Charger avec map_location=device rend le fichier portable d’une machine à l’autre : des poids sauvegardés sur GPU se chargent sans problème sur une machine dépourvue de GPU (CPU seulement).

os.makedirs("checkpoints", exist_ok=True)
torch.save(model.state_dict(), "checkpoints/mlp_weights.pt")

# restore into a fresh model instance
restored = SourceMLP(n_features, n_classes).to(device)
restored.load_state_dict(torch.load("checkpoints/mlp_weights.pt", map_location=device))
restored.eval()

# check that the restored model gives the same predictions
y_pred_restored = predict(restored, test_loader)
print(f"Restored predictions identical: {np.array_equal(y_pred, y_pred_restored)}")
Restored predictions identical: True

5.2 Poser des checkpoints pendant l’entraînement

Ne sauvegarder que les poids finaux a un défaut : la dernière époque n’est pas toujours la meilleure. Si le modèle commence à surapprendre, la perte de validation remonte pendant que l’entraînement continue, et les poids finaux sont pires qu’une version antérieure. Les checkpoints corrigent cela : pendant l’entraînement, nous avons sauvegardé le modèle chaque fois que la perte de validation s’améliorait (section 4.5), avec le numéro d’époque et l’état de l’optimiseur. L’état de l’optimiseur vous permet de reprendre l’entraînement exactement là où le checkpoint a été pris.

Nous restaurons maintenant le meilleur checkpoint et l’évaluons sur l’ensemble de test.

ckpt = torch.load("checkpoints/best_mlp.pt", map_location=device)
print(f"Best checkpoint: epoch {ckpt['epoch']}, validation loss {ckpt['val_loss']:.3f}")

best_model = SourceMLP(n_features, n_classes).to(device)
best_model.load_state_dict(ckpt["state_dict"])
best_model.eval()

y_pred_best = predict(best_model, test_loader)
print(f"Test accuracy (final model): {accuracy_score(y_test, y_pred):.3f}")
print(f"Test accuracy (best checkpoint): {accuracy_score(y_test, y_pred_best):.3f}\n")
print(classification_report(y_test, y_pred_best, target_names=class_names))
Best checkpoint: epoch 19, validation loss 0.316
Test accuracy (final model): 0.890
Test accuracy (best checkpoint): 0.886

               precision    recall  f1-score   support

   earthquake       0.89      0.85      0.87       200
    explosion       0.86      0.82      0.84       200
        noise       0.90      0.93      0.92       200
surface event       0.89      0.94      0.91       200

     accuracy                           0.89       800
    macro avg       0.89      0.89      0.89       800
 weighted avg       0.89      0.89      0.89       800

6. Le MLP dans scikit-learn

Scikit-learn embarque son propre MLP, MLPClassifier (et MLPRegressor). C’est la même famille de modèles : couches entièrement connectées, activations ReLU, entraînement par mini-lots avec Adam. Le compromis porte sur le contrôle. Vous renoncez à la boucle d’entraînement sur mesure, au dropout, à la normalisation par lots, aux checkpoints et à l’exécution sur GPU ; en échange, tout le flux de travail tient en quelques lignes conformes à l’interface familière fit/predict. Pour de petits problèmes tabulaires, c’est souvent tout ce qu’il faut.

Nous le démontrons sur les caractéristiques synthétiques du détecteur d’événements de mlgeo_synth : 4 caractéristiques dérivées de la forme d’onde (STA/LTA, kurtosis, centroïde spectral, fréquence dominante) et une étiquette binaire équilibrée (événement contre bruit).

from mlgeo_synth import detector_features
from sklearn.neural_network import MLPClassifier
from sklearn.pipeline import make_pipeline

det = detector_features(n=2000, event_fraction=0.5, seed=0)
X_det = det.drop(columns=["label"]).to_numpy()
y_det = det["label"].to_numpy()
Xd_train, Xd_test, yd_train, yd_test = train_test_split(
    X_det, y_det, test_size=0.2, random_state=42, stratify=y_det)

clf = make_pipeline(
    StandardScaler(),
    MLPClassifier(hidden_layer_sizes=(32, 16), max_iter=500, random_state=42))
clf.fit(Xd_train, yd_train)
print(f"Train accuracy: {clf.score(Xd_train, yd_train):.3f}")
print(f"Test accuracy:  {clf.score(Xd_test, yd_test):.3f}")
print(classification_report(yd_test, clf.predict(Xd_test),
                            target_names=["noise", "event"]))
Train accuracy: 0.991
Test accuracy:  0.985
              precision    recall  f1-score   support

       noise       0.97      1.00      0.99       200
       event       1.00      0.97      0.98       200

    accuracy                           0.98       400
   macro avg       0.99      0.98      0.98       400
weighted avg       0.99      0.98      0.98       400

Deux lignes de code de modèle, pas de boucle d’entraînement, et le pipeline gère la normalisation. Mais les choix d’architecture s’arrêtent aux tailles de couches, à l’activation et à une poignée de réglages du solveur. Quand il vous faut du dropout, de la normalisation par lots, des pertes personnalisées ou un GPU, vous écrivez la version PyTorch.

7. Résumé

Dans ce carnet, vous avez :

  • revu les fonctions d’activation et la structure des MLP de régression et de classification,
  • construit en PyTorch un MLP à trois couches cachées, avec dropout et normalisation par lots, entraîné par mini-lots avec Adam sur les données de sources sismiques à quatre classes,
  • lu le surapprentissage sur les courbes d’apprentissage entraînement/validation,
  • sauvegardé et restauré des modèles avec state_dict, et utilisé des checkpoints pour conserver la meilleure version rencontrée pendant l’entraînement,
  • comparé avec le MLPClassifier de scikit-learn.

Nous avons choisi ici à la main les tailles de couches, le taux de dropout et le taux d’apprentissage. Les choisir de façon systématique, en explorant architectures et hyperparamètres, est l’objet du TP du carnet 4.5.