TP 3 : Arbres de décision

Introduction aux concepts de l’Intelligence Artificielle

29 Février 2024

  1. Découvrir les arbres de décision
    • Connaître la définition des arbres de décision
    • Savoir trouver comment un arbre de décision va classifier un vecteur x⃗ de données.
  2. Découvrir la librairie Python Scikit Learn
    • Savoir importer les parties pertinentes de la librairie
    • Savoir importer un jeu de données
    • Savoir afficher graphiquement le jeu de données
  3. Mettre en place une solution simple d’apprentissage
    • Savoir utiliser Scikit Learn pour apprendre un arbre de décision depuis un jeu de données.
    • Savoir mettre en place une évaluation du modèle obtenu.

Introduction

Une tâche important en IA est de pouvoir classifier des objets : reconnaître des images, détecter des spams, trouver la famille d’une plante etc. Souvent, on ne sait pas vraiment définir explicitement une fonction permettant de classifier un objet. En effet, le processus que nous utilisons mentalement pour reconnaître un chat sur une image est difficile à décrire et d’autant plus à implémenter sur une machine pour laquelle une image n’est rien d’autre, au départ, qu’un tableau de nombres : on repère des formes, les yeux et la tête, on regarde la forme des oreilles etc. Autant de concepts qu’il faudrait réussir à formaliser dans un programme.

On va s’intéresser ici à l’apprentissage automatique d’une fonction de classification à partir d’exemples en utilisant une famille d’hypothèses appelées arbres de décision. On va voir comment utiliser les arbres de décision pour apprendre à classifier depuis des exemples, comment on peut mettre cet apprentissage en place avec python en utilisant la librairie scikit learn pour apprendre un arbre de décision depuis un jeu de données et comment évaluer la qualité de la fonction apprise par cross validation.

Arbres de décision

Un arbre de décision est une structure de données permettant de classifier des vecteurs de données la forme (x1,…,xn) ∈ D1 × … × Dn dans des classes 𝒞 en répondant à une succession de questions. On utilise parfois leurs cousins, les flowcharts, pour expliquer le fonctionnement de certains systèmes (voir par exemple l’excellent SMBC Comics expliquant que l’IA détruira l’humanité). Les arbres de décision sont des structures de données particulièrement adaptée pour les problèmes de décision faisant intervenir des attributs qui prennent un nombre de valeur finie. Ils ont aussi l’avantage d’être facilement compréhensible par un humain : on peut exactement voir comment un arbre de décision arrive à une conclusion depuis un vecteur de données. On dit souvent qu’ils sont, en ce sens, interprétables. Cette affirmation est cependant rarement formalisée et à prendre avec des pincettes, la façon un arbre prend ses décisions a toutefois été apprise sur des données et il reste difficile d’y détecter des biais.

Formellement, un arbre de décision T est un arbre dont les noeuds internes sont étiquettés par des questions de la forme xi = v, xi ≤ v ou xi ≥ v et les noeuds terminaux sont étiquetés par une classe c ∈ 𝒞. Chaque noeud a deux fils : un fils négatif, représenté à droite, et un fils positif, représenté à gauche. Étant donné un vecteur x = (x1,…,xn) de valeurs, T classifie x dans une classe c ∈ 𝒞 si on arrive dans une feuille étiquetée par c en suivant le processus suivant :

Par exemple, dans l’exemple suivant, un vecteur {"couleur": "jaune", "poids": "200g", "taille": "20cm", "diamQueue": "1cm"} sera classé en tant que banane. En effet, on commencera par descendre à droite puisque la couleur n’est pas verte, puis à gauche (la couleur est jaune) puis à droite (la taille est plus grande que 15cm). Vous pouvez passer votre souris sous l’image ci-dessous pour voir le chemin se colorer.

l P Poids > 500 Poivron1 Poivron P->Poivron1 oui Chou Chou P->Chou non T Taille < 15 Poivron2 Poivron T->Poivron2 oui Banane Banane T->Banane non Q diamQueue < 0.5 Poivron3 Poivron Q->Poivron3 oui Pomme Pomme Q->Pomme non PV couleur = jaune PV->T oui PV->Q non V couleur = vert V->P oui V->PV non

