ML-7 : Systèmes de Recommandation avec ML.NET

Navigation : Index | << ML-6-ONNX

Objectifs d’apprentissage

A la fin de ce notebook, vous saurez : 1. Comprendre les principes des systèmes de recommandation 2. Utiliser l’algorithme Matrix Factorization pour le collaborative filtering 3. Construire un système de recommandation de produits/films 4. Évaluer la qualité des recommandations (Precision, Recall, NDCG) 5. Gérer le Cold Start Problem (nouveaux utilisateurs/items)

Prérequis

  • ML-1 à ML-6 complétés
  • Notions d’algèbre linéaire (matrices, factorisation)

Durée estimée : 45-60 minutes


Introduction aux Systèmes de Recommandation

Qu’est-ce qu’un système de recommandation ?

Un système de recommandation prédit la préférence d’un utilisateur pour des items (produits, films, musiques, articles).

Exemples : - Netflix : Recommandation de films - Amazon : Recommandation de produits - Spotify : Recommandation de musiques - YouTube : Recommandation de vidéos

Approches de recommandation

1. Collaborative Filtering

Utilisateur A aime → Item 1, Item 2
Utilisateur B aime → Item 1, Item 3
→ Recommander Item 3 à l'Utilisateur A (similaire à B)

2. Content-Based Filtering

Utilisateur A aime → Action, Sci-Fi
Item X est un → Action, Sci-Fi
→ Recommander Item X à l'Utilisateur A

3. Hybrid Approaches

  • Combiner collaborative + content-based
  • Exemple : Netflix (vos préférences + préférences similaires)

Dans ce notebook, nous nous concentrerons sur Matrix Factorization (Collaborative Filtering).

#r "nuget: Microsoft.ML, 5.0.0"
#r "nuget: Microsoft.ML.Recommender, 0.23.0"
#r "nuget: XPlot.Plotly.Interactive, 3.0.2"

using Microsoft.ML;
using Microsoft.ML.Data;
using Microsoft.ML.Trainers;
using System;
using System.Collections.Generic;
using System.Linq;
using XPlot.Plotly;

Console.WriteLine("Packages de recommandation chargés !");
Installing Packages
  • Microsoft.ML
  • Microsoft.ML.Recommender
  • XPlot.Plotly.Interactive

Exemple 1 : Recommandation de films

Nous allons construire un système de recommandation de films basé sur les notes des utilisateurs.

Structure des données

using Microsoft.ML;
using Microsoft.ML.Data;
using Microsoft.ML.Trainers;
using System;
using System.Collections.Generic;
using System.Linq;

// Classes de données pour la recommandation
public class MovieRating
{
    public float UserId { get; set; }    // ID de l'utilisateur
    public float MovieId { get; set; }   // ID du film
    public float Rating { get; set; }    // Note (1-5)
}

public class MovieRatingPrediction
{
    public float Score { get; set; }     // Note prédite
}

// Données d'exemple : notes de films par 5 utilisateurs
var movieData = new List<MovieRating>
{
    // Utilisateur 1 : aime Action et Sci-Fi
    new MovieRating { UserId = 1, MovieId = 1, Rating = 5 },  // Matrix
    new MovieRating { UserId = 1, MovieId = 2, Rating = 4 },  // Inception
    new MovieRating { UserId = 1, MovieId = 3, Rating = 1 },  // Titanic
    
    // Utilisateur 2 : aime Romance
    new MovieRating { UserId = 2, MovieId = 1, Rating = 2 },
    new MovieRating { UserId = 2, MovieId = 2, Rating = 1 },
    new MovieRating { UserId = 2, MovieId = 3, Rating = 5 },
    
    // Utilisateur 3 : goûts variés
    new MovieRating { UserId = 3, MovieId = 1, Rating = 4 },
    new MovieRating { UserId = 3, MovieId = 2, Rating = 3 },
    new MovieRating { UserId = 3, MovieId = 3, Rating = 4 },
    
    // Utilisateur 4 : Action fan
    new MovieRating { UserId = 4, MovieId = 1, Rating = 5 },
    new MovieRating { UserId = 4, MovieId = 2, Rating = 4 },
    new MovieRating { UserId = 4, MovieId = 3, Rating = 2 },
    
    // Utilisateur 5 : Romance fan
    new MovieRating { UserId = 5, MovieId = 1, Rating = 1 },
    new MovieRating { UserId = 5, MovieId = 2, Rating = 2 },
    new MovieRating { UserId = 5, MovieId = 3, Rating = 5 },
};

Console.WriteLine("=== Données de notes de films ===");
Console.WriteLine("Total de notes : " + movieData.Count);
Console.WriteLine("Nombre d'utilisateurs : " + movieData.Select(x => x.UserId).Distinct().Count());
Console.WriteLine("Nombre de films : " + movieData.Select(x => x.MovieId).Distinct().Count());
=== Données de notes de films ===
Total de notes : 15
Nombre d'utilisateurs : 5
Nombre de films : 3

Visualisation de la matrice Utilisateur-Item

using System;
using System.Linq;

// Créer une matrice utilisateur-film
var userIds = movieData.Select(x => (int)x.UserId).Distinct().OrderBy(x => x).ToArray();
var movieIds = movieData.Select(x => (int)x.MovieId).Distinct().OrderBy(x => x).ToArray();

