Traitement de l’information incertaine

Réseaux Bayésiens

Exercice 1: Raisonnement probabiliste

Informellement, la probabilité que vous soyez malade est toujours proportionnelle à la probabilité de cette maladie. Ainsi, sur 10‘000 personnes, l’une d’entre elle sera effectivement malade et sera diagnostiquée comme telle par le test avec 99% de chance. Cependant, pour les 9‘999 personnes restantes, le test se trompera dans 1% des cas. Environ 100 personnes seront donc positives au test bien que n’étant pas malade. Globalement, vous avez donc seulement 1% de risque d’être effectivement malade si le test est positif.

Plus formellement, soit T la variable représentant le résultat du test, et M la variable représentant la maladie. Par définition des taux de faux négatifs et de faux positifs, on a P(T=1|M=1)= 1-P(T=0|M=1) = 1-0.01=0.99 et P(T=0|M=0)=1-P(T=1|M=0)=1-0.01=0.99. Cependant, P(M=1)=0.0001 puisque la maladie ne frappe qu’une personne sur 10‘000. La probabilité qui nous intéresse est P(M=1|T=1), puisque nous souhaitons déterminer le risque que vous soyez effectivement malade sachant que votre test a été positif. Or,

\begin{split}
P(M=1|T=1) &= \frac{P(T=1|M=1) \cdot P(M=1)} {P(T=1|M=1) \cdot P(M=1)+P(T=1|M=0) \cdot P(M=0)}\\
&= \frac{0.99 \cdot 0.0001} {0.99 \cdot 0.0001+0.01 \cdot 0.9999}\\
&= 0.009804
\end{split}

Comme ce chiffre demeure très faible, vous ne devez pas forcément vous inquiéter. Bien sûr, cette conclusion est due à la faible fréquence de la maladie dans la population.

Exercice 2: Causalité

Modélisation du problème

Question 2.1 :

Le réseau bayésien (figure 1) possède les noeuds :

  • I = Route-Gelée
  • H = Accident-Holmes
  • W = Accident-Watson
  • S = Professeur-Sauvé

et les arcs:

  • I \rightarrow H et I \rightarrow W, car l’état des routes influence les accidents de Holmes et de Watson.
  • W \rightarrow S et H \rightarrow S, car les accidents de Holmes et de Watson influencent la résolution de l’affaire.
../_images/graph-1.png

Figure 1 Le réseau bayésien avec les noeuds: I=Route-Gelée, H=Accident-Holmes, W=Accident-Watson, S=Professeur-Sauvé

Question 2.2 (Inférence déductive) :

  • Pour calculer P(H), il nous faut connaître P(H | I) :

P(H=h) = P(H=h|I=1)P(I=1) + P(H=h|I=0)P(I=0)

Il en va de même de la probabilité P(W), pour laquelle il faut connaître P(W | I) :

P(W=w) = P(W=w|I=1)P(I=1) + P(W=w|I=0)P(I=0)

  • Pour calculer P(S), il nous faut en plus P(S | H, W) :

P(S=s) = \sum_{i,h,w \in {1, 0}}P(S=s|H=h,W=w)P(H=h|I=i)P(W=w|I=i)P(I=i)

Question 2.3 (Inférence abductive) :

La probabilité que nous cherchons est P(I|W). Pour la calculer, il nous faut P(W|I). Par la règle de Bayes, nous avons :

P(I=i|W=w) = \frac{P(W=w|I=i)P(I=i)}{P(W=w)}

Question 2.4 (Déduction et abduction) :

  • On cherche P(H|W). Il nous faut donc P(W|I) et P(H|I):

P(H=h|W=w) = \sum_{i \in {1, 0}} P(H=h|I=i)P(I=i|W=w)

  • On cherche P(H|W,S). Il nous faut donc en plus P(S|H,W):

P(H=h|W=w, S=s) = \alpha P(S=s|H=h,W=w) P(H=h|W=w)

On détermine \alpha = \frac{1}{P(S=s|W =w)} de sorte que:

\sum_{h \in {1, 0}}P(H=h|W=w,S=s) = 1

  • On cherche P(H|I,W,S) :

P(H=h|I=i,W=w,S=s) = \alpha P(S=s|H=h,W=w) P(H=h|I=i)

