import numpy as np
import pandas as pd
from sklearn.decomposition import NMF
print(f"numpy {np.__version__} | pandas {pd.__version__} | sklearn NMF charge")numpy 2.4.4 | pandas 3.0.2 | sklearn NMF charge
Navigation : Index | << ML-6-ONNX | Jumeau .NET (ML.NET) | Suivant >> ML-8-Clustering
Ce notebook est le jumeau Python (scikit-learn) de ML-7-Recommendation.ipynb (ML.NET). Il couvre la même pédagogie (filtrage collaboratif par factorisation de matrice) avec
sklearn.decomposition.NMF.
À la fin de ce notebook, vous saurez :
Un système de recommandation suggère des items (films, produits, articles) pertinents à un utilisateur. Trois grandes familles :
Ce notebook se concentre sur le filtrage collaboratif par factorisation de matrice, l’approche canonique (Koren, Bell & Volinsky, 2009 — prix Netflix).
Le cas d’usage : une plateforme de films dispose des notes (1 à 5) attribuées par des utilisateurs à des films. Dans les vrais systèmes, la matrice utilisateur-item est clairsemée (chaque utilisateur ne note qu’une petite fraction des films). Notre exemple jouet ci-dessous est volontairement presque complet (23 notes sur 25, 92 % — cf. cellule 4) pour rester lisible ; il contient malgré tout deux notes manquantes à prédire. On veut prédire les notes manquantes pour recommander les films que l’utilisateur noterait haut.
On construit un petit jeu de données réeliste : 5 utilisateurs notent 5 films sur l’échelle 1-5. Les profils de goût sont nets pour exercer le détecteur de facteurs latents :
Les cellules 0 représentent les notes manquantes (film non vu) — c’est ce qu’on veut prédire.
| Film | Genre |
|---|---|
| Matrix | Sci-Fi / Action |
| Inception | Sci-Fi |
| Titanic | Romance |
| Interstellar | Sci-Fi |
| Le Notebook | Romance |
movies = ["Matrix", "Inception", "Titanic", "Interstellar", "Le Notebook"]
users = ["U1", "U2", "U3", "U4", "U5"]
# Matrice utilisateur-item : lignes = utilisateurs, colonnes = films. 0 = note manquante (film non vu).
R = np.array([
[5, 4, 1, 5, 0], # U1 : Sci-Fi fan (n'a pas vu Le Notebook)
[2, 1, 5, 0, 5], # U2 : Romance fan (n'a pas vu Interstellar)
[4, 3, 4, 4, 3], # U3 : goûts variés
[5, 4, 2, 4, 1], # U4 : Action fan
[1, 2, 5, 1, 5], # U5 : Romance fan
], dtype=float)
ratings_df = pd.DataFrame(R, index=users, columns=movies)
print("=== Matrice Utilisateur-Film (0 = non vu) ===")
print(ratings_df)
print(f"\nNotes observées : {int(np.count_nonzero(R))} / {R.size} ({100*np.count_nonzero(R)/R.size:.0f}% rempli)")=== Matrice Utilisateur-Film (0 = non vu) ===
Matrix Inception Titanic Interstellar Le Notebook
U1 5.0 4.0 1.0 5.0 0.0
U2 2.0 1.0 5.0 0.0 5.0
U3 4.0 3.0 4.0 4.0 3.0
U4 5.0 4.0 2.0 4.0 1.0
U5 1.0 2.0 5.0 1.0 5.0
Notes observées : 23 / 25 (92% rempli)
La factorisation de matrice (Koren, Bell & Volinsky, 2009) décompose la matrice utilisateur-item \(R\) (\(U \times I\)) en deux matrices de plus faible rang :
\[R \approx W \cdot H\]
où \(W\) est \(U \times K\) (profils latents des utilisateurs) et \(H\) est \(K \times I\) (profils latents des items). K est le nombre de facteurs latents (features cachées apprises).
Interprétation : avec \(K=2\), l’algorithme peut découvrir automatiquement deux axes latents — par exemple « affinité Sci-Fi/Action » et « affinité Romance » — sans qu’on lui ait jamais dit quels films sont quel genre. C’est la puissance du filtrage collaboratif : les goûts émergent des notes, pas des métadonnées.
On utilise NMF (Non-negative Matrix Factorization) de scikit-learn : elle contraint \(W\) et \(H\) à être non-négatifs, ce qui convient bien aux notes (1-5) et rend les facteurs latents interprétables (chaque facteur est une « composante de goût » additive). C’est l’équivalent Python idiomatique du trainer MatrixFactorization de ML.NET, qui apprend lui aussi des facteurs latents utilisateur/item par descente de gradient.
Note sur les zéros. NMF traite les
0(notes manquantes) comme des valeurs à reconstruire. C’est une approximation (le0« non vu » est confondu avec le0« noté 0 ») ; sur une petite matrice clairement structurée, elle reste très pédagogique. Les vrais systèmes gèrent le caractère creux différemment (SGD sur les notes observées seulement).
# Factorisation : K=2 facteurs latents (la matrice a 2 axes de goût dominants : Sci-Fi vs Romance)
K = 2
nmf = NMF(n_components=K, init="nndsvda", solver="mu", max_iter=500, random_state=42)
W = nmf.fit_transform(R) # (5 utilisateurs, K) : profils latents utilisateurs
H = nmf.components_ # (K, 5 films) : profils latents films
R_hat = W @ H # matrice reconstruite (prédictions de toutes les notes)
print(f"NMF ajustée (K={K} facteurs latents) | erreur de reconstruction : {nmf.reconstruction_err_:.3f}")
print("\n=== Matrice reconstruite R_hat (notes prédites) ===")
print(pd.DataFrame(R_hat.round(2), index=users, columns=movies))NMF ajustée (K=2 facteurs latents) | erreur de reconstruction : 1.493
=== Matrice reconstruite R_hat (notes prédites) ===
Matrix Inception Titanic Interstellar Le Notebook
U1 5.07 3.97 1.11 4.93 0.05
U2 1.33 1.28 5.06 0.45 5.00
U3 4.17 3.41 3.81 3.54 3.08
U4 4.75 3.77 2.02 4.44 1.07
U5 1.62 1.51 5.05 0.74 4.93
Regardons les profils latents des films (\(H\)) : chaque film est décrit par ses coordonnées sur les \(K\) facteurs. Si l’algorithme a bien capturé la structure, un facteur devrait opposer les Sci-Fi aux romances.
=== Profils latents des films (H) ===
Matrix Inception Titanic Interstellar Le Notebook
Facteur 1 0.873 0.883 4.154 0.143 4.158
Facteur 2 3.880 3.035 0.841 3.773 0.029
=> Un facteur 'pèse' fort sur Matrix/Inception/Interstellar (Sci-Fi), l'autre sur Titanic/Le Notebook (Romance).
Les cellules 0 (films non vus) sont maintenant reconstruites par \(R\_hat\). Ce sont nos prédictions : la note qu’on s’attendrait à ce que l’utilisateur attribue s’il voyait le film.
# Prédictions pour les notes manquantes (cellules où R == 0)
print("=== Notes prédites pour les films non vus ===")
for u in range(len(users)):
for i in range(len(movies)):
if R[u, i] == 0:
print(f" {users[u]} -> {movies[i]:<12} : note prédite {R_hat[u, i]:.2f}")
print("\nLecture :")
print(" U1 (Sci-Fi) n'a pas vu 'Le Notebook' (Romance) -> note prédite basse (cohérent).")
print(" U2 (Romance) n'a pas vu 'Interstellar' (Sci-Fi) -> note prédite basse (cohérent).")=== Notes prédites pour les films non vus ===
U1 -> Le Notebook : note prédite 0.05
U2 -> Interstellar : note prédite 0.45
Lecture :
U1 (Sci-Fi) n'a pas vu 'Le Notebook' (Romance) -> note prédite basse (cohérent).
U2 (Romance) n'a pas vu 'Interstellar' (Sci-Fi) -> note prédite basse (cohérent).
Pour chaque utilisateur, on recommande les N films non vus dont la note prédite est la plus élevée.
def recommend_top_n(user_idx, R, R_hat, movie_names, top_n=3):
"""Renvoie les top_n films non vus (note prédite la plus élevée) pour un utilisateur."""
unseen = np.where(R[user_idx] == 0)[0]
scored = [(movie_names[i], float(R_hat[user_idx, i])) for i in unseen]
scored.sort(key=lambda x: -x[1])
return scored[:top_n]
print("=== Top-3 recommandations par utilisateur ===")
for u in range(len(users)):
recs = recommend_top_n(u, R, R_hat, movies, top_n=3)
if recs:
recs_str = ", ".join(f"{m} ({s:.2f})" for m, s in recs)
print(f" {users[u]} : {recs_str}")
else:
print(f" {users[u]} : (a tout vu)")=== Top-3 recommandations par utilisateur ===
U1 : Le Notebook (0.05)
U2 : Interstellar (0.45)
U3 : (a tout vu)
U4 : (a tout vu)
U5 : (a tout vu)
Un système de recommandation se laisse évaluer dès lors qu’on a des notes de référence. On compare les notes prédites aux notes réelles sur les cellules observées (non nulles) via le RMSE (root mean squared error) : plus il est faible, mieux la factorisation reconstruit les goûts.
mask = R > 0 # notes observées
rmse = np.sqrt(np.mean((R[mask] - R_hat[mask]) ** 2))
mae = np.mean(np.abs(R[mask] - R_hat[mask]))
print(f"=== Qualité de la reconstruction sur les notes observées ===")
print(f" RMSE : {rmse:.3f} (échelle des notes : 1-5)")
print(f" MAE : {mae:.3f}")
print("\nUn RMSE ~0.3 sur cette petite matrice tres structuree indique une excellente reconstruction (K=2 capture bien l'axe Sci-Fi vs Romance).")=== Qualité de la reconstruction sur les notes observées ===
RMSE : 0.297 (échelle des notes : 1-5)
MAE : 0.222
Un RMSE ~0.3 sur cette petite matrice tres structuree indique une excellente reconstruction (K=2 capture bien l'axe Sci-Fi vs Romance).
La factorisation de matrice a une limite structurelle : elle ne sait pas quoi recommander à un nouvel utilisateur (aucune note → pas de profil latent) ni comment noter un nouvel item (aucune note reçue → pas de profil latent non plus). C’est le cold start problem.
Les systèmes industriels (Netflix, Spotify) combinent CF + content-based + popularité pour mitiger le cold start.
Ajouter un 6ᵉ film et un 6ᵉ utilisateur change la matrice et donc les facteurs latents.
Objectifs : 1. Ajouter un film (colonne) et un utilisateur (ligne) à R, avec un profil de goût cohérent 2. Ré-ajuster la NMF et comparer les facteurs latents des films 3. Constater que la structure Sci-Fi vs Romance émerge toujours
Indice : définis une nouvelle matrice R2 (6×6), ré-invoie NMF(n_components=2).fit_transform(R2), affiche H.
# Exercice 1 : Enrichir le jeu de données
# TODO étudiant : ajoute un film + un utilisateur à R, ré-ajuste la NMF, compare les facteurs latents.
# Indice : R2 = np.vstack/np.column_stack pour étendre ; NMF(n_components=2).fit_transform(R2) ; affiche H.
# Étape 1 : construis R2 (6x6) avec un nouveau film (colonne) et un nouvel utilisateur (ligne)
# Étape 2 : ajuste NMF(K=2) sur R2
# Étape 3 : affiche les profils latents des films et compare à H ci-dessus
result_ex1 = None # TODO étudiant
print("Exercice 1 à compléter")Exercice 1 à compléter
Deux utilisateurs aux goûts proches devraient avoir des profils latents (\(W\)) similaires.
Objectifs : 1. Calculer la similarité cosinus entre tous les couples de lignes de \(W\) (profils utilisateurs) 2. Identifier les paires d’utilisateurs les plus similaires 3. Vérifier que U1/U4 (Sci-Fi) et U2/U5 (Romance) sont bien regroupés
Indice : sklearn.metrics.pairwise.cosine_similarity(W) renvoie la matrice 5×5 des similarités.
# Exercice 2 : Similarité entre utilisateurs (sur les facteurs latents W)
# TODO étudiant : similarité cosinus entre les lignes de W, identifier les paires les plus similaires.
# Indice : from sklearn.metrics.pairwise import cosine_similarity ; S = cosine_similarity(W).
# Étape 1 : calcule la matrice de similarité cosinus entre les profils latents utilisateurs (W)
# Étape 2 : identifie les paires les plus similaires (hors diagonale)
# Étape 3 : vérifie que les fans Sci-Fi (U1/U4) et Romance (U2/U5) sont regroupés
result_ex2 = None # TODO étudiant
print("Exercice 2 à compléter")Exercice 2 à compléter
Le paramètre \(K\) (nombre de facteurs latents) contrôle le compromis biais/variance : trop petit, le modèle sous-ajuste (ne capture pas assez de goûts) ; trop grand, il sur-ajuste (reconstruit le bruit).
Objectifs : 1. Boucler sur \(K = 1, 2, 3, 4, 5\) 2. Pour chaque \(K\), ajuster une NMF et calculer le RMSE sur les notes observées 3. Identifier le \(K\) optimal et discuter du sur-ajustement quand \(K\) approche le rang de la matrice
Indice : réutilise le calcul de RMSE (np.sqrt(np.mean((R[mask] - R_hat[mask])**2))) dans une boucle sur n_components. Quand \(K\) = nombre de films, NMF peut reconstruire exactement (RMSE → 0) mais perd tout pouvoir de généralisation.
# Exercice 3 : Effet du nombre de facteurs latents (K) sur le RMSE
# TODO étudiant : boucle sur K = 1..5, calcule le RMSE sur les notes observées pour chaque K.
# Indice : NMF(n_components=K) -> R_hat = W @ H -> RMSE sur R[mask>0]. Quand K = nb_films, RMSE ~0 (sur-ajustement).
# Étape 1 : pour K dans {1,2,3,4,5}, ajuste NMF(K) et calcule le RMSE sur les notes observées
# Étape 2 : affiche K | RMSE
# Étape 3 : identifie le K optimal (compromis sous-ajustement / sur-ajustement)
result_ex3 = None # TODO étudiant
print("Exercice 3 à compléter")Exercice 3 à compléter
Ce notebook a introduit les systèmes de recommandation par factorisation de matrice en Python :
| Concept | Description |
|---|---|
| Système de recommandation | Suggère des items pertinents (CF, content-based, hybride) |
| Matrice utilisateur-item | Notes \(R\) (\(U \times I\)) ; creuse dans les vrais systèmes (notre jouet est presque complet : 23/25) |
sklearn.decomposition.NMF |
Factorisation non-négative : \(R \approx W \cdot H\) (facteurs latents) |
| Facteurs latents | Axes de goût appris automatiquement (interprétables en NMF) |
| Top-N | Films non vus classés par note prédite décroissante |
| RMSE | Écart entre notes prédites et réelles sur les cellules observées |
| Cold start | Limitation : nouvel utilisateur / nouvel item sans profil latent |
Points clés :
Équivalence ML.NET : ce notebook est le jumeau Python de ML-7-Recommendation.ipynb. NMF reproduit fidèlement l’esprit du trainer MatrixFactorization de ML.NET (tous deux apprennent des facteurs latents utilisateur/item) ; NMF est l’implémentation scikit-learn idiomatique pour des notes positives.