{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "8076c7d0",
   "metadata": {},
   "source": [
    "# XI.2 - Travaux pratiques\n",
    "\n",
    "L'objectif de cette séance de travaux pratiques est d'étendre le modèle de régression logistique du TP précédent à un réseau de neurones plus complexe. Précédemment, nous avons implémenté à la main une régression logistique, c'est-à-dire le plus simple des réseaux de neurones linéaires. Pour cette séance, nous allons implémenter un perceptron multi-couches (*multi-layer perceptron* ou MLP). Contrairement à la régression logistique, qui se limite à des séparateurs linéaires, le perceptron permet d'apprendre des frontières de décisions non-linéaires. En outre, la famille des perceptrons est un approximateur universels des fonctions continues. Autrement dit, il s'agit d'un modèle très expressif, à la base de l'apprentissage profond.\n",
    "\n",
    "Nous allons voir dans un premier temps comment implémenter la phase *forward* (passe avant) pour produire des prédictions, puis la phase *forward* (passe arrière) pour l'optimisation d'un perceptron à une seule couche cachée.\n",
    "\n",
    "Commençons par charger à nouveau le jeu de données MNIST :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6a4a31e9",
   "metadata": {},
   "outputs": [],
   "source": [
    "from keras.datasets import mnist\n",
    "# Import de MNIST depuis Keras\n",
    "(X_train, y_train), (X_test, y_test) = mnist.load_data()\n",
    "# Transformation des images 28x28 en vecteur de dimension 784\n",
    "X_train = X_train.reshape(60000, 784).astype('float32')\n",
    "X_test = X_test.reshape(10000, 784).astype('float32')\n",
    "# Normalisation entre 0 et 1\n",
    "X_train /= 255\n",
    "X_test /= 255"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fa6aa42b",
   "metadata": {},
   "source": [
    "## Prédiction avec un perceptron (*forward*)\n",
    "\n",
    "L'architecture du perceptron à une seule couche cachée est illustrée dans la figure ci-dessous.\n",
    "\n",
    "Comme la dernière fois, on considère les données de la base MNIST. Chaque image est représentée par un vecteur de taille $28^2=784$. Le perceptron applique différentes opérations mathématiques pour transformer l'entrée et produire la prédiction finale, c'est-à-dire la catégorie de l'image :\n",
    "\n",
    "1. Une projection linéaire, qui va projeter chaque image sur un vecteur de dimensions $(1, L)$. $L$ représente ici la largeur (le nombre de neurones) de la couche cachée du perceptron, par exemple $L=100$. En considérant que chaque exemple $\\mathbf{x_i}$ est un vecteur ligne $(1,784)$, la projection linéaire est représentée par une matrice $\\mathbf{W^h}$ $(784, L)$ et un vecteur de biais $\\mathbf{b^h}$ $(1, L)$. La projection s'écrit:\n",
    "\n",
    "   $$\n",
    "   \\mathbf{u_i} = \\mathbf{x_i} \\mathbf{W^h} + \\mathbf{b^h}.\n",
    "   $$\n",
    "\n",
    "2. L'application d'une fonction de transfert non-linéaire, par exemple une sigmoïde :\n",
    "\n",
    "   $$\n",
    "   \\forall j \\in \\left\\lbrace 1; L \\right\\rbrace, ~ h_{i,j} = \\frac{1}{1+\\exp(-u_{i,j})}.\n",
    "   $$\n",
    "\n",
    "3. Une deuxième projection linéaire, qui va projeter la représentation interne (les activations de la couche cachée) de dimensions $(1,L)$ en un vecteur de $(1, K)$, avec $K$ le nombre de classes considérées (ici, 10). $K$ représente le nombre de neurones en sortie, c'est-à-dire la dimensionalité du vecteur prédit. Cette opération de projection linéaire est représentée par la matrice $\\mathbf{W^y}$ de dimensions $(L, K)$ et le vecteur de biais $\\mathbf{b^y}$ de dimensions $(1, K)$). Matriciellement, la projection est représentée par l'opération :\n",
    "\n",
    "   $$\n",
    "   \\mathbf{v_i} =\\mathbf{h_i} \\mathbf{W^y} + \\mathbf{b^y}.\n",
    "   $$\n",
    "\n",
    "4. Enfin, l'application d'une non-linéarité *softmax*. Comme pour la régression logistique, cela permet de transformer les activations de sortie en probabilités pour une distribution catégorielle :\n",
    "\n",
    "   $$\n",
    "   \\forall j \\in \\left\\lbrace 1; K \\right\\rbrace ~ y_{i,j} = \\frac{\\exp(v_{i,j})}{\\sum\\limits_{k=1}^K \\exp(v_{i,k})}.\n",
    "   $$\n",
    "\n",
    "Notre objectif pour cette séance va être d'implémenter un perceptron (et son apprentissage) sur la base MNIST. Commençons par transformer les étiquettes en vecteur encodé au format *one-hot*:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4e317dea",
   "metadata": {},
   "outputs": [],
   "source": [
    "from keras.utils import to_categorical\n",
    "n_classes = 10\n",
    "# Conversion des étiquettes au format one-hot\n",
    "Y_train = to_categorical(y_train, n_classes)\n",
    "Y_test = to_categorical(y_test, n_classes)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f324ac32",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "En reprenant le squelette du code de la régression logistique, compléter le code ci-dessous pour implémenter la *forward pass* (phase de prédiction) du perceptron multi-couche.\n",
    "\n",
    "Vous devrez notamment écrire une fonction `forward(batch, Wh, bh, Wy, by)` qui renvoie la prédiction $\\hat{\\mathbf{y}}$ ainsi que la matrice des activations de la couche cachée. Si l'on considère un batch des données de taille $n \\times 784$, les paramètres $\\mathbf{W^h}$ ($784\\times L$), $\\mathbf{b^h}$ ($1\\times L$), $\\mathbf{W^y}$ ($L\\times K$) et $\\mathbf{b^y}$ ($1\\times K$), la fonction `forward` renvoie :\n",
    "\n",
    "- la prédiction $\\mathbf{\\hat{Y}}$ sur le batch ($n\\times K$),\n",
    "- la matrice $\\mathbf{H}$ des activations de la couche cachée ($n\\times L$),\n",
    "\n",
    "pour un batch de $n$ exemples.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8acb6d44",
   "metadata": {
    "tags": [
     "remove-cell"
    ]
   },
   "outputs": [],
   "source": [
    "def forward(batch, Wh, bh, Wy, by):\n",
    "    \"\"\" Entrées:\n",
    "        - batch: un batch de n images de MNIST au format vecteur (n, 784)\n",
    "        - Wh: une matrice de poids entrée -> couche cachée\n",
    "        - bh: un vecteur de biais pour la couche cachée\n",
    "        - Wy: une matrice de poids couche cachée -> sortie\n",
    "        - by: un vecteur de biais pour la sortie\n",
    "\n",
    "        Renvoie:\n",
    "        - Y_pred: prédictions de sortie\n",
    "        - H: activations de la couche cachée\n",
    "    \"\"\"\n",
    "    # À compléter\n",
    "    ...\n",
    "    return Y_pred, H"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3324c22d",
   "metadata": {},
   "source": [
    "## Apprentissage du perceptron (*backward*)\n",
    "\n",
    "Comme pour la régression logistique, nous allons entraîner le perceptron à l'aide de l'algorithme de descente de gradient. Pour calculer les gradients par rapport aux paramètres de la couche cachée, nous allons avoir besoin d'utiliser l'algorithme de rétro-propagation du gradient (*backpropagation*). Rappellons que pour chaque batch d'exemples, l'algorithme effectue une passe `forward` (implémentée ci-dessus) qui permet de calculer la prédiction du perceptron pour les exemples du batch.\n",
    "\n",
    "La fonction de coût considérée sera encore l'entropie croisée entre la sortie prédite et les étiquettes de supervision. On calculera alors le gradient de l'erreur par rapport à tous les paramètres du modèle, c'est-à-dire:\n",
    "\n",
    "- $\\mathbf{W^y}$ (dimensions $(L, K)$),\n",
    "- $\\mathbf{b^y}$ (dimensions $(1, K)$),\n",
    "- $\\mathbf{W^h}$ (dimensions $(784, L)$),\n",
    "- $\\mathbf{b^h}$ (dimensions $(1, L)$).\n",
    "\n",
    "On rappelle ci-dessous les équations des gradients, effectuées depuis la sortie vers l'entrée du réseau :\n",
    "\n",
    "1. Mise à jour de $\\mathbf{W^y}$ et $\\mathbf{b^y}$:\n",
    "\n",
    "$$\n",
    "\\frac{\\partial \\mathcal{L}}{\\partial \\mathbf{v_i}} = \\mathbf{\\delta^y_i} = \\mathbf{\\hat{y_i}} - \\mathbf{y_i^*}\n",
    "$$\n",
    "\n",
    "$$\n",
    "\\frac{\\partial \\mathcal{L}}{\\partial \\mathbf{W^y}} = \\frac{1}{n} \\mathbf{H}^T (\\mathbf{\\hat{Y}} - \\mathbf{Y^*}) = \\frac{1}{n} \\mathbf{H}^T \\mathbf{\\Delta^y} \\tag{1}\n",
    "$$\n",
    "\n",
    "$$\n",
    "\\frac{\\partial \\mathcal{L}}{\\partial \\mathbf{b^y}} = \\frac{1}{n}\\sum_{i=1}^{n}(\\mathbf{\\hat{y_i}} - \\mathbf{y_i^*}) \\tag{2}\n",
    "$$\n",
    "\n",
    "où $\\mathbf{H}$ est la matrice des couches cachées sur le batch ($784 \\times L$), $\\mathbf{\\hat{Y}}$ est la matrice des prédictions sur le batch (taille $n \\times K$), $\\mathbf{Y^*}$ est la matrice des étiquettes issues de la supervision (*ground truth*, $n \\times K$) et $\\mathbf{\\Delta^y}=\\mathbf{\\hat{Y}}-\\mathbf{Y^*}$.\n",
    "\n",
    "2. Mise à jour de $\\mathbf{W^h}$ et $\\mathbf{b^h}$:\n",
    "\n",
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Montrer que\n",
    "\n",
    "$$\n",
    "\\frac{\\partial \\mathcal{L}}{\\partial \\mathbf{u_i}} = \\mathbf{\\delta^h_i} = \\mathbf{\\delta^y_i} \\mathbf{W^{y}}^T   \\odot \\sigma^{'}(\\mathbf{u_i}) = \\mathbf{\\delta^y_i} \\mathbf{W^{y}}^T \\odot (\\mathbf{h_i} \\odot (1-\\mathbf{h_i}))\n",
    "$$\n",
    "\n",
    "</div>\n",
    "\n",
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "En déduire que les gradients de $\\mathcal{L}$ par rapport à $\\mathbf{W^h}$ et $\\mathbf{b^h}$ s'écrivent matriciellement :\n",
    "\n",
    "$$\n",
    "\\frac{\\partial \\mathcal{L}}{\\partial \\mathbf{W^h}} =  \\frac{1}{n} \\mathbf{X}^T \\mathbf{\\Delta^h}\n",
    "~~~\\text{et}~~~\n",
    "\\frac{\\partial \\mathcal{L}}{\\partial \\mathbf{b^h}} = \\frac{1}{n}\\sum_{i=1}^{n}(\\delta^h_i)\n",
    "$$\n",
    "\n",
    "où $\\mathbf{X}$ est la matrice des données sur le batch ($n \\times 784$) et $\\mathbf{\\Delta^h}$ est la matrice des $\\delta^h_i$ sur le batch ($n \\times L$).\n",
    "\n",
    "</div>\n",
    "\n",
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Compléter la fonction `backward` ci-dessous qui calcule et renvoie les gradients de l'erreur par rapport aux paramètres du perceptron.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f68ac148",
   "metadata": {
    "tags": [
     "remove-cell"
    ]
   },
   "outputs": [],
   "source": [
    "def backward(Y_pred, Y, X, H, Wy):\n",
    "    \"\"\" Entrées:\n",
    "        - Y_pred: batch de vecteur des prédictions (one-hot)\n",
    "        - Y: batch de vecteur des étiquettes (one-hot)\n",
    "        - X: batch d'images (au format vectoriel (n, 784))\n",
    "        - H: matrice des activations cachées\n",
    "        - Wy: matrice des poids\n",
    "\n",
    "        Renvoie:\n",
    "        - gradWy: gradient de l'erreur (entropie croisée) par rapport à Wy\n",
    "        - gradby: gradient de l'erreur (entropie croisée) par rapport à by\n",
    "        - gradWh: gradient de l'erreur (entropie croisée) par rapport à Wh\n",
    "        - gradbh: gradient de l'erreur (entropie croisée) par rapport à bh\n",
    "    \"\"\"\n",
    "    # À compléter\n",
    "    ...\n",
    "    return gradWy, gradby, gradWh, gradbh"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "182b62c2",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "La fonction de coût de l'Eq. (CE) est-elle convexe par rapport aux paramètres $\\mathbf{W}$, $\\mathbf{b}$ du modèle ? Avec un pas de gradient bien choisi, peut-on assurer la convergence vers le minimum global de la solution ?\n",
    "\n",
    "</div>\n",
    "\n",
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Compléter le code ci-dessous de sorte à appliquer la descente de gradient sur le perceptron multi-couche défini par les paramètres `Wy, Wh, by, bh`.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "73182955",
   "metadata": {
    "tags": [
     "remove-cell"
    ]
   },
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "N, d = X_train.shape # N exemples, dimension d\n",
    "hidden_size = 100 # Nombre de neurones de la couche cachée\n",
    "# Initialisation des poids et des biais\n",
    "Wy = np.zeros((hidden_size, n_classes))\n",
    "Wh = np.zeros((d, hidden_size))\n",
    "by = np.zeros((1, n_classes))\n",
    "bh = np.zeros((1, hidden_size))\n",
    "\n",
    "n_epochs = 20 # Nombre d'epochs de la descente de gradient\n",
    "eta = 1e-1 # Learning rate (pas d'apprentissage)\n",
    "batch_size = 100 # Taille du lot\n",
    "n_batches = int(float(N) / batch_size)\n",
    "\n",
    "# Allocation des matrices pour stocker les valeurs des gradients\n",
    "gradWy = np.zeros((hidden_size, n_classes))\n",
    "gradWh = np.zeros((d, hidden_size))\n",
    "gradby = np.zeros((1, n_classes))\n",
    "gradbh = np.zeros((1, hidden_size))\n",
    "\n",
    "for epoch in range(n_epochs):\n",
    "    for batch_idx in range(n_batches):\n",
    "        # ********* À compléter **********\n",
    "        # FORWARD PASS : calculer la prédiction y à partir des paramètres courants pour les images du batch\n",
    "        # BACKWARD PASS :\n",
    "        # 1) calculer les gradients de l'erreur par rapport à W et b\n",
    "        # 2) mettre à jour les paramètres W et b selon la descente de gradient\n",
    "        ..."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bae030da",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Tester en initialisant les matrices à 0 comme pour la régression logistique. Quelles performances obtenez-vous ? Que peut-on en conclure ?\n",
    "\n",
    "</div>\n",
    "\n",
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Tester deux autres initialisations :\n",
    "\n",
    "- initialiser les poids avec une loi normale de moyenne nulle et d'écart type à déterminer, par exemple $10^{-1}$,\n",
    "- initialiser les poids selon la règle de Xavier Glorot [GB10], qui divise la valeur de la gaussienne par $\\sqrt{n_i}$, où $n_i$ est le nombre de neurones en entrée de la couche.\n",
    "\n",
    "</div>\n",
    "\n",
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Évaluer les performances du modèle en classification. Vous devriez obtenir un score d'environ 98% (*accuracy*) sur la base de test pour ce réseau de neurones à une couche cachée.\n",
    "\n",
    "</div>\n",
    "\n",
    "[GB10] Xavier Glorot and Yoshua Bengio. Understanding the difficulty of training deep feedforward neural networks. In *In Proceedings of the International Conference on Artificial Intelligence and Statistics (AISTATS'10). Society for Artificial Intelligence and Statistics*. 2010."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
