Infer-1b : Introduction a Infer.NET

Serie : Programmation Probabiliste avec Infer.NET
Duree estimee : 2h30
Prerequis : C# et .NET Interactive, notions de probabilités, loi normale, inférence bayésienne


Objectifs d’apprentissage

  • Comprendre le concept de variable aléatoire en programmation probabiliste
  • Modeliser un problème d’incertitude avec des variables Variable<T> et Infer.NET
  • Entrainer un modèle bayesien simple (gaussien) par inférence postérieure
  • Refactorer un modèle en classes C# orientees objet reutilisables
  • Implementer l’apprentissage en ligne par mise a jour des a priori
  • Comparer des modèles (simple vs mélange) avec la preuve bayésienne (log evidence)

Introduction à la programmation probabiliste avec Infer.Net

Avant-propos

Les ordinateurs sont rigoureusement logiques, mais le monde réel ne l’est pas. Par exemple, supposons que vous deviez représenter ce qu’un utilisateur griffonne sur un écran tactile sous forme de mot. Les gens ne sont généralement pas très appliqués dans leur écriture, donc ce gribouillis peut correspondre à plusieurs mots possibles comme “hill”, “bull” ou “hello”. L’utilisateur sait ce qu’il a écrit, mais pour l’application, la valeur correcte est incertaine. Cependant, certaines valeurs sont plus probables que d’autres.

Comment représentez-vous une telle incertitude dans un programme ? Des variables conventionnelles telles que bool ou int doivent avoir des valeurs bien définies. Une solution est d’utiliser des variables aléatoires.

Variables aléatoires

La programmation probabiliste est conçue pour gérer une telle incertitude en utilisant des variables aléatoires. Une variable aléatoire représente un ensemble ou une plage de valeurs possibles, chacune associée à une probabilité.

// Exemple de création d'une variable aléatoire
Variable<bool> isHeads = Variable.Bernoulli(0.5);

Modèles probabilistes

Un modèle probabiliste définit comment les utilisateurs transforment les mots en gribouillis. Ce modèle reconnaît que le même mot peut conduire à différents gribouillis et que des mots différents peuvent conduire à des gribouillis similaires.

Inférence probabiliste

L’inférence probabiliste utilisé une méthodologie statistique connue sous le nom d’inférence bayésienne pour raisonner rétrospectivement d’une observation jusqu’à son origine.

Apprentissage probabiliste

Un modèle a généralement un ensemble de paramètres ajustables qui régissent son comportement. Pour adapter les paramètres au style d’écriture d’un utilisateur, un programme probabiliste peut traiter les paramètres du modèle eux-mêmes comme des variables aléatoires et apprendre les valeurs réelles des paramètres en fonction des entrées de l’utilisateur.

Présentation d’Infer.Net

Qu’est-ce que Infer.Net

Infer.NET est un framework permettant d’exécuter l’inférence bayésienne dans des modèles graphiques. Il fournit les algorithmes de passe-messages et les routines statistiques nécessaires à la réalisation d’inférences pour une grande variété d’applications.

Les principales caractéristiques d’Infer.NET sont :

  • Langage de modélisation : Supporte les variables univariées et multivariées, continues et discrètes.
  • Algorithmes d’inférence : Inclut la propagation d’espérance (EP), la propagation des convictions, le passage de messages variationnels et l’échantillonnage de Gibbs.
  • Conçu pour l’inférence à grande échelle : Compile les modèles dans du code source dédié.
  • Extensible par l’utilisateur : Permet d’ajouter des distributions de probabilité, des facteurs, des opérations de message et des algorithmes d’inférence.

Comment fonctionne Infer.NET

Infer.NET fonctionne en compilant une définition de modèle dans le code source nécessaire pour calculer un ensemble de requêtes d’inférence sur le modèle.

  1. Création de la définition de modèle : Utilisation de l’API de modélisation.
  2. Compilation du modèle : Le compilateur crée le code source nécessaire à l’exécution des requêtes d’inférence.
  3. Compilation du code source : Pour créer un algorithme compilé.
  4. Exécution de l’inférence : Utilisation du moteur d’inférence pour produire les distributions marginales demandées.

Installation des Packages Nuget et Usings

Pour utiliser Infer.NET dans votre projet .NET Interactive, vous devez installer le package Nuget Microsoft.ML.Probabilistic et inclure les usings nécessaires.

// Pour installer Infer.NET
#r "nuget: Microsoft.ML.Probabilistic"
#r "nuget: Microsoft.ML.Probabilistic.Compiler"
Console.WriteLine("Packages NuGet Microsoft.ML.Probabilistic charges");
Installed Packages
  • Microsoft.ML.Probabilistic, 0.4.2504.701
  • Microsoft.ML.Probabilistic.Compiler, 0.4.2504.701
Packages NuGet Microsoft.ML.Probabilistic charges

1a. Ce que l’installation etablit — et pourquoi le nom du paquet compte

La sortie Packages NuGet Microsoft.ML.Probabilistic charges est plus informative qu’elle n’en a l’air : Infer.NET n’est plus un paquet autonome. Il vit désormais a l’interieur de Microsoft.ML.Probabilistic, la bibliothèque probabiliste de la galaxie ML.NET. Deux consequences pratiques pour tout le reste du notebook.

D’abord, la rumeur tenace selon laquelle « Infer.NET est un projet abandonne » se lit ici pour ce qu’elle est : le projet a change d’adresse, pas disparu. Le moteur qui compilé les modèles et l’API Variable.* sont ceux d’Infer.NET, sous un autre nom de paquet.

Ensuite, l’installation est un geste unique et suffisant : tout ce que ce notebook utilisé — modèles, moteur d’inférence, distributions a posteriori — vient de ce seul paquet. Il n’y a pas de dependance native a installer, pas de compilateur externe a cabler. C’est ce qui rend le notebook executable de bout en bout sur une machine nue, et c’est cette propriete qui permet de committer des sorties réelles plutot qu’un mode degrade.

(Infer fait désormais parti de la bibliothèque ML.Net)

Import des espaces de noms essentiels

using Microsoft.ML.Probabilistic;
using Microsoft.ML.Probabilistic.Distributions;
using Microsoft.ML.Probabilistic.Utilities;
using Microsoft.ML.Probabilistic.Math;
using Microsoft.ML.Probabilistic.Models;
using Microsoft.ML.Probabilistic.Algorithms;
using Microsoft.ML.Probabilistic.Compiler;
Console.WriteLine("Espaces de noms Microsoft.ML.Probabilistic importes");
Espaces de noms Microsoft.ML.Probabilistic importes

1b. Ce que deux espaces de noms separent

Espaces de noms Microsoft.ML.Probabilistic importes : l’import tient en deux lignes, et la separation n’est pas cosmetique.

Le premier espace porte les variables aléatoires et les operateurs — Variable.Bernoulli, Variable.GaussianFromMeanAndPrecision, Variable.If. C’est le vocabulaire avec lequel on écrit un modèle. Le second porte les algorithmes et le moteur — InferenceEngine, CompilerChoice, Algorithms. C’est l’outillage qui execute le modèle.

Retenez la frontiere, elle structuré tout ce qui suit : un modèle Infer.NET se lit comme une declaration (des variables et des liens de dependance), jamais comme une suite d’instructions. La cellule suivante va le montrer sur trois lignes — et c’est cette separation qui explique que l’on puisse changer d’algorithme d’inférence sans reecrire une seule ligne du modèle.

Un exemple simple

Voici un exemple d’utilisation d’Infer.NET pour déterminer la probabilité d’obtenir deux faces lors du lancement de deux pièces non biaisées.

Variable<bool> premierePiece = Variable.Bernoulli(0.5);
Variable<bool> deuxiemePiece = Variable.Bernoulli(0.5);
Variable<bool> deuxFaces = premierePiece & deuxiemePiece;

InferenceEngine engine = new InferenceEngine();
engine.Compiler.CompilerChoice = CompilerChoice.Roslyn;
Console.WriteLine("Probabilité d’avoir deux faces: " + engine.Infer(deuxFaces));
Compiling model...done.
Probabilité d’avoir deux faces: Bernoulli(0,25)

1c. Bernoulli(0,25) : la première verification arithmetique du notebook

La sortie Probabilite d'avoir deux faces: Bernoulli(0,25) est le premier résultat verifiable du notebook, et il faut le verifier tout de suite : c’est lui qui etablit que la chaine complete (modèle -> moteur -> requête) fonctionne reellement.

Deux pièces independantes donnent 0,25. Chacune vaut 0,5, et les deux lancers sont independants dans le modèle. Le produit 0,5 x 0,5 = 0,25 est donc exactement ce que le moteur rend. Un modèle mal cable — pièces correlees, mauvaise distribution, requête sur la mauvaise variable — rendrait un autre nombre, et le desaccord serait visible immediatement.

C’est cette propriete qu’il faut conserver comme contrôle positif pour tout le reste du notebook : a chaque fois qu’une sortie sera plus difficile a lire (jusqu’au facteur de Bayes de la section 7), on pourra revenir ici se rappeler que le moteur ne se contente pas d’imprimer des distributions plausibles — il en calcule.