// Afficher directement les données
Console.WriteLine("\n=== Matrice Utilisateur-Film ===");
Console.Write("Film\t");
foreach (var movieId in movieIds)
{
    Console.Write($"Movie {movieId}\t");
}
Console.WriteLine();

foreach (var userId in userIds)
{
    Console.Write($"User {userId}:\t");
    foreach (var movieId in movieIds)
    {
        var rating = movieData.FirstOrDefault(x => x.UserId == userId && x.MovieId == movieId);
        Console.Write($"{rating.Rating}\t");
    }
    Console.WriteLine();
}

=== Matrice Utilisateur-Film ===
Film    Movie 1 Movie 2 Movie 3 
User 1: 5   4   1   
User 2: 2   1   5   
User 3: 4   3   4   
User 4: 5   4   2   
User 5: 1   2   5   

Exercice 1 : Enrichissement du jeu de données de notes

Le jeu de données actuel contient seulement 5 utilisateurs et 3 films. Ajoutez au moins 3 nouveaux utilisateurs et 2 nouveaux films avec des profils de préférences varies (par exemple un amateur de comedie, un fan de thrillers). créez les nouvelles données et affichez la matrice utilisateur-item enrichie.

Indice : Definissez des profils clairs pour chaque nouvel utilisateur (ex: User 6 aime les thrillers mais pas la romance). Ajoutez les nouveaux MovieRating a la liste movieData. Reconstruisez la matrice avec le code de la cellule précédente pour verifier que les données sont coherentes.

// Exercice 1 : Enrichissement du jeu de donnees de notes
// TODO etudiant : Ajoutez de nouveaux utilisateurs et films avec des profils varies
// Indice : creez des MovieRating supplementaires avec des patterns de preference distincts
// Etape 1 : ajouter 2 nouveaux films (MovieId 4 et 5) avec des genres differents (ex: Comedie, Thriller)
// Etape 2 : ajouter au moins 3 nouveaux utilisateurs (UserId 6, 7, 8) avec des profils varies
// Etape 3 : creer les notes pour chaque nouvel utilisateur sur tous les films
// Etape 4 : afficher la nouvelle matrice utilisateur-item enrichie

var enrichedMovieData = movieData; // TODO etudiant : enrichir avec les nouvelles donnees
Console.WriteLine("Exercice a completer : enrichissement du jeu de donnees de notes");
Exercice a completer : enrichissement du jeu de donnees de notes

Matrix Factorization avec ML.NET

Matrix Factorization (Koren, Bell & Volinsky, 2009) décompose la matrice utilisateur-item en deux matrices de plus faible rang :

Matrice originale (U × I) = Matrice Utilisateur (U × K) × Matrice Item (K × I)

Où K est le nombre de facteurs latents (features cachées).

Interprétation : - Facteur 1 : pourrait représenter “niveau d’action” - Facteur 2 : pourrait représenter “niveau de romance” - etc.

using Microsoft.ML;
using Microsoft.ML.Trainers;
using System;

// Creer le contexte ML
var mlContext = new MLContext(seed: 42);

// Charger les donnees
var trainingData = mlContext.Data.LoadFromEnumerable(movieData);

Console.WriteLine("=== Configuration du systeme de recommandation ===");
Console.WriteLine("Algorithme : Matrix Factorization (Collaborative Filtering)");

// Pipeline : convertir UserId et MovieId en types Key, puis appliquer MatrixFactorization
var pipeline = mlContext.Transforms.Conversion.MapValueToKey("UserIdKey", "UserId")
    .Append(mlContext.Transforms.Conversion.MapValueToKey("MovieIdKey", "MovieId"))
    .Append(mlContext.Recommendation().Trainers.MatrixFactorization(
        new MatrixFactorizationTrainer.Options
        {
            MatrixColumnIndexColumnName = "UserIdKey",
            MatrixRowIndexColumnName = "MovieIdKey",
            LabelColumnName = "Rating",
            NumberOfIterations = 20,
            ApproximationRank = 10  // Nombre de facteurs latents
        }));

Console.WriteLine("Nombre d'iterations : 20");
Console.WriteLine("Facteurs latents (ApproximationRank) : 10");
=== Configuration du systeme de recommandation ===
Algorithme : Matrix Factorization (Collaborative Filtering)
Nombre d'iterations : 20
Facteurs latents (ApproximationRank) : 10

Entraînement du modèle

using Microsoft.ML;
using Microsoft.ML.Data;
using System;

// Entraîner le modèle
Console.WriteLine("\nEntraînement du modèle...");
var model = pipeline.Fit(trainingData);
Console.WriteLine("Modèle entraîné !");

// Créer le moteur de prédiction
var predictionEngine = mlContext.Model.CreatePredictionEngine<MovieRating, MovieRatingPrediction>(model);

// Faire des prédictions
Console.WriteLine("\n=== Prédictions de notes ===");

// Prédire la note de l'Utilisateur 1 pour le Film 3
var prediction1 = predictionEngine.Predict(new MovieRating { UserId = 1, MovieId = 3 });
Console.WriteLine($"Utilisateur 1 - Film 3 : Note prédite = {prediction1.Score:F2} (vraie note = 1)");

