Infer-11-Topic-Models : Latent Dirichlet Allocation (LDA)

Serie : Programmation Probabiliste avec Infer.NET (11/19)
Duree estimee : 60 minutes
Prerequis : Infer-8-TrueSkill


Objectifs

  • Comprendre le topic modeling et LDA
  • Implementer la structure documents-topics-mots
  • Utiliser les distributions Dirichlet pour les melanges
  • Inferer les topics a partir d’un corpus

1. Configuration

Nous preparons l’environnement pour le topic modeling avec LDA (Latent Dirichlet Allocation). Ce modèle generatif decouvre automatiquement les thèmes latents dans un corpus de documents en utilisant des distributions Dirichlet comme priors conjugues.

#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 la visualisation des factor graphs
#load "FactorGraphHelper.cs"

Console.WriteLine("FactorGraphHelper charge.");
Console.WriteLine($"Graphviz disponible : {FactorGraphHelper.IsGraphvizAvailable()}");
FactorGraphHelper charge.
Graphviz disponible : True

Helper pour la visualisation des factor graphs

Le FactorGraphHelper permet d’afficher les graphes de facteurs generes par Infer.NET directement dans le notebook. Il utilise Graphviz pour convertir les fichiers .gv en images SVG.

Fonctions principales : - GetLatestFactorGraphHtml() : Retourne le HTML du dernier graphe genere - ConfigureEngine(engine) : Configure un moteur pour generer des graphes - IsGraphvizAvailable() : Verifie si Graphviz est installe

Environnement pret

Packages charges : Microsoft.ML.Probabilistic et son compilateur

Namespace Rôle dans LDA
Distributions Dirichlet, Discrete pour les melanges
Models Variable, VariableArray, Range pour la structure
Algorithms VariationalMessagePassing (VMP)

Note : Infer.NET utilise VMP (Variational Message Passing) par defaut pour LDA, car l’inference exacte est intractable pour les modèles avec variables latentes discretes.

2. Introduction au Topic Modeling

Problème

Etant donne un corpus de documents, decouvrir les thèmes latents (topics) et la composition de chaque document.

Applications

  • Organisation automatique de documents
  • Recommandation de contenu
  • Analyse de tendances
  • Recherche sémantique

Representation Bag-of-Words

Un document est represente par le compte de chaque mot, ignorant l’ordre.

"Le chat mange la souris" -> {le: 1, chat: 1, mange: 1, la: 1, souris: 1}

Contexte historique

LDA (Latent Dirichlet Allocation) a ete introduit par David Blei, Andrew Ng et Michael Jordan en 2003. C’est l’un des modèles de topic modeling les plus influents.

Annee Développement
2003 Publication originale de LDA (Blei, Ng, Jordan)
2006 Correlated Topic Model (CTM)
2006 Hierarchical Dirichlet Process (HDP) - Teh et al.
2007 Online LDA pour corpus massifs
2010+ Integration avec deep learning (Topic-RNN, etc.)

Pourquoi LDA a revolutionne le domaine :

  1. Interpretabilite : Les topics sont des distributions sur des mots humainement lisibles
  2. Fondements bayesiens : Gestion naturelle de l’incertitude
  3. Scalabilite : Algorithmes d’inference efficaces (variationnel, Gibbs)
  4. Extensibilite : Base pour des centaines de variantes

3. Structure LDA

Modèle generatif

Pour chaque document d : 1. Tirer la distribution de topics : \(\theta_d \sim \text{Dirichlet}(\alpha)\) 2. Pour chaque mot w dans d : - Tirer un topic : \(z \sim \text{Discrete}(\theta_d)\) - Tirer un mot : \(w \sim \text{Discrete}(\phi_z)\)

Pour chaque topic k : - \(\phi_k \sim \text{Dirichlet}(\beta)\) : distribution sur le vocabulaire

Schema

                alpha
                  |
                  v
              theta[d]     (distribution topics par document)
                  |
                  v
               z[d,n]      (topic du mot n dans doc d)
                  |
                  v
               w[d,n]  <-- phi[z]  <-- beta
          (mot observe)   (dist mots par topic)

Références canoniques

Le modèle LDA est le topic model fondateur du domaine :

  • Blei, D. M., Ng, A. Y., & Jordan, M. I. (2003). Latent Dirichlet Allocation. Journal of Machine Learning Research, 3, 993-1022. Le papier fondateur de LDA — définit le modèle génératif documents-topics-mots, dérive l’inférence variationnelle, démontre l’application à de grands corpus (textes scientifiques, news).
  • MBML — How to Read a Model → sub-page ModelAnalysis_Latent_Dirichlet_Allocation.html. Winn, J., Bishop, C. M., & Diethe, T. Analyse d’un modèle LDA complet avec Infer.NET-like factor graphs discrets — la sous-page du Ch.8 How to Read a Model qui illustre structurellement le même paradigme (le notebook Ch.8 du MBML est un interlude et non un chapitre numéroté ; la sub-page est l’unité canonique pour LDA dans MBML).
  • Pritchard, J. K., Stephens, M., & Donnelly, P. (2000). Inference of population structure using multilocus genotype data. Genetics, 155(2), 945-959. L’ancêtre population-genetics du modèle à mélange de Dirichlet — application à la structuration de populations, formalise le prior Dirichlet sur les proportions de mélange.
  • Wang, X., & Grimson, E. (2007). Spatial Latent Dirichlet Allocation. NIPS 2007. Extension spatiale de LDA (modélisation de topics avec dépendance géographique) — exemple d’extension structurellement compatible avec le framework Dirichlet-Multinomial.

Le notebook couvre la chaîne canonique Dirichlet → Multinomial (topic z puis mot w) telle que définie par Blei, Ng & Jordan 2003 ; la sub-page MBML ci-dessus propose une lecture pédagogique équivalente. C’est la correction factuelle n°2 du mapping fondateur #8087 (« MBML Chap.10 » est incorrect — MBML n’a pas de Chap.10 ; la référence canonique est la sub-page Ch.8 How to Read a Model). Cf. sub-issue #8205.

Intuition mathematique de LDA

Le prior Dirichlet : Pour comprendre LDA, il faut d’abord comprendre la distribution Dirichlet.

\[\text{Dirichlet}(\alpha_1, ..., \alpha_K) \propto \prod_{k=1}^{K} x_k^{\alpha_k - 1}\]

Effet du paramètre alpha :

Valeur de \(\alpha\) Effet sur \(\theta\) Interpretation
\(\alpha < 1\) Sparse (proche des coins) Documents mono-thematiques
\(\alpha = 1\) Uniforme sur le simplexe Aucune préférence
\(\alpha > 1\) Dense (proche du centre) Documents multi-thematiques

Conjugaison : La beaute de LDA est que Dirichlet est le prior conjugue de la loi categorique (Discrete). Cela permet une inference efficace :

\[P(\theta \mid \text{mots}) = \text{Dirichlet}(\alpha + \text{comptes})\]

Rappel : Un prior conjugue donne un posterior de la même famille que le prior, simplifiant considerablement les calculs.

4. Implementation LDA Simplifiee

Specificites de l’implementation Infer.NET

Pourquoi VMP pour LDA ?

Infer.NET utilise Variational Message Passing (VMP) qui approxime la distribution posterieure par une famille factorisee :

\[q(\theta, z, \phi) \approx q(\theta) \prod_n q(z_n) \prod_k q(\phi_k)\]

Algorithme Avantages Inconvenients
VMP (utilise ici) Rapide, déterministe Mode local, symetrie
EP Meilleure approximation Plus lent, moins stable
Gibbs Sampling Explore tout l’espace Très lent, diagnostic difficile

Points d’attention pour Infer.NET :

  1. SetValueRange() est obligatoire pour Variable.Switch() - sinon erreur de compilation
  2. Variable.ForEach() créé une boucle de plaque implicite
  3. Les priors Dirichlet doivent avoir des valeurs > 0 pour eviter des NaN

Astuce : Pour diagnostiquer les problemes de convergence, activez moteur.ShowMslMessages = true pour voir les messages VMP.

// Donnees : corpus synthetique
// Vocabulaire : [sport, equipe, match, politique, election, vote, musique, concert, artiste]
string[] vocabulaire = { "sport", "equipe", "match", "politique", "election", "vote", "musique", "concert", "artiste" };
int vocabSize = vocabulaire.Length;
int numTopics = 3;  // Sport, Politique, Musique

// Documents (indices des mots)
int[][] documents = {
    new[] { 0, 1, 2, 0, 2 },           // Doc 1 : sport
    new[] { 3, 4, 5, 4, 3 },           // Doc 2 : politique
    new[] { 6, 7, 8, 7, 6 },           // Doc 3 : musique
    new[] { 0, 1, 3, 4, 0 },           // Doc 4 : sport + politique
    new[] { 6, 7, 0, 1, 8 },           // Doc 5 : musique + sport
    new[] { 2, 2, 1, 0, 2 },           // Doc 6 : sport
    new[] { 2, 2, 5, 2, 4 }            // Doc 7 : politique via "match" (polysemique) + vote/election
};

int numDocs = documents.Length;

Console.WriteLine("=== Corpus ===");
for (int d = 0; d < numDocs; d++)
{
    string mots = string.Join(", ", documents[d].Select(i => vocabulaire[i]));
    Console.WriteLine($"Doc {d+1} : {mots}");
}
=== Corpus ===
Doc 1 : sport, equipe, match, sport, match
Doc 2 : politique, election, vote, election, politique
Doc 3 : musique, concert, artiste, concert, musique
Doc 4 : sport, equipe, politique, election, sport
Doc 5 : musique, concert, sport, equipe, artiste
Doc 6 : match, match, equipe, sport, match
Doc 7 : match, match, vote, match, election

Analyse du corpus synthetique

Structure du vocabulaire : 9 mots repartis en 3 groupes thematiques

Topic Indices Mots
Sport 0, 1, 2 sport, équipe, match
Politique 3, 4, 5 politique, election, vote
Musique 6, 7, 8 musique, concert, artiste

Types de documents :

Document Type Description
Doc 1, 2, 3, 6 Pur Un seul topic dominant
Doc 7 Ambigu “match” (polysemique Sport/Politique) en contexte politique
Doc 4, 5 Mixte Melange de deux topics