Comment seront classés les vecteurs suivants :

  1. {"couleur": "rouge", "poids": "200g", "taille": "20cm", "diamQueue": "0.3cm"}
  2. {"couleur": "vert", "poids": "100g", "taille": "15cm", "diamQueue": "1cm"}
  3. {"couleur": "vert", "poids": "2000g", "taille": "30cm", "diamQueue": "0cm"}

Comment serait classé ce poivron à partir de sa représentation ? Le langage de représentation vous semble-t-il suffisamment riche pour classifier les légumes considérés dans l’arbre précédent ?

Classifier des iris !

Dans cette partie, on va s’intéresser à un jeu de données classique pour les arbres de décision : le jeu de données IRIS. Ce jeu de données contient des exemples d’iris classés en trois espèces : les iris setosa, versicolor et virginica. Les attributs qui sont utilisés dans le jeu de données sont : la longueur et largeur des pétales, la longeur et la largeur des sépales.

Iris Setosa
Iris Versicolor
Iris Virginica

On peut trouver une version du jeu de données au format ARFF sur OpenML, ou au format CSV. Il existe de nombreux formats de fichiers pour échanger des jeux de données (ARFF, CSV, svmlight etc.). La plupart des librairies pour l’apprentissage vous permettra d’importer sans trop de problème les données depuis différents formats dans leur format local. Il faut cependant garder à l’esprit que la constitution de tels jeux de données et leur normalisation est en général un travail fastidieux mais nécessaire pour l’apprentissage. Nous utiliserons ici le fait que Scikit Learn propose déjà une fonction pour charger le jeu de données IRIS afin de faciliter la manipulation.

La librairie Scikit Learn

La libriaire Scikit Learn en python vous permet de mettre facilement en place des solutions utilisant l’apprentissage. Cette librairie est très riche et nous l’utiliserons plusieurs fois dans ce cours. Nous proposons ici de découvrir certaines de ses nombreuses fonctionnalités.

Installez la librairie dans Thonny (Outils -> Gérer les paquets) avec le paquet scikit-learn.

Si vous n’utilisez pas Thonny, vous pouvez le faire en ligne de commande avec

On va charger le jeu de données. Ce jeu de données est très populaire et on peut donc le charger directement depuis la librairiescikit-learn. En général, on dispose du jeu de données dans un fichier externe qu’on doit charger. Nous n’aborderons pas cet aspect aujourd’hui.

Pour importer la fonction dont on a besoin et charger le jeu de données, on écrira au début de notre fichier :

from sklearn.datasets import load_iris

iris = load_iris()

En utilisant print(iris), essayez de visualiser un peu le contenu de la variable iris.

On peut voir que iris.data contient un tableau. Quel est sa taille ? Même question avec iris.target.

Aide: la fonction len(t) renvoie la taille d’un tableau.

On remarque que iris.data et iris.target ont le même nombre de lignes. Ce qu’il faut donc comprendre c’est que iris.data[i] contient tous les attributs d’un vecteur exemple et iris.target[i] contient sa classe. On remarque que ces tableaux ne contiennent que des chiffres et qu’on ne voit pas à quoi ils correspondent. On trouvera les détails dans les tableaux iris.feature_names et iris.target_names.

Affichez les tableaux iris.feature_names et iris.target_names. Que contiennent-ils ?

Ce qu’il faut comprendre c’est que la classe qui a le numéro i a le nom iris.target_names[i]. Pareillement, l’attribut en position i a le nom iris.feature_names[i]. Par exemple :

print(iris.feature_names)
>>> ['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)']

print(iris.data[0])
>>> [5.1 3.5 1.4 0.2]

Cela nous indique que la première ligne du jeu de données (iris.data[0]) contient un exemple d’iris dont la longueur des sépales est 5.1cm, la largeur des sépales est 3.5cm, la longueur des pétales est 1.4cm et la largeur des pétales est 0.2cm. De plus :

print(iris.target[0])
>>> 0

print(iris.target_names)
>>> ['setosa' 'versicolor' 'virginica']

Cela nous indique que la classe de la première ligne du jeu de donnée (iris.target[0]) est de classe 0, c’est-à-dire, "setosa".

