{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "ab064842",
   "metadata": {},
   "source": [
    "# II.2 - Travaux pratiques\n",
    "\n",
    "**L'objectif** de cette séance de travaux pratiques est de montrer l'utilisation des techniques de validation croisée pour l'évaluation et la comparaison de modèles décisionnels, ainsi que des méthodes de recherche de valeurs pour les hyper-paramètres (comme le coefficient de régularisation).\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",
    "## Estimation des performances par validation croisée\n",
    "\n",
    "Afin d'illustrer l'utilisation de la validation croisée, nous considérons un problème de classement similaire à celui examiné lors de la précédente séance. Nous considérons un jeu de données pour lequel nous disposons de l'information de supervision (les étiquettes des classes) et nous le partitionnons en un ensemble d'apprentissage et un ensemble de test. Nous utiliserons les [Iris de Fisher](https://fr.wikipedia.org/wiki/Iris_de_Fisher) comme jeu de données. Pour rappel, celui-ci consiste à identifier à quelle espèce appartient une plante parmi trois possibilités (*Iris Setosa*, *Iris Virginica* et *Iris Versicolor*) à partir de la longueur et la largeur des sépales et des pétales.\n",
    "\n",
    "Nous employons des perceptrons multi-couches (PMC) avec une seule couche cachée de 100 neurones et une valeur $\\alpha = 1$ pour la constante de régularisation (pondération du terme d'oubli ou *weight decay*). Il n'est pas indispensable pour cette séance de savoir en détails comment fonctionne ces réseaux de neurones, ces modèles décisionnels seront revus plus tard dans le cours.\n",
    "\n",
    "La validation croisée sera utilisée pour estimer les performances de généralisation *à partir de l'ensemble d'apprentissage*. Ensuite, cette estimation sera comparée à l'estimation obtenue sur l'ensemble de test que nous avions mis de côté au départ. Les explications sur la validation croisée et sa mise en œuvre dans Scikit-learn se trouvent [dans la documentation](http://scikit-learn.org/stable/modules/cross_validation.html)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1db487c2",
   "metadata": {},
   "outputs": [],
   "source": [
    "# importations\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "import sklearn\n",
    "\n",
    "# Graine aléatoire, pour la reproductibilité (découpage train/test, initialisation des PMC)\n",
    "np.random.seed(0)\n",
    "\n",
    "# chargement du jeu de données Iris\n",
    "from sklearn import datasets\n",
    "iris = datasets.load_iris()\n",
    "data, labels = iris.data, iris.target\n",
    "\n",
    "# découpage initial en données d'apprentissage et données de test\n",
    "from sklearn.model_selection import train_test_split\n",
    "X_train, X_test, y_train, y_test = train_test_split(data, labels, test_size=0.5)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9f7d7293",
   "metadata": {},
   "source": [
    "La fonction `train_test_split` de scikit-learn nous permet de diviser aléatoirement le jeu de données en deux partitions `train` (apprentissage) et `test` (évaluation) selon des proportions arbitraires.\n",
    "\n",
    "Comme d'habitude, il est intéressant de visualiser les données à notre disposition. Nous pouvons construire les nuages de points en deux dimensions à l'aide de Matplotlib :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a1fbdb12",
   "metadata": {},
   "outputs": [],
   "source": [
    "fig = plt.figure(figsize=(16, 12))\n",
    "n_features = data.shape[-1]\n",
    "n_plots = 6\n",
    "idx = 1\n",
    "cmp = np.array(['r', 'g', 'b'])\n",
    "for dim1 in range(0, n_features):\n",
    "    for dim2 in range(dim1+1, n_features):\n",
    "        fig.add_subplot(2, n_plots // 2, idx)\n",
    "        plt.scatter(X_train[:, dim1], X_train[:, dim2],c=cmp[y_train], s=50, edgecolors='none')\n",
    "        plt.scatter(X_test[:,  dim1], X_test[:, dim2], c='none',  s=50, edgecolors=cmp[y_test])\n",
    "        plt.xlabel(iris.feature_names[dim1])\n",
    "        plt.ylabel(iris.feature_names[dim2])\n",
    "        idx += 1\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e2293379",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Pourquoi a-t-on six nuages de points différents ? Que représente chaque figure ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7be6417c",
   "metadata": {},
   "source": [
    "Pour ce problème de classement, nous allons utiliser un PMC avec les valeurs par défaut proposés par scikit-learn pour le nombre de couches (1) et de neurones (100)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ef6f1e17",
   "metadata": {},
   "outputs": [],
   "source": [
    "# emploi de PMC\n",
    "from sklearn.neural_network import MLPClassifier\n",
    "clf = MLPClassifier(solver='lbfgs', alpha=1, tol=5e-3, max_iter=1000)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "01daccb9",
   "metadata": {},
   "source": [
    "Pour estimer l'erreur de généralisation, nous allons utiliser la validation croisée *K-fold*. Cela va nous permettre d'ajuster, si besoin, les hyperparamètres du modèle décisionnel. scikit-learn implémente diverses stratégies de validation croisée dans le module `sklearn.model_selection`. Commençons par expérimenter avec l'approche *K-fold*. L'objet `KFold` dispose d'une méthode `.split()` qui permet de générer les listes des indices des observations à utiliser pour le sous-jeu d'apprentissage et pour le sous-jeu de *validation*. Plus de détails sur cet objet se trouvent dans la [documentation de K-Fold](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.KFold.html)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9fd4b5fe",
   "metadata": {},
   "outputs": [],
   "source": [
    "# KFold pour différentes valeurs de k\n",
    "from sklearn.model_selection import KFold\n",
    "\n",
    "# valeurs de k\n",
    "n_folds = np.array([2, 3, 5, 7, 10, 13, 16, 20])\n",
    "\n",
    "# préparation des listes pour stocker les résultats\n",
    "cv_scores = []\n",
    "cv_scores_std = []\n",
    "\n",
    "for k in n_folds:    # pour chaque valeur de k\n",
    "    kf = KFold(n_splits=k)\n",
    "    scores = []\n",
    "    # apprentissage puis évaluation d'un modèle sur chaque split\n",
    "    for train_idx, val_idx in kf.split(X_train):\n",
    "        # apprentissage avec .fit()\n",
    "        clf.fit(X_train[train_idx], y_train[train_idx])\n",
    "        scores.append(clf.score(X_train[val_idx], y_train[val_idx]))\n",
    "    # calcul de la moyenne et de l'écart-type des performances obtenues\n",
    "    cv_scores.append(np.mean(scores))\n",
    "    cv_scores_std.append(np.std(scores))\n",
    "\n",
    "cv_scores, cv_scores_std = np.array(cv_scores), np.array(cv_scores_std)\n",
    "\n",
    "# affichage performance moyenne +- 1 écart-type pour chaque k\n",
    "plt.figure(figsize=(8, 6))\n",
    "plt.plot(n_folds, cv_scores, 'b')\n",
    "plt.fill_between(n_folds, cv_scores + cv_scores_std, cv_scores - cv_scores_std, alpha=0.5)\n",
    "plt.xlabel(\"Valeur de $k$ du K-Fold\")\n",
    "plt.ylabel(\"Score moyen\")\n",
    "plt.xlim(2, max(n_folds))\n",
    "plt.xticks(n_folds)\n",
    "plt.title(\"Erreur de généralisation estimée en fonction de $k$\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "542d4e6a",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Que constatez-vous en examinant ce graphique ? Ajoutez des valeurs pour *k* (par ex. 40, 100, attention ce sera plus long...) et examinez de nouveau le graphique.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0856bcce",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Pour chaque modèle appris par validation croisée *k-fold*, ajoutez son évaluation sur les données de test mises de côté au départ `X_test, y_test`. Affichez les courbes sur le même graphique. Que constatez-vous ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "47140a8c",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Réalisez l'estimation des performances en utilisant la [validation croisée leave one out (LOO)](http://scikit-learn.org/stable/modules/generated/sklearn.model_selection.LeaveOneOut.html#sklearn.model_selection.LeaveOneOut). Que constatez-vous en comparant les résultats de *k-fold* et de *leave one out* ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7a52a04e",
   "metadata": {},
   "source": [
    "<div class=\"admonition note\">\n",
    "\n",
    "**Note**\n",
    "\n",
    "Pour l'illustration, nous avons itéré ici manuellement à l'aide des itérateurs `KFold` et `LeaveOneOut`. Toutefois il est possible d'automatiser ce procédé dans le cas du *KFold* à l'aide de la fonction `cross_val_score`:\n",
    "\n",
    "```python\n",
    "from sklearn.model_selection import cross_val_score\n",
    "clf = MLPClassifier(solver='lbfgs', alpha=1, tol=5e-3)\n",
    "\n",
    "# Cross-validation KFold avec k=5\n",
    "scores = cross_val_score(clf, X_train, y_train, cv=5)\n",
    "```\n",
    "\n",
    "Cette fonction prend des paramètres supplémentaires (métriques, parallélisation, etc.) qui sont décrites dans [sa documentation](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.cross_val_score.html#sklearn.model_selection.cross_val_score).\n",
    "\n",
    "</div>\n",
    "\n",
    "## Évaluation d'un classifieur binaire\n",
    "\n",
    "Précédemment, nous avons évalué le modèle en calculant le taux de bonnes prédictions (*accuracy*). Néanmoins, cette métrique est biaisée en faveur des classes les plus représentées lorsque le jeu de données est déséquilibré. Une alternative courante, notamment dans le cas de la classification binaire, est de s'intéresser au taux de faux positifs et au taux de faux négatifs.\n",
    "\n",
    "Pour illustrer ces concepts, considérons le jeu de données *Breast Cancer Wisconsin*. Ce jeu de données comporte 569 observations pour 30 variables médicales obtenues par biopsie. Les observations sont réparties dans deux classes: tumeur maligne (`0`) ou tumeur bénigne (`1`)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "86c1233e",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.datasets import load_breast_cancer\n",
    "# Chargement des données *Breast Cancer Wisconsin*\n",
    "X, y = load_breast_cancer(return_X_y=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "408c16ec",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Combien y a-t-il de tumeurs bénignes dans ce jeu de données? De tumeurs malignes?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3a17fd97",
   "metadata": {},
   "source": [
    "Dans un premier temps, nous allons diviser le jeu de données en deux : un jeu d'apprentissage et un jeu de validation. Comme il s'agit d'un problème de classification binaire, nous allons tracer la courbe ROC (*Receiver Operating Characteristic*), qui correspond au taux de vrais positifs en fonction du taux de faux positifs."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d399d7d7",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Division du jeu de données (50%/50%)\n",
    "from sklearn.model_selection import train_test_split\n",
    "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.5, random_state=0)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0fa7233f",
   "metadata": {},
   "source": [
    "Entraînons deux modèles sur ce jeu de données : un percepton multi-couches (`MLPClassifier`) et une analyse factorielle discriminante (`LinearDiscriminantAnalysis`)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "405b7c3e",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.neural_network import MLPClassifier\n",
    "\n",
    "mlp = MLPClassifier(hidden_layer_sizes=50, alpha=1e-5, max_iter=1000, solver='adam', random_state=0)\n",
    "mlp.fit(X_train, y_train)\n",
    "print(f\"Accuracy du MLP: {mlp.score(X_test, y_test)*100:.1f}%\")\n",
    "\n",
    "\n",
    "from sklearn.discriminant_analysis import LinearDiscriminantAnalysis\n",
    "\n",
    "lda = LinearDiscriminantAnalysis()\n",
    "lda.fit(X_train, y_train)\n",
    "print(f\"Accuracy de la LDA: {lda.score(X_test, y_test)*100:.1f}%\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "29d31db9",
   "metadata": {},
   "source": [
    "Comme nous l'avons vu, l'*accuracy* est une métrique biaisée en faveur de la classe la plus représentée. À la place, nous pouvons calculer la précision et le rappel pour chaque classe. Dans notre cas, c'est notamment la précision et le rappel sur la classe `0` (tumeur maligne) qui nous intéresse : cela nous permettra d'évaluer combien de fausses alarmes ont été produites et combien de tumeur malignes ont été « ratées ».\n",
    "\n",
    "scikit-learn propose une [fonction bien utile](https://scikit-learn.org/stable/modules/generated/sklearn.metrics.classification_report.html) permettant de résumer ces métriques de classification :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "429fa3c4",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.metrics import classification_report\n",
    "\n",
    "print(\"\\t========= Métriques pour le MLP ============\")\n",
    "print(classification_report(y_test, mlp.predict(X_test)))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6310323f",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Calculer ces mêmes métriques pour l'analyse factorielle discriminante (*LDA*).\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "aad54371",
   "metadata": {},
   "source": [
    "Pour tracer la courbe ROC, nous devons maintenant calculer les taux de vrais positifs et de faux positifs pour différentes valeurs du seuil de classification. La plupart des classifieurs produisent en sortie une probabilité d'appartenance à chaque classe, entre 0 et 1. Si l'on ne spécifie rien, la règle consiste la plupart du temps à assigner une observation $x$ à la classe $k$ si la probabilité assignée par le modèle pour $x$ à la $k$ est supérieure à 0,5. Toutefois, nous pouvons faire varier ce seuil. Si on diminue le seuil, alors de plus en plus d'observations sont assignées à la classe $k$: le taux de vrais positifs augmente mais le taux de faux positifs aussi. Et inversement si l'on diminue ce seuil.\n",
    "\n",
    "La courbe ROC correspondent à l'ensemble des points (taux de faux positifs, taux de vrais positifs) obtenus en faisant varier le seuil de décision entre 0 et 1.\n",
    "\n",
    "scikit-learn permet de calculer automatiquement les taux de vrais positifs et faux positifs pour différents points de fonctionnement à l'aide de la méthode `metrics.roc_curve` (cf. [documentation scikit-learn](https://scikit-learn.org/stable/modules/generated/sklearn.metrics.roc_curve.html)):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b8547c5a",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.metrics import roc_curve\n",
    "\n",
    "# Calcul de la courbe ROC\n",
    "# `pos_label` permet d'indiquer que l'étiquette \"positive\" (tumeur maligne\")\n",
    "# correspond à la valeur 0\n",
    "fpr, tpr, thresholds = roc_curve(y_test, mlp.predict_proba(X_test)[:,0], pos_label=0)\n",
    "\n",
    "plt.plot(fpr, tpr, label=\"MLP\")\n",
    "plt.xlim(-0.05, 1)\n",
    "plt.ylim(0, 1.05)\n",
    "plt.title(\"Courbes ROC\")\n",
    "plt.xlabel(\"Taux de faux positifs\")\n",
    "plt.ylabel(\"Taux de vrais positifs\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e1d24823",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Calculer et tracer en superposition la courbe ROC pour la *LDA*. Quel modèle vous semble le plus approprié pour ce jeu de données ? Quelle métrique pourrait-on calculer pour confirmer cette hypothèse ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "57fe67f9",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "On souhaite un taux de faux positifs inférieur à 10% (c'est-à-dire que moins d'une tumeur prédite comme « maligne » sur 10 soit en réalité bénigne). Déterminer pour le modèle de votre choix la valeur du seuil pour la classe `0` (tumeur maligne) permettant d'atteindre cet objectif. À quel taux de vrais positifs cela correspond-t-il ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cf2451b8",
   "metadata": {},
   "source": [
    "## Pour aller plus loin\n",
    "\n",
    "Pour simplifier, nous avons ici considéré une division *train/test* en deux parties. Toutefois, comme nous l'avons vu précédemment, il serait plus rigoureux de réaliser une validation croisée, par exemple *K-Fold*. scikit-learn permet de réaliser une validation croisée en spécifiant la métrique à conserver."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e6b7e0b6",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "*(À terminer chez vous)* Réaliser une validation croisée *KFold* sur le modèle de votre choix en utilisant l'aire sous la courbe ROC (`metrics.roc_auc_score`) comme métrique de référence. La [documentation de la validation croisée avec scikit-learn](https://scikit-learn.org/stable/modules/cross_validation.html#computing-cross-validated-metrics) pourra vous être utile.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3f5008e5",
   "metadata": {},
   "source": [
    "<div class=\"admonition note\">\n",
    "\n",
    "**Note**\n",
    "\n",
    "Bien que la courbe ROC soit définie dans le cadre de la classification binaire, il est possible de l'étendre au cadre multiclasse. Dans ce cas, on moyenne généralement plusieurs courbes ROC, en considérant des classifieurs binaires *one-versus-all* pour chacune des classes. Voir [cet exemple de scikit-learn](https://scikit-learn.org/stable/auto_examples/model_selection/plot_roc.html) pour plus de détails.\n",
    "\n",
    "</div>\n",
    "\n",
    "## Recherche des meilleures valeurs pour les hyperparamètres\n",
    "\n",
    "Une des problématiques centrales en modélisation décisionnelle est de pouvoir déterminer les hyperparamètres du modèle qui débouchent sur les meilleurs performances. Formellement, cela revient à chercher la famille paramétrique qui obtient l'erreur de généralisation la plus faible. Néanmoins, comme nous l'avons vu, nous ne pouvons pas utiliser le jeu d'évaluation pour comparer des centaines de modèles : l'estimation de l'erreur généralisation deviendrait bien trop optimiste. Nous allons donc utiliser la validation croisée pour comparer nos modèles, puis seulement une fois les meilleures valeurs des hyperparamètres déterminées pourrons nous estimer l'erreur de généralisation sur le jeu de test.\n",
    "\n",
    "La méthode la plus naïve pour la recherche d'hyperparamètres est la recherche systématique, ou *grid search*. Elle consiste à explorer toutes les combinaisons de valeurs possibles dans un ensemble donné.\n",
    "\n",
    "Utilisons *grid search* pour trouver les meilleures valeurs de deux hyperparamètres pour les PMC dans la même tâche de classement que précédemment. Ces hyperparamètres sont le nombre de neurones dans l'unique couche cachée du PMC et la valeur de la constante de régularisation (par *weight decay*), $\\alpha$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6630100e",
   "metadata": {},
   "outputs": [],
   "source": [
    "# importations\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "# Graine aléatoire, pour la reproductibilité (découpage train/test, initialisation des PMC)\n",
    "np.random.seed(0)\n",
    "\n",
    "# chargement des données iris\n",
    "from sklearn import datasets\n",
    "data, labels = datasets.load_iris(return_X_y=True)\n",
    "\n",
    "# découpage initial en données d'apprentissage et données de test\n",
    "from sklearn.model_selection import train_test_split\n",
    "X_train, X_test, y_train, y_test = train_test_split(data, labels, test_size=0.5)\n",
    "\n",
    "# emploi de PMC\n",
    "from sklearn.neural_network import MLPClassifier"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6bbcb391",
   "metadata": {},
   "source": [
    "Afin d'utiliser la [recherche dans une grille](http://scikit-learn.org/stable/modules/grid_search.html) et la validation croisée pour comparer les modèles obtenus avec toutes les combinaisons de valeurs pour les hyperparamètres, il est nécessaire d'employer [GridSearchCV](http://scikit-learn.org/stable/modules/generated/sklearn.model_selection.GridSearchCV.html#sklearn.model_selection.GridSearchCV). Cet estimateur scikit-learn combine automatiquement une rechercher par grille avec une validation croisée *K-fold*."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b8ead8f6",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.model_selection import GridSearchCV"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0038c2ef",
   "metadata": {},
   "source": [
    "Il est nécessaire d'indiquer dans un « dictionnaire » quels sont les hyperparamètres dont on souhaite explorer les valeurs et quelles sont les différentes valeurs à évaluer. Chaque entrée du dictionnaire consiste en une chaîne de caractères qui contient le nom de l'hyperparamètre tel qu'il est défini dans l'estimateur employé. Nous nous servirons ici de `MLPClassifier`, les noms des paramètres peuvent donc être trouvés dans [la présentation de cette classe](http://scikit-learn.org/stable/modules/generated/sklearn.neural_network.MLPClassifier.html#sklearn.neural_network.MLPClassifier). Nous considérons ici seulement deux paramètres, `hidden_layer_sizes` (nombre de neurones dans l'unique couche cachée) et `alpha` (la constante $\u0007lpha$ de régularisation par *weight decay*)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "56ecdb92",
   "metadata": {},
   "outputs": [],
   "source": [
    "tuned_parameters = {'hidden_layer_sizes':[(5,), (20,), (50,), (100,), (150,), (200,)],\n",
    "                    'alpha':   [0.001, 0.01, 1, 2]}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "64bb2e6e",
   "metadata": {},
   "source": [
    "Dans l'appel de `GridSearchCV` nous indiquons ensuite pour `MLPClassifier` le solveur à utiliser systématiquement (qui n'est pas celui par défaut), ensuite le dictionnaire avec les valeurs des (hyper)paramètres à explorer et enfin le fait que c'est la validation croisée *k-fold* avec $k=5$ qui est employée pour comparer les différents modèles."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5981c5fa",
   "metadata": {},
   "outputs": [],
   "source": [
    "clf = GridSearchCV(MLPClassifier(solver='lbfgs', tol=5e-3, max_iter=1000), tuned_parameters, cv=5)\n",
    "\n",
    "# exécution de grid search\n",
    "clf.fit(X_train, y_train)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "644daa45",
   "metadata": {},
   "source": [
    "Scikit-learn exécute alors le programme suivant :\n",
    "\n",
    "- à partir des listes de valeurs pour les différents (hyper)paramètres sont générées toutes les combinaisons de valeurs,\n",
    "- pour chaque combinaison, les performances des modèles correspondants sont évaluées par validation croisée *5-fold* (appliquée uniquement sur les **données d'apprentissage** `X_train, y_train`),\n",
    "- sont sélectionnées les valeurs des (hyper)paramètres correspondant aux meilleures performances de validation croisée,\n",
    "- avec ces valeurs pour les (hyper)paramètres un nouvel apprentissage est réalisé avec la totalité des données de `X_train, y_train` (et non seulement $\\frac{k-1}{k}$ *folds*).\n",
    "\n",
    "Les lignes suivantes permettent d'afficher les résultats : les paramètres du meilleur modèle avec `clf.best_params_`, ainsi que les résultats de validation croisée obtenus pour toutes les combinaisons de valeurs pour les (hyper)paramètres (`clf.cv_results_` donne accès à ces informations et à bien d'autres)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "52eae021",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(clf.best_params_)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "df4978f8",
   "metadata": {},
   "source": [
    "Nous pouvons également faire un affichage plus complet, traçant la surface du score moyen des différentes modèles en fonction de la combinaison des deux hyperparamètres :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d2543a5e",
   "metadata": {},
   "outputs": [],
   "source": [
    "n_hidden = np.array([s[0] for s in tuned_parameters['hidden_layer_sizes']])\n",
    "alphas = np.array(tuned_parameters['alpha'])\n",
    "\n",
    "# création de la grille des hyperparamètres\n",
    "xx, yy = np.meshgrid(n_hidden, alphas)\n",
    "scores = clf.cv_results_['mean_test_score'].reshape(xx.shape)\n",
    "\n",
    "# affichage sous forme de fil de fer de la surface des résultats des modèles évalués\n",
    "from mpl_toolkits.mplot3d import Axes3D\n",
    "fig = plt.figure(figsize=(8, 6))\n",
    "ax = plt.axes(projection='3d')\n",
    "ax.set_xlabel(\"Neurones cachés\")\n",
    "ax.set_ylabel(\"Régularisation $\\\\alpha$\")\n",
    "ax.set_zlabel(\"Taux de bon classement\")\n",
    "ax.plot_wireframe(xx, yy, scores)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a9bb9c80",
   "metadata": {},
   "source": [
    "Nous avons employé ici `plot_wireframe` car la lisibilité est meilleure qu'avec `plot_surface`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4ac46c90",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Combien de PMC au total sont appris pendant la validation croisée dans cet exemple ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d55523d8",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Quelle est la signification du paramètre `refit` de `GridSearchCV` ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "75110ef5",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Examinez de façon plus complète le contenu de `clf.cv_results_`.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a2dce0ba",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Evaluez le modèle sélectionné sur les données de test (`X_test, y_test`).\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b0f0f8c5",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "L'aspect des résultats vous incite à affiner la grille ? Modifiez la grille et examinez les nouveaux résultats.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1d118fb",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Utilisez la recherche aléatoire avec [RandomizedSearchCV](http://scikit-learn.org/stable/modules/generated/sklearn.model_selection.RandomizedSearchCV.html#sklearn.model_selection.RandomizedSearchCV). Le « budget » (nombre total de combinaisons évaluées) peut être fixé avec `n_iter`. Motivez le choix des lois employées pour le tirage des valeurs des deux (hyper)paramètres `hidden_layer_sizes` et `alpha`.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3c3ebf03",
   "metadata": {},
   "source": [
    "## Pour aller plus loin\n",
    "\n",
    "D'autres stratégies plus avancées de sélection d'hyperparamètres sont envisageables. En effet, la recherche par grille, comme la recherche aléatoire, sont des méthodes relativement rudimentaires et plutôt coûteuses en ressources. Cela peut notamment poser problème lorsque les modèles décisionnels que l'on considère appartiennent à une famille paramétrique coûteuse en calculs, comme les réseaux de neurones profonds.\n",
    "\n",
    "scikit-learn propose ainsi une implémentation expérimentale d'une recherche par dichotomie, permettant de concentrer les ressources sur les combinaisons d'hyperparamètres les plus prometteuses."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ce71006e",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "*(À terminer chez vous)* Expérimenter avec `HalvingGridSearchCV` (cf. [la documentation](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.HalvingGridSearchCV.html#sklearn.model_selection.HalvingGridSearchCV)). Comparer notamment le score final et le temps de calcul à celui obtenu avec `GridSearchCV`.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1d7b61a5",
   "metadata": {},
   "source": [
    "<div class=\"admonition note\">\n",
    "\n",
    "**Note**\n",
    "\n",
    "Hors de scikit-learn, il existe également d'autres bibliothèques implémentant des stratégies avancées de sélection utilisant diverses heuristiques pour choisir les combinaisons d'hyperparamètres à explorer. Certaines de ces bibliothèques sont compatibles avec l'interface de scikit-learn, par exemple [tune-sklearn](https://github.com/ray-project/tune-sklearn) ou encore [sklearn-genetic-opt](https://github.com/rodrigo-arenas/Sklearn-genetic-opt).\n",
    "\n",
    "</div>"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