// Prédire la note de l'Utilisateur 2 pour le Film 1
var prediction2 = predictionEngine.Predict(new MovieRating { UserId = 2, MovieId = 1 });
Console.WriteLine($"Utilisateur 2 - Film 1 : Note prédite = {prediction2.Score:F2} (vraie note = 2)");

// Prédire une note pour un nouveau film (Film 4)
var prediction3 = predictionEngine.Predict(new MovieRating { UserId = 1, MovieId = 4 });
Console.WriteLine($"Utilisateur 1 - Film 4 (nouveau) : Note prédite = {prediction3.Score:F2}");

Entraînement du modèle...
Modèle entraîné !

=== Prédictions de notes ===
Utilisateur 1 - Film 3 : Note prédite = 1,12 (vraie note = 1)
Utilisateur 2 - Film 1 : Note prédite = 1,68 (vraie note = 2)
Utilisateur 1 - Film 4 (nouveau) : Note prédite = NaN

Exercice 2 : Similarite entre utilisateurs

Calculez la similarite cosinus entre chaque paire d’utilisateurs a partir de leurs vecteurs de notes dans la matrice utilisateur-item. Identifiez les deux utilisateurs les plus similaires et les deux les plus différents.

Indice : La similarite cosinus entre deux vecteurs A et B = (A.B) / (||A|| * ||B||). Representez chaque utilisateur par un vecteur de notes (taille = nombre de films, 0 pour les films non notes). Comparez les paires (1,2), (1,3), (1,4), (1,5), (2,3), (2,4), (2,5), (3,4), (3,5), (4,5).

// Exercice 2 : Similarite entre utilisateurs
// TODO etudiant : Calculez la similarite cosinus entre toutes les paires d'utilisateurs
// Indice : cos(A,B) = (A.B) / (||A|| * ||B||), representez chaque user par son vecteur de notes
// Etape 1 : construire un dictionnaire UserId -> vecteur de notes (3 dimensions, 1 par film)
// Etape 2 : implementer la fonction similarite cosinus (produit scalaire / normes)
// Etape 3 : calculer la similarite pour chaque paire d'utilisateurs
// Etape 4 : identifier les utilisateurs les plus similaires et les plus differents

// var userSimilarities = ... // TODO etudiant : remplacer par la matrice de similarite
Console.WriteLine("Exercice a completer : similarite cosinus entre utilisateurs");
Exercice a completer : similarite cosinus entre utilisateurs

Générer les Top-N recommandations

Pour chaque utilisateur, nous pouvons recommander les N films avec les notes prédites les plus élevées.

using Microsoft.ML;
using Microsoft.ML.Data;
using System;
using System.Collections.Generic;
using System.Linq;

// Fonction pour recommander les Top-N films
public List<(int movieId, float score)> RecommendTopMovies(
    int userId, 
    int[] allMovieIds,
    int[] watchedMovieIds,
    int topN = 3)
{
    var predictions = new List<(int movieId, float score)>();
    
    foreach (var movieId in allMovieIds)
    {
        // Ne pas recommander les films déjà notés
        if (watchedMovieIds.Contains(movieId))
            continue;
        
        var prediction = predictionEngine.Predict(
            new MovieRating { UserId = userId, MovieId = movieId }
        );
        
        predictions.Add((movieId, prediction.Score));
    }
    
    return predictions.OrderByDescending(x => x.score).Take(topN).ToList();
}

// Films disponibles (incluant nouveaux films)
var allMovieIds = new[] { 1, 2, 3, 4, 5 };

Console.WriteLine("\n=== Top-3 Recommandations par utilisateur ===");

foreach (var userId in new[] { 1, 2, 3 })
{
    var watchedMovies = movieData
        .Where(x => x.UserId == userId)
        .Select(x => (int)x.MovieId)
        .ToArray();
    
    var recommendations = RecommendTopMovies(userId, allMovieIds, watchedMovies, topN: 3);
    
    Console.WriteLine($"\nUtilisateur {userId}:");
    foreach (var (movieId, score) in recommendations)
    {
        Console.WriteLine($"  - Film {movieId} : note prédite {score:F2}");
    }
}

=== Top-3 Recommandations par utilisateur ===

Utilisateur 1:
  - Film 4 : note prédite NaN
  - Film 5 : note prédite NaN

Utilisateur 2:
  - Film 4 : note prédite NaN
  - Film 5 : note prédite NaN

Utilisateur 3:
  - Film 4 : note prédite NaN
  - Film 5 : note prédite NaN

Interprétation : le problème du cold start, observé en direct

Toutes les notes prédites ci-dessus valent NaN pour les Films 4 et 5. Ce n’est pas un bug : c’est la manifestation directe du problème du cold start (démarrage à froid), la limitation fondamentale du filtrage collaboratif.

Pourquoi NaN ? Le modèle de Matrix Factorization a été entraîné sur movieData, qui ne contient des notes que pour les Films 1, 2 et 3 (cf. la cellule de configuration du système). Or la fonction de recommandation demande une prédiction pour les Films 4 et 5 — des films nouveaux, absents du jeu d’entraînement. La Matrix Factorization apprend un vecteur latent pour chaque film à partir des notes qu’il a reçues : pour un film jamais noté, aucun facteur n’est appris, et le produit scalaire utilisateur·film devient indéfini (NaN).