A noter la forme Bernoulli(0,25) : la notation emploie la virgule decimale. La même convention d’affichage reviendra partout, y compris dans Gaussian(15,33, 1,32), ou elle se combine avec un second piege — plus difficile, celui-la (section 2a).

Ce court exemple contient les trois éléments clés de tout programme Infer.NET :

  1. Définition d’un modèle probabiliste : Les variables aléatoires premierePiece et deuxiemePiece et la variable dépendante deuxFaces.
  2. Création d’un moteur d’inférence : Le moteur d’inférence est créé et configuré.
  3. Exécution d’une requête d’inférence : Le moteur est utilisé pour déduire la distribution marginale de deuxFaces.

Exercice 1 : Lancer de trois pièces et probabilités conditionnelles

Objectif : Etendre l’exemple des deux pièces pour modeliser trois lancers independants et calculer des probabilités conditionnelles.

Contexte : Vous lancez trois pièces non biaisées. Vous observez que la première est tombee sur face. Quelle est la probabilité que les trois soient face ?

Indices : - Indice 1 : Utilisez Variable.Bernoulli(0.5) pour chaque piece - Indice 2 : Combinez les trois avec l’opérateur & - Indice 3 : Pour le conditionnement, utilisez ObservedValue sur la première piece

// Exercice 1 : Lancer de trois pieces
// TODO etudiant : Creer trois variables booleennes pour trois pieces
// TODO etudiant : Calculer la probabilite que les trois soient face
// TODO etudiant : Conditionner sur la premiere piece etant face, puis recalculer
Console.WriteLine("Exercice a completer : lancer de trois pieces");
Exercice a completer : lancer de trois pieces

Mise en œuvre d’Infer.Net dans un exemple détaillé

Contexte

Scénario

Les programmes présentés ici sont basés sur le scénario suivant :

  • Plusieurs de vos collègues et vous-même rendez-vous à vélo chaque jour au travail.
  • Le temps de trajet d’un cycliste varie de jour en jour et sa valeur est donc aléatoire.
  • L’incertitude du temps de trajet est représentée par une distribution de probabilité qui définit le temps de trajet moyen et combien il varie.
  • L’application apprendra cette distribution à partir de plusieurs temps de trajet observés et utilisera ces connaissances pour faire des prévisions sur les temps de déplacement futurs.

Création du modèle

La première étape consiste à créer les variables aléatoires dureeMoyenne et bruitTrafic avec des distributions initiales.

// Définir le modèle
Variable<double> dureeMoyenne = Variable.GaussianFromMeanAndPrecision(15, 0.01);
Variable<double> bruitTrafic = Variable.GammaFromShapeAndScale(2, 0.5);
Console.WriteLine("Modele defini : dureeMoyenne ~ Gaussian(15, 0.01), bruitTrafic ~ Gamma(2, 0.5)");
Modele defini : dureeMoyenne ~ Gaussian(15, 0.01), bruitTrafic ~ Gamma(2, 0.5)

2a. Le piege fondateur : GaussianFromMeanAndPrecision(15, 0.01) n’est pas un a priori serre

Le modèle est declare par Variable.GaussianFromMeanAndPrecision(15, 0.01) et Variable.GammaFromShapeAndScale(2, 0.5). Lisez ces deux lignes avant de regarder la suite, parce que le nom de la fonction est le seul endroit ou le second argument se declare comme une précision :

Ligne de code Second argument Sa lecture correcte Variance equivalente
GaussianFromMeanAndPrecision(15, 0.01) précision 0,01 1 / 0,01 100
GammaFromShapeAndScale(2, 0.5) forme et échelle moyenne = 2 x 0,5 = 1,0 précision 1,0 -> variance 1,0

Le reflexe naturel — lire 0.01 comme une variance — donnerait un écart type de 0,1 minute : un a priori extraordinairement serre, qui ecraserait les observations et forcerait la moyenne a rester collee a 15. Ce n’est pas ce que fait ce modèle. La précision 0,01 equivaut a une variance de 100, soit un écart type de 10 minutes : un a priori volontairement vague, presque sans opinion.

La différence n’est pas theorique, elle est lisible dans les résultats. La section 2b montre que la moyenne a posteriori vaut 15,33 après trois observations dont la moyenne arithmetique est exactement 15,33 : le posterior est entierement détermine par les données, ce qui n’est possible que parce que l’a priori laissait la place. Avec un a priori de variance 0,01, la même experience aurait rendu une moyenne coincee pres de 15.

Le vrai enseignement est methodologique : dans Infer.NET, le parametrage est porte par le nom de la fabrique, et une même distribution s’écrit de plusieurs facons selon la fabrique choisie. La section 6a montrera que ce notebook emploie, quelques cellules plus loin, l’autre notation pour exactement le même a priori.

Ensuite, nous définissons les variables pour les temps de trajet observés (dureeLundi, dureeMardi, dureeMercredi).

// Définir les temps de trajet
Variable<double> dureeLundi = Variable.GaussianFromMeanAndPrecision(dureeMoyenne, bruitTrafic);
Variable<double> dureeMardi = Variable.GaussianFromMeanAndPrecision(dureeMoyenne, bruitTrafic);
Variable<double> dureeMercredi = Variable.GaussianFromMeanAndPrecision(dureeMoyenne, bruitTrafic);
Console.WriteLine("Variables de trajet definies : dureeLundi, dureeMardi, dureeMercredi");
Variables de trajet definies : dureeLundi, dureeMardi, dureeMercredi

Entraînement du modèle

Nous allons maintenant observer les temps de trajet pour trois jours et entraîner le modèle.

// Observations des temps de trajet
dureeLundi.ObservedValue = 13;
dureeMardi.ObservedValue = 17;
dureeMercredi.ObservedValue = 16;
Console.WriteLine("Observations definies : lundi=13, mardi=17, mercredi=16");
Observations definies : lundi=13, mardi=17, mercredi=16

Pour calculer les distributions postérieures pour dureeMoyenne et bruitTrafic, nous utilisons le moteur d’inférence.

// Entraînement du modèle et calcul des postérieurs
InferenceEngine engine = new InferenceEngine();
engine.Compiler.CompilerChoice = CompilerChoice.Roslyn;
Gaussian moyennePosterieure = engine.Infer<Gaussian>(dureeMoyenne);
Gamma bruitPosterieur = engine.Infer<Gamma>(bruitTrafic);

Console.WriteLine($"Moyenne a posteriori: {moyennePosterieure}");
Console.WriteLine($"Bruit traffic a posteriori: {bruitPosterieur}");
Compiling model...done.
Iterating: 
.........|.........|.........|.........|.........| 50
Moyenne a posteriori: Gaussian(15,33, 1,32)
Bruit traffic a posteriori: Gamma(2,242, 0,2445)[mean=0,5482]

2b. Lire une sortie d’Infer.NET : ce que Gaussian(15,33, 1,32) et Gamma(2,242, 0,2445)[mean=0,5482] disent exactement

Deux lignes de sortie, deux objets de nature differente, et un piege de lecture dans chacune.

Moyenne a posteriori: Gaussian(15,33, 1,32) — le second argument est la variance, pas l’écart type (la section 2c en donne la preuve independante par Math.Sqrt). Donc : moyenne 15,33, écart type 1,15. La moyenne arithmetique des trois observations (13 + 17 + 16) / 3 = 15,33 est retrouvee au centieme — c’est le signe que l’a priori vague de la section 2a a bien laisse les données parler.

Bruit traffic a posteriori: Gamma(2,242, 0,2445)[mean=0,5482] — ici l’arithmetique se verifie directement : 2,242 x 0,2445 = 0,5482, et c’est exactement le [mean=...] imprime. Le moteur affiche donc, en plus de la distribution, la moyenne qu’il en a calculee : c’est un contrôle gratuit que l’on peut refaire de tete a chaque ligne de ce genre dans le notebook.

Reste a savoir ce que ce 0,5482 mesure. bruitTrafic est la précision des observations, pas leur variance : une précision plus haute signifie des trajets plus reguliers. Sa valeur postérieure est donc a lire a l’envers de l’intuition — et c’est précisément ce que la section 3b etablit sur neuf observations, ou cette quantite s’effondre alors que l’on aurait pu croire le contraire.

Interpretation : Posterieurs après 3 observations

Avec les observations (13, 17, 16 min), l’inférence bayésienne met a jour les distributions :

Paramètre A priori Posterieur
Moyenne (dureeMoyenne) Gaussian(15, 100) Gaussian(15.33, 1.32)
Bruit (bruitTrafic) Gamma(2, 0.5) Gamma(2.24, 0.24)

L’a priori sur la moyenne etait très peu informatif (variance = 100), donc le postérieur est presque entierement détermine par les 3 observations. La moyenne arithmetique des données est (13+17+16)/3 = 15.33 min — identique au postérieur, ce qui confirme que l’a priori n’a pas “tire” le résultat.

Prédiction du temps de trajet

Nous allons maintenant utiliser le modèle entraîné pour prédire le temps de trajet de demain (dureeDemain).

