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 :

:download:`id3.zip <../../squelettes/id3.zip>`

Module ``moteur_id3/noeud_de_decision.py`` :

.. include::  ../../squelettes/id3/moteur_id3/noeud_de_decision.py
    :code:

Module ``moteur_id3/id3.py`` :

.. include::  ../../squelettes/id3/moteur_id3/id3.py
    :code:

Module ``exemple_profits.py`` :

.. include::  ../../squelettes/id3/exemple_profits.py
    :code:

Module ``exemple_maladies.py`` :

.. include::  ../../squelettes/id3/exemple_maladies.py
    :code:

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 vaut ``None`` pour 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 vaut ``None`` pour 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 :math:`H(C|A)` l'entropie de la classification après avoir partitionné les données selon la valeur de l'attribut :math:`A`. Sa valeur est donnée par l'équation :

.. math:: 
    H(C|A) = \sum_{j=1}^{M}p(a_j)H(C|a_j)

où :math:`a_{j}` est une valeur de l'attribut :math:`A`, :math:`M` est le nombre total de valeurs possibles de :math:`A` et :math:`p(a_{j})` est la probabilité que la valeur de l'attribut :math:`A` soit :math:`a_{j}`. 

:math:`H(C|a_{j})` est l'entropie de la classification parmi les données pour lesquels l'attribut :math:`A` prend la valeur :math:`a_{j}`. Elle est définie par l'égalité :

.. math:: 
    H(C|a_j) = -\sum_{i=1}^{N}p(c_i|a_j)\log_2 p(c_i|a_j)

où :math:`N` est le nombre de classes différentes, et :math:`p(c_{i} | a_{j})` la probabilité conditionnelle qu'un objet appartienne à la classe :math:`c_{i}` sachant que son attribut :math:`A` vaut :math:`a_{j}`.

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é :math:`p(a_j)`, 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é :math:`p(c_i | a_j)` 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 :math:`H(C|a_{j})`, où :math:`a_{j}` est la valeur ``valeur`` de l'attribut ``attribut``. Aidez-vous de la deuxième équation ci-dessus. (Lorsque :math:`p = 0`, le résultat de :math:`p\log_2 p` est indéfini. Il faut alors prendre la limite et traiter ce cas comme :math:`p\log_2 p = 0`.)                                             

Finalement, écrivez une méthode ``h_C_A``, qui prenne quatre arguments : ``self``, ``donnees``, ``attribut`` et ``valeurs``, et retourne l'entropie :math:`H(C|A)` 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 :math:`a_{j}` de l'attribut :math:`A`.

La méthode doit retourner un dictionnaire qui associe à chaque valeur :math:`a_{j}` de :math:`A` une liste contenant les données pour lesquelles :math:`A` vaut :math:`a_{j}`. (Si une certaine valeur :math:`a_{j}` 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``.