On détermine \alpha = \frac{1}{P(S=s|I=i,W=w)} de sorte que :

\sum_{h \in {1, 0}}P(H=h|I=i,W=w,S=s) = 1

Calcul probabiliste

Question 2.5 :

P(H|I) I=1 I=0 P(W|I) I=1 I=0
H=1 0.9 0.1 W=1 0.7 0.5
H=0 0.1 0.9 W=0 0.3 0.5

Question 2.6 :

\begin{split}
P(H=1) &= P(H=1|I=1)P(I=1) + P(H=1|I=0)P(I=0) \\
       &= 0.9 \cdot 0.7 + 0.1 \cdot 0.3 = 0.66\\
P(W=1) &= P(W=1|I=1)P(I=1) + P(W=1|I=0)P(I=0) \\
       &= 0.7 \cdot 0.7 + 0.5 \cdot 0.3 = 0.64
\end{split}

Question 2.7 :

\begin{split}
P(I=1|W=1) &= \frac{P(W=1|I=1) \cdot P(I=1)}{P(W=1)} \\
           &= \frac{0.7 \cdot 0.7}{0.64} \cong 0.766
\end{split}

Il en résulte que P(I=0|W=1) \cong 0.234.\

Question 2.8 (Dépendance) :

\begin{split}
P(H=1| W=1) &=P(H=1|I=1) \cdot P(I=1|W=1)\\
            &+ P(H=1|I=0) \cdot P(I=0|W=1)\\
            &= 0.9 \cdot 0.766 + 0.1 \cdot 0.234 \cong 0.713
\end{split}

On peut constater que la probabilité d’accident de Holmes a augmenté suite à la connaissance de l’accident de Watson, car cet accident laisse supposer que la route peut être gelée. H et W sont deux événements dépendants.

Question 2.9 (Indépendance conditionnelle) :

Si l’on sait que la route n’est pas gelée, I=0 et l’accident de Watson n’a plus d’influence sur la probabilité d’accident de Holmes. En effet, l’accident de Watson n’a pas d’effet sur la probabilité que la route soit gelée puisque celle-ci est connue avec certitude. Ainsi, on a: P(H=1|I=0, W=1) = P(H=1|I=0) = 0.1, sachant que I, H et W sont deux événements indépendants.

Question 2.10 :

P(S|W,H) H=1,W=1 H=1,W=0 H=0,W=1 H=0,W=0
S=1 0.1 0.2 0.8 1
S=0 0.9 0.8 0.2 0

Question 2.11 (Causes multiples) :

\begin{split}
P(S=s) &= \sum_{i,h,w \in {1, 0}}P(S=s|H=h,W=w)P(H=h|I=i)P(W=w|I=i)P(I=i)\\
       & = 0.1 \cdot (0.9 \cdot 0.7 \cdot 0.7 + 0.1 \cdot 0.5 \cdot 0.3) \\
       & + 0.2 \cdot (0.9 \cdot 0.3 \cdot 0.7 + 0.1 \cdot 0.5 \cdot 0.3)\\
       & + 0.8 \cdot (0.1 \cdot 0.7 \cdot 0.7 + 0.9 \cdot 0.5 \cdot 0.3)\\
       & + 1 \cdot (0.1 \cdot 0.3 \cdot 0.7 + 0.9 \cdot 0.5 \cdot 0.3)\\
       &= 0.3896
\end{split}

Question 2.12 :

\begin{split}
P(H=1|W=1,S=1) &= \alpha P(S=1|H=1,W=1) P(H=1|W=1)\\
               &= \alpha \cdot 0.1 \cdot 0.713 = \alpha \cdot 0.0713\\
P(H=0|W=1,S=1) &= \alpha P(S=1|H=0,W=1) P(H=0|W=1)\\
               &= \alpha \cdot 0.8 \cdot 0.287 = \alpha \cdot 0.23
\end{split}

Il faut que \alpha \cdot (0.0713 + 0.23) = 1, donc on trouve \alpha \cong 3.319, P(H=1 | W=1, S=1) \cong 0.237 et P(H=0 | W=1, S=1) \cong 0.763.

Question 2.13 (Abduction avec plusieurs conséquences) :