// Prédiction du temps de trajet de demain
Variable<double> dureeDemain = Variable.GaussianFromMeanAndPrecision(dureeMoyenne, bruitTrafic);
Gaussian distribDemain = engine.Infer<Gaussian>(dureeDemain);

Console.WriteLine($"Prédiction demain: {distribDemain}, écart type: {Math.Sqrt(distribDemain.GetVariance())}");
Compiling model...done.
Iterating: 
.........|.........|.........|.........|.........| 50
Prédiction demain: Gaussian(15,33, 4,613), écart type: 2,14776700894404

2c. Compiling model...done. : l’inférence est compilee — et c’est visible dans les sorties

Le debut de cette sortie porte Compiling model...done., et ce n’est pas un ornement de journal : le moteur compilé le modèle en code, il ne l’interprete pas. La cellule c18 a d’ailleurs fixe explicitement le compilateur (engine.Compiler.CompilerChoice = CompilerChoice.Roslyn), ce qui rend la chose visible : le modèle probabiliste devient du C# compilé, puis execute.

Cette mecanique explique un detail de lecture qui, sinon, passe pour un caprice : certaines cellules impriment Compiling model...done. et d’autres pas du tout. Comparez la sortie de cette cellule avec celle de c40 (apprentissage en ligne) : la première compilé, la seconde non. La raison n’est pas l’importance du calcul, mais ceci : c40 reutilise un objet monEntrainement dont le modèle a déjà ete compilé et dont la structuré n’a pas change — seules les valeurs observées diffèrent. La compilation est mise en cache tant que la structuré du modèle est identique.

Retenez donc la regle de lecture, elle vaut pour tout Infer.NET : Compiling model...done. signale un modèle dont la structuré change entre deux requêtes. Changer une valeur observee, ou interroger une nouvelle variable derivee du même modèle (ce que fait cette cellule avec dureeDemain), peut declencher une recompilation ou non, et la sortie le dit.

Enfin, Iterating: ........| 50 est le compte de rendu de Expectation Propagation, l’algorithme par defaut. Ce compteur reviendra identique dans presque toutes les cellules suivantes : il ne mesure pas la qualité du résultat, il mesure l’effort du moteur. Un 50 partout n’est pas une coincidence, c’est le plafond d’iterations par defaut.

Interpretation : Distribution predictive

La prédiction du trajet de demain a une variance plus large que le postérieur de la moyenne. Infer.NET affiche Gaussian(moyenne, variance) : le second paramètre est la variance (verifiable via GetVariance() dans la cellule précédente, sqrt(4.613) ~ 2.15), et non la précision. La variance predictive se decompose donc en deux termes qui s’ajoutent pour atteindre le total observé :

Source Variance
Incertitude sur la moyenne (Gaussian(15.33, 1.32)) 1.32 min^2
Bruit du trajet (E[1/bruit], bruit etant la précision Gamma(2.24, 0.24)) ~ 3.29 min^2
Variance totale predictive 1.32 + 3.29 ~ 4.61 min^2 -> écart type 2.15 min

L’incertitude totale combine l’incertitude sur les paramètres ET le bruit inherent du trajet. Même si on connaissait exactement la moyenne réelle, les trajets varieraient toujours autour d’elle.

Calculer des probabilités basées sur les prédictions

Nous pouvons également calculer la probabilité que le trajet de demain prenne moins de 18 minutes.

// Calcul de la probabilité
double probMoinsDe18Mn = engine.Infer<Bernoulli>(dureeDemain < 18.0).GetProbTrue();
Console.WriteLine($"Probabilité que le trajet prenne moins de 18mn: {probMoinsDe18Mn:F2}");
Compiling model...done.
Iterating: 
.........|.........|.........|.........|.........| 50
Probabilité que le trajet prenne moins de 18mn: 0,89

2d. Verifier la probabilité imprimee : le contrôle qui valide toute la lecture

Probabilite que le trajet prenne moins de 18mn: 0,89. Une probabilité a deux decimales est facile a imprimer et impossible a falsifier du regard : il faut la recalculer. C’est faisable, parce que la cellule précédente a donne les deux paramètres de la distribution predictive.

Moyenne 15,33 et variance 4,613, donc écart type 2,148. On cherche la masse sous 18 minutes :

  • écart reduit : z = (18 − 15,33) / 2,148 = 1,243 ;
  • probabilité cumulee : la fonction de repartition de la loi normale en 1,243 vaut 0,893.

Le 0,89 imprime est exactement cet arrondi. Ce n’est pas une coincidence : la même verification reussit sur les deux autres probabilités du notebook (section 3c et section 4b). Cette concordance etablit trois choses d’un coup :

  1. la distribution predictive est bien une gaussienne de moyenne 15,33 ;
  2. son second paramètre est bien une variance et non un écart type, sans quoi le calcul ci-dessus aurait donne un tout autre nombre ;
  3. la requête engine.Infer<Bernoulli>(dureeDemain < 18.0) calcule une vraie probabilité de depassement, et non une heuristique.

C’est le genre de contrôle qui coute trente secondes et qui separe une sortie lue d’une sortie recopiee. Un notebook d’inférence qui n’en pose aucun laisse son lecteur croire sur parole.

Résumé

En résumé, nous avons défini un modèle probabiliste pour les temps de trajet d’un cycliste, observé les données pour entraîner le modèle, et utilisé ce modèle pour faire des prédictions sur les temps de trajet futurs. Le code est structuré de manière à tirer parti des capacités interactives du notebook .NET Interactive, permettant ainsi une approche progressive et claire.

Restructuration de notre application

Introduction

L’application DureeCycliste1 a présenté les basés de l’apprentissage des paramètres, mais elle n’est pas facilement extensible pour des scénarios plus sophistiqués. Nous allons restructurer l’application pour encapsuler le code de modélisation dans des classes séparées et introduire l’utilisation de tableaux de variables aléatoires pour les variables observées.

Encapsulation dans des classes séparées

Nous allons créer une basé commune pour les modèles d’entraînement et de prédiction en utilisant une classe de basé CyclisteBase, puis créer des classes spécifiques pour l’entraînement (EntrainementCycliste) et la prédiction (PredictionCycliste).

Classe de basé

La classe CyclisteBase contient les éléments communs aux modèles d’entraînement et de prédiction.

using Microsoft.ML.Probabilistic.Algorithms;

public struct DonneesCycliste
{
    public Gaussian DistribMoyenne;
    public Gamma DistribBruitTraffic;
    public DonneesCycliste(Gaussian moyenne, Gamma precision)
    {
        DistribMoyenne = moyenne;
        DistribBruitTraffic = precision;
    }
}

public class CyclisteBase
{
    public InferenceEngine MoteurInference;
    protected Variable<double> Moyenne;
    protected Variable<double> Bruit;
    protected Variable<Gaussian> MoyenneAPriori;
    protected Variable<Gamma> BruitAPriori;

    public virtual void CreationModeleBayesien()
    {
        MoyenneAPriori = Variable.New<Gaussian>();
        BruitAPriori = Variable.New<Gamma>();
        Moyenne = Variable.Random<double, Gaussian>(MoyenneAPriori);
        Bruit = Variable.Random<double, Gamma>(BruitAPriori);

        if (MoteurInference == null)
        {
            MoteurInference = new InferenceEngine(new ExpectationPropagation());
            MoteurInference.Compiler.CompilerChoice = CompilerChoice.Roslyn;
        }
    }

    public virtual void DefinirDistributions(DonneesCycliste distribsApriori)
    {
        MoyenneAPriori.ObservedValue = distribsApriori.DistribMoyenne;
        BruitAPriori.ObservedValue = distribsApriori.DistribBruitTraffic;
    }
    
}

Console.WriteLine($"Classes definies : CyclisteBase, ModeleCycliste");
Classes definies : CyclisteBase, ModeleCycliste

3a. Ce que la refactorisation OO change — et ce qu’elle ne change pas

Classes definies : CyclisteBase, ModeleCycliste : le modèle probabiliste vient de passer d’une suite de variables locales a une hierarchie de classes, avec une classe de basé portant la structuré et deux sous-classes, l’une pour l’entraînement et l’autre pour la prédiction.

Ce qu’elle ne change pas, d’abord — et c’est le point a ne pas manquer. Le modèle mathematique est identique : mêmes a priori, mêmes liens de dependance, même moteur. La section 3c le montrera chiffre en main, en comparant les sorties des deux chemins sur les mêmes données. Une refactorisation qui deplacerait les nombres serait un bug, pas une amelioration.

Ce qu’elle change, ensuite, et qui justifie le detour :

  • Le modèle devient une valeur reutilisable. Un objet d’entraînement peut être instancie plusieurs fois, pour plusieurs cyclistes, sans recopier la définition du modèle.
  • Entrainement et prédiction sont separes. C’est cette separation qui rend possible la cellule c40, ou l’on reutilise un objet déjà entraîné en lui fournissant de nouvelles données — donc l’apprentissage en ligne de la section 4.
  • Les distributions deviennent des paramètres. DefinirDistributions recoit les a priori (et plus tard les postérieurs) en argument. C’est ce qui permet a la section 4a de passer les postérieurs d’une semaine comme a priori de la suivante : sans cette entree, l’apprentissage en ligne demanderait de reecrire le modèle a chaque periode.

