{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "0f3b1ba4",
   "metadata": {},
   "source": [
    "# X.2 - Travaux pratiques\n",
    "\n",
    "L'objectif de cette séance de travaux pratiques est de découvrir la bibliothèque\n",
    "[PyTorch](https://pytorch.org/) et ses fonctionnalités pour la création et l'apprentissage\n",
    "de réseaux de neurones profonds. En particulier, nous allons reprendre les deux modèles\n",
    "vus précédemment (régression logistique et perceptron) pour montrer comment les\n",
    "implémenter avec PyTorch. Nous verrons également un premier modèle simple de réseau\n",
    "de neurones profonds convolutif.\n",
    "\n",
    "## Importation de PyTorch\n",
    "\n",
    "PyTorch est une bibliothèque logicielle open-source pour l'apprentissage profond, développée par Meta AI.\n",
    "Elle implémente les briques de base (opérations tensorielles, couches, fonctions de transfert, différentiation\n",
    "automatique) et gère notamment la possibilité d'exécuter un réseau de neurones sur le processeur (*CPU*) ou sur\n",
    "accélérateur graphique matériel (*GPU*)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "35a4eb1b",
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "from torch import nn\n",
    "\n",
    "# Graine aléatoire, pour la reproductibilité (initialisation des poids, ordre des batches)\n",
    "torch.manual_seed(42)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "411c50b2",
   "metadata": {},
   "source": [
    "Contrairement à Keras, qui automatise l'apprentissage d'un modèle à l'aide des méthodes `.compile()`\n",
    "et `.fit()`, PyTorch expose directement la boucle d'apprentissage (calcul de la prédiction, de l'erreur,\n",
    "des gradients, puis mise à jour des paramètres). C'est un peu plus verbeux à écrire, mais cela permet de\n",
    "garder le contrôle total sur ce qui se passe à chaque itération — ce qui nous sera utile dans les séances\n",
    "suivantes.\n",
    "\n",
    "PyTorch intègre, via le sous-module `torchvision`, des utilitaires pour charger et manipuler un certain\n",
    "nombre de jeux de données populaires, comme MNIST :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a8c3c523",
   "metadata": {},
   "outputs": [],
   "source": [
    "from torchvision import datasets\n",
    "\n",
    "# Chargement du jeu de données MNIST (téléchargé puis mis en cache dans ./data)\n",
    "mnist_train = datasets.MNIST(root='data', train=True, download=True)\n",
    "mnist_test = datasets.MNIST(root='data', train=False, download=True)\n",
    "\n",
    "# Redimensionnement des images 28x28 en vecteurs d=784\n",
    "X_train = mnist_train.data.reshape(-1, 784).float()\n",
    "X_test = mnist_test.data.reshape(-1, 784).float()\n",
    "y_train = mnist_train.targets\n",
    "y_test = mnist_test.targets\n",
    "\n",
    "# Normalisation entre 0 et 1\n",
    "X_train /= 255\n",
    "X_test /= 255"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d6c30514",
   "metadata": {},
   "source": [
    "<div class=\"admonition note\">\n",
    "\n",
    "**Note**\n",
    "\n",
    "Contrairement à Keras, qui attend des étiquettes au format *one-hot encoding* pour les fonctions de coût\n",
    "de classification, PyTorch attend directement l'indice entier de la classe (ici, `y_train` et `y_test`\n",
    "restent des vecteurs d'entiers entre 0 et 9). C'est la fonction de coût `nn.CrossEntropyLoss` elle-même\n",
    "qui se charge, en interne, de comparer cet indice à la distribution prédite.\n",
    "\n",
    "</div>\n",
    "\n",
    "Enfin, pour parcourir les données par *batch* pendant l'apprentissage, PyTorch fournit les classes\n",
    "`TensorDataset` et `DataLoader` :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1062a55a",
   "metadata": {},
   "outputs": [],
   "source": [
    "from torch.utils.data import TensorDataset, DataLoader\n",
    "\n",
    "train_dataset = TensorDataset(X_train, y_train)\n",
    "train_loader = DataLoader(train_dataset, batch_size=300, shuffle=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "62d138ad",
   "metadata": {},
   "source": [
    "`train_loader` peut ensuite être parcouru dans une boucle `for batch_X, batch_y in train_loader:` pour\n",
    "obtenir successivement chaque batch d'exemples.\n",
    "\n",
    "## Régression logistique avec PyTorch\n",
    "\n",
    "PyTorch propose plusieurs façons différentes de définir un réseau de neurones. La façon la plus simple pour\n",
    "les réseaux à propagation avant (*feedforward*), qui empilent les couches de façon séquentielle, est\n",
    "d'utiliser la classe `nn.Sequential` (voir [sa documentation](https://pytorch.org/docs/stable/generated/torch.nn.Sequential.html)),\n",
    "un équivalent direct de la classe `Sequential` de Keras :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dbaeacff",
   "metadata": {},
   "outputs": [],
   "source": [
    "model = nn.Sequential(\n",
    "    nn.Linear(784, 10),\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "115a4ef5",
   "metadata": {},
   "source": [
    "`nn.Linear(784, 10)` définit une couche entièrement connectée (l'équivalent de `Dense` sous Keras), qui\n",
    "projette un vecteur d'entrée de dimension 784 vers un vecteur de sortie de dimension 10.\n",
    "\n",
    "<div class=\"admonition note\">\n",
    "\n",
    "**Note**\n",
    "\n",
    "On ne place pas ici d'activation *softmax* explicite en sortie du modèle : `nn.CrossEntropyLoss`\n",
    "(utilisée plus bas) applique elle-même un `log_softmax` en interne, de façon numériquement plus stable\n",
    "que d'enchaîner un softmax puis un logarithme séparément. Le modèle renvoie donc directement des scores\n",
    "bruts (*logits*), pas des probabilités.\n",
    "\n",
    "</div>\n",
    "\n",
    "On peut visualiser l'architecture du réseau en affichant simplement le modèle, et compter le nombre de\n",
    "paramètres à l'aide d'une petite fonction utilitaire :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9201094c",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(model)\n",
    "\n",
    "n_params = sum(p.numel() for p in model.parameters())\n",
    "print(f\"Nombre de paramètres : {n_params}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "17b6d520",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Quel modèle de prédiction reconnaissez-vous ? Vérifier le nombre de paramètres du réseau.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cb48a5e2",
   "metadata": {},
   "source": [
    "En plus de la définition de l'architecture, nous avons encore besoin de spécifier deux éléments avant\n",
    "d'entraîner notre modèle : une fonction de coût (*loss*) et une méthode d'optimisation. Nous allons utiliser\n",
    "l'entropie croisée (`nn.CrossEntropyLoss`) comme fonction de coût et la descente de gradient stochastique\n",
    "(*stochastic gradient descent*, `torch.optim.SGD`) :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "16e205d8",
   "metadata": {},
   "outputs": [],
   "source": [
    "criterion = nn.CrossEntropyLoss()\n",
    "optimizer = torch.optim.SGD(model.parameters(), lr=0.5)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c9fbf443",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "À l'aide de la [documentation de SGD](https://pytorch.org/docs/stable/generated/torch.optim.SGD.html), déterminer à quoi correspond le paramètre `lr=`.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "437a39e6",
   "metadata": {},
   "source": [
    "Contrairement à Keras, où la boucle d'apprentissage est entièrement automatisée par la méthode `.fit()`,\n",
    "avec PyTorch on écrit explicitement, pour chaque batch, les quatre étapes suivantes : (1) calculer la\n",
    "prédiction du modèle, (2) calculer l'erreur par rapport aux étiquettes, (3) calculer les gradients de\n",
    "l'erreur par rapport aux paramètres (rétropropagation), (4) mettre à jour les paramètres avec l'optimiseur."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ec855da3",
   "metadata": {},
   "outputs": [],
   "source": [
    "n_epochs = 10\n",
    "\n",
    "for epoch in range(n_epochs):\n",
    "    for batch_X, batch_y in train_loader:\n",
    "        # 1) Calcul de la prédiction (forward)\n",
    "        outputs = model(batch_X)\n",
    "        # 2) Calcul de l'erreur\n",
    "        loss = criterion(outputs, batch_y)\n",
    "        # 3) Calcul des gradients (backward)\n",
    "        optimizer.zero_grad()\n",
    "        loss.backward()\n",
    "        # 4) Mise à jour des paramètres\n",
    "        optimizer.step()\n",
    "    print(f\"Epoch {epoch}: loss = {loss.item():.4f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "893e7029",
   "metadata": {},
   "source": [
    "-  `batch_size` (passé plus haut à `DataLoader`) correspond au nombre d'exemples utilisé pour estimer\n",
    "   le gradient de la fonction de coût.\n",
    "-  `n_epochs` est le nombre d'époques (*i.e.* passages sur l'ensemble des exemples de la base\n",
    "   d'apprentissage) lors de la descente de gradient.\n",
    "-  `optimizer.zero_grad()` est nécessaire car PyTorch *accumule* par défaut les gradients à chaque appel\n",
    "   de `.backward()` ; il faut donc explicitement les remettre à zéro avant de calculer ceux du batch\n",
    "   courant.\n",
    "\n",
    "L'évaluation des performances du modèle sur le jeu de test se fait en calculant nous-mêmes la métrique\n",
    "souhaitée, à l'intérieur d'un bloc `torch.no_grad()` (qui désactive le calcul, inutile ici, des gradients) :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9e969498",
   "metadata": {},
   "outputs": [],
   "source": [
    "with torch.no_grad():\n",
    "    outputs = model(X_test)\n",
    "    test_loss = criterion(outputs, y_test)\n",
    "    accuracy = (outputs.argmax(dim=1) == y_test).float().mean()\n",
    "print(f\"Erreur de test : {test_loss.item()*100:.2f}\")\n",
    "print(f\"Accuracy de test : {accuracy.item()*100:.2f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0acb6d98",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Comparer les performances obtenues par cette régression logistique implémentée avec PyTorch avec celles obtenues lors du premier TP. Conclure.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5e3684a6",
   "metadata": {},
   "source": [
    "## Perceptron avec PyTorch\n",
    "\n",
    "Nous pouvons maintenant enrichir ce modèle de régression logistique en insérant\n",
    "une couche de neurones cachés complètement connectée ainsi qu'une\n",
    "fonction d'activation non linéaire de type sigmoïde entre la couche\n",
    "d'entrée et la couche de sortie. Ce nouveau modèle va ainsi correspondre\n",
    "au réseau de neurones à une couche cachée (perceptron) vu précédemment."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e3ab0acb",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Créer un nouveau modèle `model = nn.Sequential(...)` et le compléter en lui\n",
    "ajoutant :\n",
    "\n",
    "- une couche entièrement connectée avec 100 neurones cachés,\n",
    "- une activation sigmoide (`nn.Sigmoid()`),\n",
    "- une couche entièrement connectée avec 10 neurones de sortie,\n",
    "\n",
    "de sorte à retrouver le *multi-layer perceptron* du TP précédent. On rappelle qu'aucune activation\n",
    "n'est nécessaire après la dernière couche (voir la note plus haut sur `nn.CrossEntropyLoss`).\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a1da8e84",
   "metadata": {},
   "outputs": [],
   "source": [
    "model = ... # À compléter"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1a18e24d",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Combien ce modèle a-t-il de paramètres ? Justifier le calcul et vérifier à l'aide du décompte des paramètres du modèle.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "76dff2a8",
   "metadata": {},
   "source": [
    "L'entraînement de ce nouveau modèle s'effectue de façon strictement identique à ce que nous avons vu\n",
    "précédemment : PyTorch ne fait aucune différence entre les modèles une fois qu'ils sont construits, la boucle\n",
    "d'apprentissage manipule n'importe quel `nn.Sequential` de la même façon."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e7776ea2",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Recréer un optimiseur `SGD` pour ce nouveau modèle, puis l'entraîner sur la base d'apprentissage de MNIST. On utilisera la même fonction de coût et le même pas d'apprentissage que pour la régression logistique.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "520b693b",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Évaluer les performances du perceptron multi-couche sur le jeu de test de MNIST et comparer à celles obtenues lors du précédent TP. Conclure.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7af41db5",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "À l'aide de la [documentation de PyTorch](https://pytorch.org/docs/stable/generated/torch.nn.Linear.html), déterminer comment sont initialisés les paramètres du modèle pour les couches entièrement connectées.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a8e0e60b",
   "metadata": {},
   "source": [
    "Une fonctionnalité utile de PyTorch est la possibilité de sauvegarder des modèles sur le disque pour les\n",
    "partager ou les réutiliser plus tard. On sauvegarde en général uniquement les *paramètres* du modèle\n",
    "(son `state_dict`), pas son architecture :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "96f4e126",
   "metadata": {},
   "outputs": [],
   "source": [
    "torch.save(model.state_dict(), 'MLP.pt')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b53a0f3f",
   "metadata": {},
   "source": [
    "Il est possible de recharger un modèle sur le disque, à condition de recréer d'abord une instance du même\n",
    "modèle (l'architecture, elle, n'est pas sauvegardée) :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "72209181",
   "metadata": {},
   "outputs": [],
   "source": [
    "model = nn.Sequential(\n",
    "    nn.Linear(784, 100),\n",
    "    nn.Sigmoid(),\n",
    "    nn.Linear(100, 10),\n",
    ")\n",
    "model.load_state_dict(torch.load('MLP.pt'))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c05afa3b",
   "metadata": {},
   "source": [
    "## Réseaux de neurones convolutifs avec PyTorch\n",
    "\n",
    "Pour cette dernière partie, nous allons passer du réseau entièrement connecté à un modèle convolutif.\n",
    "En effet, les réseaux de neurones convolutifs profonds (*Convolutional Neural Networks* ou CNN) sont\n",
    "particulièrement adaptés à la reconnaissance d'images.\n",
    "\n",
    "Les réseaux convolutifs manipulent des images multi-dimensionnelles en entrée (des tenseurs). Comme nous\n",
    "avions initialement transformé nos images en vecteurs, nous allons les redimensionner sous forme d'image.\n",
    "\n",
    "<div class=\"admonition note\">\n",
    "\n",
    "**Note**\n",
    "\n",
    "Contrairement à Keras, qui place par convention le nombre de canaux en dernière position\n",
    "($H \\times L \\times C$, *channels-last*), PyTorch place le nombre de canaux juste après la\n",
    "dimension de batch ($C \\times H \\times L$, *channels-first*). Pour MNIST (images en niveaux de\n",
    "gris, un seul canal), cela donne :\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d95a0606",
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train_img = X_train.reshape(-1, 1, 28, 28)\n",
    "X_test_img = X_test.reshape(-1, 1, 28, 28)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "90c638ba",
   "metadata": {},
   "source": [
    "Par rapport aux réseaux complètement connectés, les réseaux convolutifs\n",
    "utilisent les briques élémentaires suivantes :\n",
    "\n",
    "1. Des couches de convolution, qui transforment un tenseur d'entrée de\n",
    "   taille $p \\times n_x \\times n_y$ en un tenseur de sortie\n",
    "   $n_H \\times n_{x'} \\times n_{y'}$, où $n_H$ est le nombre\n",
    "   de filtres choisi.\n",
    "\n",
    "PyTorch implémente bien entendu par défaut les couches convolutives. Il s'agit de la classe `nn.Conv2d`.\n",
    "Par exemple, une couche de convolution pour traiter les images d'entrée de MNIST peut être créée de la\n",
    "manière suivante :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "69d1159e",
   "metadata": {},
   "outputs": [],
   "source": [
    "nn.Conv2d(in_channels=1, out_channels=32, kernel_size=5, padding='same')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5e536c79",
   "metadata": {},
   "source": [
    "Dans l'instanciation de cette classe, on retrouve de nombreux arguments :\n",
    "\n",
    "-  `in_channels=1` correspond au nombre de canaux de l'image d'entrée (1 pour une image en niveaux de gris).\n",
    "-  `out_channels=32` correspond au nombre de filtres souhaité pour cette couche convolutive.\n",
    "-  `kernel_size=5` est la taille (5x5) du noyau de convolution de chacun des filtres.\n",
    "-  `padding='same'` ajoute des 0 à l'image d'entrée de sorte à conserver la même taille spatiale en sortie qu'en entrée.\n",
    "\n",
    "<div class=\"admonition note\">\n",
    "\n",
    "**Note**\n",
    "\n",
    "Contrairement à Keras, où la non-linéarité peut être passée directement en argument de la couche de\n",
    "convolution (`activation=...`), PyTorch garde les couches de convolution et les fonctions d'activation\n",
    "séparées : on les enchaîne explicitement dans le `nn.Sequential` (par exemple `nn.Conv2d(...)` suivie\n",
    "de `nn.Sigmoid()`).\n",
    "\n",
    "</div>\n",
    "\n",
    "2. Des couches d'agrégation spatiale (*pooling*), afin de permettre une\n",
    "   invariance aux translations locales. Elles sont implémentées par la classe `nn.MaxPool2d` :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "13b61824",
   "metadata": {},
   "outputs": [],
   "source": [
    "nn.MaxPool2d(kernel_size=2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "20707597",
   "metadata": {},
   "source": [
    "-  `kernel_size=2` est la taille spatiale (2x2) sur laquelle l'opération d'agrégation est effectuée.\n",
    "\n",
    "Ces deux briques élémentaires peuvent être ajoutées à n'importe quel modèle séquentiel PyTorch, comme les\n",
    "couches entièrement connectées."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "61193ffa",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Implémenter un réseau de neurones convolutif reprenant l'architecture suivante, similaire à celle du modèle historique LeNet5, illustré ci-dessous :\n",
    "\n",
    "![](figures/LeNet5.png)\n",
    "\n",
    "-  Une couche de convolutions avec 16 filtres de taille\n",
    "   $5 \\times 5$ (`padding='same'`), suivie d'une non-linéarité sigmoïde puis\n",
    "   d'une couche de max pooling $2 \\times 2$.\n",
    "-  Une couche de convolutions avec 32 filtres de taille\n",
    "   $5 \\times 5$ (`padding='same'`), suivie d'une non-linéarité sigmoïde puis\n",
    "   d'une couche de max pooling $2 \\times 2$.\n",
    "-  Une seconde couche de convolution avec 64 filtres de taille\n",
    "   $5 \\times 5$ (`padding='same'`), suivie d'une non linéarité sigmoïde puis\n",
    "   d'une couche de max pooling $2 \\times 2$.\n",
    "-  Comme dans le réseau LeNet, on considérera la sortie du second bloc\n",
    "   convolutif comme un vecteur, ce que revient à « mettre à plat » les\n",
    "   couches convolutives précédentes (`nn.Flatten()`).\n",
    "-  Une couche complètement connectée de taille 100, suivie d'une sigmoide.\n",
    "-  Une couche complètement connectée de taille 10.\n",
    "\n",
    "On rappelle que MNIST fait $28\\times28$ pixels : après trois poolings $2\\times2$\n",
    "successifs, la taille spatiale est divisée par $2^3=8$, soit des cartes de taille\n",
    "$28/8=3,5$, arrondi à $3\\times3$ par PyTorch. Le nombre d'éléments en entrée de la\n",
    "première couche entièrement connectée est donc $64 \\times 3 \\times 3$.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6967c952",
   "metadata": {},
   "outputs": [],
   "source": [
    "model = nn.Sequential(\n",
    "    # ...\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dc2c3094",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Créer un optimiseur, puis entraîner le modèle sur le jeu d'apprentissage de MNIST (au format image, `X_train_img`). On reprendra la même fonction de coût que précédemment.\n",
    "\n",
    "<div class=\"admonition note\">\n",
    "\n",
    "**Note**\n",
    "\n",
    "Avec un réseau aussi profond (3 couches convolutives) et une non-linéarité sigmoïde, la descente de gradient stochastique classique (`SGD`), même avec momentum, a tendance à rester bloquée près de son initialisation : les gradients deviennent trop faibles dans les premières couches pour s'en échapper (un symptôme du problème de disparition du gradient, *vanishing gradient*). On utilisera donc ici l'optimiseur `Adam`, dont la normalisation adaptative du pas d'apprentissage par paramètre le rend bien plus robuste à ce problème.\n",
    "\n",
    "</div>\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6dfd886e",
   "metadata": {},
   "outputs": [],
   "source": [
    "torch.save(model.state_dict(), 'LeNet.pt')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0b035e7a",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Évaluer les performances du modèle sur le jeu de test de MNIST (au format image, `X_test_img`). Vous devriez obtenir un score proche de 99% pour ce modèle.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ce0dd302",
   "metadata": {},
   "source": [
    "### Accélération par GPU\n",
    "\n",
    "Avec PyTorch, l'exécution sur GPU se fait en déplaçant explicitement le modèle et les tenseurs de données\n",
    "sur le périphérique voulu, à l'aide de la méthode `.to()` :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "112d5765",
   "metadata": {},
   "outputs": [],
   "source": [
    "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n",
    "model = model.to(device)\n",
    "# Dans la boucle d'apprentissage, déplacer aussi chaque batch :\n",
    "# batch_X, batch_y = batch_X.to(device), batch_y.to(device)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "48a39fe3",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "*(pour aller plus loin)* Combien de temps de calcul dure une époque avec ce modèle convolutif ? Comparer sur votre propre machine dotée d'une carte graphique (GPU), sur JupyterHub GPU ou sur Google Colab.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "13bbc138",
   "metadata": {},
   "source": [
    "[LBD+89] Yann LeCun, Bernhard Boser, John S Denker, Donnie Henderson,\n",
    "Richard E Howard, Wayne Hubbard, and Lawrence D Jackel. Backpropagation\n",
    "applied to handwritten zip code recognition. *Neural computation*,\n",
    "1(4):541–551, 1989."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
