{ "nbformat": 4, "nbformat_minor": 0, "metadata": { "colab": { "name": "7_Machine_Learning_Introduction.ipynb", "provenance": [], "collapsed_sections": [], "toc_visible": true, "include_colab_link": true }, "kernelspec": { "name": "python3", "display_name": "Python 3" } }, "cells": [ { "cell_type": "markdown", "metadata": { "id": "view-in-github", "colab_type": "text" }, "source": [ "\"Open" ] }, { "cell_type": "markdown", "metadata": { "id": "DfQ-d2onvooo", "colab_type": "text" }, "source": [ "Mickaël Tits\n", "CETIC\n", "mickael.tits@cetic.be" ] }, { "cell_type": "markdown", "metadata": { "id": "Ug1Bj8_tAVWq", "colab_type": "text" }, "source": [ "# Chapitre 7 - Introduction au Machine Learning\n", "\n", "Un sous-domaine important de l'analyse de données est le machine learning (apprentissage automatique). Le machine learning regroupe un ensemble de techniques permettant, à partir des données, d'apprendre automatiquement différents types de relations entre les variables.\n", "\n", "On distingue généralement:\n", "* Le machine learning **supervisé**, qui permet d'apprendre des relations entre les données et des labels. Plus spécifiquement, on entraîne généralement un algorithme à prédire les **labels** (aussi appelés **variables dépendantes**, ou **targets**) un ensemble de **variables** (appelées **variables prédictives**, ou **features**). Un modèle consiste en un algorithme doté de **paramètres** et qui, à partir d'opérations entre les paramètres et les variables prédictives, prédit un nouveau label. L'apprentissage consiste alors à modifier les paramètres du modèle de manière à ce que les labels prédits soient les plus proches des labels réels. Autrement dit, on **minimise l'erreur (absolue ou quadratique) moyenne des prédictions**. Cette minimisation de l'erreur se fait généralement **de manière itérative**, par **descente de gradient**.\n", "\n", " Il existe deux applications principales de l'apprentissage supervisé:\n", " \n", " * La **classification**: on a un ensemble de données, par exemple des images, à partir desquelles on aimerait prédire une classe: le **label** de chaque image est alors une variable catégorielle, par exemple \"chat\" ou \"chien\". Les **variables prédictives** permettant d'entraîner l'algorithme sont par exemple les pixels des images, ou des relations entre ces pixels.\n", " \n", " * La **régression**: le **label** à prédire à partir des données est une valeur continue: par exemple le prix d'une maison qu'on peut prédire à partir de différentes caractéristiques, ou l'âge d'une personne à prédire à partir d'une image.\n", "\n", "* Le machine learning **non-supervisé**, qui permet d'apprendre la structure desdonnées, ou des relations entre différentes données, telles que les similitudes entre différentes variables, ou différentes données (e.g.: Clustering, Réduction de dimensions, Détection d'anomalies). Ces techniques ne nécessitent pas de labelliser les données.\n" ] }, { "cell_type": "markdown", "metadata": { "id": "SjzPpoDJYNA-", "colab_type": "text" }, "source": [ "Chargez d'abord le dataframe préparé lors du chapitre précédent." ] }, { "cell_type": "code", "metadata": { "id": "p8HniutIYMdW", "colab_type": "code", "colab": {} }, "source": [ "import pandas as pd\n", "\n", "#Si vous venez d'exécuter le notebook précédent, vous pouvez simplement récupérer le fichier temporaire créé.\n", "#df = pd.read_csv(\"houses_features.csv\", index_col = 0)\n", "\n", "#Vous pouvez aussi récupérer une version du fichier hébergée ici:\n", "df = pd.read_csv(\"https://raw.githubusercontent.com/titsitits/Python_Data_Science/master/Donn%C3%A9es/houses_features.csv\", index_col = 0)\n", "dfbxl = df[df.city == \"Bruxelles\"]\n", "df" ], "execution_count": 0, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "aL8QR73xbp57", "colab_type": "text" }, "source": [ "# Prédiction du prix d'une maison" ] }, { "cell_type": "markdown", "metadata": { "id": "KVxiLN5Bbg5U", "colab_type": "text" }, "source": [ "## Préparation du dataset" ] }, { "cell_type": "code", "metadata": { "id": "Z9ChZn7dl5xz", "colab_type": "code", "colab": {} }, "source": [ "#L'adresse n'a pas d'intérêt (on a déjà extrait la ville, qui est une catégorie intéressante)\n", "dataset = df.drop(\"address\",axis=1)\n", "#dataset = dfbxl.drop(\"address\",axis=1)\n" ], "execution_count": 0, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "YII0m04S5ElQ", "colab_type": "text" }, "source": [ "De nombreux algorithmes de machine learning nécessitent l'utilisation exclusive de **variables prédictives numériques**. En effet, ces algorithmes se basent entièrement sur des opérations algébriques à partir de ces variables pour prédire la variable dépendante.\n", "Afin de pouvoir utiliser l'information contenue dans les variables catégorielles, il existe différentes techniques pour les transformer en variables continues:\n", "\n", "* **One-hot encoding** (dummy variable extraction): Pour chaque catégorie possible d'une variable catégorielle, on crée une variable binaire, indiquant si l'échantillon est de cette catégorie ou non. Par exemple, la variable \"`Ville`\" [\"Bruxelles\" ou \"Namur\"] peut être simplement transformée en une variable binaire \"`is_bruxelles`\": \"Bruxelles\" => 1, \"Namur\" => 0\n", "\n", " Si `Ville` contient trois possibilités (e.g., [\"Bruxelles\" ou \"Namur\" ou \"Fleurus\"]), elle nécessitera la création de variables binaires, e.g.: `is_bruxelles` et `is_namur`. Pour chaque observation, une seule des deux variables ne peut être à 1 (puisque la variable `Ville` ne peut être qu'une ville à la fois). Si les deux sont fausses, c'est que la ville est Fleurus.\n", " \n", " De manière générale, une variable à n catégories nécessitera la création de n-1 variables binaires. On appelle généralement ces variables **dummy**.\n", " \n", "* **Echelle** : certaines variables sont ordinales, comme `quality` [`bien`, `moyen`, `mauvais`]. Celle-ci peut être directement transformée un variable numérique: [bien => 0, moyen = >1, mauvais => 2]. Si ce n'est pas le cas, l'interprétation sémantique de la variable, et sa combinaison avec une autre variable continue permet de définir une échelle. Par exemple, on peut extraire certaines caractéristiques numériques d'une ville à partir du prix moyen des maisons, ou du nombre d'habitants: [\"Bruxelles\" => 300000, \"Namur\" => 250000, \"Fleurus\" => 200000].\n", "\n", "\n" ] }, { "cell_type": "code", "metadata": { "id": "3ntQNgTI5CeS", "colab_type": "code", "colab": {} }, "source": [ "#One-hot encoding des variables catégorielles\n", "dataset = pd.get_dummies(dataset, drop_first=True)\n", "dataset" ], "execution_count": 0, "outputs": [] }, { "cell_type": "code", "metadata": { "id": "cinRBDirvnUR", "colab_type": "code", "colab": {} }, "source": [ "dataset.corr()" ], "execution_count": 0, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "PWDhrdhzaEBD", "colab_type": "text" }, "source": [ "## Une framework Python de référence pour le machine learing: Scikit-learn\n", "\n", "Une librairie Python incontournable pour le machine learning est la librairie Scikit-learn: https://scikit-learn.org/stable/ .\n", "\n", "Elle inclut un geand nombre d'algorithmes de machine learning supervisé, non-supervisé, de préprocessing de caractéristiques, et de sélection de modèles de machine learning. L'API est assez simple, et est basée sur les librairies vues précédemment (Numpy, Scipy, Matplotlib).\n", "\n", "La librairie contient de nombreuses classes permettant d'instancier des modèles de machine learning, que l'on peut ensuite utiliser pour: \n", "1. Entraîner sur un ensemble de **données d'entraînement (ou training set)**.\n", "\n", "2. Chaque modèle contient généralement différents paramètres qui peuvent être adaptés aux données utilisées, afin d'améliorer les performances de l'algorithme. Pour vérifier les performances du modèles selon différents paramètres, on teste le modèle sur un set de **données de validation (ou validation set)**. On parle généralement de processus de **cross-validation**.\n", "\n", "3. Afin de vérifier que le modèle obtenu est efficace, on peut ensuite le tester avec de nouvelles données (autres que celles utilisées durant l'entraînement et la cross-validation), les **données de test (ou test set)**." ] }, { "cell_type": "markdown", "metadata": { "id": "z43cMjWtqHGw", "colab_type": "text" }, "source": [ "### Régression linéaire" ] }, { "cell_type": "markdown", "metadata": { "id": "W3MQD_61rQ-6", "colab_type": "text" }, "source": [ "Etant donné le peu de données de l'exemple, on utilisera un modèle simple (régression linéaire), avec un nombre restreint de caractéristiques (surface, rooms, et éventuellement la ville). On ne contentera d'un training set et d'un test set (pas de validation ici: on gardera les paramètres par défaut du modèle)." ] }, { "cell_type": "code", "metadata": { "id": "3nRN3Xn6b9bk", "colab_type": "code", "colab": {} }, "source": [ "from sklearn.linear_model import LinearRegression\n", "\n", "#On crée un objet de la classe LinearRegression\n", "regressor = LinearRegression()\n" ], "execution_count": 0, "outputs": [] }, { "cell_type": "code", "metadata": { "id": "TVGUPjD2bnXE", "colab_type": "code", "colab": {} }, "source": [ "trainsize = 6\n", "\n", "trainset = dataset.iloc[:trainsize]\n", "testset = dataset.iloc[trainsize:]\n", "\n", "features = [\"surface\",\"rooms\"]\n", "#Pour tester un autre set de features, décommenter la ligne suivante\n", "features = [\"surface\",\"rooms\",\"city_Fleurus\",\"city_Namur\"]\n", "\n", "Xtrain, ytrain, Xtest, ytest = trainset[features], trainset[\"price\"], testset[features], testset[\"price\"]" ], "execution_count": 0, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "W7DakFy-Eg4o", "colab_type": "text" }, "source": [ "#### Entraînement" ] }, { "cell_type": "code", "metadata": { "id": "JFBVkNPZp3Mi", "colab_type": "code", "colab": {} }, "source": [ "regressor.fit(Xtrain, ytrain)" ], "execution_count": 0, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "JKN5aS1WEkef", "colab_type": "text" }, "source": [ "####Prédictions" ] }, { "cell_type": "code", "metadata": { "id": "hp-fiF2xqn4L", "colab_type": "code", "colab": {} }, "source": [ "trainpred = regressor.predict(Xtrain)\n", "testpred = regressor.predict(Xtest)\n", "print(testpred[0], ytest.values[0])" ], "execution_count": 0, "outputs": [] }, { "cell_type": "code", "metadata": { "id": "u1_XPhrBq9Y8", "colab_type": "code", "colab": {} }, "source": [ "regressor.coef_" ], "execution_count": 0, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "5-bMh1zTEmr3", "colab_type": "text" }, "source": [ "#### Evaluation des prédictions" ] }, { "cell_type": "code", "metadata": { "id": "p8UdTc9dBu0R", "colab_type": "code", "colab": {} }, "source": [ "from sklearn.metrics import mean_absolute_error\n", "\n", "mae_train = mean_absolute_error(trainpred,ytrain)\n", "mae_test = mean_absolute_error(testpred,ytest)\n", "\n", "mae_train, mae_test" ], "execution_count": 0, "outputs": [] }, { "cell_type": "code", "metadata": { "id": "dYFCaKf7FhWT", "colab_type": "code", "colab": {} }, "source": [ "trainpred" ], "execution_count": 0, "outputs": [] }, { "cell_type": "code", "metadata": { "id": "2_Drmv0LF3o5", "colab_type": "code", "colab": {} }, "source": [ "testpred" ], "execution_count": 0, "outputs": [] }, { "cell_type": "code", "metadata": { "id": "iQN47OsoQZ1O", "colab_type": "code", "colab": {} }, "source": [ "ytest" ], "execution_count": 0, "outputs": [] }, { "cell_type": "code", "metadata": { "id": "GebyAFN7CYYI", "colab_type": "code", "colab": {} }, "source": [ "from matplotlib import pyplot as plt\n", "\n", "plt.scatter(ytrain,trainpred)\n", "plt.scatter(ytest,testpred)\n", "\n", "\n", "plt.legend([\"train\",\"test\"])\n", "plt.xlabel(\"Labels\")\n", "plt.ylabel(\"Prédictions\")\n", "\n", "#Des prédictions parfaites devraient se situer sur la droite suivante (erreur de prédiction nulle)\n", "plt.plot([200000,600000],[200000,600000], 'g')" ], "execution_count": 0, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "7jwJYoauQ3cf", "colab_type": "text" }, "source": [ "Le modèle semble indiquer qu'une des maisons du testset est sous-évaluée: le modèle estime sa valeur plus haute que le prix auquel elle est vendue." ] }, { "cell_type": "code", "metadata": { "id": "rpj2otZvVWA7", "colab_type": "code", "colab": {} }, "source": [ "output = df.iloc[trainsize:]\n", "output = output.assign(predictions = testpred)\n", "output[\"delta\"] = output[\"price\"] - output[\"predictions\"]\n", "output" ], "execution_count": 0, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "thSKg_ZQWLne", "colab_type": "text" }, "source": [ "La maison à la Rue de Fer 29, Namur\tserait donc fortement sous-évaluée. On remarque en l'occurrence qu'elle a un prix par pièce particulièrement faible." ] }, { "cell_type": "code", "metadata": { "id": "Uxyj34OcWryT", "colab_type": "code", "colab": {} }, "source": [ "df" ], "execution_count": 0, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "_ScIRJCUDmhx", "colab_type": "text" }, "source": [ "# Importance des variables prédictives\n", "\n", "On peut évaluer l'impact des variables prédictives sur la prédiction à partir de leur coefficient dans la régression linéaire. Néanmoins, pour que les coefficients soient comparables, il faut au préalable mettre à la même échelle les variable prédictives." ] }, { "cell_type": "code", "metadata": { "id": "QuOMDeh7EHSZ", "colab_type": "code", "colab": {} }, "source": [ "from sklearn.preprocessing import StandardScaler\n", "\n", "#Scaling\n", "scaler = StandardScaler()\n", "Xtrain_scaled = scaler.fit_transform(Xtrain)\n", "Xtest_scaled = scaler.transform(Xtest)\n", "\n", "#Entraînement\n", "regressor.fit(Xtrain_scaled, ytrain)\n", "\n", "#Prédictions\n", "trainpred = regressor.predict(Xtrain_scaled)\n", "testpred = regressor.predict(Xtest_scaled)\n", "\n", "(features,regressor.coef_)" ], "execution_count": 0, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "brQfBgjyEakn", "colab_type": "text" }, "source": [ "Le coefficient le plus grand en valeur absolue est le deuxième, et semble indiquer que c'est le nombre de pièces qui influence le plus le prix." ] }, { "cell_type": "markdown", "metadata": { "id": "X1uEKyBx4FnD", "colab_type": "text" }, "source": [ "# Descente de gradient" ] }, { "cell_type": "code", "metadata": { "id": "ddB3nBOJ4Iqo", "colab_type": "code", "colab": {} }, "source": [ "from sklearn.linear_model import SGDRegressor\n", "\n", "#On crée un objet de la classe SGDRegression\n", "regressor = SGDRegressor(loss = \"squared_loss\", learning_rate = \"constant\", eta0 = 0.01, max_iter = 300)\n", "regressor.fit(Xtrain_scaled, ytrain)\n", "print(regressor.predict(Xtest_scaled)[0], ytest.values[0])" ], "execution_count": 0, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "8SWA7APirNcj", "colab_type": "text" }, "source": [ "# k-Fold (ou leave-one-out) validation\n", "Lorsque l'ensemble de données est petit (comme dans cet exemple), il est intéressant d'utiliser une technique de \"tournante\" sur les données de test pour vérifier la qualité de l'algorithme choisi. Pour un ensemble de n observations, on divise l'échantillon en k sous-ensembles (de taille n/k), et pour chacun de ceux-ci, on entraîne un modèle sur toutes les autres données et on calcule les prédictions sur le sous-ensemble retenu. On entraîne donc k modèles pour prédire les outputs de chaque sous-ensemble gardé comme set de test. On peut ensuite calculer une métrique (e.g., MAE ou MSE) sur l'ensemble des données. Le **leave-one-out** est un cas particulier de la **k-fold** validation, où on garde à chaque itération une seule observation de test et on entraîne sur toutes les autres (k = n). On doit donc entraîner n modèles au total." ] }, { "cell_type": "code", "metadata": { "id": "GvdvVEv4qRWz", "colab_type": "code", "colab": {} }, "source": [ "#Leave-one-out validation\n", "from sklearn.metrics import mean_squared_error\n", "import numpy as np\n", "\n", "testpredictions = []\n", "\n", "input_features = features\n", "\n", "\n", "for i in dataset.index:\n", " \n", " #trainset: tout sauf i\n", " trainset = dataset[dataset.index!=i]\n", " \n", " #testset: i (une seule observation)\n", " testset = dataset.loc[i].to_frame().transpose()\n", " Xtrain, ytrain, Xtest, ytest = trainset[input_features], trainset[\"price\"], testset[input_features], testset[\"price\"]\n", " \n", " scaler = StandardScaler()\n", " Xtrain_scaled = scaler.fit_transform(Xtrain)\n", " Xtest_scaled = scaler.transform(Xtest)\n", "\n", " regressor.fit(Xtrain_scaled, ytrain)\n", " \n", " trainpred = regressor.predict(Xtrain_scaled)\n", " testpred = regressor.predict(Xtest_scaled)\n", " \n", " print(i, \" - training mean error:\", np.mean(np.abs(trainpred - ytrain)), \"\\n\\t test mean error:\", np.mean(np.abs(testpred - ytest)), \"\\n\")\n", " \n", " #On ajoute la prédiction pour i à la liste des prédictions de test\n", " testpredictions.append(testpred[0])\n", " " ], "execution_count": 0, "outputs": [] }, { "cell_type": "code", "metadata": { "id": "5em1yoQltVk6", "colab_type": "code", "colab": {} }, "source": [ "testpredictions" ], "execution_count": 0, "outputs": [] }, { "cell_type": "code", "metadata": { "id": "1cu-R5nWt60W", "colab_type": "code", "colab": {} }, "source": [ "mean_absolute_error(dataset[\"price\"],testpredictions)" ], "execution_count": 0, "outputs": [] }, { "cell_type": "markdown", "metadata": { "id": "BLSgoaulk4Sd", "colab_type": "text" }, "source": [ "Si les performances sur le test set sont bien plus mauvaises que sur les données d'entraînement, il est fort probable qu'on soit dans une situation d'**overfitting**: le modèle a appris \"par coeur\" ce qu'il fallait prédire pour chaque donnée du training set. Il ne se généralise donc pas bien sur de nouvelles données. Afin d'éviter ce problème, on peut soit:\n", "\n", "* diminuer la complexité (le nombre de degrés de liberté, i.e. de paramètres) du modèle, soit en diminuant le nombre de caractéristique, soit en utilisant un algorithme plus simple.\n", "* entraîner le modèle sur plus de données.\n", "\n", "Si à l'inverse, les performances obtenues lors de l'entraînement sont faibles, on est en situation d'**underfitting**. Il faut alors au contraire augmenter la complexité au modèle et extraire des caractéristiques de meilleure qualité (ayant une relation pus forte avec le label à prédire).\n", "\n", "Le dataset minimaliste utilisé ici a permis de présenter très simplement certains concepts de base du machine learning. Néanmoins, en pratique, l'utilisation du machine learning nécessite généralement de beaucoup plus grands ensemble de données, permettant notamment de développer des modèles plus complexes qu'une régression linéaire. Dans le Chapitre suivant, nous utiliserons un dataset réel et plus pertinent pour explorer quelques concepts fondamentaux du machine learning.\n" ] }, { "cell_type": "markdown", "metadata": { "id": "d7N2l8ycx0CD", "colab_type": "text" }, "source": [ "Vous pouvez maintenant passer au [Chapitre 8 : Un exemple concret: estimation du prix d'une maison à Ames (Iowa, USA)](https://colab.research.google.com/github/titsitits/UNamur_Python_Analytics/blob/master/8_Example_Ames_Housing_Dataset.ipynb)\n" ] } ] }