Par construction, le filtrage collaboratif ne sait rien faire pour un item (ou un utilisateur) sans aucune interaction historique. C’est précisément cette limite que la section « Le Cold Start Problem (Schein et al., 2002) », plus bas dans ce notebook, formalise et propose de mitiger (approches content-based, hybrides, biais appris).

Leçon à retenir pour l’évaluation qui suit : un système purement collaboratif est aveugle aux nouveautés — une contrainte structurelle à intégrer dans le choix des métriques et le design du système.

Évaluation du système de recommandation

Métriques d’évaluation

Métrique Définition Interprétation
RMSE √(Moyenne((note_prédite - note_réelle)²)) Erreur de prédiction (plus bas = mieux)
MAE Moyenne(\|note_prédite - note_réelle\|) Erreur absolue moyenne
Precision@K \|Recommandations pertinentes\| / K Proportion de recommandations utiles
Recall@K \|Recommandations pertinentes\| / \|Total pertinent\| Couverture des items pertinents
NDCG@K Normalized DCG Qualité du classement

Split Train/Test pour l’évaluation

using Microsoft.ML;
using Microsoft.ML.Data;
using System;

// Diviser les données en train/test (80%/20%)
var split = mlContext.Data.TrainTestSplit(trainingData, testFraction: 0.2, seed: 42);

// Entraîner sur le train set
var trainModel = pipeline.Fit(split.TrainSet);

// Évaluer sur le test set
var predictions = trainModel.Transform(split.TestSet);
var metrics = mlContext.Regression.Evaluate(predictions, labelColumnName: "Rating");

Console.WriteLine("=== Évaluation sur le test set ===");
Console.WriteLine($"RMSE : {metrics.RootMeanSquaredError:F3}");
Console.WriteLine($"MAE : {metrics.MeanAbsoluteError:F3}");
Console.WriteLine($"R2 : {metrics.RSquared:F3}");

// Interprétation
Console.WriteLine("\nInterprétation :");
if (metrics.RootMeanSquaredError < 1.0)
    Console.WriteLine("Excellent : RMSE < 1");
else if (metrics.RootMeanSquaredError < 1.5)
    Console.WriteLine("Bon : RMSE < 1.5");
else
    Console.WriteLine("Moyen : RMSE >= 1.5");


// Note pedagogique sur le R2
Console.WriteLine();
Console.WriteLine("Note : un R2 tres negatif (ex: -10) signifie que le modele predit");
Console.WriteLine("moins bien qu'une simple moyenne. Cela arrive souvent avec un jeu");
Console.WriteLine("de donnees tres petit (15 notes) et un split train/test qui isole");
Console.WriteLine("seulement 2-3 exemples dans le test set. Dans un scenario reel,");
Console.WriteLine("on utiliserait des milliers de notes pour obtenir un R2 positif.");
=== Évaluation sur le test set ===
RMSE : 2,997
MAE : 2,610
R2 : -34,933

Interprétation :
Moyen : RMSE >= 1.5

Note : un R2 tres negatif (ex: -10) signifie que le modele predit
moins bien qu'une simple moyenne. Cela arrive souvent avec un jeu
de donnees tres petit (15 notes) et un split train/test qui isole
seulement 2-3 exemples dans le test set. Dans un scenario reel,
on utiliserait des milliers de notes pour obtenir un R2 positif.

Pourquoi MF echoue sur 15 notes mais reussit sur 500

L’evaluation ci-dessus (cellule 18) donne un R² de −34 : sur 15 notes, Matrix Factorization predit moins bien que la simple moyenne. Ce n’est pas un bug du moteur — c’est une limite fondamentale des facteurs latents : avec 5 utilisateurs et 3 films, la matrice utilisateur×film est pleine (15 cases sur 15), il n’y a aucune structure latente a decouvrir. Le modèle sur-ajuste un exemple unique par case.

La capacite distinctive de MF — extraire des facteurs latents (genres, affinites) a partir d’une matrice sparse (majorite de cases vides, remplissage ~10%) — ne s’exerce que sur un volume suffisant. On applique donc le même moteur sur un jeu de données reel sparse (product-ratings.csv : 500 notes, 99 utilisateurs × 50 produits). Sur ce volume, les facteurs latents capturent enfin la structure, et MF bat la baseline de la moyenne.

using System.Globalization;
using System.IO;

// --- Redemption : le MEME moteur MF sur un jeu de donnees reel sparse ---
// Sur 15 notes, MF ne battait pas la moyenne (R2 = -34, sur-ajustement d'une matrice pleine).
// Sur 500 notes sparse (densite ~10%), les facteurs latents captent la structure.
public class ProdRating { public float UserID; public float ProductID; public float Rating; }

var prodLines = File.ReadAllLines("product-ratings.csv").Skip(1);
var prods = new List<ProdRating>();
foreach (var ln in prodLines)
{
    var p = ln.Split(',');
    prods.Add(new ProdRating {
        UserID = float.Parse(p[0], CultureInfo.InvariantCulture),
        ProductID = float.Parse(p[1], CultureInfo.InvariantCulture),
        Rating = float.Parse(p[2], CultureInfo.InvariantCulture) });
}

var prodData = mlContext.Data.LoadFromEnumerable(prods);
var prodSplit = mlContext.Data.TrainTestSplit(prodData, testFraction: 0.2, seed: 42);

