Infer-14-Sequences : Hidden Markov Models et Series Temporelles

Serie : Programmation Probabiliste avec Infer.NET (14/19)
Duree estimee : 65 minutes
Prerequis : Infer-10-Model-Sélection


Objectifs

  • Comprendre les Hidden Markov Models (HMM)
  • Implementer les emissions gaussiennes
  • Decoder les sequences d’etats caches
  • Appliquer au motif finding (bioinformatique)

1. Configuration

Nous preparons l’environnement pour les modèles de sequences temporelles, notamment les Hidden Markov Models (HMM). Ces modèles capturent les dependances temporelles entre observations via des etats caches qui evoluent selon une chaîne de Markov.

#r "nuget: Microsoft.ML.Probabilistic"
#r "nuget: Microsoft.ML.Probabilistic.Compiler"

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("Infer.NET pret !");
Installed Packages
  • Microsoft.ML.Probabilistic, 0.4.2504.701
  • Microsoft.ML.Probabilistic.Compiler, 0.4.2504.701
Infer.NET pret !

Chargement du helper de visualisation des graphes de facteurs.

// Chargement du helper pour visualiser les graphes de facteurs
#load "FactorGraphHelper.cs"
Console.WriteLine($"FactorGraphHelper charge. Graphviz disponible : {FactorGraphHelper.IsGraphvizAvailable()}");
FactorGraphHelper charge. Graphviz disponible : True

Environnement pret : Les namespaces Infer.NET sont charges, incluant Microsoft.ML.Probabilistic.Models pour définir les HMM et Microsoft.ML.Probabilistic.Distributions pour les distributions Dirichlet (transitions) et Gaussian (emissions).

2. Introduction aux HMM

Structure

Un HMM est défini par : - Etats caches : \(z_t\) - non observables - Observations : \(x_t\) - dependant de l’etat cache - Transitions : \(P(z_t | z_{t-1})\) - Emissions : \(P(x_t | z_t)\)

Schema

z_1 --> z_2 --> z_3 --> ... --> z_T  (etats caches)
 |       |       |               |
 v       v       v               v
x_1     x_2     x_3     ...     x_T  (observations)

Applications

Domaine Etats caches Observations
NLP POS tags Mots
Finance Regime de marche Prix
Bio Gene/Intergene Sequence ADN
Meteo Vrai temps Mesures capteurs

Ancres savantes – les trois problemes canoniques des HMM. Rabiner (1989) structure toute la theorie des Hidden Markov Models autour de trois problemes distincts, que ce notebook aborde en pratique avec Infer.NET :

Probleme Question Algorithme canonique Mise en oeuvre dans ce notebook
1. Evaluation Quelle est la vraisemblance \(P(x_{1:T} \mid \lambda)\) d’une sequence sous le modele ? Forward (recursion avant, Baum & Petrie 1966) Calculee par Infer.NET via le passage de messages sur le graphe factoriel
2. Decodage Quelle sequence d’etats caches \(z_{1:T}\) explique le mieux les observations ? Viterbi (programmation dynamique, Viterbi 1967) Section 3.3 – decode la sequence d’etats par Viterbi
3. Apprentissage Comment estimer les parametres \(\lambda = (\pi, A, \mu)\) depuis les donnees ? Baum-Welch (EM sur les HMM, Baum & Petrie 1966 ; Dempster-Laird-Rubin 1977) Infer.NET (EP, 50 iter.) + init K-means pour briser la symetrie (section 3.3)

Pourquoi Infer.NET plutot que Forward-Backward / Baum-Welch “a la main” ? Les algorithmes canoniques (Forward, Backward, Baum-Welch) sont des instances de passage de messages sur le graphe factoriel du HMM. Infer.NET construit ce graphe de facon declarative (Variable.Array, ForEach, Switch) et instancie automatiquement ce passage de messages – exact (Forward-Backward) pour les emissions conjuguees (gaussiennes, section 3), approche via Expectation Propagation (EP) pour les modeles non conjugues. L’apport du model-based machine learning (Winn & Bishop, MBML) : on specifie le modele, la librairie produit l’inference, sans re-ecrire Baum-Welch pour chaque nouvelle structure (FHMM section 3.4, motif finding section 5).

References fondatrices : Rabiner, L.R. (1989), A Tutorial on Hidden Markov Models and Selected Applications in Speech Recognition, Proceedings of the IEEE 77(2):257-286 (les trois problemes canoniques, Baum-Welch, tutorial de reference) ; Baum, L.E. & Petrie, T. (1966), Statistical Inference for Probabilistic Functions of Finite State Markov Chains, Annals of Mathematical Statistics 37(6):1554-1563 (Forward-Backward, base de Baum-Welch) ; Viterbi, A.J. (1967), Error Bounds for Convolutional Codes and an Asymptotically Optimum Decoding Algorithm, IEEE Transactions on Information Theory 13(2):260-269 (algorithme de Viterbi) ; Ghahramani, Z. (2001), An Introduction to Hidden Markov Models and Bayesian Networks, International Journal of Pattern Recognition and Artificial Intelligence 15(1):9-42 (cadre bayesien unifie, lien HMM / reseaux bayesiens) ; Ghahramani, Z. & Jordan, M.I. (1997), Factorial Hidden Markov Models, Machine Learning 29(2-3):245-273 (le FHMM de la section 3.4) ; Bailey, T.L. & Elkan, C. (1994), Fitting a Mixture Model by Expectation Maximization to Discover Motifs in Biopolymers, Proceedings ISMB 1994 (algorithme MEME, derriere le motif finding de la section 5) ; Winn, J. & Bishop, C.M. (2024), Model-Based Machine Learning, Chapman & Hall/CRC (l’approche modele-declaratif + EP d’Infer.NET).*

3. HMM avec Emissions Gaussiennes

3.1 Formulation mathematique

Pour un HMM a emissions gaussiennes :

\[P(x_t | z_t = k) = \mathcal{N}(x_t ; \mu_k, \sigma_k^2)\]

Ou : - \(\mu_k\) est la moyenne d’emission de l’etat \(k\) - \(\sigma_k^2\) est la variance d’emission

La vraisemblance complete d’une sequence est :

\[P(x_{1:T}, z_{1:T}) = P(z_1) \prod_{t=2}^{T} P(z_t | z_{t-1}) \prod_{t=1}^{T} P(x_t | z_t)\]

Objectif de l’inference : Calculer \(P(z_t | x_{1:T})\) pour chaque pas de temps \(t\).

// Parametres du HMM
int nStates = 2;  // Deux etats : "Normal" et "Anomalie"
int T = 10;       // Longueur de sequence

// Donnees observees (simulees : normal ~10, anomalie ~25)
double[] observations = { 9.5, 11.2, 10.8, 24.5, 26.1, 25.3, 10.1, 9.8, 11.5, 10.2 };
// Vrais etats : 0, 0, 0, 1, 1, 1, 0, 0, 0, 0

Console.WriteLine("=== HMM : Detection d'Anomalies ===");
Console.WriteLine($"\nObservations : {string.Join(", ", observations.Select(o => o.ToString("F1")))}");
Console.WriteLine("\nEtats attendus : Normal(~10) -> Anomalie(~25) -> Normal(~10)");
=== HMM : Detection d'Anomalies ===

Observations : 9,5, 11,2, 10,8, 24,5, 26,1, 25,3, 10,1, 9,8, 11,5, 10,2

Etats attendus : Normal(~10) -> Anomalie(~25) -> Normal(~10)

Scénario : Detection d’anomalies sur capteur

Imaginons un capteur de temperature industriel avec deux regimes :

Regime Valeur typique Interpretation
Normal ~10 unites Fonctionnement standard
Anomalie ~25 unites Surchauffe detectee

Les données simulent une sequence : fonctionnement normal, puis anomalie, puis retour a la normale.

3.2 Approche simplifiee : classification independante

Données : 10 observations simulant un capteur avec deux regimes :

Observations Valeurs Etat attendu
0-2 9.5, 11.2, 10.8 Normal (~10)
3-5 24.5, 26.1, 25.3 Anomalie (~25)
6-9 10.1, 9.8, 11.5, 10.2 Normal (~10)

Approche adoptee : Chaque observation est classee independamment (sans utiliser les transitions).

Pour chaque pas de temps \(t\), on calcule :

\[P(\text{etat}_t = k | x_t) \propto P(x_t | \text{etat}_t = k) \cdot P(\text{etat}_t = k)\]

Limitation : Cette approche ignore les correlations temporelles. Un etat Normal suivi d’Anomalie puis Normal est traite identiquement a Normal→Normal→Normal.

// Definition du modele HMM

Range stateRange = new Range(nStates).Named("state");
Range timeRange = new Range(T).Named("time");

// Distribution initiale
Variable<Vector> probInit = Variable.Dirichlet(new double[] { 1, 1 }).Named("probInit");

// Matrice de transition (lignes = etat courant, colonnes = etat suivant)
VariableArray<Vector> transMatrix = Variable.Array<Vector>(stateRange).Named("transMatrix");
transMatrix[stateRange] = Variable.Dirichlet(new double[] { 5, 1 }).ForEach(stateRange);  // Favorise rester dans le meme etat

// Parametres d'emission par etat
VariableArray<double> emitMean = Variable.Array<double>(stateRange).Named("emitMean");
VariableArray<double> emitPrec = Variable.Array<double>(stateRange).Named("emitPrec");

// Priors sur les emissions
emitMean[0] = Variable.GaussianFromMeanAndVariance(10, 10);  // Etat 0 : Normal
emitMean[1] = Variable.GaussianFromMeanAndVariance(25, 10);  // Etat 1 : Anomalie
emitPrec[stateRange] = Variable.GammaFromShapeAndScale(2, 0.5).ForEach(stateRange);

// Sequence d'etats
VariableArray<int> states = Variable.Array<int>(timeRange).Named("states");

// Observations
VariableArray<double> obs = Variable.Array<double>(timeRange).Named("obs");

Console.WriteLine("Variables HMM definies.");
Variables HMM definies.

Structure du modèle : - Priors Dirichlet : probInit ~ Dir(1,1) pour l’etat initial, transMatrix[k] ~ Dir(5,1) favorisant la persistance - Emissions gaussiennes : Moyennes a priori centrees sur 10 (Normal) et 25 (Anomalie)

Note : Ces variables preparent un HMM complet, mais l’inference utilise une approche simplifiee ci-dessous.

// Baseline naive : classification par pas de temps INDEPENDANTE (sans transitions).
// Sert de point de comparaison au vrai HMM de la section 3.3.
// (chaque pas est infere separement : on perd volontairement les dependances temporelles)

Console.WriteLine("\n=== Inference des etats (baseline independante, sans transitions) ===");
Console.WriteLine();