\begin{split}
P(I=1|W=1,S=1) &= \sum_{h \in {1,0}} P(I=1|H=h,W=1) P(H=h|W=1,S=1)\\
               & = \sum_{h \in {1,0}} \frac{P(H=h|I=1)P(W=1|I=1)P(I=1)}{P(H=h|W=1)P(W=1)}P(H=h|W=1,S=1)\\
               & = \frac{0.9 \cdot 0.7 \cdot 0.7}{0.713 \cdot 0.64}\cdot0.237 + \frac{0.1 \cdot 0.7 \cdot 0.7}{0.287 \cdot 0.64} \cdot 0.763\\
               & = 0.229 + 0.204 = 0.433
\end{split}

Question 2.14 :

\begin{split}
P(H=1|I=0,W=1,S=1) &= \alpha P(S=1|H=1,W=1) P(H=1|I=0)\\
                   & = \alpha \cdot 0.1 \cdot 0.1 =  \alpha \cdot 0.01\\
P(H=0|I=0,W=1,S=1) &= \alpha P(S=1|H=0,W=1) P(H=0|I=0)\\
                   & = \alpha \cdot 0.8 \cdot 0.9 =  \alpha \cdot 0.72\\
\end{split}

Il faut que \alpha \cdot (0.01 + 0.72) = 1, donc on trouve \alpha = 1.370 et P(H=1 | I=0, W=1, S=1) = 0.014 et P(H=0 |I=0, W=1, S=1) = 0.986.

Question 2.15 (Dépendance conditionnelle) :

\begin{split}
P(H=1|I=0, S=1) &= \alpha P(S=1|H=1,I=0) P(H=1|I=0)\\
                & = \alpha \cdot \sum_{w \in {1,0}}P(S=1|H=1,W=w) \cdot P(W=w|I=0) \cdot P(H=1|I=0)\\
                & = \alpha \cdot (0.1 \cdot 0.5 + 0.2 \cdot 0.5) \cdot 0.1 = \alpha \cdot 0.015\\
P(H=0|I=0,S=1) &= \alpha P(S=1|H=0,I=0) P(H=0|I=0)\\
               & = \alpha \cdot \sum_{w \in {1,0}}P(S=1|H=0,W=w) \cdot P(W=w|I=0) \cdot P(H=0|I=0)\\
               & = \alpha \cdot (0.8 \cdot 0.5 + 1 \cdot 0.5) \cdot 0.9 = \alpha \cdot 0.81
\end{split}

Il faut que \alpha \cdot (0.015 + 0.81) = 1, donc on trouve \alpha = 1.21 et P(H=1|I=0,S=1) = 0.02 et P(H=0|I=0,S=1) = 0.98. Donc, si en plus on sait que Watson a eu un accident, la probabilité que Holmes en ait eu un aussi redescend un peu. Ainsi, H et W sont des événements dépendants sachant que S et I.

../_images/graph-2.png

Figure 2 Le nouveau réseau bayésien avec les noeuds: I=Route-Gelée, H=Accident-Holmes, W=Accident-Watson, S=Professeur-Sauvé, V=Vieux-Pneus-Watson

Question 2.16 :

On ajoute le noeud V = Vieux-Pneus-Watson et l’arc V \rightarrow W (figure 2).

Question 2.17 :

On cherche P(V|I,W). Il nous faut donc P(W|I,V) :

\begin{split}
P(V=v|I=i,W=w) = \alpha P(W=w|I=i,V=v) P(V=v)
\end{split}

On détermine \alpha = \frac{1}{P(W=w|I=i)} de sorte que :

\sum_{v \in {1, 0}}P(V=v|I=i,W=w) = 1

Inférence à chaînage avant avec facteurs de certitude

Module .../moteur_avec_variables_fc/facteurs_certitude.py :

""" Fonctions utilitaires pour gérer des facteurs de certitude. """

def fc_ou(fc1, fc2):
    """ Calcule le facteur de certitude d'une disjonction de faits. """
    if fc1 > 0.0 and fc2 > 0.0:
        return fc1 + fc2 - (fc1 * fc2)
    elif fc1 < 0.0 and fc2 < 0.0:
        return fc1 + fc2 + (fc1 * fc2)
    else:
        return (fc1 + fc2) / (1.0 - min(abs(fc1), abs(fc2)))

