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)
=== 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-filmvar 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éesConsole.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 enrichievar enrichedMovieData = movieData;// TODO etudiant : enrichir avec les nouvelles donneesConsole.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).
using Microsoft.ML;using Microsoft.ML.Trainers;using System;// Creer le contexte MLvar mlContext =newMLContext(seed:42);// Charger les donneesvar 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 MatrixFactorizationvar 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èleConsole.WriteLine("\nEntraînement du modèle...");var model = pipeline.Fit(trainingData);Console.WriteLine("Modèle entraîné !");// Créer le moteur de prédictionvar predictionEngine = mlContext.Model.CreatePredictionEngine<MovieRating, MovieRatingPrediction>(model);// Faire des prédictionsConsole.WriteLine("\n=== Prédictions de notes ===");// Prédire la note de l'Utilisateur 1 pour le Film 3var 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 1var 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 similariteConsole.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 filmspublic 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ésif(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 innew[]{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.
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 setvar trainModel = pipeline.Fit(split.TrainSet);// Évaluer sur le test setvar 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étationConsole.WriteLine("\nInterprétation :");if(metrics.RootMeanSquaredError<1.0) Console.WriteLine("Excellent : RMSE < 1");elseif(metrics.RootMeanSquaredError<1.5) Console.WriteLine("Bon : RMSE < 1.5");else Console.WriteLine("Moyen : RMSE >= 1.5");// Note pedagogique sur le R2Console.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.publicclass ProdRating {publicfloat UserID;publicfloat ProductID;publicfloat 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 utilisateurvar newUserId =6;// Option 1 : Recommander les films les plus populairesvar 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 filmsConsole.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-basedConsole.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 ameliorationvar diversityScore =0.0;// TODO etudiant : remplacer par le score de diversite calculeConsole.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'achatpublicclass ProductPurchase{publicfloat UserId {get;set;}publicfloat ProductId {get;set;}publicbool Purchased {get;set;}// false = non acheté, true = acheté}publicclass ProductRecommendationPrediction{publicfloat Score {get;set;}// Marge brute (log-odds, non bornée) produite par le classifieurpublicfloat Probability {get;set;}// Probabilite calibree (sigmoid), dans [0, 1]}// Simuler des données d'achatvar purchaseData =new List<ProductPurchase>();var rand =newRandom(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éesfloat 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înementvar 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'entrainervar productData = mlContext.Data.LoadFromEnumerable(purchaseData);// Convertir le label bool en type Boolean explicite + concatenation des featuresvar 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
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-5Console.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 recommandationsConsole.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
Définir les classes BookRating (UserId, BookId, Rating)
Créer un jeu de données avec 10 utilisateurs et 8 livres
Entraîner un modèle de recommandation
Générer les Top-3 recommandations pour un utilisateur spécifique
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 BookRatingpublicclass BookRating{// Propriétés: UserId, BookId, Rating (1-5)}// TODO: Définir la classe BookRatingPredictionpublicclass 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éesMLContext bookContext =null;IDataView bookTrainingData =null;// TODO: Construire le pipeline de recommandationIEstimator<ITransformer> bookPipeline =null;// TODO: Entraîner le modèleITransformer bookModel =null;// TODO: Créer le moteur de prédictionPredictionEngine<BookRating, BookRatingPrediction> bookEngine =null;// TODO: Générer les Top-3 recommandations pour l'Utilisateur 1Console.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 livresList<(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.