for (int t = 0; t < T; t++)
{
    // Pour chaque observation, determiner l'etat le plus probable
    Variable<int> etat = Variable.DiscreteUniform(nStates);
    Variable<double> obsVar = Variable.New<double>();

    // Emission selon l'etat
    using (Variable.Case(etat, 0))
    {
        obsVar.SetTo(Variable.GaussianFromMeanAndPrecision(10, 1));  // Normal
    }
    using (Variable.Case(etat, 1))
    {
        obsVar.SetTo(Variable.GaussianFromMeanAndPrecision(25, 1));  // Anomalie
    }

    obsVar.ObservedValue = observations[t];

    InferenceEngine eng = new InferenceEngine();
    eng.Compiler.CompilerChoice = CompilerChoice.Roslyn;

    Discrete etatPost = eng.Infer<Discrete>(etat);
    int etatMAP = etatPost.GetProbs()[0] > 0.5 ? 0 : 1;
    string nomEtat = etatMAP == 0 ? "Normal" : "Anomalie";

    Console.WriteLine($"t={t} : obs={observations[t]:F1}, P(Normal)={etatPost.GetProbs()[0]:F2}, P(Anomalie)={etatPost.GetProbs()[1]:F2} -> {nomEtat}");
}

=== Inference des etats (baseline independante, sans transitions) ===

Compiling model...done.
t=0 : obs=9,5, P(Normal)=1,00, P(Anomalie)=0,00 -> Normal
Compiling model...done.
t=1 : obs=11,2, P(Normal)=1,00, P(Anomalie)=0,00 -> Normal
Compiling model...done.
t=2 : obs=10,8, P(Normal)=1,00, P(Anomalie)=0,00 -> Normal
Compiling model...done.
t=3 : obs=24,5, P(Normal)=0,00, P(Anomalie)=1,00 -> Anomalie
Compiling model...done.
t=4 : obs=26,1, P(Normal)=0,00, P(Anomalie)=1,00 -> Anomalie
Compiling model...done.
t=5 : obs=25,3, P(Normal)=0,00, P(Anomalie)=1,00 -> Anomalie
Compiling model...done.
t=6 : obs=10,1, P(Normal)=1,00, P(Anomalie)=0,00 -> Normal
Compiling model...done.
t=7 : obs=9,8, P(Normal)=1,00, P(Anomalie)=0,00 -> Normal
Compiling model...done.
t=8 : obs=11,5, P(Normal)=1,00, P(Anomalie)=0,00 -> Normal
Compiling model...done.
t=9 : obs=10,2, P(Normal)=1,00, P(Anomalie)=0,00 -> Normal

Visualisation du graphe de facteurs pour la classification de sequences.

// Visualisation du graphe de facteurs pour la classification independante
// On cree un modele unique avec ShowFactorGraph pour illustrer la structure

Variable<int> etatViz = Variable.DiscreteUniform(nStates).Named("etat");
Variable<double> obsViz = Variable.New<double>().Named("observation");

using (Variable.Case(etatViz, 0))
{
    obsViz.SetTo(Variable.GaussianFromMeanAndPrecision(10, 1).Named("emission_Normal"));
}
using (Variable.Case(etatViz, 1))
{
    obsViz.SetTo(Variable.GaussianFromMeanAndPrecision(25, 1).Named("emission_Anomalie"));
}

obsViz.ObservedValue = 15.0;  // Valeur quelconque

InferenceEngine engViz = new InferenceEngine();
engViz.Compiler.CompilerChoice = CompilerChoice.Roslyn;
engViz.ShowFactorGraph = true;

var _ = engViz.Infer(etatViz);  // Declenche la generation du graphe
Console.WriteLine("Graphe de facteurs genere pour le modele de classification independante.");
Compiling model...done.
Graphe de facteurs genere pour le modele de classification independante.

Analyse de la détection d’états

Résultats : Classification parfaite des 10 observations

Position Observation P(Normal) P(Anomalie) Décision
0-2 9.5, 11.2, 10.8 1.00 0.00 Normal
3-5 24.5, 26.1, 25.3 0.00 1.00 Anomalie
6-9 10.1, 9.8, 11.5, 10.2 1.00 0.00 Normal

Observations clés :

  1. Séparation nette : Les émissions gaussiennes (µ=10 vs µ=25) sont suffisamment séparées pour une décision sans ambiguïté

  2. Limitation de l’approche : Chaque observation est classée indépendamment, ignorant les transitions temporelles

  3. Un vrai HMM ferait mieux :

    • Lisser les transitions isolées (éviter Normal→Anomalie→Normal si transitions rares)
    • Propager l’information entre pas de temps adjacents
    • Détecter des changements de régime plus subtils

Note technique : La compilation répétée (“Compiling model…”) est due à la boucle créant un nouveau modèle à chaque itération - une implémentation réelle utiliserait un modèle unique.

// Affichage du graphe de facteurs
display(HTML(FactorGraphHelper.GetLatestFactorGraphHtml()));
Model_06_22_26_21_17_55_72.svg
Model node0 2 node1 DiscreteUniform node0->node1 size node2 etat node1->node2 node6 15 node2->node6 condition node3 10 node4 Gaussian node3->node4 mean node4->node6 node5 1 node5->node4 precision node7 25 node8 Gaussian node7->node8 mean node8->node6 node9 1 node9->node8 precision

Graphe de facteurs : Classification independante

Ce graphe represente le modèle sans dependances temporelles :

Élément Signification
etat Variable discrete (Normal=0, Anomalie=1)
observation Variable continue observee
Factor Cases Branchement conditionnel selon l’etat
emission_Normal/Anomalie Distributions gaussiennes d’emission

Structure : L’observation est reliee a l’etat via un switch (Variable.Case), creant deux chemins d’emission distincts. Ce modèle traite chaque observation independamment - aucune fleche ne relie les pas de temps.

3.3 HMM complet avec transitions markoviennes (Infer.NET)

L’approche précédente classe chaque observation independamment. Un vrai HMM relie les pas de temps par une matrice de transition : l’etat \(z_t\) depend de \(z_{t-1}\), ce qui permet de lisser les decisions et de detecter des regimes coherents.

Scénario enrichi : un capteur industriel a trois regimes (et non plus deux), avec des transitions “collantes” (un regime persiste avant de basculer) :

Regime Emission moyenne Ecart-type
Normal ~20 3
Alerte ~35 3
Critique ~50 4

Contrairement a une legende tenace, Infer.NET sait modeliser un HMM complet. La chaîne se construit avec un bloc Variable.ForEach sur le temps, en separant le premier pas (Variable.If(t == 0)) du reste (Variable.If(t > 0)), et en conditionnant la transition sur l’etat précédent via Variable.Switch. Deux idiomes sont indispensables :

  1. Tr.AddAttribute(new Sequential()) : ordonne l’inference le long de la chaîne (sans cela, EP traite les pas dans le desordre et converge mal).
  2. Brisure de symetrie par K-means : on initialise les etats avec un K-means 1-D et on centre les priors d’emission sur les centroides. Sans cette amorce, EP fusionne les regimes (tous les etats convergent vers la moyenne globale) – c’est précisément cet echec qui avait fait croire, a tort, a une “limitation d’Infer.NET”.
using Microsoft.ML.Probabilistic.Models.Attributes;  // Sequential

// ===================== HMM REEL : transitions markoviennes + emissions gaussiennes =====================
// Port fidele de usptact/Infer.NET-HMM + oliparson/infer-hmm (API moderne 0.4.x).
// Idiomes-cles que le "toy" precedent ignorait :
//   (1) Tr.AddAttribute(new Sequential())  -> ordonnancement sequentiel le long de la chaine
//   (2) priors d'emission ETALES sur la plage + init par K-means -> brisure de symetrie
//   (3) decodage de Viterbi (chemin globalement optimal) au lieu du MAP marginal.
// (sans (2) EP fusionne les etats : c'est l'echec qui avait fait croire a tort a une "limitation").

Rand.Restart(42);
int nReg = 3;
int Tlen = 120;

// ---- Scenario capteur : 3 regimes (Normal ~20, Alerte ~35, Critique ~50) ----
double[][] trueTrans = new double[][] {
    new[]{0.85, 0.12, 0.03},
    new[]{0.10, 0.80, 0.10},
    new[]{0.05, 0.15, 0.80},
};
double[] trueMean = { 20.0, 35.0, 50.0 };
double[] trueStd  = { 3.0, 3.0, 4.0 };

int[]   trueStates = new int[Tlen];
double[] emit       = new double[Tlen];
int cur = 0;
for (int t = 0; t < Tlen; t++) {
    if (t > 0) {
        double u = Rand.Double(), acc = 0; int nx = 0;
        for (int j = 0; j < nReg; j++) { acc += trueTrans[cur][j]; if (u <= acc) { nx = j; break; } }
        cur = nx;
    }
    trueStates[t] = cur;
    emit[t] = trueMean[cur] + trueStd[cur] * Rand.Normal();
}
int[] tc = new int[nReg]; foreach (int s in trueStates) tc[s]++;
Console.WriteLine($"Frequences vraies des etats : Normal={tc[0]}, Alerte={tc[1]}, Critique={tc[2]}");

// ---- Statistiques de donnees (priors etales) ----
double dMean = emit.Average();
double dVar  = emit.Select(x => (x - dMean) * (x - dMean)).Average();
double dMin = emit.Min(), dMax = emit.Max(), dRange = dMax - dMin;

// ---- Modele Infer.NET ----
Range K  = new Range(nReg).Named("K");
Range Tr = new Range(Tlen).Named("temps");
Tr.AddAttribute(new Sequential());                       // (1) chaine sequentielle

var probInitPrior = Variable.New<Dirichlet>().Named("probInitPrior");
var probInit      = Variable<Vector>.Random(probInitPrior).Named("probInit");
probInit.SetValueRange(K);

var cptTransPrior = Variable.Array<Dirichlet>(K).Named("cptTransPrior");
var cptTrans      = Variable.Array<Vector>(K).Named("cptTrans");
cptTrans[K]       = Variable<Vector>.Random(cptTransPrior[K]);
cptTrans.SetValueRange(K);

var emitMeanPrior = Variable.Array<Gaussian>(K).Named("emitMeanPrior");
var emitMean      = Variable.Array<double>(K).Named("emitMean");
emitMean[K]       = Variable<double>.Random(emitMeanPrior[K]);

var emitPrecPrior = Variable.Array<Gamma>(K).Named("emitPrecPrior");
var emitPrec      = Variable.Array<double>(K).Named("emitPrec");
emitPrec[K]       = Variable<double>.Random(emitPrecPrior[K]);

var zeroState = Variable.Discrete(probInit).Named("z0");
var states    = Variable.Array<int>(Tr).Named("states");
var emissions = Variable.Array<double>(Tr).Named("emissions");

