La NMF décompose un tableau de valeurs positives (achats, comptages de mots, pixels) en un petit nombre de parties qui s'additionnent. Comme rien ne peut se soustraire, chaque partie se lit comme un thème : un panier devient un mélange de missions de courses, un texte un mélange de sujets.
Une cuisine observée à l'envers : en regardant des milliers de plats, on retrouve les préparations de base (pâte, sauce, garniture) et la dose de chacune dans chaque plat.
Une ligne par ticket, une colonne par produit, une valeur positive : acheté ou non, pondérée par TF-IDF pour que les produits présents partout pèsent moins.
X ≈ W x H. H décrit k missions par leur poids sur chaque produit. W dit dans quelle proportion chaque ticket mélange ces missions. Aucune valeur négative n'est permise.
Pour chaque mission, on liste les produits de plus fort poids et on la nomme. Pour chaque ticket, la mission de plus fort poids donne son intention principale.
Chaque ticket liste les produits achetés parmi 28 références alimentaires et ménagères. On garde les tickets d'au moins deux articles, les seuls qui disent quelque chose des associations.
Sur le jeu d'exemple, la NMF retrouve sans aide : plat de pâtes (pâtes, sauce tomate, parmesan), apéritif (bière, chips), plateau fromage (fromage, vin rouge), tartines (pain, beurre), pause café (café, biscuits), et une mission « courses du quotidien » qui rassemble lait, eau, yaourts, œufs, jambon, fruits et légumes.
L'erreur de reconstruction baisse toujours quand on ajoute des missions : elle ne suffit pas pour choisir k. On vérifie que chaque mission se nomme en deux ou trois produits, qu'elle réapparaît quand on change la graine ou l'échantillon, et qu'elle sert une action : implantation en rayon, promotion croisée, bon de réduction ciblé.
Noms donnés pour R (package NMF) et Python (scikit-learn).
Nombre de thèmes. Trop peu, les missions se mélangent. Trop, des thèmes à un seul produit apparaissent. On teste plusieurs valeurs et on garde celle qui se nomme le mieux.
Sur des 0/1 bruts, les produits très fréquents forment des thèmes à eux seuls : sur l'exemple, sans pondération, l'eau, le lait et les yaourts occupent chacun un thème. La pondération TF-IDF les atténue et fait ressortir les associations.
L'erreur minimisée : moindres carrés (method="lee" en R, défaut en Python) ou divergence de Kullback-Leibler, adaptée aux comptages (brunet, défaut du package R ; beta_loss="kullback-leibler" avec solver="mu" en Python).
Le résultat dépend du point de départ. init="nndsvda" donne en Python un départ déterministe ; en R, on fixe seed ou on lance plusieurs essais (nrun) et on garde le meilleur.
# Missions de courses : NMF en R
library(NMF)
tickets <- read.csv("tickets_caisse.csv")
paniers <- as.matrix(as.data.frame.matrix(table(tickets$id_ticket, tickets$produit)))
paniers <- paniers[rowSums(paniers) >= 2, ] # un article seul ne dit rien des associations
# TF-IDF : un produit présent dans beaucoup de paniers pèse moins
idf <- log(nrow(paniers) / colSums(paniers > 0))
X <- sweep(paniers, 2, idf, "*")
X <- X / sqrt(rowSums(X^2))
modele <- nmf(X, rank = 6, method = "lee", seed = 42)
W <- basis(modele) # poids de chaque mission dans chaque ticket
H <- coef(modele) # poids de chaque produit dans chaque mission
principale <- apply(W, 1, which.max)
for (j in 1:6) {
cat("Mission", j, ":", colnames(X)[H[j, ] >= 0.25 * max(H[j, ])],
"| mission principale de", round(100 * mean(principale == j)), "% des tickets\n")
}
# Missions de courses : NMF en Python
import numpy as np
import pandas as pd
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.decomposition import NMF
tickets = pd.read_csv("tickets_caisse.csv")
paniers = pd.crosstab(tickets["id_ticket"], tickets["produit"])
paniers = paniers[paniers.sum(axis=1) >= 2] # un article seul ne dit rien des associations
# TF-IDF : un produit présent dans tous les paniers pèse moins
X = TfidfTransformer().fit_transform(paniers.values)
nmf = NMF(n_components=6, init="nndsvda", max_iter=2000, random_state=42)
W = nmf.fit_transform(X) # poids de chaque mission dans chaque ticket
H = nmf.components_ # poids de chaque produit dans chaque mission
for j in range(6):
produits = paniers.columns[H[j] >= 0.25 * H[j].max()] # produits qui portent la mission
print(f"Mission {j + 1} :", ", ".join(produits),
f"| mission principale de {np.mean(W.argmax(axis=1) == j):.0%} des tickets")
Non-negative Matrix Factorization, factorisation en matrices non négatives. La méthode a été popularisée en 1999 par Lee et Seung dans la revue Nature, avec l'exemple de visages décomposés en parties : yeux, nez, bouche.
Les deux donnent souvent des thèmes proches. La NMF est plus rapide et fonctionne bien avec une pondération TF-IDF. LDA est un modèle probabiliste, plus adapté aux comptages bruts. On teste souvent les deux et on garde les thèmes les plus lisibles.
Parce que les parties ne peuvent que s'additionner. Un panier est une somme de missions, jamais une mission moins une autre. Dans une SVD ou une ACP, un axe mélange des poids positifs et négatifs qui se compensent, ce qui le rend difficile à nommer.
Modélise chaque document comme un mélange de thèmes avec un modèle génératif. Plus lent, mieux fondé statistiquement sur du texte.
Voir la fiche → la version sans contrainteAxes optimaux au sens des moindres carrés, mais avec des poids négatifs difficiles à lire comme des thèmes.
Voir la fiche → l'approche par règlesCherche des règles si / alors entre produits, y compris rares. Complémentaire des missions de la NMF.
Voir la fiche →Dataistudio forme les équipes au machine learning et à l'IA, sur des cas concrets.
Nous utilisons des cookies de mesure d'audience et de suivi publicitaire pour comprendre la fréquentation du site et l'efficacité de nos annonces. Rien n'est déposé sans votre accord. En savoir plus