// Baseline triviale : predire la note moyenne du train set sur le test set.
float trainMean = mlContext.Data.CreateEnumerable<ProdRating>(prodSplit.TrainSet, reuseRowObject: false)
    .Average(x => x.Rating);
var testRows = mlContext.Data.CreateEnumerable<ProdRating>(prodSplit.TestSet, reuseRowObject: false).ToList();
double meanRMSE = Math.Sqrt(testRows.Average(x => Math.Pow(x.Rating - trainMean, 2)));

// Matrix Factorization (meme config que l'exemple jouet : rank 10, 20 iterations)
var prodPipe = mlContext.Transforms.Conversion.MapValueToKey("UID", "UserID")
    .Append(mlContext.Transforms.Conversion.MapValueToKey("PID", "ProductID"))
    .Append(mlContext.Recommendation().Trainers.MatrixFactorization(
        new MatrixFactorizationTrainer.Options {
            MatrixColumnIndexColumnName = "UID",
            MatrixRowIndexColumnName = "PID",
            LabelColumnName = "Rating",
            NumberOfIterations = 20,
            ApproximationRank = 10 }));

var prodModel = prodPipe.Fit(prodSplit.TrainSet);
var prodMetrics = mlContext.Regression.Evaluate(prodModel.Transform(prodSplit.TestSet), labelColumnName: "Rating");

Console.WriteLine("=== MF sur un jeu de donnees reel sparse (product-ratings.csv) ===");
Console.WriteLine($"Notes : {prods.Count} | Utilisateurs : {prods.Select(x => x.UserID).Distinct().Count()} | Produits : {prods.Select(x => x.ProductID).Distinct().Count()}");
Console.WriteLine($"Baseline (moyenne = {trainMean:F2}) RMSE = {meanRMSE:F3}");
Console.WriteLine($"MF RMSE = {prodMetrics.RootMeanSquaredError:F3} | MAE = {prodMetrics.MeanAbsoluteError:F3} | R2 = {prodMetrics.RSquared:F3}");
Console.WriteLine($"MF bat la moyenne ? RMSE {prodMetrics.RootMeanSquaredError:F3} < {meanRMSE:F3} => {(prodMetrics.RootMeanSquaredError < meanRMSE ? "OUI" : "NON")}");
Console.WriteLine($"Gain relatif : {(1 - prodMetrics.RootMeanSquaredError / meanRMSE) * 100:F1}% de RMSE en moins vs la baseline moyenne.");
Console.WriteLine();
Console.WriteLine("Contraste avec l'exemple jouet (cellule 18, 15 notes) : R2 = -34 (MF < moyenne).");
Console.WriteLine("=> Les facteurs latents de MF ne sont utiles QUE sur une matrice sparse suffisamment remplie.");
=== MF sur un jeu de donnees reel sparse (product-ratings.csv) ===
Notes : 500 | Utilisateurs : 99 | Produits : 50
Baseline (moyenne = 4,21) RMSE = 0,613
MF RMSE = 0,388 | MAE = 0,313 | R2 = 0,591
MF bat la moyenne ? RMSE 0,388 < 0,613 => OUI
Gain relatif : 36,6% de RMSE en moins vs la baseline moyenne.

Contraste avec l'exemple jouet (cellule 18, 15 notes) : R2 = -34 (MF < moyenne).
=> Les facteurs latents de MF ne sont utiles QUE sur une matrice sparse suffisamment remplie.

Le Cold Start Problem (Schein et al., 2002)

Problème : Comment recommander à un nouvel utilisateur ou pour un nouvel item ?

Solutions : 1. Utiliser des approches hybrides : Collaborative + Content-based 2. Demander des préférences : “Quels genres aimez-vous ?” 3. Populaire par défaut : Recommander les items les plus notés 4. Matrice de moyenne : Utiliser la note moyenne de l’item/utilisateur

using System;
using System.Linq;

// Exemple : Cold Start pour un nouvel utilisateur
var newUserId = 6;

// Option 1 : Recommander les films les plus populaires
var popularMovies = movieData
    .GroupBy(x => x.MovieId)
    .Select(g => new { MovieId = g.Key, AvgRating = g.Average(x => x.Rating), Count = g.Count() })
    .OrderByDescending(x => x.AvgRating)
    .Take(3)
    .ToList();

Console.WriteLine("=== Recommandations pour nouvel utilisateur (Cold Start) ===");
Console.WriteLine("\nOption 1 - Films les plus populaires :");
foreach (var movie in popularMovies)
{
    Console.WriteLine($"  - Film {movie.MovieId} : note moyenne {movie.AvgRating:F2} ({movie.Count} notes)");
}

// Option 2 : Demander à l'utilisateur de noter quelques films
Console.WriteLine("\nOption 2 - Demander des préférences :");
Console.WriteLine("  'Pouvez-vous noter 3 films que vous avez aimés ?'");
Console.WriteLine("  Une fois les notes obtenues, utiliser Matrix Factorization normalement.");

// Option 3 : Approche content-based
Console.WriteLine("\nOption 3 - Recommandations basées sur le contenu :");
Console.WriteLine("  'Vous aimez l'Action ? Voici les meilleurs films d'Action.'");
=== Recommandations pour nouvel utilisateur (Cold Start) ===

