{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "6b793b89",
   "metadata": {},
   "source": [
    "# VI.2 - Travaux pratiques\n",
    "\n",
    "L'objectif de cette séance de travaux pratiques est de montrer l'utilisation des machines à vecteurs de support (*Support Vector Machines*, SVM) pour les problèmes de classification non-lineaire en python avec 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",
    "  * [Site LibSVM](https://www.csie.ntu.edu.tw/~cjlin/libsvm/)\n",
    "  * [Site LibLinear](https://www.csie.ntu.edu.tw/~cjlin/liblinear/)\n",
    "\n",
    "## Machines à vecteurs de support (SVM)\n",
    "\n",
    "Les machines à vecteurs de support (SVM : *Support Vector Machines*) sont une classe des méthodes d'apprentissage statistique basées sur le principe de la maximisation de la marge (séparation des classes). Il existe plusieurs formulations (linéaires, versions à noyaux) qui peuvent s'appliquer sur des données séparables (linéairement) mais aussi sur des données non séparables.\n",
    "\n",
    "Les avantages des SVM :\n",
    "\n",
    "* Très efficaces en dimension élevée.\n",
    "* Ils sont aussi efficaces dans le cas où la dimension de l'espace est plus grande que le nombre d'échantillons d'apprentissage.\n",
    "* N'utilisent pas tous les échantillons d'apprentissage, mais seulement une partie (les vecteurs de support). En conséquence, ces algorithmes demandent moins de mémoire.\n",
    "\n",
    "Désavantages :\n",
    "\n",
    "* Si le nombre d'attributs est beaucoup plus grand que le nombre d'échantillons, les performances seront moins bonnes.\n",
    "* Comme il s'agit de méthodes de discrimination entre les classes, elles ne fournissent pas directement des estimations de probabilités.\n",
    "\n",
    "## Scikit-learn\n",
    "\n",
    "Dans Scikit-learn, les SVM sont implémentées dans le module `sklearn.svm`. Dans cette partie nous allons nous intéresser à la version à noyaux (Scikit-learn utilise la bibliothèque LibSVM déjà discutée)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "baf62a71",
   "metadata": {},
   "outputs": [],
   "source": [
    "!pip install -U scikit-learn\n",
    "# Une version récente de sklearn est nécessaire pour gamma='scale' (cf. plus bas)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "be39913c",
   "metadata": {},
   "source": [
    "Relisez le TP SVM linéaire (partie Scikit-learn) pour vous remettre dans le contexte (quels sont les classes Python utilisées et leurs paramètres). La [documentation de scikit-learn sur les SVM](https://scikit-learn.org/stable/modules/generated/sklearn.svm.SVC.html#sklearn.svm.SVC) vous sera utile.\n",
    "\n",
    "Nous allons reprendre la classification sur les données Iris."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "748b6f00",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "from sklearn import svm, datasets\n",
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "# Chargement des données\n",
    "iris = datasets.load_iris()\n",
    "X, y = iris.data, iris.target\n",
    "# On conserve 50% du jeu de données pour l'évaluation\n",
    "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.5)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e2198715",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Refaites la classification de la base de données `iris` mais avec un noyau gaussien. Testez l'effet du paramètre d'échelle du noyau (`gamma`) et du paramètre de régularisation `C`.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "53896c14",
   "metadata": {},
   "source": [
    "Comme dans le TP précédent, nous pouvons afficher la frontière de décision en ne conservant que deux variables explicatives :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ea6a9b4e",
   "metadata": {},
   "outputs": [],
   "source": [
    "X, y = iris.data[:,:2], iris.target\n",
    "# On conserve 50% du jeu de données pour l'évaluation\n",
    "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.5)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c3f02d67",
   "metadata": {},
   "outputs": [],
   "source": [
    "clf = svm.SVC(C=0.1, kernel='rbf', gamma=0.25)\n",
    "clf.fit(X_train, y_train)\n",
    "\n",
    "# Pour afficher la surface de décision on va discrétiser l'espace avec un pas h\n",
    "h = .02\n",
    "# Créer la surface de décision discretisée\n",
    "x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1\n",
    "y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1\n",
    "xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))\n",
    "\n",
    "# Surface de décision\n",
    "Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])\n",
    "Z = Z.reshape(xx.shape)\n",
    "\n",
    "plt.contourf(xx, yy, Z, cmap=plt.cm.coolwarm, alpha=0.8)\n",
    "# Afficher aussi les points d'apprentissage\n",
    "plt.scatter(X_train[:, 0], X_train[:, 1], label=\"train\", edgecolors='k', c=y_train, cmap=plt.cm.coolwarm)\n",
    "plt.scatter(X_test[:, 0], X_test[:, 1], label=\"test\", marker='*', c=y_test, cmap=plt.cm.coolwarm)\n",
    "plt.xlabel('Sepal length')\n",
    "plt.ylabel('Sepal width')\n",
    "plt.title(\"SVM RBF\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4e0162b2",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Que constatez-vous par rapport au TP précédent ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1119c12e",
   "metadata": {},
   "source": [
    "## Jeu de données Digits\n",
    "\n",
    "Reprenons notre base de données Digits de chiffres manuscrits."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1b40bb3f",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.datasets import load_digits\n",
    "digits = load_digits()\n",
    "X, y = digits.data, digits.target\n",
    "print(X.shape)\n",
    "print(y.shape)\n",
    "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b4a711c3",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Réalisez une classification par une SVM linéaire et une SVM à noyau gaussien du jeu de données Digits. Comment est choisi le paramètre `gamma` dans scikit-learn ? Testez différentes valeurs de ce paramètre pour évaluer son influence. En particulier, testez les paramètres `gamma='auto'` et `gamma='scale'`. À quoi correspondent-ils ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5033e84f",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Réalisez une analyse en composante principale (ACP) et gardez les 2 premières composantes principales (voir la [documentation Scikit-learn](https://scikit-learn.org/stable/modules/generated/sklearn.decomposition.PCA.html)). Ensuite faites une classification avec un noyau gaussien et affichez les points de test ainsi que la surface de décision (reprendre le code du TP SVM linéaire). Comparez avec une SVM linéaire.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "52d111b7",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Réalisez une recherche par grille afin de déterminer sur le jeu de données Digits complet (sans l'ACP) :\n",
    "\n",
    "- le meilleur noyau à utiliser,\n",
    "- la meilleure valeure de `C`,\n",
    "- la meilleure valeur de `gamma` (ou le degré du polynome pour un noyau polynomial).\n",
    "- (optionnel) la meilleure valeur de `n_components` de l'ACP\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8fef5fbf",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "(optionnel) Combien de composantes faut-il garder au minimum dans l'ACP pour classer correctement au moins 97% des images ? À quel facteur de réduction de dimension cela correspond-il ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a2c7f2fc",
   "metadata": {},
   "source": [
    "## Approfondissement : LibSVM\n",
    "\n",
    "<div class=\"admonition note\">\n",
    "\n",
    "**Note**\n",
    "\n",
    "Cette partie est facultative. Elle vous permet de manipuler directement les bibiothèques LibLinear et LibSVM sur lesquelles s'appuie scikit-learn pour les machines à vecteur de supports. Ces implémentations sont très rapides et sont écrites en langage C.\n",
    "\n",
    "Il est nécessaire d'avoir un compilateur C fonctionnel pour cette partie.\n",
    "\n",
    "</div>\n",
    "\n",
    "LibSVM est une implémentation sous licence libre qui s'est imposée ces derniers années et qui est utilisée par beaucoup de logiciels comme moteur de classification. Par exemple, Scikit-learn utilise cette bibliothèque pour son module `sklearn.svm`.\n",
    "\n",
    "Pour l'obtenir et la compiler suivre ces pas :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "512df24f",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%bash\n",
    "cd ~\n",
    "rm -rf tpsvm\n",
    "mkdir tpsvm\n",
    "cd tpsvm\n",
    "wget http://cedric.cnam.fr/~ferecatu/RCP209/libsvm-3.22.tar.gz\n",
    "tar xzvf libsvm-3.22.tar.gz\n",
    "cd libsvm-3.22/\n",
    "make"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9bc4f999",
   "metadata": {},
   "source": [
    "Pour comprendre les différents noyaux disponibles dans cette bibliothèque et leurs paramètres, lisez le document README dans le répertoire `libsvm-3.22/`.\n",
    "\n",
    "Dans une première étape, nous allons essayer de dépasser les performances de `libLinear` sur la base de données MNIST en utilisant un C-SVC à noyau gaussien. Nous allons tester seulement 2 classes, les chiffres 5 et 6."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b5fb3beb",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%bash\n",
    "cd ~\n",
    "cd tpsvm\n",
    "# Récupérer et compiler libLinear\n",
    "wget http://cedric.cnam.fr/~ferecatu/RCP209/liblinear-2.2.tar.gz\n",
    "tar xzvf liblinear-2.2.tar.gz\n",
    "cd liblinear-2.2/\n",
    "make\n",
    "# Récupérer la base de données\n",
    "cd ~\n",
    "cd tpsvm\n",
    "mkdir databases\n",
    "cd databases\n",
    "wget http://cedric.cnam.fr/~ferecatu/RCP209/mnist.bz2\n",
    "wget http://cedric.cnam.fr/~ferecatu/RCP209/mnist.t.bz2\n",
    "bzip2 -d mnist.bz2\n",
    "bzip2 -d mnist.t.bz2\n",
    "cat mnist | grep '^5 \\|^6 ' | shuf -n 2000 > 56\n",
    "cat mnist.t | grep '^5 \\|^6 ' | shuf -n 1000 > 56.t\n",
    "# Nombre d'échantillons d'apprentissage de la classe 5\n",
    "cat 56 | grep '^5' | wc -l\n",
    "# >>> 951\n",
    "# Nombre d'échantillons d'apprentissage de la classe 6\n",
    "cat 56 | grep '^6' | wc -l\n",
    "# >>> 1049"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ca54c316",
   "metadata": {},
   "source": [
    "Entrainer un modèle C-SVM linéaire avec les paramètres par défaut :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "65dac9b7",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%bash\n",
    "../liblinear-2.2/train 56 56.model_linear\n",
    "../liblinear-2.2/predict 56.t 56.model_linear 56.t.output_linear\n",
    "# >>> Accuracy = 96.4% (964/1000)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cefcb82d",
   "metadata": {},
   "source": [
    "Entrainer un modèle C-SVM à noyau gaussien et paramètres par défaut :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e3882f4f",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%bash\n",
    "../libsvm-3.22/svm-train 56 56.model_default\n",
    "../libsvm-3.22/svm-predict 56.t 56.model_default 56.output_default\n",
    "# >>> Accuracy = 52.3% (523/1000) (classification)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4f03319b",
   "metadata": {},
   "source": [
    "Comme on peut le voir, les performances avec les paramètres par défaut ne sont pas très bonnes. Surtout le paramètre `gamma` est beaucoup trop petit (`gamma` est inversement proportionnel à la variance de la gaussienne, donc on est en situation de sous apprentissage - gaussienne trop large, car variance élevée). Par la suite on va mettre les données à l'échelle (chaque attribut entre 0 et 1) et garder toujours les paramètres par défaut."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ab96afac",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%bash\n",
    "../libsvm-3.22/svm-scale -l 0 56 > 56.scaled\n",
    "../libsvm-3.22/svm-scale -l 0 56.t > 56.t.scaled\n",
    "../liblinear-2.2/train 56.scaled 56.scaled.model_linear\n",
    "../liblinear-2.2/predict 56.t.scaled 56.scaled.model_linear 56.t.scaled.output_linear\n",
    "# >>> Accuracy = 96.7% (967/1000)\n",
    "# On ne gagne pas beaucoup avec un classifieur linéaire\n",
    "../libsvm-3.22/svm-train 56.scaled 56.scaled.model_default\n",
    "../libsvm-3.22/svm-predict 56.t.scaled 56.scaled.model_default 56.scaled.output_default\n",
    "# >>> Accuracy = 97.8% (978/1000) (classification)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "40aecd55",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Cette fois-ci les résultats sont meilleurs. Regardez le fichier `56.scaled.model_default`, quelles sont les valeurs des paramètres C et `gamma` ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7823d947",
   "metadata": {},
   "source": [
    "Nous allons chercher les meilleurs paramètres par *grid search* pour ce jeux de données."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "30c83ad3",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%bash\n",
    "python ../libsvm-3.22/tools/grid.py -svmtrain ../libsvm-3.22/svm-train 56.scaled\n",
    "# >>> [local] 5 -7 98.7 (best c=32.0, g=0.0078125, rate=98.7)\n",
    "# >>> ...\n",
    "# >>> 8.0 0.03125 99.15"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ea184405",
   "metadata": {},
   "source": [
    "Le script nous propose `C = 8` et `gamma = 0.03125`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "03bf05e9",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%bash\n",
    "../libsvm-3.22/svm-train -c 8 -g 0.03125 56.scaled 56.scaled.model\n",
    "../libsvm-3.22/svm-predict 56.t.scaled 56.scaled.model 56.scaled.output\n",
    "# >>> Accuracy = 99.3% (993/1000) (classification)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c28a33af",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Un bien meilleur score donc que dans le cas linéaire.\n",
    "\n",
    "Extraire un partie de 10000 échantillons de la base MNIST (toutes les 10 classes) et réduire le jeux de données entre 0 et 1. Coupez la base en deux parties égales apprentissage (`train`) et test (`test`) avec `libsvm-3.22/tools/subset.py`. Comparez le classifieurs libLinear et libSVM avec les paramètres par défaut. Esayez d'autres noyaux que le gaussien. Faites varier les parametres.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fc9c47b2",
   "metadata": {},
   "source": [
    "### SVM-toy\n",
    "\n",
    "Par la suite on va explorer l'outil `svm-toy` qui permet de visualiser la classification pour des problèmes en deux dimensions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4277b966",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%bash\n",
    "cd ~/tpsvm/libsvm-3.22/svm-toy/gtk\n",
    "make\n",
    "./svm-toy"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "62c09429",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Cliquez sur la surface d'affichage pour ajouter des points d'apprentissage. Changez de couleur pour ajouter une nouvelle classe (l'outil gère aussi la classification multi-classe). Utilisez l'option `-t 2` pour sélectionner la classification par noyau gaussien. Pour changer le paramètre C=valeur utilisez l'option `-c valeur` (par exemple : `-c 2.5`). Pour changer le paramètre `gamma` du noyau utilisez l'option `-g valeur` (par exemple : `-g 2.5`). Testez plusieurs valeurs pour les paramètres C et `gamma` afin de comprendre leur effet sur le fichier suivant : `04.dat`. Générez d'autres jeux de données par vous-même est testez d'autres noyaux (voir le fichier `libsvm-3.22/README`).\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6cf09759",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "*[Optionnel : à faire à la fin du TP si'il vous reste du temps]* Regardez les sources `c` de la bibliothèque libSVM. Où sont implementés les noyaux ? Ajoutez le noyau angulaire dans le code (K(x, y) = - ||x-y||) et recompilez svm-toy. Testez-le avec un jeu de données de type spirale (voir transparent 27 du cours). Comparez avec le noyau gaussien.\n",
    "\n",
    "</div>"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