Note methodologique : Ce corpus introduit deliberement un mot polysemique : “match” (indice 2) appartient au Sport (un match de foot) ET a la Politique (un match electoral). Le Doc 7 melange 3 occurrences de “match” avec “vote” et “election”. Cette ambiguite est exactement ce que le comptage naif (section 5) ne sait pas gerer, mais que l’inference jointe LDA (section 4bis) resout grace a la co-occurrence.

// Modele LDA simplifie (pour un seul document)

// Prior sur les topics (symetrique)
double[] alphaPrior = Enumerable.Repeat(1.0, numTopics).ToArray();

// Prior sur les mots par topic (symetrique)
double[] betaPrior = Enumerable.Repeat(1.0, vocabSize).ToArray();

// Distribution des mots par topic (phi)
Range topicRange = new Range(numTopics).Named("topic");
Range vocabRange = new Range(vocabSize).Named("vocab");

VariableArray<Vector> phi = Variable.Array<Vector>(topicRange).Named("phi");
phi[topicRange] = Variable.Dirichlet(betaPrior).ForEach(topicRange);

Console.WriteLine("Variables LDA definies.");
Console.WriteLine($"  Nombre de topics : {numTopics}");
Console.WriteLine($"  Taille vocabulaire : {vocabSize}");
Variables LDA definies.
  Nombre de topics : 3
  Taille vocabulaire : 9

Structure du modèle LDA dans Infer.NET

Variables définies :

Variable Type Rôle
phi[k] Vector (Dirichlet) Distribution des mots pour le topic k
topicRange Range(3) Indexation des 3 topics
vocabRange Range(9) Indexation des 9 mots du vocabulaire

Prior Dirichlet symetrique :

Le prior betaPrior = [1, 1, 1, 1, 1, 1, 1, 1, 1] est un Dirichlet(1,…,1) qui correspond a une distribution uniforme sur le simplexe. Cela signifie que chaque mot a la même probabilite a priori pour chaque topic.

\[\phi_k \sim \text{Dirichlet}(\beta) \quad \text{avec} \quad \beta = (1, 1, ..., 1)\]

Attention : Ce prior symetrique pose un problème d’identifiabilite que nous verrons dans la cellule suivante.

// Inference pour un document
int docIndex = 0;  // Premier document (sport)
int[] docWords = documents[docIndex];
int numWordsInDoc = docWords.Length;

// Distribution de topics pour ce document
Variable<Vector> theta = Variable.Dirichlet(alphaPrior).Named("theta");

Range wordRange = new Range(numWordsInDoc).Named("word");
VariableArray<int> wordObs = Variable.Array<int>(wordRange).Named("wordObs");
VariableArray<int> topicAssign = Variable.Array<int>(wordRange).Named("topicAssign");

// IMPORTANT: SetValueRange est necessaire pour utiliser Variable.Switch()
topicAssign.SetValueRange(topicRange);

using (Variable.ForEach(wordRange))
{
    topicAssign[wordRange] = Variable.Discrete(theta);
    using (Variable.Switch(topicAssign[wordRange]))
    {
        wordObs[wordRange] = Variable.Discrete(phi[topicAssign[wordRange]]);
    }
}

wordObs.ObservedValue = docWords;

InferenceEngine moteurLDA = new InferenceEngine(new VariationalMessagePassing());
moteurLDA.Compiler.CompilerChoice = CompilerChoice.Roslyn;
moteurLDA.ShowFactorGraph = true;  // Activer la generation du factor graph

Dirichlet thetaPost = moteurLDA.Infer<Dirichlet>(theta);

Console.WriteLine($"\n=== Inference pour Doc {docIndex + 1} ===");
Console.WriteLine($"Mots : {string.Join(", ", docWords.Select(i => vocabulaire[i]))}\n");
Console.WriteLine($"Distribution de topics (theta) :");
Vector thetaMean = thetaPost.GetMean();
for (int k = 0; k < numTopics; k++)
{
    Console.WriteLine($"  Topic {k+1} : {thetaMean[k]:F3}");
}
Compiling model...done.
Iterating: 
.........|.........|.........|.........|.........| 50

=== Inference pour Doc 1 ===
Mots : sport, equipe, match, sport, match

Distribution de topics (theta) :
  Topic 1 : 0,333
  Topic 2 : 0,333
  Topic 3 : 0,333

Analyse des résultats LDA (Document 1) - Problème de Symétrie

Résultats observés : Distribution uniforme (0.333 par topic)

Observation Explication
Topics équiprobables Le modèle converge vers un mode local symétrique
Cause principale Les distributions \(\phi\) (mots par topic) ont des priors symétriques
Prior Dirichlet(1,…,1) N’encode aucune préférence entre topics

Pourquoi ce résultat ? Le problème de symétrie dans LDA

C’est un problème classique de LDA avec VMP (Variational Message Passing) :

  1. Symétrie initiale : Tous les topics sont interchangeables au départ
  2. Mode local : VMP converge vers le point-selle symétrique où tous les topics sont identiques
  3. Brisure de symétrie nécessaire : Il faut “guider” l’inférence vers des solutions distinctes

Solutions possibles : - Priors asymétriques sur \(\phi\) (le plus efficace) - Initialisation aléatoire des paramètres - Gibbs Sampling au lieu de VMP (explore mieux l’espace)

Note technique : VMP converge en 50 itérations mais vers une solution dégénérée - ce n’est pas un bug, c’est une propriété mathématique des méthodes variationnelles face à des modèles symétriques.

// Visualisation du factor graph LDA
display(HTML(FactorGraphHelper.GetLatestFactorGraphHtml()));
Model_08_26_26_10_56_04_02.svg
Model node0 vDirichlet1 node1 Random node0->node1 dist node2 theta node1->node2 node3 Discrete node2->node3 probs node4 topicAssign[word] node3->node4 node7 wordObs[word] node4->node7 condition node5 phi[topicAssign[word]] node6 Discrete node5->node6 probs node6->node7 node8 vDirichlet0 node9 Random node8->node9 dist node10 phi[topic] node9->node10 node10->node5

Interpretation du factor graph LDA

Le graphe de facteurs ci-dessus represente la structure du modèle LDA pour un seul document.

Composants du graphe :

Noeud Type Rôle dans LDA
theta Variable latente Distribution de topics du document (Dirichlet)
phi Paramètre Distribution des mots par topic (tableau de Dirichlet)
topicAssign Variable latente Assignation de topic pour chaque mot (Discrete)
wordObs Observation Mots observes dans le document

Structure hiérarchique :

theta (Dirichlet prior)
   |
   v
topicAssign[word] (Discrete)  <-- choix du topic pour chaque mot
   |
   v
wordObs[word] (Discrete)  <-- mot genere selon phi[topic]
   ^
   |
phi[topic] (Dirichlet prior)

Messages VMP : Les aretes representent les messages variationnels echanges entre facteurs. VMP itere jusqu’a convergence des paramètres de l’approximation \(q(\theta, z)\).

4bis. Solution : LDA avec Priors Asymétriques

Pour briser la symétrie, nous utilisons des priors Dirichlet asymétriques sur \(\phi\) (distribution des mots par topic). L’idée est d’encoder notre connaissance a priori que certains mots sont plus probables pour certains topics.

Stratégie de brisure de symétrie

Topic Sport :     beta = [10, 10, 10, 1, 1, 1, 1, 1, 1]  → favorise mots 0-2
Topic Politique : beta = [1, 1, 1, 10, 10, 10, 1, 1, 1]  → favorise mots 3-5
Topic Musique :   beta = [1, 1, 1, 1, 1, 1, 10, 10, 10]  → favorise mots 6-8

Ces priors ne sont pas arbitraires : ils encodent une hypothèse structurelle sur le vocabulaire. En pratique, on peut : - Utiliser des embeddings de mots pour initialiser - Faire une première passe de clustering - Encoder des connaissances du domaine

Exécution : Definition des priors asymetriques

Le code suivant définit la matrice de priors asymetriques. Observez comment chaque topic recoit un prior qui favorise un groupe de mots spécifique :

// LDA avec priors asymétriques pour briser la symétrie

// Priors asymétriques sur phi (mots par topic)
// Chaque topic a une "affinité" pour un groupe de mots
double[][] betaAsym = new double[][] {
    // Topic 0 (Sport) : favorise mots 0, 1, 2 (dont 'match' polysemique)
    new double[] { 10, 10, 10, 1, 1, 1, 1, 1, 1 },
    // Topic 1 (Politique) : favorise mots 3, 4, 5 ET 'match' (polysemique Sport/Politique)
    new double[] { 1, 1, 10, 10, 10, 10, 1, 1, 1 },
    // Topic 2 (Musique) : favorise mots 6, 7, 8
    new double[] { 1, 1, 1, 1, 1, 1, 10, 10, 10 }
};

// Redefinition du modele avec priors asymetriques
Range topicRangeAsym = new Range(numTopics).Named("topicAsym");
Range vocabRangeAsym = new Range(vocabSize).Named("vocabAsym");

VariableArray<Vector> phiAsym = Variable.Array<Vector>(topicRangeAsym).Named("phiAsym");

// Assigner des priors differents a chaque topic
for (int k = 0; k < numTopics; k++)
{
    phiAsym[k] = Variable.Dirichlet(betaAsym[k]);
}

Console.WriteLine("=== LDA avec Priors Asymétriques ===");
Console.WriteLine("\nPriors sur phi (log-echelle relative) :");
for (int k = 0; k < numTopics; k++)
{
    var topMots = betaAsym[k]
        .Select((b, i) => (mot: vocabulaire[i], beta: b))
        .Where(x => x.beta > 1)
        .ToList();
    Console.WriteLine($"  Topic {k} : {string.Join(", ", topMots.Select(x => x.mot))} (beta=10)");}
Console.WriteLine();
=== LDA avec Priors Asymétriques ===

Priors sur phi (log-echelle relative) :
  Topic 0 : sport, equipe, match (beta=10)
  Topic 1 : match, politique, election, vote (beta=10)
  Topic 2 : musique, concert, artiste (beta=10)

Exécution : Inference LDA sur plusieurs documents

Maintenant que les priors asymetriques sont définis, nous allons tester l’inference sur 5 documents : 3 documents “purs” (un seul topic) et 2 documents “mixtes” (melange de topics).

Documents testes :

Document Contenu attendu Type
Doc 1 Sport Pur
Doc 2 Politique Pur
Doc 3 Musique Pur
Doc 4 Sport + Politique Mixte
Doc 5 Musique + Sport Mixte
// Inference LDA avec priors asymetriques sur plusieurs documents

