TP-reseaux-profond/TP0.ipynb
2023-06-22 20:35:38 +02:00

1223 lines
47 KiB
Plaintext
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"nbformat": 4,
"nbformat_minor": 0,
"metadata": {
"coursera": {
"course_slug": "nlp-sequence-models",
"graded_item_id": "xxuVc",
"launcher_item_id": "X20PE"
},
"kernelspec": {
"display_name": "Python 3",
"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.7.6"
},
"toc": {
"nav_menu": {},
"number_sections": true,
"sideBar": true,
"skip_h1_title": false,
"toc_cell": true,
"toc_position": {},
"toc_section_display": "block",
"toc_window_display": false
},
"colab": {
"name": "TP Réseaux de Neurones avec Numpy - Sujet.ipynb",
"provenance": [],
"collapsed_sections": [],
"toc_visible": true
}
},
"cells": [
{
"cell_type": "markdown",
"source": [
"# Construire et entraîner un perceptron multi-couches - étape par étape\n",
"\n",
"Dans ce TP, vous allez mettre en œuvre l'entraînement d'un réseau de neurones (perceptron multi-couches) à l'aide de la librairie **numpy**. Pour cela nous allons procéder par étapes successives. Dans un premier temps nous allons traiter le cas d'un perceptron mono-couche, en commençant par la passe *forward* de prédiction d'une sortie à partir d'une entrée et des paramètres du perceptron, puis en implémentant la passe *backward* de calcul des gradients de la fonction objectif par rapport aux paramètrès. A partir de là, nous pourrons tester l'entraînement à l'aide de la descente de gradient stochastique.\n",
"\n",
"Une fois ces étapes achevées, nous pourrons nous atteler à la construction d'un perceptron multi-couches, qui consistera pour l'essentiel en la composition de perceptrons mono-couche. \n",
"\n",
"Dans ce qui suit, nous adoptons les conventions de notation suivantes : \n",
"\n",
"- $(x, y)$ désignent un couple donnée/label de la base d'apprentissage ; $\\hat{y}$ désigne quant à lui la prédiction du modèle sur la donnée $x$.\n",
"\n",
"- L'indice $i$ indique la $i^{\\text{ème}}$ dimension d'un vecteur.\n",
"\n",
"- L'exposant $[l]$ désigne un objet associé à la $l^{\\text{ème}}$ couche.\n",
"\n",
"- L'exposant $(k)$ désigne un objet associé au $k^{\\text{ème}}$ exemple. \n",
" \n",
"Exemple: \n",
"- $a^{(2)[3]}_5$ indique la 5ème dimension du vecteur d'activation du 2ème exemple d'entraînement (2), de la 3ème couche [3].\n",
"\n",
"\n",
"Commençons par importer tous les modules nécessaires : "
],
"metadata": {
"id": "5b3pjAUEk2LQ"
}
},
{
"cell_type": "code",
"source": [
"import numpy as np\n",
"import math\n",
"import matplotlib.pyplot as plt \n",
"\n",
"from sklearn.model_selection import train_test_split\n",
"from sklearn import datasets"
],
"metadata": {
"id": "R6LBs_NLla1a"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"# Perceptron mono-couche\n"
],
"metadata": {
"id": "3JZIXefJlXSV"
}
},
{
"cell_type": "markdown",
"source": [
"### Perceptron mono-couche - passe *forward*\n",
"\n",
"Un perceptron mono-couche est un modèle liant une couche d'entrée (en vert, qui n'effectue pas d'opération) à une couche de sortie. Les neurones des deux couches sont connectés par des liaisons pondérées (les poids synaptiques) $W_{xy}$, et les neurones de la couche de sortie portent chacun un biais additif $b_y$. Enfin, une fonction d'activation $f$ est appliquée à l'issue de ces opérations pour obtenir la prédiction du réseau $\\hat{y}$. \n",
"\n",
"On a donc :\n",
"\n",
"$$\\hat{y} = f ( W_{xy} x + b_y )$$ \n",
"\n",
"On posera pour la suite :\n",
"$$ z = W_{xy} x + b_y $$\n",
"\n",
"La figure montre une représentation de ces opérations sous forme de réseau de neurones (à gauche), mais aussi sous une forme fonctionnelle (à droite) qui permet de bien visualiser l'ordre des opérations.\n",
"\n",
"<img src=\"https://drive.google.com/uc?id=1RZeiaKue0GLXJr3HRtKkuP6GD8r6I1_Q\" height=300>\n",
"<img src=\"https://drive.google.com/uc?id=1dnQ6SSdpEX1GDTgoNTrUwA3xjiP9rTYU\" height=250> \n",
"\n",
"\n",
"Notez que les paramètres du perceptron, que nous allons ajuster par un processus d'optimisation, sont donc les poids synaptiques $W_{xy}$ et les biais $b_y$. Par commodité dans le code, nous considérerons également comme un paramètre le choix de la fonction d'activation.\n",
"\n",
"**Remarque importante** : En pratique, on traite souvent les données par *batch*, c'est-à-dire que les prédictions sont faites pour plusieurs données simultanément. Ici pour une taille de *batch* de $m$, cela signifie en fait que :\n",
" \n",
"$$ x \\in \\mathbb{R}^{4 \\times m} \\text{ et } y \\in \\mathbb{R}^{5 \\times m}$$ \n"
],
"metadata": {
"id": "azdcz3QV_k-r"
}
},
{
"cell_type": "markdown",
"source": [
"Complétez la fonction *dense_layer_forward* qui calcule la prédiction d'un perceptron mono-couche pour une entrée $x$. "
],
"metadata": {
"id": "RBtX2euQDSCS"
}
},
{
"cell_type": "code",
"source": [
"def dense_layer_forward(x, Wxy, by, activation):\n",
" \"\"\"\n",
" Réalise une unique étape forward de la couche dense telle que décrite dans la figure précédente\n",
"\n",
" Arguments:\n",
" x -- l'entrée, tableau numpy de dimension (n_x, m).\n",
" Wxy -- Matrice de poids multipliant l'entrée, tableau numpy de shape (n_y, n_x)\n",
" by -- Biais additif ajouté à la sortie, tableau numpy de dimension (n_y, 1)\n",
" activation -- Chaîne de caractère désignant la fonction d'activation choisie : 'linear', 'sigmoid' ou 'relu'\n",
"\n",
" Retourne :\n",
" y_pred -- prédiction, tableau numpy de dimension (n_y, m)\n",
" cache -- tuple des valeurs utiles pour la passe backward (rétropropagation du gradient), contient (x, z)\n",
" \"\"\"\n",
" \n",
" \n",
" \n",
" ### A COMPLETER \n",
" # calcul de z\n",
" z = ...\n",
" # calcul de la sortie en appliquant la fonction d'activation\n",
" if activation == 'relu':\n",
" y_pred = ...\n",
" elif activation == 'sigmoid':\n",
" y_pred = ...\n",
" elif activation == 'linear':\n",
" y_pred = ...\n",
" else:\n",
" print(\"Erreur : la fonction d'activation n'est pas implémentée.\")\n",
" \n",
" ### FIN\n",
"\n",
" # sauvegarde du cache pour la passe backward\n",
" cache = (x, z)\n",
" \n",
" return y_pred, cache"
],
"metadata": {
"id": "YGYbWrRfmIwx"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"Exécutez les lignes suivantes pour vérifier la validité de votre code :"
],
"metadata": {
"id": "1dCFTHOqD_Tp"
}
},
{
"cell_type": "code",
"source": [
"np.random.seed(1)\n",
"x_tmp = np.random.randn(3,10)\n",
"Wxy = np.random.randn(2,3)\n",
"by = np.random.randn(2,1)\n",
"activation = 'relu'\n",
"y_pred_tmp, cache_tmp = dense_layer_forward(x_tmp, Wxy, by, activation)\n",
"\n",
"\n",
"print(\"y_pred.shape = \\n\", y_pred_tmp.shape)\n",
"\n",
"print('----------------------------')\n",
"\n",
"print(\"y_pred[1] =\\n\", y_pred_tmp[1])\n",
"\n",
"print('----------------------------')\n",
"\n",
"activation = 'sigmoid'\n",
"\n",
"y_pred_tmp, cache_tmp = dense_layer_forward(x_tmp, Wxy, by, activation)\n",
"print(\"y_pred[1] =\\n\", y_pred_tmp[1])\n",
"\n",
"print('----------------------------')\n",
"\n",
"activation = 'linear'\n",
"\n",
"y_pred_tmp, cache_tmp = dense_layer_forward(x_tmp, Wxy, by, activation)\n",
"print(\"y_pred[1] =\\n\", y_pred_tmp[1])\n"
],
"metadata": {
"id": "B6wlVU37on1k"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"**Affichage attendu**: \n",
"```Python\n",
"y_pred.shape = \n",
" (2, 10)\n",
"----------------------------\n",
"y_pred[1] =\n",
" [0. 2.11983968 0.88583246 1.39272594 0. 2.92664609\n",
" 0. 1.47890228 0. 0.04725575]\n",
"----------------------------\n",
"y_pred[1] =\n",
" [0.10851642 0.89281659 0.70802939 0.80102707 0.21934644 0.94914804\n",
" 0.24545321 0.81440672 0.48495927 0.51181174]\n",
"----------------------------\n",
"y_pred[1] =\n",
" [-2.10598556 2.11983968 0.88583246 1.39272594 -1.26947904 2.92664609\n",
" -1.12301093 1.47890228 -0.06018107 0.04725575]\n",
"\n",
"```"
],
"metadata": {
"id": "YYbiDw8TptiN"
}
},
{
"cell_type": "markdown",
"source": [
"### Perceptron mono-couche - passe *backward*\n",
"\n",
"Dans les librairies d'apprentissage profond actuelles, il suffit d'implémenter la passe *forward*, et la passe *backward* est réalisée automatiquement, avec le calcul des gradients (différentiation automatique) et la mise à jour des paramètres. Il est cependant intéressant de comprendre comment fonctionne la passe *backward*, en l'implémentant sur un exemple simple.\n",
"\n",
"<img src=\"https://drive.google.com/uc?id=1MC8Nxu6BQnpB7cGLwunIbgx9s1FaGw81\" height=350> \n",
"\n",
"Il faut calculer les dérivées de la fonction objectif par rapport aux différents paramètres, pour ensuite mettre à jour ces derniers pendant la descente de gradient. Les équations de calcul des gradients sont données ci-dessous (c'est un bon exercice que de les calculer à la main). \n",
"\n",
"\\begin{align}\n",
"\\displaystyle {dW_{xy}} &= \\frac{\\partial J}{\\partial W_{xy}} &= (d\\hat{y} * \\frac{\\partial \\hat{y}}{\\partial z}) . x^{T}\\tag{1} \\\\[8pt]\n",
"\\displaystyle db_{y} &= \\frac{\\partial J}{\\partial b_y} &= \\sum_{batch}(d\\hat{y} * \\frac{\\partial \\hat{y}}{\\partial z})\\tag{2} \\\\[8pt]\n",
"\\displaystyle dx &= \\frac{\\partial J}{\\partial x} &= { W_{xy}}^T . (d\\hat{y} * \\frac{\\partial \\hat{y}}{\\partial z}) \\tag{3} \\\\[8pt]\n",
"\\end{align}\n",
"\n",
"\n",
"Ici, $*$ indique une multiplication élément par élément tandis que l'absence de symbole indique une multiplication matricielle. Par ailleurs $d\\hat{y}$ désigne $\\frac{\\partial J}{\\partial \\hat{y}}$, $dW_{xy}$ désigne $\\frac{\\partial J}{\\partial W_{xy}}$, $db_y$ désigne $\\frac{\\partial J}{\\partial b_y}$ et $dx$ désigne $\\frac{\\partial J}{\\partial x}$ (ces noms ont été choisis pour être utilisables dans le code).\n",
"\n",
"Il vous reste à déterminer, par vous même, le terme $\\frac{\\partial \\hat{y}}{\\partial z}$, qui constitue en fait la dérivée de la fonction d'activation évaluée en $z$. Par exemple, pour la fonction d'activation linéaire (l'identité), la dérivée est égale à 1 pour tout $z$. A vous de déterminer, et d'implémenter, la dérivée des fonctions *sigmoid* et *relu*.\n"
],
"metadata": {
"id": "GypgZ8jBqooR"
}
},
{
"cell_type": "code",
"source": [
"def dense_layer_backward(dy_hat, Wxy, by, activation, cache):\n",
" \"\"\"\n",
" Implémente la passe backward de la couche dense.\n",
"\n",
" Arguments :\n",
" dy_hat -- Gradient de la fonction objectif par rapport à la sortie ŷ\n",
" cache -- dictionnaire python contenant des variables utiles (issu de dense_layer_forward())\n",
"\n",
" Retourne :\n",
" gradients -- dictionnaire python contenant les gradients suivants :\n",
" dx -- Gradient de la fonction objectif par rapport aux entrées, de dimension (n_x, m)\n",
" dby -- Gradient de la fonction objectif par rapport aux biais, de dimension (n_y, 1)\n",
" dWxy -- Gradient de la fonction objectif par rapport aux poids synaptiques Wxy, de dimension (n_y, n_x)\n",
" \"\"\"\n",
" \n",
" # Récupérer les informations du cache\n",
" (x, z) = cache\n",
" \n",
" ### A COMPLETER \n",
" # calcul de la sortie en appliquant l'activation\n",
" if activation == 'relu':\n",
" dyhat_dz = ...\n",
" elif activation == 'sigmoid':\n",
" dyhat_dz = ...\n",
" elif activation == 'linear':\n",
" dyhat_dz = ...\n",
" else:\n",
" print(\"Erreur : la fonction d'activation n'est pas implémentée.\")\n",
"\n",
" # calculer le gradient de la perte par rapport à x\n",
" dx = ...\n",
"\n",
" # calculer le gradient de la perte par rapport à Wxy\n",
" dWxy = ...\n",
"\n",
" # calculer le gradient de la perte par rapport à by \n",
" dby = ...\n",
"\n",
" ### FIN\n",
" \n",
" # Stocker les gradients dans un dictionnaire\n",
" gradients = {\"dx\": dx, \"dby\": dby, \"dWxy\": dWxy}\n",
" \n",
" return gradients"
],
"metadata": {
"id": "wEi_y3W_rCMc"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"On peut maintenant créer une classe *DenseLayer*, qui comprend en attribut toutes les informations nécessaires à la description d'une couche dense, c'est-à-dire : \n",
"\n",
"\n",
"* Le nombre de neurones en entrée de la couche dense (input_size)\n",
"* Le nombre de neurones en sortie de la couche dense (output_size)\n",
"* La fonction d'activation choisie sur cette couche (activation)\n",
"* Les poids synaptiques de la couche dense, stockés dans une matrice de taille (output_size, input_size) (Wxy)\n",
"* Les biais de la couche dense, stockés dans un vecteur de taille (output_size, 1) (by)\n",
"\n",
"On ajoute également un attribut cache qui permettra de stocker les entrées de la couche dense (x) ainsi que les calculs intermédiaires (z) réalisés lors de la passe *forward*, afin d'être réutilisés pour la basse *backward*.\n",
"\n",
"A vous de compléter les 4 jalons suivants : \n",
"\n",
"* **L'initialisation des paramètres** Wxy et by : Wxy doit être positionnée suivant [l'initialisation de Glorot](https://www.tensorflow.org/api_docs/python/tf/keras/initializers/GlorotUniform), et by est initialisée par un vecteur de zéros.\n",
"* **La fonction *forward***, qui consiste simplement en un appel de la fonction *dense_layer_forward* implémentée précédemment.\n",
"* **La fonction *backward***, qui consiste simplement en un appel de la fonction *dense_layer_backward* implémentée précédemment.\n",
"* Et enfin **la fonction *update_parameters*** qui applique la mise à jour de la descente de gradient en fonction d'un taux d'apprentissage (*learning_rate*) et des gradients calculés dans la passe *forward*.\n"
],
"metadata": {
"id": "E5KeDgyO-ZPJ"
}
},
{
"cell_type": "code",
"source": [
"class DenseLayer:\n",
" def __init__(self, input_size, output_size, activation):\n",
" self.input_size = input_size\n",
" self.output_size = output_size\n",
" self.activation = activation\n",
" self.cache = None # Le cache sera mis à jour lors de la passe forward\n",
" ### A COMPLETER\n",
" self.Wxy = ...\n",
" self.by = ...\n",
"\n",
" def forward(self, x_batch):\n",
"\n",
" y, cache = dense_layer_forward(...)\n",
" self.cache = cache\n",
" return y\n",
"\n",
" def backward(self, dy_hat):\n",
" return dense_layer_backward(...)\n",
"\n",
" def update_parameters(self, gradients, learning_rate):\n",
" self.Wxy = ...\n",
" self.by = ...\n",
" ### FIN"
],
"metadata": {
"id": "u2K9dp1IL3yM"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"### Fonction de coût : erreur quadratique moyenne"
],
"metadata": {
"id": "9GlEB8K3Lani"
}
},
{
"cell_type": "markdown",
"source": [
"Pour entraîner notre modèle, nous devons mettre en place un optimiseur. Nous implémenterons la descente de gradient stochastique avec mini-batch. Il nous faut cependant au préalable implanter la fonction de coût que nous utiliserons pour évaluer la qualité de nos prédictions. \n",
"\n",
"Pour le moment, nous allons nous contenter d'une erreur quadratique moyenne, qui associée à une fonction d'activation linéaire (l'identité) permet de résoudre les problèmes de régression. \n",
"\n",
"La fonction de coût prend en entrée deux paramètres : la vérité-terrain *y_true* et la prédiction du modèle *y_pred*. Ces deux matrices sont de dimension $bs \\times output\\text{_}size$. La fonction retourne deux grandeurs : *loss* qui correspond à l'erreur quadratique moyenne des prédictions par rapport aux vérités-terrains, et $d\\hat{y}$ au gradient de l'erreur quadratique moyenne par rapport aux prédictions. Autrement dit : \n",
"$$ d\\hat{y} = \\frac{\\partial J_{mb}}{\\partial \\hat{y}}$$\n",
"\n",
"où $\\hat{y}$ correspond à *y_pred*, et $J_{mb}$ à la fonction objectif calculée sur un mini-batch $mb$ de données."
],
"metadata": {
"id": "2KMcQzlskdI1"
}
},
{
"cell_type": "code",
"source": [
"### A COMPLETER\n",
"def mean_square_error(y_true, y_pred):\n",
" loss = ...\n",
" dy_hat = ...\n",
" return loss, dy_hat"
],
"metadata": {
"id": "FRDUnhJma6jf"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"### Descente de gradient stochastique"
],
"metadata": {
"id": "uZRnPbBjQvZc"
}
},
{
"cell_type": "markdown",
"source": [
"La descente de gradient stochastique prend en entrée les paramètres suivants : \n",
"* *x_train* et *y_train* respectivement les données et labels de l'ensemble d'apprentissage (que l'on suppose de taille $N$).\n",
"* *model* une instance du modèle que l'on veut entraîner (qui doit implanter les 3 fonctions vues précédemment *forward*, *backward* et *update_parameters*).\n",
"* *loss_function* peut prendre deux valeurs : 'mse' (erreur quadratique moyenne) ou 'bce' (entropie croisée binaire, que nous implémenterons par la suite).\n",
"* *learning_rate* le taux d'apprentissage choisi pour la descente de gradient.\n",
"* *epochs* le nombre de parcours complets de l'ensemble d'apprentissage que l'on veut réaliser.\n",
"* *batch_size* la taille de mini-batch désirée pour la descente de gradient stochastique. \n",
"\n",
"L'algorithme à implémenter est rappelé ci-dessous : \n",
"```\n",
"N_batch = floor(N/batch_size)\n",
"\n",
"Répéter epochs fois\n",
"\n",
" Pour b de 1 à N_batch Faire\n",
"\n",
" - Sélectionner les données x_train_batch et labels y_train_batch du b-ème mini-batch\n",
" - Calculer la prédiction y_pred_batch du modèle pour ce mini-batch\n",
" - Calculer la perte batch_loss et le gradient de la perte batch_grad par rapport aux prédictions sur ce mini-batch\n",
" - Calculer les gradients de la perte par rapport à chaque paramètre du modèle\n",
" - Mettre à jour les paramètres du modèle \n",
"\n",
" Fin Pour\n",
"\n",
"Fin Répéter\n",
"\n",
"```\n",
"Deux remarques additionnelles : \n",
"1. A chaque *epoch*, les *mini-batches* doivent être différents (les données doivent être réparties dans différents *mini-batches*).\n",
"2. Il est intéressant de calculer (et d'afficher !) la perte moyennée sur l'ensemble d'apprentissage à chaque *epoch*. Pour cela, on peut accumuler les pertes de chaque *mini-batch* sur une *epoch* et diviser l'ensemble par le nombre de *mini-batches*."
],
"metadata": {
"id": "w2XnUBj2n-Df"
}
},
{
"cell_type": "code",
"source": [
"def SGD(x_train, y_train, model, loss_function, learning_rate=0.03, epochs=10, batch_size=10):\n",
" # Nombre de batches par epoch\n",
" nb_batches = math.floor(x_train.shape[0] / batch_size)\n",
"\n",
" # Pour gérer le tirage aléatoire des batches parmi les données d'entraînement... \n",
" indices = np.arange(x_train.shape[0])\n",
"\n",
" for e in range(epochs):\n",
"\n",
" running_loss = 0\n",
"\n",
" # Nouvelle permutation des indices pour la prochaine epoch\n",
" indices = np.random.permutation(indices)\n",
"\n",
" for b in range(nb_batches):\n",
"\n",
" # Sélection des données du batch courant\n",
" x_train_batch = x_train[indices[b*batch_size:(b+1)*batch_size]]\n",
" y_train_batch = y_train[indices[b*batch_size:(b+1)*batch_size]]\n",
"\n",
" ### A COMPLETER\n",
" # Prédiction du modèle pour le batch courant\n",
" y_pred_batch = ...\n",
"\n",
" # Calcul de la fonction objectif et de son gradient sur le batch courant\n",
" if loss_function == 'mse':\n",
" batch_loss, batch_dy_hat = mean_square_error(...)\n",
" elif loss_function == 'bce':\n",
" batch_loss, batch_dy_hat = binary_cross_entropy(...)\n",
"\n",
" running_loss += batch_loss \n",
"\n",
" # Calcul du gradient de la perte par rapport aux paramètres du modèle\n",
" param_updates = ...\n",
"\n",
" # Mise à jour des paramètres du modèle\n",
" model.update_parameters(...)\n",
" ### FIN\n",
"\n",
" print(f\"Epoch {e:4d} : Loss {running_loss/nb_batches:.4f}\")\n",
" \n",
" \n",
" return model\n"
],
"metadata": {
"id": "lk3lypUOLXbv"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"### Test sur un problème de régression "
],
"metadata": {
"id": "9bybDhHivjXq"
}
},
{
"cell_type": "markdown",
"source": [
"Le bloc de code suivant permet de générer et d'afficher un ensemble de données pour un problème de régression linéaire classique. "
],
"metadata": {
"id": "N7q44eS0vrrZ"
}
},
{
"cell_type": "code",
"source": [
"x, y = datasets.make_regression(n_samples=250, n_features=1, n_targets=1, random_state=1, noise=10)\n",
"\n",
"plt.plot(x, y, 'b.', label='Ensemble d\\'apprentissage')\n",
"\n",
"plt.legend()\n",
"plt.show()"
],
"metadata": {
"id": "nGcIVuALraDG"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"A vous de déterminer le nombre de neurones à positionner en entrée et en sortie du perceptron monocouche pour résoudre ce problème. Une fois ceci fait, le code ci-après affiche également la prédiction de votre modèle."
],
"metadata": {
"id": "q7lfdRFMRFZH"
}
},
{
"cell_type": "code",
"source": [
"### A COMPLETER\n",
"model = DenseLayer(..., ..., ...)\n",
"model = SGD(x, y, model, ..., learning_rate=0.1, epochs=10, batch_size=20)\n",
"### FIN\n",
"\n",
"plt.plot(x, y, 'b.', label='Ensemble d\\'apprentissage')\n",
"\n",
"x_gen = np.expand_dims(np.linspace(-3, 3, 10), 1)\n",
"y_gen = np.transpose(model.forward(np.transpose(x_gen)))\n",
"\n",
"plt.plot(x_gen, y_gen, 'g-', label='Prédiction du modèle')\n",
"plt.legend()\n",
"plt.show()"
],
"metadata": {
"id": "GKFJ3c2MmomL"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"### Test sur un problème de classification binaire"
],
"metadata": {
"id": "mA9-6PqLwff4"
}
},
{
"cell_type": "markdown",
"source": [
"Afin de pouvoir tester notre perceptron mono-couche sur un problème de classification binaire (i.e. effectuer une régression logistique), il est d'abord nécessaire d'implémenter l'entropie croisée binaire.\n",
"\n",
"Rappel : \n",
"$$ bce(y, \\hat{y}) = -y log(\\hat{y}) - (1-y) log(1-\\hat{y}) $$"
],
"metadata": {
"id": "K9AHAgGBwjro"
}
},
{
"cell_type": "code",
"source": [
"### A COMPLETER\n",
"def binary_cross_entropy(y_true, y_pred):\n",
" loss = ...\n",
" grad = ...\n",
"\n",
" return loss, grad"
],
"metadata": {
"id": "_xCXP-pQb2oL"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"Le bloc de code suivant permet de générer et d'afficher un ensemble de données pour un problème de classification binaire classique. "
],
"metadata": {
"id": "0L3pPIpfSVU7"
}
},
{
"cell_type": "code",
"source": [
"from sklearn.model_selection import train_test_split\n",
"from sklearn import datasets\n",
"import matplotlib.pyplot as plt \n",
"\n",
"\n",
"x, y = datasets.make_blobs(n_samples=250, n_features=2, centers=2, center_box=(- 3, 3), random_state=1)\n",
"\n",
"plt.plot(x[y==0,0], x[y==0,1], 'b.')\n",
"plt.plot(x[y==1,0], x[y==1,1], 'r.')\n",
"\n",
"plt.show()"
],
"metadata": {
"id": "4AxQRaegdntx"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"A nouveau, vous devez déterminer le nombre de neurones à positionner en entrée et en sortie du perceptron monocouche pour résoudre ce problème. Une fois ceci fait, le code ci-après affiche également la prédiction de votre modèle."
],
"metadata": {
"id": "X7o-u0kcSk_l"
}
},
{
"cell_type": "code",
"source": [
"### A COMPLETER\n",
"model = DenseLayer(..., ..., ...)\n",
"model = SGD(x, y, model, ..., learning_rate=0.3, epochs=50, batch_size=20)\n",
"### FIN\n",
"\n",
"plt.plot(x[y==0,0], x[y==0,1], 'b.')\n",
"plt.plot(x[y==1,0], x[y==1,1], 'r.')\n",
"\n",
"x1_gen = np.linspace(-6, 2, 10)\n",
"x2_gen = -model.Wxy[0,0]*x1_gen/model.Wxy[0,1] - model.by[0,0]/model.Wxy[0,1]\n",
"\n",
"plt.plot(x1_gen, x2_gen, 'g-')\n",
"\n",
"plt.show()"
],
"metadata": {
"id": "TdyntT9zSrum"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"## Test sur un problème de classification binaire plus complexe"
],
"metadata": {
"id": "Ypq84RCl0bnI"
}
},
{
"cell_type": "markdown",
"source": [
"Testons maintenant un problème de classification plus complexe : "
],
"metadata": {
"id": "6OPzEofrSrSF"
}
},
{
"cell_type": "code",
"source": [
"x, y = datasets.make_gaussian_quantiles(n_samples=250, n_features=2, n_classes=2, random_state=1)\n",
"\n",
"plt.plot(x[y==0,0], x[y==0,1], 'r.')\n",
"plt.plot(x[y==1,0], x[y==1,1], 'b.')\n",
"\n",
"plt.show()"
],
"metadata": {
"id": "_IQdphRV0hsB"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"Le code ci-dessous vous permettra d'afficher la frontière de décision établie par votre modèle :"
],
"metadata": {
"id": "8Ol3eqKGSyC5"
}
},
{
"cell_type": "code",
"source": [
"def print_decision_boundaries(model, x, y):\n",
" dx, dy = 0.1, 0.1\n",
" y_grid, x_grid = np.mgrid[slice(-4, 4 + dy, dy),\n",
" slice(-4, 4 + dx, dx)]\n",
"\n",
"\n",
" x_gen = np.concatenate((np.expand_dims(np.reshape(y_grid, (-1)),1),np.expand_dims(np.reshape(x_grid, (-1)),1)), axis=1)\n",
" z_gen = model.forward(np.transpose(x_gen)).reshape(x_grid.shape)\n",
"\n",
" z_min, z_max = 0, 1\n",
"\n",
" c = plt.pcolor(x_grid, y_grid, z_gen, cmap='RdBu', vmin=z_min, vmax=z_max)\n",
" plt.colorbar(c)\n",
" plt.plot(x[y==0,0], x[y==0,1], 'r.')\n",
" plt.plot(x[y==1,0], x[y==1,1], 'b.')\n",
" plt.show()"
],
"metadata": {
"id": "lN8d7YK76MBm"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"Complétez le code ci-dessous :"
],
"metadata": {
"id": "SRNifc8KS_MM"
}
},
{
"cell_type": "code",
"source": [
"### A COMPLETER\n",
"model = DenseLayer(..., ..., ...)\n",
"model = SGD(x, y, model, ..., learning_rate=0.3, epochs=50, batch_size=20)\n",
"### FIN\n",
"\n",
"print_decision_boundaries(model, x, y)"
],
"metadata": {
"id": "E9WV-Az70mR6"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"Cette fois-ci il n'est pas possible de faire résoudre un problème aussi \"complexe\" à notre simple perceptron monocouche. Nous allons pour cela devoir passer au perceptron multi-couches !"
],
"metadata": {
"id": "J9jMU_YcTAJl"
}
},
{
"cell_type": "markdown",
"source": [
"---"
],
"metadata": {
"id": "yiGyXLvum0uI"
}
},
{
"cell_type": "markdown",
"source": [
"# Perceptron multi-couches"
],
"metadata": {
"id": "HIEVrFXkDdMD"
}
},
{
"cell_type": "markdown",
"source": [
"## Implémentation du perceptron multi-couches"
],
"metadata": {
"id": "6ZWNGM7vVlCb"
}
},
{
"cell_type": "markdown",
"source": [
"A partir du perceptron mono-couche créé précédemment, nous pouvons maintenant implémenter un perceptron multi-couches, qui est un véritable réseau de neurones dans la mesure où il met en jeu plusieurs couches de neurones successives. **Concrètement, le perceptron multi-couches est une composition de perceptron monocouches**, chacun prenant en entrée l'activation de sortie de la couche précédente. Prenons l'exemple ci-dessous : \n",
"\n",
"<img src=\"https://drive.google.com/uc?id=1ILboVqVVwy71lqAwM3ZGm6umCQegvmuV\" height=350> \n",
"\n",
"\n",
"Ce perceptron multi-couches est la composition de deux perceptrons monocouches, le premier liant deux neurones d'entrée à deux neurones de sortie, et le second deux neurones d'entrée à un neurone de sortie.\n",
"\n",
"<img src=\"https://drive.google.com/uc?id=1hyrrsf8ZpqUcy2_T89HbQX7fpmqtbNwa\" height=350> \n",
"\n",
"Voici comment nous l'implémenterons : le perceptron multi-couches consiste simplement en une liste de perceptrons monocouches (*DenseLayer*). A l'initialisation, le perceptron multi-couches est une liste vide, dans laquelle il est possible d'ajouter des couches denses (fonction *add_layer()*). \n",
"\n",
"```python\n",
"model = MultiLayerPerceptron()\n",
"model.add_layer(DenseLayer(2, 2, 'relu'))\n",
"model.add_layer(DenseLayer(2, 1, 'sigmoid'))\n",
"```\n",
"\n",
"La fonction *forward()* du perceptron multi-couches consiste en le calcul successif de la sortie des couches denses. Chaque couche dense effectue une prédiction sur la sortie de la couche dense précédente.\n",
"\n",
"La fonction *backward()* implémente l'algorithme de rétro-propagation du gradient. Les gradients des paramètres de la dernière couche sont calculés en premier, et sont utilisés pour calculer les gradients de la couche précédente, comme illustré sur cette figure.\n",
"\n",
"<img src=\"https://drive.google.com/uc?id=1KVH0DWbAwT7R6-XmpqmpWob1jqftqC84\" height=350> "
],
"metadata": {
"id": "1a6VuuWODu8G"
}
},
{
"cell_type": "code",
"source": [
"class MultiLayerPerceptron:\n",
" def __init__(self):\n",
" # Initialisation de la liste de couches du perceptron multi-couches à la liste vide\n",
" self.layers = []\n",
"\n",
" # Fonction permettant d'ajouter la couche passée en paramètre dans la liste de couches\n",
" # du perceptron multi-couches\n",
" def add_layer(self, layer):\n",
" self.layers.append(layer)\n",
"\n",
" # Fonction réalisant la prédiction du perceptron multi-couches :\n",
" # Elle consiste en la prédiction successive de chacune des couches de la liste de couches,\n",
" # chacune prenant en entrée la prédiction de la couche précédente\n",
" def forward(self, x_batch):\n",
" \n",
" for i in range(len(self.layers)):\n",
" ### A COMPLETER\n",
"\n",
" return ...\n",
"\n",
" # Fonction de calcul des gradients de la fonction objectif par rapport à chaque paramètre \n",
" # du perceptron multi-couches\n",
" # L'entrée dy_hat correspond au gradient de la fonction objectif par rapport à la prédiction\n",
" # finale du perceptron multi-couches (notée dJ/dŷ sur la figure précédente)\n",
" # Cette fonction doit implémenter la rétropropagation du gradient : on parcourt la liste des\n",
" # couches en sens inverse (fonction reversed) et le gradient de la fonction objectif par rapport \n",
" # à l'entrée d'une couche est utilisé pour calculer les gradients de la couche précédente\n",
" # \n",
" # Cette fonction retourne une liste de dictionnaires de gradients, de même dimension que le nombre\n",
" # de couches\n",
" def backward(self, dy_hat):\n",
" gradients = []\n",
" for i in reversed(range(len(self.layers))):\n",
" ### A COMPLETER\n",
"\n",
" return gradients\n",
"\n",
" # Fonction de mise à jour des paramètres en fonction des gradients établis dans la \n",
" # fonction backward et d'un taux d'apprentissage\n",
" def update_parameters(self, gradients, learning_rate):\n",
" for i in range(len(self.layers)):\n",
" ### A COMPLETER"
],
"metadata": {
"id": "RNhqq0KXm4Jd"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"## Test sur le problème simple de classification binaire"
],
"metadata": {
"id": "GyIW025tVcPR"
}
},
{
"cell_type": "markdown",
"source": [
"Vous pouvez maintenant tester votre perceptron multi-couches sur le problème précédent. Deux couches suffisent pour résoudre le problème !"
],
"metadata": {
"id": "JEg5-Z7mVEWd"
}
},
{
"cell_type": "code",
"source": [
"x, y = datasets.make_gaussian_quantiles(n_samples=250, n_features=2, n_classes=2, random_state=1)\n",
"\n",
"plt.plot(x[y==0,0], x[y==0,1], 'r.')\n",
"plt.plot(x[y==1,0], x[y==1,1], 'b.')\n",
"\n",
"plt.show()"
],
"metadata": {
"id": "pijGm1ipwrAw"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"model = MultiLayerPerceptron()\n",
"model.add_layer(DenseLayer(2, 10, 'relu'))\n",
"model.add_layer(DenseLayer(10, 1, 'sigmoid'))\n",
"\n",
"model = SGD(x, y, model, 'bce', learning_rate=0.3, epochs=60, batch_size=20)\n",
"\n",
"print_decision_boundaries(model, x, y)"
],
"metadata": {
"id": "h3He5gXmxQ1j"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"# Quelques exercices supplémentaires"
],
"metadata": {
"id": "SMTeraduVplm"
}
},
{
"cell_type": "markdown",
"source": [
"## Evanescence du gradient"
],
"metadata": {
"id": "46K0mq5bVvT1"
}
},
{
"cell_type": "markdown",
"source": [
"Testez le réseau suivant sur le problème simple de classification binaire évoqué dans la partie précédente :\n",
"```python\n",
"model.add_layer(DenseLayer(2, 10, 'sigmoid'))\n",
"model.add_layer(DenseLayer(10, 10, 'sigmoid'))\n",
"model.add_layer(DenseLayer(10, 10, 'sigmoid'))\n",
"model.add_layer(DenseLayer(10, 10, 'sigmoid'))\n",
"model.add_layer(DenseLayer(10, 1, 'sigmoid'))\n",
"```\n",
"\n",
" \n",
"\n",
"1. Qu'observez-vous ?\n",
"2. Comment résoudre ce problème ?\n",
"\n",
"\n"
],
"metadata": {
"id": "pVBCGX9iVzdL"
}
},
{
"cell_type": "markdown",
"source": [
"## Application à un problème de classification d'image\n"
],
"metadata": {
"id": "YBChCCJREOuP"
}
},
{
"cell_type": "markdown",
"source": [
"Le code ci-dessous vous permet de charger l'ensemble de données CIFAR-10 qui regroupe des imagettes de taille $32 \\times 32$ représentant 10 types d'objets différents. \n",
"\n",
"Des images de chat et de chien sont extraites de cet ensemble : à vous de mettre en place un perceptron multi-couches de classification binaire pour apprendre à reconnaître un chien d'un chat dans une image."
],
"metadata": {
"id": "C7efDmj6WNSg"
}
},
{
"cell_type": "code",
"source": [
"import tensorflow as tf\n",
"\n",
"# Récupération des données\n",
"(x_train, y_train), (x_test, y_test) = tf.keras.datasets.cifar10.load_data()\n",
"\n",
"# La base de données CIFAR contient des images issues de 10 classes :\n",
"# 0\tairplane\n",
"# 1\tautomobile\n",
"# 2\tbird\n",
"# 3\tcat\n",
"# 4\tdeer\n",
"# 5\tdog\n",
"# 6\tfrog\n",
"# 7\thorse\n",
"# 8\tship\n",
"# 9\ttruck\n",
"\n",
"# Préparation des données pour la classification binaire : \n",
"\n",
"# Extraction des images des classes de chat et chien\n",
"indices_train = np.squeeze(y_train)\n",
"x_cat_train = x_train[indices_train==3,:]\n",
"x_dog_train = x_train[indices_train==5,:]\n",
"\n",
"indices_test = np.squeeze(y_test)\n",
"x_cat_test = x_test[indices_test==3,:]\n",
"x_dog_test = x_test[indices_test==5,:]\n",
"\n",
"# Création des données d'apprentissage et de test\n",
"# Les images sont redimensionnées en vecteurs de dimension 3072 (32*32*3)\n",
"# On assigne 0 à la classe chat et 1 à la classe chien\n",
"x_train = np.concatenate((np.resize(x_cat_train[0:250],(250, 32*32*3)), np.resize(x_dog_train[0:250],(250, 32*32*3))), axis=0)\n",
"y_train = np.concatenate((np.zeros((250)), np.ones((250))),axis=0)\n",
"\n",
"x_test = np.concatenate((np.resize(x_cat_test,(1000, 32*32*3)), np.resize(x_dog_test,(1000, 32*32*3))), axis=0)\n",
"y_test = np.concatenate((np.zeros((1000)), np.ones((1000))),axis=0)\n",
"\n",
"# Normalisation des entrées\n",
"x_train = x_train/255\n",
"x_test = x_test/255"
],
"metadata": {
"id": "ZFyeFRYfEN3A"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"# A COMPLETER\n",
"model = MultiLayerPerceptron()\n",
"model.add_layer(DenseLayer(..., ..., ...))\n",
"...\n",
"# A vous de tester le nombre de couches qui vous semble adéquat\n",
"\n",
"model = SGD(x_train, y_train, model, ..., learning_rate=0.03, epochs=10, batch_size=10)"
],
"metadata": {
"id": "VBzhs000JbHT"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"# Prédiction du modèle sur les données de test\n",
"y_pred_test = model.forward(np.transpose(x_test))\n",
"\n",
"# Calcul de la précision : un écart inférieur à 0.5 entre prédiction et label\n",
"# est considéré comme bonne prédiction\n",
"prediction_eval = np.where(np.abs(y_pred_test-y_test)<0.5, 1, 0)\n",
"overall_test_precision = 100*np.sum(prediction_eval)/y_test.shape[0]\n",
"print(f\"Précision de {overall_test_precision:2.1f} %\")"
],
"metadata": {
"id": "hPUcXM60L0-b"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"Si vous obtenez une précision supérieure à 50%, votre réseau est meilleur qu'une prédiction aléatoire, ce qui est déjà bien ! Notez qu'ici nous avons circonscrit l'ensemble d'apprentissage à 500 échantillons (250 de chaque classe) car les calculs de produit matriciel sont longs. C'est tout l'intérêt de porter les calculs sur GPU ou TPU, des dispositifs matériels spécialement conçus et optimisés pour paralléliser ces calculs."
],
"metadata": {
"id": "A1jASzh3PSKa"
}
},
{
"cell_type": "markdown",
"source": [
"## Utilisation de la librairie Keras"
],
"metadata": {
"id": "YV4WZTfL0KB9"
}
},
{
"cell_type": "markdown",
"source": [
"L'utilisation d'une librairie comme Keras permet d'abstraire toutes les difficultés présentées dans ce TP : voici par exemple comment résoudre grâce à Keras le premier problème de régression linéaire présenté dans ce TP."
],
"metadata": {
"id": "XFR3jwelW1jh"
}
},
{
"cell_type": "code",
"source": [
"x, y = datasets.make_regression(n_samples=250, n_features=1, n_targets=1, random_state=1, noise=10)\n",
"\n",
"plt.plot(x, y, 'b.', label='Ensemble d\\'apprentissage')\n",
"\n",
"plt.legend()\n",
"plt.show()"
],
"metadata": {
"id": "ew3_k9uK0P9g"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"from tensorflow.keras.models import Sequential\n",
"from tensorflow.keras.layers import Dense\n",
"\n",
"model = Sequential()\n",
"model.add(Dense(1, activation='linear', input_dim=1)) # input_dim indique la dimension de la couche d'entrée, ici 1\n",
"\n",
"model.summary() # affiche un résumé du modèle"
],
"metadata": {
"id": "jBQYiUU-XX9a"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"from tensorflow.keras import optimizers\n",
"\n",
"sgd = optimizers.SGD(learning_rate=0.1) # On choisit la descente de gradient stochastique, avec un taux d'apprentssage de 0.1\n",
"\n",
"# On définit ici, pour le modèle introduit plus tôt, l'optimiseur choisi, la fonction de perte (ici\n",
"# l'erreur quadratique moyenne pour un problème de régression) et les métriques que l'on veut observer pendant\n",
"# l'entraînement. L'erreur absolue moyenne (MAE) est un indicateur plus simple à interpréter que la MSE.\n",
"model.compile(optimizer=sgd,\n",
" loss='mean_squared_error',\n",
" metrics=['mae'])\n",
"\n",
"# Entraînement du modèle avec des mini-batchs de taille 20, sur 10 epochs. \n",
"# Le paramètre validation_split signifie qu'on tire aléatoirement une partie des données\n",
"# (ici 20%) pour servir d'ensemble de validation\n",
"history = model.fit(x, y, validation_split=0.2, epochs=10, batch_size=20)\n"
],
"metadata": {
"id": "S0Vqoo26Xfe3"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"plt.plot(x, y, 'b.', label='Ensemble d\\'apprentissage')\n",
"\n",
"x_gen = np.expand_dims(np.linspace(-3, 3, 10), 1)\n",
"y_gen = model.predict(x_gen)\n",
"\n",
"plt.plot(x_gen, y_gen, 'g-', label='Prédiction du modèle')\n",
"plt.legend()\n",
"plt.show()"
],
"metadata": {
"id": "46LiNDvGYQdK"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"S'il vous reste du temps, reprenez les différents problèmes définis précédemment et utilisez la librairie Keras pour les résoudre."
],
"metadata": {
"id": "kHu5v6lUYqTm"
}
}
]
}