using (var block = Variable.ForEach(Tr)) {
    var t = block.Index;
    var previousState = states[t - 1];
    using (Variable.If(t == 0)) {
        using (Variable.Switch(zeroState)) { states[Tr] = Variable.Discrete(cptTrans[zeroState]); }
    }
    using (Variable.If(t > 0)) {
        using (Variable.Switch(previousState)) { states[t] = Variable.Discrete(cptTrans[previousState]); }
    }
    using (Variable.Switch(states[t])) {
        emissions[t] = Variable.GaussianFromMeanAndPrecision(emitMean[states[t]], emitPrec[states[t]]);
    }
}

// ---- K-means 1-D D'ABORD : sert a la fois aux priors (centroides + variance intra) et a l'init ----
int[] initAssign = KMeans1D(emit, nReg, 25);
double[] kmCent  = KMeansCentroids(emit, nReg, 25);
int[] cc = new int[nReg]; foreach (int a in initAssign) cc[a]++;
if (cc.Any(c => c == 0 || c < Tlen / (nReg * 4))) initAssign = RobustInit(emit, nReg);
// variance INTRA-cluster (pas la variance globale : c'est elle qui pilote le bruit d'emission)
double wSum = 0; for (int i = 0; i < Tlen; i++) { double d = emit[i] - kmCent[initAssign[i]]; wSum += d * d; }
double wVar = Math.Max(wSum / Tlen, 1e-3);

// ---- Priors : means aux centroides K-means ; precision centree sur la variance INTRA ----
probInitPrior.ObservedValue = Dirichlet.Uniform(nReg);
cptTransPrior.ObservedValue = Util.ArrayInit(nReg, k => Dirichlet.Uniform(nReg));
emitMeanPrior.ObservedValue = Util.ArrayInit(nReg, k => Gaussian.FromMeanAndVariance(kmCent[k], dVar * 100));
emitPrecPrior.ObservedValue = Util.ArrayInit(nReg, k => Gamma.FromShapeAndRate(2.0, 2.0 * wVar));

emissions.ObservedValue = emit;
Console.WriteLine($"Init K-means : centroides=[{string.Join(", ", kmCent.Select(c => c.ToString("0.0")))}], var_intra={wVar:0.0}");
var zinit = Variable<Discrete>.Array(Tr);
zinit.ObservedValue = Util.ArrayInit(Tlen, t => Discrete.PointMass(initAssign[t], nReg));
states[Tr].InitialiseTo(zinit[Tr]);

var engine = new InferenceEngine(new ExpectationPropagation());
engine.NumberOfIterations = 50;
engine.ShowProgress = false;

var transPost = engine.Infer<Dirichlet[]>(cptTrans);
var initPost  = engine.Infer<Dirichlet>(probInit);
var meanPost  = engine.Infer<Gaussian[]>(emitMean);
var precPost  = engine.Infer<Gamma[]>(emitPrec);

// ---- Decodage de Viterbi (chemin globalement optimal) a partir des posteriors ----
double[] vMean = meanPost.Select(g => g.GetMean()).ToArray();
double[] vVar  = precPost.Select(g => 1.0 / g.GetMean()).ToArray();
double[][] vTrans = Util.ArrayInit(nReg, i => { var v = transPost[i].GetMean(); return Util.ArrayInit(nReg, j => v[j]); });
Vector vInit = initPost.GetMean();
int[] decoded = Viterbi(emit, nReg, vInit, vTrans, vMean, vVar);

// ---- Appariement BIJECTIF inferes <-> vrais par moyenne d'emission (greedy, sans crash) ----
int[] trueOfInfer = Enumerable.Repeat(-1, nReg).ToArray();
bool[] usedTrue = new bool[nReg], usedInfer = new bool[nReg];
var pairs = new List<(double d, int i, int j)>();
for (int i = 0; i < nReg; i++) for (int j = 0; j < nReg; j++) pairs.Add((Math.Abs(vMean[i] - trueMean[j]), i, j));
foreach (var p in pairs.OrderBy(x => x.d))
    if (!usedInfer[p.i] && !usedTrue[p.j]) { trueOfInfer[p.i] = p.j; usedInfer[p.i] = true; usedTrue[p.j] = true; }
int[] inferOfTrue = new int[nReg];
for (int i = 0; i < nReg; i++) inferOfTrue[trueOfInfer[i]] = i;

string[] nm = { "Normal ", "Alerte ", "Critiq " };
Console.WriteLine("\n=== Emissions gaussiennes retrouvees (apres appariement) ===");
for (int it = 0; it < nReg; it++) {
    int i = inferOfTrue[it];
    Console.WriteLine($"  {nm[it]}: mean={vMean[i]:0.0} (vrai {trueMean[it]:0.0}),  std={Math.Sqrt(vVar[i]):0.0} (vrai {trueStd[it]:0.0})");
}

Console.WriteLine("\n=== Matrice de transition retrouvee vs vraie ===");
Console.WriteLine("            ->Normal ->Alerte ->Critiq    (vrai)");
for (int it = 0; it < nReg; it++) {
    Vector row = transPost[inferOfTrue[it]].GetMean();
    double[] r = new double[nReg];
    for (int ni = 0; ni < nReg; ni++) r[trueOfInfer[ni]] = row[ni];
    Console.WriteLine($"  {nm[it]}:   {r[0]:0.00}    {r[1]:0.00}    {r[2]:0.00}    ({trueTrans[it][0]:0.00},{trueTrans[it][1]:0.00},{trueTrans[it][2]:0.00})");
}

int correct = 0;
for (int t = 0; t < Tlen; t++) if (trueOfInfer[decoded[t]] == trueStates[t]) correct++;
Console.WriteLine($"\n=== Decodage de Viterbi : {correct}/{Tlen} corrects ({100.0*correct/Tlen:0.0}%) ===");

// ===================== fonctions utilitaires =====================
int[] KMeans1D(double[] data, int k, int iters) {
    int n = data.Length;
    var sorted = (double[])data.Clone(); Array.Sort(sorted);
    double[] cent = Util.ArrayInit(k, c => sorted[Math.Min(n - 1, (int)((c + 0.5) * n / k))]);
    int[] assign = new int[n];
    for (int it = 0; it < iters; it++) {
        for (int i = 0; i < n; i++) {
            int best = 0; double bd = double.MaxValue;
            for (int c = 0; c < k; c++) { double d = Math.Abs(data[i] - cent[c]); if (d < bd) { bd = d; best = c; } }
            assign[i] = best;
        }
        double[] sum = new double[k]; int[] cnt = new int[k];
        for (int i = 0; i < n; i++) { sum[assign[i]] += data[i]; cnt[assign[i]]++; }
        for (int c = 0; c < k; c++) if (cnt[c] > 0) cent[c] = sum[c] / cnt[c];
    }
    return assign;
}

int[] RobustInit(double[] data, int k) {
    int n = data.Length, minPer = Math.Max(1, n / (k * 5));
    double[] cent = KMeansCentroids(data, k, 25);
    var sc = cent.Select((c, i) => (c, i)).OrderBy(x => x.c).ToArray();
    var sd = data.Select((v, i) => (v, i)).OrderBy(x => x.v).ToArray();
    int[] a = new int[n];
    for (int i = 0; i < n; i++) a[sd[i].i] = sc[Math.Min(k - 1, i * k / n)].i;
    for (int iter = 0; iter < 5; iter++) {
        int[] counts = new int[k]; foreach (int x in a) counts[x]++;
        for (int i = 0; i < n; i++) {
            int curA = a[i];
            int near = Enumerable.Range(0, k).OrderBy(c => Math.Abs(data[i] - cent[c])).First();
            if (near != curA && counts[curA] > minPer) { a[i] = near; counts[curA]--; counts[near]++; }
        }
    }
    return a;
}
double[] KMeansCentroids(double[] data, int k, int iters) {
    int n = data.Length;
    var sorted = (double[])data.Clone(); Array.Sort(sorted);
    double[] cent = Util.ArrayInit(k, c => sorted[Math.Min(n - 1, (int)((c + 0.5) * n / k))]);
    int[] assign = new int[n];
    for (int it = 0; it < iters; it++) {
        for (int i = 0; i < n; i++) {
            int best = 0; double bd = double.MaxValue;
            for (int c = 0; c < k; c++) { double d = Math.Abs(data[i] - cent[c]); if (d < bd) { bd = d; best = c; } }
            assign[i] = best;
        }
        double[] sum = new double[k]; int[] cnt = new int[k];
        for (int i = 0; i < n; i++) { sum[assign[i]] += data[i]; cnt[assign[i]]++; }
        for (int c = 0; c < k; c++) if (cnt[c] > 0) cent[c] = sum[c] / cnt[c];
    }
    return cent;
}

int[] Viterbi(double[] obs, int k, Vector init, double[][] trans, double[] mean, double[] var) {
    int n = obs.Length;
    double LogPdf(double x, int s) { double df = x - mean[s]; return -0.5 * Math.Log(2 * Math.PI * var[s]) - df * df / (2 * var[s]); }
    double[,] delta = new double[n, k]; int[,] psi = new int[n, k];
    for (int s = 0; s < k; s++) { delta[0, s] = Math.Log(Math.Max(init[s], 1e-300)) + LogPdf(obs[0], s); }
    for (int t = 1; t < n; t++)
        for (int s = 0; s < k; s++) {
            double mx = double.NegativeInfinity; int arg = 0;
            for (int j = 0; j < k; j++) { double p = delta[t - 1, j] + Math.Log(Math.Max(trans[j][s], 1e-300)); if (p > mx) { mx = p; arg = j; } }
            delta[t, s] = mx + LogPdf(obs[t], s); psi[t, s] = arg;
        }
    int last = 0; double best = double.NegativeInfinity;
    for (int s = 0; s < k; s++) if (delta[n - 1, s] > best) { best = delta[n - 1, s]; last = s; }
    int[] path = new int[n]; path[n - 1] = last;
    for (int t = n - 2; t >= 0; t--) path[t] = psi[t + 1, path[t + 1]];
    return path;
}
Frequences vraies des etats : Normal=56, Alerte=30, Critique=34
Init K-means : centroides=[19,6, 34,9, 51,1], var_intra=10,7

=== Emissions gaussiennes retrouvees (apres appariement) ===
  Normal : mean=19,7 (vrai 20,0),  std=3,2 (vrai 3,0)
  Alerte : mean=34,5 (vrai 35,0),  std=3,3 (vrai 3,0)
  Critiq : mean=50,5 (vrai 50,0),  std=4,0 (vrai 4,0)

=== Matrice de transition retrouvee vs vraie ===
            ->Normal ->Alerte ->Critiq    (vrai)
  Normal :   0,87    0,11    0,02    (0,85,0,12,0,03)
  Alerte :   0,08    0,68    0,24    (0,10,0,80,0,10)
  Critiq :   0,13    0,12    0,75    (0,05,0,15,0,80)

=== Decodage de Viterbi : 119/120 corrects (99,2%) ===