Option 1 - Films les plus populaires :
  - Film 1 : note moyenne 3,40 (5 notes)
  - Film 3 : note moyenne 3,40 (5 notes)
  - Film 2 : note moyenne 2,80 (5 notes)

Option 2 - Demander des préférences :
  'Pouvez-vous noter 3 films que vous avez aimés ?'
  Une fois les notes obtenues, utiliser Matrix Factorization normalement.

Option 3 - Recommandations basées sur le contenu :
  'Vous aimez l'Action ? Voici les meilleurs films d'Action.'

Exercice 3 : Diversite des recommandations

Les systèmes de recommandation ont tendance a proposer des items trop similaires. Implementez une mesure de diversite en calculant la moyenne des distances entre les items recommandes (par exemple en utilisant l’ecart-type des MovieId dans le Top-5). Proposez une stratégie simple pour ameliorer la diversite.

Indice : Un ensemble diversifie a des MovieId repartis sur un large spectre. Calculer l’ecart-type des MovieId dans le Top-5 recommande. Pour ameliorer la diversite, vous pouvez forcer la sélection d’items de catégories différentes, ou penaliser les items trop similaires dans le score final.

// Exercice 3 : Diversite des recommandations
// TODO etudiant : Mesurez et ameliorez la diversite des recommandations
// Indice : calculez l'ecart-type des MovieId dans le Top-5 recommande
// Etape 1 : generer les Top-5 recommandations pour l'utilisateur 4 (par exemple)
// Etape 2 : calculer l'ecart-type des MovieId recommandes (mesure de diversite)
// Etape 3 : implementer une strategie d'amélioration (ex: penaliser les items consecutifs)
// Etape 4 : comparer la diversite avant et apres amelioration

var diversityScore = 0.0; // TODO etudiant : remplacer par le score de diversite calcule
Console.WriteLine("Exercice a completer : diversite des recommandations");
Exercice a completer : diversite des recommandations

Exemple 2 : Recommandation de produits E-commerce

Adaptons le système pour un scénario e-commerce avec des achats (binary : acheté/pas acheté) - le cadre du feedback implicite (Hu, Koren & Volinsky, 2008).

using Microsoft.ML;
using Microsoft.ML.Data;
using System;
using System.Collections.Generic;
using System.Linq;

// Classe pour les données d'achat
public class ProductPurchase
{
    public float UserId { get; set; }
    public float ProductId { get; set; }
    public bool Purchased { get; set; }  // false = non acheté, true = acheté
}

public class ProductRecommendationPrediction
{
    public float Score { get; set; }        // Marge brute (log-odds, non bornée) produite par le classifieur
    public float Probability { get; set; }  // Probabilite calibree (sigmoid), dans [0, 1]
}

// Simuler des données d'achat
var purchaseData = new List<ProductPurchase>();
var rand = new Random(123);

for (int userId = 1; userId <= 50; userId++)
{
    for (int productId = 1; productId <= 20; productId++)
    {
        // Probabilité d'achat basée sur des préférences cachées
        float purchaseProb = ((userId % 5 == productId % 5) ? 0.7f : 0.1f);
        bool purchased = rand.NextDouble() < purchaseProb;
        
        purchaseData.Add(new ProductPurchase
        {
            UserId = userId,
            ProductId = productId,
            Purchased = purchased
        });
    }
}

// Filtrer uniquement les achats positifs pour l'entraînement
var positivePurchases = purchaseData.Where(x => x.Purchased).ToList();

Console.WriteLine($"Total achats positifs : {positivePurchases.Count}");
Total achats positifs : 224

Import des espaces de noms pour le système de recommandation.

using Microsoft.ML;
using Microsoft.ML.Data;
using Microsoft.ML.Trainers;
using System;
using System.Collections.Generic;
using System.Linq;

// Utiliser TOUTES les donnees (achats ET non-achats) pour la classification binaire
// Un classifieur binaire necessite les deux classes (true/false) pour s'entrainer
var productData = mlContext.Data.LoadFromEnumerable(purchaseData);

// Convertir le label bool en type Boolean explicite + concatenation des features
var productPipeline = mlContext.Transforms.Conversion.ConvertType("Purchased", outputKind: DataKind.Boolean)
    .Append(mlContext.Transforms.Concatenate("Features", "UserId", "ProductId"))
    .Append(mlContext.BinaryClassification.Trainers.SdcaLogisticRegression(
        labelColumnName: "Purchased",
        featureColumnName: "Features",
        maximumNumberOfIterations: 100));

Console.WriteLine("Entrainement du modele de recommandation de produits...");
var productModel = productPipeline.Fit(productData);
Console.WriteLine("Modele entraine !");

// Faire des recommandations pour un utilisateur.
// SdcaLogisticRegression est un classifieur CALIBRE : il expose deux sorties distinctes :
//   - Score       : la marge brute (log-odds, valeur reelle non bornee, peut etre negative)
//   - Probability : la probabilite calibree par sigmoid, dans [0, 1] (ce qu'on veut afficher)
// Tri par Probability (equivalent au tri par Score car la sigmoid est croissante, mais
// sémantiquement correct : on ordonne selon la vraie probabilite d'achat).
var productEngine = mlContext.Model.CreatePredictionEngine<ProductPurchase, ProductRecommendationPrediction>(productModel);