Console.WriteLine("=== Inference LDA Corrigee ===\n");

// On teste sur les 3 premiers documents (1 par topic)
int[] testDocs = { 0, 1, 2, 3, 4, 6 };  // Sport, Politique, Musique, mixtes, +Doc 7 (politique ambigu 'match')

foreach (int docIdx in testDocs)
{
    int[] dWords = documents[docIdx];
    int nWords = dWords.Length;
    
    // Nouveau modele pour ce document
    Variable<Vector> thetaDoc = Variable.Dirichlet(alphaPrior).Named($"theta_{docIdx}");
    
    Range wRange = new Range(nWords).Named($"word_{docIdx}");
    VariableArray<int> wordsDoc = Variable.Array<int>(wRange).Named($"words_{docIdx}");
    VariableArray<int> topicsDoc = Variable.Array<int>(wRange).Named($"topics_{docIdx}");
    
    topicsDoc.SetValueRange(topicRangeAsym);
    
    using (Variable.ForEach(wRange))
    {
        topicsDoc[wRange] = Variable.Discrete(thetaDoc);
        using (Variable.Switch(topicsDoc[wRange]))
        {
            wordsDoc[wRange] = Variable.Discrete(phiAsym[topicsDoc[wRange]]);
        }
    }
    
    wordsDoc.ObservedValue = dWords;
    
    // Inference
    InferenceEngine moteurLDAAsym = new InferenceEngine(new VariationalMessagePassing());
    moteurLDAAsym.Compiler.CompilerChoice = CompilerChoice.Roslyn;
    moteurLDAAsym.ShowFactorGraph = true;  // Activer la generation du factor graph
    moteurLDAAsym.ShowProgress = false;
    
    Dirichlet thetaPostAsym = moteurLDAAsym.Infer<Dirichlet>(thetaDoc);
    Vector thetaMeanAsym = thetaPostAsym.GetMean();
    
    // Affichage
    string motsStr = string.Join(", ", dWords.Select(i => vocabulaire[i]));
    Console.WriteLine($"Doc {docIdx + 1} : {motsStr}");
    Console.WriteLine($"  Theta : Sport={thetaMeanAsym[0]:F3}, Politique={thetaMeanAsym[1]:F3}, Musique={thetaMeanAsym[2]:F3}");
    
    // Topic dominant
    int topicDom = thetaMeanAsym[0] > thetaMeanAsym[1] && thetaMeanAsym[0] > thetaMeanAsym[2] ? 0 :
                   thetaMeanAsym[1] > thetaMeanAsym[2] ? 1 : 2;
    string[] topicLabels = { "Sport", "Politique", "Musique" };
    Console.WriteLine($"  Topic dominant : {topicLabels[topicDom]} ({thetaMeanAsym[topicDom]:P0})");
    Console.WriteLine();
}
=== Inference LDA Corrigee ===

Doc 1 : sport, equipe, match, sport, match
  Theta : Sport=0,718, Politique=0,153, Musique=0,129
  Topic dominant : Sport (72 %)

Doc 2 : politique, election, vote, election, politique
  Theta : Sport=0,129, Politique=0,741, Musique=0,130
  Topic dominant : Politique (74 %)

Doc 3 : musique, concert, artiste, concert, musique
  Theta : Sport=0,128, Politique=0,128, Musique=0,744
  Topic dominant : Musique (74 %)

Doc 4 : sport, equipe, politique, election, sport
  Theta : Sport=0,508, Politique=0,360, Musique=0,131
  Topic dominant : Sport (51 %)

Doc 5 : musique, concert, sport, equipe, artiste
  Theta : Sport=0,368, Politique=0,130, Musique=0,502
  Topic dominant : Musique (50 %)

Doc 7 : match, match, vote, match, election
  Theta : Sport=0,226, Politique=0,645, Musique=0,129
  Topic dominant : Politique (65 %)

Analyse : Résultats avec Priors Asymétriques

Amélioration observée : Les topics sont maintenant correctement identifiés !

Document Mots Topic attendu Résultat
Doc 1 sport, équipe, match Sport Sport ~72%
Doc 2 politique, election, vote Politique Politique ~74%
Doc 3 musique, concert, artiste Musique Musique ~74%
Doc 4 sport + politique Mixte Sport ~51%, Politique ~36%
Doc 5 musique + sport Mixte Musique ~50%, Sport ~37%

Pourquoi ça fonctionne maintenant ?

  1. Brisure de symétrie : Les priors asymétriques créent des “bassins d’attraction” distincts
  2. Vraisemblance dirigée : Un mot “sport” a 10× plus de chances sous Topic 0
  3. Inférence correcte : VMP converge vers le mode correspondant aux données

Comparaison avant/après :

Aspect Prior symétrique Prior asymétrique
Theta Doc 1 (0.33, 0.33, 0.33) (~0.72, ~0.15, ~0.13)
Mode Symétrique (dégénéré) Correct
Utilité Aucune Classification fonctionnelle

Note : Cette approche est “semi-supervisée” car les priors encodent une connaissance préalable. Un LDA purement non-supervisé nécessiterait des techniques plus avancées (initialisation aléatoire multiple, collapsed Gibbs sampling).

// Visualisation du factor graph LDA avec priors asymetriques
display(HTML(FactorGraphHelper.GetLatestFactorGraphHtml()));
Model_08_26_26_10_56_11_43.svg
Model node0 vDirichlet10 node1 Random node0->node1 dist node2 theta_6 node1->node2 node3 Discrete node2->node3 probs node4 topics_6[word_6] node3->node4 node7 words_6[word_6] node4->node7 condition node5 phiAsym[topics_6[word_6]] node6 Discrete node5->node6 probs node6->node7 node8 phiAsym[topics_0[word_0]] node9 Discrete node8->node9 probs node10 words_0[word_0] node9->node10 node11 topics_0[word_0] node11->node10 condition node12 phiAsym[topics_1[word_1]] node13 Discrete node12->node13 probs node14 words_1[word_1] node13->node14 node15 topics_1[word_1] node15->node14 condition node16 phiAsym[topics_2[word_2]] node17 Discrete node16->node17 probs node18 words_2[word_2] node17->node18 node19 topics_2[word_2] node19->node18 condition node20 phiAsym[topics_3[word_3]] node21 Discrete node20->node21 probs node22 words_3[word_3] node21->node22 node23 topics_3[word_3] node23->node22 condition node24 phiAsym[topics_4[word_4]] node25 Discrete node24->node25 probs node26 words_4[word_4] node25->node26 node27 topics_4[word_4] node27->node26 condition node28 theta_4 node29 Discrete node28->node29 probs node29->node27 node30 vDirichlet9 node31 Random node30->node31 dist node31->node28 node32 theta_3 node33 Discrete node32->node33 probs node33->node23 node34 vDirichlet8 node35 Random node34->node35 dist node35->node32 node36 theta_2 node37 Discrete node36->node37 probs node37->node19 node38 vDirichlet7 node39 Random node38->node39 dist node39->node36 node40 theta_1 node41 Discrete node40->node41 probs node41->node15 node42 vDirichlet6 node43 Random node42->node43 dist node43->node40 node44 theta_0 node45 Discrete node44->node45 probs node45->node11 node46 vDirichlet5 node47 Random node46->node47 dist node47->node44 node48 vDirichlet4 node49 Random node48->node49 dist node50 phiAsym[vint14] node49->node50 node50->node5 node50->node8 node50->node12 node50->node16 node50->node20 node50->node24 node53 phiAsym[vint13] node50->node53 node56 phiAsym[vint12] node50->node56 node51 vDirichlet3 node52 Random node51->node52 dist node52->node53 node53->node5 node53->node8 node53->node12 node53->node16 node53->node20 node53->node24 node53->node50 node53->node56 node54 vDirichlet2 node55 Random node54->node55 dist node55->node56 node56->node5 node56->node8 node56->node12 node56->node16 node56->node20 node56->node24 node56->node50 node56->node53

Interpretation du factor graph LDA avec priors asymetriques

Le graphe ci-dessus montre la structure du modèle LDA avec priors asymetriques sur les distributions de mots par topic.

Différence cle avec le modèle précédent :

Aspect Prior symetrique Prior asymetrique
phi[k] Dirichlet(1,…,1) identique Dirichlet(betaAsym[k]) différent par topic
Brisure de symetrie Non Oui
Convergence VMP Mode degenere Mode informatif

Structure du graphe :

Les noeuds phiAsym[k] ont maintenant des priors différents pour chaque topic k : - Topic 0 (Sport) : prior fort sur mots 0-2 - Topic 1 (Politique) : prior fort sur mots 3-5
- Topic 2 (Musique) : prior fort sur mots 6-8

Impact sur l’inference :

L’asymetrie des priors créé des “bassins d’attraction” distincts dans l’espace des paramètres, permettant a VMP de converger vers des solutions interpretables plutot que vers le point-selle symetrique.

Note technique : Le graphe genere correspond au dernier document traite dans la boucle. La structure est identique pour tous les documents, seules les observations (wordsDoc) changent.

5. LDA sur Corpus Complet

Approche simplifiee : comptage par catégorie

Avant d’utiliser l’inference probabiliste complete, nous pouvons obtenir une première approximation des compositions de topics par comptage direct des mots appartenant a chaque catégorie thematique.

Cette approche exploite la structure connue du vocabulaire : - Indices 0-2 : mots de Sport - Indices 3-5 : mots de Politique - Indices 6-8 : mots de Musique

Limitation : Cette méthode ne capture pas l’incertitude ni les correlations entre mots, et echoue sur les mots polysemiques. Sur le Doc 7, le comptage classe “match” en Sport (indice <= 2) alors que le document est politique (vote, election). L’inference probabiliste jointe est necessaire pour resoudre cette ambiguite via la co-occurrence.

// Inference simplifiee document par document

Console.WriteLine("=== LDA sur corpus complet ===");
Console.WriteLine();

// Distribution des mots par topic (initialisation supervisee pour demo)
// Topic 0 : Sport (mots 0, 1, 2)
// Topic 1 : Politique (mots 3, 4, 5)
// Topic 2 : Musique (mots 6, 7, 8)