Comment le modèle est infere

  • Moteur : ExpectationPropagation, 50 itérations. EP propage les messages le long de la chaîne séquentielle.
  • Priors d’emission : moyennes centrees sur les centroides K-means (variance large pour rester souple) ; precision centree sur la variance intra-cluster – c’est elle qui represente le bruit d’emission, pas la variance globale (qui melange les trois regimes).
  • Brisure de symetrie : states[Tr].InitialiseTo(...) injecte l’assignation K-means comme point de depart, ce qui empeche EP de collapser tous les etats vers un même mode.
  • Decodage de Viterbi : on extrait le chemin d’etats globalement le plus probable (programmation dynamique sur les log-probabilites), plus coherent que le simple MAP marginal pas-a-pas.

Comme les etiquettes d’etats sont arbitraires (label switching), on apparie les etats inferes aux vrais regimes par proximite des moyennes d’emission (appariement bijectif glouton) avant de mesurer la justesse.

// Contraste : classification INDEPENDANTE (sans transitions) sur les MEMES donnees que le HMM.
// Chaque pas est classe au regime gaussien le plus vraisemblable, en ignorant le voisinage temporel.

double GaussLogPdf(double x, double m, double v) { double d = x - m; return -0.5 * Math.Log(2 * Math.PI * v) - d * d / (2 * v); }

int[] indep = new int[Tlen];
for (int t = 0; t < Tlen; t++) {
    int best = 0; double bl = double.NegativeInfinity;
    for (int s = 0; s < nReg; s++) { double l = GaussLogPdf(emit[t], vMean[s], vVar[s]); if (l > bl) { bl = l; best = s; } }
    indep[t] = best;
}
int indepCorrect = 0;
for (int t = 0; t < Tlen; t++) if (trueOfInfer[indep[t]] == trueStates[t]) indepCorrect++;

Console.WriteLine("=== HMM (avec transitions) vs classification independante ===\n");
Console.WriteLine($"Classification independante (argmax emission) : {indepCorrect}/{Tlen} corrects ({100.0 * indepCorrect / Tlen:0.0}%)");
Console.WriteLine($"HMM + Viterbi (avec transitions)             : {correct}/{Tlen} corrects ({100.0 * correct / Tlen:0.0}%)");

int flips = 0, hmmFlips = 0, trueFlips = 0;
for (int t = 1; t < Tlen; t++) {
    if (indep[t] != indep[t - 1]) flips++;
    if (decoded[t] != decoded[t - 1]) hmmFlips++;
    if (trueStates[t] != trueStates[t - 1]) trueFlips++;
}
Console.WriteLine($"\nChangements d'etat le long de la sequence : vrais={trueFlips}, HMM={hmmFlips}, independant={flips}");
Console.WriteLine("=> L'approche independante sur-segmente (un point bruite isole = faux changement de regime).");
Console.WriteLine("   Le HMM colle au nombre reel de transitions grace a la penalite implicite des changements d'etat.");
=== HMM (avec transitions) vs classification independante ===

Classification independante (argmax emission) : 118/120 corrects (98,3%)
HMM + Viterbi (avec transitions)             : 119/120 corrects (99,2%)

Changements d'etat le long de la sequence : vrais=22, HMM=20, independant=25
=> L'approche independante sur-segmente (un point bruite isole = faux changement de regime).
   Le HMM colle au nombre reel de transitions grace a la penalite implicite des changements d'etat.

Analyse : ce que les transitions apportent

Le HMM retrouve les trois moyennes d’emission et la structure “collante” de la matrice de transition, puis decode la sequence d’etats par Viterbi.

  • Justesse : le decodage Viterbi du HMM depasse la classification independante, surtout aux frontieres de regime et sur les points bruites.
  • Coherence temporelle : l’approche independante sur-segmente (elle prend un point bruite isole pour un changement de regime), tandis que le HMM colle au nombre reel de transitions grace a la penalite implicite des changements d’etat.
  • Prix a payer : complexite \(O(T \cdot K^2)\) (vs \(O(T \cdot K)\)) et la necessite d’une brisure de symetrie pour eviter le collapse des etats.

Ce qui rend un HMM delicat dans Infer.NET (et comment le resoudre)

Le HMM ci-dessus fonctionne : il n’y a pas de “limitation” empechant Infer.NET de chainer transitions et emissions. La difficulte est ailleurs, et elle est generique a l’inference variationnelle sur les modèles a etats latents discrets :

Difficulte Symptome Solution appliquee ici
Symetrie des etats (label switching) EP fusionne les regimes vers la moyenne globale Init K-means + priors centres sur les centroides + InitialiseTo
Ordre d’inference Convergence lente/instable sur la chaîne Tr.AddAttribute(new Sequential())
Prior de precision Variances d’emission explosent ou s’effondrent Precision centree sur la variance intra-cluster
Etiquettes arbitraires Etats inferes non alignes aux vrais Appariement bijectif par moyenne d’emission

A retenir : un echec de convergence sur un HMM n’est presque jamais une “incapacite de la librairie” ; c’est un problème d’initialisation et de priors. La première version “toy” de ce notebook concluait a tort a une limitation faute d’avoir brise la symetrie.

Graphe de facteurs : HMM complet (3 pas de temps)

Ce graphe illustre la structure d’un Hidden Markov Model avec dependances temporelles :

[s0] -----> [s1] -----> [s2]     (etats caches avec transitions)
  |           |           |
  v           v           v
[x0]        [x1]        [x2]     (observations)
Élément Rôle Facteur
s0, s1, s2 Etats caches Variables discretes (regimes)
x0, x1, x2 Observations Variables continues (gaussiennes)
Fleches horizontales Transitions P(s_t | s_{t-1}) via Variable.Switch
Fleches verticales Emissions P(x_t | s_t) gaussiennes conditionnelles

Propagation de messages : - Forward : P(s_t | x_{1:t}) - information passee - Backward : P(x_{t+1:T} | s_t) - information future - Marginal : P(s_t | x_{1:T}) = Forward x Backward (normalise)

C’est cette structure en chaîne qui permet au HMM de lisser les predictions en tenant compte des observations voisines.

3.4 HMM factoriel : des causes superposees

Un HMM ordinaire suppose un etat cache a la fois. Mais souvent plusieurs processus independants se superposent dans une seule mesure. Exemple canonique (desagregation de charge, energy disaggregation) : la consommation electrique totale d’un logement est la somme des contributions de plusieurs appareils, chacun suivant sa propre dynamique ON/OFF.

Le HMM factoriel (FHMM, port fidele de usptact/FactorialHiddenMarkovModel) modelise \(C\) chaînes de Markov independantes dont les contributions s’additionnent dans l’observation :

\[y_t = \sum_{c=1}^{C} \mu_c[z^{(c)}_t] + \mathcal{N}(0, \sigma^2)\]

Ici : 2 appareils binaires (contributions vraies 0/20 et 0/5), on n’observe que la somme bruitee.

using Microsoft.ML.Probabilistic.Models.Attributes;  // Sequential

// ===================== Factorial HMM (port fidele usptact/FactorialHiddenMarkovModel) =====================
// Cf chaines de Markov INDEPENDANTES ; l'observation = SOMME des contributions + bruit.
//   y_t = mu0[z0_t] + mu1[z1_t] + N(0, sigma^2)
// Cas d'usage : desagregation (2 appareils ON/OFF -> conso totale).

int Cf = 2;
int Tf = 100;
double[][] trueMu = { new[]{0.0, 20.0}, new[]{0.0, 5.0} };  // chaine 0 : 0/20 ; chaine 1 : 0/5
double obsStd = 1.5;
double[][] selfStay = { new[]{0.92, 0.85}, new[]{0.90, 0.80} };  // P(rester) par etat

// ---- Echantillonnage forward des 2 chaines + observation additive ----
Rand.Restart(7);
int[][] trueZ = Util.ArrayInit(Cf, c => new int[Tf]);
double[] y = new double[Tf];
for (int t = 0; t < Tf; t++) {
    for (int c = 0; c < Cf; c++) {
        if (t == 0) trueZ[c][t] = Rand.Double() < 0.5 ? 0 : 1;
        else {
            int prev = trueZ[c][t - 1];
            double stay = selfStay[c][prev];
            trueZ[c][t] = Rand.Double() < stay ? prev : 1 - prev;
        }
    }
    y[t] = trueMu[0][trueZ[0][t]] + trueMu[1][trueZ[1][t]] + obsStd * Rand.Normal();
}
double dMean = y.Average(), dMin = y.Min(), dMax = y.Max(), dRange = dMax - dMin;
double dVar = y.Select(v => (v - dMean) * (v - dMean)).Average();
Console.WriteLine($"Donnees : plage=[{dMin:0.0},{dMax:0.0}], 4 niveaux additifs attendus ~ 0/5/20/25\n");

