{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "1bc6d9e0",
   "metadata": {},
   "source": [
    "# I.2 - Travaux pratiques\n",
    "\n",
    "**L'objectif** de cette première séance de travaux pratiques est de vous faire découvrir les problèmes de classement et les problèmes de régression avec des modèles linéaires et des perceptrons multi-couches (PMC).\n",
    "\n",
    "Références externes utiles :\n",
    "\n",
    "  * [Documentation NumPy](https://docs.scipy.org/doc/numpy/user/index.html)\n",
    "  * [Documentation SciPy](https://docs.scipy.org/doc/scipy/reference/)\n",
    "  * [Documentation MatPlotLib](http://matplotlib.org/)\n",
    "  * [Site scikit-learn](http://scikit-learn.org/stable/index.html)\n",
    "  * [Site langage python](https://www.python.org)\n",
    "\n",
    "<div class=\"admonition note\">\n",
    "\n",
    "**Note**\n",
    "\n",
    "Il est fortement conseillé d'utiliser Python 3 pour ces travaux pratiques, voir [cette page\n",
    "d'installation](http://cedric.cnam.fr/vertigo/Cours/ml/installationScikitLearn.html).\n",
    "Python 2 n'est plus maintenu depuis janvier 2020 et scikit-learn a\n",
    "abandonné son support depuis sklearn 0.20. Certaines fonctionnalités\n",
    "plus récentes pourraient ne pas être disponibles sous Python 2.x.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "04f45744",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Import des bibliothèques utiles\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import sklearn"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f5d0c21f",
   "metadata": {},
   "source": [
    "## Régression avec modèles linéaires et polynomiaux\n",
    "\n",
    "Pour commencer ce premier TP, nous examinerons un problème simple de\n",
    "régression avec une seule variable prédictive, similaire à celui\n",
    "illustré dans le support de cours. À partir d'un ensembles\n",
    "d'observations $\\{(x_1, y_1), (x_2, y_2), \\dots, (x_n, y_n)\\}$,\n",
    "l'objectif est de trouver la fonction $\\hat{f}$ telle que\n",
    "$\\hat{f}(x_i) = y_i$.\n",
    "\n",
    "Pour cet exemple, commençons par générer des données synthétiques. En\n",
    "pratique, nous n'aurions bien entendu pas à réaliser ce travail : le jeu\n",
    "de données est constitué des observations réelles. Notre jeu de données\n",
    "d'exemple consiste en une portion de sinusoïde sur laquelle un bruit\n",
    "gaussien a été appliqué."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5c55d705",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Fixe l'aléas pour pouvoir reproduire les résultats\n",
    "np.random.seed(0)\n",
    "# On tire au hasard 100 points dans l'intervalle [0,2]\n",
    "X = 2 * np.random.rand(100, 1)\n",
    "# On calcule la valeur de sin(x) pour chaque point, plus un bruit gaussien aléatoire\n",
    "y = np.sin(X[:,0]) + 0.15 * np.random.randn(100)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f1d09332",
   "metadata": {},
   "source": [
    "Nous pouvons visualisation toutes les données générées, qui forment\n",
    "notre matrice d'observation pour ce problème de régression :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e9e24ae9",
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(8, 6))\n",
    "plt.scatter(X, y, s=50)\n",
    "plt.xlabel(\"$x$\")\n",
    "plt.ylabel(\"$y$\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c8c527c7",
   "metadata": {},
   "source": [
    "Nous générons maintenant un premier découpage entre données d'apprentissage et données de test. Ce découpage est aléatoire et\n",
    "s'effectue à l'aide de la fonction [train_test_split](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html)\n",
    "de scikit-learn. Le paramètre `test_size` permet de spécifier le pourcentage du jeu de données qui sera contenu dans le jeu de test."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9e84bf1f",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=0)\n",
    "plt.figure(figsize=(8, 6))\n",
    "plt.scatter(X_train, y_train, s=50, edgecolors='none', label=\"Exemples d'entraînement\")\n",
    "plt.scatter(X_test, y_test, c='none', s=50, edgecolors='blue', label=\"Exemples d'évaluation\")\n",
    "plt.legend()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b69b8083",
   "metadata": {},
   "source": [
    "Nous cherchons d'abord un modèle linéaire pour ce problème de régression. Regardez la classe\n",
    "[LinearRegression](http://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html#sklearn.linear_model.LinearRegression)\n",
    "de scikit-learn (ainsi que [ces explications](http://scikit-learn.org/stable/modules/linear_model.html#ordinary-least-squares)).\n",
    "Dans le cas présent, scikit-learn va chercher la fonction affine $f: x \\rightarrow y = ax + b$ en déterminant les paramètres\n",
    "$a$ et $b$ à l'aide de la méthode des moindres carrés.\n",
    "\n",
    "Les résultats sont évalués ici à travers le **coefficient de détermination**, qui est le rapport entre la variance expliquée par le\n",
    "modèle et la variance totale (de la variable expliquée), voir [les explications](https://scikit-learn.org/stable/modules/generated/sklearn.metrics.r2_score.html).\n",
    "\n",
    "Commençons par instancier un modèle de régression linéaire."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c8cdd530",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn import linear_model\n",
    "reg = linear_model.LinearRegression()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1ff3c983",
   "metadata": {},
   "source": [
    "Tous les modèles implémentés dans scikit-learn suivent la même interface et présentent au moins les trois méthodes suivantes :\n",
    "\n",
    "-  `.fit()` permet d'apprendre le modèle en estimant ses paramètres à\n",
    "   partir d'un jeu de données,\n",
    "-  `.predict()` ou `.transform()` permet d'appliquer le modèle à de\n",
    "   nouvelles données,\n",
    "-  `.score()` permet d'évaluer le modèle selon un critère prédéfini\n",
    "   (taux de bonne classification, coefficient de détermination, etc.)\n",
    "   sur un jeu de test.\n",
    "\n",
    "Par exemple, dans notre cas, la méthode `.fit(X,y)` permet de déterminer les paramètres de la régression linéaire à partir de nos\n",
    "observations `X` et de notre vérité terrain `y`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "be422341",
   "metadata": {},
   "outputs": [],
   "source": [
    "# optimisation du modèle: détermination des paramètres de la régression linéaire par la méthode des moindres carrés\n",
    "reg.fit(X_train, y_train)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "66cda2be",
   "metadata": {},
   "source": [
    "Nous pouvons évaluer à quel point le modèle « colle » aux données à l'aide du coefficient de détermination calculé sur les exemples du jeu\n",
    "d'apprentissage :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "61ae48fc",
   "metadata": {},
   "outputs": [],
   "source": [
    "# attention, score() ici ne renvoie pas l'erreur mais la valeur du coefficient de détermination R² !\n",
    "coeff_train = reg.score(X_train, y_train)\n",
    "print(f\"Coefficient de détermination R² en train : {coeff_train:.2f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1c0787e8",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Calculer le coefficient de détermination R² sur le jeu de test. Que constatez-vous ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b044a328",
   "metadata": {},
   "source": [
    "Nous pouvons ensuite, à partir des coefficients qui ont été estimés,\n",
    "tracer notre modèle de régression :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "357485d3",
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(8, 6))\n",
    "plt.scatter(X_train,y_train, s=50, edgecolors='none', label=\"Exemples d'apprentissage\")\n",
    "plt.scatter(X_test,y_test, c='none', s=50, edgecolors='blue', label=\"Exemples d'évaluation\")\n",
    "plt.xlabel(\"$x$\")\n",
    "plt.ylabel(\"$y$\")\n",
    "\n",
    "x_min, x_max = plt.xlim()\n",
    "nx = 100\n",
    "xx = np.linspace(x_min, x_max, nx).reshape(-1,1)\n",
    "plt.plot(xx,reg.predict(xx), color='k', label=\"Régression linéaire\")\n",
    "plt.title(\"Régression linéaire unidimensionnelle\")\n",
    "plt.legend()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f7ba7f78",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Calculez l'erreur quadratique moyenne du modèle sur les données d'apprentissage et ensuite sur les données de test. Vous pouvez l'implémenter vous-même à l'aide de NumPy ou bien consulter la [documentation du module sklearn.metrics](https://scikit-learn.org/stable/modules/classes.html#module-sklearn.metrics).\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "41e3a5b4",
   "metadata": {},
   "source": [
    "### Régression polynomiale\n",
    "\n",
    "La régression linéaire est un outil pratique mais quelque peu limité : les relations entre les variables explicatives et la variable à prédire\n",
    "sont rarement linéaires en pratique ! Nous pourrions chercher dans une famille paramétrique plus grande, par exemple les polynômes à une seule\n",
    "variable de degré $d$ : $P : x \\rightarrow a_0 + a_1 x + a_2 x^2 + \\dots + a_d x^d$.\n",
    "\n",
    "La visualisation interactive ci-dessous permet de visualiser les résultats d'une régression polynomiale appliquée sur notre jeu de\n",
    "données, de tracer la régression et d'afficher les coefficients de détermination R² en apprentissage et en test."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "10d2470f",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.preprocessing import PolynomialFeatures\n",
    "from sklearn import linear_model\n",
    "from sklearn.pipeline import Pipeline\n",
    "\n",
    "from ipywidgets import interact, fixed\n",
    "\n",
    "@interact(degree=(0,10,1), show=fixed(True))\n",
    "def polynomial_fit(degree, show=False):\n",
    "    pipe = Pipeline([('poly', PolynomialFeatures(degree=degree)),\n",
    "                    ('reg', linear_model.LinearRegression())])\n",
    "\n",
    "    pipe.fit(X_train, y_train)\n",
    "    train_score = pipe.score(X_train, y_train)\n",
    "    test_score = pipe.score(X_test, y_test)\n",
    "\n",
    "    if show:\n",
    "        plt.figure(figsize=(8, 6))\n",
    "        plt.scatter(X_train, y_train, s=50, edgecolors='none', label=\"Exemples d'apprentissage\")\n",
    "        plt.scatter(X_test, y_test, c='none', s=50, edgecolors='blue', label=\"Exemples de test\")\n",
    "        x_min, x_max = plt.xlim()\n",
    "        xx = np.linspace(x_min, x_max, nx).reshape(-1,1)\n",
    "        plt.plot(xx,pipe.predict(xx),color='black', label=\"Régression polynomiale\")\n",
    "        plt.legend()\n",
    "        plt.show()\n",
    "\n",
    "        print(f\"Coefficient R² (train): {train_score:.3f}\")\n",
    "        print(f\"Coefficient R² (test): {test_score:.3f}\")\n",
    "    return train_score, test_score"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "292b896c",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Que se passe-t-il lorsque l'on modifie le degré du polynôme ? Quelles observations pouvez-vous faire concernant le sous-apprentissage ? Le sur-apprentissage ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "59a87541",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "*(pour aller plus loin)* Tracer les courbes du coefficient R² sur le jeu d'apprentissage et sur le jeu de test en fonction du degré du polynôme (entre 1 et 10). Vous pouvez utiliser la fonction `polynomial_fit` pour ce faire qui renvoie un tuple contenant les deux scores. Que constatez-vous ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "877b3ba9",
   "metadata": {},
   "source": [
    "## Classement avec modèles linéaires et perceptrons multicouches\n",
    "\n",
    "Voyons maintenant comment les notions de sur-apprentissage et sous-apprentissage peuvent se manifester dans le cas d'un problème de\n",
    "classement (ou classification). Nous examinerons un problème simple de discrimination entre deux classes, similaire à celui illustré dans le\n",
    "support de cours.\n",
    "\n",
    "Nous générons des données à partir de lois normales bidimensionnelles. Pour la première classe nous employons une seule loi avec des variances\n",
    "différentes et une rotation qui introduit des covariances. La seconde classe est un mélange de 4 lois normales avec des centres différents."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b0ef9447",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "# fixer la graine aléatoire de numpy permet d'obtenir systématiquement les mêmes résultats\n",
    "np.random.seed(150)\n",
    "\n",
    "from sklearn import datasets\n",
    "\n",
    "dataset = \"gaussian\"\n",
    "\n",
    "if dataset == \"gaussian\":\n",
    "    # définir matrices de rotation et de dilatation\n",
    "    rot = np.array([[0.94, -0.34], [0.34, 0.94]])\n",
    "    sca = np.array([[3.4, 0], [0, 2]])\n",
    "    # générer données classe 1\n",
    "    c1d = (np.random.randn(100,2)).dot(sca).dot(rot)\n",
    "\n",
    "    # générer données classe 2\n",
    "    c2d1 = np.random.randn(25,2)+[-10, 2]\n",
    "    c2d2 = np.random.randn(25,2)+[-7, -2]\n",
    "    c2d3 = np.random.randn(25,2)+[-2, -6]\n",
    "    c2d4 = np.random.randn(25,2)+[5, -7]\n",
    "\n",
    "    data = np.concatenate((c1d, c2d1, c2d2, c2d3, c2d4))\n",
    "\n",
    "    # générer étiquettes de classe\n",
    "    l1c = np.ones(100, dtype=int)\n",
    "    l2c = np.zeros(100, dtype=int)\n",
    "    labels = np.concatenate((l1c, l2c))\n",
    "elif dataset == \"iris\":\n",
    "    iris_X, iris_y = datasets.load_iris(return_X_y=True)\n",
    "    data = iris_X[:, :2]\n",
    "    labels = iris_y\n",
    "elif dataset == \"wine\":\n",
    "    wine_X, wine_y = datasets.load_wine(return_X_y=True)\n",
    "    data = wine_X[:, (0,10)]\n",
    "    labels = wine_y"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c2251d2d",
   "metadata": {},
   "source": [
    "Visualisation de toutes les données :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7171e9aa",
   "metadata": {},
   "outputs": [],
   "source": [
    "# les échantillons de la classe 1 (label 1) sont en vert (\"green\") 'g', ceux de la classe 2 (label 0) en rouge 'r'\n",
    "cmp = np.array(['r','g', 'b'])\n",
    "plt.figure(figsize=(8, 6))\n",
    "plt.scatter(data[:,0],data[:,1], c=cmp[labels], s=50, edgecolors='none')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f7ef35d9",
   "metadata": {},
   "source": [
    "Nous générons maintenant un premier découpage entre données d'apprentissage et données de test. Les données de test sont affichées\n",
    "avec cercles vides (`c='none'`), les données d'apprentissage avec des cercles remplis."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5b021148",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "# découpage des données en train et test\n",
    "X_train, X_test, y_train, y_test = train_test_split(data, labels, test_size=0.33, random_state=0)\n",
    "\n",
    "plt.figure(figsize=(8, 6))\n",
    "plt.scatter(X_train[:,0],X_train[:,1],c=cmp[y_train], s=50, edgecolors='none', label=\"Exemples d'apprentissage\")\n",
    "plt.scatter(X_test[:,0], X_test[:,1], c='none' ,s=50, edgecolors=cmp[y_test], label=\"Exemples de test\")\n",
    "plt.legend()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8cccb673",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Combien d'échantillons le jeu de données d'apprentissage contient-il ? La [documentation de scikit-learn](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) peut vous aider.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0e510b18",
   "metadata": {},
   "source": [
    "### Modèle linéaire : AFD\n",
    "\n",
    "Nous cherchons d'abord un modèle linéaire pour ce problème de discrimination entre deux classes et utilisons pour cela l'étape\n",
    "décisionnelle de l'analyse factorielle discriminante (AFD). En résumé, cette méthode cherche la frontière de décision linéaire qui sépare au\n",
    "mieux les données en maximisant la variance entre les barycentres des deux groupes. Pour plus de détails, vous pouvez consulter [la\n",
    "documentation de l'AFD dans scikit-learn](http://scikit-learn.org/stable/modules/generated/sklearn.discriminant_analysis.LinearDiscriminantAnalysis.html)\n",
    "ainsi que le chapitre et les travaux pratiques de l'[UE RCP208](https://cedric.cnam.fr/vertigo/Cours/ml/tpAfd.html) portant sur les méthodes factorielles."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0e0986fc",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.discriminant_analysis import LinearDiscriminantAnalysis\n",
    "lda = LinearDiscriminantAnalysis()\n",
    "\n",
    "# évaluation et affichage sur split1\n",
    "lda.fit(X_train, y_train)\n",
    "print(\"Le score sur le jeu d'apprentissage est de : {:.3f}\".format(lda.score(X_train, y_train)))\n",
    "\n",
    "print(\"Le score sur le jeu de test est de : {:.3f}\".format(lda.score(X_test, y_test)))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "edcd3d80",
   "metadata": {},
   "source": [
    "Nous pouvons examiner visuellement le modèle trouvé (la frontière de discrimination linéaire, c'est-à-dire ici une droite dans le plan) :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "04318d03",
   "metadata": {},
   "outputs": [],
   "source": [
    "# on créé une nouvelle figure sur laquelle on affiche les points\n",
    "plt.figure(figsize=(8, 6))\n",
    "plt.scatter(X_train[:,0], X_train[:,1], c=cmp[y_train], s=50, edgecolors='none', label=\"Train\")\n",
    "plt.scatter(X_test[:,0],  X_test[:,1], c='none', s=50, edgecolors=cmp[y_test], label=\"Test\")\n",
    "\n",
    "# on calcule pour chaque point du plan sa probabilité d'appartenir à chaque classe\n",
    "nx, ny = 400, 400\n",
    "x_min, x_max = plt.xlim()\n",
    "y_min, y_max = plt.ylim()\n",
    "# meshgrid permet d'échantillonner tous les points du plan (entre x_min et x_max)\n",
    "xx, yy = np.meshgrid(np.linspace(x_min, x_max, nx),np.linspace(y_min, y_max, ny))\n",
    "# .predict_proba permet de prédire le score de la LDA pour un ensemble d'observations\n",
    "Z = lda.predict_proba(np.c_[xx.ravel(), yy.ravel()])\n",
    "for cls_idx in range(max(labels)):\n",
    "    zz = Z[:, cls_idx].reshape(xx.shape)\n",
    "    # on dessine la frontière correspond à un score de 0,5\n",
    "    # les scores < 0,5 correspondent à la classe 0\n",
    "    # les scores > 0,5 correspondent à la classe 1\n",
    "    plt.contour(xx, yy, zz, [0.5])\n",
    "plt.legend()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d750265e",
   "metadata": {},
   "source": [
    "### Modèle non-linéaire: perceptron multicouche\n",
    "\n",
    "Comme vous avez pu le constater, la frontière de séparation idéale entre les deux groupes n'est pas linéaire. Il nous faut donc changer de\n",
    "famille paramétrique pour en choisir une de plus grande capacité. Nous allons donc utiliser un réseau de neurones simple (un perceptron multicouche) pour réaliser la discrimination binaire. Nous utilisons\n",
    "pour cela la classe [MLPClassifier](http://scikit-learn.org/stable/modules/generated/sklearn.neural_network.MLPClassifier.html#sklearn.neural_network.MLPClassifier)\n",
    "de scikit-learn (voir aussi [ces explications](http://scikit-learn.org/stable/modules/neural_networks_supervised.html#classification) concernant le fonctionnement général de cet objet). Il est très\n",
    "instructif d'examiner [les valeurs par défaut des paramètres](http://scikit-learn.org/stable/modules/generated/sklearn.neural_network.MLPClassifier.html#sklearn.neural_network.MLPClassifier).\n",
    "\n",
    "Les perceptrons multicouches ont été présentés dans [le dernier chapitre de l'UE RCP208](https://cedric.cnam.fr/vertigo/Cours/ml/coursReseauxNeuronesMulticouches.html)\n",
    "portant sur les réseaux de neurones. Il n'est pas indispensable de connaître le fonctionnement précis des réseaux de neurones pour ce TP, ces modèles seront revus par la suite dans la partie du cours portant\n",
    "sur l'apprentissage profond.\n",
    "\n",
    "Les trois paramètres importants que nous allons manipuler sont le coefficient d'oubli, le nombre de neurones par couche et le nombre de couches. Par défaut, scikit-learn instancie un réseau de neurones à une\n",
    "seule couche cachée contenant 100 neurones.\n",
    "\n",
    "Nous utilisons d'abord un coefficient « d'oubli » (*weight decay*) `alpha = 1e-5`. Ce terme correspond à l'intensité de la régularisation L2 appliquée sur le perceptron. Pour rappel, l'objectif de la\n",
    "régularisation est de réduire la capacité du modèle."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "652a13de",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.neural_network import MLPClassifier\n",
    "# nous utilisons ici l'algorithme L-BFGS pour optimiser le perceptron\n",
    "clf = MLPClassifier(solver='lbfgs', alpha=1e-5, max_iter=1000, random_state=42)\n",
    "\n",
    "# évaluation et affichage sur split1\n",
    "clf.fit(X_train, y_train)\n",
    "train_score = clf.score(X_train, y_train)\n",
    "print(f\"Le score en train est {train_score:.3f}\")\n",
    "\n",
    "test_score = clf.score(X_test, y_test)\n",
    "print(f\"Le score en test est {test_score:.3f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "22ae668a",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "À l'aide de la documentation de scikit-learn, déterminez à quoi correspond la valeur renvoyée par la méthode `.score()`.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "61eee269",
   "metadata": {},
   "source": [
    "Comme précédemment pour l'analyse factorielle discriminante, nous pouvons désormais tracer la frontière de décision du perceptron multicouche que nous venons d'optimiser."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "58061229",
   "metadata": {},
   "outputs": [],
   "source": [
    "# créer une nouvelle figure\n",
    "plt.figure()\n",
    "# afficher les nuages de points apprentissage (remplis) et de test (vides)\n",
    "plt.scatter(X_train[:,0],X_train[:,1],c=cmp[y_train],s=50, edgecolors='none', label=\"Train\")\n",
    "plt.scatter(X_test[:,0], X_test[:,1], c='none', s=50, edgecolors=cmp[y_test], label=\"Test\")\n",
    "\n",
    "# calculer la probabilité de sortie du perceptron pour tous les points du plan\n",
    "nx, ny = 200, 200\n",
    "x_min, x_max = plt.xlim()\n",
    "y_min, y_max = plt.ylim()\n",
    "xx, yy = np.meshgrid(np.linspace(x_min, x_max, nx),np.linspace(y_min, y_max, ny))\n",
    "Z = clf.predict_proba(np.c_[xx.ravel(), yy.ravel()])\n",
    "for cls_idx in range(max(labels)):\n",
    "    zz = Z[:, cls_idx].reshape(xx.shape)\n",
    "    # on dessine la frontière correspond à un score de 0,5\n",
    "    # les scores < 0,5 correspondent à la classe 0\n",
    "    # les scores > 0,5 correspondent à la classe 1\n",
    "    plt.contour(xx, yy, zz, [0.5])\n",
    "plt.legend()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4d668ab2",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Refaites l'expérience avec `alpha = 1`. Dans quel cas la régularisation est plus forte ? Quelle est la conséquence sur les résultats ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e1ffa1cf",
   "metadata": {},
   "source": [
    "La visualisation interactive ci-dessous permet de manipuler simultanément les trois paramètres importants que nous avons décrit précédemment : coefficient d'oubli, nombre de couches, nombre de\n",
    "neurones par couche. Dans le perceptron, les paramètres à optimiser sont les poids des connexions entre les neurones. Plus il y a de couches et plus il y a de neurones dans une couche, plus le nombre de connexions est élevé (et par conséquent plus il y a de paramètres)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cadcda3e",
   "metadata": {},
   "outputs": [],
   "source": [
    "import ipywidgets as widgets\n",
    "\n",
    "@interact(alpha=widgets.FloatLogSlider(value=1, base=10, min=-5, max=3, step=1),\n",
    "          neurons=(10, 200, 10),\n",
    "          layers=(1, 4, 1))\n",
    "def mlp_fit(alpha, neurons, layers):\n",
    "    # nous utilisons ici l'algorithme L-BFGS pour optimiser le perceptron\n",
    "    layer_sizes = (neurons,) * layers\n",
    "    clf = MLPClassifier(solver='lbfgs', alpha=alpha, hidden_layer_sizes=layer_sizes, max_iter=1000, tol=1e-3, random_state=0)\n",
    "\n",
    "    # évaluation et affichage sur split1\n",
    "    clf.fit(X_train, y_train)\n",
    "    train_score = clf.score(X_train, y_train)\n",
    "    print(f\"Le score en train est {train_score:.3f}\")\n",
    "\n",
    "    test_score = clf.score(X_test, y_test)\n",
    "    print(f\"Le score en test est {test_score:.3f}\")\n",
    "\n",
    "    # on créé une nouvelle figure sur laquelle on affiche les points\n",
    "    plt.figure(figsize=(8, 6))\n",
    "    plt.scatter(X_train[:,0], X_train[:,1], c=cmp[y_train], s=50, edgecolors='none', label=\"Train\")\n",
    "    plt.scatter(X_test[:,0],  X_test[:,1], c='none', s=50, edgecolors=cmp[y_test], label=\"Test\")\n",
    "\n",
    "    # on calcule pour chaque point du plan sa probabilité d'appartenir à chaque classe\n",
    "    nx, ny = 400, 400\n",
    "    x_min, x_max = plt.xlim()\n",
    "    y_min, y_max = plt.ylim()\n",
    "    # meshgrid permet d'échantillonner tous les points du plan (entre x_min et x_max)\n",
    "    xx, yy = np.meshgrid(np.linspace(x_min, x_max, nx),np.linspace(y_min, y_max, ny))\n",
    "    # .predict_proba permet de prédire le score de chaque classe pour un ensemble d'observations\n",
    "    Z = clf.predict_proba(np.c_[xx.ravel(), yy.ravel()])\n",
    "    for cls_idx in range(max(labels)):\n",
    "        zz = Z[:, cls_idx].reshape(xx.shape)\n",
    "        # on dessine la frontière correspond à un score de 0,5\n",
    "        # les scores < 0,5 correspondent à la classe 0\n",
    "        # les scores > 0,5 correspondent à la classe 1\n",
    "        plt.contour(xx, yy, zz, [0.5])\n",
    "    plt.legend()\n",
    "    plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5ddd70c5",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Expérimentez avec la visualisation ci-dessus. Que se passe-t-il lorsque le nombre de couches augmente ? Lorsque le nombre de neurones augmente ? Comment pouvez-vous interpréter ces résultats du point de vue de la capacité du modèle, du sur-apprentissage et du sous-apprentissage ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "aff3918e",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Générez d'autres découpages apprentissage/test en réexécutant la fonction `train_test_split` avec une autre valeur de graine aléatoire (`random_state=`). Examinez la variabilité des résultats lorsque ce découpage change. Dans quel cas elle est plus forte ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "361c44f9",
   "metadata": {},
   "source": [
    "## Pour aller plus loin\n",
    "\n",
    "Reprenez la deuxième partie du TP sur le problème de discrimination à l'aide d'un modèle linéaire (AFD) et d'un modèle non-linéaire (PMC) mais\n",
    "en changeant le jeu de données. Vous pouvez modifier la variable `dataset` par `\"wine\"` ou `\"iris\"` pour utiliser respectivement le\n",
    "*Wine Dateset* ou les Iris de Fisher.\n",
    "\n",
    "Le jeu de données *Wine Dataset* est une collection de 178 vins italiens répartis en 3 catégories, chacune correspondant à un vignoble différent.\n",
    "Treize mesures physico-chimiques sont disponibles mais dans ce TP nous n'utilisons que les deux premières (taux d'alcool et la couleur du vin).\n",
    "Plus de détails [ici](https://scikit-learn.org/stable/datasets/toy_dataset.html#wine-dataset).\n",
    "\n",
    "Les Iris de Fisher est un jeu de données célèbre en classification automatique. Il comporte 150 échantillons répartis en trois classes de\n",
    "fleurs (Iris Setosa, Iris Versicolour et Iris Virginica). Pour chaque plante, quatre caractéristiques végétales ont été mesurés : longueur\n",
    "sépale, largeur sépale, longueur de pétale, largeur de pétale. Dans ce TP nous n'utilisons que la longueur et la largeur sépale. Plus\n",
    "d'informations sur Iris [ici](https://scikit-learn.org/stable/datasets/toy_dataset.html#iris-dataset)."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