for (int d = 0; d < numDocs; d++)
{
    int[] dWords = documents[d];
    int nWords = dWords.Length;
    
    // Comptage simple des mots par categorie
    int countSport = dWords.Count(w => w <= 2);
    int countPolitique = dWords.Count(w => w >= 3 && w <= 5);
    int countMusique = dWords.Count(w => w >= 6);
    double total = countSport + countPolitique + countMusique + 0.001;
    
    Console.WriteLine($"Doc {d+1} : Sport={countSport/total:F2}, Politique={countPolitique/total:F2}, Musique={countMusique/total:F2}");
}

Console.WriteLine("\n(Proportions basees sur les mots observes)");
=== LDA sur corpus complet ===

Doc 1 : Sport=1,00, Politique=0,00, Musique=0,00
Doc 2 : Sport=0,00, Politique=1,00, Musique=0,00
Doc 3 : Sport=0,00, Politique=0,00, Musique=1,00
Doc 4 : Sport=0,60, Politique=0,40, Musique=0,00
Doc 5 : Sport=0,40, Politique=0,00, Musique=0,60
Doc 6 : Sport=1,00, Politique=0,00, Musique=0,00
Doc 7 : Sport=0,60, Politique=0,40, Musique=0,00

(Proportions basees sur les mots observes)

Analyse des compositions de topics

Résultats : Classification basée sur le comptage — correcte, sauf sur le mot polysémique

Document Topics détectés Observation
Doc 1, 6 Sport = 100% Documents thématiques purs
Doc 2 Politique = 100% Document thématique pur
Doc 7 Sport = 60% (erreur) Comptage trompé par « match » polysémique (doc en réalité politique)
Doc 3 Musique = 100% Document thématique pur
Doc 4 Sport 60% / Politique 40% Mélange détecté
Doc 5 Musique 60% / Sport 40% Mélange détecté

Observations clés :

  1. Documents purs : La séparation du vocabulaire permet une classification triviale — sauf sur le mot polysémique « match » (Doc 7), où le comptage se trompe

  2. Documents mixtes : Les proportions reflètent directement le comptage (Doc 4 : 3 mots sport, 2 mots politique → 60/40)

  3. Limitation : Cette approche de comptage ne capture pas les co-occurrences ni les corrélations sémantiques entre mots

Contraste avec l’inférence LDA : l’inférence jointe de la section précédente (cellule d’inference à priors asymétriques) récupère correctement le Doc 7 comme Politique (≈ 65 %) en exploitant la co-occurrence de « match » avec « vote » et « election » — là où le comptage échoue (Sport 60 %). C’est toute la valeur de l’inférence probabiliste sur un vocabulaire non parfaitement partitionné.

6. Visualisation des Topics

Visualisation de la matrice phi

Pour comprendre ce que chaque topic a “appris”, nous visualisons la distribution \(\phi_k\) des mots pour chaque topic. Les mots les plus probables definissent l’interpretation sémantique du topic.

En pratique, on affiche les top-N mots par topic pour faciliter l’interpretation humaine.

#load "SvgChartHelper.cs"

// Distribution phi (mots par topic) - simulee

double[,] phiSimule = {
    // Topic Sport
    { 0.30, 0.30, 0.30, 0.02, 0.02, 0.02, 0.02, 0.01, 0.01 },
    // Topic Politique
    { 0.02, 0.02, 0.02, 0.30, 0.30, 0.30, 0.02, 0.01, 0.01 },
    // Topic Musique
    { 0.02, 0.01, 0.01, 0.02, 0.02, 0.02, 0.30, 0.30, 0.30 }
};

string[] topicNames = { "Sport", "Politique", "Musique" };

Console.WriteLine("=== Distribution Mots par Topic (phi) ===");
Console.WriteLine();

for (int k = 0; k < numTopics; k++)
{
    Console.WriteLine($"Topic {k+1} ({topicNames[k]}) :");

    // Top mots
    var topMots = Enumerable.Range(0, vocabSize)
        .Select(v => (mot: vocabulaire[v], prob: phiSimule[k, v]))
        .OrderByDescending(x => x.prob)
        .Take(3);

    foreach (var (mot, prob) in topMots)
    {
        Console.WriteLine($"  {mot,-12} : {prob:F2}");
    }
    Console.WriteLine();
}

// --- Visualisation SVG inline : distributions mots par topic (matrice phi 3x9) ---
// La heatmap Plotly-CDN precedente rendait BLANC en static (GitHub sandbox les <script src=cdn.plot.ly>).
// Technique canonique : SVG inline via SvgChartHelper.cs (#6942 MERGED), zero-dependance NuGet.
// On deploie 3 Bar() distincts (un par topic) montrant la distribution P(mot | topic) sur le vocabulaire.
// Le pattern bloc-diagonal (3 mots a 0.30 par topic) devient visible : chaque Bar() met en evidence
// le top-3 mots du topic via une legende textuelle ci-dessous.

var vocab = vocabulaire;
{
    // Topic Sport (index 0)
    var ySport = new double[vocabSize];
    for (int v = 0; v < vocabSize; v++) ySport[v] = phiSimule[0, v];
    display(SvgChartHelper.Bar("P(mot | Topic=Sport) - 3 mots dominants a 0.30", vocab, ySport));
    Console.WriteLine("  [Sport] pic sur les 3 premiers mots (0.30) puis plateau quasi-nul (0.01-0.02).");

    // Topic Politique (index 1)
    var yPolitique = new double[vocabSize];
    for (int v = 0; v < vocabSize; v++) yPolitique[v] = phiSimule[1, v];
    display(SvgChartHelper.Bar("P(mot | Topic=Politique) - 3 mots dominants a 0.30", vocab, yPolitique));
    Console.WriteLine("  [Politique] bloc-diagonal decale : mots 4-6 (indices 3-5) a 0.30.");

    // Topic Musique (index 2)
    var yMusique = new double[vocabSize];
    for (int v = 0; v < vocabSize; v++) yMusique[v] = phiSimule[2, v];
    display(SvgChartHelper.Bar("P(mot | Topic=Musique) - 3 mots dominants a 0.30", vocab, yMusique));
    Console.WriteLine("  [Musique] bloc-diagonal en queue : mots 7-9 (indices 6-8) a 0.30.");
}
=== Distribution Mots par Topic (phi) ===

Topic 1 (Sport) :
  sport        : 0,30
  equipe       : 0,30
  match        : 0,30

Topic 2 (Politique) :
  politique    : 0,30
  election     : 0,30
  vote         : 0,30

Topic 3 (Musique) :
  musique      : 0,30
  concert      : 0,30
  artiste      : 0,30
P(mot | Topic=Sport) - 3 mots dominants a 0.3000.0810.1620.2430.324sportequipematchpolitiqueelectionvotemusiqueconcertartiste
  [Sport] pic sur les 3 premiers mots (0.30) puis plateau quasi-nul (0.01-0.02).
P(mot | Topic=Politique) - 3 mots dominants a 0.3000.0810.1620.2430.324sportequipematchpolitiqueelectionvotemusiqueconcertartiste
  [Politique] bloc-diagonal decale : mots 4-6 (indices 3-5) a 0.30.
P(mot | Topic=Musique) - 3 mots dominants a 0.3000.0810.1620.2430.324sportequipematchpolitiqueelectionvotemusiqueconcertartiste
  [Musique] bloc-diagonal en queue : mots 7-9 (indices 6-8) a 0.30.

Interpretation de la matrice phi

Structure de la distribution mots/topics :

La matrice \(\phi\) de dimension \((K \times V) = (3 \times 9)\) encode la probabilite de chaque mot sachant le topic.

\[\phi_{k,v} = P(\text{mot} = v \mid \text{topic} = k)\]

Proprietes observees :

Propriete Valeur Interpretation
Prob. mots dominants 0.30 Forte association mot-topic
Prob. mots hors-topic 0.01-0.02 Faible bruit de fond
Somme par topic 1.0 Distribution normalisee
Entropie par topic Faible Topics bien separes

Visualisation mentale :

           sport équipe match polit elect vote  musiq conc  artis
Topic 0:   ████  ████   ████  ░     ░     ░     ░     ░     ░
Topic 1:   ░     ░      ░     ████  ████  ████  ░     ░     ░
Topic 2:   ░     ░      ░     ░     ░     ░     ████  ████  ████

Note : En pratique, les distributions phi sont apprises par inference et montrent souvent des chevauchements plus subtils entre topics.

7. Prediction sur Nouveaux Documents

Inference sur un nouveau document

L’objectif de LDA n’est pas seulement d’analyser le corpus d’entrainement, mais aussi de predire la composition thematique de nouveaux documents jamais vus.

Méthode : Pour un nouveau document, on calcule la vraisemblance de chaque topic en utilisant les distributions \(\phi\) apprises, puis on normalise pour obtenir des probabilites.

\[P(\text{topic} = k \mid \text{document}) \propto \prod_{w \in \text{doc}} \phi_{k,w}\]

Document test : Un melange de mots “sport” et “musique” pour voir comment le modèle gere les documents multi-thematiques.

// Prediction de topics pour un nouveau document

int[] nouveauDoc = { 0, 1, 6, 7, 0 };  // Sport + Musique
Console.WriteLine("=== Prediction Nouveau Document ===");
Console.WriteLine($"Mots : {string.Join(", ", nouveauDoc.Select(i => vocabulaire[i]))}");

// Calcul des vraisemblances par topic
double[] logLik = new double[numTopics];

for (int k = 0; k < numTopics; k++)
{
    logLik[k] = 0;
    foreach (int w in nouveauDoc)
    {
        logLik[k] += Math.Log(phiSimule[k, w] + 1e-10);
    }
}

// Normalisation (softmax)
double maxLogLik = logLik.Max();
double[] lik = logLik.Select(ll => Math.Exp(ll - maxLogLik)).ToArray();
double sumLik = lik.Sum();
double[] topicProbs = lik.Select(l => l / sumLik).ToArray();

Console.WriteLine("\nProbabilites de topics :");
for (int k = 0; k < numTopics; k++)
{
    Console.WriteLine($"  {topicNames[k],-12} : {topicProbs[k]:F3}");
}
=== Prediction Nouveau Document ===
Mots : sport, equipe, musique, concert, sport

Probabilites de topics :
  Sport        : 0,937
  Politique    : 0,000
  Musique      : 0,062

Analyse de la prédiction

Document testé : “sport, équipe, musique, concert, sport” (2 mots musique, 3 mots sport)

Résultat : Sport = 93.7%, Musique = 6.2%, Politique ≈ 0%