Console.WriteLine("\n=== Top-5 Recommandations de produits pour l'Utilisateur 1 ===");
var userRecommendations = new List<(int productId, float probability, float score)>();

for (int productId = 1; productId <= 20; productId++)
{
    var pred = productEngine.Predict(new ProductPurchase { UserId = 1, ProductId = productId });
    userRecommendations.Add((productId, pred.Probability, pred.Score));
}

var top5 = userRecommendations.OrderByDescending(x => x.probability).Take(5);
foreach (var (productId, probability, score) in top5)
{
    Console.WriteLine($"  - Produit {productId} : probabilite d'achat = {probability:F3}  (marge = {score:F3})");
}
Entrainement du modele de recommandation de produits...
Modele entraine !

=== Top-5 Recommandations de produits pour l'Utilisateur 1 ===
  - Produit 1 : probabilite d'achat = 0,326  (marge = -0,729)
  - Produit 2 : probabilite d'achat = 0,319  (marge = -0,756)
  - Produit 3 : probabilite d'achat = 0,314  (marge = -0,784)
  - Produit 4 : probabilite d'achat = 0,308  (marge = -0,811)
  - Produit 5 : probabilite d'achat = 0,302  (marge = -0,839)

Lecture : Score (marge) vs Probability (probabilite calibree)

Les valeurs affichees meritent une remarque importante. SdcaLogisticRegression est un classifieur binaire calibre : sa sortie expose deux champs distincts, qu’il ne faut pas confondre :

  • Score – la marge brute (log-odds), une valeur reelle non bornee qui peut etre negative (ex. \(-0{,}728\)). Plus elle est elevee, plus le modele est confiant dans la classe positive.
  • Probability – la probabilite calibree par la fonction sigmoid \(\sigma(\text{Score}) = 1/(1+e^{-\text{Score}})\), bornee dans \([0, 1]\). C’est elle que l’on veut afficher sous le nom « probabilite d’achat ».

Piege evite : afficher Score en l’etiquetant « probabilite » aurait montre des valeurs negatives (\(-0{,}728 \dots -0{,}876\)) – une absurdite pour une probabilite. La correction (utiliser Probability) donne des valeurs valides, ici faibles (\(\approx 0{,}30\)) car, pour l’Utilisateur 1, aucun produit n’est fortement predict comme « sera achete » avec le jeu d’entraînement synthetique.

L’ordre des recommandations (Top-5) est heureusement identique que l’on trie par Score ou par Probability : la sigmoid etant strictement croissante, \(\text{Score}_a > \text{Score}_b \iff \text{Probability}_a > \text{Probability}_b\). Le classement etait donc juste ; seules les valeurs affichees etaient erronees.

Résumé et conclusion

Ce notebook a présenté les systèmes de recommandation avec ML.NET :

Concept Clé
Collaborative Filtering Utiliser les préférences des utilisateurs similaires
Matrix Factorization Décomposer la matrice utilisateur-item en facteurs latents
Approximation Rank (K) Nombre de facteurs latents (10-100 typique)
Cold Start Problem Difficile de recommander à un nouvel utilisateur/item
RMSE/MAE Métriques pour évaluer la précision des prédictions

Points clés : 1. Matrix Factorization apprend des préférences cachées (facteurs latents) 2. Le cold start problem nécessite des solutions hybrides ou content-based 3. L’évaluation se fait sur des données non vues (test set) 4. Les recommandations doivent être diversifiées (pas seulement les plus populaires)

Bonnes pratiques : - Utiliser 50-100 facteurs latents pour les grands catalogues - Combiner avec le content-based pour le cold start - Ré-entraîner régulièrement avec les nouvelles données - Surveiller la serendipity (surprises positives)

Limitations : - Nécessite beaucoup de données utilisateur-item - Difficile de recommander de nouveaux items (cold start) - Les biais des utilisateurs affectent les recommandations

Pour aller plus loin : - Matrix Factorization Research - ML.NET Recommender Documentation - Collaborative Filtering Deep Dive


Navigation : << ML-6-ONNX | Index

Exercices supplementaires

Exercice 4 : Recommandations Top-N personnalisees

Etant donne le modèle de recommandation entraine dans les exemples précédents, generez les 5 meilleures recommandations pour un utilisateur spécifique en calculant le score de tous les items et en les triant.

Indice : Parcourez tous les MovieId possibles, utilisez le predictionEngine pour obtenir le score de chaque film non encore note par l’utilisateur, puis triez par score decroissant.

// Exercice 4 : Recommandations Top-N personnalisees
// TODO etudiant : Generez les Top-5 recommandations pour un utilisateur donne
// Indice : parcourez tous les MovieId, predisez le score, triez par ordre decroissant
// Etape 1 : choisir un utilisateur (par exemple User 4)
// Etape 2 : identifier les films deja notes par cet utilisateur
// Etape 3 : pour chaque film non note, predire le score avec predictionEngine
// Etape 4 : trier par score decroissant et afficher le Top 5

// var top5Recommendations = ... // TODO etudiant : remplacer par la liste des Top-5
Console.WriteLine("Exercice a completer : recommandations Top-5 personnalisees");
Exercice a completer : recommandations Top-5 personnalisees

Exercice 5 : stratégie de cold start