def fc_et(fc1, fc2):
    """ Calcule le facteur de certitude d'une conjonction de faits. """
    return min(fc1, fc2)

Module .../moteur_avec_variables_fc/regle_avec_variables_fc.py :

from .facteurs_certitude import fc_et

class RegleAvecVariables_FC:
    """ Représentation d'une règle d'inférence pour le chaînage avec variables. """

    def __init__(self, conditions, conclusion, fc=1.0):
        """ Construit une règle étant donné une liste de conditions,\
            une conclusion et un facteur de certitude associé.

            :param list conditions: une collection de propositions (pouvant\
            contenir des variables) nécessaires à déclencher la règle.
            :param conclusion: la proposition (pouvant contenir des variables)\
            résultant du déclenchement de la règle.
            :param fc: le facteur de certitude associé.
        """
        self.conditions = conditions
        self.conclusion = conclusion
        self.fc = fc

    def depend_de(self, fait, methode):
        """ Vérifie qu'un fait fait partie, sous réserve de substitution,\
            des conditions de la règle.

            :param fait: un fait qui doit faire partie des conditions de\
            déclenchement.
            :param methode: ``Filtre`` ou ``Unificateur``, détermine le type\
             de pattern match à appliquer.
            :return: un dictionnaire qui attribue un environnement à chaque\
            condition qui peut être satisfaite par le fait pasée en paramètre.\
            ``False`` si aucune condition n'est satisfaite par le fait.
        """

        envs = {}

        for condition in self.conditions:
            # Si au moins une des conditions retourne un environnement,
            # nous savons que la proposition satisfait une des conditions.
            env = methode.pattern_match(fait, condition, {})
            if env != methode.echec:
                envs[condition] = env

        return envs

    def satisfaite_par(self, faits, cond, env, env_fc, methode):
        """ Vérifie que des faits suffisent, sous réserve de substitution,\
            à déclencher la règle.

            :param list faits: une liste de faits.
            :param cond: la condition qui a donné lieu à ``env`` par le\
            pattern match.
            :param dict env: un environnement de départ déjà établi par\
            ``depend_de``.
            :param env_fc: le facteur de certitude associé à ``env``.
            :param methode: ``Filtre`` ou ``Unificateur``, détermine le type\
             de pattern match à appliquer.
            :return: une liste d'environnements qui correspondent à toutes les\
            substitutions possibles entre les conditions de la règle et les\
            propositions. On retourne une liste vide si au moins une condition\
            ne peut être satisfaite.
        """
        envs_et_fcs = [(env, env_fc)]
        conditions_a_tester = [cond1 for cond1 in self.conditions if cond1 != cond]

        for cond1 in conditions_a_tester:
            nouveaux_envs_et_fcs = []

            for fait, fait_fc in faits:
                for env1, env_fc1 in envs_et_fcs:
                    env1 = methode.pattern_match(fait, cond1, env1)
                    if env1 != methode.echec:
                        fc_mix = fc_et(env_fc1, fait_fc)
                        nouveaux_envs_et_fcs.append((env1, fc_mix))

            if len(nouveaux_envs_et_fcs) == 0:
                return []

            envs_et_fcs = nouveaux_envs_et_fcs

        return envs_et_fcs

    def __repr__(self):
        """ Représentation d'une règle sous forme de string. """

        return '{} => {}, {}'.format(str(self.conditions),
                                     str(self.conclusion),
                                     str(self.fc))

Module .../moteur_avec_variables_fc/connaissance_fc.py :

from .facteurs_certitude import fc_ou
from moteur_avec_variables_fc.regle_avec_variables_fc import RegleAvecVariables_FC