Aspect Observation
Dominance Sport 3 mots sur 5 = 60%, mais probabilité 93.7%
Sous-estimation Musique 2 mots sur 5 = 40%, mais probabilité 6.2%
Politique éliminée Aucun mot du topic → probabilité ~0

Explication de l’asymétrie :

Le calcul utilise la vraisemblance (produit des probabilités) : - Sport : \(0.30^3 \times 0.02^2 = 1.08 \times 10^{-5}\) - Musique : \(0.02^3 \times 0.30^2 = 7.2 \times 10^{-7}\) - Ratio : Sport est ~15× plus probable que Musique

Effet de la vraisemblance : Le modèle pénalise fortement les mots “hors topic” (prob=0.02), ce qui amplifie la différence entre topics.

Note : C’est un comportement attendu des modèles génératifs - la vraisemblance capture plus que de simples proportions.

8. Extensions de LDA

Variantes

Modèle Description
HDP (Hierarchical Dirichlet Process) Nombre de topics appris automatiquement
Correlated Topic Model Correlations entre topics
Dynamic Topic Model Evolution des topics dans le temps
Supervised LDA Avec labels de documents

Comparaison des extensions de LDA

Extension Avantage principal Complexite Cas d’usage
LDA standard Simple, bien compris \(O(NKV)\) Corpus statiques
HDP Pas de K a specifier \(O(NK^2V)\) Exploration non-supervisee
CTM Topics correles \(O(NK^2 + NKV)\) Documents structurees
DTM Evolution temporelle \(O(TNK^2V)\) Flux de documents
sLDA Prediction supervisee \(O(NKV + NK)\) Classification

Formules des extensions :

  • HDP : \(G_0 \sim \text{DP}(\gamma, H)\), \(G_j \sim \text{DP}(\alpha, G_0)\) (processus de Dirichlet hiérarchique)
  • CTM : \(\eta_d \sim \mathcal{N}(\mu, \Sigma)\), \(\theta_d = \text{softmax}(\eta_d)\) (normale multivariee pour correlations)
  • DTM : \(\beta_{t,k} \sim \mathcal{N}(\beta_{t-1,k}, \sigma^2 I)\) (evolution gaussienne des topics)

Conseil pratique : Commencez toujours par LDA standard. N’utilisez les extensions que si les données montrent clairement des correlations temporelles ou entre topics.

8bis. Exemple guide : Selection du nombre de topics (K-selection)

Jusqu’ici, le numTopics du modele LDA etait choisi a la main. Cette section repond a la question : le modele peut-il choisir lui-meme sa granularite ? Non, et c’est le point central. Un modele de topics apprend phi et theta pour un K donne ; il ne fournit pas le niveau de granularite pertinent. Pour le montrer mesurablement, on construit un corpus controle dont la structure est connue (sans jamais l’utiliser pour apprendre) : 4 themes x 4 mots + 3 mots partages, 16 documents d’entrainement + 4 documents de validation. On ajuste le modele pour K = 2..6 et on compare trois metriques a posteriori :

  1. Cohérence UMass (top-5) - les tops mots de chaque topic co-occurrent-ils dans les documents ?
  2. Redondance Jensen-Shannon - les topics se recouvrent-ils (moyenne) ou un topic est-il fragmente (minimum) ?
  3. Completion held-out - perplexite sur la seconde moitie de documents, theta infere sur la premiere moitie.

// --- corpus controle : 4 themes x 4 mots + 3 mots partages (structure connue, jamais utilisee pour apprendre) ---
var randSel = new Random(42);
string[] vocabSel = {
    "football","equipe","match","competition",
    "ordinateur","logiciel","internet","application",
    "recette","ingredient","cuisson","gastronomie",
    "hotel","avion","destination","tourisme",
    "recherche","performance","preparation"     // 16-18 partages
};
int Vsel = vocabSel.Length;

double[][] phiGen = {
    new double[]{.24,.20,.18,.16,.01,.01,.01,.01,.01,.01,.01,.01,.01,.01,.01,.01,.05,.04,.03},
    new double[]{.01,.01,.01,.01,.24,.20,.18,.16,.01,.01,.01,.01,.01,.01,.01,.01,.03,.05,.04},
    new double[]{.01,.01,.01,.01,.01,.01,.01,.01,.24,.20,.18,.16,.01,.01,.01,.01,.04,.03,.06},
    new double[]{.01,.01,.01,.01,.01,.01,.01,.01,.01,.01,.01,.01,.24,.20,.18,.16,.03,.04,.05}
};
for (int k = 0; k < 4; k++) { double s = phiGen[k].Sum(); for (int v = 0; v < Vsel; v++) phiGen[k][v] /= s; }

double[][] thetaGen = new double[16][];
int diSel = 0;
for (int t = 0; t < 4; t++) for (int r = 0; r < 3; r++) {
    var v = Enumerable.Repeat(0.1, 4).ToArray(); v[t] = 0.7; thetaGen[diSel++] = v;
}
thetaGen[12] = new double[]{.4,.4,.1,.1};
thetaGen[13] = new double[]{.1,.4,.4,.1};
thetaGen[14] = new double[]{.4,.1,.4,.1};
thetaGen[15] = new double[]{.25,.25,.25,.25};
double[][] heldThetaGen = { new double[]{.7,.1,.1,.1}, new double[]{.1,.7,.1,.1},
                            new double[]{.1,.1,.7,.1}, new double[]{.4,.1,.1,.4} };
int docLen = 24;

static int SampleDiscrete(Random rng, double[] p) { double u = rng.NextDouble(); double c = 0; for (int i = 0; i < p.Length; i++) { c += p[i]; if (u < c) return i; } return p.Length - 1; }
int[] GenDoc(Random rng, double[] thetaT) =>
    Enumerable.Range(0, docLen).Select(n => phiGen[SampleDiscrete(rng, thetaT)])
              .Select(ph => SampleDiscrete(rng, ph)).ToArray();

int[][] trainSel = thetaGen.Select(t => GenDoc(randSel, t)).ToArray();
int[][] heldSel = heldThetaGen.Select(t => GenDoc(randSel, t)).ToArray();
Console.WriteLine($"train docs: {trainSel.Length}, held: {heldSel.Length}, V={Vsel}, L={docLen}");
train docs: 16, held: 4, V=19, L=24

Pourquoi un corpus controle ?

La structure generative (phiGen, thetaGen) est connue, mais n’est jamais injectee dans l’ajustement : les modeles K=2..6 apprennent uniquement sur les mots trainSel. La verite ne sert qu’a evaluer les resultats - exactement la posture d’evaluation des sections precedentes. C’est la seule facon de savoir quel K est « correct » sans dependre d’un regard humain subjectif.


// --- ajustement LDA complet pour un K donne (moteur VMP, phi ET theta appris) ---
(double[][] ph, double[][] th) FitLdaSel(int K) {
    int nDocs = trainSel.Length;
    var D = new Microsoft.ML.Probabilistic.Models.Range(nDocs).Named("D");
    var Z = new Microsoft.ML.Probabilistic.Models.Range(K).Named("Z");
    var Wrd = new Microsoft.ML.Probabilistic.Models.Range(Vsel).Named("Wrd");
    VariableArray<int> sizes = Variable.Constant(trainSel.Select(d => d.Length).ToArray(), D).Named("sizes");
    var N = new Microsoft.ML.Probabilistic.Models.Range(sizes[D]).Named("N");

    // VMP ne quitte pas le point symetrique (priors uniformes => gradient ELBO nul).
    // Perturbation deterministe seed-42, amplitude 1e-2 : brise la symetrie SANS
    // coder aucune connaissance des themes.
    double[] MakePrior(int n, int seed) { var r = new Random(seed);
        return Enumerable.Range(0, n).Select(_ => 0.7 + 0.02 * (r.NextDouble() - 0.5)).ToArray(); }

    var theta = Variable.Array<Vector>(D).Named("theta");
    theta[D] = Variable.Dirichlet(MakePrior(K, 42)).ForEach(D);
    var phi = Variable.Array<Vector>(Z).Named("phi");
    for (int k = 0; k < K; k++) phi[k] = Variable.Dirichlet(MakePrior(Vsel, 100 + k));

    var z = Variable.Array(Variable.Array<int>(N), D).Named("z");
    var w = Variable.Array(Variable.Array<int>(N), D).Named("w");
    using (Variable.ForEach(D)) {
        z[D].SetValueRange(Z);
        using (Variable.ForEach(N)) {
            z[D][N] = Variable.Discrete(theta[D]);
            using (Variable.Switch(z[D][N])) {
                w[D][N] = Variable.Discrete(phi[z[D][N]]);
            }
        }
    }
    w.ObservedValue = trainSel;

    var engine = new InferenceEngine(new VariationalMessagePassing()) {
        Compiler = { CompilerChoice = CompilerChoice.Roslyn }, ShowProgress = false
    };
    var phiPost = engine.Infer<Dirichlet[]>(phi);
    var thetaPost = engine.Infer<Dirichlet[]>(theta);
    return (phiPost.Select(d => d.GetMean().ToArray()).ToArray(),
            thetaPost.Select(d => d.GetMean().ToArray()).ToArray());
}

// --- coherence UMass top-5 sur les topics APPRIS (comptages train) ---
double UMassSel(double[][] ph, int top) {
    int[] Dw = Enumerable.Range(0, Vsel).Select(v => trainSel.Count(d => d.Contains(v))).ToArray();
    int[,] Co = new int[Vsel, Vsel];
    foreach (var d in trainSel) foreach (int i in d.Distinct()) foreach (int j in d.Distinct()) Co[i, j]++;
    var scores = new List<double>();
    foreach (var row in ph) {
        var tw = Enumerable.Range(0, Vsel).OrderByDescending(v => row[v]).Take(top).ToArray();
        double s = 0; int n = 0;
        for (int i = 0; i < tw.Length; i++) for (int j = i + 1; j < tw.Length; j++) {
            s += Math.Log((Co[tw[i], tw[j]] + 1.0) / Dw[tw[j]]); n++;
        }
        scores.Add(s / n);
    }
    return scores.Average();
}

// --- redondance/fragmentation : divergence de Jensen-Shannon entre topics ---
double JsdSel(double[] a, double[] b) {
    var m = a.Zip(b, (x, y) => (x + y) / 2).ToArray();
    double kl(double[] p, double[] q) => p.Zip(q, (x, y) => x > 1e-12 ? x * Math.Log(x / y) : 0).Sum();
    return Math.Sqrt(0.5 * kl(a, m) + 0.5 * kl(b, m));
}