Autrement dit, la refactorisation ne rend pas le calcul meilleur — elle rend le modèle manipulable. C’est la condition technique des trois sections suivantes.

Classe d’entraînement

La classe EntrainementCycliste hérite de CyclisteBase et implémente le modèle d’entraînement.

public class EntrainementCycliste : CyclisteBase
{
    protected VariableArray<double> TempsDeTrajet;
    protected Variable<int> NombreDeTrajets;

    public override void CreationModeleBayesien()
    {
        base.CreationModeleBayesien();
        NombreDeTrajets = Variable.New<int>();
        Range indiceTrajet = new Range(NombreDeTrajets);
        TempsDeTrajet = Variable.Array<double>(indiceTrajet);
        using (Variable.ForEach(indiceTrajet))
        {
            TempsDeTrajet[indiceTrajet] = Variable.GaussianFromMeanAndPrecision(Moyenne, Bruit);
        }
    }

    public DonneesCycliste CalculePosterieurs(double[] donneesObservees)
    {
        DonneesCycliste posterieurs;
        NombreDeTrajets.ObservedValue = donneesObservees.Length;
        TempsDeTrajet.ObservedValue = donneesObservees;
        posterieurs.DistribMoyenne = MoteurInference.Infer<Gaussian>(Moyenne);
        posterieurs.DistribBruitTraffic = MoteurInference.Infer<Gamma>(Bruit);
        return posterieurs;
    }
}
Console.WriteLine("Classe EntrainementCycliste definie (entrainement par gaussienne simple)");
Classe EntrainementCycliste definie (entrainement par gaussienne simple)

Classe de prédiction

La classe PredictionCycliste implémente le modèle de prédiction.

public class PredictionCycliste : CyclisteBase
{
    private Gaussian demainDistrib;
    public Variable<double> demainTemps;

    public override void CreationModeleBayesien()
    {
        base.CreationModeleBayesien();
        demainTemps = Variable.GaussianFromMeanAndPrecision(Moyenne, Bruit);
    }

    public Gaussian EstimerTempsDemain()
    {
        demainDistrib = MoteurInference.Infer<Gaussian>(demainTemps);
        return demainDistrib;
    }

    public Bernoulli EstimerTempsDemainInferieurA(double duree)
    {
        return MoteurInference.Infer<Bernoulli>(demainTemps < duree);
    }
}
Console.WriteLine("Classe PredictionCycliste definie (prediction par modele simple)");
Classe PredictionCycliste definie (prediction par modele simple)

Utilisation du modèle

Nous allons utiliser ces classes pour entraîner le modèle et prédire le temps de déplacement de demain.

Entraînement

double[] donneesTrajets = new[] { 13, 17, 20, 25, 16, 11, 16, 14, 12.5 };
DonneesCycliste mesDistributions = new DonneesCycliste(
    Gaussian.FromMeanAndPrecision(1, 0.01),
    Gamma.FromShapeAndScale(2, 0.5));

EntrainementCycliste monEntrainement = new EntrainementCycliste();
monEntrainement.CreationModeleBayesien();
monEntrainement.DefinirDistributions(mesDistributions);
DonneesCycliste monPosterieur = monEntrainement.CalculePosterieurs(donneesTrajets);

Console.WriteLine($"Moyenne a posteriori: {monPosterieur.DistribMoyenne}");
Console.WriteLine($"Bruit traffic a posteriori: {monPosterieur.DistribBruitTraffic}");
Compiling model...done.
Iterating: 
.........|.........|.........|.........|.........| 50
Moyenne a posteriori: Gaussian(15,85, 1,762)
Bruit traffic a posteriori: Gamma(5,158, 0,01568)[mean=0,08087]

3b. Neuf observations : ce que la précision du bruit mesure vraiment

Les deux postrerieurs de cette cellule sont Gaussian(15,85, 1,762) et Gamma(5,158, 0,01568)[mean=0,08087] — et le second est celui qu’il faut regarder de pres, parce qu’il contredit une lecture spontanee.

Verifions d’abord l’arithmetique, comme en 2b : 5,158 x 0,01568 = 0,0809, soit exactement le [mean=...] imprime. La encore, le moteur publie sa propre moyenne, et elle se recalcule.

Maintenant la comparaison des deux experiences du notebook :

3 observations (13, 17, 16) 9 observations (13, 17, 20, 25, 16, 11, 16, 14, 12,5)
Moyenne postérieure 15,33 15,85
Precision du bruit postérieure 0,5482 0,0809
Variance du bruit (1 / précision) 1,82 12,36
Variance de la moyenne postérieure 1,32 1,76

La précision du bruit a baisse d’un facteur 6,8 en passant de 3 a 9 observations, et la variance du bruit a augmente d’autant. On aurait pu attendre l’inverse de la part d’un modèle « mieux informe » : c’est la lecture qu’un tableau hâtif suggere plus haut dans le notebook.

La raison est que cette précision ne mesure pas la quantite de données, elle mesure leur dispersion. Le premier echantillon (13, 17, 16) est serre : trois trajets a deux minutes d’écart au plus. Le second va de 11 a 25 minutes — une amplitude de 14 minutes. Un modèle qui explique ces neuf valeurs doit admettre beaucoup plus de bruit que celui qui expliquait les trois premieres, et le posterior le dit sans qu’on le lui demande.

Retenez la distinction, elle est le coeur de cette section : plus de données ne veut pas dire un modèle plus confiant, cela veut dire un modèle mieux calibre. Confiance et calibrage sont deux choses differentes, et ici elles evoluent en sens contraires. C’est ce que la section 3c met a l’epreuve sur les prédictions.

Interpretation : Posterieurs avec 9 observations

Avec 9 observations (vs 3 dans l’exemple précédent), le modèle est plus précis :

Paramètre A priori Posterieur (3 obs) Posterieur (9 obs)
Moyenne Gaussian(1, 0.01) Gaussian(15.33, 1.32) Gaussian(15.85, 1.76)
Precision bruit Gamma(2, 0.5) Gamma(2.24, 0.24) Gamma(5.16, 0.016)

Plus de données → postérieurs plus concentres (précision plus élevée) → prédictions plus fiables.

Prédiction

PredictionCycliste maPrediction = new PredictionCycliste();
maPrediction.CreationModeleBayesien();
maPrediction.DefinirDistributions(monPosterieur);
Gaussian distribDemain = maPrediction.EstimerTempsDemain();

Console.WriteLine($"Prédiction demain: {distribDemain}, écart type: {Math.Sqrt(distribDemain.GetVariance())}");
double probMoinsDe18Mn = maPrediction.EstimerTempsDemainInferieurA(18).GetProbTrue();
Console.WriteLine($"Probabilité que le trajet prenne moins de 18mn: {probMoinsDe18Mn:F2}");
Compiling model...done.
Prédiction demain: Gaussian(15,85, 17,1), écart type: 4,135447159938724
Compiling model...done.
Probabilité que le trajet prenne moins de 18mn: 0,70

3c. Le contrôle de coherence a travers les deux chemins de code

Cette prédiction vient du chemin OO (classes EntrainementCycliste / PredictionCycliste), après 9 observations. La section 2 avait obtenu une prédiction par le chemin « plat », après 3 observations. Comparons ce qui doit l’être, et verifions les nombres au passage.

Chemin plat, 3 obs (section 2) Chemin OO, 9 obs (ici)
Prediction Gaussian(15,33, 4,613) Gaussian(15,85, 17,1)
Ecart type imprime 2,14776700894404 4,135447159938724
P(trajet < 18 min) 0,89 0,70

Trois verifications, toutes reussies :

  1. L’écart type est bien la racine de la variance. racine(4,613) = 2,1478 et racine(17,1) = 4,1352 — les deux valeurs imprimees par Math.Sqrt(distribDemain.GetVariance()) concordent avec le second argument de la distribution, a l’arrondi d’affichage pres (le moteur imprime la variance a trois decimales, puis sa racine a seize). Cette concordance, refaite ici sur deux jeux de données différents, est la preuve que Gaussian(moyenne, variance) se lit avec une variance.
  2. La probabilité est coherente avec ses paramètres. z = (18 − 15,85) / 4,135 = 0,520 et la fonction de repartition vaut 0,698 : le 0,70 imprime suit. Meme contrôle qu’en 2d, sur un autre jeu.
  3. Les deux chemins de code calculent le même modèle. La prédiction a change (15,33 -> 15,85) parce que les données ont change, pas parce que l’architecture a change. Si la refactorisation OO avait modifie le modèle, l’écart serait apparu ici.

Et l’effet de domaine, qui est le vrai enseignement de cette section : la prédiction est devenue bien plus incertaine en recevant plus de données — écart type 2,15 -> 4,14, probabilité d’arriver en moins de 18 minutes 0,89 -> 0,70. Ce n’est pas un paradoxe, c’est la consequence directe de la section 3b. Comme la précision du bruit a chute (0,5482 -> 0,0809), la variance predictive a augmente. Le modèle a appris que cette cycliste est imprevisible, et il le dit. Une baisse de la probabilité imprimee n’est donc pas un echec du modèle : c’est un résultat.

