Infer-15-Recommenders : systèmes de Recommandation

Serie : Programmation Probabiliste avec Infer.NET (15/19)
Duree estimee : 60 minutes
Prerequis : Infer-14-Sequences


Objectifs

  • Comprendre le filtrage collaboratif bayesien
  • Implementer la factorisation matricielle
  • Gerer le problème du cold-start
  • Reconcilier des sources multiples (ClickModel)

1. Configuration

Nous chargeons Infer.NET pour implementer des systèmes de recommandation probabilistes. Ces modèles utilisent la factorisation matricielle bayesienne pour predire les préférences des utilisateurs a partir de données partiellement observees, tout en gerant l’incertitude inherente aux recommandations.

#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 !");
Installing Packages
  • Microsoft.ML.Probabilistic
  • Microsoft.ML.Probabilistic.Compiler
Infer.NET pret !

Chargement du helper de visualisation des graphes de facteurs.

// Chargement du helper de visualisation des graphes factoriels
#load "FactorGraphHelper.cs"

Console.WriteLine("FactorGraphHelper charge - visualisation des graphes factoriels disponible.");
FactorGraphHelper charge - visualisation des graphes factoriels disponible.

Configuration chargee

L’import des namespaces Infer.NET est reussi. Nous disposons maintenant de : - Microsoft.ML.Probabilistic.Models : Definition des modèles graphiques - Microsoft.ML.Probabilistic.Distributions : Distributions (Gaussian, Gamma, Dirichlet, etc.) - Microsoft.ML.Probabilistic.Algorithms : Algorithmes d’inference (EP, VMP)

Note : Les systèmes de recommandation utilisent intensivement Expectation Propagation (EP) car les modèles de factorisation impliquent des produits de variables gaussiennes, non supportes par VMP.

2. Introduction au Filtrage Collaboratif

Principe

Le filtrage collaboratif predit les préférences d’un utilisateur en se basant sur les préférences d’utilisateurs similaires.

Matrice de notes

          Film1  Film2  Film3  Film4
User1     5      ?      3      ?
User2     ?      4      ?      2
User3     4      3      5      ?
User4     ?      ?      4      5

Approches

méthode Description Avantage
Voisinage k-NN sur similarite Simple
Factorisation Decomposition U x V Scalable
Bayesien Incertitude + priors Robuste

3. Factorisation Matricielle Bayesienne

Fondements mathematiques

La factorisation matricielle decompose la matrice des notes \(R\) (partiellement observee) en deux matrices de facteurs latents :

\[R \approx U \times V^T\]

Ou : - \(U \in \mathbb{R}^{n_{users} \times k}\) : traits latents des utilisateurs - \(V \in \mathbb{R}^{n_{items} \times k}\) : traits latents des items - \(k\) : nombre de facteurs latents (hyperparametre)

Interpretation probabiliste :

\[r_{ui} \sim \mathcal{N}\left(\sum_{t=1}^{k} U_{ut} \cdot V_{it}, \sigma^2\right)\]

La note \(r_{ui}\) est generee par le produit scalaire des traits, plus un bruit gaussien.

Avantage bayesien : On place des priors sur \(U\) et \(V\), ce qui : - Regularise automatiquement le modèle - Fournit des intervalles de confiance sur les predictions - Gere naturellement les données manquantes

Origine. La factorisation matricielle pour les systemes de recommandation a ete popularisee par le Netflix Prize (2006-2009) et synthetrisee par Koren, Bell & Volinsky (Koren, Y., Bell, R. & Volinsky, C., 2009, “Matrix Factorization Techniques for Recommender Systems”, IEEE Computer 42(8):30-37, doi:10.1109/MC.2009.263). Ce notebook en est la reformulation bayesienne : on met des priors gaussiens sur les traits latents U et V (au lieu de les apprendre par SGD sur l’erreur RMSE comme dans la version originale du Netflix Prize), et on infere leur distribution a posteriori plutot qu’une estimation ponctuelle. La distinction est pedagogiquement importante : la version bayesienne fournit des intervalles de confiance sur les predictions, utiles pour le cold-start (cf. section 5).

Preparation des données

Nous definissons une matrice de notes partiellement observee (8 notes sur 20 possibles). Le format sparse (user, item, note) est standard pour les systèmes de recommandation avec matrices creuses.

paramètres du modèle : - nUsers : Nombre d’utilisateurs - nItems : Nombre d’items (films, produits, etc.) - nTraits : Nombre de facteurs latents (hyperparametre critique)

// Donnees : notes observees
int nUsers = 4;
int nItems = 5;
int nTraits = 2;  // Facteurs latents

// Observations : (user, item, note)
int[] userObs = { 0, 0, 1, 1, 2, 2, 3, 3 };
int[] itemObs = { 0, 2, 1, 3, 0, 2, 2, 4 };
double[] noteObs = { 5.0, 3.0, 4.0, 2.0, 4.0, 5.0, 4.0, 5.0 };
int nObs = userObs.Length;

Console.WriteLine("=== Factorisation Matricielle ===");
Console.WriteLine($"\nUtilisateurs : {nUsers}, Items : {nItems}, Traits : {nTraits}");
Console.WriteLine($"Observations : {nObs} notes");
Console.WriteLine("\nNotes observees :");
for (int i = 0; i < nObs; i++)
{
    Console.WriteLine($"  User {userObs[i]} -> Item {itemObs[i]} : {noteObs[i]}");
}
=== Factorisation Matricielle ===

Utilisateurs : 4, Items : 5, Traits : 2
Observations : 8 notes

Notes observees :
  User 0 -> Item 0 : 5
  User 0 -> Item 2 : 3
  User 1 -> Item 1 : 4
  User 1 -> Item 3 : 2
  User 2 -> Item 0 : 4
  User 2 -> Item 2 : 5
  User 3 -> Item 2 : 4
  User 3 -> Item 4 : 5

Interpretation des données chargees

Sortie obtenue : 8 observations (user, item, note)

Statistique Valeur
Utilisateurs 4
Items 5
Observations 8 (40% de la matrice)
Traits latents 2

Visualisation de la matrice sparse :

          Item0  Item1  Item2  Item3  Item4
User 0      5      -      3      -      -
User 1      -      4      -      2      -
User 2      4      -      5      -      -
User 3      -      -      4      -      5

Les cases - sont les notes a predire. L’objectif de la factorisation est de completer cette matrice.

Definition des variables latentes

Nous definissons maintenant les matrices de traits latents U (utilisateurs) et V (items).

Architecture du modèle : - userTraits[u, t] : Trait t de l’utilisateur u - itemTraits[i, t] : Trait t de l’item i - noisePrecision : Precision du bruit gaussien

Prior choisi : Gaussian(0, 1) pour tous les traits - Moyenne 0 : Pas de biais a priori - Precision 1 : Regularisation moderee

// Modele de factorisation

Range userRange = new Range(nUsers).Named("user");
Range itemRange = new Range(nItems).Named("item");
Range traitRange = new Range(nTraits).Named("trait");
Range obsRange = new Range(nObs).Named("obs");

// Traits utilisateurs : U[user, trait]
VariableArray2D<double> userTraits = Variable.Array<double>(userRange, traitRange).Named("userTraits");
userTraits[userRange, traitRange] = Variable.GaussianFromMeanAndPrecision(0, 1).ForEach(userRange, traitRange);

// Traits items : V[item, trait]
VariableArray2D<double> itemTraits = Variable.Array<double>(itemRange, traitRange).Named("itemTraits");
itemTraits[itemRange, traitRange] = Variable.GaussianFromMeanAndPrecision(0, 1).ForEach(itemRange, traitRange);

// Precision du bruit
Variable<double> noisePrecision = Variable.GammaFromShapeAndScale(2, 0.5).Named("noisePrecision");

Console.WriteLine("Traits latents definis (U et V).");
Traits latents definis (U et V).

Structure du graphe factoriel

Le modèle de factorisation peut etre visualise comme un graphe factoriel :

Prior Gaussian(0,1)     Prior Gaussian(0,1)
       |                       |
       v                       v
   U[user,trait]           V[item,trait]
       |                       |
       +--------> * <----------+
                  |
                  v
           Sum(produits) = affinite
                  |
                  v
            Gaussian(affinite, precision)
                  |
                  v
              Rating observe

Note technique : Le produit de deux variables gaussiennes (U * V) n’est pas gaussien, ce qui rend l’inference exacte impossible. Infer.NET utilise Expectation Propagation (EP) pour approximer les posterieurs.

Lien entre traits et observations

Nous connectons maintenant les observations aux variables latentes via le modèle generatif.

Equation du modèle : \[r_{ui} = \sum_{t=1}^{k} U_{u,t} \cdot V_{i,t} + \epsilon, \quad \epsilon \sim \mathcal{N}(0, 1/\text{precision})\]

Le code utilise : 1. Variable.ForEach(obsRange) : Boucle sur chaque observation 2. userTraits[userIndex[obs], trait] * itemTraits[itemIndex[obs], trait] : Produit élément par élément 3. Variable.Sum(produits) : Produit scalaire (affinite) 4. GaussianFromMeanAndPrecision(affinite, noisePrecision) : Generation de la note

// Observations indexees
VariableArray<int> userIndex = Variable.Observed(userObs, obsRange).Named("userIndex");
VariableArray<int> itemIndex = Variable.Observed(itemObs, obsRange).Named("itemIndex");
VariableArray<double> rating = Variable.Observed(noteObs, obsRange).Named("rating");

// Modele de generation des notes
using (Variable.ForEach(obsRange))
{
    // Produit scalaire des traits
    VariableArray<double> produits = Variable.Array<double>(traitRange).Named("produits");
    produits[traitRange] = userTraits[userIndex[obsRange], traitRange] * itemTraits[itemIndex[obsRange], traitRange];
    
    Variable<double> affinite = Variable.Sum(produits).Named("affinite");
    
    // Note = affinite + bruit gaussien
    rating[obsRange] = Variable.GaussianFromMeanAndPrecision(affinite, noisePrecision);
}