// --- completion held-out : theta infere sur la moitie A (phi fixe au posterior appris), log-vrais de B ---
double HeldoutPplSel(double[][] ph) {
    double ll = 0; int nw = 0; int K = ph.Length;
    foreach (var hd in heldSel) {
        var A = hd.Take(docLen / 2).ToArray(); var B = hd.Skip(docLen / 2).ToArray();
        var engine = new InferenceEngine(new VariationalMessagePassing()) {
            Compiler = { CompilerChoice = CompilerChoice.Roslyn }, ShowProgress = false };
        Variable<Vector> th = Variable.Dirichlet(Enumerable.Repeat(0.7, K).ToArray()).Named("th");
        var Zk = new Microsoft.ML.Probabilistic.Models.Range(K).Named("Zk");
        var phiConst = Variable.Constant(ph.Select(r => Vector.FromArray(r)).ToArray(), Zk).Named("phiC");
        var Nc = new Microsoft.ML.Probabilistic.Models.Range(A.Length);
        VariableArray<int> wc = Variable.Array<int>(Nc).Named("wc");
        using (Variable.ForEach(Nc)) {
            Variable<int> zc = Variable.Discrete(th);
            zc.SetValueRange(Zk);
            using (Variable.Switch(zc)) wc[Nc] = Variable.Discrete(phiConst[zc]);
        }
        wc.ObservedValue = A;
        var thMean = engine.Infer<Dirichlet>(th).GetMean().ToArray();
        double[] pdoc = new double[Vsel];
        for (int v = 0; v < Vsel; v++) pdoc[v] = Enumerable.Range(0, K).Sum(k => thMean[k] * ph[k][v]);
        var cnt = B.GroupBy(x => x).ToDictionary(g => g.Key, g => g.Count());
        foreach (var kv in cnt) ll += kv.Value * Math.Log(Math.Max(pdoc[kv.Key], 1e-12));
        nw += B.Length;
    }
    return Math.Exp(-ll / nw);
}
Console.WriteLine("Helpers de K-selection prets.");
Helpers de K-selection prets.

Interprétation d’une exécution de la boucle

Le SetValueRange (cellule precedente) est indispensable : sans lui, Variable.Switch(z[D][N]) échoue - l’index d’un topic discret doit savoir dans quel domaine ses valeurs vivent. La perturbation seed-42 documentee dans le code est le seul apport : elle remplace une symetrie parfaite (dont VMP ne sort jamais, gradient ELBO nul) par une asymetrie infinitesimale ne codant aucune information sur les themes.


// --- boucle K=2..6 : coherence, redondance, completion ---
var swSel = System.Diagnostics.Stopwatch.StartNew();
foreach (int K in new[] { 2, 3, 4, 5, 6 }) {
    var (ph, th) = FitLdaSel(K);
    double c5 = UMassSel(ph, 5);
    var jss = new List<double>();
    for (int i = 0; i < K; i++) for (int j = i + 1; j < K; j++) jss.Add(JsdSel(ph[i], ph[j]));
    double ppl = HeldoutPplSel(ph);
    var tops = ph.Select(row => string.Join("|", Enumerable.Range(0, Vsel)
        .OrderByDescending(v => row[v]).Take(3).Select(v => vocabSel[v]))).ToArray();
    Console.WriteLine($"K={K}: coherence5={c5:F3}  JSmoy={jss.Average():F3}  JSmin={jss.Min():F3}  ppl_heldout={ppl:F2}  ({swSel.Elapsed.TotalSeconds:F0}s cum)");
    Console.WriteLine($"     tops: [{string.Join(" ; ", tops)}]");
}
K=2: coherence5=-0,204  JSmoy=0,528  JSmin=0,528  ppl_heldout=20,08  (1s cum)
     tops: [football|equipe|competition ; ingredient|gastronomie|recette]
K=3: coherence5=-0,237  JSmoy=0,504  JSmin=0,435  ppl_heldout=18,40  (3s cum)
     tops: [football|equipe|competition ; ingredient|internet|ordinateur ; hotel|avion|destination]
K=4: coherence5=-0,202  JSmoy=0,505  JSmin=0,441  ppl_heldout=16,27  (4s cum)
     tops: [football|equipe|match ; ingredient|recette|cuisson ; hotel|avion|destination ; internet|ordinateur|logiciel]
K=5: coherence5=-0,242  JSmoy=0,471  JSmin=0,389  ppl_heldout=16,68  (5s cum)
     tops: [football|equipe|competition ; internet|ordinateur|logiciel ; avion|hotel|destination ; logiciel|hotel|preparation ; ingredient|cuisson|recette]
K=6: coherence5=-0,237  JSmoy=0,434  JSmin=0,299  ppl_heldout=16,75  (6s cum)
     tops: [football|equipe|match ; internet|ordinateur|logiciel ; avion|hotel|tourisme ; logiciel|preparation|application ; ingredient|recette|gastronomie ; cuisson|preparation|football]

Lecture des resultats et verdict honnete

Resultats observes sur cette execution (corpus controle, seed 42) :

K coherence5 JSmoy JSmin ppl_heldout tops (3 premiers mots)
2 -0.204 0.528 0.528 20.1 football/equipe/competition ; ingredient/gastronomie/recette
3 -0.237 0.504 0.435 18.4 football/equipe/competition ; ingredient/internet/ordinateur ; hotel/avion/destination
4 -0.202 0.505 0.441 16.3 les 4 themes generateurs exactly
5 -0.242 0.471 0.389 16.7 … + topic « logiciel/hotel/preparation » (fragmente)
6 -0.237 0.434 0.299 16.8 … + topic « cuisson/preparation/football » (garbage)

Comparaison entre twins : les niveaux absolus de coherence5 ne sont pas comparables entre twins — np.random.default_rng(42) (Python/PyMC) et le RNG C# (Infer) sont deux algorithmes de génération différents (PCG64 pour np.random.default_rng(42), System.Random soustractif pour C#) — aucune valeur de graine ne les fera coïncider. Le même numéro de graine produit ici des instances de corpus différentes, donc des matrices de co-occurrence différentes (signes mêmes opposés : ~-0.20 ici, ~+0.5 côté PyMC). Comparer uniquement la tendance intra-twin. Légende : tops top-3 affichés ; coherence5 calculée sur top-5.

Lecture honnete : aucune metrique ne designe K=4 a elle seule.

  • La coherence UMass est quasi plate (-0.20 a -0.24) : K=2 la maximale a egalite avec K=4, elle ne tranche pas. Et elle est calculee sur les comptages d’entrainement : elle ne peut pas predire la performance sur des mots jamais vus.
  • La redondance JS (moyenne) decroit mecaniquement avec K (plus de topics => moins de recouvrement moyen). Sa min diagnostique utile : elle chute des K=5-6 (0.389, 0.299) quand un theme vrai est scinde en topics fragmentes - mais elle ne dit pas ou s’arreter (non monotone avant).
  • La perplexite held-out dessine une vallee plate entre K=4 et K=5 : le gain de K=4 sur K=2 (~20%) est reel mais petit, et aucune poursuite n’est nette.
  • Seule la confrontation au corpus generateur connu tranche : les tops mots de K=4 reproduisent quasi exactement les 4 phiGen des themes de reference. Aux autres K, un topic melange ou un topic garbage apparait.

Conclusion : le modele de topics n’offre pas sa propre granularite. Il apprend des phi et theta coherents pour tout K (ici meme K=6 converge sans erreur). Choisir K est un choix de modelisation : un critere externe (coherence sur un corpus de reference, perplexite a estimation d’importance, ou connaissance semantique) combine a une inspection des tops mots - c’est ce que l’exercice 11ter propose de determiner vous-meme ci-dessous.

9. Exemple guide : Analyser un Corpus

Enonce

Etendez le vocabulaire et ajoutez des documents pour créer un corpus plus realiste avec 4 topics.

Objectifs de l’exercice

L’extension a 4 topics permet d’explorer :

  1. Scalabilite : Comment le modèle se comporte avec plus de topics
  2. Chevauchements : Comment gerer les documents multi-thematiques
  3. Equilibre : Importance d’avoir des topics de taille comparable

Paramètres a ajuster :

Paramètre Valeur suggeree Impact
\(\alpha\) (prior theta) 0.1-1.0 Sparsity des documents
\(\beta\) (prior phi) 0.01-0.1 Sparsity des topics
Nombre de topics K 3-10 Granularite thematique
Itérations VMP 50-200 Convergence

Defi supplementaire : Essayez d’ajouter des mots ambigus partages entre topics (ex: “competition” pour Sport et Tech) et observez comment le modèle les attribue.

Implementation de l’exercice

Le code suivant créé un corpus etendu avec 4 topics et 16 mots de vocabulaire. La classification est effectuee par comptage simple des indices de mots.

// Exemple guide : Corpus etendu

string[] vocabEtendu = {
    // Sport (0-3)
    "football", "basketball", "tennis", "competition",
    // Tech (4-7)
    "ordinateur", "logiciel", "internet", "application",
    // Cuisine (8-11)
    "recette", "ingredient", "cuisson", "gastronomie",
    // Voyage (12-15)
    "hotel", "avion", "destination", "tourisme"
};

int[][] docsEtendus = {
    new[] { 0, 1, 2, 3, 0 },     // Sport
    new[] { 4, 5, 6, 7, 5 },     // Tech
    new[] { 8, 9, 10, 11, 9 },   // Cuisine
    new[] { 12, 13, 14, 15, 13 },// Voyage
    new[] { 0, 4, 5, 1, 6 },     // Sport + Tech
    new[] { 8, 12, 13, 10, 14 }, // Cuisine + Voyage
    new[] { 2, 3, 0, 1, 2 },     // Sport
    new[] { 6, 7, 4, 5, 7 }      // Tech
};

Console.WriteLine("=== Corpus Etendu (4 topics) ===");