Interpretation : Predictions du modèle OO

Le modèle OO produit les mêmes résultats que le modèle plat (même algorithme, meilleure architecture) :

  • Prediction demain : Gaussian(15.85, 17.1) → moyenne 15.85 min, écart type 4.14 min
  • P(trajet < 18 min) : 0.70 (70% de chances)

La refactorisation OO n’affecte pas les résultats numériques, mais permet de : 1. Reutiliser les classes pour plusieurs cyclistes independants 2. Separer l’entraînement de la prédiction 3. Gerer facilement l’apprentissage en ligne

Avec cette approche, nous avons encapsulé les modèles d’entraînement et de prédiction dans des classes séparées tout en mutualisant les parties communes dans une classe de basé. Cela permet de gérer efficacement plusieurs ensembles d’observations et améliore les performances en évitant la recompilation du modèle.

Apprentissage en ligne avec Infer.NET

L’application DureeCycliste3 implémente l’apprentissage en ligne et la gestion des événements extraordinaires en utilisant un modèle de mélange de Gaussiennes. Cette méthode permet d’apprendre progressivement les paramètres du modèle en fonction des nouvelles données, améliorant ainsi les prévisions.

1. Mise à jour des postérieurs

Dans l’apprentissage en ligne, nous commençons par des a priori initiaux larges, puis nous les mettons à jour avec les postérieurs calculés à partir des nouvelles données. Voici comment implémenter ce processus :

Code d’apprentissage en ligne

double[] semaineSuivante = new double[] { 18, 25, 30, 14, 11 };
maPrediction.DefinirDistributions(monPosterieur);
DonneesCycliste posterieurSemaineSuivante = monEntrainement.CalculePosterieurs(semaineSuivante);
Console.WriteLine($"moyenne postérieure semaine suivante: {posterieurSemaineSuivante.DistribMoyenne}");
Console.WriteLine($"bruit postérieur semaine suivante: {posterieurSemaineSuivante.DistribBruitTraffic}");
Iterating: 
.........|.........|.........|.........|.........| 50
moyenne postérieure semaine suivante: Gaussian(18,49, 9,785)
bruit postérieur semaine suivante: Gamma(2,747, 0,01289)[mean=0,03541]

4a. L’apprentissage en ligne, et la seule cellule qui ne recompile pas

moyenne postérieure semaine suivante: Gaussian(18,49, 9,785) et bruit postérieur semaine suivante: Gamma(2,747, 0,01289)[mean=0,03541].

Regardez d’abord ce qui manque a cette sortie : il n’y a aucun Compiling model...done.. Elle commence directement par Iterating:, ce qui la distingue de toutes les cellules de calcul du notebook. La raison a ete etablie en 2c : cette cellule reutilise monEntrainement, dont la structuré de modèle est déjà compilee, et ne fait que lui fournir un nouveau tableau. La compilation est mise en cache tant que la structuré ne bouge pas.

L’arithmetique se verifie encore : 2,747 x 0,01289 = 0,03541, exactement le [mean=...] imprime.

Sur le fond, ce que fait l’apprentissage en ligne est exactement ceci : les postérieurs de la periode précédente deviennent les a priori de la periode suivante. Les cinq nouveaux trajets (18, 25, 30, 14, 11) ne sont donc pas traites comme cinq observations venant s’ajouter a un a priori vague : ils viennent mettre a jour une croyance déjà etablie. D’ou deux consequences lisibles :

  • la moyenne se deplace de 15,85 a 18,49, tiree vers le haut par les trajets longs de la deuxieme semaine ;
  • la précision du bruit tombe a 0,03541, la plus basse du notebook — la deuxieme semaine est la plus dispersee (11 a 30 minutes) et le modèle, encore une fois, le declare.

Notez enfin que ce déplacement n’est pas un oubli de la première semaine : le prior de la semaine 2 etant le posterior de la semaine 1 (variance 1,76, donc informatif), les neuf premiers trajets continuent de peser. Online ne signifie pas « amnesique ».

Interpretation : Mise a jour des postérieurs (apprentissage en ligne)

L’apprentissage en ligne consiste a passer les postérieurs de la semaine 1 comme a priori de la semaine 2 :

Paramètre Après semaine 1 Après semaine 2 Commentaire
Moyenne Gaussian(15.85, 1.76) Gaussian(18.49, 9.79) Augmentation due aux longs trajets
Bruit Gamma(5.16, 0.016) Gamma(2.75, 0.013) Variance plus élevée (semaine instable)

La cle de l’apprentissage en ligne : les postérieurs de la periode t deviennent les a priori de la periode t+1, permettant une mise a jour progressive sans necessiter toutes les données historiques.

2. Nouvelles prédictions

Après avoir mis à jour les postérieurs avec les nouvelles données, nous utilisons ces postérieurs pour faire de nouvelles prédictions.

Code de prédiction

maPrediction.DefinirDistributions(posterieurSemaineSuivante);
Gaussian distribDemain = maPrediction.EstimerTempsDemain();
Console.WriteLine($"Prédiction demain: {distribDemain}");
Console.WriteLine($"Ecart type: {Math.Sqrt(distribDemain.GetVariance())}");
double probMoinsDe18Mn = maPrediction.EstimerTempsDemainInferieurA(18).GetProbTrue();
Console.WriteLine($"Probabilité que le trajet prenne moins de 18mn: {probMoinsDe18Mn:F2}");
Prédiction demain: Gaussian(18,49, 54,19)
Ecart type: 7,361051425330401
Compiling model...done.
Probabilité que le trajet prenne moins de 18mn: 0,47

4b. Une prevision qui traverse sa propre moyenne

Prédiction demain: Gaussian(18,49, 54,19) puis Ecart type: 7,361051425330401, et Probabilité que le trajet prenne moins de 18mn: 0,47.

Le detail qui vaut la peine d’être vu est le dernier. La moyenne predictive est de 18,49 minutes et la question posee est « moins de 18 minutes » : la prevision a donc traverse sa propre moyenne. Autrement dit, l’événement demande est désormais marginalement improbable, alors qu’il etait confortablement probable au debut du notebook.

Verifions le nombre, comme partout ailleurs : racine(54,19) = 7,3614 — l’écart type imprime concorde a l’arrondi d’affichage pres. Puis z = (18 − 18,49) / 7,361 = −0,067, et la fonction de repartition vaut 0,4735, soit le 0,47 imprime. Troisieme reussite du même contrôle.

Le trajet de l’incertitude sur l’ensemble du notebook se lit alors en une ligne :

Etape Moyenne predictive Ecart type P(< 18 min)
3 observations 15,33 2,15 0,89
9 observations 15,85 4,14 0,70
après semaine 2 18,49 7,36 0,47

La probabilité est passee de 0,89 a 0,47 sans qu’aucun modèle n’ait change : seules les données ont change. C’est l’illustration la plus nette de ce que ce notebook cherche a montrer — en inférence bayésienne, la reponse a une question n’est pas une propriete du modèle seul, c’est une propriete du couple (modèle, données), et elle bouge avec les données.

Interpretation : Predictions après la deuxieme semaine

Après avoir observé 5 nouveaux trajets (18, 25, 30, 14, 11), le modèle a recalibré ses prédictions :

Metrique Après semaine 1 Après semaine 2 Evolution
Moyenne predictive 15.85 min 18.49 min +2.64 min
Ecart type 4.14 min 7.36 min Plus incertain
P(trajet < 18 min) 0.70 0.47 Baisse (-0.23)

L’augmentation de l’écart type reflete que la deuxieme semaine avait une forte variabilité (de 11 a 30 min). L’apprentissage en ligne a correctement integre ces nouvelles informations sans oublier la semaine précédente.

Exercice 2 : Apprentissage en ligne avec vos propres données

Objectif : Appliquer l’apprentissage en ligne avec un nouveau jeu de données et interpréter l’evolution des postérieurs.

Contexte : Vous avez collecte les temps de trajet suivants sur une nouvelle semaine : [10, 12, 11, 14, 10]. Utilisez les postérieurs déjà calculés comme a priori pour cette nouvelle semaine.

Indices : - Indice 1 : Utilisez monEntrainement.CalculePosterieurs() avec les nouvelles données - Indice 2 : Comparez les postérieurs avant et après la mise a jour - Étape 1 : Définir les nouvelles données - Étape 2 : Calculer les nouveaux postérieurs - Étape 3 : Predire et comparer avec les prédictions précédentes

// Exercice 2 : Apprentissage en ligne avec nouvelles donnees
// TODO etudiant : Definir les nouvelles observations
double[] nouvellesDonnees = new double[] { /* Vos observations ici */ };

// TODO etudiant : Utiliser les posterieurs precedents comme a priori
// Indice : maPrediction.DefinirDistributions(...) puis monEntrainement.CalculePosterieurs(...)

// TODO etudiant : Afficher les nouveaux posterieurs et comparer
Console.WriteLine("Exercice a completer : apprentissage en ligne");
Exercice a completer : apprentissage en ligne

Modèle mixte

