{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "84076609",
   "metadata": {},
   "source": [
    "# IV.2 Travaux pratiques\n",
    "\n",
    "L'objectif de cette séance de travaux pratiques est de montrer l'utilisation des méthodes d'agrégation (*bagging*, forêts aléatoires, *boosting*) pour les problèmes de classification et de régression, en python avec Sciki-learn. Ce document reprend librement certains exemples présentés dans la documentation de Scikit-learn.\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",
    "## Méthodes d'agrégation\n",
    "\n",
    "Les méthodes ensemblistes (ou d'agrégation) pour les algorithmes d'apprentissage statistique (en anglais : *ensemble learning*) sont basées sur l'idée de combiner les prédictions de plusieurs prédicteurs (ou classifieurs) pour une meilleure généralisation et pour compenser les défauts éventuels de prédicteurs\n",
    "individuels.\n",
    "\n",
    "En général, on distingue deux familles de méthodes de ce type :\n",
    "\n",
    "1. Méthodes par moyennage (*bagging*, forêts aléatoires) où le principe est de faire la moyenne de plusieurs prédictions en espérant un meilleur résultat suite à la réduction de variance de l'éstimateur moyenne.\n",
    "2. Méthodes adaptatives (*boosting*) où les paramètres sont itérativement adaptés pour produire un meilleur mélange.\n",
    "\n",
    "Dans la suite nous explorerons chacune de ces classes d'algorithme en Scikit-learn et présenterons quelques comparaisons.\n",
    "\n",
    "## *Bagging*\n",
    "\n",
    "Les méthodes de type *bagging* construisent plusieurs instances d'un estimateur, calculées sur des échantillons aléatoires tirés de la base d'apprentissage (et éventuellement une partie des attributs, également sélectionnés de façon aléatoire), et ensuite combine les prédictions individuelles en réalisant leur moyenne pour réduire la variance de l'estimateur. Leur avantage principal réside dans le fait qu'ils construisent une version améliorée de l'algorithme de base, sans demander de modification de cet algorithme. Le prix à payer est un coût de calcul plus élevé. Comme elles réduisent le sur-apprentissage, les méthodes *bagging* fonctionnent très bien avec des prédicteurs « forts ». Par contraste, les méthodes *boosting* sont mieux adaptées à des prédicteurs faibles (*weak learners*).\n",
    "\n",
    "Dans Scikit-learn, les méthodes de *bagging* sont implémentées via la classe `BaggingClassifier` et `BaggingRegressor`. Les constructeurs prennent en paramètres un estimateur de base et la stratégie de sélection des points et attributs :\n",
    "\n",
    "  * `estimator` : optionnel (default=None). Si None alors l'estimateur est un arbre de décision.\n",
    "  * `max_samples` : la taille de l'échantillon aléatoire tiré de la base d'apprentissage.\n",
    "  * `max_features` : le nombre d'attributs tirés aléatoirement.\n",
    "  * `bootstrap` : boolean, optionnel (default=True). Tirage des points avec remise ou non.\n",
    "  * `bootstrap_features` : boolean, optionnel (default=False). Tirage des attributs avec remise ou non.\n",
    "  * `oob_score` : boolean. Estimer ou non l'erreur de généralisation OOB (*Out of Bag*).\n",
    "\n",
    "Le code suivant construit un ensemble des classifieurs. Chaque classifieur de base est un `KNeighborsClassifier` (c'est-à-dire k-plus-proches-voisins), chacun utilisant au maximum 50% des points pour son apprentissage et la moitié des attributs (*features*) :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "09afa607",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.ensemble import BaggingClassifier\n",
    "from sklearn.neighbors import KNeighborsClassifier\n",
    "bagging = BaggingClassifier(KNeighborsClassifier(), max_samples=0.5, max_features=0.5)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "eab4ae09",
   "metadata": {},
   "source": [
    "Dans cet exemple nous allons utiliser la base de données `digits`, qui contient 10 classes (images des chiffres en écriture manuscrite). Il y a 1797 éléments, chaque élément a 64 attributs (8 pixels par 8)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3ead1e1f",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.datasets import load_digits\n",
    "digits = load_digits()\n",
    "\n",
    "# Affichage des 10 premières images\n",
    "import matplotlib.pyplot as plt\n",
    "fig = plt.figure()\n",
    "for i, digit in enumerate(digits.images[:10]):\n",
    "    fig.add_subplot(1,10,i+1)\n",
    "    plt.imshow(digit)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "471d1591",
   "metadata": {},
   "source": [
    "Pour ce TP, nous allons utiliser comme classifieur de base un arbre de décision `DecisionTreeClassifier`. Ce classifieur nous permet d'établir des performances de référence (c'est un ensemble à 1 modèle)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f1e30657",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "from sklearn import tree\n",
    "from sklearn.ensemble import BaggingClassifier\n",
    "\n",
    "X, y = digits.data, digits.target\n",
    "clf = tree.DecisionTreeClassifier()\n",
    "clf.fit(X, y)\n",
    "accuracy = clf.score(X,y)\n",
    "print(accuracy)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cc507cbe",
   "metadata": {},
   "source": [
    "Sur la base d'apprentissage `accuracy = 1`. Pour plus de réalisme, découpons la base de données en un jeu d'apprentissage et un je de test afin de voir le comportement de généralisation de l'arbre sur des données différentes des celles d'apprentissage :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9a0451b7",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "# 90% des données pour le test, 10% pour l'apprentissage\n",
    "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.90)\n",
    "\n",
    "clf = tree.DecisionTreeClassifier()\n",
    "clf.fit(X_train, y_train)\n",
    "\n",
    "Z = clf.predict(X_test)\n",
    "accuracy = clf.score(X_test,y_test)\n",
    "print(accuracy)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "68701fd5",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Construire la variance de la valeur `accuracy` sur 100 tirages pour la séparation apprentissage/test. Que pouvons-nous conclure ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c8cab29f",
   "metadata": {},
   "source": [
    "Pour comparer, construisons mainntenant un classifieur *bagging* sur nos données, toujours basé sur les `DecisionTreeClassifier` :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4cc16a5c",
   "metadata": {},
   "outputs": [],
   "source": [
    "clf = BaggingClassifier(tree.DecisionTreeClassifier(), max_samples=0.5, max_features=0.5, n_estimators=200)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c0008b91",
   "metadata": {},
   "source": [
    "L'apprentissage et l'évaluation de cet ensemble se font de la façon habituelle :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ed5ced42",
   "metadata": {},
   "outputs": [],
   "source": [
    "clf.fit(X_train, y_train)\n",
    "Z = clf.predict(X_test)\n",
    "accuracy=clf.score(X_test,y_test)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9710e351",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Calculer la variance de la valeur `accuracy` sur 100 tirages pour la séparation apprentissage/test. Comparer avec la variance du classifieur de base. Que pouvons-nous conclure ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "00e708f0",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Construire le graphique `accuracy` vs `n_estimators`. Que constatez-vous ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2ea05bea",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Faites varier les parametres `max_samples` et `max_features`. Pour quelles valeurs on obtient le meilleur résultat ? On pourra notamment utiliser `GridSearchCV` pour réaliser une recherche systématique.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2b57dc44",
   "metadata": {},
   "source": [
    "## Forêts aléatoires\n",
    "\n",
    "L'algorithme des forêts aléatoires propose une optimisation des arbres de décision. Il utilise le même principe que le *bagging*, mais avec une étape supplémentaire de randomisation dans la sélection des attributs des nœuds dans le but de réduire la variance de l'estimateur obtenu. Les deux objets Python qui implémentent les forêts aléatoires sont `RandomForestClassifier` et `RandomForestRegressor`. Les paramètres les plus importants sont :\n",
    "\n",
    "  * `n_estimators` : integer, optional (default=10). Le nombre d'arbres.\n",
    "  * `max_features` : le nombre d'attributs à considérer à chaque split.\n",
    "  * `max_samples` : la taille de l'échantillon aléatoire tiré de la base d'apprentissage.\n",
    "  * `min_samples_leaf` : le nombre minimal d'éléments dans un nœud feuille.\n",
    "  * `oob_score` : boolean. Estimer ou non l'erreur de généralisation OOB (*Out of Bag*).\n",
    "\n",
    "Par la suite nous allons refaire la classification sur la base Digits en utilisant un classifieur `RandomForestClassifier`. Comme d'habitude, on sépare les données en gardant 10% pour l'apprentissage et 90% pour le test."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "47cf86f9",
   "metadata": {},
   "outputs": [],
   "source": [
    "digits = load_digits()\n",
    "X, y = digits.data, digits.target\n",
    "\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.90)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2d86546b",
   "metadata": {},
   "source": [
    "On peut désormais créer et entraîner notre modèle :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b14ea3a2",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.ensemble import RandomForestClassifier\n",
    "clf = RandomForestClassifier(n_estimators=200)\n",
    "clf.fit(X_train, y_train)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a754fe43",
   "metadata": {},
   "source": [
    "Puis réaliser les prédictions et calculer le score de test :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e73142ef",
   "metadata": {},
   "outputs": [],
   "source": [
    "y_pred = clf.predict(X_test)\n",
    "accuracy = clf.score(X_test,y_test)\n",
    "print(accuracy)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d462ffb5",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Comment la valeur de la variable `accuracy` se compare avec le cas *bagging* qui utilise le même nombre d'arbres (200 dans notre cas) ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dbab0d7a",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Construire la variance de la valeur `accuracy` sur 100 tirages pour la séparation apprentissage/test. Que pouvons-nous conclure en comparant avec la séction précedente (*bagging*) ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "207a1b21",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Construire le graphique `accuracy` vs `n_estimators`. Que constatez-vous ? A partir de quelle valeur on n'améliore plus ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5c1b72de",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Regardez dans la documentation les *ExtraTreesClassifier* et refaites la classification avec ce type de classifieur. Comparez avec *RandomForestClassifier*.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d9dfd9c8",
   "metadata": {},
   "source": [
    "## *Boosting*\n",
    "\n",
    "Le principe du *boosting* est d'évaluer une séquence de classifieurs faibles (*weak learners*) sur plusieurs versions légèrement modifiées des données d'apprentissage. Les décisions obtenues sont alors combinées par une somme ponderée pour obtenir le modèle final.\n",
    "\n",
    "Avec scikit-learn, c'est la classe `AdaBoostClassifier` qui implémente cet algorithme. Les paramètres les plus importants sont :\n",
    "\n",
    "  * `n_estimators` : integer, optional (default=10). Le nombre de classifieurs faibles.\n",
    "  * `learning_rate` : contrôle la vitesse de changement des poids par itération.\n",
    "  * `estimator` : (default=DecisionTreeClassifier) le classifieur faible utilisé.\n",
    "\n",
    "Dans la suite nous allons refaire la classification sur la base Digits en utilisant un classifieur `RandomForestClassifier` :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d5590491",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.ensemble import AdaBoostClassifier\n",
    "\n",
    "digits = load_digits()\n",
    "X, y = digits.data, digits.target\n",
    "\n",
    "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.90)\n",
    "\n",
    "# AdaBoost basé sur 200 arbres de décision\n",
    "clf = AdaBoostClassifier(estimator=tree.DecisionTreeClassifier(max_depth=5), n_estimators=200, learning_rate=2)\n",
    "clf.fit(X_train, y_train)\n",
    "accuracy = clf.score(X_test, y_test)\n",
    "print(accuracy)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7e38d8f3",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Le paramètre `max_depth` contrôle la profondeur de l'arbre. Essayez plusieurs valeurs pour voir l'impact de l'utilisation d'un classifieur faible vs plus fort (`max_depth` élevé ou éliminer le paramètre). Testez aussi l'effet du paramètre `learning_rate` et le nombre de classifieurs.\n",
    "\n",
    "</div>"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