Console.WriteLine("Modele de notes defini : rating ~ Gaussian(U * V', precision).");
Modele de notes defini : rating ~ Gaussian(U * V', precision).

exécution de l’inference

Nous executons l’inference avec Expectation Propagation (EP), le seul algorithme capable de gerer les produits de variables gaussiennes.

Pourquoi EP et pas VMP ? - VMP (Variational Message Passing) ne supporte pas Variable.Product entre deux gaussiennes - EP approxime ces opérations par propagation de messages locaux

Sorties attendues : - userTraitsPost[u, t] : Distribution posterieure du trait t de l’utilisateur u - itemTraitsPost[i, t] : Distribution posterieure du trait t de l’item i - noisePrecPost : Precision du bruit inferee

// Inference
InferenceEngine moteur = new InferenceEngine();
moteur.Compiler.CompilerChoice = CompilerChoice.Roslyn;
moteur.Algorithm = new ExpectationPropagation();
moteur.ShowFactorGraph = true;

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

var userTraitsPost = moteur.Infer<Gaussian[,]>(userTraits);
var itemTraitsPost = moteur.Infer<Gaussian[,]>(itemTraits);
var noisePrecPost = moteur.Infer<Gamma>(noisePrecision);

Console.WriteLine($"\nPrecision du bruit : {noisePrecPost.GetMean():F2}");

Console.WriteLine("\nTraits utilisateurs (moyenne) :");
for (int u = 0; u < nUsers; u++)
{
    Console.Write($"  User {u} : [");
    for (int t = 0; t < nTraits; t++)
    {
        Console.Write($"{userTraitsPost[u, t].GetMean():F2}");
        if (t < nTraits - 1) Console.Write(", ");
    }
    Console.WriteLine("]");
}

=== Inference ===
Compiling model...compilation had 6 warning(s).
  [1] This model will consume excess memory due to the indexing expression userTraits[userIndex[obs], trait] inside of a loop over obs. Try simplifying this expression in your model, perhaps by creating auxiliary index arrays.  If the index is a function of obs, try creating an array over obs holding the index.
  [2] This model will consume excess memory due to the indexing expression itemTraits[itemIndex[obs], trait] inside of a loop over obs. Try simplifying this expression in your model, perhaps by creating auxiliary index arrays.  If the index is a function of obs, try creating an array over obs holding the index.
  [3] GaussianProductOp.AAverageConditional(produits_B[obs][trait], userTraits_rep_F[obs][userIndex[obs], trait], itemTraits_rep_F[obs][itemIndex[obs], trait]) has quality band Experimental which is less than the recommended quality band (Preview)
  [4] GaussianProductOp.BAverageConditional(produits_B[obs][trait], userTraits_rep_F[obs][userIndex[obs], trait], itemTraits_rep_F[obs][itemIndex[obs], trait]) has quality band Experimental which is less than the recommended quality band (Preview)
  [5] GaussianProductOp.AAverageConditional(produits_B[obs][trait], userTraits_rep_F[obs][userIndex[obs], trait], itemTraits_rep_F[obs][itemIndex[obs], trait]) has quality band Experimental which is less than the recommended quality band (Preview)
  [6] GaussianProductOp.ProductAverageConditional(produits_B[obs][trait], userTraits_rep_F[obs][userIndex[obs], trait], itemTraits_rep_F[obs][itemIndex[obs], trait]) has quality band Experimental which is less than the recommended quality band (Preview)
done.
Iterating: 
.........|.........|.........|.........|.........| 50

Precision du bruit : 0,12

Traits utilisateurs (moyenne) :
  User 0 : [0,00, 0,00]
  User 1 : [-0,00, -0,00]
  User 2 : [0,00, 0,00]
  User 3 : [-0,00, -0,00]

Analyse de l’inférence de factorisation

Résultats observés :

Paramètre Valeur Interprétation
Précision bruit 0.12 Très faible → grande variance résiduelle
Traits utilisateurs ~0.00 Presque nuls

Diagnostic : Problème de convergence

Les traits utilisateurs proches de zéro indiquent un problème :

  1. Identifiabilité : Avec seulement 8 observations pour (4 users + 5 items) × 2 traits = 18 paramètres, le modèle est sous-déterminé

  2. Warnings du compilateur :

    • “GaussianProductOp… has quality band Experimental” → opérations numériquement instables
    • “excess memory due to indexing” → structure de données non optimale
  3. Solution pratique :

    • Augmenter le nombre d’observations (>50 pour ce modèle)
    • Réduire le nombre de traits latents
    • Utiliser des priors plus informatifs

Note : En production, Matchbox (API Infer.NET) utilise des techniques d’optimisation avancées pour éviter ces problèmes.

Visualisation du graphe factoriel - Factorisation matricielle

Le graphe factoriel ci-dessous illustre la structure du modèle de recommandation par factorisation matricielle :

  • Variables latentes : userTraits[user,trait] et itemTraits[item,trait] representent les préférences cachees
  • Facteur produit scalaire : Calcule l’affinite entre un utilisateur et un item
  • Observations : rating[obs] sont les notes observees, conditionnees par l’affinite et la precision du bruit
  • Hyperparametre : noisePrecision contrôle la variance des observations

Ce modèle suppose que les préférences sont expliquees par un petit nombre de traits latents (ici 2), permettant de generaliser a des paires (user, item) non observees.

// Affichage du graphe factoriel de la factorisation matricielle
display(HTML(FactorGraphHelper.GetLatestFactorGraphHtml()));
Model_08_26_26_17_45_22_78.svg
Model node0 userTraits[userIndex[obs],trait] node1 Multiply node0->node1 a node3 produits[trait][obs] node1->node3 node2 itemTraits[itemIndex[obs],trait] node2->node1 b node8 produits[obs] node3->node8 node4 0 node5 Gaussian node4->node5 mean node7 itemTraits[item,trait] node5->node7 node6 1 node6->node5 precision node7->node2 node9 Sum node8->node9 array node10 affinite[obs] node9->node10 node11 Gaussian node10->node11 mean node13 rating[obs] node11->node13 node12 noisePrecision node12->node11 precision node14 2 node15 Sample node14->node15 shape node15->node12 node16 0,5 node16->node15 scale node17 0 node18 Gaussian node17->node18 mean node20 userTraits[user,trait] node18->node20 node19 1 node19->node18 precision node20->node0

3bis. Diagnostic : pourquoi la latente EP s’effondre

La section precedente echoue a separer des traits latents. L’intuition premiere — « pas assez de donnees » — se teste : ici nous presque doublons les observations (8 -> 15), elargissons le prior (precision 1.0 -> 0.1) et ajoutons un biais global. Si le volume de donnees etait la cause, la latente devrait se separer.

Elle ne se separe pas : les traits ressortent a exactement zero et toutes les predictions valent la meme chose. Ce n’est ni du bruit, ni un tirage malchanceux (l’inference EP est deterministe) — c’est un phenomene de symetrie que cette section mesure en trois etapes :

  1. Constater l’effondrement sur le modele renforce, avec des metriques explicites (max|U|, ecart-type latent, RMSE).
  2. Tester la cause : briser la symetrie par initialisation asymetrique (Variation A), puis par contrainte de signe (Variation B).
  3. Conclure sur ce qu’EP peut et ne peut pas faire ici — et pourquoi le meme modele apprend sous NUTS (jumeau PyMC-15, section 3bis « OOS »).

Un jeu de donnees structure — et apprenable

Nous creons un jeu de donnees avec un pattern clair : - Users 0, 1 : preferent les items 0, 1 (profil « action ») — notes 4-5 - Users 2, 3 : preferent les items 3, 4 (profil « romance ») — notes 4-5 ; ils notent l’item 2 (1 et 2) - User 4 : profil mixte (notes 3-4)

Ce pattern n’est pas une vue de l’esprit : le jumeau PyMC de ce notebook (PyMC-15-Recommenders, section 3bis « OOS ») apprend une structure equivalente sur sa propre grille. La question de cette section n’est donc pas « le probleme est-il apprenable ? » mais « pourquoi EP, lui, ne l’apprend-il pas ? ».

// Factorisation corrigee avec plus de donnees

// Configuration amelioree
int nUsers2 = 5;
int nItems2 = 5;
int nTraits2 = 2;  // Reduire les traits si peu de donnees

// Plus d'observations (15 notes au lieu de 8)
// Pattern : Users 0,1 aiment items 0,1 (action), Users 2,3 aiment items 2,3 (romance)
int[] userObs2 = { 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4 };
int[] itemObs2 = { 0, 1, 2, 0, 1, 3, 2, 3, 4, 2, 3, 4, 0, 2, 4 };
double[] noteObs2 = { 5, 5, 2, 4, 5, 1, 2, 5, 4, 1, 4, 5, 4, 3, 3 };
int nObs2 = userObs2.Length;

Console.WriteLine("=== Factorisation Corrigee ===");
Console.WriteLine($"\nParametres : {nUsers2} users, {nItems2} items, {nTraits2} traits");
Console.WriteLine($"Observations : {nObs2} notes");
Console.WriteLine($"Ratio donnees/parametres : {nObs2} / {(nUsers2 + nItems2) * nTraits2} = {(double)nObs2 / ((nUsers2 + nItems2) * nTraits2):F2}");

// Afficher la matrice de notes (partiellement observee)
Console.WriteLine("\nMatrice de notes :");
Console.WriteLine("       Item0  Item1  Item2  Item3  Item4");
for (int u = 0; u < nUsers2; u++)
{
    Console.Write($"User{u}   ");
    for (int i = 0; i < nItems2; i++)
    {
        double note = double.NaN;
        for (int o = 0; o < nObs2; o++)
        {
            if (userObs2[o] == u && itemObs2[o] == i) { note = noteObs2[o]; break; }
        }
        Console.Write(double.IsNaN(note) ? "  -    " : $" {note:F0}     ");
    }
    Console.WriteLine();
}
=== Factorisation Corrigee ===

Parametres : 5 users, 5 items, 2 traits
Observations : 15 notes
Ratio donnees/parametres : 15 / 20 = 0,75

Matrice de notes :
       Item0  Item1  Item2  Item3  Item4
User0    5      5      2       -      -    
User1    4      5       -     1       -    
User2     -      -     2      5      4     
User3     -      -     1      4      5     
User4    4       -     3       -     3     

Definition du modele renforce

Les modifications par rapport au modele original :

parametre Avant Apres Effet attendu
Prior precision 1.0 0.1 Moins de regularisation : plus de liberte pour les traits
Biais global Non Oui (prior centre sur 3) Le biais porte la moyenne, les traits n’ont plus qu’a coder les ecarts

Pourquoi un biais global ? Sans biais, le modele doit expliquer la moyenne des notes (~3.5) uniquement via les produits \(U \cdot V\), ce qui est difficile. Le biais capture cette baseline, laissant les traits encoder les deviations par rapport a la moyenne — un reflexe standard de factorisation matricielle.

Ces deux changements paraissent raisonnables. Les sorties ci-dessous montrent qu’ils ne suffisent pas : le probleme n’est ni la regularisation ni l’echelle, c’est la symetrie du modele.

// Modele avec priors ajustes

Range userRange2 = new Range(nUsers2).Named("user2");
Range itemRange2 = new Range(nItems2).Named("item2");
Range traitRange2 = new Range(nTraits2).Named("trait2");
Range obsRange2 = new Range(nObs2).Named("obs2");

// Prior plus large (precision 0.1 au lieu de 1) pour eviter l'ecrasement vers 0
double priorPrec = 0.1;  // Precision faible = variance elevee = plus de liberte

// Traits utilisateurs avec prior large
VariableArray2D<double> userTraits2 = Variable.Array<double>(userRange2, traitRange2).Named("userTraits2");
userTraits2[userRange2, traitRange2] = Variable.GaussianFromMeanAndPrecision(0, priorPrec).ForEach(userRange2, traitRange2);

// Traits items avec prior large
VariableArray2D<double> itemTraits2 = Variable.Array<double>(itemRange2, traitRange2).Named("itemTraits2");
itemTraits2[itemRange2, traitRange2] = Variable.GaussianFromMeanAndPrecision(0, priorPrec).ForEach(itemRange2, traitRange2);

// Biais global (moyenne des notes ~ 3)
Variable<double> globalBias2 = Variable.GaussianFromMeanAndPrecision(3, 1).Named("globalBias2");

// Precision du bruit
Variable<double> noisePrecision2 = Variable.GammaFromShapeAndScale(2, 0.5).Named("noisePrecision2");

// Observations indexees
VariableArray<int> userIndex2 = Variable.Observed(userObs2, obsRange2).Named("userIndex2");
VariableArray<int> itemIndex2 = Variable.Observed(itemObs2, obsRange2).Named("itemIndex2");
VariableArray<double> rating2 = Variable.Observed(noteObs2, obsRange2).Named("rating2");

// Modele avec biais
using (Variable.ForEach(obsRange2))
{
    VariableArray<double> produits2 = Variable.Array<double>(traitRange2).Named("produits2");
    produits2[traitRange2] = userTraits2[userIndex2[obsRange2], traitRange2] * itemTraits2[itemIndex2[obsRange2], traitRange2];
    
    Variable<double> affinite2 = globalBias2 + Variable.Sum(produits2);
    rating2[obsRange2] = Variable.GaussianFromMeanAndPrecision(affinite2, noisePrecision2);
}

Console.WriteLine("\nModele corrige avec :");
Console.WriteLine($"  - Prior precision : {priorPrec} (vs 1.0 avant)");
Console.WriteLine($"  - Biais global : oui");
Console.WriteLine($"  - Ratio donnees/params : {(double)nObs2 / ((nUsers2 + nItems2) * nTraits2):F2} (vs 0.4 avant)");

Modele corrige avec :
  - Prior precision : 0,1 (vs 1.0 avant)
  - Biais global : oui
  - Ratio donnees/params : 0,75 (vs 0.4 avant)

Un ground truth que le modele devrait pouvoir apprendre

Sortie obtenue : 15 observations, ratio donnees/parametres 0.75, couverture 60 % de la matrice.

Bloc Users Items Notes observees
« action » 0, 1 0, 1 4-5
« romance » 2, 3 3, 4 4-5
croisements 0,1 vs 3,4 items adverses 1-2 (user 1 -> item 3 : 1 ; users 2,3 -> item 2 : 2 et 1)

La matrice porte une structure en blocs nette : deux clusters d’utilisateurs aux gouts opposes. Un modele de factorisation qui l’apprend doit predire haut dans les blocs et bas sur les croisements — ce critere structurel servira de juge dans toute la suite.

Execution de l’inference

Inference EP deterministe (compilation Roslyn) : le modele ne contient aucun alea, deux executions donnent exactement les memes valeurs. Ce determinisme est important pour le diagnostic — si les traits ressortent a zero, ce n’est pas « une mauvaise tire ».

// Inference du modele corrige

InferenceEngine moteur3 = new InferenceEngine();
moteur3.Compiler.CompilerChoice = CompilerChoice.Roslyn;
moteur3.Algorithm = new ExpectationPropagation();
moteur3.ShowProgress = false;

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

var userTraitsPost2 = moteur3.Infer<Gaussian[,]>(userTraits2);
var itemTraitsPost2 = moteur3.Infer<Gaussian[,]>(itemTraits2);
var globalBiasPost2 = moteur3.Infer<Gaussian>(globalBias2);

Console.WriteLine($"Biais global : {globalBiasPost2.GetMean():F2}");

Console.WriteLine("\nTraits utilisateurs :");
for (int u = 0; u < nUsers2; u++)
{
    Console.Write($"  User {u} : [");
    for (int t = 0; t < nTraits2; t++)
    {
        Console.Write($"{userTraitsPost2[u, t].GetMean():F2}");
        if (t < nTraits2 - 1) Console.Write(", ");
    }
    Console.WriteLine("]");
}

Console.WriteLine("\nTraits items :");
for (int i = 0; i < nItems2; i++)
{
    Console.Write($"  Item {i} : [");
    for (int t = 0; t < nTraits2; t++)
    {
        Console.Write($"{itemTraitsPost2[i, t].GetMean():F2}");
        if (t < nTraits2 - 1) Console.Write(", ");
    }
    Console.WriteLine("]");
}
moteur3.ShowFactorGraph = true;

=== Inference Factorisation Corrigee ===

compilation had 7 warning(s).
  [1] This model will consume excess memory due to the indexing expression userTraits2[userIndex2[obs2], trait2] inside of a loop over obs2. Try simplifying this expression in your model, perhaps by creating auxiliary index arrays.  If the index is a function of obs2, try creating an array over obs2 holding the index.
  [2] This model will consume excess memory due to the indexing expression itemTraits2[itemIndex2[obs2], trait2] inside of a loop over obs2. Try simplifying this expression in your model, perhaps by creating auxiliary index arrays.  If the index is a function of obs2, try creating an array over obs2 holding the index.
  [3] GaussianProductOp.AAverageConditional(produits2_B[obs2][trait2], userTraits2_rep_F[obs2][userIndex2[obs2], trait2], itemTraits2_rep_F[obs2][itemIndex2[obs2], trait2]) has quality band Experimental which is less than the recommended quality band (Preview)
  [4] GaussianProductOp.ProductAverageConditional(produits2_B[obs2][trait2], userTraits2_rep_F[obs2][userIndex2[obs2], trait2], itemTraits2_rep_F[obs2][itemIndex2[obs2], trait2]) has quality band Experimental which is less than the recommended quality band (Preview)
  [5] GaussianProductOp.AAverageConditional(produits2_B[obs2][trait2], userTraits2_rep_F[obs2][userIndex2[obs2], trait2], itemTraits2_rep_F[obs2][itemIndex2[obs2], trait2]) has quality band Experimental which is less than the recommended quality band (Preview)
  [6] GaussianProductOp.BAverageConditional(produits2_B[obs2][trait2], userTraits2_rep_F[obs2][userIndex2[obs2], trait2], itemTraits2_rep_F[obs2][itemIndex2[obs2], trait2]) has quality band Experimental which is less than the recommended quality band (Preview)
  [7] GaussianProductOp.ProductAverageConditional(produits2_B[obs2][trait2], userTraits2_rep_F[obs2][userIndex2[obs2], trait2], itemTraits2_rep_F[obs2][itemIndex2[obs2], trait2]) has quality band Experimental which is less than the recommended quality band (Preview)
Biais global : 3,29

Traits utilisateurs :
  User 0 : [-0,00, -0,00]
  User 1 : [-0,00, -0,00]
  User 2 : [-0,00, -0,00]
  User 3 : [-0,00, -0,00]
  User 4 : [0,00, 0,00]

Traits items :
  Item 0 : [0,00, 0,00]
  Item 1 : [-0,00, -0,00]
  Item 2 : [0,00, 0,00]
  Item 3 : [0,00, 0,00]
  Item 4 : [0,00, 0,00]

Visualisation du graphe factoriel - modèle corrige

Le graphe ci-dessous montre le modèle de factorisation ameliore avec :

  • Plus de données : 15 observations au lieu de 8, avec un pattern clair (2 groupes d’utilisateurs)
  • Biais global : globalBias2 capture la moyenne générale des notes
  • Priors plus larges : Precision 0.1 au lieu de 1.0 pour permettre plus de variation dans les traits
  • Structure identique : Toujours U[user,trait] * V[item,trait] pour l’affinite

Le modèle corrige devrait montrer des traits latents plus informatifs grace au ratio observations/paramètres ameliore.

// Affichage du graphe factoriel du modele corrige
display(HTML(FactorGraphHelper.GetLatestFactorGraphHtml()));
Model_08_26_26_17_45_22_78.svg
Model node0 userTraits[userIndex[obs],trait] node1 Multiply node0->node1 a node3 produits[trait][obs] node1->node3 node2 itemTraits[itemIndex[obs],trait] node2->node1 b node8 produits[obs] node3->node8 node4 0 node5 Gaussian node4->node5 mean node7 itemTraits[item,trait] node5->node7 node6 1 node6->node5 precision node7->node2 node9 Sum node8->node9 array node10 affinite[obs] node9->node10 node11 Gaussian node10->node11 mean node13 rating[obs] node11->node13 node12 noisePrecision node12->node11 precision node14 2 node15 Sample node14->node15 shape node15->node12 node16 0,5 node16->node15 scale node17 0 node18 Gaussian node17->node18 mean node20 userTraits[user,trait] node18->node20 node19 1 node19->node18 precision node20->node0

Generation des predictions

Nous calculons maintenant les notes predites pour toutes les paires (user, item) :

\[\hat{r}_{ui} = \text{biais} + \sum_{t} U_{u,t} \cdot V_{i,t}\]

Les notes entre crochets [X.X] correspondent aux observations utilisees pour l’entrainement. Les autres sont des predictions pour des paires non observees.

// Predictions avec le modele corrige

Console.WriteLine("\n=== Predictions Corrigees ===\n");

// Matrice de predictions
Console.WriteLine("Matrice de notes predites (observees entre crochets) :");
Console.WriteLine("         Item0   Item1   Item2   Item3   Item4");

double bias = globalBiasPost2.GetMean();

for (int u = 0; u < nUsers2; u++)
{
    Console.Write($"User {u}   ");
    for (int i = 0; i < nItems2; i++)
    {
        // Prediction = biais + produit scalaire
        double pred = bias;
        for (int t = 0; t < nTraits2; t++)
        {
            pred += userTraitsPost2[u, t].GetMean() * itemTraitsPost2[i, t].GetMean();
        }
        
        // Verifier si observe
        bool observe = false;
        double noteReelle = 0;
        for (int o = 0; o < nObs2; o++)
        {
            if (userObs2[o] == u && itemObs2[o] == i) 
            { 
                observe = true; 
                noteReelle = noteObs2[o];
                break; 
            }
        }
        
        if (observe)
            Console.Write($"[{pred:F1}]   ");
        else
            Console.Write($" {pred:F1}    ");
    }
    Console.WriteLine();
}

// Top recommandation par utilisateur
Console.WriteLine("\n=== Top Recommandations ===");
for (int u = 0; u < nUsers2; u++)
{
    double maxPred = double.MinValue;
    int bestItem = -1;
    
    for (int i = 0; i < nItems2; i++)
    {
        // Ignorer si deja observe
        bool observe = false;
        for (int o = 0; o < nObs2; o++)
        {
            if (userObs2[o] == u && itemObs2[o] == i) { observe = true; break; }
        }
        if (observe) continue;
        
        double pred = bias;
        for (int t = 0; t < nTraits2; t++)
        {
            pred += userTraitsPost2[u, t].GetMean() * itemTraitsPost2[i, t].GetMean();
        }
        
        if (pred > maxPred) { maxPred = pred; bestItem = i; }
    }
    
    Console.WriteLine($"  User {u} -> Item {bestItem} (score predit: {maxPred:F2})");
}

// --- Mesures de l'effondrement (diagnostic) ---
double maxAbsUser2 = 0.0, maxAbsItem2 = 0.0;
for (int u = 0; u < nUsers2; u++)
    for (int t = 0; t < nTraits2; t++)
        maxAbsUser2 = Math.Max(maxAbsUser2, Math.Abs(userTraitsPost2[u, t].GetMean()));
for (int i = 0; i < nItems2; i++)
    for (int t = 0; t < nTraits2; t++)
        maxAbsItem2 = Math.Max(maxAbsItem2, Math.Abs(itemTraitsPost2[i, t].GetMean()));

List<double> contributions2 = new List<double>();
for (int u = 0; u < nUsers2; u++)
    for (int i = 0; i < nItems2; i++)
    {
        double c = 0.0;
        for (int t = 0; t < nTraits2; t++) c += userTraitsPost2[u, t].GetMean() * itemTraitsPost2[i, t].GetMean();
        contributions2.Add(c);
    }
double meanContrib2 = contributions2.Average();
double stdContrib2 = Math.Sqrt(contributions2.Select(c => (c - meanContrib2) * (c - meanContrib2)).Average());

double meanObs2 = noteObs2.Average();
double rmseModel2 = 0.0, rmseMean2 = 0.0;
for (int o = 0; o < nObs2; o++)
{
    double pred = bias;
    for (int t = 0; t < nTraits2; t++) pred += userTraitsPost2[userObs2[o], t].GetMean() * itemTraitsPost2[itemObs2[o], t].GetMean();
    rmseModel2 += (pred - noteObs2[o]) * (pred - noteObs2[o]);
    rmseMean2 += (meanObs2 - noteObs2[o]) * (meanObs2 - noteObs2[o]);
}
rmseModel2 = Math.Sqrt(rmseModel2 / nObs2);
rmseMean2 = Math.Sqrt(rmseMean2 / nObs2);

Console.WriteLine();
Console.WriteLine("=== Mesures de l'effondrement ===");
Console.WriteLine($"max|U| = {maxAbsUser2:F4} ; max|V| = {maxAbsItem2:F4}");
Console.WriteLine($"Ecart-type des contributions latentes U.V = {stdContrib2:F4}");
Console.WriteLine($"RMSE sur les 15 observations : modele = {rmseModel2:F3} ; constante (moyenne {meanObs2:F2}) = {rmseMean2:F3}");

=== Predictions Corrigees ===

Matrice de notes predites (observees entre crochets) :
         Item0   Item1   Item2   Item3   Item4
User 0   [3,3]   [3,3]   [3,3]    3,3     3,3    
User 1   [3,3]   [3,3]    3,3    [3,3]    3,3    
User 2    3,3     3,3    [3,3]   [3,3]   [3,3]   
User 3    3,3     3,3    [3,3]   [3,3]   [3,3]   
User 4   [3,3]    3,3    [3,3]    3,3    [3,3]   

=== Top Recommandations ===
  User 0 -> Item 3 (score predit: 3,29)
  User 1 -> Item 2 (score predit: 3,29)
  User 2 -> Item 0 (score predit: 3,29)
  User 3 -> Item 0 (score predit: 3,29)
  User 4 -> Item 1 (score predit: 3,29)

=== Mesures de l'effondrement ===
max|U| = 0,0000 ; max|V| = 0,0000
Ecart-type des contributions latentes U.V = 0,0000
RMSE sur les 15 observations : modele = 1,430 ; constante (moyenne 3,53) = 1,408

Pourquoi exactement zero ? L’hypothese de symetrie, et comment la tester

Le produit \(U_{u,t} \cdot V_{i,t}\) est invariant par le changement de signe global \((U, V) \to (-U, -V)\) : deux solutions en miroir expliquent les donnees tout aussi bien. EP approxime chaque marginal posterieur par une seule gaussienne ; si le posterior est reparti sur deux modes en miroir, la meilleure approximation gaussienne unique est centree entre eux — exactement zero.

Cette lecture est testable, et les deux tests ne disent pas la meme chose :

  • Variation A — si zero n’est qu’un point de depart symetrique, initialiser les messages EP ailleurs (moyennes non nulles et distinctes par user/item/trait) doit permettre de s’en echapper.
  • Variation B — si la symetrie contraint vraiment la solution, seule une contrainte qui la brise explicitement (signe non negatif sur le trait 0, a la maniere d’une factorisation non-negative) peut deplacer l’inference.
// Variation A : briser la symetrie par INITIALISATION asymetrique (InitialiseTo)
// Modele identique (priors centres, precision 0.1, biais, 15 obs) -- seule differe
// l'initialisation des messages EP : moyennes non nulles, distinctes, symetrie brisee.

Range initUserRange = new Range(nUsers2).Named("initUser");
Range initItemRange = new Range(nItems2).Named("initItem");
Range initObsRange = new Range(nObs2).Named("initObs");

var initU0 = Variable.Array<double>(initUserRange).Named("initU0");
var initU1 = Variable.Array<double>(initUserRange).Named("initU1");
var initV0 = Variable.Array<double>(initItemRange).Named("initV0");
var initV1 = Variable.Array<double>(initItemRange).Named("initV1");
initU0[initUserRange] = Variable.GaussianFromMeanAndPrecision(0, priorPrec).ForEach(initUserRange);
initU1[initUserRange] = Variable.GaussianFromMeanAndPrecision(0, priorPrec).ForEach(initUserRange);
initV0[initItemRange] = Variable.GaussianFromMeanAndPrecision(0, priorPrec).ForEach(initItemRange);
initV1[initItemRange] = Variable.GaussianFromMeanAndPrecision(0, priorPrec).ForEach(initItemRange);

var initBias = Variable.GaussianFromMeanAndPrecision(3, 1).Named("initBias");
var initNoise = Variable.GammaFromShapeAndScale(2, 0.5).Named("initNoise");

VariableArray<int> initUserIndex = Variable.Observed(userObs2, initObsRange).Named("initUserIndex");
VariableArray<int> initItemIndex = Variable.Observed(itemObs2, initObsRange).Named("initItemIndex");
VariableArray<double> initRating = Variable.Observed(noteObs2, initObsRange).Named("initRating");

using (Variable.ForEach(initObsRange))
{
    Variable<double> initAff = initBias
        + initU0[initUserIndex[initObsRange]] * initV0[initItemIndex[initObsRange]]
        + initU1[initUserIndex[initObsRange]] * initV1[initItemIndex[initObsRange]];
    initRating[initObsRange] = Variable.GaussianFromMeanAndPrecision(initAff, initNoise);
}

for (int t = 0; t < nTraits2; t++)
{
    var uInit = new Gaussian[nUsers2];
    var vInit = new Gaussian[nItems2];
    for (int u = 0; u < nUsers2; u++) uInit[u] = Gaussian.FromMeanAndVariance(0.3 + 0.1 * ((u + 2 * t) % 5), 1.0);
    for (int i = 0; i < nItems2; i++) vInit[i] = Gaussian.FromMeanAndVariance(0.2 + 0.1 * ((i + t) % 4), 1.0);
    (t == 0 ? initU0 : initU1).InitialiseTo(Distribution<double>.Array(uInit));
    (t == 0 ? initV0 : initV1).InitialiseTo(Distribution<double>.Array(vInit));
}

InferenceEngine initEngine = new InferenceEngine();
initEngine.Compiler.CompilerChoice = CompilerChoice.Roslyn;
initEngine.Algorithm = new ExpectationPropagation();
initEngine.ShowProgress = false;

var initUPost0 = initEngine.Infer<Gaussian[]>(initU0);
var initUPost1 = initEngine.Infer<Gaussian[]>(initU1);
var initVPost0 = initEngine.Infer<Gaussian[]>(initV0);
var initVPost1 = initEngine.Infer<Gaussian[]>(initV1);

double initMaxU = 0.0, initMaxV = 0.0;
for (int u = 0; u < nUsers2; u++)
    initMaxU = Math.Max(initMaxU, Math.Max(Math.Abs(initUPost0[u].GetMean()), Math.Abs(initUPost1[u].GetMean())));
for (int i = 0; i < nItems2; i++)
    initMaxV = Math.Max(initMaxV, Math.Max(Math.Abs(initVPost0[i].GetMean()), Math.Abs(initVPost1[i].GetMean())));
double initBiasMean = initEngine.Infer<Gaussian>(initBias).GetMean();

Console.WriteLine("=== Variation A : initialisation asymetrique (InitialiseTo) ===");
Console.WriteLine("Messages initiaux : moyennes non nulles (0.2 a 0.7), distinctes par (user, item, trait)");
Console.WriteLine($"APRES inference : max|U| = {initMaxU:F4} ; max|V| = {initMaxV:F4} ; biais = {initBiasMean:F2}");
Console.WriteLine(initMaxU < 0.01
    ? "-> RETOUR EXACT AU POINT FIXE SYMETRIQUE : l'initialisation asymetrique est absorbee par les iterations EP."
    : "-> la latente a echappe au zero.");
compilation had 8 warning(s).
  [1] GaussianProductOp.ProductAverageConditional(vdouble91_B[initObs], initU1_iteminitUserIndex_initObs__F[initObs], initV1_iteminitItemIndex_initObs__F[initObs]) has quality band Experimental which is less than the recommended quality band (Preview)
  [2] GaussianProductOp.ProductAverageConditional(vdouble87_B[initObs], initU0_iteminitUserIndex_initObs__F[initObs], initV0_iteminitItemIndex_initObs__F[initObs]) has quality band Experimental which is less than the recommended quality band (Preview)
  [3] GaussianProductOp.AAverageConditional(vdouble87_B[initObs], initU0_iteminitUserIndex_initObs__F[initObs], initV0_iteminitItemIndex_initObs__F[initObs]) has quality band Experimental which is less than the recommended quality band (Preview)
  [4] GaussianProductOp.BAverageConditional(vdouble87_B[initObs], initU0_iteminitUserIndex_initObs__F[initObs], initV0_iteminitItemIndex_initObs__F[initObs]) has quality band Experimental which is less than the recommended quality band (Preview)
  [5] GaussianProductOp.ProductAverageConditional(vdouble87_B[initObs], initU0_iteminitUserIndex_initObs__F[initObs], initV0_iteminitItemIndex_initObs__F[initObs]) has quality band Experimental which is less than the recommended quality band (Preview)
  [6] GaussianProductOp.AAverageConditional(vdouble91_B[initObs], initU1_iteminitUserIndex_initObs__F[initObs], initV1_iteminitItemIndex_initObs__F[initObs]) has quality band Experimental which is less than the recommended quality band (Preview)
  [7] GaussianProductOp.BAverageConditional(vdouble91_B[initObs], initU1_iteminitUserIndex_initObs__F[initObs], initV1_iteminitItemIndex_initObs__F[initObs]) has quality band Experimental which is less than the recommended quality band (Preview)
  [8] GaussianProductOp.ProductAverageConditional(vdouble91_B[initObs], initU1_iteminitUserIndex_initObs__F[initObs], initV1_iteminitItemIndex_initObs__F[initObs]) has quality band Experimental which is less than the recommended quality band (Preview)
=== Variation A : initialisation asymetrique (InitialiseTo) ===
Messages initiaux : moyennes non nulles (0.2 a 0.7), distinctes par (user, item, trait)
APRES inference : max|U| = 0,0000 ; max|V| = 0,0000 ; biais = 3,29
-> RETOUR EXACT AU POINT FIXE SYMETRIQUE : l'initialisation asymetrique est absorbee par les iterations EP.

Resultat A : max|U| = 0.0000, max|V| = 0.0000 — retour exact au point symetrique, malgre des messages initiaux non nulles et distincts. Zero n’est pas un point de depart mal choisi : c’est un point fixe attractif des iterations EP, qui absorbe les petites perturbations. Rien ne sert de « mieux initialiser ».

Il reste une maniere de forcer le depart : ne pas demander a l’initialisation de briser la symetrie, mais l’interdire dans le modele.

// Variation B : briser la symetrie par CONTRAINTE DE SIGNE (trait 0 non negatif)
// Trait 0 = "intensite" (>= 0 pour users ET items, saveur factorisation non-negative) ;
// trait 1 = libre ("direction"). Priors, donnees et moteur inchanges par ailleurs.

Range nnUserRange = new Range(nUsers2).Named("nnUser");
Range nnItemRange = new Range(nItems2).Named("nnItem");
Range nnTraitRange = new Range(nTraits2).Named("nnTrait");
Range nnObsRange = new Range(nObs2).Named("nnObs");

var nnUserTraits = Variable.Array<double>(nnUserRange, nnTraitRange).Named("nnUserTraits");
nnUserTraits[nnUserRange, nnTraitRange] = Variable.GaussianFromMeanAndPrecision(0, priorPrec).ForEach(nnUserRange, nnTraitRange);
var nnItemTraits = Variable.Array<double>(nnItemRange, nnTraitRange).Named("nnItemTraits");
nnItemTraits[nnItemRange, nnTraitRange] = Variable.GaussianFromMeanAndPrecision(0, priorPrec).ForEach(nnItemRange, nnTraitRange);

var nnBias = Variable.GaussianFromMeanAndPrecision(3, 1).Named("nnBias");
var nnNoise = Variable.GammaFromShapeAndScale(2, 0.5).Named("nnNoise");

VariableArray<int> nnUserIndex = Variable.Observed(userObs2, nnObsRange).Named("nnUserIndex");
VariableArray<int> nnItemIndex = Variable.Observed(itemObs2, nnObsRange).Named("nnItemIndex");
VariableArray<double> nnRating = Variable.Observed(noteObs2, nnObsRange).Named("nnRating");

using (Variable.ForEach(nnObsRange))
{
    VariableArray<double> nnProd = Variable.Array<double>(nnTraitRange).Named("nnProd");
    nnProd[nnTraitRange] = nnUserTraits[nnUserIndex[nnObsRange], nnTraitRange] * nnItemTraits[nnItemIndex[nnObsRange], nnTraitRange];
    Variable<double> nnAff = nnBias + Variable.Sum(nnProd);
    nnRating[nnObsRange] = Variable.GaussianFromMeanAndPrecision(nnAff, nnNoise);
}

for (int u = 0; u < nUsers2; u++) Variable.ConstrainPositive(nnUserTraits[u, 0]);
for (int i = 0; i < nItems2; i++) Variable.ConstrainPositive(nnItemTraits[i, 0]);

InferenceEngine nnEngine = new InferenceEngine();
nnEngine.Compiler.CompilerChoice = CompilerChoice.Roslyn;
nnEngine.Algorithm = new ExpectationPropagation();
nnEngine.ShowProgress = false;

var nnUPost = nnEngine.Infer<Gaussian[,]>(nnUserTraits);
var nnVPost = nnEngine.Infer<Gaussian[,]>(nnItemTraits);
double nnBiasMean = nnEngine.Infer<Gaussian>(nnBias).GetMean();

double nnMaxU = 0.0, nnMaxV = 0.0;
for (int u = 0; u < nUsers2; u++)
    for (int t = 0; t < nTraits2; t++) nnMaxU = Math.Max(nnMaxU, Math.Abs(nnUPost[u, t].GetMean()));
for (int i = 0; i < nItems2; i++)
    for (int t = 0; t < nTraits2; t++) nnMaxV = Math.Max(nnMaxV, Math.Abs(nnVPost[i, t].GetMean()));

Console.WriteLine("=== Variation B : contrainte de signe (trait 0 >= 0) ===");
Console.WriteLine($"max|U| = {nnMaxU:F4} ; max|V| = {nnMaxV:F4} ; biais = {nnBiasMean:F2}");

Console.WriteLine();
Console.WriteLine("Matrice predite (observations entre crochets [vrai > predit]) :");
Console.WriteLine("         Item0   Item1   Item2   Item3   Item4");
double nnRmse = 0.0;
for (int u = 0; u < nUsers2; u++)
{
    Console.Write($"User {u}   ");
    for (int i = 0; i < nItems2; i++)
    {
        double p = nnBiasMean;
        for (int t = 0; t < nTraits2; t++) p += nnUPost[u, t].GetMean() * nnVPost[i, t].GetMean();
        bool ob = false; double vrai = 0;
        for (int o = 0; o < nObs2; o++)
            if (userObs2[o] == u && itemObs2[o] == i) { ob = true; vrai = noteObs2[o]; break; }
        if (ob) { Console.Write($"[{vrai}>{p:F2}]  "); nnRmse += (p - vrai) * (p - vrai); }
        else Console.Write($" {p:F2}    ");
    }
    Console.WriteLine();
}
nnRmse = Math.Sqrt(nnRmse / nObs2);
Console.WriteLine($"RMSE sur les observations = {nnRmse:F3} (modele effondre : {rmseModel2:F3} ; constante : {rmseMean2:F3})");
compilation had 7 warning(s).
  [1] This model will consume excess memory due to the indexing expression nnUserTraits[nnUserIndex[nnObs], nnTrait] inside of a loop over nnObs. Try simplifying this expression in your model, perhaps by creating auxiliary index arrays.  If the index is a function of nnObs, try creating an array over nnObs holding the index.
  [2] This model will consume excess memory due to the indexing expression nnItemTraits[nnItemIndex[nnObs], nnTrait] inside of a loop over nnObs. Try simplifying this expression in your model, perhaps by creating auxiliary index arrays.  If the index is a function of nnObs, try creating an array over nnObs holding the index.
  [3] GaussianProductOp.AAverageConditional(nnProd_B[nnObs][nnTrait], nnUserTraits_rep_F[nnObs][nnUserIndex[nnObs], nnTrait], nnItemTraits_rep_F[nnObs][nnItemIndex[nnObs], nnTrait]) has quality band Experimental which is less than the recommended quality band (Preview)
  [4] GaussianProductOp.ProductAverageConditional(nnProd_B[nnObs][nnTrait], nnUserTraits_rep_F[nnObs][nnUserIndex[nnObs], nnTrait], nnItemTraits_rep_F[nnObs][nnItemIndex[nnObs], nnTrait]) has quality band Experimental which is less than the recommended quality band (Preview)
  [5] GaussianProductOp.AAverageConditional(nnProd_B[nnObs][nnTrait], nnUserTraits_rep_F[nnObs][nnUserIndex[nnObs], nnTrait], nnItemTraits_rep_F[nnObs][nnItemIndex[nnObs], nnTrait]) has quality band Experimental which is less than the recommended quality band (Preview)
  [6] GaussianProductOp.BAverageConditional(nnProd_B[nnObs][nnTrait], nnUserTraits_rep_F[nnObs][nnUserIndex[nnObs], nnTrait], nnItemTraits_rep_F[nnObs][nnItemIndex[nnObs], nnTrait]) has quality band Experimental which is less than the recommended quality band (Preview)
  [7] GaussianProductOp.ProductAverageConditional(nnProd_B[nnObs][nnTrait], nnUserTraits_rep_F[nnObs][nnUserIndex[nnObs], nnTrait], nnItemTraits_rep_F[nnObs][nnItemIndex[nnObs], nnTrait]) has quality band Experimental which is less than the recommended quality band (Preview)
=== Variation B : contrainte de signe (trait 0 >= 0) ===
max|U| = 1,5205 ; max|V| = 1,9492 ; biais = 2,30

Matrice predite (observations entre crochets [vrai > predit]) :
         Item0   Item1   Item2   Item3   Item4
User 0   [5>4,36]  [5>4,89]  [2>2,75]   3,71     3,76    
User 1   [4>3,74]  [5>4,11]   2,61    [1>3,28]   3,32    
User 2    4,66     5,26    [2>2,82]  [5>3,91]  [4>3,97]  
User 3    4,65     5,25    [1>2,81]  [4>3,90]  [5>3,96]  
User 4   [4>3,88]   4,29    [3>2,64]   3,38    [3>3,42]  
RMSE sur les observations = 0,952 (modele effondre : 1,430 ; constante : 1,408)

Resultat B : cette fois la latente s’echappe (max|U| et max|V| non nuls) et le RMSE observe s’ameliore nettement. Mecaniquement, la contrainte fonctionne.

Mais ce que le modele apprend est le probleme. Le biais chute et le trait non negatif ajoute une intensite positive a toutes les paires : un monde ou tout le monde aime tout, un peu plus ou un peu moins. Le juge est le critere structurel fixe plus haut — les croisements entre clusters doivent etre predits bas. Or la matrice predite donne ses scores les plus eleves aux croisements : les utilisateurs « romance » (notes 5 et 4 sur les items 3-4) se voient predire l’item 1 — le favori du cluster adverse — avec les scores maximaux de toute la matrice.

La contrainte qui brise la symetrie interdit aussi au modele de representer la bipolarite des gouts : un produit de termes non negatifs ne peut pas etre negatif, l’information de « dislike » n’a nulle part ou aller. EP s’est echappe du zero — vers une structure inversee.

Analyse : diagnostic mesure, verdict honnete

Configuration max|U| Ecart-type latent RMSE observe Ce qui est appris
Modele renforce (15 obs, prior large, biais) 0.0000 0.0000 1.43 Rien : prediction constante 3.3
+ Variation A (initialisation asymetrique) 0.0000 0.0000 1.43 Rien : retour au point fixe attractif
+ Variation B (contrainte de signe trait 0) 1.52 0.75 0.95 Une structure inversee (croisements predits hauts)

Ce que les mesures etablissent :

  1. L’effondrement n’est pas un probleme de volume de donnees. La matrice 5x5 porte une structure en blocs nette avec 15 observations ; ce n’est pas elle qui manque. Le jumeau PyMC apprend une structure equivalente sur sa propre grille (8x8) — c’est l’algorithme qui differe, pas la difficulte du probleme.
  2. Ce n’est pas un point de depart mal choisi (Variation A). Partis de messages initiaux non nulles et distincts, les iterations EP retombent exactement a zero : le point symetrique est un attracteur, pas un simple point de passage.
  3. Briser la symetrie par la force est possible mais contre-productif ici (Variation B). La contrainte de signe deplace l’inference hors du zero et ameliore meme le RMSE observe — mais le modele ne peut plus representer le dislike (un produit de termes non negatifs est non negatif), et la structure apprise contredit les donnees : les croisements entre clusters sont predits hauts.

Pourquoi NUTS, lui, apprend. L’echantillonneur visite les deux modes en miroir au lieu de les moyenner en une seule gaussienne ; evalue par score bayesien predictif (moyenne des produits \(U \cdot V\) tirage par tirage), le modele recupere la structure. Le jumeau PyMC-15-Recommenders.ipynb (section 3bis « OOS ») le mesure : RMSE test 1.11 contre 1.95 pour la baseline moyenne, classement aime > non-aime parfait. La section 4bis ci-dessous mesure le meme effondrement EP sur une grille 8x8 complete.

Lecon. Sur un modele non identifiable par symetrie de signe, l’algorithme d’inference n’est pas un detail d’implementation : EP (approximation gaussienne unimodale) moyenne les modes et efface la latente ; l’echantillonnage les traverse. Pour de vraies recommandations sans renoncer aux features explicites, la section 5 prend une route differente : le cold-start par features (age, genre) remplace les facteurs latents appris.

4. Prediction de Notes

Nous utilisons maintenant les posterieurs inferes pour predire des notes sur des paires (utilisateur, item) non observees. Cette étape permet d’evaluer la qualite du modèle et d’identifier les problemes de convergence.

Predictions du modèle original

Cette cellule utilise les posterieurs du premier modèle (8 observations, priors serres) pour illustrer le problème de convergence. Les predictions devraient etre proches de 0.

// Prediction pour des paires (user, item) non observees

Console.WriteLine("\n=== Predictions ===");
Console.WriteLine("\nNotes predites pour paires non observees :");

// Calculer l'affinite predite
for (int u = 0; u < nUsers; u++)
{
    Console.Write($"User {u} : ");
    for (int i = 0; i < nItems; i++)
    {
        // Verifier si observe
        bool observe = false;
        for (int o = 0; o < nObs; o++)
        {
            if (userObs[o] == u && itemObs[o] == i)
            {
                observe = true;
                break;
            }
        }
        
        // Calcul produit scalaire des moyennes
        double pred = 0;
        for (int t = 0; t < nTraits; t++)
        {
            pred += userTraitsPost[u, t].GetMean() * itemTraitsPost[i, t].GetMean();
        }
        
        if (observe)
            Console.Write($"[{pred:F1}] ");
        else
            Console.Write($" {pred:F1}  ");
    }
    Console.WriteLine();
}

Console.WriteLine("\n(Notes entre crochets = observees)");

=== Predictions ===

Notes predites pour paires non observees :
User 0 : [0,0]  0,0  [0,0]  -0,0   -0,0  
User 1 :  -0,0  [-0,0]  -0,0  [0,0]  0,0  
User 2 : [0,0]  0,0  [0,0]  -0,0   -0,0  
User 3 :  -0,0   -0,0  [-0,0]  0,0  [0,0] 

(Notes entre crochets = observees)

Analyse des prédictions

Observations : Toutes les prédictions sont ~0.0

User Prédictions Problème
0-3 0.0 partout Traits latents nuls → produit scalaire nul

Explication :

Le calcul \(\text{rating} = \sum_t U_{u,t} \times V_{i,t}\) donne 0 car : - \(U_{u,t} \approx 0\) pour tous les traits - Même avec \(V_{i,t} \neq 0\), le produit reste ~0

Ce que cela illustre :

  1. Importance des données : La factorisation nécessite suffisamment d’observations
  2. Régularisation implicite : Les priors Gaussian(0,1) tirent vers 0 en l’absence de signal
  3. Diagnostic rapide : Des prédictions uniformes signalent un modèle non appris

Dans un système réel :

Technique Bénéfice
Biais utilisateur/item Capture la moyenne même sans facteurs
Popularity baseline Recommande les items populaires par défaut
Features cold-start Utilise des caractéristiques explicites

4bis. Exemple guide : Evaluation hors echantillon (OOS)

Jusqu’ici, le modele etait evalue sur les memes notes qu’il a apprises : l’erreur mesuree est une borne optimiste (le modele « recopie » l’entrainement, mais a-t-il vraiment appris la structure ?). L’evaluation hors echantillon (out-of-sample, OOS) separe nettement les deux questions :

  1. Un masque fige 16 notes (2 par utilisateur : 1 aimee + 1 non-aimee) que l’inference ne verra jamais ;
  2. Le split principal garantit la representativite : chaque item apparait >= 3 fois en entrainement (rotation du test-aime entre utilisateurs) — sinon la tache deviendrait du cold-start degrade ;
  3. Comparaison des erreurs train vs test : si le test empire bien plus que le train, c’est du sur-apprentissage ;
  4. Baselines : moyenne globale (GM), puis biais utilisateur/item (UIBI) — un modele qui n’apprend rien ne doit pas faire mieux ;
  5. Cold-start = bras separe (nouvel utilisateur sans aucune note), jamais presente comme un apprentissage ;
  6. Metrique de classement : precision pairwise « item aime > item non-aime » par utilisateur de test, ou explication honnete si elle est degeneree.

Nous utilisons la meme grille 8x8 bruitee (seed 42, ecart-type 0.3) que le jumeau Python PyMC : les baselines ci-dessous sont byte-identiques des deux cotes.

// Donnees OOS : grille 8x8 seed 42 (bruit gaussien 0.3, valeurs a 1 decimale) -- identique au jumeau PyMC
int oosNUsers = 8;
int oosNItems = 8;
int oosRank = 1;   // la structure aime/non-aime est un bloc 2x2 -> rang latent 1 suffit

double[,] oosGrid = new double[,] {
    {  5.0,  3.7,  4.2,  4.3,  1.4,  1.0,  2.0,  1.0 },
    {  4.0,  3.7,  4.3,  5.0,  1.0,  2.3,  1.1,  1.7 },
    {  4.1,  3.7,  5.0,  4.0,  1.9,  1.0,  2.4,  1.0 },
    {  3.9,  4.9,  4.2,  4.1,  1.1,  2.1,  1.6,  1.9 },
    {  1.8,  1.0,  2.2,  1.3,  5.0,  3.7,  3.8,  4.2 },
    {  1.2,  2.2,  1.0,  2.1,  4.0,  4.1,  4.3,  5.0 },
    {  2.2,  1.0,  2.1,  1.2,  3.6,  3.9,  4.9,  3.8 },
    {  1.0,  2.4,  1.0,  2.3,  3.5,  4.9,  4.0,  4.2 }
};

// Entrainement : 4 notes par utilisateur (3 aimees + 1 non-aimee) = 32 observations
int[] oosUObs = { 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7 };
int[] oosIObs = { 1, 2, 3, 4, 0, 2, 3, 5, 0, 1, 3, 6, 0, 1, 2, 7, 5, 6, 7, 0, 4, 6, 7, 1, 4, 5, 7, 2, 4, 5, 6, 3 };
double[] oosRObs = {  3.7,  4.2,  4.3,  1.4,  4.0,  4.3,  5.0,  2.3,  4.1,  3.7,  4.0,  2.4,  3.9,  4.9,  4.2,  1.9,  3.7,  3.8,  4.2,  1.8,  4.0,  4.3,  5.0,  2.2,  3.6,  3.9,  3.8,  2.1,  3.5,  4.9,  4.0,  2.3 };

// Test (masque) : 2 notes par utilisateur (1 aimee + 1 non-aimee) = 16 observations invisibles
int[] oosTeU = { 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7 };
int[] oosTeI = { 0, 5, 1, 6, 2, 7, 3, 4, 4, 1, 5, 2, 6, 3, 7, 0 };
double[] oosTeR = {  5.0,  1.0,  3.7,  1.1,  5.0,  1.0,  4.1,  1.1,  5.0,  1.0,  4.1,  1.0,  4.9,  1.2,  4.2,  1.0 };
// 16 autres paires restent inobservees (masque) : bras cold-start / extension, non utilisees par l'inference

Console.WriteLine($"OOS : {oosUObs.Length} notes train, {oosTeU.Length} masquees (test). Grille {oosNUsers}x{oosNItems} (seed 42).");
// Chaque item doit rester represente en entrainement (sinon la tache devient du cold-start)
for (int k = 0; k < oosNItems; k++) {
    int c = 0; for (int o = 0; o < oosIObs.Length; o++) if (oosIObs[o] == k) c++;
    if (c == 0) Console.WriteLine($"  WARN : item {k} absent du train");
}
Console.WriteLine("Representativite : chaque item present en train (0 WARN attendu).");
OOS : 32 notes train, 16 masquees (test). Grille 8x8 (seed 42).
Representativite : chaque item present en train (0 WARN attendu).
// Baselines : moyenne globale (GM) puis biais utilisateur/item (UIBI, descente de coordonnees a somme nulle)
static (double g, double[] bu, double[] bi) OosCoord(int[] uu, int[] ii, double[] rr, int nU, int nI) {
    double g = rr.Average();
    double[] bu = new double[nU], bi = new double[nI];
    for (int it = 0; it < 40; it++) {
        for (int u = 0; u < nU; u++) { double s = 0; int c = 0; for (int o = 0; o < uu.Length; o++) if (uu[o] == u) { s += rr[o] - (g + bi[ii[o]]); c++; } bu[u] = c > 0 ? s / c : 0; }
        double mbu = bu.Average(); for (int u = 0; u < nU; u++) bu[u] -= mbu;
        for (int i = 0; i < nI; i++) { double s = 0; int c = 0; for (int o = 0; o < ii.Length; o++) if (ii[o] == i) { s += rr[o] - (g + bu[uu[o]]); c++; } bi[i] = c > 0 ? s / c : 0; }
        double mbi = bi.Average(); for (int i = 0; i < nI; i++) bi[i] -= mbi;
    }
    return (g, bu, bi);
}
static double OosRmse(double[] p, double[] t) { double s = 0; for (int o = 0; o < p.Length; o++) { double d = p[o] - t[o]; s += d * d; } return Math.Sqrt(s / p.Length); }
static double OosMae(double[] p, double[] t) { double s = 0; for (int o = 0; o < p.Length; o++) s += Math.Abs(p[o] - t[o]); return s / p.Length; }

double oosGm = oosRObs.Average();
double[] oosGmTr = new double[oosRObs.Length], oosGmTe = new double[oosTeR.Length];
for (int o = 0; o < oosRObs.Length; o++) oosGmTr[o] = oosGm;
for (int o = 0; o < oosTeR.Length; o++) oosGmTe[o] = oosGm;
Console.WriteLine($"GM   : rmse_train={OosRmse(oosGmTr, oosRObs):F3}  rmse_test={OosRmse(oosGmTe, oosTeR):F3}");

var (oosG, oosBu, oosBi) = OosCoord(oosUObs, oosIObs, oosRObs, oosNUsers, oosNItems);
double[] oosUiTr = new double[oosRObs.Length], oosUiTe = new double[oosTeR.Length];
for (int o = 0; o < oosRObs.Length; o++) oosUiTr[o] = oosG + oosBu[oosUObs[o]] + oosBi[oosIObs[o]];
for (int o = 0; o < oosTeR.Length; o++) oosUiTe[o] = oosG + oosBu[oosTeU[o]] + oosBi[oosTeI[o]];
Console.WriteLine($"UIBI : rmse_train={OosRmse(oosUiTr, oosRObs):F3}  rmse_test={OosRmse(oosUiTe, oosTeR):F3}  (g={oosG:F3})");
GM   : rmse_train=0,985  rmse_test=1,947
UIBI : rmse_train=0,938  rmse_test=2,012  (g=3,606)
// Modele bayesien (miroir du jumeau PyMC) : note ~ g + bU[u] + bI[i] + U[u].V[i], k=1  -- inference EP
using Microsoft.ML.Probabilistic.Models;
using Microsoft.ML.Probabilistic.Distributions;

Range oosUR = new Range(oosNUsers).Named("oosU");
Range oosIR = new Range(oosNItems).Named("oosI");
Range oosOR = new Range(oosUObs.Length).Named("oosO");

VariableArray<double> oosU = Variable.Array<double>(oosUR).Named("oosUM");
oosU[oosUR] = Variable.GaussianFromMeanAndPrecision(0, 0.25).ForEach(oosUR);   // sd ~ 2
VariableArray<double> oosV = Variable.Array<double>(oosIR).Named("oosVM");
oosV[oosIR] = Variable.GaussianFromMeanAndPrecision(0, 0.25).ForEach(oosIR);
VariableArray<double> oosbU = Variable.Array<double>(oosUR).Named("oosbU");
oosbU[oosUR] = Variable.GaussianFromMeanAndPrecision(0, 1).ForEach(oosUR);
VariableArray<double> oosbI = Variable.Array<double>(oosIR).Named("oosbI");
oosbI[oosIR] = Variable.GaussianFromMeanAndPrecision(0, 1).ForEach(oosIR);
Variable<double> oosGab = Variable.GaussianFromMeanAndPrecision(3, 1).Named("oosGab"); // nom distinct de oosG (baselines), collision inter-submissions evitee
Variable<double> oosNoise = Variable.GammaFromShapeAndScale(2, 0.5).Named("oosNoise");

VariableArray<int> oosUSub = Variable.Observed(oosUObs, oosOR).Named("oosUSub");
VariableArray<int> oosISub = Variable.Observed(oosIObs, oosOR).Named("oosISub");
VariableArray<double> oosRating = Variable.Observed(oosRObs, oosOR).Named("oosRating");
using (Variable.ForEach(oosOR)) {
    Variable<double> oosAff = oosGab + oosbU[oosUSub[oosOR]] + oosbI[oosISub[oosOR]] + oosU[oosUSub[oosOR]] * oosV[oosISub[oosOR]];
    oosRating[oosOR] = Variable.GaussianFromMeanAndPrecision(oosAff, oosNoise);
}

InferenceEngine oosEngine = new InferenceEngine();
oosEngine.Compiler.CompilerChoice = CompilerChoice.Roslyn;
oosEngine.ShowProgress = false;
var oosUM = oosEngine.Infer<Gaussian[]>(oosU);
var oosVM = oosEngine.Infer<Gaussian[]>(oosV);
var oosbUM = oosEngine.Infer<Gaussian[]>(oosbU);
var oosbIM = oosEngine.Infer<Gaussian[]>(oosbI);
double oosGM = oosEngine.Infer<Gaussian>(oosGab).GetMean();
double oosPrec = oosEngine.Infer<Gamma>(oosNoise).GetMean();
Console.WriteLine($"EP : g={oosGM:F3} ; noise_sigma={1.0/Math.Sqrt(oosPrec):F2} ; max|U|={oosUM.Max(x => Math.Abs(x.GetMean())):F4} ; max|V|={oosVM.Max(x => Math.Abs(x.GetMean())):F4} ; max|bU|={oosbUM.Max(x => Math.Abs(x.GetMean())):F4} ; max|bI|={oosbIM.Max(x => Math.Abs(x.GetMean())):F4}");

double[] oosUp = oosUM.Select(x => x.GetMean()).ToArray();
double[] oosVp = oosVM.Select(x => x.GetMean()).ToArray();
double[] oosbUp = oosbUM.Select(x => x.GetMean()).ToArray();
double[] oosbIp = oosbIM.Select(x => x.GetMean()).ToArray();
compilation had 4 warning(s).
  [1] GaussianProductOp.ProductAverageConditional(vdouble171_B[oosO], oosUM_itemoosUSub_oosO__F[oosO], oosVM_itemoosISub_oosO__F[oosO]) has quality band Experimental which is less than the recommended quality band (Preview)
  [2] GaussianProductOp.AAverageConditional(vdouble171_B[oosO], oosUM_itemoosUSub_oosO__F[oosO], oosVM_itemoosISub_oosO__F[oosO]) has quality band Experimental which is less than the recommended quality band (Preview)
  [3] GaussianProductOp.BAverageConditional(vdouble171_B[oosO], oosUM_itemoosUSub_oosO__F[oosO], oosVM_itemoosISub_oosO__F[oosO]) has quality band Experimental which is less than the recommended quality band (Preview)
  [4] GaussianProductOp.ProductAverageConditional(vdouble171_B[oosO], oosUM_itemoosUSub_oosO__F[oosO], oosVM_itemoosISub_oosO__F[oosO]) has quality band Experimental which is less than the recommended quality band (Preview)
EP : g=3,477 ; noise_sigma=0,86 ; max|U|=0,0000 ; max|V|=0,0000 ; max|bU|=0,2994 ; max|bI|=0,2712
// Metriques OOS : erreur train vs test, classement pairwise (aime > non-aime), cold-start
double[] oosSTr = new double[oosRObs.Length], oosSTe = new double[oosTeR.Length];
for (int o = 0; o < oosRObs.Length; o++) oosSTr[o] = oosGM + oosbUp[oosUObs[o]] + oosbIp[oosIObs[o]] + oosUp[oosUObs[o]] * oosVp[oosIObs[o]];
for (int o = 0; o < oosTeR.Length; o++) oosSTe[o] = oosGM + oosbUp[oosTeU[o]] + oosbIp[oosTeI[o]] + oosUp[oosTeU[o]] * oosVp[oosTeI[o]];
Console.WriteLine($"MODEL : rmse_train={OosRmse(oosSTr, oosRObs):F3}  rmse_test={OosRmse(oosSTe, oosTeR):F3}  mae_test={OosMae(oosSTe, oosTeR):F3}");

// Precisions pairwise : pour chaque item aime de test, est-il score au-dessus de chaque item non-aime ?
int oosNp = 0; double oosAccM = 0, oosAccU = 0;
for (int o = 0; o < oosTeU.Length; o++) {
    int u = oosTeU[o], i = oosTeI[o];
    bool love = (u < 4) ? i < 4 : i >= 4;
    if (!love) continue;
    for (int j = 0; j < oosNItems; j++) {
        bool jLove = (u < 4) ? j < 4 : j >= 4;
        if (jLove) continue;
        double si = oosGM + oosbUp[u] + oosbIp[i] + oosUp[u] * oosVp[i];
        double sj = oosGM + oosbUp[u] + oosbIp[j] + oosUp[u] * oosVp[j];
        oosAccM += (si > sj) ? 1 : 0;
        oosAccU += (oosG + oosBu[u] + oosBi[i] > oosG + oosBu[u] + oosBi[j]) ? 1 : 0;
        oosNp++;
    }
}
Console.WriteLine($"pairwise aime>non-aime : model={oosAccM/oosNp:F3}  uibi={oosAccU/oosNp:F3}  (hasard=0.500, n={oosNp})");

// Cold-start : nouvel utilisateur sans note -> prediction = moyenne + biais item (pas de signal latent appris)
Console.Write("cold-start (pred = g + bI, U=0)      : [");
for (int i = 0; i < oosNItems; i++) Console.Write($" {oosGM + oosbIp[i]:F3}");
Console.WriteLine(" ]");
double[] oosPop = new double[oosNItems];
for (int i = 0; i < oosNItems; i++) { double s = 0; int c = 0; for (int o = 0; o < oosIObs.Length; o++) if (oosIObs[o] == i) { s += oosRObs[o]; c++; } oosPop[i] = s / c; }
Console.WriteLine("popularite train                   : [" + string.Join(" ", oosPop.Select(p => " " + p.ToString("F3"))) + " ]");
MODEL : rmse_train=0,942  rmse_test=1,989  mae_test=1,734
pairwise aime>non-aime : model=0,500  uibi=0,469  (hasard=0.500, n=32)
cold-start (pred = g + bI, U=0)      : [ 3,406 3,537 3,572 3,748 3,207 3,636 3,506 3,681 ]
popularite train                   : [ 3,450  3,625  3,700  3,900  3,125  3,700  3,625  3,725 ]

Interpretation : le protocole OOS revele l’effondrement de la latente

Metrique GM UIBI Modele EP
RMSE train 0.985 0.938 0.942
RMSE test 1.947 2.012 1.989
MAE test 1.72 1.80 1.734
Pairwise aime>non-aime — 0.469 0.500 (hasard)

Verdict honnete : sur cette grille, l’inference EP d’Infer.NET n’extrait pas la structure latente — les posteriors de U et V restent a 0 (max|U| = max|V| = 0.000) et le modele se reduit a g + bU + bI : son RMSE test (1.989) ne bat pas la moyenne globale (1.947). C’est une limite connue de l’approximation variationnelle locale (GaussianProductOp est en bande de qualite Experimental ; sur un produit de Gaussiennes sparse, EP ecrase la variance) — observe aussi dans le 3bis ci-dessus, dont les traits affichaient deja 0,00.

Cette evaluation hors echantillon vaut precisement pour cette raison : un fit sur l’entrainement seul (RMSE 0.942) aurait ete flatteur ; le test demasque un modele qui n’a pas appris de signal utile au-dela des biais. On rapporte donc uniquement l’erreur de prediction et pas une metrique de classement : la precision pairwise a 0.500 (= hasard) prouve que la latente ne porte aucun signal — presenter un classement serait trompeur.

Asymetrie deja documentee par les twins : le jumeau PyMC (meme modele, meme grille) obtient, lui, avec l’echantillonnage NUTS et le score predictif bayesien (moyenne sur les tirages de g + bU + bI + U.V) : RMSE test 1.106 (vs 1.947 de la baseline), MAE 0.967 et precision pairwise 1.000 — le MCMC tire parti des termes latents que EP ecrase. C’est exactement le contraste pedagogue « deux moteurs, deux paradigmes d’inference » du bridge_verdict : la structure du probleme (notes bornees, observations par individu) est similaire, le regime d’inference change la capacite d’apprentissage.

5. Cold-Start avec Features

Le problème du cold-start : comment recommander pour un nouvel utilisateur ou item sans historique ?

Référence. L’approche combine factorisation + regression sur features observables est due a Schein, Popescul, Ungar, Pennock & Rifkin (Schein, A. I., Popescul, A., Ungar, L. H., Pennock, D. M. & Rifkin, R., 2002, “Methods and Metrics for Cold-Start Recommendations”, Proceedings of the 25th Annual International ACM SIGIR Conference on Research and Development in Information Retrieval, 253-260, doi:10.1145/564376.564421). Leur modele de regression combinee (CBFR : Combined Bayesian Factor Regression) montre que la combinaison lineaire ponderee d’une regression sur les features utilisateur/item et d’une factorisation matricielle bat chaque composant isolement sur le cold-start, tout en restant competitif sur les utilisateurs connus. C’est le cadre canonique du cold-start hybride.

Solution : utiliser des features

  • Utilisateur : age, genre, localisation
  • Item : genre, annee, realisateur

Taxonomie du problème cold-start

Type Description Exemple
Nouvel utilisateur Aucun historique de notes Nouveau client
Nouvel item Item jamais note Film sortant en salle
Nouveau système Peu de données globales Startup

stratégies de resolution :

stratégie Avantage Limite
Features explicites Immediat, interpretable Necessite meta-données
Popularity baseline Simple, efficace Pas personnalise
Exploration active Optimise apprentissage Peut degrader UX
Transfer learning Exploite autre domaine Complexe a mettre en place

La solution bayesienne avec features combine les avantages : prediction immediate + incertitude explicite qui diminue avec plus de données.

Definition des features

Nous definissons des meta-données pour chaque utilisateur et item :

Features utilisateurs (2 dimensions) : - Age normalise [0, 1] - Genre (1=homme, 0=femme)

Features items (3 dimensions) : - Proportion action [0, 1] - Proportion romance [0, 1] - Annee de sortie normalisee [0, 1]

Ces features permettent de predire des affinites même sans historique de notes.

// Modele avec features utilisateur

Console.WriteLine("=== Cold-Start avec Features ===");

// Features utilisateurs : age normalise, genre (0/1)
double[,] userFeatures = {
    { 0.2, 1.0 },  // User 0 : jeune, homme
    { 0.8, 0.0 },  // User 1 : age, femme
    { 0.5, 1.0 },  // User 2 : moyen, homme
    { 0.3, 0.0 }   // User 3 : jeune, femme
};

// Features items : genre (action/romance), annee
double[,] itemFeatures = {
    { 1.0, 0.0, 0.9 },  // Item 0 : action, recent
    { 0.0, 1.0, 0.5 },  // Item 1 : romance, ancien
    { 0.5, 0.5, 0.8 },  // Item 2 : mixte, recent
    { 0.0, 1.0, 0.3 },  // Item 3 : romance, tres ancien
    { 1.0, 0.0, 0.7 }   // Item 4 : action, moyen
};

int nUserFeatures = 2;
int nItemFeatures = 3;

Console.WriteLine($"\nFeatures utilisateurs : {nUserFeatures} (age, genre)");
Console.WriteLine($"Features items : {nItemFeatures} (action, romance, annee)");
=== Cold-Start avec Features ===

Features utilisateurs : 2 (age, genre)
Features items : 3 (action, romance, annee)

Poids de regression cold-start

Nous definissons des poids de regression pour transformer les features en scores :

\[\text{score}_{ui} = \text{biais} + w_{user}^T \cdot x_u + w_{item}^T \cdot x_i\]

Priors choisis : - Gaussian(0, 1) pour les poids → regularisation L2 implicite - Gaussian(3, 0.1) pour le biais → centre sur 3 (milieu de l’echelle 1-5)

Note : Dans un modèle hybride complet, ces poids seraient appris conjointement avec les traits latents.

// Nouveau modele avec biais bases sur features

Range uFeatRange = new Range(nUserFeatures).Named("uFeat");
Range iFeatRange = new Range(nItemFeatures).Named("iFeat");

// Poids pour les features
VariableArray<double> userWeights = Variable.Array<double>(uFeatRange).Named("userWeights");
userWeights[uFeatRange] = Variable.GaussianFromMeanAndPrecision(0, 1).ForEach(uFeatRange);

VariableArray<double> itemWeights = Variable.Array<double>(iFeatRange).Named("itemWeights");
itemWeights[iFeatRange] = Variable.GaussianFromMeanAndPrecision(0, 1).ForEach(iFeatRange);

// Biais global
Variable<double> globalBias = Variable.GaussianFromMeanAndPrecision(3, 0.1).Named("globalBias");

Console.WriteLine("Poids de regression pour cold-start definis.");
Poids de regression pour cold-start definis.

modèle mathematique du cold-start

Le modèle hybride combine factorisation et regression sur features :

\[r_{ui} = \underbrace{\mu}_{\text{biais global}} + \underbrace{w_{user}^T \cdot x_u}_{\text{effet features user}} + \underbrace{w_{item}^T \cdot x_i}_{\text{effet features item}} + \underbrace{U_u \cdot V_i^T}_{\text{factorisation}}\]

Avantage : même sans historique (\(U_u = 0\), \(V_i = 0\)), les predictions restent informatives grace aux features.

Note : Dans la cellule suivante, nous simulons les poids appris pour illustrer le mécanisme. Un modèle complet apprendrait ces poids conjointement avec la factorisation.

Simulation cold-start

Cette cellule simule une prediction cold-start avec des poids pre-définis (dans un système reel, ces poids seraient appris).

Nouvel utilisateur : age=0.4 (relativement jeune), genre=1.0 (homme)

Le calcul de score combine : 1. Contribution utilisateur : \(w_{user} \cdot x_{user}\) 2. Contribution item : \(w_{item} \cdot x_{item}\) 3. Biais global : 3.0

// Prediction cold-start simplifiee (sans factorisation complete)

Console.WriteLine("\n=== Prediction Cold-Start ===");
Console.WriteLine("\nSimulation : nouvel utilisateur (age=0.4, genre=1.0)");

// Nouvel utilisateur
double[] newUserFeat = { 0.4, 1.0 };

// Modele simple : affinite = features_user dot weights + features_item dot weights
// Utilisons les moyennes des posterieurs (simplification)
double[] wUser = { 0.5, 0.8 };   // Appris (simule)
double[] wItem = { 0.6, -0.3, 0.2 };  // Appris (simule)

Console.WriteLine("\nScores predits pour chaque item :");
for (int i = 0; i < nItems; i++)
{
    double userScore = 0;
    for (int f = 0; f < nUserFeatures; f++)
        userScore += newUserFeat[f] * wUser[f];
    
    double itemScore = 0;
    for (int f = 0; f < nItemFeatures; f++)
        itemScore += itemFeatures[i, f] * wItem[f];
    
    double prediction = 3.0 + userScore + itemScore;  // Biais + scores
    Console.WriteLine($"  Item {i} : {prediction:F2}");
}

Console.WriteLine("\n=> Recommander items avec score le plus eleve");

=== Prediction Cold-Start ===

Simulation : nouvel utilisateur (age=0.4, genre=1.0)

Scores predits pour chaque item :
  Item 0 : 4,78
  Item 1 : 3,80
  Item 2 : 4,31
  Item 3 : 3,76
  Item 4 : 4,74

=> Recommander items avec score le plus eleve

Analyse de la prédiction cold-start

Nouvel utilisateur : (age=0.4, genre=1.0) → homme, relativement jeune

Scores prédits :

Item Type Score Rang
Item 0 Action, récent 4.78 1
Item 4 Action, moyen 4.74 2
Item 2 Mixte, récent 4.31 3
Item 1 Romance, ancien 3.80 4
Item 3 Romance, très ancien 3.76 5

Interprétation des poids :

Les poids simulés (wUser=[0.5, 0.8], wItem=[0.6, -0.3, 0.2]) encodent : - Genre masculin : bonus +0.8 → préférence action - Age jeune : bonus modéré +0.5×0.4 = +0.2 - Action : bonus +0.6 pour genre action - Romance : malus -0.3 pour genre romance - Récence : léger bonus +0.2 pour films récents

Valeur du cold-start :

Même sans historique de notes, le modèle : 1. Exploite la démographie utilisateur 2. Utilise les caractéristiques des items 3. Fournit des recommandations raisonnables dès le premier contact

5bis. Exercice : Cold-Start avec un Nouvel Item

Dans la section 5, nous avons predit les scores pour un nouvel utilisateur. Maintenant, traitez le cas dual : un nouvel item sans aucune note.

scénario : Un nouveau film arrive sur la plateforme :

Feature Valeur Description
Action 0.8 Film d’action
Romance 0.1 Peu de romance
Annee 0.95 très recent

Objectifs : 1. Calculez le score predit de ce nouvel item pour chaque utilisateur existant 2. Identifiez les utilisateurs les plus susceptibles d’apprecier ce film 3. Comparez avec un film “romance ancien” (action=0.1, romance=0.9, annee=0.2)

étapes : 1. définir les features du nouvel item 2. Calculer le score pour chaque utilisateur avec les poids simules de la section 5 3. Afficher les recommandations par utilisateur 4. Repeter avec un item “romance ancien” et comparer les ciblages

Indices : - Reutilisez les mêmes poids simules : wUser = {0.5, 0.8}, wItem = {0.6, -0.3, 0.2}, biais = 3.0 - Pour un nouvel item sans historique, seul le terme features contribue (pas de traits latents) - Score = biais + wUser dot featuresUser + wItem dot featuresItem

// Exercice : Cold-Start avec un nouvel item

// Nouvel item : film d'action recent
double[] newItemFeat = { 0.8, 0.1, 0.95 };

// Poids simules (memes que section 5)
double[] wUserCS = { 0.5, 0.8 };
double[] wItemCS = { 0.6, -0.3, 0.2 };
double biaisCS = 3.0;

// Features utilisateurs (de la section 5)
// User 0 : jeune homme, User 1 : agee femme, User 2 : moyen homme, User 3 : jeune femme
double[,] userFeats = {
    { 0.2, 1.0 },  // User 0 : jeune, homme
    { 0.8, 0.0 },  // User 1 : age, femme
    { 0.5, 1.0 },  // User 2 : moyen, homme
    { 0.3, 0.0 }   // User 3 : jeune, femme
};
string[] nomsUsers = { "User 0 (jeune homme)", "User 1 (agee femme)", "User 2 (moyen homme)", "User 3 (jeune femme)" };

// TODO: Etape 1 - Calculez le score predit du nouvel item pour chaque utilisateur
// Score = biais + sum(wUserCS[f] * userFeats[u,f]) + sum(wItemCS[f] * newItemFeat[f])

// TODO: Etape 2 - Affichez les scores et identifiez les meilleurs utilisateurs cibles

// TODO: Etape 3 - Repetez avec un item "romance ancien"
double[] romanceAncienFeat = { 0.1, 0.9, 0.2 };

Console.WriteLine("Exercice a completer : prediction cold-start pour un nouvel item.");
Exercice a completer : prediction cold-start pour un nouvel item.

6. Click Model : Sources Multiples

problème

Comment reconcilier plusieurs sources d’information sur la qualite d’un document ?

  • Jugements humains : experts mais couteux
  • Clics utilisateurs : abondants mais bruites
  • Temps de lecture : signal implicite

modèle

flowchart TD
    S["Score latent (vrai) du document"] --> J["Juge"]
    S --> C["Clic"]
    S --> T["Temps de lecture"]

Origine. Le Dynamic Bayesian Click Model (DBCM) a ete formalise par Chapelle & Zhang (Chapelle, O. & Zhang, Y., 2009, “A Dynamic Bayesian Network Click Model for Web Search Ranking”, Proceedings of the 18th International World Wide Web Conference (WWW ’09), 1-10, doi:10.1145/1526709.1526711) dans le cadre du defi LETOR (Microsoft Learning to Rank). Le modele decompose la probabilite de clic en deux composantes : perception (le document a-t-il ete vu ?) et attractivite (le document a-t-il paru pertinent ?), chacune modelisee par un signal latent distinct. Notre notebook en est une simplification pedagogique : un seul score latent \(s_d\) et des observations gaussiennes (au lieu d’une Bernoulli sur le clic + Beta sur l’attractivite), suffisantes pour illustrer le principe de fusion bayesienne de sources heterogenes.

Transition : Du filtrage collaboratif au Click Model

Les sections précédentes traitaient de la prediction de préférences : estimer la note qu’un utilisateur donnerait.

Le Click Model aborde un problème différent : fusionner des signaux heterogenes pour estimer la qualite “vraie” d’un document. C’est un modèle de mesure avec sources multiples, pas de recommandation directe.

// Click Model simplifie

int nDocs = 6;

// Observations de deux sources
double[] jugements = { 4.5, 3.0, 4.0, 2.5, 5.0, 3.5 };  // Notes experts (1-5)
double[] clics = { 120, 80, 95, 60, 150, 70 };          // Nombre de clics

Console.WriteLine("=== Click Model ===");
Console.WriteLine("\nReconciliation jugements experts vs clics utilisateurs");
Console.WriteLine("\nDonnees :");
for (int d = 0; d < nDocs; d++)
{
    Console.WriteLine($"  Doc {d} : Juge={jugements[d]:F1}, Clics={clics[d]}");
}
=== Click Model ===

Reconciliation jugements experts vs clics utilisateurs

Donnees :
  Doc 0 : Juge=4,5, Clics=120
  Doc 1 : Juge=3,0, Clics=80
  Doc 2 : Juge=4,0, Clics=95
  Doc 3 : Juge=2,5, Clics=60
  Doc 4 : Juge=5,0, Clics=150
  Doc 5 : Juge=3,5, Clics=70

données multi-sources

Nous disposons de deux signaux pour evaluer la qualite des documents :

Source Avantage Inconvenient
Jugements experts précis, calibres Couteux, peu nombreux
Clics utilisateurs Abondants, gratuits Bruites, biais position

L’objectif du Click Model est de fusionner ces sources pour obtenir une estimation optimale.

Definition du modèle Click

Le modèle suppose un score latent (qualite vraie) qui genere les deux observations :

Variables du modèle : - scoreLatent[d] : Qualite “vraie” du document d - precJuge : Precision des jugements experts - precClic : Precision des clics - echelleClics : Facteur de conversion score → clics

Priors choisis : - Score latent : Gaussian(3, 0.5) → centre sur 3 (echelle 1-5) - Precision juges : Gamma(5, 1) → elevee (experts fiables) - Precision clics : Gamma(2, 0.5) → plus faible (clics bruites) - Echelle clics : Gaussian(30, 0.01) → environ 30 clics par point de score

// Modele Click

Range docRange = new Range(nDocs).Named("doc");

// Score latent (vrai) de chaque document
VariableArray<double> scoreLatent = Variable.Array<double>(docRange).Named("scoreLatent");
scoreLatent[docRange] = Variable.GaussianFromMeanAndPrecision(3, 0.5).ForEach(docRange);

// Precision de chaque source
Variable<double> precJuge = Variable.GammaFromShapeAndScale(5, 1).Named("precJuge");    // Experts : haute precision
Variable<double> precClic = Variable.GammaFromShapeAndScale(2, 0.5).Named("precClic"); // Clics : basse precision

// Facteur d'echelle pour les clics (clics = score * echelle + bruit)
Variable<double> echelleClics = Variable.GaussianFromMeanAndPrecision(30, 0.01).Named("echelleClics");

// Observations
VariableArray<double> obsJuge = Variable.Array<double>(docRange).Named("obsJuge");
VariableArray<double> obsClic = Variable.Array<double>(docRange).Named("obsClic");

// Modele generatif
obsJuge[docRange] = Variable.GaussianFromMeanAndPrecision(scoreLatent[docRange], precJuge);
obsClic[docRange] = Variable.GaussianFromMeanAndPrecision(scoreLatent[docRange] * echelleClics, precClic);

// Observations
obsJuge.ObservedValue = jugements;
obsClic.ObservedValue = clics;

Console.WriteLine("Modele Click defini avec deux sources.");
Modele Click defini avec deux sources.

Structure probabiliste du Click Model

Le modèle genere les observations a partir d’un score latent unique :

\[s_d \sim \mathcal{N}(\mu_{prior}, \sigma_{prior}^2)\]

\[\text{Jugement}_d \sim \mathcal{N}(s_d, \sigma_{juge}^2)\]

\[\text{Clics}_d \sim \mathcal{N}(s_d \times \text{echelle}, \sigma_{clic}^2)\]

Interpretation : - \(s_d\) : qualite “vraie” (latente) du document - \(\sigma_{juge}^2\) : variance du bruit des experts - \(\sigma_{clic}^2\) : variance du bruit des clics - \(\text{echelle}\) : facteur de conversion score → clics

Note : Le facteur d’echelle est crucial car les deux sources ont des unites différentes (notes 1-5 vs clics 0-200).

exécution de l’inference Click Model

Nous inferons simultanement : 1. Les scores latents de chaque document 2. Les precisions de chaque source 3. Le facteur d’echelle

L’inference pondere automatiquement les sources selon leur precision inferee.

// Inference
InferenceEngine moteur2 = new InferenceEngine();
moteur2.Compiler.CompilerChoice = CompilerChoice.Roslyn;
moteur2.ShowFactorGraph = true;

Console.WriteLine("\n=== Inference Click Model ===");

var scorePost = moteur2.Infer<Gaussian[]>(scoreLatent);
var precJugePost = moteur2.Infer<Gamma>(precJuge);
var precClicPost = moteur2.Infer<Gamma>(precClic);
var echellePost = moteur2.Infer<Gaussian>(echelleClics);

Console.WriteLine($"\nPrecision juges : {precJugePost.GetMean():F2}");
Console.WriteLine($"Precision clics : {precClicPost.GetMean():F2}");
Console.WriteLine($"Echelle clics : {echellePost.GetMean():F2}");

Console.WriteLine("\nScores latents inferes :");
for (int d = 0; d < nDocs; d++)
{
    double mean = scorePost[d].GetMean();
    double std = Math.Sqrt(scorePost[d].GetVariance());
    Console.WriteLine($"  Doc {d} : {mean:F2} +/- {std:F2}  (Juge: {jugements[d]:F1}, Clics: {clics[d]})");
}

=== Inference Click Model ===
Compiling model...compilation had 3 warning(s).
  [1] GaussianProductOp.BAverageConditional(vdouble__48_use_B[doc], scoreLatent_uses_F[doc][1], echelleClics_rep_F[doc]) has quality band Experimental which is less than the recommended quality band (Preview)
  [2] GaussianProductOp.AAverageConditional(vdouble__48_use_B[doc], scoreLatent_uses_F[doc][1], echelleClics_rep_F[doc]) has quality band Experimental which is less than the recommended quality band (Preview)
  [3] GaussianProductOp.ProductAverageConditional(vdouble__48_use_B[doc], scoreLatent_uses_F[doc][1], echelleClics_rep_F[doc]) has quality band Experimental which is less than the recommended quality band (Preview)
done.
Iterating: 
.........|.........|.........|.........|.........| 50

Precision juges : 4,50
Precision clics : 0,99
Echelle clics : 26,90

Scores latents inferes :
  Doc 0 : 4,47 +/- 0,23  (Juge: 4,5, Clics: 120)
  Doc 1 : 2,98 +/- 0,16  (Juge: 3,0, Clics: 80)
  Doc 2 : 3,54 +/- 0,19  (Juge: 4,0, Clics: 95)
  Doc 3 : 2,24 +/- 0,13  (Juge: 2,5, Clics: 60)
  Doc 4 : 5,58 +/- 0,29  (Juge: 5,0, Clics: 150)
  Doc 5 : 2,62 +/- 0,15  (Juge: 3,5, Clics: 70)

Analyse du Click Model

Paramètres inférés :

Paramètre Valeur Interprétation
Précision juges 4.50 Élevée → experts fiables
Précision clics 0.99 Faible → clics très bruités
Échelle clics 26.90 1 point de score ≈ 27 clics

Qualité des sources :

La précision relative (4.50 vs 0.99) indique que le modèle fait 4.5× plus confiance aux jugements experts qu’aux clics.

Scores latents inférés :

Doc Score Incertitude Juge Clics/échelle
4 5.58 ±0.29 5.0 5.57
0 4.47 ±0.23 4.5 4.46
2 3.54 ±0.19 4.0 3.53
1 2.98 ±0.16 3.0 2.97

Observations :

  1. Cohérence sources : Jugements et clics/échelle sont très proches → bonne calibration
  2. Doc 2 ajusté : Juge=4.0, Clics/échelle≈3.53 → score final 3.54 (score intermédiaire entre les deux sources)
  3. Incertitude croissante : Les documents mieux notés ont plus d’incertitude (variance proportionnelle au score)

Visualisation du graphe factoriel - Click Model

Le graphe factoriel du Click Model illustre la fusion de sources multiples :

  • Variable latente centrale : scoreLatent[doc] represente la qualite “vraie” de chaque document
  • Source 1 - Experts : obsJuge[doc] avec precision elevee (precJuge ~ 5)
  • Source 2 - Clics : obsClic[doc] avec precision faible (precClic ~ 1) et facteur d’echelle
  • Fusion bayesienne : Le modèle pondere automatiquement les sources selon leur fiabilite

Ce pattern est generalizable a N sources d’information (temps de lecture, partages, etc.).

// Affichage du graphe factoriel du Click Model
display(HTML(FactorGraphHelper.GetLatestFactorGraphHtml()));
Model_08_26_26_17_45_36_14.svg
Model node0 scoreLatent[doc] node1 Gaussian node0->node1 mean node4 Multiply node0->node4 a node3 obsJuge[doc] node1->node3 node2 precJuge node2->node1 precision node6 vdouble[]48[doc] node4->node6 node5 echelleClics node5->node4 b node13 Gaussian node6->node13 mean node7 30 node8 Gaussian node7->node8 mean node8->node5 node9 0,01 node9->node8 precision node10 3 node11 Gaussian node10->node11 mean node11->node0 node12 0,5 node12->node11 precision node15 obsClic[doc] node13->node15 node14 precClic node14->node13 precision node16 2 node17 Sample node16->node17 shape node17->node14 node18 0,5 node18->node17 scale node19 5 node20 Sample node19->node20 shape node20->node2 node21 1 node21->node20 scale

Classement des documents

Nous utilisons les scores latents inferes pour produire un classement final ordonne par qualite estimee.

// Classement final

Console.WriteLine("\n=== Classement Final ===");

var classement = Enumerable.Range(0, nDocs)
    .Select(d => new { Doc = d, Score = scorePost[d].GetMean() })
    .OrderByDescending(x => x.Score)
    .ToList();

Console.WriteLine("\nDocuments classes par score latent :");
int rang = 1;
foreach (var item in classement)
{
    Console.WriteLine($"  {rang}. Doc {item.Doc} (score: {item.Score:F2})");
    rang++;
}

Console.WriteLine("\n=> Le modele combine optimalement les deux sources");

=== Classement Final ===

Documents classes par score latent :
  1. Doc 4 (score: 5,58)
  2. Doc 0 (score: 4,47)
  3. Doc 2 (score: 3,54)
  4. Doc 1 (score: 2,98)
  5. Doc 5 (score: 2,62)
  6. Doc 3 (score: 2,24)

=> Le modele combine optimalement les deux sources

Analyse du classement final

Classement des documents :

Rang Document Score latent Jugement Clics
1 Doc 4 5.58 5.0 150
2 Doc 0 4.47 4.5 120
3 Doc 2 3.54 4.0 95
4 Doc 1 2.98 3.0 80
5 Doc 5 2.62 3.5 70
6 Doc 3 2.24 2.5 60

Valeur de la fusion :

Le modèle résout les désaccords entre sources : - Doc 5 : Juge=3.5, Clics=70 → Score=2.62 (clics plus bas que attendu → baisse du score) - Doc 2 : Juge=4.0, Clics=95 → Score=3.54 (clics cohérents → score intermédiaire)

Avantages du Click Model :

Aspect Bénéfice
Calibration automatique Apprend l’échelle de chaque source
Pondération adaptative Sources plus précises ont plus de poids
Incertitude explicite Quantifie la confiance du classement
Extensible Ajouter d’autres signaux (temps lecture, rebonds, etc.)

6bis. Exercice : Ajouter une Troisieme Source au Click Model

Le Click Model de la section 6 fusionne deux sources (jugements experts et clics). Etendez-le a une troisieme source : le temps de lecture moyen en secondes.

Hypothese : Plus un document est de qualite, plus les utilisateurs y passent du temps (a un facteur d’echelle pres).

Source Precision attendue Echelle
Jugements experts Elevee (~4-5) Notes 1-5
Clics Faible (~1) Compte (0-200)
Temps de lecture Moyenne (~2-3) Secondes (0-120)

données supplementaires :

Temps de lecture (secondes) : {95, 45, 75, 30, 110, 55}

Objectifs : 1. Ajoutez les variables precTemps et echelleTemps au modèle 2. Ajoutez le facteur d’observation pour le temps de lecture 3. Executez l’inference et comparez les scores avec le modèle a 2 sources

étapes : 1. définir les priors pour la precision et l’echelle du temps de lecture 2. Ajouter l’observation obsTemps[d] ~ Gaussian(scoreLatent[d] * echelleTemps, precTemps) 3. Observer les données de temps de lecture 4. Executer l’inference et comparer les scores latents avec/sans la 3e source

Indices : - Suivez exactement le même pattern que pour les clics : une precision Gamma et une echelle Gaussian - Le facteur d’echelle est echelleTemps ~ Gaussian(20, 0.01) (environ 20 secondes par point de score) - La precision precTemps ~ Gamma(3, 1) (precision moyenne)

// Exercice : Click Model avec 3 sources

// Donnees supplementaires : temps de lecture en secondes
double[] tempsLecture = { 95, 45, 75, 30, 110, 55 };

// TODO: Etape 1 - Ajoutez les variables pour la 3e source
// Variable<double> precTemps = Variable.GammaFromShapeAndScale(3, 1).Named("precTemps");
// Variable<double> echelleTemps = Variable.GaussianFromMeanAndPrecision(20, 0.01).Named("echelleTemps");
// VariableArray<double> obsTemps = Variable.Array<double>(docRange).Named("obsTemps");

// TODO: Etape 2 - Ajoutez le facteur d'observation
// obsTemps[docRange] = Variable.GaussianFromMeanAndPrecision(
//     scoreLatent[docRange] * echelleTemps, precTemps);
// obsTemps.ObservedValue = tempsLecture;

// TODO: Etape 3 - Executez l'inference et affichez les scores
// Comparez avec les scores du modele a 2 sources (section 6)

// TODO: Etape 4 - Affichez un tableau comparatif
// | Doc | Score 2 sources | Score 3 sources | Diff |

Console.WriteLine("Exercice a completer : ajoutez la 3e source au Click Model.");
Exercice a completer : ajoutez la 3e source au Click Model.

6ter. Exercice : Predire la Note d’un Utilisateur pour un Film Non Note

Dans les sections 3-4, nous avons construit un modèle de factorisation matricielle bayesienne et predit les notes manquantes. Utilisez cette architecture pour construire un mini-système de recommandation sur une nouvelle matrice de notes.

scénario : 4 utilisateurs ont note 5 films (note sur 5, NaN = non note) :

Inception Titanic Matrix Amelie Terminator
Alice 5 2 4 NaN 5
Bob NaN 4 3 5 NaN
Claire 4 NaN 5 2 4
David 3 5 NaN 4 NaN

Objectifs : 1. Implementez la factorisation matricielle avec K=2 facteurs latents 2. Predisez les notes manquantes pour chaque (utilisateur, film) 3. Recommandez le meilleur film non note pour chaque utilisateur

étapes : 1. créer les variables latentes userTraits[u][k] et itemTraits[i][k] avec priors Gaussian 2. Modeliser chaque note observee (non NaN) comme note[u][i] = dot(userTraits[u], itemTraits[i]) + bruit 3. Executer l’inference (EP) et extraire les posteriors des notes manquantes 4. Pour chaque utilisateur, identifier le film non note avec la plus haute note predite (NaN = a predire)

Indices : - Reutilisez la structure de la section 3bis (avec suffisamment de données pour converger) - Observez les notes : Alice et Claire preferent l’action, Bob et David la romance - Avec K=2, les facteurs latents devraient capturer cette dimension action vs romance - Le bruit de precision peut etre fixe (ex: Gamma(2, 0.5)) ou inferre

// Exercice : Predire la note d'un utilisateur pour un film non note

// Donnees : 4 utilisateurs x 5 films (double.NaN = non note)
int nUsers = 4;
int nFilms = 5;
int K = 2;  // Facteurs latents

string[] users = { "Alice", "Bob", "Claire", "David" };
string[] films = { "Inception", "Titanic", "Matrix", "Amelie", "Terminator" };

// Notes observees (double.NaN = manquante)
double[][] notes = {
    new double[] { 5, 2, 4, double.NaN, 5 },      // Alice : action
    new double[] { double.NaN, 4, 3, 5, double.NaN }, // Bob : romance
    new double[] { 4, double.NaN, 5, 2, 4 },       // Claire : action
    new double[] { 3, 5, double.NaN, 4, double.NaN } // David : romance
};

// Etape 1 : Creer le modele avec priors pour les traits latents
// VariableArray<VariableArray<double>> userTraits, itemTraits
// userTraits[u][k] ~ Gaussian(0, 0.1)  pour chaque u, k
// itemTraits[i][k] ~ Gaussian(0, 0.1)  pour chaque i, k

// Etape 2 : Modeliser les notes observees
// Pour chaque (u, i) ou !double.IsNaN(notes[u][i]) :
//   affinite = Variable.Sum(userTraits[u][k] * itemTraits[i][k])
//   noteObs ~ Gaussian(affinite, precisionBruit)
//   noteObs.ObservedValue = notes[u][i]

// Etape 3 : Creer les variables pour les notes manquantes
// Pour chaque (u, i) ou double.IsNaN(notes[u][i]) :
//   notePred[u][i] ~ Gaussian(affinite, precisionBruit)

// Etape 4 : Executer l'inference et afficher les predictions
// Inferer les posteriors de userTraits, itemTraits, et notePred
// Pour chaque utilisateur, afficher le film recommande (plus haute note predite)

// Indice : utilisez InferenceEngine avec algorithm = ExpectationPropagation
// Indice : parcourez la matrice et ne conditionnez que les notes non-NaN

Console.WriteLine("Exercice a completer : prediction de notes manquantes par factorisation matricielle.");
Exercice a completer : prediction de notes manquantes par factorisation matricielle.

7. Exemple guide : Recommandation de Films

Enonce

Utilisez le modèle de factorisation pour predire les notes manquantes et recommander des films.

Contexte de l’exercice

Cet exercice applique le modèle de factorisation a un scénario concret : recommander des films a des utilisateurs.

Mapping sémantique :

ID Film Type suppose
0 Inception Action/Sci-Fi
1 Titanic Romance
2 Matrix Action/Sci-Fi
3 Notebook Romance
4 Terminator Action

Profils utilisateurs (implicites) : - Alice, Claire : préférences action - Bob : préférences romance - David : préférences action

Instructions pour l’exercice

L’exercice utilise le modèle de factorisation de la section 3 pour predire des notes sur un jeu de données sémantique (films).

Objectif : Comprendre la relation entre : 1. Les traits latents appris (ou non appris) 2. La qualite des recommandations

Ce que vous devriez observer : - Avec le modèle original (8 observations), les recommandations sont uniformes (scores ~0) - Cela illustre l’importance de la qualite de l’inference en amont

Extension possible : Modifier le code pour utiliser les posterieurs du modèle corrige (section 3bis) et observer la différence.

// Exemple guide : Recommandation de films

// Films : 0=Inception, 1=Titanic, 2=Matrix, 3=NotebookFilm, 4=Terminator
string[] films = { "Inception", "Titanic", "Matrix", "Notebook", "Terminator" };

// Utilisateurs : 0=Alice, 1=Bob, 2=Claire, 3=David
string[] users = { "Alice", "Bob", "Claire", "David" };

// Notes observees (memes donnees que section 3)
Console.WriteLine("=== Recommandation de Films ===");
Console.WriteLine("\nNotes observees :");
Console.WriteLine($"  Alice -> Inception: 5, Matrix: 3");
Console.WriteLine($"  Bob -> Titanic: 4, Notebook: 2");
Console.WriteLine($"  Claire -> Inception: 4, Matrix: 5");
Console.WriteLine($"  David -> Matrix: 4, Terminator: 5");

// Utiliser les posterieurs calcules precedemment
Console.WriteLine("\n=== Top 3 Recommandations par utilisateur ===");

for (int u = 0; u < nUsers; u++)
{
    var predictions = new List<(int item, double score)>();
    
    for (int i = 0; i < nItems; i++)
    {
        // Verifier si deja note
        bool observe = false;
        for (int o = 0; o < nObs; o++)
        {
            if (userObs[o] == u && itemObs[o] == i)
            {
                observe = true;
                break;
            }
        }
        
        if (!observe)
        {
            double pred = 0;
            for (int t = 0; t < nTraits; t++)
            {
                pred += userTraitsPost[u, t].GetMean() * itemTraitsPost[i, t].GetMean();
            }
            predictions.Add((i, pred));
        }
    }
    
    var top3 = predictions.OrderByDescending(p => p.score).Take(3);
    Console.WriteLine($"\n{users[u]} :");
    foreach (var rec in top3)
    {
        Console.WriteLine($"  -> {films[rec.item]} (score predit: {rec.score:F2})");
    }
}
=== Recommandation de Films ===

Notes observees :
  Alice -> Inception: 5, Matrix: 3
  Bob -> Titanic: 4, Notebook: 2
  Claire -> Inception: 4, Matrix: 5
  David -> Matrix: 4, Terminator: 5

=== Top 3 Recommandations par utilisateur ===

Alice :
  -> Titanic (score predit: 0,00)
  -> Notebook (score predit: -0,00)
  -> Terminator (score predit: -0,00)

Bob :
  -> Terminator (score predit: 0,00)
  -> Matrix (score predit: -0,00)
  -> Inception (score predit: -0,00)

Claire :
  -> Titanic (score predit: 0,00)
  -> Notebook (score predit: -0,00)
  -> Terminator (score predit: -0,00)

David :
  -> Notebook (score predit: 0,00)
  -> Inception (score predit: -0,00)
  -> Titanic (score predit: -0,00)

Analyse des recommandations de films

Résultats : Toutes les recommandations ont un score ~0.0

Utilisateur Top recommandations
Alice Titanic, Notebook, Terminator (tous ~0.0)
Bob Terminator, Matrix, Inception (tous ~0.0)
Claire Titanic, Notebook, Terminator (tous ~0.0)
David Notebook, Inception, Titanic (tous ~0.0)

Diagnostic :

Les scores nuls reflètent le problème de convergence de la factorisation (voir analyse précédente) : - Traits utilisateurs ≈ 0 → produit scalaire ≈ 0 - Aucune différenciation possible entre items

Ce qu’un système fonctionnel montrerait :

Utilisateur Profil attendu Recommandations attendues
Alice Action (5 Inception, 3 Matrix) Terminator (action)
Bob Romance (4 Titanic, 2 Notebook) Notebook supplémentaire
Claire Action (4 Inception, 5 Matrix) Terminator
David Action (4 Matrix, 5 Terminator) Inception

Leçon : La factorisation bayésienne est puissante mais nécessite : - Suffisamment de données (>10× paramètres) - Bonne initialisation ou priors informatifs - Validation des sorties avant déploiement

Points cles de l’exercice

Observations principales :

  1. La qualite des recommandations depend de l’inference : Un modèle mal converge produit des recommandations inutilisables
  2. Diagnostic rapide : Des scores uniformes signalent un problème en amont
  3. Solution : Verifier les traits latents avant de deployer

Checklist de validation d’un système de recommandation :

Verification méthode
Traits non-nuls Inspecter moyennes des posterieurs
Variance raisonnable Traits avec incertitude, pas collapses a 0
Predictions variees Distribution des scores, pas uniformes
Coherence sémantique Top recommendations correspondent aux préférences observees

Conseil pratique : Toujours valider sur un ensemble de test (notes observees masquees) avant deploiement.

Bilan : systèmes de recommandation bayesiens

modèle Usage Points cles
Factorisation matricielle Prediction de notes Traits latents, produit scalaire, regularisation par priors
Cold-start hybride Nouveaux utilisateurs/items Features + factorisation, prediction immediate
Click Model Fusion de sources Score latent, calibration automatique, incertitude par source

Quand utiliser chaque approche :

Situation Recommandation
Beaucoup de notes, peu de nouveautes Factorisation pure
Forte rotation utilisateurs/items Cold-start avec features
Multiple signaux (clics, temps, notes) Click Model ou variantes
Production a grande echelle Matchbox (Infer.NET industriel)

8. Resume

Concept Description
Factorisation Decomposition U x V des préférences latentes
Cold-start Utiliser features pour nouveaux utilisateurs/items
Click Model Reconcilier sources de qualite variable
Matchbox Implementation industrielle dans Infer.NET

Pour aller plus loin

Si vous voulez… Consultez…
Debugger des problemes de convergence Infer-2b-Debugging-Bonnes-Pratiques
Comprendre les algorithmes EP vs VMP Infer-2b-Debugging-Bonnes-Pratiques Section 4
Ameliorer le ratio données/paramètres Infer-2b-Debugging-Bonnes-Pratiques Section 2
Trouver une definition Glossaire

Serie Complete

Felicitations ! Vous avez termine la serie Programmation Probabiliste avec Infer.NET.

# Notebook Concepts
1 Setup Installation, premier modèle, troubleshooting
2 Gaussian-Mixtures Posterieurs, melanges, Truncated Gaussian
3 Factor-Graphs Inference discrete, Monty Hall
4 Bayesian-Networks CPT, causalite
5 Skills-IRT IRT, DINA, many-to-many, ROC curves
6 TrueSkill Ranking, online learning
7 Classification BPM, A/B testing
8 Model-sélection Evidence, ARD
9 Topic-Models LDA, documents-topics, brisure de symetrie
10 Crowdsourcing Workers, communautes
11 Sequences HMM, transitions Markov, motif finding
12 Recommenders Factorisation, Click Model
13 Debugging Troubleshooting, comparaison algorithmes

Ressources

Tableau recapitulatif des distributions utilisees

Distribution Usage dans ce notebook paramètres
Gaussian(mean, precision) Traits latents, biais global, scores mean=0/3, precision=0.1-1
Gamma(shape, scale) Precision du bruit shape=2, scale=0.5
GaussianFromMeanAndPrecision Generation des notes mean=affinite, precision=bruit

Concepts probabilistes illustres

Concept Section Description
Factorisation matricielle 3 Decomposition R = U x V^T avec priors bayesiens
problème de convergence 3-4 Ratio données/paramètres insuffisant
Cold-start 5 Prediction sans historique via features
Fusion multi-sources 6 Click Model avec calibration automatique
Produit de gaussiennes 3, 6 Necessite EP (pas VMP)

Applications pratiques

Domaine Application modèle recommande
E-commerce Recommandation produits Factorisation + cold-start
Streaming Recommandation films/series Factorisation matricielle
Moteur de recherche Ranking documents Click Model
Reseau social Suggestion d’amis Factorisation avec features sociales

8bis. Exercice : Recommandation de Musique

Enonce

Appliquez la factorisation matricielle a la recommandation de musique (notes 1-5).

5 utilisateurs x 6 chansons (NaN = non ecoute) :

Bohemian R. hôtel Calif. Smells Like Imagine Stan Shape of You
Alice 5 4 ? 5 ? 2
Bob ? 5 4 ? 3 ?
Charlie 2 ? 5 1 4 ?
Diana 4 3 ? 5 ? 4
Eve ? ? 3 ? 5 3
  1. Faites tourner la factorisation avec K=2 facteurs latents
  2. Predisez la note d’Alice pour “Smells Like” et de Bob pour “Imagine”
  3. Quel utilisateur a le profil le plus proche d’Alice ?
// Exercice : Recommandation de musique - factorisation matricielle
// Notes sur 1-5, double.NaN = non ecoute
int nUsers = 5;
int nSongs = 6;
int K = 2;  // Facteurs latents

string[] users = { "Alice", "Bob", "Charlie", "Diana", "Eve" };
string[] songs = { "Bohemian", "Hotel Calif", "Smells Like", "Imagine", "Stan", "Shape of You" };

// Notes observees (-1 = non ecoute, encoder comme observations manquantes)
double[][] notes = {
    new double[] { 5, 4, double.NaN, 5, double.NaN, 2 },       // Alice
    new double[] { double.NaN, 5, 4, double.NaN, 3, double.NaN }, // Bob
    new double[] { 2, double.NaN, 5, 1, 4, double.NaN },        // Charlie
    new double[] { 4, 3, double.NaN, 5, double.NaN, 4 },        // Diana
    new double[] { double.NaN, double.NaN, 3, double.NaN, 5, 3 } // Eve
};

// TODO: Creer les facteurs latents utilisateurs et chansons
// (Reutilisez la structure de la section 6 de l'exemple guide)

// TODO: Modeliser note[u][s] = dot(userTraits[u], songTraits[s]) + bruit
// Conditionnez les observations connues, laissez les NaN comme variables a inferer

// TODO: Inferer les facteurs et predire les notes manquantes
// Qui a le profil utilisateur le plus similaire a Alice ? (distance cosinus sur userTraits)
Console.WriteLine("Exercice a completer");
Exercice a completer

Conclusion

Ce notebook a presente les systèmes de recommandation bayesiens : factorisation matricielle, cold-start hybride et fusion multi-sources via le Click Model.

modèle mécanisme Point cle
Factorisation matricielle R = U x V^T avec priors gaussiens Ratio données/paramètres > 5 pour convergence
Cold-start hybride Features utilisateur/item + biais Prediction immediate sans historique
Click Model Score latent + precision par source Calibration automatique des poids
Distribution rôle dans ce notebook
Gaussian Traits latents utilisateurs et items, biais global
Gamma Precision du bruit d’observation
GaussianFromMeanAndPrecision Generation des notes a partir de l’affinite

Lecon pratique : La factorisation bayesienne regularise naturellement via les priors, mais necessite suffisamment de données. Le Click Model illustre la fusion optimale de sources de fiabilite variable en ponderant automatiquement selon precision inferee.

Pour aller plus loin. La litterature de la recommandation bayesienne est riche : Salakhutdinov & Mnih 2008 (PMF MCMC, jumeau PyMC-15), Koren 2008 (factorisation avec facteurs latents et biais separees), Agarwal & Chen 2009 (regression hierarchique pour le cold-start), Wang & Blei 2011 (collaborative topic modeling combinant LDA et factorisation), et plus recemment l’integration de reseaux de neurones (deep recommender systems). Le chapitre 5 du livre MBML (Making Recommendations, mbmlbook.com) traite ces extensions de maniere unifiee, avec une implementation detaillee dans le framework Infer.NET.


References

Sources fondatrices (papiers primaires).

  • Koren, Y., Bell, R. & Volinsky, C. (2009), “Matrix Factorization Techniques for Recommender Systems”, IEEE Computer 42(8):30-37, doi:10.1109/MC.2009.263 — synthese canonique des techniques de factorisation matricielle popularisees par le Netflix Prize, avec discussion des biais utilisateur/item et du cold-start (section 3).
  • Schein, A. I., Popescul, A., Ungar, L. H., Pennock, D. M. & Rifkin, R. (2002), “Methods and Metrics for Cold-Start Recommendations”, Proceedings of the 25th Annual International ACM SIGIR Conference on Research and Development in Information Retrieval, 253-260, doi:10.1145/564376.564421 — modele canonique de regression combinee (CBFR) factorisation + features pour le cold-start (section 5).
  • Chapelle, O. & Zhang, Y. (2009), “A Dynamic Bayesian Network Click Model for Web Search Ranking”, Proceedings of the 18th International World Wide Web Conference (WWW ’09), 1-10, doi:10.1145/1526709.1526711 — cadre canonique du Dynamic Bayesian Click Model (DBCM) decompose en perception et attractivite (section 6).

Sources secondaires (contexte methodologique).

  • Salakhutdinov, R. & Mnih, A. (2008), “Bayesian Probabilistic Matrix Factorization Using Markov Chain Monte Carlo”, Proceedings of the 25th International Conference on Machine Learning (ICML ’08), 880-887, doi:10.1145/1390156.1390267 — pendant MCMC du meme probleme, implemente dans le jumeau PyMC-15.
  • Wang, C. & Blei, D. M. (2011), “Collaborative Topic Modeling for Recommending Scientific Articles”, Proceedings of the 17th ACM SIGKDD International Conference on Knowledge Discovery and Data Mining (KDD ’11), 448-456, doi:10.1145/2020408.2020480 — extension a des contenus riches (texte) via LDA combine avec la factorisation.
  • Minka, T. (2001), “Expectation Propagation for Approximate Bayesian Inference”, Proceedings of the 17th Conference in Uncertainty in Artificial Intelligence (UAI ’01), 362-369 — moteur d’inference EP utilise dans tout le notebook.

Relation a la serie.

  • Jumeau Python : PyMC-15-Recommenders (PyMC/PyTensor, MCMC, Salakhutdinov-Mnih 2008 PMF).
  • Theme connexe : Infer-11 (LDA, applicable a la recommandation basee sur le contenu texte, voir Wang-Blei 2011).
  • Theme connexe : Infer-13 (aggregation de jugements d’experts, dual du click model pour la fusion multi-source).
Retour au sommet