Pour gérer des événements extraordinaires, nous utilisons un modèle de mélange de deux Gaussiennes. Ce modèle permet de mieux capturer la variabilité des temps de trajet.

Classe de basé pour le modèle mixte

public struct DonneesCyclisteMixte
    {
        public Gaussian[] DistribMoyenne;
        public Gamma[] DistribBruitTraffic;
        public Dirichlet DistribMixe;
    }

public class CyclisteBaseMixte
{
    public InferenceEngine MoteurInference;
    protected int NombreComposantes = 2;
    protected VariableArray<Gaussian> MoyennesAPriori;
    protected VariableArray<Gamma> BruitsAPriori;
    protected Variable<Dirichlet> MixeAPriori;
    protected VariableArray<double> Moyennes;
    protected VariableArray<double> Bruits;
    protected Variable<Vector> Mixe;

    public virtual void CreationModeleBayesien()
    {
        Range indiceComposants = new Range(NombreComposantes);
        MoteurInference = new InferenceEngine(new VariationalMessagePassing());
        MoteurInference.Compiler.CompilerChoice = CompilerChoice.Roslyn;
        MoyennesAPriori = Variable.Array<Gaussian>(indiceComposants);
        BruitsAPriori = Variable.Array<Gamma>(indiceComposants);
        Moyennes = Variable.Array<double>(indiceComposants);
        Bruits = Variable.Array<double>(indiceComposants);
        using (Variable.ForEach(indiceComposants))
        {
            Moyennes[indiceComposants] = Variable<double>.Random(MoyennesAPriori[indiceComposants]);
            Bruits[indiceComposants] = Variable<double>.Random(BruitsAPriori[indiceComposants]);
        }
        MixeAPriori = Variable.New<Dirichlet>();
        Mixe = Variable<Vector>.Random(MixeAPriori);
        Mixe.SetValueRange(indiceComposants);
    }

    public virtual void DefinirDistributions(DonneesCyclisteMixte distribsApriori)
    {
        MoyennesAPriori.ObservedValue = distribsApriori.DistribMoyenne;
        BruitsAPriori.ObservedValue = distribsApriori.DistribBruitTraffic;
        MixeAPriori.ObservedValue = distribsApriori.DistribMixe;
    }

    
}

5a. Lire les avertissements CS0649 presents dans la sortie

La sortie de cette cellule contient des avertissements du compilateur C#, de la forme warning CS0649: Le champ '...' n'est jamais assigne et aura toujours sa valeur par defaut null. Ils meritent deux minutes, parce qu’ils disent quelque chose de la structuré du modèle mixte.

Ce que le compilateur signale est précis : la structuré DonneesCyclisteMixte declare des champs (tableaux DistribMoyenne, DistribBruitTraffic, DistribMixe) que la cellule ne remplit pas elle-même. Ce n’est pas un defaut : la structuré sert de conteneur de distributions, et ce sont les classes d’entraînement et de prédiction qui la peuplent, a la manière de DefinirDistributions en section 3a. Le compilateur ne peut pas le savoir — d’ou l’avertissement, qui est une observation sur la portee locale du champ, pas un jugement sur le modèle.

Deux consequences pratiques :

  • Un avertissement n’est pas une erreur. La cellule poursuit et rend son résultat. Confondre les deux ferait passer ce notebook pour casse alors qu’il fonctionne ; c’est la même distinction qui gouverne le traitement des sorties dans tout notebook pedagogique, ou l’on exige l’absence d’erreur, pas l’absence d’avertissement.
  • Un avertissement visible est une information, pas un bruit a masquer. Un CS0649 sur un conteneur est le signe normal d’une structuré remplie par sa classe de basé. Le supprimer en initialisant les champs a null explicitement ne changerait rien au calcul et retirerait au lecteur cet indice sur l’architecture.

La bonne lecture d’une sortie de notebook n’est donc pas « tout est vert » : c’est « je sais ce que chaque ligne annonce ».

Classe d’entraînement pour le modèle mixte

public class EntrainementCyclisteMixte : CyclisteBaseMixte
{
    protected Variable<int> NombreDeTrajets;
    protected VariableArray<double> TempsDeTrajet;
    protected VariableArray<int> ComposantesTrajets;

    public override void CreationModeleBayesien()
    {
        base.CreationModeleBayesien();
        NombreDeTrajets = Variable.New<int>();
        Range indiceTrajet = new Range(NombreDeTrajets);
        TempsDeTrajet = Variable.Array<double>(indiceTrajet);
        ComposantesTrajets = Variable.Array<int>(indiceTrajet);
        using (Variable.ForEach(indiceTrajet))
        {
            ComposantesTrajets[indiceTrajet] = Variable.Discrete(Mixe);
            using (Variable.Switch(ComposantesTrajets[indiceTrajet]))
            {
                TempsDeTrajet[indiceTrajet].SetTo(Variable.GaussianFromMeanAndPrecision(Moyennes[ComposantesTrajets[indiceTrajet]], Bruits[ComposantesTrajets[indiceTrajet]]));
            }
        }
    }

    public DonneesCyclisteMixte CalculePosterieurs(double[] donneesObservees)
    {
        DonneesCyclisteMixte posterieurs;
        NombreDeTrajets.ObservedValue = donneesObservees.Length;
        TempsDeTrajet.ObservedValue = donneesObservees;
        posterieurs.DistribMoyenne = MoteurInference.Infer<Gaussian[]>(Moyennes);
        posterieurs.DistribBruitTraffic = MoteurInference.Infer<Gamma[]>(Bruits);
        posterieurs.DistribMixe = MoteurInference.Infer<Dirichlet>(Mixe);
        return posterieurs;
    }
}
Console.WriteLine("Classe EntrainementCyclisteMixte definie (entrainement par melange de gaussiennes)");
Classe EntrainementCyclisteMixte definie (entrainement par melange de gaussiennes)

Classe de prédiction pour le modèle mixte

public class PredictionCyclisteMixte : CyclisteBaseMixte
{
    private Gaussian demainDistrib;
    public Variable<double> demainTemps;

    public override void CreationModeleBayesien()
    {
        base.CreationModeleBayesien();
        Variable<int> indiceComposant = Variable.Discrete(Mixe);
        demainTemps = Variable.New<double>();
        using (Variable.Switch(indiceComposant))
        {
            demainTemps.SetTo(Variable.GaussianFromMeanAndPrecision(Moyennes[indiceComposant], Bruits[indiceComposant]));
        }
    }

    public Gaussian EstimerTempsDemain()
    {
        demainDistrib = MoteurInference.Infer<Gaussian>(demainTemps);
        return demainDistrib;
    }
}
Console.WriteLine("Classe PredictionCyclisteMixte definie (prediction par modele mixte)");
Classe PredictionCyclisteMixte definie (prediction par modele mixte)

Utilisation du modèle mixte

Entraînement

double[] donneesTrajets = new[] { 13, 17, 20, 25, 16, 11, 16, 25, 12.5, 30 };
DonneesCyclisteMixte mesDistribsAPriori = new DonneesCyclisteMixte
{
    DistribMoyenne = new Gaussian[]
    {
        new Gaussian(15, 100), // Ordinaire
        new Gaussian(30, 100) // Extraordinaire
    },
    DistribBruitTraffic = new Gamma[]
    {
        new Gamma(2, 0.5), // O
        new Gamma(2, 0.5) // E
    },
    DistribMixe = new Dirichlet(1, 1)
};
EntrainementCyclisteMixte monEntrainement = new EntrainementCyclisteMixte();
monEntrainement.CreationModeleBayesien();
monEntrainement.DefinirDistributions(mesDistribsAPriori);
DonneesCyclisteMixte posterieur = monEntrainement.CalculePosterieurs(donneesTrajets);
Console.WriteLine($"moyenne postérieure 1: {posterieur.DistribMoyenne[0]}");
Console.WriteLine($"bruit postérieur 1: {posterieur.DistribBruitTraffic[0]}");
Console.WriteLine($"moyenne postérieure 2: {posterieur.DistribMoyenne[1]}");
Console.WriteLine($"bruit postérieur 2: {posterieur.DistribBruitTraffic[1]}");
Console.WriteLine($"Coefficients du mélange: {posterieur.DistribMixe}");
Compiling model...done.
Iterating: 
.........|.........|.........|.........|.........| 50
moyenne postérieure 1: Gaussian(15,07, 0,8674)
bruit postérieur 1: Gamma(5,498, 0,02972)[mean=0,1634]
moyenne postérieure 2: Gaussian(26,69, 1,146)
bruit postérieur 2: Gamma(3,502, 0,08199)[mean=0,2871]
Coefficients du mélange: Dirichlet(7,995 4,005)

6a. Deux notations pour le même a priori, dans le même notebook

La cellule vient d’ecrire new Gaussian(15, 100) et new Gaussian(30, 100) pour les deux composantes du mélange. Confrontez cette ligne a celle de la section 2a :

Variable.GaussianFromMeanAndPrecision(15, 0.01)   // section 2 : fabrique "Precision"
new Gaussian(15, 100)                             // ici      : constructeur de distribution