Donnez pour iris.data[12] : le nom de sa classe, la longueur et largeur de ses pétales et de ses sépales. Même question pour iris.data[22].

Pour chaque classe du problème (setosa, versicolor, virginica), combien y a-t-il de fleurs dans le jeu de données ? Cela est-il équilibré ? Pourquoi cela est-il important ?

Aide: Pour compter le nombre de fleurs de classe 0, on pourra initialiser une variable compteur à 0, faire une boucle qui parcours le tableau iris.target et incrémenter compteur chaque fois qu’on lit 0 dans le tableau. On fera de même pour les classes 1 et 2.

On va essayer de visualiser les données. Pour cela, nous allons utiliser la librairie python matplotlib.

Avec le gestionnaire de paquet de Thonny, installez matplotlib. On importera les fonctions dont on a besoin dans le code avec import matplotlib.pyplot as plt.

On va regarder la répartition de la longueur des sépales en fonction de la largeur des sépales :

ax = plt.subplot() # nécessaire pour tracer des points

# On trace des points dont l'absisse est la longueur des sépales, l'ordonnée la largeur
scatter = ax.scatter(iris.data[:,0], iris.data[:,1], c=iris.target) 

#     - iris.data[:,0] veut dire le premier élément de chaque 
#       tableau de iris.data, soit la longueur des sépales
#     - iris.data[:,1] veut dire le deuxième élément de chaque 
#       tableau de iris.data soit la largeur des sépales
#     - c=iris.target force la couleur en fonction de la classe (iris.target)

# On met en forme le diagramme
ax.set(xlabel="Longeur des sépales", ylabel="Largeur des sépales") # nom des axes
ax.legend(scatter.legend_elements()[0], iris.target_names, loc="lower right", title="Classes") # légende
plt.show() # on affiche !

On obtient :

Si vous souhaitez sauvegarder une image, vous pouvez remplacer plt.show() par plt.savefig("nom.png").

Adaptez le code précédent pour afficher la répartition des largeurs/longueurs de pétales. On adaptera la ligne définissant scatter et on changera le nom des axes.

Vous devriez générer l’image suivante :

Apprendre un arbre de décision

On va utiliser Scikit Learn pour générer un arbre de décision ayant appris à classifier depuis les exemples du jeux de données. On ne rentrera pas ici dans les détails de l’algorithme d’apprentissage pour nous concentrer sur la mise en pratique de la solution. On peut donner quand même une intuition du fonctionnement de ces algorithmes. L’algorithme d’apprentissage commence par construire la racine de l’arbre. Il va essayer de trouver une question xi = v, xi ≤ v, xi ≥ v qui découpe le “mieux” le jeu de données, c’est-à-dire qu’on va essayer de trouver la question qui discrimine le plus les différentes classes. Pour évaluer cela, on utilise en général une méthode qui évalue le degré de mélange des classes. Il en existe principalement deux : l’entropie et la fonction de Gini. Pour un jeu de donné D ayant N exemples dont Nc éléments classé dans la classe c ∈ C et donc une proportion Pc = N/Nc éléments de classe c, on définit Gini(D) = 1 − ∑c ∈ CPc2 et entropy(D) =  − ∑c ∈ CPclog (Pc). Intuitivement, ces fonctions sont maximales quand les proportions de chaque classe sont similaires. Si une classe c a une proportion plus importante que les autres, alors Pc sera proche de 1 et les autres seront proche de 0. On voit alors que Gini(D) et entropy(D) seront petites. On va essayer de privilégier les questions qui séparent le jeu de donnée en D1 et D2 avec un petit degré de mélange. On appliquera la construction récursivement sur D1 et D2 ensuite pour construire un arbre complet jusqu’à ce qu’on décide qu’une classe est suffisamment représenté pour décider que tous les éléments sont dans cette classe. Cela est résumé ci-dessous :

G D Données D Q(D)? D1 D₁ ⊆ D où question Q est vraie D->D1 D2 D₂ ⊆ D où question Q est fausse D->D2
On choisit Q telle que Gain(D,Q) = Gini(D) − |D₁|/|D| × Gini(D₁) − |D₂|/|D| × Gini(D₂) est maximal

