Les arbres de décision (ID3)¶
Si vous voulez investir dans une compagnie informatique et que vous demandez conseil à un expert financier, avant de vous répondre, celui-ci vous posera toute une série de questions concernant l’entreprise. Il voudra connaître le type de concurrence à laquelle elle est confrontée, son âge, son secteur d’activité, etc. En admettant que vous possédiez de nombreux exemples de profils d’entreprise accompagnés des conclusions de l’expert, vous auriez en quelque sorte à votre disposition une partie de son expertise. Il serait intéressant de pouvoir la réutiliser sans avoir toujours recours à lui lorsque vous souhaitez analyser de nouvelles entreprises.
Un arbre de décision est une structure qui est souvent utilisée pour représenter des connaissances. Il permet justement de remplacer un expert humain lorsque l’on désire connaître la nature d’une certaine caractéristique d’un objet, caractéristique que nous appellerons la `classe’ de cet objet. Il s’agit d’une structure en arbre qui modélise le cheminement intellectuel de l’expert et dans laquelle :
- Chaque noeud intermédiaire correspond à une question portant sur une propriété de l’objet. Nous appelons une telle propriété un attribut.
- Chaque arête correspond à une valeur de cet attribut.
- Chaque noeud terminal correspond à une collection d’objets appartenant à la même classe. Cette classe est donc associée au noeud. La même classe peut se manifester dans plusieurs noeuds terminaux.
En parcourant cet arbre, c’est-à-dire en répondant aux questions des noeuds intermédiaires et en suivant les arêtes correspondantes, on parvient à un noeud terminal qui nous renseigne sur la classe de l’objet.
Modules squelettes¶
Voici tout d’abord les squelettes de fichiers Python qui vous permettront de réaliser l’exercice. Les deux modules exemple_profits.py et exemple_maladies.py vous permettront de tester votre programme :
Module moteur_id3/noeud_de_decision.py :
class NoeudDeDecision:
"""Un noeud dans un arbre de décision. """
def __init__(self, attribut, donnees, enfants=None):
"""
:param attribut: l'attribut de partitionnement du noeud (``None`` si\
le noeud est un noeud terminal).
:param list donnees: la liste des données qui tombent dans la\
sous-classification du noeud.
:param enfants: un dictionnaire associant un fils (sous-noeud) à\
chaque valeur de l'attribut du noeud (``None`` si le\
noeud est terminal).
"""
self.attribut = attribut
self.donnees = donnees
self.enfants = enfants
def terminal(self):
""" Vérifie si le noeud courant est terminal. """
return self.enfants is None
def classe(self):
""" Si le noeud est terminal, retourne la classe des données qui\
tombent dans la sous-classification (dans ce cas, toutes les\
données font partie de la même classe.
"""
if self.terminal():
return self.donnees[0][0]
def classifie(self, donnee):
""" Classifie une donnée à l'aide de l'arbre de décision duquel le noeud\
courant est la racine.
:param donnee: la donnée à classifier.
:return: la classe de la donnée selon le noeud de décision courant.
"""
rep = ''
if self.terminal():
rep += 'Alors {}'.format(self.classe().upper())
else:
valeur = donnee[self.attribut]
enfant = self.enfants[valeur]
rep += 'Si {} = {}, '.format(self.attribut, valeur.upper())
rep += enfant.classifie(donnee)
return rep
def repr_arbre(self, level=0):
""" Représentation sous forme de string de l'arbre de décision duquel\
le noeud courant est la racine.
"""
rep = ''
if self.terminal():
rep += '---'*level
rep += 'Alors {}\n'.format(self.classe().upper())
rep += '---'*level
rep += 'Décision basée sur les données:\n'
for donnee in self.donnees:
rep += '---'*level
rep += str(donnee) + '\n'
else:
for valeur, enfant in self.enfants.items():
rep += '---'*level
rep += 'Si {} = {}: \n'.format(self.attribut, valeur.upper())
rep += enfant.repr_arbre(level+1)
return rep
def __repr__(self):
""" Représentation sous forme de string de l'arbre de décision duquel\
le noeud courant est la racine.
"""
return str(self.repr_arbre(level=0))
Module moteur_id3/id3.py :
from math import log
from .noeud_de_decision import NoeudDeDecision
class ID3:
""" Algorithme ID3. """
def construit_arbre(self, donnees):
""" Construit un arbre de décision à partir des données d'apprentissage.
:param list donnees: les données d'apprentissage\
``[classe, {attribut -> valeur}, ...]``.
:return: une instance de NoeudDeDecision correspondant à la racine de\
l'arbre de décision.
"""
# Nous devons extraire les domaines de valeur des
# attributs, puisqu'ils sont nécessaires pour
# construire l'arbre.
attributs = {}
for donnee in donnees:
for attribut, valeur in donnee[1].items():
valeurs = attributs.get(attribut)
if valeurs is None:
valeurs = set()
attributs[attribut] = valeurs
valeurs.add(valeur)
arbre = self.construit_arbre_recur(donnees, attributs)
return arbre
def construit_arbre_recur(self, donnees, attributs):
""" Construit rédurcivement un arbre de décision à partir
des données d'apprentissage et d'un dictionnaire liant
les attributs à la liste de leurs valeurs possibles.
:param list donnees: les données d'apprentissage\
``[classe, {attribut -> valeur}, ...]``.
:param attributs: un dictionnaire qui associe chaque\
attribut A à son domaine de valeurs a_j.
:return: une instance de NoeudDeDecision correspondant à la racine de\
l'arbre de décision.
"""
print('à compléter')
def partitionne(self, donnees, attribut, valeurs):
""" Partitionne les données sur les valeurs a_j de l'attribut A.
:param list donnees: les données à partitioner.
:param attribut: l'attribut A de partitionnement.
:param list valeurs: les valeurs a_j de l'attribut A.
:return: un dictionnaire qui associe à chaque valeur a_j de\
l'attribut A une liste l_j contenant les données pour lesquelles A\
vaut a_j.
"""
print('à compléter')
def p_aj(self, donnees, attribut, valeur):
""" p(a_j) - la probabilité que la valeur de l'attribut A soit a_j.
:param list donnees: les données d'apprentissage.
:param attribut: l'attribut A.
:param valeur: la valeur a_j de l'attribut A.
:return: p(a_j)
"""
print('à compléter')
def p_ci_aj(self, donnees, attribut, valeur, classe):
""" p(c_i|a_j) - la probabilité conditionnelle que la classe C soit c_i\
étant donné que l'attribut A vaut a_j.
:param list donnees: les données d'apprentissage.
:param attribut: l'attribut A.
:param valeur: la valeur a_j de l'attribut A.
:param classe: la valeur c_i de la classe C.
:return: p(c_i | a_j)
"""
print('à compléter')
def h_C_aj(self, donnees, attribut, valeur):
""" H(C|a_j) - l'entropie de la classe parmi les données pour lesquelles\
l'attribut A vaut a_j.
:param list donnees: les données d'apprentissage.
:param attribut: l'attribut A.
:param valeur: la valeur a_j de l'attribut A.
:return: H(C|a_j)
"""
print('à compléter')
def h_C_A(self, donnees, attribut, valeurs):
""" H(C|A) - l'entropie de la classe après avoir choisi de partitionner\
les données suivant les valeurs de l'attribut A.
:param list donnees: les données d'apprentissage.
:param attribut: l'attribut A.
:param list valeurs: les valeurs a_j de l'attribut A.
:return: H(C|A)
"""
print('à compléter')
Module exemple_profits.py :
from moteur_id3.noeud_de_decision import NoeudDeDecision
from moteur_id3.id3 import ID3
# Les données d'apprentissage.
donnees = [
['down', {
'age': 'old',
'competition': 'no',
'type': 'software'
}],
['down', {
'age': 'midlife',
'competition': 'yes',
'type': 'software'
}],
['up', {
'age': 'midlife',
'competition': 'no',
'type': 'hardware'
}],
['down', {
'age': 'old',
'competition': 'no',
'type': 'hardware'
}],
['up', {
'age': 'new',
'competition': 'no',
'type': 'hardware'
}],
['up', {
'age': 'new',
'competition': 'no',
'type': 'software'
}],
['up', {
'age': 'midlife',
'competition': 'no',
'type': 'software'
}],
['up', {
'age': 'new',
'competition': 'yes',
'type': 'software'
}],
['down', {
'age': 'midlife',
'competition': 'yes',
'type': 'hardware'
}],
['down', {
'age': 'old',
'competition': 'yes',
'type': 'hardware'
}],
]
id3 = ID3()
arbre = id3.construit_arbre(donnees)
print('Arbre de décision :')
print(arbre)
print()
print('Exemplification :')
print(arbre.classifie({
'age': 'midlife',
'competition': 'no',
'type': 'hardware'}))
Module exemple_maladies.py :
from moteur_id3.noeud_de_decision import NoeudDeDecision
from moteur_id3.id3 import ID3
# Les données d'apprentissage.
donnees = [
['angine-érythémateuse', {
'fièvre': 'élevée',
'amygdales': 'gonflées',
'ganglions': 'oui',
'gêne-à-avaler': 'oui',
'mal-au-ventre': 'non',
'toux': 'non',
'rhume': 'non',
'respiration': 'normale',
'joues': 'normales',
'yeux': 'normaux'}
],
['angine-pultacée', {
'fièvre': 'élevée',
'amygdales': 'points-blancs',
'ganglions': 'oui',
'gêne-à-avaler': 'oui',
'mal-au-ventre': 'non',
'toux': 'non',
'rhume': 'non',
'respiration': 'normale',
'joues': 'normales',
'yeux': 'normaux'}
],
['angine-diphtérique', {
'fièvre': 'légère',
'amygdales': 'enduit-blanc',
'ganglions': 'oui',
'gêne-à-avaler': 'oui',
'mal-au-ventre': 'non',
'toux': 'non',
'rhume': 'non',
'respiration': 'normale',
'joues': 'normales',
'yeux': 'normaux'}
],
['appendicite', {
'fièvre': 'légère',
'amygdales': 'normales',
'ganglions': 'non',
'gêne-à-avaler': 'non',
'mal-au-ventre': 'oui',
'toux': 'non',
'rhume': 'non',
'respiration': 'normale',
'joues': 'normales',
'yeux': 'normaux'}
],
['bronchite', {
'fièvre': 'légère',
'amygdales': 'normales',
'ganglions': 'oui',
'gêne-à-avaler': 'non',
'mal-au-ventre': 'non',
'toux': 'oui',
'rhume': 'oui',
'respiration': 'gênée',
'joues': 'normales',
'yeux': 'normaux'}
],
['coqueluche', {
'fièvre': 'légère',
'amygdales': 'normales',
'ganglions': 'non',
'gêne-à-avaler': 'oui',
'mal-au-ventre': 'non',
'toux': 'oui',
'rhume': 'oui',
'respiration': 'gênée',
'joues': 'normales',
'yeux': 'normaux'}
],
['pneumonie', {
'fièvre': 'élevée',
'amygdales': 'normales',
'ganglions': 'non',
'gêne-à-avaler': 'non',
'mal-au-ventre': 'non',
'toux': 'oui',
'rhume': 'non',
'respiration': 'rapide',
'joues': 'rouges',
'yeux': 'normaux'}
],
['rougeole', {
'fièvre': 'légère',
'amygdales': 'normales',
'ganglions': 'non',
'gêne-à-avaler': 'oui',
'mal-au-ventre': 'non',
'toux': 'oui',
'rhume': 'oui',
'respiration': 'normale',
'joues': 'normales',
'yeux': 'larmoyants'}
],
['rougeole', {
'fièvre': 'légère',
'amygdales': 'normales',
'ganglions': 'non',
'gêne-à-avaler': 'oui',
'mal-au-ventre': 'non',
'toux': 'oui',
'rhume': 'oui',
'respiration': 'normale',
'joues': 'taches-rouges',
'yeux': 'larmoyants'}
],
['rubéole', {
'fièvre': 'légère',
'amygdales': 'normales',
'ganglions': 'oui',
'gêne-à-avaler': 'non',
'mal-au-ventre': 'non',
'toux': 'non',
'rhume': 'non',
'respiration': 'normale',
'joues': 'taches-rouges',
'yeux': 'normaux'}
],
['rubéole', {
'fièvre': 'non',
'amygdales': 'normales',
'ganglions': 'oui',
'gêne-à-avaler': 'non',
'mal-au-ventre': 'non',
'toux': 'non',
'rhume': 'non',
'respiration': 'normale',
'joues': 'taches-rouges',
'yeux': 'normaux'}
],
['rubéole', {
'fièvre': 'non',
'amygdales': 'normales',
'ganglions': 'oui',
'gêne-à-avaler': 'non',
'mal-au-ventre': 'non',
'toux': 'non',
'rhume': 'non',
'respiration': 'normale',
'joues': 'normales',
'yeux': 'normaux'}
],
]
id3 = ID3()
arbre = id3.construit_arbre(donnees)
print('Arbre de décision :')
print(arbre)
print()
print('Exemplification :')
print(arbre.classifie({
'fièvre': 'non',
'amygdales': 'normales',
'ganglions': 'oui',
'gêne-à-avaler': 'non',
'mal-au-ventre': 'non',
'toux': 'non',
'rhume': 'non',
'respiration': 'normale',
'joues': 'normales',
'yeux': 'normaux'}))
L’algorithme ID3¶
ID3 est un algorithme de construction d’arbres de décision qui vise à minimiser le nombre de questions à poser. Il construit un arbre de décision à partir d’un ensemble de données constituées d’objets décrits par leurs attributs et leur classe. L’algorithme est le suivant :
ID3(données, attributs)
1. IF données est vide THEN
2. RETURN NULL
3. ELSE IF toutes les données font partie de la même classe THEN
4. # Nœud terminal
5. RETURN un nœud terminal contenant tous les données
6. ELSE
7. # Nœud intermédiaire
8. A <- l'attribut minimisant l'entropie de la classification
9. valeurs <- liste des valeurs possibles pour A
10. FOR v IN valeurs DO
11. # Partitionnement:
12. partitions[v] <- les données qui ont v comme valeur pour A
14. # Calcul des sous-nœuds
15. enfants[v] <- ID3(partitions[v], attributs - A)
16. END FOR
17. RETURN un nœud avec enfants comme successeurs
18. END IF
END ID3
Bien évidemment, la qualité de l’arbre de décision construit par ID3 dépend des données ; plus elles sont variées et nombreuses, plus la classification de nouveaux objets sera fiable.
Structures de données¶
Voyons les structures de données dont nous aurons besoin. Nous représenterons les donnée d’apprentissage (un objet avec sa classe) sous forme de listes composées du nom de la classe et d’un dictionnaire {attribut: valeur} :
donnée ::= [val-classe,
{attribut-1: val-attribut-1,
... ,
attribut-k: val-attribut-k}]
où k est le nombre d’attributs. Chaque donnée doit spécifier une valeur pour chaque attribut. Vous pouvez trouver des exemples de telles données d’apprentissage dans les modules de test exemples_maladies.py et exemples_profits.py.
Nous utiliserons aussi :
- Un dictionnaire qui fait correspondre chaque attribut à son domaine de valeurs :
attributs ::= {attribut-1: [val-attribut-1-1, ...],
...,
attribut-k: [val-attribut-1-k, ...]}
- Une liste contenant toutes les classes des données d’apprentissage :
classes ::= [val-classe-1, ...]
La classe NoeudDeDecision¶
Les noeuds de l’arbre de décision seront modélisés par la classe NoeudDeDecision. La classe contient trois champs :
attribut: l’attribut de partitionnement d’un noeud. Ce champ vautNonepour un noeud terminal.donnees: la liste des données qui tombent dans la sous-classification du noeud.enfants: un dictionnaire associant un fils (sous-noeud) à chaque valeur de l’attribut du noeud. Ce champ vautNonepour un noeud terminal.
Exercice 1 : L’entropie¶
La classe ID3 du module id3.py implémente l’algorithme ci-dessus. Elle contient une méthode qui construit un arbre de décision à partir des données d’apprentissage. Cette méthode s’appuie à son tour sur une méthode utilitaire qui calcule l’entropie conditionnelle de la classe étant donné un attribut qui partitionne les données.
L’entropie est une mesure de l’information, ou plutôt de l’incertitude, à l’égard de la classification d’un objet. ID3 utilise cette mesure comme une heuristique visant à minimiser la taille de l’arbre de décision, ne conservant à chaque étape que l’information absolument nécessaire pour classer un objet. Chaque fois que l’on doit choisir un attribut pour partitionner les données, on privilégie ainsi celui qui génère une classification dont l’entropie est minimale.
Nous notons
l’entropie de la classification après avoir partitionné les données selon la valeur de l’attribut
. Sa valeur est donnée par l’équation :

où
est une valeur de l’attribut
,
est le nombre total de valeurs possibles de
et
est la probabilité que la valeur de l’attribut
soit
.
est l’entropie de la classification parmi les données pour lesquels l’attribut
prend la valeur
. Elle est définie par l’égalité :

où
est le nombre de classes différentes, et
la probabilité conditionnelle qu’un objet appartienne à la classe
sachant que son attribut
vaut
.
Dans la classe ID3, écrivez donc une méthode p_aj, avec quatre arguments : self, donnees, attribut et valeur. Cette méthode doit retourner la probabilité p(attribut=valeur), c’est-à-dire la probabilité
, sur la base de donnees, que l’attribut attribut vaille valeur.
De façon similaire, écrivez une deuxième méthode p_ci_aj qui prenne un argument de plus : classe. Cette méthode doit retourner la probabilité conditionnelle p(classe=classe|attribut=valeur), c’est-à-dire la probabilité
qu’une donnée appartienne à la classe classe lorsque son attribut attribut vaut valeur. Cette probabilité devra être calculée par rapport aux objets de donnees.
Ensuite, écrivez une méthode h_C_aj, avec quatre arguments : self, donnees, attribut et valeur, qui retourne l’entropie de la sous-classification
, où
est la valeur valeur de l’attribut attribut. Aidez-vous de la deuxième équation ci-dessus. (Lorsque
, le résultat de
est indéfini. Il faut alors prendre la limite et traiter ce cas comme
.)
Finalement, écrivez une méthode h_C_A, qui prenne quatre arguments : self, donnees, attribut et valeurs, et retourne l’entropie
de la classification des objets de donnees après avoir choisi l’attribut attribut. Aidez-vous de la première équation ci-dessus.
Exercice 2 : La méthode partitionne¶
Dans la même classe, écrivez une méthode partitionne qui prendra trois paramètres (outre self) :
donnees: les données d’apprentissage à partitionner ;attribut: l’attribut A de partitionnement ;valeurs: une liste contenant les valeurs
de l’attribut
.
La méthode doit retourner un dictionnaire qui associe à chaque valeur
de
une liste contenant les données pour lesquelles
vaut
. (Si une certaine valeur
n’apparaît pas dans donnees, la partition correspondante vaudra []).
Exercice 3 : La méthode construit_arbre_recur¶
Nous pouvons maintenant passer à la construction de l’arbre proprement dite. Pour cela, écrivez une méthode construit_arbre_recur, qui doit accepter trois paramètres :
self: la classe ID3 ;donnees: les données de la sous-classification courante ;attributs: les attributs encore disponibles pour partitionner les exemples.
Cette méthode doit construire un arbre de décision en suivant l’algorithme ID3, dont le pseudo-code vous a été donné ci-dessus. Nous vous suggérons d’utilisez la méthode h_C_A pour construire une liste qui associe un attribut à son entropie, puis de choisir l’attribut dont l’entropie est la plus petite. Utilisez la méthode partitionne pour partitionner les exemples selon les valeurs de cet attribut.
Notez que construit_arbre_recur est appelée par construit_arbre, qui sert d’interface. Le code de cette dernière vous est donné et consiste en une routine qui extrait les domaines des attributs, avant de les passer à construit_arbre_recur.
Test du programme¶
Vous pouvez maintenant tester votre module avec les modules d’exemple :
exemple_profits.py: présente des profils d’entreprises informatiques avec leurs espérances de profit ;exemple_maladies.py: essaie de trouver de quelle maladie souffre un enfant.
Utilisez les méthodes repr_arbre et __repr__ de la classe NoeudDeDecision afin d’afficher l’arbre de décision résultant de construit_arbre. Essayez d’utiliser l’arbre de décision comme un expert humain de manière interactive à l’aide de la méthode classifie de la classe NoeudDeDecision.