{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Séance 1 - Réseau de neurones dense\n",
    "\n",
    "On se propose de classifier les chiffres manuscrit du dataset [MNIST](https://yann.lecun.com/exdb/mnist/) en définissant ses propres réseaux de neurones denses. L'objectif est de découvrir la manière d'entraîner ces algorithmes et observer en pratique les bases théoriques discutées en cours.\n",
    "\n",
    "## Exploration des données\n",
    "\n",
    "Commençons par importer les données."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "\n",
    "%matplotlib inline\n",
    "import matplotlib.pyplot as plt\n",
    "import seaborn as sns\n",
    "sns.set(style='whitegrid')\n",
    "\n",
    "import tensorflow as tf\n",
    "from tensorflow import keras\n",
    "\n",
    "(X_train_full, y_train_full), (X_test, y_test) = (keras.datasets.mnist.load_data())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Consigne** : À l'aide de la fonction [`train_test_split`](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html), séparer le jeu d'entraînement complet en un dataset d'entraînement et un dataset de validation. Afficher les tailles des datasets respectifs."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(48000, 28, 28) (48000,)\n",
      "(12000, 28, 28) (12000,)\n"
     ]
    }
   ],
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "X_train, X_valid, y_train, y_valid = train_test_split(\n",
    "    X_train_full, y_train_full, test_size=0.2, random_state=42\n",
    ")\n",
    "print(X_train.shape, y_train.shape)\n",
    "print(X_valid.shape, y_valid.shape)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Consigne** : Afficher plusieurs images du dataset d'entraînement aléatoirement. On pourra utiliser la fonction [`imshow`](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.imshow.html)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 35868 characters]",
      "text/plain": [
       "<Figure size 1000x1000 with 25 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "plt.figure(figsize=(10,10))\n",
    "for i in range(25):\n",
    "    plt.subplot(5,5,i+1)\n",
    "    plt.xticks([])\n",
    "    plt.yticks([])\n",
    "    plt.grid(False)\n",
    "    plt.imshow(X_train[i], cmap=plt.cm.binary)\n",
    "    plt.xlabel(y_train[i])\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Les images sont en niveau de gris, donc de valeurs entre 0 et 255. Pour entraîner correctement un réseau de neurones, il est préférable que les inputs soit standardisés.\n",
    "\n",
    "**Consigne** : Standardiser les données en utilisant la classe [`StandardScaler`](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.StandardScaler.html). On commencera par applatir les images en utilisant la méthode [`reshape`](https://numpy.org/doc/stable/reference/generated/numpy.reshape.html), puis on applique le pré-processing et on termine par reformer la matrice."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.preprocessing import StandardScaler\n",
    "\n",
    "scaler = StandardScaler()\n",
    "X_train_flat = X_train.reshape(X_train.shape[0], -1).astype(np.float32)\n",
    "X_valid_flat = X_valid.reshape(X_valid.shape[0], -1).astype(np.float32)\n",
    "X_test_flat = X_test.reshape(X_test.shape[0], -1).astype(np.float32)\n",
    "\n",
    "X_train = scaler.fit_transform(X_train_flat).reshape(X_train.shape)\n",
    "X_valid = scaler.transform(X_valid_flat).reshape(X_valid.shape)\n",
    "X_test = scaler.transform(X_test_flat).reshape(X_test.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Modélisation du réseau de neurones\n",
    "\n",
    "Pour le moment, nous travaillons avec des images de tailles $28\\times28$, mais nous ne savons pas définir (pour le moment) de réseau de neurones capable de travailler directement avec une image. Nous allons utiliser une couche nommée [`Flatten`](https://keras.io/api/layers/reshaping_layers/flatten/) dont le but est *d'applatir* une matrice de dimension *(height, width, channel)* en un vecteur de taille *height $\\times$ width $\\times$ channel*. Dans le cadre des données MNIST, *channel*=1 puisque nous sommes en niveau de gris, et *height=width=28*. On aura un vecteur de 784 dimensions.\n",
    "\n",
    "Une fois que nous aurons décrit l'ensemble du réseau, nous devrons terminer le réseau par une couche avec dix neurones : un pour chaque classe. Pour s'assurer que l'on aura une estimation de probabilité d'appartenance à la classe, on utilisera la fonction softmax. Pour un vecteur $x = (x_0, x_1, \\ldots, x_n)$ on a:\n",
    "\n",
    "$$\\text{softmax}(x)_j = \\frac{e^{x_j}}{\\displaystyle \\sum_{i=0}^n e^{x_i}}$$\n",
    "\n",
    "On veut définir le réseau suivant:\n",
    "* **Couche cachée 1** : 256 neurones avec fonction d'activation ReLU\n",
    "* **Couche cachée 2** : 128 neurones avec fonction d'activation ReLU\n",
    "\n",
    "On peut définir de plusieurs manières un réseau de neurones. La première est de la même manière qu'une liste à laquelle on ajoute des couches en utilisation le template de modèle *Sequential* :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "model = keras.models.Sequential()\n",
    "model.add(keras.layers.Input(shape=[28, 28]))\n",
    "model.add(keras.layers.Flatten())\n",
    "model.add(keras.layers.Dense(256, activation=\"relu\"))\n",
    "model.add(keras.layers.Dense(128, activation=\"relu\"))\n",
    "model.add(keras.layers.Dense(10, activation=\"softmax\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En début de réseau de neurones on doit définir la dimension de l'input: ici (28, 28). Le reste des dimensions pour l'ensemble des couches qui lui succède sont calculées automatiquement.\n",
    "\n",
    "La deuxième manière est directement sous le format d'une liste:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "model = keras.models.Sequential([\n",
    "    keras.layers.Input(shape=[28, 28]),\n",
    "    keras.layers.Flatten(),\n",
    "    keras.layers.Dense(256, activation=\"relu\"),\n",
    "    keras.layers.Dense(128, activation=\"relu\"),\n",
    "    keras.layers.Dense(10, activation=\"softmax\")\n",
    "])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Consigne** : Calculer à la main le nombre de neurones du modèle, couche par couche. Puis utiliser la méthode [`summary`](https://keras.io/api/models/model/#summary-method) pour vérifier les calculs."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "784\n",
      "256\n",
      "128\n",
      "10\n",
      "235146\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"><span style=\"font-weight: bold\">Model: \"sequential_1\"</span>\n",
       "</pre>\n"
      ],
      "text/plain": [
       "\u001b[1mModel: \"sequential_1\"\u001b[0m\n"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓\n",
       "┃<span style=\"font-weight: bold\"> Layer (type)                    </span>┃<span style=\"font-weight: bold\"> Output Shape           </span>┃<span style=\"font-weight: bold\">       Param # </span>┃\n",
       "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩\n",
       "│ flatten_1 (<span style=\"color: #0087ff; text-decoration-color: #0087ff\">Flatten</span>)             │ (<span style=\"color: #00d7ff; text-decoration-color: #00d7ff\">None</span>, <span style=\"color: #00af00; text-decoration-color: #00af00\">784</span>)            │             <span style=\"color: #00af00; text-decoration-color: #00af00\">0</span> │\n",
       "├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
       "│ dense_3 (<span style=\"color: #0087ff; text-decoration-color: #0087ff\">Dense</span>)                 │ (<span style=\"color: #00d7ff; text-decoration-color: #00d7ff\">None</span>, <span style=\"color: #00af00; text-decoration-color: #00af00\">256</span>)            │       <span style=\"color: #00af00; text-decoration-color: #00af00\">200,960</span> │\n",
       "├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
       "│ dense_4 (<span style=\"color: #0087ff; text-decoration-color: #0087ff\">Dense</span>)                 │ (<span style=\"color: #00d7ff; text-decoration-color: #00d7ff\">None</span>, <span style=\"color: #00af00; text-decoration-color: #00af00\">128</span>)            │        <span style=\"color: #00af00; text-decoration-color: #00af00\">32,896</span> │\n",
       "├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
       "│ dense_5 (<span style=\"color: #0087ff; text-decoration-color: #0087ff\">Dense</span>)                 │ (<span style=\"color: #00d7ff; text-decoration-color: #00d7ff\">None</span>, <span style=\"color: #00af00; text-decoration-color: #00af00\">10</span>)             │         <span style=\"color: #00af00; text-decoration-color: #00af00\">1,290</span> │\n",
       "└─────────────────────────────────┴────────────────────────┴───────────────┘\n",
       "</pre>\n"
      ],
      "text/plain": [
       "┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓\n",
       "┃\u001b[1m \u001b[0m\u001b[1mLayer (type)                   \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mOutput Shape          \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1m      Param #\u001b[0m\u001b[1m \u001b[0m┃\n",
       "┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩\n",
       "│ flatten_1 (\u001b[38;5;33mFlatten\u001b[0m)             │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m784\u001b[0m)            │             \u001b[38;5;34m0\u001b[0m │\n",
       "├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
       "│ dense_3 (\u001b[38;5;33mDense\u001b[0m)                 │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m256\u001b[0m)            │       \u001b[38;5;34m200,960\u001b[0m │\n",
       "├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
       "│ dense_4 (\u001b[38;5;33mDense\u001b[0m)                 │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m128\u001b[0m)            │        \u001b[38;5;34m32,896\u001b[0m │\n",
       "├─────────────────────────────────┼────────────────────────┼───────────────┤\n",
       "│ dense_5 (\u001b[38;5;33mDense\u001b[0m)                 │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m10\u001b[0m)             │         \u001b[38;5;34m1,290\u001b[0m │\n",
       "└─────────────────────────────────┴────────────────────────┴───────────────┘\n"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"><span style=\"font-weight: bold\"> Total params: </span><span style=\"color: #00af00; text-decoration-color: #00af00\">235,146</span> (918.54 KB)\n",
       "</pre>\n"
      ],
      "text/plain": [
       "\u001b[1m Total params: \u001b[0m\u001b[38;5;34m235,146\u001b[0m (918.54 KB)\n"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"><span style=\"font-weight: bold\"> Trainable params: </span><span style=\"color: #00af00; text-decoration-color: #00af00\">235,146</span> (918.54 KB)\n",
       "</pre>\n"
      ],
      "text/plain": [
       "\u001b[1m Trainable params: \u001b[0m\u001b[38;5;34m235,146\u001b[0m (918.54 KB)\n"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"><span style=\"font-weight: bold\"> Non-trainable params: </span><span style=\"color: #00af00; text-decoration-color: #00af00\">0</span> (0.00 B)\n",
       "</pre>\n"
      ],
      "text/plain": [
       "\u001b[1m Non-trainable params: \u001b[0m\u001b[38;5;34m0\u001b[0m (0.00 B)\n"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "print(28*28)\n",
    "print(256)\n",
    "print(128)\n",
    "print(10)\n",
    "\n",
    "print(28 * 28 * 256 + 256 + 256 * 128 + 128 + 128 * 10 + 10)\n",
    "\n",
    "model.summary()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Nous avons décrit l'architecture du réseau de neurones. Il faut maintenant définir comment il va s'entraîner. Nous devons spécifier:\n",
    "\n",
    "* **Loss** : Quelle fonction de perte est à minimiser ?\n",
    "* **Optimizer** Quel schéma de descente de gradient est à utiliser ?\n",
    "* **Metrics** : Quelles métrique de performance souhaite-on observer pendant l'entraînement ?\n",
    "\n",
    "Puisque nous travaillons sur un problème de classification avec plusieurs classes, la fonction de perte [`sparse_categorical_crossentropy`](https://keras.io/api/losses/probabilistic_losses/#sparsecategoricalcrossentropy-class) est celle qu'il nous faut.\n",
    "\n",
    "Concernant l'*optimizer* il y a plusieurs possibilités que nous verrons dans une prochaine séance. Pour le moment nous travaillerons avec une descente de gradient stochastique par mini-batch [`SGD`](https://keras.io/api/optimizers/sgd/). Pour la définir, nous devons statuer sur:\n",
    "* **Learning rate** : pas de descente, on décide de choisir la valeur 0.001\n",
    "* **Batch size** : nombre d'observations à considérer pour chacune des passes. On décide de prendre 32 images par batch. Cette valeur sera à renseigner un peu plus tard.\n",
    "\n",
    "Pour les métriques, nous suivrons l'accuracy parce que la distribution des catégories à prédire n'est pas déséquilibrées."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "model.compile(\n",
    "    loss=\"sparse_categorical_crossentropy\",\n",
    "    optimizer=keras.optimizers.SGD(learning_rate=1e-3),\n",
    "    metrics=[\"accuracy\"],\n",
    ")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Entraînement\n",
    "\n",
    "Le modèle est maintenant prêt à être entraîné, il nous reste à lui indiquer:\n",
    "* **Données** : jeu d'entraînement et jeu de validation\n",
    "* **Époques** : le nombre de passes à réaliser sur l'ensemble du dataset\n",
    "* **Batch size** : le nombre d'observations pour chaque batch, nous avions décidé juste avant que ce serait 32"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch 1/5\n",
      "\u001b[1m1500/1500\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 1ms/step - accuracy: 0.6391 - loss: 1.2957 - val_accuracy: 0.8216 - val_loss: 0.7425\n",
      "Epoch 2/5\n",
      "\u001b[1m1500/1500\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 855us/step - accuracy: 0.8513 - loss: 0.5829 - val_accuracy: 0.8708 - val_loss: 0.4937\n",
      "Epoch 3/5\n",
      "\u001b[1m1500/1500\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 856us/step - accuracy: 0.8828 - loss: 0.4307 - val_accuracy: 0.8915 - val_loss: 0.4044\n",
      "Epoch 4/5\n",
      "\u001b[1m1500/1500\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 847us/step - accuracy: 0.8984 - loss: 0.3613 - val_accuracy: 0.9036 - val_loss: 0.3565\n",
      "Epoch 5/5\n",
      "\u001b[1m1500/1500\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 836us/step - accuracy: 0.9091 - loss: 0.3192 - val_accuracy: 0.9107 - val_loss: 0.3250\n"
     ]
    }
   ],
   "source": [
    "epochs = 5\n",
    "batch_size = 32\n",
    "\n",
    "history = model.fit(\n",
    "    X_train,\n",
    "    y_train,\n",
    "    epochs=epochs,\n",
    "    batch_size=batch_size,\n",
    "    validation_data=(X_valid, y_valid),\n",
    ")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Nous avons des informations disponible dans l'objet *history*, plus précisement dans *history.history*\n",
    "\n",
    "**Consigne** : Créer un [`DataFrame`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.html) à partir de *history.history* puis inspecter-le."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "   accuracy      loss  val_accuracy  val_loss\n",
      "0  0.639125  1.295723      0.821583  0.742507\n",
      "1  0.851313  0.582944      0.870750  0.493708\n",
      "2  0.882833  0.430686      0.891500  0.404362\n",
      "3  0.898396  0.361310      0.903583  0.356492\n",
      "4  0.909104  0.319176      0.910667  0.324995\n"
     ]
    }
   ],
   "source": [
    "history_df = pd.DataFrame(history.history)\n",
    "\n",
    "print(history_df)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Consigne**: Définir une fonction `plot_learning_curves` qui prend en paramètre l'objet *history* et qui renvoie un graphique. Le graphique correspondra à deux graphiques côte à côte :\n",
    "1. Le premier montre l'évolution de la fonction de perte en fonction des époques\n",
    "2. Le second montre l'évolution de l'accuracy en fonction des époques\n",
    "Dans les deux cas, les valeurs de performance sur le dataset de validation doivent être en pointillé. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_learning_curves(history_df: pd.DataFrame) -> None:\n",
    "    plt.figure(figsize=(12, 4))\n",
    "\n",
    "    plt.subplot(1, 2, 1)\n",
    "    plt.plot(history_df['loss'], label='Training Loss')\n",
    "    plt.plot(history_df[\"val_loss\"], label=\"Validation Loss\")\n",
    "    plt.xlabel(\"Epochs\")\n",
    "    plt.ylabel(\"Loss\")\n",
    "    plt.legend()\n",
    "\n",
    "    plt.subplot(1, 2, 2)\n",
    "    plt.plot(history_df['accuracy'], label='Accuracy')\n",
    "    plt.plot(history_df[\"val_accuracy\"], label=\"Validation Accuracy\")\n",
    "    plt.xlabel('Epochs')\n",
    "    plt.ylabel('Accuracy')\n",
    "    plt.legend()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Consigne** : Exploiter la fonction précédente pour observer les courbes d'apprentissage du l'entraînement précédent."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 59224 characters]",
      "text/plain": [
       "<Figure size 1200x400 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "plot_learning_curves(history_df)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exploitation des prédictions\n",
    "\n",
    "On souhaite à présent utiliser le modèle pour prédire le chiffre présent dans une image.\n",
    "\n",
    "**Consigne** : Prédire sur le jeu de test à l'aide de la méthode [`predict`](https://keras.io/api/models/model_training_apis/#predict-method), puis observer le résultat sur la première image."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001b[1m313/313\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m0s\u001b[0m 427us/step\n",
      "[6.0739734e-05 4.0368459e-04 9.8676115e-01 3.0002455e-04 2.9265425e-07\n",
      " 7.8633835e-04 1.1406293e-02 1.2295802e-06 2.7377973e-04 6.4694455e-06]\n"
     ]
    }
   ],
   "source": [
    "predictions = model.predict(X_test)\n",
    "\n",
    "print(predictions[1])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Consigne** : Après avoir vérifier que si l'on somme les chiffres affichés pour la première image vaut bien 1, identifier la classe prédite par le modèle. Vérifier visuellement."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1.0\n",
      "2\n",
      "2\n"
     ]
    }
   ],
   "source": [
    "print(np.sum(predictions[1]))\n",
    "print(np.argmax(predictions[1]))\n",
    "print(y_test[1])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Quel est l'impact du learning rate ?\n",
    "\n",
    "On s'intéresse à présent à l'importance du choix du learning rate. On se propose de tester plusieurs valeurs pour obtenir les meilleurs performances.\n",
    "\n",
    "**Consigne** : Définir une fonction `get_model` qui prend en paramètre un `float` qui correspond à un learning rate. La fonction renvoie un modèle compilé avec les mêmes paramètres que précédemment, sauf la valeur du learning rate qui est renseignée par l'utilisateur."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_model(learning_rate: float) -> keras.Model:\n",
    "    model = keras.models.Sequential(\n",
    "        [\n",
    "            keras.layers.Input(shape=[28, 28]),\n",
    "            keras.layers.Flatten(),\n",
    "            keras.layers.Dense(256, activation=\"relu\"),\n",
    "            keras.layers.Dense(128, activation=\"relu\"),\n",
    "            keras.layers.Dense(10, activation=\"softmax\"),\n",
    "        ]\n",
    "    )\n",
    "    model.compile(\n",
    "        loss=\"sparse_categorical_crossentropy\",\n",
    "        optimizer=keras.optimizers.SGD(learning_rate=learning_rate),\n",
    "        metrics=[\"accuracy\"],\n",
    "    )\n",
    "\n",
    "    return model"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Avant de lancer sur plusieurs époques, commençons par écrire une ébauche de la boucle de comparaison avec 5 époques."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Learning rate: 0.1 - époques: 5\n",
      "Epoch 1/5\n",
      "\u001b[1m1500/1500\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 923us/step - accuracy: 0.9294 - loss: 0.2604 - val_accuracy: 0.9551 - val_loss: 0.2009\n",
      "Epoch 2/5\n",
      "\u001b[1m1500/1500\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 844us/step - accuracy: 0.9674 - loss: 0.1181 - val_accuracy: 0.9597 - val_loss: 0.2333\n",
      "Epoch 3/5\n",
      "\u001b[1m1500/1500\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 846us/step - accuracy: 0.9787 - loss: 0.0740 - val_accuracy: 0.9691 - val_loss: 0.1443\n",
      "Epoch 4/5\n",
      "\u001b[1m1500/1500\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 862us/step - accuracy: 0.9871 - loss: 0.0469 - val_accuracy: 0.9673 - val_loss: 0.2344\n",
      "Epoch 5/5\n",
      "\u001b[1m1500/1500\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 894us/step - accuracy: 0.9915 - loss: 0.0333 - val_accuracy: 0.9688 - val_loss: 0.1642\n",
      "Learning rate: 0.01 - époques: 5\n",
      "Epoch 1/5\n",
      "\u001b[1m1500/1500\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m3s\u001b[0m 2ms/step - accuracy: 0.8728 - loss: 0.4616 - val_accuracy: 0.9284 - val_loss: 0.2625\n",
      "Epoch 2/5\n",
      "\u001b[1m1500/1500\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 1ms/step - accuracy: 0.9428 - loss: 0.1983 - val_accuracy: 0.9450 - val_loss: 0.2082\n",
      "Epoch 3/5\n",
      "\u001b[1m1500/1500\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 1ms/step - accuracy: 0.9572 - loss: 0.1477 - val_accuracy: 0.9517 - val_loss: 0.1845\n",
      "Epoch 4/5\n",
      "\u001b[1m1500/1500\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 1ms/step - accuracy: 0.9659 - loss: 0.1174 - val_accuracy: 0.9580 - val_loss: 0.1667\n",
      "Epoch 5/5\n",
      "\u001b[1m1500/1500\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 979us/step - accuracy: 0.9725 - loss: 0.0971 - val_accuracy: 0.9612 - val_loss: 0.1587\n",
      "Learning rate: 0.001 - époques: 5\n",
      "Epoch 1/5\n",
      "\u001b[1m1500/1500\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 917us/step - accuracy: 0.6303 - loss: 1.3171 - val_accuracy: 0.8044 - val_loss: 0.7800\n",
      "Epoch 2/5\n",
      "\u001b[1m1500/1500\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 840us/step - accuracy: 0.8430 - loss: 0.6094 - val_accuracy: 0.8692 - val_loss: 0.5088\n",
      "Epoch 3/5\n",
      "\u001b[1m1500/1500\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 835us/step - accuracy: 0.8806 - loss: 0.4418 - val_accuracy: 0.8893 - val_loss: 0.4054\n",
      "Epoch 4/5\n",
      "\u001b[1m1500/1500\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 932us/step - accuracy: 0.8979 - loss: 0.3657 - val_accuracy: 0.9027 - val_loss: 0.3520\n",
      "Epoch 5/5\n",
      "\u001b[1m1500/1500\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m1s\u001b[0m 861us/step - accuracy: 0.9093 - loss: 0.3207 - val_accuracy: 0.9101 - val_loss: 0.3175\n"
     ]
    }
   ],
   "source": [
    "n_epochs = 5\n",
    "batch_size = 32\n",
    "learning_rates = [10 ** (-power) for power in range(1, 4)]\n",
    "\n",
    "results = []\n",
    "for learning_rate in learning_rates:\n",
    "    print(f\"Learning rate: {learning_rate} - époques: {n_epochs}\")\n",
    "    model = get_model(learning_rate=learning_rate)\n",
    "    history = model.fit(\n",
    "        X_train,\n",
    "        y_train,\n",
    "        epochs=n_epochs,\n",
    "        batch_size=batch_size,\n",
    "        validation_data=(X_valid, y_valid),\n",
    "    )\n",
    "    result = {\n",
    "        \"learning_rate\": learning_rate,\n",
    "        \"n_epochs\": n_epochs,\n",
    "        \"history\": pd.DataFrame(history.history),\n",
    "    }\n",
    "    results.append(result)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Consigne** : Définir une fonction `show_results` qui prend en paramètre l'objet *results* construit précédemment et qui renvoie un graphique similaire à celui renvoyé par `plot_learning_curves`. Cependant, les différentes itérations doivent être présente sur chaque graphique, ici les courbes d'entraînement pour chaque learning rate, avec la bonne légende pour chaque graphique."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [],
   "source": [
    "def show_results(results: list) -> None:\n",
    "    plt.figure(figsize=(12, 4))\n",
    "    colors = sns.color_palette(\"husl\", len(results))\n",
    "    for _, result in enumerate(results):\n",
    "        history_df = result[\"history\"]\n",
    "        learning_rate = result[\"learning_rate\"]\n",
    "\n",
    "        plt.subplot(1, 2, 1)\n",
    "        plt.plot(\n",
    "            history_df[\"val_loss\"],\n",
    "            linestyle=\"--\",\n",
    "            color=colors[_]\n",
    "        )\n",
    "        plt.plot(\n",
    "            history_df[\"loss\"], label=f\"LR={learning_rate}\", alpha=0.5,\n",
    "            color=colors[_]\n",
    "        )\n",
    "        plt.xlabel(\"Epochs\")\n",
    "        plt.ylabel(\"Loss\")\n",
    "        plt.legend()\n",
    "\n",
    "        plt.subplot(1, 2, 2)\n",
    "        plt.plot(\n",
    "            history_df[\"val_accuracy\"],\n",
    "            linestyle=\"--\",\n",
    "            color=colors[_]\n",
    "        )\n",
    "        plt.plot(\n",
    "            history_df[\"accuracy\"],\n",
    "            label=f\"LR={learning_rate}\",\n",
    "            alpha=0.5,\n",
    "            color=colors[_]\n",
    "        )\n",
    "        plt.xlabel(\"Epochs\")\n",
    "        plt.ylabel(\"Accuracy\")\n",
    "        plt.legend()\n",
    "\n",
    "    plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Consigne**: Lancer l'entraînement pour plus d'époques afin de comparer avec la fonction `show_results` les différences d'entraînement. Commenter.\n",
    "\n",
    "Pour gagner du temps, on pourra augmenter le batch_size à 256 voire 528. Pour éviter de surcharger l'affichage, on peut utiliser le paramètre *verbose* de la méthode `fit` : s'il vaut 0 alors il n'y a aucun affichage."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 84800 characters]",
      "text/plain": [
       "<Figure size 1200x400 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "show_results(results)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Learning rate: 0.1 - époques: 50\n",
      "Learning rate: 0.01 - époques: 50\n",
      "Learning rate: 0.001 - époques: 50\n"
     ]
    }
   ],
   "source": [
    "n_epochs = 50\n",
    "batch_size = 256\n",
    "learning_rates = [10 ** (-power) for power in range(1, 4)]\n",
    "\n",
    "results = []\n",
    "for learning_rate in learning_rates:\n",
    "    print(f\"Learning rate: {learning_rate} - époques: {n_epochs}\")\n",
    "    model = get_model(learning_rate=learning_rate)\n",
    "    history = model.fit(\n",
    "        X_train,\n",
    "        y_train,\n",
    "        epochs=n_epochs,\n",
    "        batch_size=batch_size,\n",
    "        validation_data=(X_valid, y_valid),\n",
    "        verbose=0,\n",
    "    )\n",
    "    result = {\n",
    "        \"learning_rate\": learning_rate,\n",
    "        \"n_epochs\": n_epochs,\n",
    "        \"history\": pd.DataFrame(history.history),\n",
    "    }\n",
    "    results.append(result)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 73004 characters]",
      "text/plain": [
       "<Figure size 1200x400 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "show_results(results)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Pour continuer\n",
    "\n",
    "Choisir une ou plusieurs pistes de recherche parmi les suivantes. Il est possible de choisir une autre direction, mais elle doit être validé auparavant.\n",
    "\n",
    "1. L'initialisation des réseaux de neurones étant aléatoire, et la mise à jour des poids étant réalisées avec SGD, on ne peut pas considérer un exemple comme une généralité. Reproduire l'étude précédente en lançant plusieurs fois le même modèle pour être capable de générer un graphique avec des intervalles de confiance.\n",
    "2. Nous avons vu en cours que l'initialisation des poids peut avoir un impact fort sur la suite de l'entraînement. En exploitant le paramètre `kernel_initializer` présent dans la définition de la couche [`Dense`](https://keras.io/api/layers/core_layers/dense/), proposer et réaliser une étude pour vérifier ou infirmer cela.\n",
    "3. Les réseaux de neurones peuvent sur-apprendre. Il est important de pouvoir les régulariser. En exploitant le paramètre `kernel_regularizer` présent dans la définition de la couche [`Dense`](https://keras.io/api/layers/core_layers/dense/), proposer une étude pour visualiser son impact sur l'apprentissage."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "studies",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