// ---- Construction du modele FHMM (2 chaines binaires) ----
InferenceEngine BuildAndInfer(int seed, out double evidence,
    out Gaussian[][] meanPost, out Dirichlet[][] transPost, out Discrete[][] statesPost, out Gamma obsPrecPost) {
    var K = Util.ArrayInit(Cf, c => new Range(2).Named($"K{c}"));
    var Tr = new Range(Tf).Named("Tf");
    Tr.AddAttribute(new Sequential());

    var ev = Variable.Bernoulli(0.5).Named("evidence");
    var probInit = new Variable<Vector>[Cf];
    var cptTrans = new VariableArray<Vector>[Cf];
    var emitMean = new VariableArray<double>[Cf];
    var emitPrec = new VariableArray<double>[Cf];
    var zero = new Variable<int>[Cf];
    var chain = new VariableArray<int>[Cf];
    Variable<double> obsPrec = null;
    VariableArray<double> emissions = null;

    using (Variable.If(ev)) {
        for (int c = 0; c < Cf; c++) {
            var pInitPrior = Variable.Observed(Dirichlet.Uniform(2));
            probInit[c] = Variable<Vector>.Random(pInitPrior); probInit[c].SetValueRange(K[c]);
            var transPrior = Variable.Observed(Util.ArrayInit(2, k => Dirichlet.Uniform(2)), K[c]);
            cptTrans[c] = Variable.Array<Vector>(K[c]);
            cptTrans[c][K[c]] = Variable<Vector>.Random(transPrior[K[c]]); cptTrans[c].SetValueRange(K[c]);
            // anchrage : etat 0 = contribution ~0 (tight) ; etat 1 = libre (broad, autour de la moitie de la plage)
            var mPrior = Variable.Observed(new[]{
                Gaussian.FromMeanAndVariance(0.0, 4.0),
                Gaussian.FromMeanAndVariance(dMin + dRange * (c + 0.5) / Cf, dVar * 100) }, K[c]);
            emitMean[c] = Variable.Array<double>(K[c]); emitMean[c][K[c]] = Variable<double>.Random(mPrior[K[c]]);
            var pPrior = Variable.Observed(Util.ArrayInit(2, k => Gamma.FromShapeAndRate(2.0, 2.0 * dVar)), K[c]);
            emitPrec[c] = Variable.Array<double>(K[c]); emitPrec[c][K[c]] = Variable<double>.Random(pPrior[K[c]]);
            zero[c] = Variable.Discrete(probInit[c]);
            chain[c] = Variable.Array<int>(Tr);
        }
        var obsPrecPrior = Variable.Observed(Gamma.FromShapeAndRate(2.0, 2.0 * (obsStd * obsStd)));
        obsPrec = Variable<double>.Random(obsPrecPrior);
        emissions = Variable.Array<double>(Tr);

        using (var block = Variable.ForEach(Tr)) {
            var t = block.Index;
            for (int c = 0; c < Cf; c++) {
                var prev = chain[c][t - 1];
                using (Variable.If(t == 0)) using (Variable.Switch(zero[c])) chain[c][Tr] = Variable.Discrete(cptTrans[c][zero[c]]);
                using (Variable.If(t > 0)) using (Variable.Switch(prev)) chain[c][t] = Variable.Discrete(cptTrans[c][prev]);
            }
            using (Variable.Switch(chain[0][t]))
            using (Variable.Switch(chain[1][t])) {
                var sum = emitMean[0][chain[0][t]] + emitMean[1][chain[1][t]];
                emissions[t] = Variable.GaussianFromMeanAndPrecision(sum, obsPrec);
            }
        }
    }
    emissions.ObservedValue = y;

    // brisure de symetrie : init aleatoire par chaine
    Rand.Restart(seed);
    for (int c = 0; c < Cf; c++) {
        var zInit = Variable<Discrete>.Array(Tr);
        zInit.ObservedValue = Util.ArrayInit(Tf, t => Discrete.PointMass(Rand.Int(2), 2));
        chain[c][Tr].InitialiseTo(zInit[Tr]);
    }

    var eng = new InferenceEngine(new ExpectationPropagation());
    eng.NumberOfIterations = 75; eng.ShowProgress = false;
    meanPost = Util.ArrayInit(Cf, c => eng.Infer<Gaussian[]>(emitMean[c]));
    transPost = Util.ArrayInit(Cf, c => eng.Infer<Dirichlet[]>(cptTrans[c]));
    statesPost = Util.ArrayInit(Cf, c => eng.Infer<Discrete[]>(chain[c]));
    obsPrecPost = eng.Infer<Gamma>(obsPrec);
    evidence = eng.Infer<Bernoulli>(ev).LogOdds;
    return eng;
}

// ---- Multi-restart : on garde le run de meilleure evidence (recommande par usptact) ----
double bestEv = double.NegativeInfinity;
Gaussian[][] bMean = null; Dirichlet[][] bTrans = null; Discrete[][] bStates = null; Gamma bObs = default;
for (int r = 0; r < 6; r++) {
    double ev; Gaussian[][] mp; Dirichlet[][] tp; Discrete[][] sp; Gamma op;
    BuildAndInfer(100 + r * 13, out ev, out mp, out tp, out sp, out op);
    Console.WriteLine($"  restart {r} (seed {100+r*13}) : log-evidence = {ev:0.0}");
    if (ev > bestEv) { bestEv = ev; bMean = mp; bTrans = tp; bStates = sp; bObs = op; }
}
Console.WriteLine($"\nMeilleure evidence retenue : {bestEv:0.0}\n");

// ---- Resultats : contributions retrouvees par chaine ----
for (int c = 0; c < Cf; c++) {
    var m = bMean[c].Select(g => g.GetMean()).OrderBy(x => x).ToArray();
    Console.WriteLine($"Chaine {c} : contributions retrouvees = [{m[0]:0.0}, {m[1]:0.0}]  (vraies = [{trueMu[c][0]:0.0}, {trueMu[c][1]:0.0}])");
}
Console.WriteLine($"Ecart-type d'observation retrouve : {Math.Sqrt(1.0/bObs.GetMean()):0.0} (vrai {obsStd:0.0})");

// ---- Reconstruction additive : MAP par chaine ----
// Une FHMM est identifiable seulement A PERMUTATION DES CHAINES pres (les chaines sont
// echangeables : seul leur SOMME est observee). On score donc "modulo relabeling" :
// on essaie l'identite + l'echange des chaines, et le flip d'etiquette dans chaque chaine.
int[][] mapZ = Util.ArrayInit(Cf, c => bStates[c].Select(s => s.GetMode()).ToArray());
int[][] perms = { new[]{0,1}, new[]{1,0} };  // 2 chaines -> 2 permutations
int best = 0; int[] bestPerm = perms[0]; int[] bestFlip = {0,0};
foreach (var perm in perms)
for (int f0 = 0; f0 < 2; f0++)
for (int f1 = 0; f1 < 2; f1++) {
    int[] flip = { f0, f1 };
    int corr = 0;
    for (int t = 0; t < Tf; t++) {
        bool ok = true;
        for (int c = 0; c < Cf; c++) {
            int rec = mapZ[perm[c]][t] ^ flip[c];   // chaine recuperee perm[c], etiquette eventuellement inversee
            if (rec != trueZ[c][t]) ok = false;
        }
        if (ok) corr++;
    }
    if (corr > best) { best = corr; bestPerm = perm; bestFlip = flip; }
}
Console.WriteLine($"\nReconstruction conjointe (modulo echange des chaines) : {best}/{Tf} pas exacts ({100.0*best/Tf:0.0}%)");
Console.WriteLine($"  -> chaine recuperee {bestPerm[0]} = vraie chaine 0 ; chaine recuperee {bestPerm[1]} = vraie chaine 1 (symetrie d'echange FHMM)");
Donnees : plage=[-3,0,28,0], 4 niveaux additifs attendus ~ 0/5/20/25

  restart 0 (seed 100) : log-evidence = -259,9
  restart 1 (seed 113) : log-evidence = -259,9
  restart 2 (seed 126) : log-evidence = -259,9
  restart 3 (seed 139) : log-evidence = -259,9
  restart 4 (seed 152) : log-evidence = -260,0
  restart 5 (seed 165) : log-evidence = -259,9

Meilleure evidence retenue : -259,9

Chaine 0 : contributions retrouvees = [-0,0, 5,4]  (vraies = [0,0, 20,0])
Chaine 1 : contributions retrouvees = [0,1, 20,0]  (vraies = [0,0, 5,0])
Ecart-type d'observation retrouve : 1,6 (vrai 1,5)

