{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "fa9d7e3b",
   "metadata": {},
   "source": [
    "# VII.2 - Travaux pratiques\n",
    "\n",
    "L'objectif de cette séance de travaux pratiques est de montrer l'utilisation des plusieurs algorithmes à noyaux en python (scikit-learn). Nous allons aborder dans cette séance les *One Class* SVM (OCSVM) et SVM pour la régression.\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",
    "## *One Class* SVM (OCSVM)\n",
    "\n",
    "Les OCSVM sont des estimateurs de support de densité pour des données multidimensionnelles. L'idée derrière l'implémentation est de trouver l'hyperplan le plus éloigné de l'origine qui sépare les données de l'origine.\n",
    "\n",
    "Dans scikit-learn, les *One-Class* SVM sont implémentés dans le module `sklearn.svm`, n'hésitez pas à consulter la [documentation de ce module](https://scikit-learn.org/stable/modules/generated/sklearn.svm.OneClassSVM.html) pour plus de détails.\n",
    "Nous utilisons un noyau gaussien pour faire la détection des *outliers* dans un échantillon de données en deux dimensions.\n",
    "Pour cet exemple, nous générons deux clusters gaussiens auxquels nous ajoutons 10% de données anormales, tirées au hasard uniformément dans l'espace à deux dimensions considéré :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c29379a5",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "from sklearn import svm\n",
    "\n",
    "# On créé deux groupes séparés (échantillons de gaussiennes)\n",
    "N = 200\n",
    "data1 = 0.3 * np.random.randn(N // 2, 2) + [2,2]\n",
    "data2 = 0.3 * np.random.randn(N // 2, 2) - [2,2]\n",
    "\n",
    "# On créé 10% de données anormales (*outliers*)\n",
    "outliers = np.random.uniform(size=(N // 10, 2), low=-6, high=6)\n",
    "\n",
    "# Les données = groupes + anomalies\n",
    "X = np.concatenate((data1, data2, outliers))\n",
    "\n",
    "plt.scatter(X[:,0], X[:,1]) and plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "62c02bd6",
   "metadata": {},
   "source": [
    "Nous pouvons ensuite créer le modèle de *one-class* SVM avec `sklearn`. Le paramètre `nu` correspond à la proportion maximale d'erreurs autorisées, c'est-à-dire au pourcentage maximal de points du jeu de données que l'on acceptera d'exclure de notre classe. Cette fraction doit peu ou prou correspondre au pourcentage de données anormales attendu dans le jeu de données. Dans notre cas, nous savons qu'il y a environ 10% d'*outliers* donc nous pouvons choisir `nu=0.1`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "629dc461",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Construction du modèle (noyau RBF)\n",
    "clf = svm.OneClassSVM(nu=0.1, kernel=\"rbf\", gamma=0.05)\n",
    "clf.fit(X)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6f9883af",
   "metadata": {},
   "source": [
    "Le code suivant permet alors de tracer les frontières (plus exactement les lignes de niveaux) de la fonction de décision de la `OneClassSVM` ainsi entraînée :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d0296d6e",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Afficher les points et les vecteurs les plus proches du plan de séparation\n",
    "xx, yy = np.meshgrid(np.linspace(-7, 7, 500), np.linspace(-7, 7, 500))\n",
    "Z = clf.decision_function(np.c_[xx.ravel(), yy.ravel()])\n",
    "Z = Z.reshape(xx.shape)\n",
    "y_pred = clf.predict(X)\n",
    "\n",
    "# Choix du jeu de couleurs\n",
    "plt.set_cmap(plt.cm.Paired)\n",
    "# Trace le contour de la fonction de décision\n",
    "plt.contourf(xx, yy, Z)\n",
    "# Affiche les points considérés comme \"inliers\"\n",
    "plt.scatter(X[y_pred>0,0], X[y_pred>0,1], c='white', edgecolors='k', label='inliers')\n",
    "# Affiche les points considérés comme \"outliers\"\n",
    "plt.scatter(X[y_pred<=0,0], X[y_pred<=0,1], c='black', label='outliers')\n",
    "plt.legend()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "50500a8a",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Testez plusieurs valeurs pour le paramètre `gamma`. Pour quelle valeur le résultat semble meilleur (moins de *outliers* incorrectement classés) ? En pratique on ne connait pas les *outliers*, l'utilité des OCSVM est de les détecter. Le paramètre `nu` doit aussi avoir une bonne valeur pour ne pas sous-estimer (ou sur-estimer) le support de la distribution.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b4adba7d",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Remplacez le noyau `rbf` par un noyau linéaire. Quel problème constatez-vous ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "613b8164",
   "metadata": {},
   "source": [
    "## SVM pour la régression\n",
    "\n",
    "Dans le cas de la régression, l'objectif est d'apprendre un modèle qui prédit les valeurs d'une fonction à partir des valeurs des variables d'entrée. L'idée est de trouver la fonction la plus « lisse » qui passe par les (ou à proximité des) données d'apprentissage. Scikit-learn implémente le modèle SVR (epsilon-regression) dans le module Python `sklearn.svm.SVR` dont vous pouvez bien sûr consulter [la documentation](https://scikit-learn.org/stable/modules/generated/sklearn.svm.SVR.html).\n",
    "\n",
    "Dans cette partie nous présenterons la régression dans le cas unidimensionnel en comparant plusieurs noyaux avec scikit-learn. Le module `sklearn.svm.SVR` permet de faire varier tous les paramètres.\n",
    "\n",
    "Il faut d'abord importer les modules :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "107a85bc",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "from sklearn.svm import SVR"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d37f91bb",
   "metadata": {},
   "source": [
    "### Données synthétiques\n",
    "\n",
    "Dans un premier temps, nous allons travailler sur des données générées. Notre objectif sera de reproduire une sinusoïde, comme dans le TP sur les arbres de décision :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "76fe5e94",
   "metadata": {},
   "outputs": [],
   "source": [
    "X = np.sort(5 * np.random.rand(40, 1), axis=0)\n",
    "y = np.sin(X).ravel()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ede68b57",
   "metadata": {},
   "source": [
    "On ajoute un bruit aléatoire sur 20% des observations. Nos échantillons d'entraînement correspondent donc à la courbe suivante :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5c0436d4",
   "metadata": {},
   "outputs": [],
   "source": [
    "y[::5] += 3 * (0.5 - np.random.rand(8))\n",
    "plt.scatter(X, y, color='darkorange', label='Données')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c3f0c003",
   "metadata": {},
   "source": [
    "Nous pouvons facilement entraîner trois modèles de SVM pour la régression grâce à la classe `SVR` de scikit-learn :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e85d620a",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Création des SVM\n",
    "C = 1e3\n",
    "svr_rbf = SVR(kernel='rbf', C=C, gamma=0.1)\n",
    "svr_lin = SVR(kernel='linear', C=C)\n",
    "svr_poly = SVR(kernel='poly', C=C, degree=2)\n",
    "\n",
    "# Entraînement des SVM sur les observations bruitées\n",
    "y_rbf = svr_rbf.fit(X, y).predict(X)\n",
    "y_lin = svr_lin.fit(X, y).predict(X)\n",
    "y_poly = svr_poly.fit(X, y).predict(X)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e0d48420",
   "metadata": {},
   "source": [
    "Afficher les résultats :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "37482d05",
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.scatter(X, y, color='darkorange', label='Données')\n",
    "plt.plot(X, y_rbf, color='navy', lw=2, label='RBF')\n",
    "plt.plot(X, y_lin, color='c', lw=2, label='Linéaire')\n",
    "plt.plot(X, y_poly, color='cornflowerblue', lw=2, label='Polynomial')\n",
    "plt.xlabel('X')\n",
    "plt.ylabel('y')\n",
    "plt.title('Support Vector Regression')\n",
    "plt.legend()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7612801a",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Pourquoi employer une valeur aussi grande pour le parametre `C` (ici, `C = 1000`) ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "12876942",
   "metadata": {},
   "source": [
    "### Diabetes dataset\n",
    "\n",
    "`sklearn` contient plusieurs jeux de données réels d'exemple. Concentrons-nous sur le jeu de données *Diabetes* consistant à prévoir la progression de la maladie (représentée par un indice quantitatif) à partir de différentes variables : âge, sexe, pression artérielle, [IMC](https://fr.wikipedia.org/wiki/Indice_de_masse_corporelle) et six valeurs de prélévements sanguins."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c0c990bf",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.datasets import load_diabetes\n",
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "diabetes = load_diabetes()\n",
    "X_train, X_test, y_train, y_test = train_test_split(diabetes.data, diabetes.target, test_size=0.30, random_state=0)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4616d2a2",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Chargez la base de données Diabetes du module `sklearn.datasets` et faites une partition aléatoire en partie apprentissage et partie test (70% apprentissage, 30% test). Construisez un modèle de SVM de régression sur cette base et calculez l'erreur quadratique moyenne sur l'ensemble de test. Utilisez `GridSearchCV` pour déterminer le meilleur noyau à utiliser.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1afcfaf9",
   "metadata": {},
   "source": [
    "## Approfondissement : LibSVM (optionnel)\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",
    "### *One Class* SVM\n",
    "\n",
    "LibSVM est une implémentation sous licence libre qui s'est imposée ces dernières années et qui est utilisée par de nombreux logiciels comme « moteur de classification » (par exemple scikit-learn). Les deux paramètres qui permettent une meilleure adaptation aux données d'apprentissage sont le paramètre `nu` de régularisation et le paramètre de l'échelle du noyau (`gamma`). Pour obtenir et compiler libSVM suivre ces pas :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b742c48c",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%bash\n",
    "cd ~\n",
    "rm -rf tpalgos\n",
    "mkdir tpalgos\n",
    "cd tpalgos\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": "7ad0358a",
   "metadata": {},
   "source": [
    "Pour comprendre les différents algorithmes, 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 évaluons les performances de OCSVM sur la base de données MNIST en utilisant un noyau gaussien. Nous examinons seulement 1 classe : le chiffre 5."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7832b346",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%bash\n",
    "# Récupérer la base de données\n",
    "cd ~\n",
    "cd tpalgos\n",
    "mkdir databases\n",
    "cd databases\n",
    "# Rendre les binaires visibles dans le repertoire courant\n",
    "ln -s ../libsvm-3.22/svm-train .\n",
    "ln -s ../libsvm-3.22/svm-predict .\n",
    "ln -s ../libsvm-3.22/svm-scale .\n",
    "ln -s ../libsvm-3.22/README .\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"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "84149183",
   "metadata": {},
   "source": [
    "OCSVM utilise les étiquettes de +1 pour les éléments de la classe (*inliers*) et -1 pour les *outliers* (hors classe). Nous employons `sed` pour préparer les données avec les bonnes étiquettes (+1) :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1ff82aae",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%bash\n",
    "cat mnist | grep '^5' | shuf -n 2000 | sed -r s/^5/1/g > 5\n",
    "cat mnist.t | grep '^5' | shuf -n 1000 | sed -r s/^5/1/g > 5.t\n",
    "# Nombre échantillons apprentissage et test classe 5\n",
    "wc -l 5\n",
    "# >>> 2000\n",
    "wc -l 5.t\n",
    "# >>> 892"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "81d6b019",
   "metadata": {},
   "source": [
    "Entrainer un modèle OC-SVM à noyau gaussien et paramètres par défaut :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "267f48a4",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%bash\n",
    "./svm-train -s 2 5 5.model_default\n",
    "./svm-predict 5.t 5.model_default 5.output_default\n",
    "# >>> Accuracy = 0% (0/892) (classification)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c372865b",
   "metadata": {},
   "source": [
    "Nous pouvons remarqueer que les paramètres par défaut ne sont pas très adéquats. Les valeurs des données représentent les intensités des pixels dans une image en niveaux de gris (entre 0 et 255). La valeur par défaut pour le paramètre `gamma` (regardez le fichier `5.model_default`) est 0.00133511 (`1/num_features`), ce qui n'est pas très adapté. Nous essayons avec une valeur plus raisonnable :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "046892ab",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%bash\n",
    "./svm-train -s 2 -g 0.0000001 5 5.model_default\n",
    "./svm-predict 5.t 5.model_default 5.output_default\n",
    "# >> Accuracy = 47.1973% (421/892) (classification)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "339992ea",
   "metadata": {},
   "source": [
    "Ceci est plus proche du résultat attendu, car le paramètre `nu` par défaut a une valeur de 0.5 (50% de *outliers*). Une autre approche est de mettre les données à l'échelle entre 0 et 1 sur tous les attributs :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "84e8e975",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%bash\n",
    "./svm-scale -l 0 5 > 5.scaled\n",
    "./svm-scale -l 0 5.t > 5.t.scaled\n",
    "./svm-train -s 2 5.scaled 5.model_scaled\n",
    "./svm-predict 5.t.scaled 5.model_scaled 5.output_scaled\n",
    "# >> Accuracy = 47.7578% (426/892) (classification)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "db61ff3d",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Changez le paramètre `nu` à 0.1. Quelle est l'erreur de classement ? Expliquez.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c57c9fb2",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Testez differents noyaux sur le même probleme.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "50da4b8a",
   "metadata": {},
   "source": [
    "### SVM-toy\n",
    "\n",
    "Dans la suite nous employons l'outil `svm-toy` qui permet de visualiser le classement pour des problèmes en deux dimensions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b15f1b01",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%bash\n",
    "cd ~/tpsvm/libsvm-3.22/svm-toy/gtk\n",
    "make\n",
    "./svm-toy"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4dc58230",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "*Travail à faire* : Pour un ensemble de données bidimensionnelles dans un carré utilisez le noyau gaussien et essayez plusieurs valeurs pour les parametres `nu` (regularisation) et `gamma` (échelle du noyau : plus `gamma` est faible, plus le noyau est « large »). Essayez de vous former une intuition sur la connexion entre l'espacement entre les points d'apprentissage (leur densité locale), les bonnes valeurs pour les parametrès et la probabilité pour que les points soient classés comme des *outliers*. Experimentez avec d'autres formes de classes, qui contiennent eventuellement des détails visibles à plusieurs échelles.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d1f534db",
   "metadata": {},
   "source": [
    "### SVM-toy pour la régerssion\n",
    "\n",
    "Lancez l'utilitaire `svm-toy` avec un paramètre `-s 4` (nu-SVR) et placez des points sur la surface en suivant le contour d'une sinusoïde. Ajouter quelques points de « bruit » un peu plus éloignés. Testez plusieurs valeurs pour le paramètre C. Pour quelle valeur la prédiction SVR semble ignorer le bruit ?"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