Deux notations, et le même a priori exactement : 1 / 0,01 = 100. La fabrique nomme la précision, le constructeur prend la variance ; le notebook emploie les deux, a quelques cellules d’écart, sans le signaler. C’est la meilleure demonstration possible du piege de la section 2a — il n’est pas theorique, il traverse tout le fichier.

La regle de lecture qui en decoule : ne jamais déduire le sens du second argument du nombre, le déduire du nom. GaussianFromMeanAndPrecision -> précision. Gaussian.FromMeanAndPrecision -> précision. new Gaussian(m, v) -> variance. Un 100 peut donc valider un a priori serre ou vague selon la ligne qui l’écrit, et 0,01 fait ici l’inverse de ce qu’il suggere.

Sur le résultat lui-même, l’arithmetique se verifie une fois de plus :

Composante Moyenne postérieure Precision postérieure Verification
1 (ordinaire) 15,07 Gamma(5,498, 0,02972) -> 0,1634 5,498 x 0,02972 = 0,1634
2 (extraordinaire) 26,69 Gamma(3,502, 0,08199) -> 0,2871 3,502 x 0,08199 = 0,2871

Et les poids : Dirichlet(7,995, 4,005), dont le total vaut 12 — soit les 10 observations plus les 2 pseudo-comptages de l’a priori Dirichlet(1, 1) declare dans le code. Les poids rendus sont donc 7,995 / 12 = 0,666 et 4,005 / 12 = 0,334. Le prior n’est pas efface par les données : il est compte comme deux observations virtuelles, et il reste visible dans la somme.

Interpretation : Modèle a mélange de gaussiennes

Le modèle identifie deux regimes dans les données :

Composante Moyenne postérieure Interpretation
Composante 1 ~15 min Trajet normal (circulation fluide)
Composante 2 ~27 min Événement extraordinaire (accident, pluie…)

Les coefficients du mélange Dirichlet(8, 4) indiquent que ~67% des trajets sont normaux et ~33% sont extraordinaires, ce qui correspond aux données observées (7 valeurs <= 20 min, 3 valeurs >= 25 min dans les 10 observations).

Prédiction

// 2 - Prédiction
PredictionCyclisteMixte maPrediction = new PredictionCyclisteMixte();
maPrediction.CreationModeleBayesien();
maPrediction.DefinirDistributions(posterieur);
Gaussian distribDemain = maPrediction.EstimerTempsDemain();
Console.WriteLine($"Prédiction demain: {distribDemain}");
Console.WriteLine($"Ecart type: {Math.Sqrt(distribDemain.GetVariance())}");
Compiling model...done.
Iterating: 
.........|.........|.........|.........|.........| 50
Prédiction demain: Gaussian(18,48, 33,39)
Ecart type: 5,778650340614958

6b. La prevision du modèle mixte, lue sur les mêmes criteres

Prédiction demain: Gaussian(18,48, 33,39) puis Ecart type: 5,778650340614958.

Appliquons la même grille de lecture qu’aux sections précédentes, sans exception — c’est ainsi que l’on verifie qu’un modèle plus riche ne se contente pas d’être plus complique.

  • racine(33,39) = 5,7784, soit l’écart type imprime, a l’arrondi d’affichage pres. Quatrieme concordance variance / écart type du notebook.
  • La moyenne predictive (18,48) tombe presque exactement sur celle du modèle a une seule gaussienne en section 4b (18,49), alors que les deux composantes du mélange sont pourtant a 15,07 et 26,69. C’est le comportement attendu d’un mélange : la prevision est une moyenne ponderee des composantes, et les poids 0,666 / 0,334 de la section 6a la ramenent au voisinage du centre de gravite des données.
  • La variance, elle, ne se résumé pas de la même facon : 33,39 contre 54,19 pour le modèle simple, soit un écart type de 5,78 contre 7,36. Le modèle mixte est moins incertain, parce qu’il explique une partie de la dispersion des trajets par l’existence de deux regimes plutot que par un bruit unique et large.

Ce dernier point est le vrai apport de cette section, et il prepare la section 7 : un modèle qui structuré la variabilité (deux regimes) rend des prévisions plus tranchees qu’un modèle qui se contente de l’absorber dans un bruit unique. Reste a savoir si cette structuré est justifiee, ou si elle fait seulement payer des paramètres supplementaires. C’est exactement la question a laquelle la comparaison par preuve va repondre.

Résultats

Les résultats montrent que le modèle prédit une durée de trajet moyen plus élevée et un écart type plus important en tenant compte des événements extraordinaires, ce qui rend le modèle plus réaliste et adapté aux variations réelles des trajets.

Comparaison de Modèles

Nous disposons maintenant de deux modèles pour représenter le temps de trajet des cyclistes : l’un basé sur une seule gaussienne et l’autre sur un mélange de deux gaussiennes. Pourquoi ne pas envisager un modèle basé sur un mélange de trois gaussiennes, ou vingt ou trente gaussiennes ? Ou bien est-ce qu’une seule gaussienne suffit ? Comment choisir le meilleur modèle ?

En général, un modèle complexe avec plus de paramètres ajustables représente un ensemble de données particulier plus précisément qu’un modèle plus simple avec moins de paramètres. La question plus intéressante est de savoir si le modèle plus complexe offre une meilleure représentation qu’un modèle plus simple. Les modèles trop adaptés à un ensemble de données particulier ne seront pas utiles en pratique car ils ne s’ajusteront pas nécessairement bien aux nouvelles données.

Par exemple, lorsque vous ajustez un polynôme à un ensemble de points de données, vous pouvez toujours obtenir un ajustement exact en ajoutant suffisamment d’éléments au polynôme. Cependant, un modèle qui correspond exactement à chaque point de données fluctue généralement de manière erratique entre les points et ne correspondra donc pas bien aux nouvelles données. Ce phénomène est connu sous le nom de surapprentissage (overfitting).

La meilleure approche consiste à trouver un modèle qui ajuste raisonnablement bien les données sans être trop complexe. Comparer visuellement les modèles avec les données est subjectif et peu pratique pour des scénarios plus complexes. Ce qu’il vous faut est un critère objectif, comme le rasoir d’Occam, qui évalue quantitativement la qualité du modèle et détermine le compromis optimal entre précision et complexité. En inférence bayésienne, il existe un mécanisme robuste pour évaluer la qualité du modèle appelé preuve du modèle (evidence).

Calculer la preuve du modèle avec Infer.NET

Avec Infer.NET, la preuve est représentée par une variable aléatoire booléenne. La procédure de basé est illustrée dans l’exemple suivant.

Variable<bool> Evidence = Variable.Bernoulli(0.5);
using (Variable.If(Evidence))
{
    // Implémenter le modèle d'entraînement à évaluer
}
// Observer les données d'entraînement
// Interroger le moteur d'inférence pour les postérieurs du modèle
// Interroger le moteur d'inférence pour la distribution de la preuve
Console.WriteLine("Variable Evidence creee (pattern de calcul de preuve bayesienne)");
Variable Evidence creee (pattern de calcul de preuve bayesienne)

Infer.NET définit des modèles à deux branches en utilisant Variable.If et Variable.IfNot, qui sont l’équivalent de if-else en C#. La condition est une variable aléatoire booléenne. La probabilité que la condition soit vraie détermine la proportion de la branche If dans le mélange, et le reste du mélange est la branche IfNot.

Pour évaluer la preuve d’un modèle, utilisez If/IfNot comme illustré dans l’exemple, ce qui crée un mélange de deux modèles :

  • Le modèle que vous souhaitez évaluer, représenté par la branche If.
  • Un modèle “vide”, représenté par la branche IfNot manquante.

La variable Preuve est la condition qui contrôle les proportions dans le mélange. Son a priori initial est généralement défini sur Bernoulli(0.5). Pour déterminer la distribution réelle de la preuve, observez les données et calculez le postérieur de la variable Preuve.

Implémentation de

La nouvelle application utilisé la preuve pour évaluer si le modèle EntrainementCycliste ou EntrainementCyclisteMixte représente le mieux les données d’entraînement de EntrainementCyclisteMixte.

Classe CyclisteAvecPreuve

La classe CyclisteAvecPreuve évalue la preuve pour le modèle EntrainementCycliste.

public class CyclisteAvecPreuve : EntrainementCycliste
{
    protected Variable<bool> Preuve;

    public override void CreationModeleBayesien()
    {
        Preuve = Variable.Bernoulli(0.5);
        using (Variable.If(Preuve))
        {
            base.CreationModeleBayesien();
        }
    }

    public double CalculPreuve(double[] trainingData)
    {
        double logEvidence;
        DonneesCycliste posteriors = base.CalculePosterieurs(trainingData);
        logEvidence = MoteurInference.Infer<Bernoulli>(Preuve).LogOdds;
        return logEvidence;
    }
}
Console.WriteLine("Classe CyclisteAvecPreuve definie (evaluateur de preuve pour modele simple)");
Classe CyclisteAvecPreuve definie (evaluateur de preuve pour modele simple)

Classe CyclisteMixedAvecPreuve

La classe CyclisteMixedAvecPreuve évalue la preuve pour le modèle EntrainementCyclisteMixte.