Reconstruction conjointe (modulo echange des chaines) : 98/100 pas exacts (98,0%)
  -> chaine recuperee 1 = vraie chaine 0 ; chaine recuperee 0 = vraie chaine 1 (symetrie d'echange FHMM)

Interpretation : identifiabilite a permutation pres

Le FHMM retrouve les contributions additives de chaque chaîne (~0/20 et ~0/5) et reconstruit l’etat conjoint avec une très bonne precision.

Point subtil et pedagogique : un FHMM n’est identifiable qu’a permutation des chaînes pres. Comme seule leur somme est observee, rien ne distingue “chaîne A = gros appareil, chaîne B = petit” de l’arrangement inverse – ni l’etiquette 0/1 a l’interieur d’une chaîne. Le score est donc calcule modulo relabeling (on essaie l’identite + l’echange des chaînes, et le flip d’etiquette dans chaque chaîne, on garde le meilleur).

On choisit aussi le run de meilleure log-evidence sur plusieurs initialisations aleatoires (multi-restart) : c’est la encore une parade a la symetrie, recommandee par l’implementation de reference.

4. Detection de Regimes Meteo

Cas d’usage classique : inferer une variable latente (le vrai temps) a partir d’observations bruitees (temperature).

Regime Temperature moyenne Ecart-type
Soleil 22C 2C
Pluie 15C 2C
// Exemple : Detection de regimes meteo (Soleil/Pluie) a partir de temperature

// Soleil : temperature ~22C
// Pluie : temperature ~15C

double[] tempJour = { 21, 23, 22, 20, 15, 14, 16, 15, 14, 21, 22, 23 };
int Tmeteo = tempJour.Length;

Console.WriteLine("=== Detection Regimes Meteo ===");
Console.WriteLine($"\nTemperatures : {string.Join(", ", tempJour)}\n");

for (int t = 0; t < Tmeteo; t++)
{
    Variable<int> meteo = Variable.DiscreteUniform(2);
    Variable<double> temp = Variable.New<double>();
    
    using (Variable.Case(meteo, 0))  // Soleil
    {
        temp.SetTo(Variable.GaussianFromMeanAndVariance(22, 4));
    }
    using (Variable.Case(meteo, 1))  // Pluie
    {
        temp.SetTo(Variable.GaussianFromMeanAndVariance(15, 4));
    }
    
    temp.ObservedValue = tempJour[t];
    
    InferenceEngine eng = new InferenceEngine();
    eng.Compiler.CompilerChoice = CompilerChoice.Roslyn;
    
    Discrete meteoPost = eng.Infer<Discrete>(meteo);
    string regime = meteoPost.GetProbs()[0] > 0.5 ? "Soleil" : "Pluie";
    
    Console.WriteLine($"Jour {t+1,2} : {tempJour[t]:F0}C -> {regime} (P={meteoPost.GetProbs().Max():F2})");
}
=== Detection Regimes Meteo ===

Temperatures : 21, 23, 22, 20, 15, 14, 16, 15, 14, 21, 22, 23

Compiling model...done.
Jour  1 : 21C -> Soleil (P=0,99)
Compiling model...done.
Jour  2 : 23C -> Soleil (P=1,00)
Compiling model...done.
Jour  3 : 22C -> Soleil (P=1,00)
Compiling model...done.
Jour  4 : 20C -> Soleil (P=0,93)
Compiling model...done.
Jour  5 : 15C -> Pluie (P=1,00)
Compiling model...done.
Jour  6 : 14C -> Pluie (P=1,00)
Compiling model...done.
Jour  7 : 16C -> Pluie (P=0,99)
Compiling model...done.
Jour  8 : 15C -> Pluie (P=1,00)
Compiling model...done.
Jour  9 : 14C -> Pluie (P=1,00)
Compiling model...done.
Jour 10 : 21C -> Soleil (P=0,99)
Compiling model...done.
Jour 11 : 22C -> Soleil (P=1,00)
Compiling model...done.
Jour 12 : 23C -> Soleil (P=1,00)

Visualisation du graphe de facteurs pour la detection de regimes meteo.

// Visualisation du graphe de facteurs pour la detection meteo
// Modele identique a la classification independante

Variable<int> meteoViz = Variable.DiscreteUniform(2).Named("meteo");
Variable<double> tempViz = Variable.New<double>().Named("temperature");

using (Variable.Case(meteoViz, 0))
{
    tempViz.SetTo(Variable.GaussianFromMeanAndVariance(22, 4).Named("emission_Soleil"));
}
using (Variable.Case(meteoViz, 1))
{
    tempViz.SetTo(Variable.GaussianFromMeanAndVariance(15, 4).Named("emission_Pluie"));
}

tempViz.ObservedValue = 18.0;

InferenceEngine engMeteoViz = new InferenceEngine();
engMeteoViz.Compiler.CompilerChoice = CompilerChoice.Roslyn;
engMeteoViz.ShowFactorGraph = true;

var _meteo = engMeteoViz.Infer(meteoViz);
Console.WriteLine("\nGraphe de facteurs genere pour le modele meteo.");
Compiling model...done.

Graphe de facteurs genere pour le modele meteo.

Interpretation des résultats meteo

Jours Temperature Regime Confiance
1-4 20-23C Soleil 93-100%
5-9 14-16C Pluie 99-100%
10-12 21-23C Soleil 99-100%

Observation : Le jour 4 (20C) montre une confiance de 93% car cette temperature est a mi-chemin entre les deux regimes. C’est exactement le type de cas ou un HMM avec transitions aiderait : si les jours 1-3 sont “Soleil”, le jour 4 devrait l’etre aussi par continuite.

Visualisons le graphe de facteurs pour ce modèle de classification :

// Affichage du graphe meteo
display(HTML(FactorGraphHelper.GetLatestFactorGraphHtml()));
Model_06_22_26_21_18_32_44.svg
Model node0 2 node1 DiscreteUniform node0->node1 size node2 meteo node1->node2 node6 18 node2->node6 condition node3 22 node4 GaussianFromMeanAndVariance node3->node4 mean node4->node6 node5 4 node5->node4 variance node7 15 node8 GaussianFromMeanAndVariance node7->node8 mean node8->node6 node9 4 node9->node8 variance

Graphe de facteurs : Modèle meteo (classification)

Structure identique a la detection d’anomalies - un modèle de melange gaussien :

Composante Paramètres
Soleil N(22, 4) - temperature elevee
Pluie N(15, 4) - temperature basse

Le graphe montre le branchement conditionnel (Variable.Case) qui selectionne l’emission appropriee selon l’etat latent meteo.

Application : Ce modèle simple est equivalent a un classifieur de Bayes naive pour une seule feature (temperature).

Résultats : Detection correcte des deux regimes (Soleil jours 1-4 et 10-12, Pluie jours 5-9).

Point d’intérêt : Le jour 4 (20C) a une confiance “seulement” 93% car la temperature est intermediaire entre les deux regimes.

4bis. Exercice : Sensibilite du Modèle aux Emissions Chevauchantes

Dans la section 4, les deux regimes meteo (Soleil ~22C, Pluie ~15C) sont bien separes avec un ecart de 7C pour un ecart-type de 2C. Que se passe-t-il quand les emissions se chevauchent davantage ?

Scénario : Deux villes ont des regimes de temperature proches :

Ville Moyenne Ecart-type
Ville A (oceanique) 18C 4C
Ville B (semi-continental) 16C 4C

L’ecart entre les moyennes (2C) est plus petit que l’ecart-type (4C) : les distributions se recouvrent fortement.

Objectifs : 1. Implementez un modèle de classification independante avec ces paramètres 2. Testez sur les observations : {15, 17, 19, 16, 18, 14, 20, 17} 3. Identifiez les observations ambigues (P < 0.8) 4. Repetez avec des emissions mieux separees (moyennes 20C et 12C) et comparez

Étapes : 1. Définir les paramètres d’emission pour les deux villes (GaussianFromMeanAndVariance avec variance 16) 2. Boucler sur les observations et inferer l’appartenance a chaque ville avec Variable.Case 3. Afficher P(Ville A) et P(Ville B) pour chaque observation 4. Repeter avec muA=20, muB=12 et comparer les confiances

Indices : - Utilisez Variable.GaussianFromMeanAndVariance(mu, sigma2) avec sigma2 = 16 (sigma = 4) - Une observation est ambigu si P(Ville A) est entre 0.2 et 0.8 - Avec des moyennes plus eloignees (20 et 12), l’ecart (8C) depasse 2 sigma : separation nette attendue

// Exercice : Sensibilite du modele aux emissions chevauchantes

// Observations
double[] tempsVilles = { 15, 17, 19, 16, 18, 14, 20, 17 };

// Parametres d'emission (ecart faible = chevauchement)
double muA = 18.0, muB = 16.0;
double sigma2Ville = 16.0; // sigma = 4

// TODO: Etape 1 - Implementez la classification independante
// (Reutilisez le pattern Variable.DiscreteUniform + Variable.Case de la section 4)

// TODO: Etape 2 - Affichez P(Ville A) et P(Ville B) pour chaque observation
// Identifiez les observations ambigues (P entre 0.2 et 0.8)

// TODO: Etape 3 - Repetez avec muA=20 et muB=12 (emissions bien separees)
// Comparez les niveaux de confiance

Console.WriteLine("Exercice a completer : analysez la sensibilite aux emissions chevauchantes.");
Exercice a completer : analysez la sensibilite aux emissions chevauchantes.

5. Motif Finding (Bioinformatique)

Problème

Trouver des motifs conserves dans des sequences ADN.

Modèle

  • Arriere-plan : nucleotides uniformes (A, C, G, T)
  • Motif : positions avec distributions spécifiques

Un modèle generatif de motif (et son miroir : l’inference)

Le comptage de k-mers (naif) trouve les sous-chaînes frequentes, mais il ne modelise pas l’incertitude : une position du motif peut tolerer plusieurs bases. Le modèle probabiliste de dotnet/infer (MotifFinder) decrit comment les sequences sont engendrees, puis inverse ce processus pour retrouver le motif.

Modèle generatif (port fidele du MotifFinder officiel) : - PWM (Position Weight Matrix) : pour chacune des \(W\) positions du motif, une distribution sur {A, C, G, T}, de prior Dirichlet. - Pour chaque sequence : presence du motif motifPresence ~ Bernoulli(0.8), et si present, une position motifPos ~ Uniforme. - La sequence = fond uniforme (gauche) + motif (echantillonne de la PWM) + fond uniforme (droite), assemblee par Variable.StringFromArray / Variable.StringOfLength (inference sur des chaînes de caractères, une specialite d’Infer.NET).

L’inference et la generation sont symetriques : on echantillonne d’abord depuis une PWM connue (ÉTAPE 1), puis on fait tourner le modèle a l’envers pour retrouver cette PWM a partir des seules sequences (ÉTAPE 2). Comparer les deux est le coeur pedagogique de la section.

// ETAPE 1 - GENERATION (forward) : on echantillonne des sequences depuis une PWM CONNUE.
// C'est le miroir pedagogique de l'inference : meme modele, parcouru dans l'autre sens.

Rand.Restart(1337);
int SequenceCount = 70;
int SequenceLength = 25;
double MotifPresenceProbability = 0.8;

DiscreteChar NucleobaseDist(double a, double c, double g, double t) {
    Vector probs = PiecewiseVector.Zero(char.MaxValue + 1);
    probs['A'] = a; probs['C'] = c; probs['G'] = g; probs['T'] = t;
    return DiscreteChar.FromVector(probs);
}

// ---- Vraie PWM du motif (matrice de frequences position-specifique, 8 positions = original) ----
// La position 4 (index 3) est volontairement uniforme : le modele doit aussi gerer une colonne non-informative.
var trueMotif = new[] {
    NucleobaseDist(0.80, 0.10, 0.05, 0.05),
    NucleobaseDist(0.00, 0.90, 0.05, 0.05),
    NucleobaseDist(0.00, 0.00, 0.50, 0.50),
    NucleobaseDist(0.25, 0.25, 0.25, 0.25),
    NucleobaseDist(0.10, 0.10, 0.10, 0.70),
    NucleobaseDist(0.00, 0.00, 0.90, 0.10),
    NucleobaseDist(0.90, 0.05, 0.00, 0.05),
    NucleobaseDist(0.50, 0.50, 0.00, 0.00),
};
int motifLength = trueMotif.Length;
var background = NucleobaseDist(0.25, 0.25, 0.25, 0.25);

// ---- Echantillonnage FORWARD (background + motif insere a position aleatoire) ----
string[] seqData = new string[SequenceCount];
int[]    posData = new int[SequenceCount];
for (int i = 0; i < SequenceCount; i++) {
    if (Rand.Double() <= MotifPresenceProbability) {
        posData[i] = Rand.Int(SequenceLength - motifLength + 1);
        var before = Util.ArrayInit(posData[i], j => background.Sample());
        var after  = Util.ArrayInit(SequenceLength - motifLength - posData[i], j => background.Sample());
        var mc     = Util.ArrayInit(motifLength, j => trueMotif[j].Sample());
        seqData[i] = new string(before) + new string(mc) + new string(after);
    } else {
        posData[i] = -1;
        seqData[i] = new string(Util.ArrayInit(SequenceLength, j => background.Sample()));
    }
}


// ---- Affichage : la PWM "verite terrain" + quelques sequences echantillonnees ----
void ShowPWM(string cap, Func<int,char,double> w) {
    Console.WriteLine(cap + " (lignes = base, colonnes = position du motif) :");
    foreach (char b in new[]{'A','C','G','T'}) {
        Console.Write($"  {b}:");
        for (int i = 0; i < motifLength; i++) Console.Write($"  {w(i,b):0.00}");
        Console.WriteLine();
    }
}
Console.WriteLine($"Echantillonnage FORWARD : {SequenceCount} sequences de longueur {SequenceLength}, motif present avec prob {MotifPresenceProbability}.\n");
ShowPWM("Vraie PWM du motif (8 positions, position 4 volontairement uniforme)", (i,b) => trueMotif[i][b]);
Console.WriteLine("\nQuelques sequences echantillonnees (^ marque la position vraie du motif) :");
for (int i = 0; i < 6; i++) {
    string mark = posData[i] < 0 ? "(pas de motif)" : new string(' ', posData[i]) + new string('^', motifLength);
    Console.WriteLine($"  seq {i}: {seqData[i]}");
    Console.WriteLine($"         {mark}");
}
Echantillonnage FORWARD : 70 sequences de longueur 25, motif present avec prob 0,8.

Vraie PWM du motif (8 positions, position 4 volontairement uniforme) (lignes = base, colonnes = position du motif) :
  A:  0,80  0,00  0,00  0,25  0,10  0,00  0,90  0,50
  C:  0,10  0,90  0,00  0,25  0,10  0,00  0,05  0,50
  G:  0,05  0,05  0,50  0,25  0,10  0,90  0,00  0,00
  T:  0,05  0,05  0,50  0,25  0,70  0,10  0,05  0,00

Quelques sequences echantillonnees (^ marque la position vraie du motif) :
  seq 0: CTACTTCGAATTTACCCCTATATTT
           ^^^^^^^^
  seq 1: TTGTGCGGGCATAAGACTTTGACTA
                        ^^^^^^^^
  seq 2: CAAACGTCGGGCAGACCGTACTCCG
         (pas de motif)
  seq 3: ACTTTGACCTACCAGAGCCTGATGA
         ^^^^^^^^
  seq 4: GGTTGACGCCACCGTCGATGAACGA
                       ^^^^^^^^
  seq 5: AGCGTACTCTGAATCAGTCGTAGCA
              ^^^^^^^^

Étape 2 : inference – retrouver la PWM, la presence et la position

On observe maintenant uniquement les sequences (en oubliant la PWM et les positions vraies) et on demande a Infer.NET de les reconstruire. Le même modèle generatif sert de squelette ; le moteur infere a posteriori : - la PWM du motif (a comparer a la verite terrain de l’étape 1), - pour chaque sequence, la probabilite de presence du motif et sa position la plus probable.

using Microsoft.ML.Probabilistic.Factors.Attributes;  // QualityBand

// ETAPE 2 - INFERENCE (backward) : on OBSERVE les sequences et on RETROUVE
// la PWM + la presence + la position du motif, par programmation probabiliste.
// (reutilise seqData / posData / trueMotif / background generes a l'etape 1)

// ---- Modele generatif Infer.NET ----
Vector pseudo = PiecewiseVector.Constant(char.MaxValue + 1, 1e-6);
pseudo['A'] = pseudo['C'] = pseudo['G'] = pseudo['T'] = 1.0;

Range mcr      = new Range(motifLength);
var motifProbs = Variable.Array<Vector>(mcr);
motifProbs[mcr] = Variable.Dirichlet(pseudo).ForEach(mcr);

var sr = new Range(SequenceCount);
var sequences = Variable.Array<string>(sr);
var motifPos  = Variable.Array<int>(sr);
motifPos[sr]  = Variable.DiscreteUniform(SequenceLength - motifLength + 1).ForEach(sr);
var motifPresence = Variable.Array<bool>(sr);
motifPresence[sr] = Variable.Bernoulli(MotifPresenceProbability).ForEach(sr);

using (Variable.ForEach(sr)) {
    using (Variable.If(motifPresence[sr])) {
        var motifChars = Variable.Array<char>(mcr);
        motifChars[mcr] = Variable.Char(motifProbs[mcr]);
        var motif = Variable.StringFromArray(motifChars);
        var bgRightLen = SequenceLength - motifLength - motifPos[sr];
        var bgLeft  = Variable.StringOfLength(motifPos[sr], background);
        var bgRight = Variable.StringOfLength(bgRightLen, background);
        sequences[sr] = bgLeft + motif + bgRight;
    }
    using (Variable.IfNot(motifPresence[sr])) {
        sequences[sr] = Variable.StringOfLength(SequenceLength, background);
    }
}

// ---- Inference ----
sequences.ObservedValue = seqData;
var eng = new InferenceEngine();
eng.NumberOfIterations = 30;
eng.Compiler.RecommendedQuality = QualityBand.Experimental;
eng.ShowProgress = false;

var pwmPost  = eng.Infer<IList<Dirichlet>>(motifProbs);
var presPost = eng.Infer<IList<Bernoulli>>(motifPresence);
var posPost  = eng.Infer<IList<Discrete>>(motifPos);

// ---- Resultats : PWM vraie vs retrouvee ----
void PrintPWM(string caption, Func<int,char,double> w) {
    Console.WriteLine(caption + " :");
    foreach (char b in new[]{'A','C','G','T'}) {
        Console.Write($"  {b}:");
        for (int i = 0; i < motifLength; i++) Console.Write($"  {w(i,b):0.00}");
        Console.WriteLine();
    }
}
PrintPWM("Vraie PWM du motif", (i,b) => trueMotif[i][b]);
Console.WriteLine();
PrintPWM("PWM RETROUVEE (moyenne posterieure)", (i,b) => pwmPost[i].GetMean()[b]);

Console.WriteLine("\n=== Quelques sequences : position predite vs vraie ===");
int shown = 0;
for (int i = 0; i < SequenceCount && shown < 8; i++) {
    int predPos = presPost[i].GetProbTrue() > 0.5 ? posPost[i].GetMode() : -1;
    Console.WriteLine($"  seq {i,2}: {seqData[i]}  P(motif)={presPost[i].GetProbTrue():0.00}  pos_pred={predPos,2} (vrai {posData[i],2})");
    shown++;
}
int posOK = 0, present = 0;
for (int i = 0; i < SequenceCount; i++) {
    if (posData[i] >= 0) { present++; if (presPost[i].GetProbTrue() > 0.5 && posPost[i].GetMode() == posData[i]) posOK++; }
}
Console.WriteLine($"\n=== Positions exactes retrouvees : {posOK}/{present} sequences avec motif ===");
Vraie PWM du motif :
  A:  0,80  0,00  0,00  0,25  0,10  0,00  0,90  0,50
  C:  0,10  0,90  0,00  0,25  0,10  0,00  0,05  0,50
  G:  0,05  0,05  0,50  0,25  0,10  0,90  0,00  0,00
  T:  0,05  0,05  0,50  0,25  0,70  0,10  0,05  0,00

PWM RETROUVEE (moyenne posterieure) :
  A:  0,77  0,02  0,02  0,25  0,11  0,03  0,88  0,38
  C:  0,07  0,91  0,03  0,33  0,17  0,02  0,04  0,58
  G:  0,09  0,05  0,49  0,20  0,09  0,90  0,02  0,02
  T:  0,07  0,02  0,46  0,23  0,64  0,06  0,06  0,02

=== Quelques sequences : position predite vs vraie ===
  seq  0: CTACTTCGAATTTACCCCTATATTT  P(motif)=0,98  pos_pred= 2 (vrai  2)
  seq  1: TTGTGCGGGCATAAGACTTTGACTA  P(motif)=1,00  pos_pred=15 (vrai 15)
  seq  2: CAAACGTCGGGCAGACCGTACTCCG  P(motif)=0,27  pos_pred=-1 (vrai -1)
  seq  3: ACTTTGACCTACCAGAGCCTGATGA  P(motif)=1,00  pos_pred= 0 (vrai  0)
  seq  4: GGTTGACGCCACCGTCGATGAACGA  P(motif)=0,94  pos_pred=14 (vrai 14)
  seq  5: AGCGTACTCTGAATCAGTCGTAGCA  P(motif)=1,00  pos_pred= 5 (vrai  5)
  seq  6: CGACCTATCTTGAGTGTTGCTACAA  P(motif)=0,01  pos_pred=-1 (vrai -1)
  seq  7: ATTGAACTCTGAACTCGTCCGGTTG  P(motif)=1,00  pos_pred= 5 (vrai  5)

=== Positions exactes retrouvees : 50/54 sequences avec motif ===

Résultats et lissage laplacien

La PWM retrouvee reproduit fidelement la verite terrain (colonnes fortement conservees retrouvees nettement ; la colonne volontairement uniforme reste plate), et la majorite des positions du motif sont correctement localisees.

Pourquoi l’erreur residuelle ? Les quelques ecarts de probabilite dans la PWM et les rares faux positifs/negatifs de position viennent du lissage laplacien (Laplacian / additive smoothing) : le prior Dirichlet place des pseudo-comptes (=1.0 pour A/C/G/T) sur chaque position. Ces pseudo-comptes empechent les probabilites de tomber a exactement 0 ou 1 (utile : un nucleotide jamais vu dans l’echantillon reste possible), mais ils tirent legerement les estimations vers l’uniforme. Avec plus de sequences, le poids des données domine les pseudo-comptes et l’estimation se resserre vers la PWM vraie.

C’est exactement le compromis biais-variance du lissage : un peu de biais (vers l’uniforme) contre beaucoup moins de variance (pas de probabilite 0 catastrophique sur un petit echantillon).

6. Exemple guide : Detection d’Anomalies

Enonce

Utilisez un HMM pour detecter des periodes anormales dans une serie temporelle de ventes.


Transition : Après avoir explore les HMM theoriquement (section 3) et sur des exemples meteo (section 4) et bioinformatiques (section 5), passons a un exercice pratique de detection d’anomalies dans un contexte business.

Detection d’anomalies dans les series temporelles de ventes :

Scénario Caractéristiques Action
Promotion Hausse temporaire Optimiser le stock
Rupture stock Baisse soudaine Reapprovisionner
Tendance Evolution graduelle Ajuster previsions
// Exemple guide : Detection d'anomalies dans les ventes

// Ventes journalieres (normal ~100, promo ~200)
double[] ventes = { 98, 105, 102, 99, 195, 210, 205, 198, 103, 97, 101, 100 };

Console.WriteLine("=== Detection Periodes de Promotion ===");
Console.WriteLine($"\nVentes : {string.Join(", ", ventes.Select(v => v.ToString("F0")))}\n");

for (int t = 0; t < ventes.Length; t++)
{
    Variable<int> regime = Variable.DiscreteUniform(2);
    Variable<double> venteVar = Variable.New<double>();
    
    using (Variable.Case(regime, 0))  // Normal
    {
        venteVar.SetTo(Variable.GaussianFromMeanAndVariance(100, 100));
    }
    using (Variable.Case(regime, 1))  // Promotion
    {
        venteVar.SetTo(Variable.GaussianFromMeanAndVariance(200, 100));
    }
    
    venteVar.ObservedValue = ventes[t];
    
    InferenceEngine eng = new InferenceEngine();
    eng.Compiler.CompilerChoice = CompilerChoice.Roslyn;
    
    Discrete regimePost = eng.Infer<Discrete>(regime);
    string etat = regimePost.GetProbs()[1] > 0.5 ? "PROMO" : "Normal";
    
    Console.WriteLine($"Jour {t+1,2} : {ventes[t],3:F0} -> {etat} (P={regimePost.GetProbs().Max():F2})");
}

Console.WriteLine("\n=> Jours 5-8 detectes comme periode de promotion");
=== Detection Periodes de Promotion ===

Ventes : 98, 105, 102, 99, 195, 210, 205, 198, 103, 97, 101, 100

Compiling model...done.
Jour  1 :  98 -> Normal (P=1,00)
Compiling model...done.
Jour  2 : 105 -> Normal (P=1,00)
Compiling model...done.
Jour  3 : 102 -> Normal (P=1,00)
Compiling model...done.
Jour  4 :  99 -> Normal (P=1,00)
Compiling model...done.
Jour  5 : 195 -> PROMO (P=1,00)
Compiling model...done.
Jour  6 : 210 -> PROMO (P=1,00)
Compiling model...done.
Jour  7 : 205 -> PROMO (P=1,00)
Compiling model...done.
Jour  8 : 198 -> PROMO (P=1,00)
Compiling model...done.
Jour  9 : 103 -> Normal (P=1,00)
Compiling model...done.
Jour 10 :  97 -> Normal (P=1,00)
Compiling model...done.
Jour 11 : 101 -> Normal (P=1,00)
Compiling model...done.
Jour 12 : 100 -> Normal (P=1,00)

=> Jours 5-8 detectes comme periode de promotion

Visualisation du graphe de facteurs pour la detection d’anomalies dans les ventes.

// Visualisation du graphe pour la detection de promotions
Variable<int> regimeViz = Variable.DiscreteUniform(2).Named("regime_ventes");
Variable<double> venteViz = Variable.New<double>().Named("ventes_journalieres");

using (Variable.Case(regimeViz, 0))
{
    venteViz.SetTo(Variable.GaussianFromMeanAndVariance(100, 100).Named("emission_Normal"));
}
using (Variable.Case(regimeViz, 1))
{
    venteViz.SetTo(Variable.GaussianFromMeanAndVariance(200, 100).Named("emission_Promo"));
}

venteViz.ObservedValue = 150.0;

InferenceEngine engVenteViz = new InferenceEngine();
engVenteViz.Compiler.CompilerChoice = CompilerChoice.Roslyn;
engVenteViz.ShowFactorGraph = true;

var _regime = engVenteViz.Infer(regimeViz);
Console.WriteLine("\nGraphe de facteurs genere pour la detection de promotions.");
Compiling model...done.

Graphe de facteurs genere pour la detection de promotions.

Interpretation des résultats de l’exemple guide

Le detecteur identifie correctement la periode promotionnelle (jours 5-8) :

Periode Ventes moyennes Regime detecte
Jours 1-4 ~101 Normal
Jours 5-8 ~202 PROMO
Jours 9-12 ~100 Normal

Applications business : - Planification stocks : Anticiper les pics de demande - Analyse retrospective : Identifier les periodes promotionnelles non documentees - Detection de fraude : Reperer les anomalies inexpliquees

Visualisons le graphe de facteurs pour ce modèle :

// Affichage du graphe de detection de promotions
display(HTML(FactorGraphHelper.GetLatestFactorGraphHtml()));
Model_06_22_26_21_18_45_80.svg
Model node0 2 node1 DiscreteUniform node0->node1 size node2 regime_ventes node1->node2 node6 150 node2->node6 condition node3 100 node4 GaussianFromMeanAndVariance node3->node4 mean node4->node6 node5 100 node5->node4 variance node7 200 node8 GaussianFromMeanAndVariance node7->node8 mean node8->node6 node9 100 node9->node8 variance

Graphe de facteurs : Detection de promotions

Même structure que les modèles précédents - classification par melange gaussien :

Regime Distribution Interpretation
Normal N(100, 100) Ventes moyennes ~100 unites
Promo N(200, 100) Ventes elevees ~200 unites

Extension possible : Pour une detection plus robuste des periodes promotionnelles, on ajouterait des transitions Markoviennes comme dans la section 3.3. Cela permettrait de :

  1. Penaliser les changements frequents Normal <-> Promo
  2. Detecter des periodes coherentes (promotion sur plusieurs jours consecutifs)
  3. Lisser les observations ambigues (ex: 150 unites) selon le contexte

Résultats : Detection parfaite - jours 5-8 identifies comme periode promotionnelle (ventes ~200 vs ~100 en normal).

6bis. Exercice : Impact des Paramètres de Transition sur le Lissage

Dans la section 3.3, nous avons construit un HMM complet a transitions markoviennes (trois regimes Normal/Alerte/Critique, inference par Expectation Propagation puis decodage de Viterbi) et observe comment la matrice de transition lisse les decisions la ou la classification independante sur-segmente.

Votre mission : modifiez les paramètres du HMM et analysez l’impact sur les probabilites a posteriori.

Scénario : Un capteur de debit hydraulique produit les mesures suivantes :

{8.2, 9.1, 9.5, 12.5, 14.0, 9.8, 8.5, 12.8}

Les deux regimes possibles sont : - Debit normal : ~N(9, 2) (moyenne 9, variance 2) - Fuite : ~N(13, 2) (moyenne 13, variance 2)

Objectifs : 1. Implementez la passe Forward avec les paramètres fournis 2. Testez deux configurations de transitions : - Config A : pStay = 0.7, pSwitch = 0.3 (transitions frequentes) - Config B : pStay = 0.95, pSwitch = 0.05 (transitions rares) 3. Comparez les probabilites a posteriori pour les observations ambigues (12.5, 14.0, 9.8, 12.8) 4. Expliquez pourquoi la Config B produit des sequences plus “lisses”

Indice : Les valeurs 12.5 et 12.8 sont ambigues car entre les deux moyennes (9 et 13). Comparez P(Fuite) entre les deux configs pour ces valeurs - la config B devrait favoriser la continuite avec les voisins plutot que la vraisemblance seule.

// Exercice : Impact des parametres de transition sur le lissage

// Donnees capteur de debit hydraulique
double[] debit = { 8.2, 9.1, 9.5, 12.5, 14.0, 9.8, 8.5, 12.8 };
int Tdebit = debit.Length;

// Parametres d'emission
double muNormal = 9.0, muFuite = 13.0;
double sigma2Debit = 2.0;

// Fonction de vraisemblance gaussienne (reutilisez celle de la section 3.3)
// gaussLik(x, mu, s2) = exp(-0.5 * (x - mu)^2 / s2)

// TODO: Etape 1 - Implementez la passe Forward pour une config donnee
// Parametres : pStay, pSwitch, observations
// Retourne : tableau alpha[T, 2] normalise

// TODO: Etape 2 - Implementez la passe Backward
// Retourne : tableau beta[T, 2] normalise

// TODO: Etape 3 - Calculez les posterieurs gamma[T, 2] = alpha * beta (normalise)

// TODO: Etape 4 - Testez Config A (pStay=0.7) et affichez les resultats
double pStayA = 0.7;
double pSwitchA = 0.3;

// TODO: Etape 5 - Testez Config B (pStay=0.95) et affichez les resultats
double pStayB = 0.95;
double pSwitchB = 0.05;

// TODO: Etape 6 - Affichez un tableau comparatif pour les 4 observations ambigues
// | t | Obs | P(Fuite) Config A | P(Fuite) Config B | Decision A | Decision B |

Console.WriteLine("Exercice a completer : implementez Forward-Backward pour les deux configs.");
Exercice a completer : implementez Forward-Backward pour les deux configs.

7. Resume

Concept Description
HMM Modèle a etats caches avec dependances temporelles
Emissions Distribution des observations selon l’etat
Transitions Probabilites de changement d’etat
Viterbi Algorithme pour trouver la sequence d’etats optimale
Forward-Backward Calcul des probabilites marginales

Pour aller plus loin

Si vous voulez… Consultez…
Comprendre Variable.Switch Infer-3-Factor-Graphs
Comparer EP vs VMP pour HMM Infer-2b-Debugging-Bonnes-Pratiques Section 4
Debugger des problemes de convergence Infer-2b-Debugging-Bonnes-Pratiques
Trouver une definition (HMM, Viterbi, etc.) Glossaire

Plus loin dans la serie

Dans Infer-15-Recommenders, nous explorerons :

  • Les systèmes de recommandation
  • La factorisation matricielle
  • Le modèle ClickModel pour sources multiples

7bis. Exercice : HMM a 3 Etats - Detection de Pannes Machine

Etendez le HMM a 3 etats pour modeliser l’etat d’une machine industrielle :

  • Etat 0 : Normal - capteur moyen ~100, precision elevee (sigma~5)
  • Etat 1 : Degrade - capteur moyen ~70, precision faible (sigma~15)
  • Etat 2 : En panne - capteur moyen ~20, precision très faible (sigma~25)

Données capteur : {98, 102, 99, 95, 68, 75, 72, 65, 18, 22, 15, 25, 71, 68, 100, 103}

Attendu : Normal (jours 1-4), Degrade (5-8), Panne (9-12), Degrade (13-14), Normal (15-16).

Objectifs : 1. Adapter le modèle de classification independante a 3 etats au lieu de 2 2. Parametrer les emissions gaussiennes pour chaque etat (moyenne + precision) 3. Implementer la boucle d’inference et afficher les decisions

Indice : Utilisez Variable.DiscreteUniform(3) pour 3 etats et ajoutez un troisieme bloc Variable.Case(etat, 2) pour l’etat “En panne”.

// Exercice : HMM 3 etats pour detection de pannes machine
double[] capteur = { 98, 102, 99, 95, 68, 75, 72, 65, 18, 22, 15, 25, 71, 68, 100, 103 };
int T = capteur.Length;
int K = 3;  // 3 etats : Normal=0, Degrade=1, En panne=2

// Parametres d'emission par etat (moyennes et precisions)
double[] moyennesEtat = { 100.0, 70.0, 20.0 };
double[] precisionsEtat = { 1.0/25.0, 1.0/225.0, 1.0/625.0 };  // sigma = 5, 15, 25

// Matrice de transition suggeree (auto-persistance elevee):
// Normal    : [0.90, 0.09, 0.01]
// Degrade   : [0.10, 0.80, 0.10]
// En panne  : [0.01, 0.19, 0.80]
Vector[] transMatrix = new Vector[] {
    Vector.FromArray(0.90, 0.09, 0.01),
    Vector.FromArray(0.10, 0.80, 0.10),
    Vector.FromArray(0.01, 0.19, 0.80)
};

// TODO: Creer le moteur d'inference

// TODO: Construire le HMM 3 etats
// (Reutilisez la structure HMM de l'exemple guide en changeant nStates de 2 a 3)

// TODO: Observer les donnees capteur et inferer les etats

// TODO: Afficher les probabilites d'etat pour les jours cles

Console.WriteLine("Exercice a completer : adaptez le modele de classification a 3 etats.");
Exercice a completer : adaptez le modele de classification a 3 etats.

Conclusion

Ce notebook a explore les Hidden Markov Models avec Infer.NET : etats caches, emissions gaussiennes, transitions markoviennes, decodage de Viterbi, HMM factoriel et motif finding sur chaînes de caractères.

Concept Point cle
HMM Etats caches lies par transitions, observations via emissions
Classification independante Chaque observation classee seule, sans contexte temporel (baseline)
HMM complet (Infer.NET) ForEach + If(t==0)/If(t>0) + Switch ; possible, a condition de briser la symetrie
Brisure de symetrie Init K-means + priors centres + precision intra-cluster ; sinon EP fusionne les etats
Viterbi Chemin d’etats globalement optimal (programmation dynamique)
HMM factoriel Chaînes independantes dont les contributions s’additionnent ; identifiable a permutation pres
Motif finding Modèle generatif PWM + inference sur chaînes (StringFromArray), lissage laplacien
Distribution Usage
Discrete Etats caches (regimes, presence de motif)
Gaussian Emissions conditionnelles aux etats
Dirichlet Priors sur transitions et PWM (pseudo-comptes = lissage laplacien)

A retenir : Infer.NET sait chainer transitions et emissions (HMM, FHMM, motif finding). Les difficultes rencontrees ne sont pas des “limitations de la librairie” mais des problemes classiques d’initialisation, de priors et d’identifiabilite (label switching, permutation des chaînes) – qui se resolvent par K-means, priors informatifs et appariement.

Retour au sommet