Leçon 6 — Construire le moteur

Regrouper automatiquement : un clustering simple

5 min de lecture

Regrouper automatiquement : un clustering simple

L'idée du k-means

Le clustering (partitionnement) range des éléments en groupes sans étiquettes préalables. Le k-means est l'algorithme le plus simple :

flowchart TB
  A["Choisir k fiches au hasard comme centres"] --> B["Affecter chaque fiche au centre le plus proche (cosinus)"]
  B --> C["Recalculer chaque centre = moyenne de son groupe, renormalisée"]
  C --> D{"Les centres bougent encore ?"}
  D -- "oui" --> B
  D -- "non" --> E["Groupes finaux"]

Avec des embeddings, on mesure la proximité au cosinus et on renormalise les centres : c'est un k-means sphérique. Le résultat dépend du tirage initial ; on relance donc plusieurs fois et on garde la partition la plus compacte (la plus grande somme des cosinus au centre).

files/moteur.py (extrait)
def kmeans(vecteurs, k, iterations=50, graine=0, essais=5):
    rng = np.random.default_rng(graine)
    meilleur, meilleure_cohesion = None, -np.inf
    for _ in range(essais):
        centres = vecteurs[rng.choice(len(vecteurs), k, replace=False)]
        for _ in range(iterations):
            etiquettes = np.argmax(vecteurs @ centres.T, axis=1)
            nouveaux = normaliser(np.stack([vecteurs[etiquettes == c].mean(0) if np.any(etiquettes == c)
                                            else centres[c] for c in range(k)]))
            if np.allclose(nouveaux, centres):
                break
            centres = nouveaux
        cohesion = float(np.sum(vecteurs * centres[etiquettes]))  # somme des cosinus au centre
        if cohesion > meilleure_cohesion:
            meilleur, meilleure_cohesion = etiquettes, cohesion
    return meilleur

Mesurer : la pureté

Comme on connaît les vraies catégories, on peut noter le résultat. La pureté : pour chaque groupe, on compte les fiches de sa catégorie majoritaire, on additionne, on divise par le total. 1,0 signifie que chaque groupe ne contient qu'une catégorie.

files/moteur.py (extrait)
def purete(etiquettes, categories) -> float:
    total = 0
    for c in set(etiquettes.tolist()):
        membres = [categories[i] for i in np.where(etiquettes == c)[0]]
        total += max(membres.count(x) for x in set(membres))
    return total / len(categories)

Le préfixe « clustering » change-t-il quelque chose ?

gemini-embedding-2 propose une tâche symétrique task: clustering | query: .... La commande clusters calcule ces vecteurs (3 appels pour 60 fiches) et compare la pureté avec celle des vecteurs de recherche déjà indexés.

VecteursFormatagePureté mesurée (k = 10)
de recherche (index)title: ... | text: ...0,78
« clustering »task: clustering | query: ...0,77

Sur ce petit catalogue, aucune différence significative. Lors d'un essai précédent, avec un seul tirage par graine, l'écart allait de 0,01 à 0,07 en faveur du préfixe « clustering » selon la graine : la variabilité du k-means pesait plus que le préfixe. Leçon de méthode : mesurer, et relancer plusieurs fois avant de conclure.

Pourquoi relancer le k-means plusieurs fois ?

Une pureté de 0,78 sur 60 fiches signifie :

Exercice : nommer les groupes automatiquement

Proposez une méthode pour donner un nom lisible à chaque groupe, avec ou sans LLM.

Voir la solution

Sans LLM : prendre le nom de la fiche la plus proche du centre du groupe (le « représentant ») :

for c in range(k):
    membres = np.where(etiquettes == c)[0]
    centre = moteur.normaliser(vecteurs[membres].mean(0))
    representant = membres[np.argmax(vecteurs[membres] @ centre)]
    print(c, produits[representant]["nom"])

Avec un LLM : envoyer les noms des membres à Gemini avec la consigne « Donne un nom de rayon de 2 à 4 mots pour ces produits ». Un appel par groupe, un résultat plus naturel, mais à relire.

Résultat obtenu

python moteur.py clusters 10 exécuté le 2026-10-15 (extrait) :

Pureté avec les vecteurs de recherche : 0.78
Pureté avec les vecteurs « clustering » : 0.77
Groupe 0: Valise cabine Escale 55, Sac à dos urbain Navette, Sac de voyage Weekend, Cubes de rangement Ordre, Pèse-bagage Envol, Sac à dos randonnée Crête 45
Groupe 2: Poussette Citadine Plume, Babyphone Vidéo Veille, Porte-bébé Kangou, Chaise haute Évolutive, Tapis d'éveil Jungle, Gigoteuse Douillette TOG 2.5
Groupe 3: Tondeuse sans fil Herbo 40, Tuyau extensible Flexi 30 m, Composteur Humus 400 L, Sécateur Coupe-Net, Récupérateur d'eau 300 L, Hôtel à insectes Ruche
Groupe 5: Asphalte Flow, Randonnée Cimes Mid, Sandale Rivière, Tente Dôme 3 places, Sac de couchage Grand Froid, Réchaud gaz Mini-Flamme, Lampe frontale Lumen 400, Matelas autogonflant Repos, Tente Dôme 3 personnes
Groupe 7: Lampe de bureau Halo, Support écran double Duo, Tapis de souris XXL Plaine, Micro USB Podcast, Webcam Full HD Visio, Support PC portable Alu, Ampoule connectée Teinte
Groupe 9: Indoor Court, Pantoufle Nuage

Bagagerie, bébé et jardin sont retrouvés parfaitement. Le groupe 5 mélange chaussures de marche et camping : c'est un rayon « plein air » cohérent, que nos catégories ne nomment pas.

Je sais faire

  • Expliquer les étapes du k-means
  • Adapter le k-means aux embeddings (cosinus, centres renormalisés)
  • Mesurer une partition avec la pureté quand des étiquettes existent
  • Interpréter un groupe inattendu comme une information plutôt qu'une erreur
Suivant