class BaseConnaissances_FC:
    """ Une base de connaissances destinée à contenir les faits et les règles\
        d'un système de chaînage avec facteurs de certitude.
    """

    def __init__(self):
        """ Construit une base de connaissances. """

        self.faits = {}
        self.regles = []

    def ajoute_un_fait(self, fait):
        """ Ajoute un fait dans la base de connaissances.

            Un fait est un tuple composé du fait proprement dit, c'est-à-dire\
            une proposition sans variable, et optionnellement d'un scalaire\
            représentant son facteur de certitude.

            :param fait: un fait.
        """
        if len(fait) == 2:
            prop, fc = fait
        elif len(fait) == 1:
            prop, fc = fait[0], 1.0
        else:
            raise ValueError("Fait mal formé: " + str(fait))

        fc_deja_present = self.faits.get(prop)
        if fc_deja_present is not None:
            nouveau_fc = fc_ou(fc, fc_deja_present)
            self.faits[prop] = nouveau_fc
        else:
            self.faits[prop] = fc

    def ajoute_faits(self, faits):
        """ Ajoute une liste de faits dans la base de connaissances.

            :param list faits: une liste de faits.
        """

        for fait in faits:
            self.ajoute_un_fait(fait)

    def ajoute_une_regle(self, description):
        """ Ajoute une règle dans la base de connaissances étant donné sa\
            description.

            Une règle est décrite par une liste (ou un tuple) de deux ou trois\
            éléments: une liste de conditions, une conclusion et un facteur de\
            certitude optionnel.

            Les conditions et la conclusion doivent être des propositions.

            :param description: une description de règle.
        """
        if len(description) == 2:
            regle = RegleAvecVariables_FC(description[0], description[1])
        elif len(description) == 3:
            regle = RegleAvecVariables_FC(description[0], description[1], description[2])
        else:
            raise ValueError("Une règle doit avoir deux ou trois éléments. Reçu " +
                             str(description))

        self.regles.append(regle)

    def ajoute_regles(self, descriptions):
        """ Ajoute des règles dans la base de connaissance.

            L'argument est une liste de descriptions, chacune composée d'une\
            liste de conditions, d'une conséquence et optionnellement d'un\
            facteur de certitude.

            :param list descriptions: une liste de descriptions de règles.
        """

        for description in descriptions:
            self.ajoute_une_regle(description)

Module .../moteur_avec_variables_fc/chainage_avant_avec_variables_fc.py :

from moteur_sans_variables.chainage import Chainage
from moteur_avec_variables.filtre import Filtre

class ChainageAvantAvecVariables_FC(Chainage):
    """ Un moteur d'inférence à chaînage avant avec variables. """

    def __init__(self, connaissances, methode=None):
        """
            :param methode: ``Filtre`` ou ``Unificateur``, détermine le type de\
            pattern match à appliquer. ``Filtre`` par défaut.
        """

        Chainage.__init__(self, connaissances)

        if methode is None:
            self.methode = Filtre()
        else:
            self.methode = methode

    def instancie_conclusion(self, regle, envs_et_fcs):
        """ Instancie la conclusion d'une règle pour tous les environnements.

            :param regle: la règle dont la conclusion doit être instanciée.
            :param list envs_et_fcs: une liste de paires d'environnements et de\
            facteurs de certitude servant à instancier la conclusion de la règle.
            :return: une liste de propositions correspondant aux différentes\
            instanciations de la conclusion.
        """
        nouveaux_faits = []

        for env, env_fc in envs_et_fcs:
            prop = self.methode.substitue(regle.conclusion, env)
            nouveau_fait = (prop, max(0.0, env_fc) * regle.fc)
            nouveaux_faits.append(nouveau_fait)

        return nouveaux_faits

    def chaine(self):
        """ Effectue le chaînage avant sur les faits et les règles contenus\
            dans la base de connaissances.
        """
        queue = [p for p in self.connaissances.faits.items()]
        self.reinitialise()

        while len(queue) > 0:
            fait, fait_fc = queue.pop(0)

            if (fait, fait_fc) not in self.solutions:
                self.trace.append((fait, fait_fc))
                self.solutions.append((fait, fait_fc))

                # Si le facteur de certitude du fait est supérieur à 0.0
                if fait_fc > 0.0:

                    # on vérifie si des règles sont déclenchées par le nouveau fait.
                    for regle in self.connaissances.regles:
                        envs = regle.depend_de(fait, self.methode)

                        for cond, env in envs.items():
                            # On remplace l'environnement par ceux qui satisfont toutes les conditions de la règle
                            # et pas seulement la première condition.
                            envs1 = regle.satisfaite_par(self.solutions, cond, env, fait_fc, self.methode)

                            # On ajoute à la queue la conclusion de la règle instanciée selon chaque environnement possible.
                            if len(envs1) > 0:
                                queue.extend(self.instancie_conclusion(regle, envs1))
                                self.trace.append(regle)

        return self.solutions