for (int d = 0; d < docsEtendus.Length; d++)
{
    // Classification simple basee sur les indices de mots
    int sport = docsEtendus[d].Count(w => w <= 3);
    int tech = docsEtendus[d].Count(w => w >= 4 && w <= 7);
    int cuisine = docsEtendus[d].Count(w => w >= 8 && w <= 11);
    int voyage = docsEtendus[d].Count(w => w >= 12);
    
    string dominant;
    int max = new[] { sport, tech, cuisine, voyage }.Max();
    if (sport == max) dominant = "Sport";
    else if (tech == max) dominant = "Tech";
    else if (cuisine == max) dominant = "Cuisine";
    else dominant = "Voyage";
    
    string mots = string.Join(", ", docsEtendus[d].Select(i => vocabEtendu[i]));
    Console.WriteLine($"Doc {d+1} [{dominant,-8}] : {mots}");
}
=== Corpus Etendu (4 topics) ===
Doc 1 [Sport   ] : football, basketball, tennis, competition, football
Doc 2 [Tech    ] : ordinateur, logiciel, internet, application, logiciel
Doc 3 [Cuisine ] : recette, ingredient, cuisson, gastronomie, ingredient
Doc 4 [Voyage  ] : hotel, avion, destination, tourisme, avion
Doc 5 [Tech    ] : football, ordinateur, logiciel, basketball, internet
Doc 6 [Voyage  ] : recette, hotel, avion, cuisson, destination
Doc 7 [Sport   ] : tennis, competition, football, basketball, tennis
Doc 8 [Tech    ] : internet, application, ordinateur, logiciel, application

Analyse du corpus étendu

Structure : 4 topics (Sport, Tech, Cuisine, Voyage) × 4 mots chacun

Document Topic dominant Composition
Doc 1, 7 Sport Documents purs
Doc 2, 8 Tech Documents purs
Doc 3 Cuisine Document pur
Doc 4 Voyage Document pur
Doc 5 Tech (3) > Sport (2) Mélange tech-sport
Doc 6 Voyage (3) > Cuisine (2) Mélange voyage-cuisine

Observations :

  1. Scalabilité : Le passage de 3 à 4 topics reste gérable avec le comptage simple

  2. Mélanges asymétriques : Doc 5 et Doc 6 montrent des documents multi-thématiques

  3. Vocabulaire disjoint : La structure en blocs de 4 mots consécutifs simplifie l’identification

Exercice proposé : Ajouter des mots partagés entre topics (ex: “compétition” pour Sport et Tech) pour observer comment le modèle gère l’ambiguïté lexicale.

10. Resume

Concept Description
LDA Modèle generatif pour documents
Topic Distribution sur le vocabulaire
Theta Distribution de topics par document
Phi Distribution de mots par topic
Dirichlet Prior conjugue pour distributions categoriques

Pour aller plus loin

Si vous voulez… Consultez…
Comprendre les priors Dirichlet Infer-2-Gaussian-Mixtures
Debugger un problème de convergence Infer-2b-Debugging-Bonnes-Pratiques
Comparer VMP et EP Infer-2b-Debugging-Bonnes-Pratiques Section 4
Trouver une definition Glossaire

Prochaine étape

Dans Infer-10-Model-Sélection, nous explorerons :

  • La sélection et comparaison de modèles
  • L’evidence bayesienne (marginal likelihood)
  • Le facteur de Bayes

Points cles a retenir

Ce que nous avons appris :

  1. LDA est un modèle generatif : Il decrit comment les documents sont “generes” a partir de topics latents

  2. Problème de symetrie : Avec des priors uniformes, VMP converge vers des solutions degenerees - les priors asymetriques ou l’initialisation aleatoire sont necessaires

  3. Dirichlet est central : Prior conjugue pour les melanges de distributions categoriques

  4. Trade-off interpretation/scalabilite : Le comptage simple fonctionne sur des corpus synthetiques, mais l’inference probabiliste capture les incertitudes

Erreurs courantes a eviter :

Erreur Consequence Solution
Prior symetrique Mode degenere Priors asymetriques ou init. aleatoire
K trop grand Overfitting, topics non-interpretatifs Validation croisee ou HDP
K trop petit Topics melanges Augmenter K ou utiliser hiérarchie
Ignorer la preprocessing Mots frequents dominent Stop-words, TF-IDF

Applications pratiques :

  • Recherche d’information : Indexation sémantique de documents
  • Recommandation : “Utilisateurs qui aiment ce topic aiment aussi…”
  • Analyse de sentiments : Topics combines avec polarite
  • Bioinformatique : Decouverte de motifs dans les sequences

11. Exercice : Decouvrir les Topics d’un Corpus Francais

Enonce

Analysez un corpus de 9 documents repartis sur 3 thèmes : Cuisine, Voyage, Science.

Vocabulaire (15 mots, indices 0-14) : - Cuisine (0-4) : “recette”, “cuisson”, “saveur”, “ingredient”, “plat” - Voyage (5-9) : “destination”, “hôtel”, “vol”, “decouverte”, “carte” - Science (10-14) : “expérience”, “donnee”, “analyse”, “résultat”, “hypothese”

Documents (indices des mots) : 3 Cuisine + 3 Voyage + 3 Science.

  1. Entrainer LDA avec K=3 topics
  2. Verifier que chaque topic correspond a un thème
  3. Afficher les 3 mots les plus probables par topic
// Exercice : LDA 3 topics sur corpus francais
string[] vocab = {
    "recette", "cuisson", "saveur", "ingredient", "plat",       // 0-4: Cuisine
    "destination", "hotel", "vol", "decouverte", "carte",       // 5-9: Voyage
    "experience", "donnee", "analyse", "resultat", "hypothese"  // 10-14: Science
};
int V = vocab.Length;  // 15 mots
int K = 3;             // 3 topics attendus

// Documents (chaque sous-tableau = indices des mots du document)
int[][] docs = {
    new[] {0, 1, 2, 0, 2, 3},      // Doc 0: Cuisine
    new[] {0, 2, 3, 3, 4},         // Doc 1: Cuisine
    new[] {1, 3, 4, 1, 0},         // Doc 2: Cuisine
    new[] {5, 6, 7, 5, 8},         // Doc 3: Voyage
    new[] {5, 7, 8, 9, 5, 6},      // Doc 4: Voyage
    new[] {6, 8, 9, 6, 7},         // Doc 5: Voyage
    new[] {10, 11, 12, 11, 13},    // Doc 6: Science
    new[] {11, 12, 13, 14, 11},    // Doc 7: Science
    new[] {10, 12, 14, 13, 10}     // Doc 8: Science
};

// TODO: Creer le modele LDA avec K=3 topics
// (Reutilisez la structure LDA de l'exemple guide : phi, theta, z, w)

// TODO: Entrainer et inferer les distributions topic-par-mot (phi)
// Chaque topic devrait concentrer son poids sur 5 mots d'un theme

// TODO: Afficher les 3 mots les plus probables pour chaque topic
Console.WriteLine("Exercice a completer");
Exercice a completer

12. Exercice : Detecter l’Emergence d’un Nouveau Topic

Enonce

Vous disposez d’un corpus de 12 documents couvrant 3 thèmes connus (Sport, Politique, Musique) et un nouveau thème emergent (Economie) que vous ne connaissez pas a priori.

Objectif : Determinez le nombre optimal de topics K et identifiez le thème emergent.

Données

Un vocabulaire de 15 mots et 12 documents dont certains contiennent des mots d’un nouveau thème inconnu.

Tâches

  1. Entrainez LDA avec K=3 topics. Qu’observez-vous dans les distributions phi ?
  2. Entrainez LDA avec K=4 topics. Le nouveau topic est-il identifie ?
  3. Comparez les distributions theta pour un document contenant le thème emergent avec K=3 vs K=4.

Indice

Avec K=3, les mots du thème emergent seront “absorbes” par les topics existants, creant des distributions phi brouillees. Avec K=4, un topic dedie devrait apparaitre avec des mots clairs.

Étapes suggerees :

  1. Définir le vocabulaire et les 12 documents
  2. Créer le modèle LDA avec K=3, observer les distributions phi
  3. Créer le modèle LDA avec K=4, comparer
  4. Afficher les top-mots de chaque topic pour les deux valeurs de K
// Exercice : Detecter un theme emergent avec LDA
// Vocabulaire elargi (15 mots, 4 themes caches)
string[] vocabEx = {
    "sport", "equipe", "match",           // 0-2: Sport
    "politique", "election", "vote",      // 3-5: Politique
    "musique", "concert", "artiste",      // 6-8: Musique
    "marche", "investissement", "action"  // 9-11: Economie (theme emergent)
};

// 12 documents dont certains melangent Economie avec d'autres themes
int[][] docsEx = {
    new[] { 0, 1, 2, 0, 2 },             // Sport
    new[] { 3, 4, 5, 4, 3 },             // Politique
    new[] { 6, 7, 8, 7, 6 },             // Musique
    new[] { 9, 10, 11, 9, 10 },          // Economie pur
    new[] { 0, 1, 9, 10, 0 },            // Sport + Economie
    new[] { 6, 7, 9, 11, 8 },            // Musique + Economie
    new[] { 3, 4, 5, 3, 4 },             // Politique
    new[] { 0, 2, 1, 2, 0 },             // Sport
    new[] { 9, 11, 10, 9, 11 },          // Economie pur
    new[] { 6, 8, 7, 6, 8 },             // Musique
    new[] { 3, 5, 9, 10, 4 },            // Politique + Economie
    new[] { 0, 1, 2, 1, 0 }              // Sport
};

// TODO: Etape 1 - Entrainer LDA avec K=3 topics sur ce corpus
// Observer: les mots d'economie seront-ils absorbes par quel(s) topic(s) ?

// TODO: Etape 2 - Entrainer LDA avec K=4 topics
// Observer: un topic dedie a l'economie devrait emerger

// TODO: Etape 3 - Comparer les distributions theta pour les documents mixtes
// (ex: doc 4 = Sport+Economie) entre K=3 et K=4

Console.WriteLine("Exercice a completer : detection de theme emergent");
Exercice a completer : detection de theme emergent

11bis. Exercice : Evaluation de la Coherence des Topics

Comment savoir si les topics appris par LDA sont de “bonne qualite” ? La mesure de coherence UMass evalue si les mots les plus probables d’un topic apparaissent frequemment ensemble dans les documents.

Principe : Pour chaque paire de mots \((w_i, w_j)\) parmi les top-N mots d’un topic, on calcule :

\[C_{\text{UMass}}(w_i, w_j) = \log \frac{D(w_i, w_j) + 1}{D(w_j)}\]