Implementez une stratégie simple de cold start pour les nouveaux utilisateurs : si un utilisateur a moins de 3 notes, recommandez les items les plus populaires (meilleure note moyenne avec un minimum de 10 evaluations).

Indice : Pour les utilisateurs avec peu de notes, calculez la note moyenne de chaque film en exigeant un minimum d’evaluations. Triez par note moyenne decroissante pour obtenir les films les plus populaires.

// Exercice 5 : Strategie de cold start
// TODO etudiant : Implementez une strategie de recommandation pour les nouveaux utilisateurs
// Indice : pour les utilisateurs avec moins de 3 notes, recommandez les films les plus populaires
// Etape 1 : identifier les utilisateurs ayant moins de 3 notes dans les donnees
// Etape 2 : calculer la note moyenne de chaque film (minimum 10 evaluations dans un cas reel)
// Etape 3 : trier les films par note moyenne decroissante
// Etape 4 : afficher les recommandations cold start pour un nouvel utilisateur

// var coldStartRecommendations = ... // TODO etudiant : remplacer par les recommandations
Console.WriteLine("Exercice a completer : strategie de cold start pour nouveaux utilisateurs");
Exercice a completer : strategie de cold start pour nouveaux utilisateurs

🎯 Exercice 6: Système de recommandation de livres

Créez un système de recommandation pour une bibliothèque en ligne.

Objectifs

  1. Définir les classes BookRating (UserId, BookId, Rating)
  2. Créer un jeu de données avec 10 utilisateurs et 8 livres
  3. Entraîner un modèle de recommandation
  4. Générer les Top-3 recommandations pour un utilisateur spécifique
  5. Gérer le cold start pour un nouvel utilisateur (optionnel)

Indices - Les livres peuvent avoir des genres : Fiction, Science-Fiction, Roman, Thriller, Biographie - Créez des patterns de préférence (ex: User 1 aime SF, User 2 aime Romans) - Utilisez le pipeline MatrixFactorization comme dans l’exemple films (MapValueToKey + MatrixFactorization) - Pour le cold start, recommandez les livres les mieux notés globalement

// Exercice 6 : Système de recommandation de livres

// TODO: Définir la classe BookRating
public class BookRating
{
    // Propriétés: UserId, BookId, Rating (1-5)
}

// TODO: Définir la classe BookRatingPrediction
public class BookRatingPrediction
{
    // Propriété: Score (note prédite)
}

// TODO: Créer les données d'entraînement (10 utilisateurs, 8 livres)
// Variez les préférences : certains aiment la SF, d'autres les Romans, etc.
var bookData = new List<BookRating>
{
    // Ajoutez au moins 30 notes
};

// TODO: Créer le MLContext et charger les données
MLContext bookContext = null;
IDataView bookTrainingData = null;

// TODO: Construire le pipeline de recommandation
IEstimator<ITransformer> bookPipeline = null;

// TODO: Entraîner le modèle
ITransformer bookModel = null;

// TODO: Créer le moteur de prédiction
PredictionEngine<BookRating, BookRatingPrediction> bookEngine = null;

// TODO: Générer les Top-3 recommandations pour l'Utilisateur 1
Console.WriteLine("=== Top-3 Recommandations de livres pour l'Utilisateur 1 ===");
// Pour chaque livre non noté par l'utilisateur, prédire la note
// Trier par note prédite décroissante et afficher le Top 3

// TODO (bonus): Cold Start - Recommandations pour un nouvel utilisateur (User 11)
Console.WriteLine("\n=== Cold Start - Recommandations pour nouvel utilisateur ===");
// Utiliser la moyenne globale des livres
List<(int bookId, float score)> topBooks = null; // Livres les mieux notés globalement
=== Top-3 Recommandations de livres pour l'Utilisateur 1 ===

=== Cold Start - Recommandations pour nouvel utilisateur ===

Références

Matrix Factorization et Collaborative Filtering - Koren, Y., Bell, R., & Volinsky, C. (2009). Matrix Factorization Techniques for Recommender Systems. IEEE Computer, 42(8), 30-37. — référence canonique du collaborative filtering par factorisation, issue du Netflix Prize. - Hu, Y., Koren, Y., & Volinsky, C. (2008). Collaborative Filtering for Implicit Feedback Datasets. IEEE ICDM. — cadre du feedback implicite (achats binaires de l’Exemple 2). - Sarwar, B., Karypis, G., Konstan, J., & Riedl, J. (2001). Item-Based Collaborative Filtering Recommendation Algorithms. WWW.

Problème du démarrage à froid (Cold Start) - Schein, A. I., Popescul, A., Ungar, L. H., & Pennock, D. M. (2002). Methods and Metrics for Cold-Start Recommendations. ACM SIGIR.

Implémentation (ML.NET / LIBMF) - Chin, W.-S., Zhuang, Y., Juan, Y.-C., & Lin, C.-J. (2015). A Fast Parallel Stochastic Gradient Method for Matrix Factorization in Shared Memory Systems. ACM TIST. — bibliothèque LIBMF, moteur du MatrixFactorizationTrainer de ML.NET. - Ahmed, Z., et al. (2019). Machine Learning at Microsoft with ML.NET. ACM SIGKDD (KDD).

Contexte historique - Bennett, J., & Lanning, S. (2007). The Netflix Prize. KDD Cup and Workshop.

Retour au sommet