Dans ce TP, on va aborder la classification supervisée en la mettant en œuvre sur le jeu de données olives. Les notions essentielles de classification supervisée ont été vues en L2. Il s'agît donc ici de rappel et surtout de mise en pratique. Si nécessaire, on pourra consulter mes notes de cours de fouille de données pour une présentation de la classification supervisée.
Les 2 TPs précédents (tableaux de données et graphiques) doivent impérativement avoir été faits.
À l'issue de ce TP, vous m'envoyez par email un compte-rendu (format odt ou pdf) indiquant la réponse aux questions qui sont posées. Vous m'envoyez également un fichier python ou R réalisant toutes les manipulations de ce TP : je dois pouvoir exécuter le fichier Python en tapant python3 nom-de-votre-fichier.py ou le fichier R en tapant source ("nom-de-votre-fichier.R") dans R et reproduire vos résultats. Cette exécution ne doit pas provoquer d'erreur. Remarque : un notebook ne convient pas.
Les olives du jeu de données proviennent chacune d'une région particulière d'Italie : sud, Sardaigne, nord. Celles-ci sont numérotées et la région d'origine de chaque olive est dans l'attribut 1 dénommé region. Chaque région est découpée en sous-régions ; cette sous-région est indiquée dans les attributs 0 et 2 ; l'attribut 2 est un numéro associé à chaque sous-région dont le nom est donné par l'attribut 0. La carte ci-dessous indique ces régions et certaines sous-régions.
(Cette figure est issue du livre sur ggobi.)
Chaque olive étant par ailleurs caractérisée par sa teneur en certains acides, la question que nous étudions dans ce TP est : à partir de cette composition chimique, peut-on déterminer la provenance géographique de l'olive ?
Si on arrive à déterminer la provenance à partir de la composition chimique de l'olive, on peut garantir que la provenance indiquée est la bonne et pourquoi pas, détecter ainsi des tentatives de fraudes si ce n'est pas le cas.
On va pratiquer en deux phases. Durant la première, on va explorer visuellement le jeu de données afin de déterminer si la réponse à la question posée est visible sur un ou des graphiques. Durant la seconde phase, on va utiliser un algorithme d'apprentissage automatique pour essayer de répondre à la question. Les deux approches ne sont pas exclusives, bien au contraire : la première guide le choix d'une méthode à appliquer lors de la seconde.
Face à un jeu de données, la première chose à faire et de l'explorer visuellement. Dans le cas où on souhaite prédire la valeur d'un attribut, on se pose la question : la réponse à la question qui est posée saute-t-elle aux yeux ?
Donc, on commence par faire des graphiques très simples pour voir s'il est possible de prédire la région de provenance de l'olive.
Cette exploration visuelle permet également d'orienter le choix d'un modèle pour réaliser la tâche de prédiction : si on voit des relations simples, on sait que des modèles simples pourront réaliser les prédictions voulues.
Par graphique très simple, on entend la visualisation de la répartition de la valeur d'un attribut (univarié) en fonction de sa classe. Pour chaque acide, on réalise un graphique comme celui-ci pour l'attribut palmitic :
D'un coup d'œil, on voit que cet attribut ne permet pas de distinguer les olives des 3 classes. Quand on considère une valeur de l'acide palmitique supérieure à 1400, on voit que l'on peut en déduire que l'olive provient de la zone 1 : il n'y a que des points bleus pour ces abscisses : tout va bien. Par contre, si cette valeur est comprise entre 1000 et 1200, les 3 classes sont possibles : pour ces abscisses, on voit des points des 3 couleurs. Donc, si la teneur en acide palmitique est 1100, on ne peut pas déterminer la zone d'origine de l'olive.
En conclusion, l'attribut palmitic ne permet pas de déterminer la région d'origine de l'olive.
À faire : réalisez ce graphique pour chacun des 8 acides et concluez (vérifiez que vous obtenez bien le même graphique que moi pour l'acide palmitique). Y a-t-il un, ou des attributs, qui permet de déterminer la région d'origine ? ou qui peut aider ?
Remarque : en python, pour réaliser ce graphique, j'ai ajouté une colonne numero au tableau de données olives. Cette colonne contient tout simplement le numéro de la donnée. En R, j'ai juste utiliser un vecteur d'entiers de 1 à N (N étant le nombre de données dans le jeu de données).
À faire : sans dévoiler la solution, vous devez trouver un attribut qui sépare l'une des classes des deux autres. Pour celui-ci, vous obtenez un graphique qui ressemble à celui-ci :
Ce graphique montre que la classe 1 (les points bleus) est facilement identifiée grâce à cet attribut.
Quand vous l'avez trouvé, vous pouvez vous concentrer sur les 2 classes qui sont encore mélangées (jaune et rouge) et recommencer le même raisonnement en vous occupant uniquement des données de ces deux classes. Vous devez trouver un attribut qui sépare bien ces 2 classes restantes.
Quels sont ces deux attributs et comment pouvez-vous déterminer la classe à partir de ces deux attributs ?
« Bivariée » signifie que l'on considère deux attributs.
L'idée est ici la même que précédemment, mais au lieu de chercher une relation entre un attribut et la classe, on cherche une relation entre un couple d'attributs et la classe.
Reprenons le schéma réalisé à la fin du TP précédent :
On voit que ces deux attributs ne permettent pas de déterminer la classe de manière simple : les points des 3 couleurs sont mélangés.
On peut quand même voir que pour une valeur de l'acide oléique inférieure à (environ) 7200, l'acide palmitique permet à peu près de bien prédire la classe 1 ou 3. Cela ne répond pas à notre question, mais c'est un élément d'information.
Je pourrais vous demander de chercher s'il existe une paire d'attributs qui permettent de prédire la région d'origine des olives mais je sais que cette recherche sera vaine et qu'elle vous prendra du temps. Donc vous pouvez passer à la suite, ou quand même vérifier qu'il n'y a rien à voir de simple sur aucun de ces graphiques.
Il arrive que l'approche visuelle ne permette pas de voir quelque chose. Quoiqu'il en soit, en complément de l'exploration visuelle, nous allons essayer de prédire la région d'origine des olives à l'aide de méthodes qui ont pour objectif d'apprendre un modèle réalisant ce type de prédiction.
Face à un jeu de données de petite taille comme celui-ci (quelques centaines de lignes, une dizaine de colonnes, quelques classes), on commence par essayer une méthode qui généralement donne de très bons résultats, les arbres de décision. L'induction d'un arbre de décision est très souvent une méthode très efficace, très peu coûteuse en temps de calcul, qui donne de très bonnes performances pour la prédiction de la classe.
Pour cela, on va utiliser la bibliothèque scikit_learn (importé par import sklearn). Pour pouvoir créer des arbres de décision, on fera ensuite : from sklearn import tree.
La documentation est là.
Pour cela, on va utiliser la bibliothèque rpart (à importer avec require (rpart)).
Pour induire un modèle, on fait comme suit : le jeu de données est découpé en deux parties, l'une utilisée pour induire le modèle, la seconde pour estimer la probabilité qu'il commette une erreur lors d'une prédiction.
La première partie constitue les données d'entraînement, la seconde les données de test.
Il est classique d'utiliser 80% des données disponibles comme données d'entraînement, les 20% restant constituant les données de test.
La répartition entre les deux parties est faite aléatoirement. Il est important de vérifier que les données sont stratifiées, c'est-à-dire que la proportion des données de chaque classe est à peu près la même dans les deux parties.
Pour découper le jeu de données en ces deux parties, on utilise la fonction train_test_split () de la bibliothèque sklearn.model_selection.
On fera donc : from sklearn.model_selection import train_test_split.
et ensuite, pour partitionner le jeu d'exemples en 20% pour le jeu de test et 80% pour le jeu d'entraînement :
olivesX_train, olivesX_test, olivesY_train, olivesY_test = \
train_test_split (olives.iloc [:,3:11], olives.region, test_size = .2, random_state = 123)
Suite à cet appel, on obtient 4 objets :
olivesX_train est un tableau de données qui contient 80% des olives. Seuls les 8 attributs numérotés 3 à 10 ont été sélectionnés, c'est-à-dire les attributs indiquant la composition chimique des olives.olivesX_test est un tableau de données qui contient les 20% restants des olives.olivesY_train est un tableau de données comprenant une seule colonne (region) qui indique la classe de chaque élément de olivesX_train.olivesY_test est un tableau de données comprenant une seule colonne (region) qui indique la classe de chaque élément de olivesX_test.
olivesX_train et olivesY_train constituent le jeu de données d'entraînement avec lequel on construit un modèle de prédiction de la classe.
olivesX_test et olivesY_test constituent le jeu de données de test avec lequel on mesure la qualité du modèle (est-ce que le modèle prédit bien la classe d'une donnée ? ou dit autement, quelle est la probabilité que le modèle fasse une prédiction erronnée).
Nous pouvons maintenant induire l'arbre de décision. Il faut tout d'abord importer la fonction qui induit des arbres de décision avec from sklearn import tree, puis l'induction de l'arbre se fait comme quit :
arbre = tree.DecisionTreeClassifier() arbre = arbre.fit (olivesX_train, olivesY_train)
La première ligne crée un objet arbre de décision et l'affecte à la variable dénommée arbre. La seconde induit l'arbre en utilisant les données et étiquettes du jeu d'entraînement.
À faire :
olivesX_train, olivesX_test, olivesY_train et olivesY_test.olivesY_train. Il y a beaucoup de paramètres qui peuvent être réglés mais nous nous en tiendrons à leur valeur par défaut dans un premier temps (cf. ci-dessus).
Pour découper le jeu de données en deux parties, on va créer deux listes d'indices : les indices des exemples du jeu d'entraînement d'une part, les indices du jeu de test d'autre part.
Les exemples sont numérotés de 1 à N. On va sélectionner au hasard 20% de ces entiers qui constitueront les indices du jeu de test. La fonction sample.int() fait cela :
N <- nrow (olives) # nombre de données test.size <- 0.2 # proportion d'exemples dans le jeu de test Ntest <- N * test.size # nombre d'exemples dans le jeu de test Ntrain <- N - Ntrain # nombre d'exemples dans le jeu d'entraînement olivesTrain.indices <- sample.int (N, Ntrain) olivesTest.indices <- setdiff (1:N, olivesTrain.indices)
Pour olivesTrain.indices, on a sélectionné au hasard Ntrain entiers parmi les entiers de 1 à N. Ainsi, olivesTrain.indices contient les indices des exemples du jeu d'entraînement.
La fonction setdiff(A, B) calcule la différence (le complémentaire) entre deux ensembles A et B : elle renvoie la liste des éléments de l'ensemble A qui ne sont pas élément de l'ensemble B.
On va maintenant induire un arbre de décision en utilisant les exemples dont les indices sont dans olivesTrain.indices. Ensuite, on utilise les exemples dont les indices sont dans pour mesurer la qualité du modèle (est-ce que le modèle prédit bien la classe d'une donnée ? ou dit autement, quelle est la probabilité que le modèle fasse une prédiction erronnée). On induit l'arbre de cette manière :
arbre <- rpart (Area.Name ~ . - area - region, olives, subset = olivesTrain.indices)
qui signifie :
olives est le jeu d'exemples utilisés ;subset = olivesTrain.indices indique les indices des exemples à utiliser pour induire l'arbre de décision ;Area.Name ~ . - area - region est une formule qui indique que l'on veut prédire l'attribut Area.Name en fonction de tous les autres attributs (le .) exceptés (le symbole -) les attributs area et region.À faire :
Avant même de regarder à quoi l'arbre ressemble, il est important de commencer par déterminer si cet arbre est capable de prédire la classe de manière précise. On va donc estimer la probabilité qu'il commette une erreur lorsqu'il prédit la classe d'une donnée. Pour cela, on :
Une fois un arbre de décision induit, on peut l'utiliser pour prédire la classe de données en utilisant la méthode predict (). Cette méthode prend simplement un tableau de données. Par exemple, arbre.predict (olivesX_train) calcule la classe prédite par l'arbre sur les données contenues dans le tableau de données olivesX_train.
olivesX_test,olivesY_test.
Une fois un arbre de décision induit, on peut l'utiliser pour prédire la classe de données en utilisant la fonction predict (). Cette fonction prend simplement un arbre et un ensemble d'exemples en paramètre. Par exemple, predict (arbre, olives [olivesTrain.indices, ]) calcule la classe prédite par l'arbre sur les exemples contenues dans le tableau de données olivesTrain.indices. Plus précisément, cette fonction estime la probabilité que chaque exemple appartienne à chacune des classes ; elle renvoie un tableau.
predict() renvoie un tableau contenant une estimation de la probabilité que l'exemple soit de chacune des classes. On considère la classe dont la probabilité estimée est la plus grande. Si la classe prédite et différente de la classe figurant dans le jeu d'exemples, on compte 1 erreur.
À faire : faites ce qui vient d'être expliqué. Quelle est la valeur de cette estimation ?
Ce point (diagnostic du modèle) est très important et nous y reviendrons. Nous n'avons ici qu'abordé le problème de manière très simple.
On peut obtenir une visualisation de l'arbre de décision en faisant par exemple :
import matplotlib.pyplot as plt fig, ax = plt.subplots () tree.plot_tree (arbre) fig.show ()
plot (arbre) text (arbre)
qui ouvre une fenêtre avec le graphique suivant :
À faire : réalisez ce graphique et le comprendre. En particulier, comprendre ce qui est affiché dans chaque nœud.
Les tests réalisés à chaque nœud sont difficiles à comprendre. Pour afficher le nom des attributs, on ajoutera feature_names = list (olivesX_train) dans l'appel de la méthode plot_tree(), c'est-à-dire la liste des attributs du tableau de données qui a été utilisé pour construire l'arbre de décision. Et on obtient :
En consultant la documentation, on peut avoir une représentation plus jolie et informative avec la classe prédite qui est indiquée dans chaque feuille, comme celle-ci :
Cet arbre est facile à interpréter. Chaque nœud contient un test. Si le test est vrai, on descend dans la branche gauche, s'il est faux, on descend dans la branche droite. Et ainsi de suite jusqu'à atteindre une feuille qui indique la classe prédite (la plus probable puisque R estime la probabilité d'appartenir à chacune des classes).
En cherchant un peu et en utilisant la bibliothèque rpart.plot(), on peut obtenir une représentation de l'arbre beaucoup plus jolie avec la fonction rpart.plot(). Par exemple :
Cette fonction offre de très nombreuses options pour ajuster la représentation graphique de l'arbre (et si vous trouvez qu'il n'y en a pas assez, regardez la fonction prp()).
Faites-le.
Un arbre de décision peut facilement être transformé en un ensemble de règles de décision. Ici l'arbre est très petit, c'est facile.
À faire : quelles sont ces règles ? Interprétez graphiquement ces règles. Retrouvez-vous une observation faite précédemment ?
Nous avons construit un arbre de décision qui donne de très bons résultats. Est-ce juste de la chance ou est-ce que décidément, cette tâche se résout facilement à l'aide d'un arbre de décision ?
C'est une question à laquelle il n'est pas facile de répondre. Nous ne disposons pas de méthode mathématiquement sûre, mais il y a des manières de procéder qui font concensus. C'est l'objet du prochain TP.