ou \(D(w_i, w_j)\) est le nombre de documents contenant les deux mots, et \(D(w_j)\) le nombre de documents contenant \(w_j\).

Objectif : Calculer la coherence UMass pour 3 topics estimes et identifier celui qui est le moins coherent.

Données fournies : - 3 topics estimes (distributions phi simulees sur 9 mots) - Topic A : principalement Sport (bruite) - Topic B : principalement Politique - Topic C : melange Musique + Sport (topic mal isole)

Étapes :

  1. Top-mots – Pour chaque topic, extraire les 3 mots les plus probables et les afficher.
  2. Coherence UMass – Pour chaque topic, calculer la coherence sur les paires de top-mots. Un topic coherent aura une coherence positive ; un topic melange aura une coherence plus faible.
  3. Diagnostic – Identifier le topic le moins coherent et expliquer pourquoi (presence de mots de thèmes différents dans le même topic).

Indices : - # Indice : Pour compter les co-occurrences \(D(w_i, w_j)\), parcourez les documents et verifiez si les deux indices de mots sont presents dans le même document. - # Indice : La coherence UMass est plus elevee quand les mots co-occurrent souvent. Le Topic C devrait avoir une coherence plus faible car ses top-mots viennent de deux thèmes différents. - # Indice : Utilisez Enumerable.Range(0, Vcoh).OrderByDescending(i => phiEstimated[k][i]).Take(topN) pour extraire les top-mots.

// Exercice : Evaluation de la coherence des topics
// On dispose de 3 topics estimes par LDA sur un corpus de 12 documents
// Chaque topic est represente par sa distribution phi sur 12 mots

string[] vocabCoherence = {
    "ballon", "terrain", "match",       // 0-2: Sport
    "parlement", "lois", "debat",       // 3-5: Politique
    "guitare", "scene", "melodie"       // 6-8: Musique
};
int Vcoh = vocabCoherence.Length;

// Topics estimes (distributions phi simulees)
double[][] phiEstimated = {
    // Topic A : principalement Sport (mais un peu bruite)
    new double[] { 0.30, 0.28, 0.25, 0.03, 0.02, 0.02, 0.03, 0.04, 0.03 },
    // Topic B : principalement Politique
    new double[] { 0.03, 0.02, 0.03, 0.29, 0.30, 0.28, 0.01, 0.02, 0.02 },
    // Topic C : melange Musique + Sport (topic mal isole)
    new double[] { 0.15, 0.12, 0.10, 0.03, 0.02, 0.02, 0.20, 0.18, 0.18 }
};

// TODO: Etape 1 - Pour chaque topic, afficher les 3 mots les plus probables
// Indice : trier les indices par probabilite decroissante avec OrderByDescending

// TODO: Etape 2 - Calculer la coherence UMass pour chaque topic
// Pour chaque paire de mots parmi les top-5, calculer :
//   C(i,j) = log( (D(w_i, w_j) + 1) / D(w_j) )
// ou D(w_i, w_j) = nombre de documents contenant les deux mots
// et D(w_j) = nombre de documents contenant w_j
// La coherence d'un topic = moyenne des C(i,j) sur toutes les paires

// Donnees : 12 documents (indices de mots)
int[][] docsCoherence = {
    new[] { 0, 1, 2 },           // Sport
    new[] { 0, 2, 0 },           // Sport
    new[] { 1, 2, 0, 1 },        // Sport
    new[] { 3, 4, 5 },           // Politique
    new[] { 3, 5, 4, 3 },        // Politique
    new[] { 4, 5, 3 },           // Politique
    new[] { 6, 7, 8 },           // Musique
    new[] { 6, 8, 7, 6 },        // Musique
    new[] { 7, 8, 6 },           // Musique
    new[] { 0, 1, 6, 7 },        // Sport + Musique (melange)
    new[] { 2, 0, 6, 8, 1 },     // Sport + Musique (melange)
    new[] { 3, 4, 0, 6 }         // Politique + Sport + Musique
};

// TODO: Etape 3 - Identifier le topic le moins coherent et proposer une explication
// Indice : Topic C a des mots de deux themes differents -> coherence plus faible

Console.WriteLine("Exercice a completer : evaluation de coherence des topics");
Exercice a completer : evaluation de coherence des topics

11ter. Exercice 3 : Determiner le Nombre Optimal de Topics

Le choix du nombre de topics K est un hyperparametre central de LDA. Un K trop petit fusionne des thèmes distincts ; un K trop grand créé des topics redondants ou non-interpretables.

Objectif : Pour un corpus couvrant un nombre inconnu de thèmes, tester plusieurs valeurs de K (de 2 a 6) et sélectionner celle qui produit les topics les plus coherents en utilisant la metrique de coherence UMass vue precedemment.

Données fournies : - Un corpus de 15 documents couvrant 4 thèmes caches (Sport, Politique, Musique, Economie) - Un vocabulaire de 16 mots

Étapes :

  1. Grille de K – Pour chaque K dans {2, 3, 4, 5, 6}, entrainer un modèle LDA (comptage simple ou inference) et extraire les 3 top-mots de chaque topic.
  2. Coherence par K – Calculer la coherence UMass moyenne de tous les topics pour chaque valeur de K.
  3. Sélection – Identifier le K optimal (celui qui maximise la coherence moyenne) et verifier visuellement que les top-mots sont coherents.

Indices : - # Indice : Pour chaque K, les topics sont obtenus en regroupant les mots du vocabulaire en K clusters. Avec le comptage simple, chaque document est classifie selon son thème dominant, puis les mots de chaque cluster forment un topic. - # Indice : La coherence devrait augmenter jusqu’au vrai nombre de topics (K=4), puis stagner ou diminuer quand K depasse la vraie valeur. - # Indice : Reutilisez la formule UMass de l’Exercice 2 : C(i,j) = log((D(w_i, w_j) + 1) / D(w_j)).

// Exercice 3 : Determiner le nombre optimal de topics
string[] vocabOpt = {
    "football", "equipe", "match", "competition",     // 0-3: Sport
    "parlement", "lois", "debat", "vote",              // 4-7: Politique
    "guitare", "scene", "melodie", "concert",          // 8-11: Musique
    "marche", "investissement", "action", "bourse"     // 12-15: Economie
};
int Vopt = vocabOpt.Length;

// 15 documents couvrant 4 themes caches
int[][] docsOpt = {
    new[] { 0, 1, 2, 3, 0 },          // Sport
    new[] { 1, 2, 0, 3, 1 },          // Sport
    new[] { 0, 3, 2, 0, 1 },          // Sport
    new[] { 4, 5, 6, 7, 4 },          // Politique
    new[] { 5, 6, 4, 7, 5 },          // Politique
    new[] { 6, 7, 4, 5, 6 },          // Politique
    new[] { 8, 9, 10, 11, 8 },        // Musique
    new[] { 9, 10, 8, 11, 9 },        // Musique
    new[] { 10, 11, 8, 9, 10 },       // Musique
    new[] { 12, 13, 14, 15, 12 },     // Economie
    new[] { 13, 14, 12, 15, 13 },     // Economie
    new[] { 14, 15, 12, 13, 14 },     // Economie
    new[] { 0, 1, 12, 13, 2 },        // Sport + Economie (melange)
    new[] { 8, 9, 4, 5, 10 },         // Musique + Politique (melange)
    new[] { 3, 14, 15, 6, 7 }         // Sport + Economie + Politique (melange)
};

// TODO: Etape 1 - Pour chaque K dans {2, 3, 4, 5, 6}, classer les documents en K topics
// par comptage simple des groupes de 4 mots, puis extraire les 3 top-mots de chaque topic.
// Indice : avec K topics, decouper les indices 0..15 en K intervalles et assigner
// chaque document au groupe dominant.

// TODO: Etape 2 - Calculer la coherence UMass moyenne pour chaque K
// Indice : reutilisez la formule C(i,j) = log((D(w_i, w_j) + 1) / D(w_j))
// et calculez D(w_i, w_j) en parcourant docsOpt

// TODO: Etape 3 - Identifier le K optimal et afficher un resume
// Indice : K optimal = argmax de la coherence moyenne

Console.WriteLine("Exercice a completer : nombre optimal de topics");
Exercice a completer : nombre optimal de topics

Conclusion

Ce notebook a couvert le topic modeling avec LDA : modèle generatif documents-topics-mots, distributions Dirichlet et problème de symetrie.

Concept Point cle
LDA Modèle generatif : theta (docs) x phi (topics) -> mots observes
Dirichlet Prior conjugue pour les melanges de distributions categoriques
Symetrie VMP Priors uniformes -> mode degenere, solution : priors asymetriques
Theta Distribution de topics par document (interpretable)
Phi Distribution de mots par topic (top-mots pour interpretation)
Distribution Rôle
Dirichlet Prior sur theta (composition documents) et phi (mots par topic)
Discrete Assignation de topic et generation de mots
VariationalMessagePassing Algorithme d’inference pour LDA

Point critique : Le choix du prior Dirichlet determine la qualite de l’inference. Un prior symetrique Dir(1,…,1) mene VMP vers un mode degenere. Les priors asymetriques ou l’initialisation aleatoire sont necessaires pour briser la symetrie et obtenir des topics interpretables.

Références

Référence Section / Numéro Lien au notebook
Blei, Ng & Jordan (2003), JMLR 3:993-1022, “Latent Dirichlet Allocation” — Article fondateur de LDA (modèle génératif documents-topics-mots, inférence variational EM)
Pritchard, Stephens & Donnelly (2000), Genetics 155:945-959 — Version ancestrale (LDA-like) avec priors Pritchard-Stephens-Donnelly (PSD) sur les proportions de population
Wang & Grimson (2007), Proc. IEEE CVPR — Application LDA à la segmentation d’images ; illustre VMP sur de grandes échelles
Heinrich (2005) “Parameter estimation for text analysis” Technical report Tutorial LDA + Gibbs sampling + variational ; convergence et symétrie du posterior
MBML “Latent Dirichlet Allocation” (sub-page) — Portage MBML-IDIAP du modèle LDA — version courte canonique suivant Blei 2003

Note pedagogique : le notebook utilise Variational Message Passing (VMP) fourni par Infer.NET. Pour comprendre le lien VMP ↔︎ l’algorithme variational de Blei 2003, voir §10.1 et §10.4.1 de Bishop (Approximate Inference) ou la note technique de Heinrich (2005).

Retour au sommet