TP 3 : Arbre de décisions

Programmation pour l’IA

Pour tester nos algorithmes, 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.

Assurez-vous d’avoir installé la librairie scikit-learn.

  1. Familiarisez-vous avec le contenu du fichier iris.csv.
  2. Utilisez numpy.loadtxt ou pandas.read_csv et pandas.DataFrame.to_numpy pour charger le fichier. On créera les tableaux numpy suivants :
    • feature_names de shape (4,) qui contiendra le nom de chaque feature, data de shape (N,4) oĂą N est le nombre d’exemple dans le fichier et qui contiendra les valeurs.
    • classes de shape (N,)qui contiendra les classes (dans le sens oĂą classes[i]est la classe du point data[i]). Pour faciliter l’utilisation avec scikit-learn, on renommera les classes avec des entiers : {"Virginica" : 0, "Versicolor": 1, "Setosa": 2}.
    • cname = ["Virginica", "Versicolor", "Setosa"].

On pourra utiliser les paramètres skiprows, usecols, max_rows de numpy.loadtxt ou des manipulations classiques des dataframes.

  1. 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 ?
  2. En utilisant matplotlib, visualisez le jeu de données. On pourra par exemple afficher, en nuage de points comme ci-dessous (on pourra utiliser c=classes pour avoir des couleurs qui dépendent des classes) :

Nous allons désormais trouver un arbre de décision pour expliquer les données. 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()
  1. Utilisez les instructions ci-dessus pour entraîner et afficher l’arbre de décision.
  2. 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 ?7. Même question en utilisant 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]]).

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=52)

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. random_state fixe la graine, ce qui permet de reproduire les résultats.

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

  2. Calculez la précision de votre modèle, c’est-à-dire le nombre d’exemples tests bien classés divisé par le nombre total d’exemples tests. On remarquera que clf.predict peut prendre un tableau de données et renvoyer un tableau de décisions.

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)
  1. Utilisez la fonction précédente pour calculer la précision de votre modèle et vérifiez votre résultat.
  2. Refaire l’exercice en prenant seulement 50% du jeux de données. Qu’observez-vous ?
  3. En allant lire la documentation, entraînez un classifier en utilisant l’entropie comme fonction de mélange au lieu de la fonction Gini.

Dans cet exercice, on va implémenter nous-mêmes un algorithme pour trouver un bon arbre de décision. On va implémenter une version simple de l’algorithme CART. On va se limiter au cas où les données sont de la forme \((x_0,\dots,x_k)\) avec \(x_i\) un float et les decisions prises dans chaque nœud sont de la forme \(x_i < v\) pour une certaine valeur \(v\).

  1. Écrire une fonction decide(data: np.array, classes: np.array, i : int, v : float) qui prend un ensemble de données data avec leur classe classes, et renvoie deux tableaux numpy (qdata, qclasses) tels que qdata[0] est le sous-ensemble de data où \(x_i \geq v\) et qdata[1] est le sous-ensemble de data où \(x_i < v\). Les classes de data[0] seront qclasses[0] et celles de data[1] seront qclasses[1]. On rappelle qu’on peut écrire data[:,i] qui est un tableau tel que data[:,i][j] = data[j][i].

On veut trouver la décision de la forme \(x_i < v\) la plus “clivante”, c’est-à-dire, celle qui sépare le mieux les classes. Pour cela, on utilise une fonction \(F\) qui estime le degré de mélange, comme l’entropie ou la fonction de Gini vu en cours. On estime le gain d’un test \(x_i < v\) comme

\[F(D) - |D_0| \cdot F(D_0) - |D_1| \cdot F(D_1)\]

où \(D_0\) est l’ensemble des éléments de \(D\) qui ne satisfont pas le test et \(D_1\) l’ensemble des éléments de \(D\) qui satisfont le test.

    1. Écrire une fonction gini(data: np.array, classes: np.array) qui estime le degré de mélange du jeu de données
    2. Écrire une fonction gain(data: np.array, classes: np.array, i: int, v : float) qui estime le gain du test \(x_i < v\) pour la fonction de mélange gini.
  1. Écrire une fonction maxgain(data: np.array, classes: np.array) qui renvoie un couple (i,v) maximisant le gain possible parmis tous les tests \(x_j < w\) possible. Pour cela, on remarquera que pour chaque i, on a un nombre restreint de valeurs à tester (bonus : réfléchir à différentes façons de tester cela).

  2. On va représenter un arbre de décision comme suit:

class LeafNode(DecisionNode):
    def __init__(self, value):
        self.value = value

class InternalNode(DecisionNode):
    def __init__(self, i, v, left, right):
        self.i = feature_index
        self.v = threshold
        self.left = left
        self.right = right

Créer une récursive fonction fit(data,classes) qui crée un arbre de décision pour les données passées en paramètre et renvoie la racine de cet arbre. Votre fonction devra créer l’arbre ainsi :

  1. Ajouter des méthodes pour afficher les arbres de décision et prédire une valeur.
  2. Essayez d’évaluer votre arbre sur le jeu de données iris.csv. Qu’observez-vous ? Comment pourriez-vous améliorer la précision de votre modèle ?