Si on se contente de cette méthode d’apprentissage, on risque cependant de surapprendre notre modèle sur les exemples (overfitting) ce qui donnerait des arbres de décision mauvais en pratique sur des vecteurs en dehors de l’ensemble des exemples. La plupart des algorithmes d’apprentissage des arbres de décisions passent ensuite par une phase d’élagage : certains sous-arbres qui semblent être trop spécialisés aux exemples sont enlevés afin de mieux généraliser.

Dans ce TP, nous allons faire confiance aux développeurs de Scikit Learn et utiliser leur implémentation des arbres de décisions.

On importera les fonctions nécessaires avec :

from sklearn.tree import DecisionTreeClassifier, plot_tree

On crée un arbre de décision avec

clf = DecisionTreeClassifier()

Et on l’entraînera sur les exemples avec fit. On lui passera comme argument un tableau contenant les données et un tableau contenant les classes :

clf.fit(donnees, classes)

Enfin, on affichera l’arbre calculé avec la fonction plot_tree. noms_attributs est un tableau contenant le nom des attributs et noms_classes est un tableau contenant le nom des classes, ce qui permet d’avoir un affichage un peu plus lisible :

plt.figure(figsize(20,20)) # zoom pour rendre l'arbre plus lisible 
                           # changer en fonction de la taille de votre écran.
plot_tree(clf, feature_names=noms_attributs, class_names=noms_classes, filled=True, rounded=True)
plt.show()

Utilisez les instructions ci-dessus pour entraîner et afficher l’arbre de décision. On veillera à adapter les variables donnees, classes, noms_attributs et noms_classes au jeu de données considéré ici.

Comment sera classée une fleur où la longueur des sépale est 10, la largeur 4, la longueur des pétales 5 et leur largeur 1 par votre modèle ?

On peut utiliser clf.predict(t) pour trouver la classe de plusieurs point d’entrée (t est un tableau de vecteurs d’attributs, par exemple [[10,4,5,0.1], [12,1,5,1]].

Vérifiez que votre réponse précédente correspond au résultat de clf.predict().

Évaluation du modèle

Le problème avec l’approche précédente est que l’on a utilisé toutes les données à notre disposition pour entraîner notre modèle. On n’a donc aucun moyen de vérifier la qualité de notre modèle. L’approche la plus simple pour résoudre cela est d’utiliser une partie des données pour apprendre et une autre partie des données pour vérifier la qualité du modèle. Pour que cela ait du sens, il faut choisir la partition des données de façon aléatoire, sinon on risque de biaiser notre modèle.

Scikit Learn nous permet facilement de créer ce genre de partition. On utilisera pour cela la fonction train_test_split :

from sklearn.model_selection import train_test_split
donnees_train, donnees_test, classes_train, classes_test = train_test_split(donnees, classes, test_size=0.2, random_state=42)

Cela permet de créer une partition des données et de leur classe en une partie apprentissage et une partie entrainement. Dans l’exemple ci-dessus, on prélève 20% des données pour faire nos tests.

Entraînez de nouveau un arbre de décision sur 80% des données et affichez-le.

En utilisant la fonction clf.predict, calculez les réponses de votre arbre de décision sur l’ensemble de test. Affichez ces réponses ainsi que la vrai classe, donnée par le jeu d’entraînement. Votre modèle vous semble-t-il bon ?

On peut calculer la précision d’un modèle avec la fonction suivante :

from sklearn import metrics
accuracy = metrics.accuracy_score(classe_test, classe_predites)

Utilisez la fonction précédente pour calculer la précision de votre modèle.

Aller plus loin

On a vu ici, d’un bout à l’autre, comment apprendre un arbre de décision et évaluer sa précision. Une autre approche pour évaluer sa précision serait de faire de l’évaluation croisée, pour comprendre si le modèle qu’on a choisi est un bon modèle : on entraîne le modèle sur toutes les données, mais on le valide comme suit : on partitionne les données S en k ensembles S1, …, Sk de même taille. On entraîne k modèles T1, …, Tk : Ti est entraîné sur les données S \ Si et on calcule sa précision ei en testant sur Si. On retourne iei/k comme estimation de l’erreur du modèle.

Implémentez une validation croisée pour le jeu de données Iris et k = 5.