public class CyclisteMixedAvecPreuve : EntrainementCyclisteMixte
{
    protected Variable<bool> Preuve;

    public override void CreationModeleBayesien()
    {
        Preuve = Variable.Bernoulli(0.5);
        using (Variable.If(Preuve))
        {
            base.CreationModeleBayesien();
        }
    }

    public double CalculPreuve(double[] trainingData)
    {
        double logEvidence;
        DonneesCyclisteMixte posteriors = base.CalculePosterieurs(trainingData);
        logEvidence = MoteurInference.Infer<Bernoulli>(Preuve).LogOdds;
        return logEvidence;
    }
}
Console.WriteLine("Classe CyclisteMixedAvecPreuve definie (evaluateur de preuve pour modele mixte)");
Classe CyclisteMixedAvecPreuve definie (evaluateur de preuve pour modele mixte)

Calcul de la preuve

On calcule la preuve pour les deux modèles et on affiche les résultats.

double[] trainingData = new double[] { 13, 17, 16, 12, 13, 12, 14, 18, 16, 16, 27, 32 };

DonneesCycliste initPriors = new DonneesCycliste(
    Gaussian.FromMeanAndPrecision(15.0, 0.01),
    Gamma.FromShapeAndScale(2.0, 0.5));

CyclisteAvecPreuve cyclistWithEvidence = new CyclisteAvecPreuve();
cyclistWithEvidence.CreationModeleBayesien();
cyclistWithEvidence.DefinirDistributions(initPriors);
double logEvidence = cyclistWithEvidence.CalculPreuve(trainingData);
// Preuves pour le modèle CyclistMixedWithEvidence (CyclingTime3)
DonneesCyclisteMixte initPriorsMixed = new DonneesCyclisteMixte
{
    DistribMoyenne = new Gaussian[] { new Gaussian(15.0, 100), new Gaussian(30.0, 100) },
    DistribBruitTraffic = new Gamma[] { new Gamma(2.0, 0.5), new Gamma(2.0, 0.5) },
    DistribMixe = new Dirichlet(1, 1)
};
CyclisteMixedAvecPreuve cyclistMixedWithEvidence = new CyclisteMixedAvecPreuve();
cyclistMixedWithEvidence.CreationModeleBayesien();
cyclistMixedWithEvidence.DefinirDistributions(initPriorsMixed);
double logEvidenceMixed = cyclistMixedWithEvidence.CalculPreuve(trainingData);
// Affichage des résultats
Console.WriteLine($"Preuve logarithmique pour une gaussienne simple : {logEvidence}");
Console.WriteLine($"Preuve logarithmique pour un mélange de deux gaussiennes : {logEvidenceMixed}");
Compiling model...done.
Iterating: 
.........|.........|.........|.........|.........| 50
Compiling model...done.
Iterating: 
.........|.........|.........|.........|.........| 50
Preuve logarithmique pour une gaussienne simple : -45,79730358385471
Preuve logarithmique pour un mélange de deux gaussiennes : -40,98166057283106

7a. Pourquoi LogOdds d’un Bernoulli(0.5) est le facteur de Bayes

Les deux nombres de cette cellule sont -45,79730358385471 et -40,98166057283106. Leur différence vaut 4,8156 nats, et exp(4,8156) = 123,4. Le notebook a déjà écrit ce facteur ; ce qui manque est pourquoi ce calcul est licite, et ce qui le rend valide ici.

Le mécanisme tient en deux lignes du code, et il faut les lire ensemble :

Preuve = Variable.Bernoulli(0.5);           // a priori sur l'interrupteur de modele
using (Variable.If(Preuve)) { base.CreationModeleBayesien(); }
// ...
logEvidence = MoteurInference.Infer<Bernoulli>(Preuve).LogOdds;

Preuve est une variable booléenne a priori equilibree : Bernoulli(0.5) donne des chances egales aux deux branches, donc une probabilité a priori de 1/2 pour chacune et des chances a priori de 1. Or LogOdds rend le logarithme du rapport des chances a posteriori. Comme les chances a priori valent 1, leur logarithme est nul, et le résultat imprime est donc directement le logarithme du rapport de vraisemblances marginales entre les deux branches — le facteur de Bayes, sans correction a appliquer.

Trois consequences, qui sont le contenu veritable de cette section :

  1. La comparaison n’a de sens que sur les mêmes données et le même a priori. Les deux classes heritent de la même structuré Variable.If / Bernoulli(0.5), et la section fixe trainingData une seule fois pour les deux appels. C’est ce qui rend la différence interpretable.
  2. Aucune penalite de complexité n’est ajoutee a la main — et c’est le point le plus important. Le mélange mobilise davantage de paramètres libres (deux moyennes, deux precisions, un poids) que la gaussienne simple, et il gagne pourtant de 4,82 nats. La raison est que la vraisemblance marginale integre ces paramètres au lieu de les optimiser : le volume de l’espace qu’ils occupent est déjà compte dans la preuve. Le facteur d’Occam est automatique ; il n’y a rien a corriger.
  3. L’échelle de lecture. 4,82 nats = 4,82 / ln(10) = 2,09 unites de log decimal, soit un facteur de 123. Sur les echelles usuelles de comparaison de modèles, un facteur de cet ordre est decisif — et il est obtenu sur les mêmes 12 trajets ou le mélange a du payer ses paramètres supplementaires.

Le résultat de domaine du notebook est donc celui-ci : les données contraignent la structuré, pas seulement les paramètres. Un modèle a un seul regime ne peut pas expliquer les 12 trajets aussi bien qu’un modèle a deux regimes, et la mesure le chiffre a 123 contre 1 plutot que de l’affirmer.

Interpretation : Comparaison par log evidence

Les résultats montrent que le mélange de deux gaussiennes est nettement préférable :

Modèle Log evidence Evidence relative
Gaussienne simple -45.80 1 (reference)
Melange 2 gaussiennes -40.98 exp(4.82) ≈ 124 fois mieux

La différence de log evidence de 4.82 nats correspond a un facteur de Bayes de ~124 en faveur du modèle mixte. Cela indique que les données contiennent bel et bien deux regimes distincts (trajets normaux ~14 min, événements extraordinaires ~25-32 min).

Le “rasoir d’Occam” bayesien penalise automatiquement la complexité superflue : le modèle mixte gagne ici car la complexité additionnelle (2 gaussiennes) est justifiee par les données.


Exercice : Variables aléatoires et Inférence probabiliste

Objectifs : 1. Créer un modèle simple avec des données d’entraînement 2. Comparer deux modèles (simple vs mixte) avec leurs preuves (logEvidence) 3. Interpréter les résultats

Contexte : Vous disposez de temps de trajet observés et devez déterminer si un modèle simple (une gaussienne) ou un modèle mixte (deux gaussiennes) représente mieux les données.

Questions de réflexion : 1. Quel modèle a la meilleure preuve logarithmique ? Pourquoi ? 2. Comment le modèle mixte capture-t-il les événements extraordinaires ? 3. Que se passerait-il avec un mélange de 3 gaussiennes ?


Navigation : << Retour au README

// Exercice : Comparaison de modèles probabilistes
Console.WriteLine("Exercice a completer");

// TODO: Définir les données d'entraînement
double[] mesDonnees = new double[] { /* Vos observations ici */ };

// TODO: Créer et configurer le modèle simple
// var modeleSimple = new ...
// modeleSimple.CreationModeleBayesien();
// ...

// TODO: Créer et configurer le modèle mixte
// var modeleMixte = new ...
// modeleMixte.CreationModeleBayesien();
// ...

// TODO: Calculer et comparer les preuves logarithmiques
// double preuveSimple = ...
// double preuveMixte = ...
// Console.WriteLine($"Preuve simple: {preuveSimple}");
// Console.WriteLine($"Preuve mixte: {preuveMixte}");

// TODO: Interpréter les résultats - quel modèle est préférable ?
Exercice a completer

Resume : Programmation Probabiliste avec Infer.NET

Ce notebook a couvert les grandes étapes de la programmation probabiliste avec Infer.NET :

Étape Concept API cle
1. Modèle simple Variables aléatoires + inférence Variable.GaussianFromMeanAndPrecision, InferenceEngine.Infer<>
2. Apprentissage Observation + postérieurs .ObservedValue, Gaussian, Gamma
3. Prediction Distribution predictive Variable.GaussianFromMeanAndPrecision(Moyenne, Bruit)
4. Programmation OO Encapsulation dans des classes CyclisteBase, EntrainementCycliste, PredictionCycliste
5. Apprentissage en ligne Mise a jour des a priori Posterieurs → nouveaux a priori
6. Modèle mixte Melange de gaussiennes Variable.Discrete(Mixe), Variable.Switch(...)
7. Sélection de modèle Preuve bayésienne Variable.Bernoulli(0.5), Variable.If(Evidence)

Points clés a retenir : - Les variables Variable<T> representent des distributions, pas des valeurs fixes - L’inférence postérieure conditionne sur les observations (ObservedValue) - La preuve logarithmique (log evidence) permet de comparer des modèles objectivement - Un log evidence plus eleve (moins negatif) indique un meilleur equilibre fit/complexité


Retour au sommet