Aller au contenu principal

Clustering : regrouper automatiquement

Objectifs

  • Regrouper des textes par thème sans labels prédéfinis
  • Utiliser K-Means et HDBSCAN sur des embeddings
  • Nommer automatiquement les clusters

Pourquoi le clustering ?

Contrairement à la classification, le clustering ne nécessite pas de catégories prédéfinies. Il découvre automatiquement les groupes thématiques dans vos données :

  • Analyser des milliers de feedbacks clients
  • Identifier les sujets récurrents dans des tickets de support
  • Explorer un corpus de documents inconnu

K-Means sur des embeddings

from openai import OpenAI
from sklearn.cluster import KMeans
import numpy as np

client = OpenAI()

# Corpus d'exemple
textes = [
    "L'application crash au démarrage sur Android",
    "Bug : impossible de se connecter depuis hier",
    "Le bouton de validation ne fonctionne pas",
    "Pourriez-vous ajouter un export Excel ?",
    "Suggestion : un mode hors-ligne serait utile",
    "Feature request : intégrer Slack",
    "Comment changer mon adresse email ?",
    "Où trouver ma facture de janvier ?",
    "Quel est le prix de l'abonnement annuel ?",
    "Votre produit est excellent, bravo !",
    "Merci pour le support réactif",
    "Je recommande vivement cette application",
]

# Générer les embeddings
response = client.embeddings.create(
    input=textes,
    model="text-embedding-3-large"
)
embeddings = np.array(
    [d.embedding for d in sorted(response.data, key=lambda x: x.index)]
)

# Clustering K-Means
n_clusters = 4
kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
labels = kmeans.fit_predict(embeddings)

# Afficher les clusters
for cluster_id in range(n_clusters):
    print(f"\n--- Cluster {cluster_id} ---")
    indices = [i for i, l in enumerate(labels) if l == cluster_id]
    for idx in indices:
        print(f"  {textes[idx]}")

Trouver le bon nombre de clusters

Méthode du coude (Elbow)

from sklearn.metrics import silhouette_score
import matplotlib.pyplot as plt

inertias = []
silhouettes = []
K_range = range(2, 10)

for k in K_range:
    km = KMeans(n_clusters=k, random_state=42, n_init=10)
    km.fit(embeddings)
    inertias.append(km.inertia_)
    silhouettes.append(silhouette_score(embeddings, km.labels_))

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))

ax1.plot(K_range, inertias, "bo-")
ax1.set_xlabel("Nombre de clusters")
ax1.set_ylabel("Inertie")
ax1.set_title("Méthode du coude")

ax2.plot(K_range, silhouettes, "ro-")
ax2.set_xlabel("Nombre de clusters")
ax2.set_ylabel("Score silhouette")
ax2.set_title("Score silhouette")

plt.tight_layout()
plt.savefig("choix_k.png", dpi=150)
plt.show()

meilleur_k = K_range[np.argmax(silhouettes)]
print(f"Meilleur k selon silhouette : {meilleur_k}")

HDBSCAN : clustering sans K

HDBSCAN détecte automatiquement le nombre de clusters et identifie les outliers :

import hdbscan

clusterer = hdbscan.HDBSCAN(
    min_cluster_size=3,
    min_samples=2,
    metric="euclidean"
)
labels_hdbscan = clusterer.fit_predict(embeddings)

n_clusters = len(set(labels_hdbscan)) - (1 if -1 in labels_hdbscan else 0)
n_outliers = list(labels_hdbscan).count(-1)

print(f"Clusters trouvés : {n_clusters}")
print(f"Outliers : {n_outliers}")

for cluster_id in range(n_clusters):
    print(f"\n--- Cluster {cluster_id} ---")
    indices = [i for i, l in enumerate(labels_hdbscan) if l == cluster_id]
    for idx in indices:
        print(f"  {textes[idx]}")

if n_outliers > 0:
    print("\n--- Outliers ---")
    for i, l in enumerate(labels_hdbscan):
        if l == -1:
            print(f"  {textes[i]}")

Nommer les clusters automatiquement

Utilisez un LLM pour donner un nom significatif à chaque cluster :

def nommer_clusters(
    textes: list[str],
    labels: list[int]
) -> dict[int, str]:
    """Nomme chaque cluster avec un LLM."""
    noms = {}
    clusters = set(l for l in labels if l >= 0)

    for cluster_id in clusters:
        exemples = [
            textes[i] for i, l in enumerate(labels)
            if l == cluster_id
        ][:10]  # Max 10 exemples

        response = client.chat.completions.create(
            model="gpt-5.3",
            messages=[{
                "role": "user",
                "content": (
                    f"Voici des textes regroupés dans un même cluster :\n\n"
                    + "\n".join(f"- {e}" for e in exemples) +
                    f"\n\nDonne un nom court (2-4 mots) qui décrit "
                    f"le thème commun de ce groupe. "
                    f"Retourne uniquement le nom."
                )
            }],
            temperature=0,
            max_tokens=20
        )
        noms[cluster_id] = response.choices[0].message.content.strip()

    return noms

# Utilisation
noms = nommer_clusters(textes, labels)
for cluster_id, nom in noms.items():
    print(f"Cluster {cluster_id} : {nom}")
    indices = [i for i, l in enumerate(labels) if l == cluster_id]
    for idx in indices:
        print(f"  - {textes[idx]}")

Pipeline de clustering complet

class PipelineClustering:
    def __init__(self, model: str = "text-embedding-3-large"):
        self.client = OpenAI()
        self.model = model

    def analyser(self, textes: list[str], methode: str = "auto") -> dict:
        """Pipeline complet : embedding → clustering → naming."""
        # 1. Embeddings
        response = self.client.embeddings.create(
            input=textes, model=self.model
        )
        embs = np.array([
            d.embedding for d in sorted(response.data, key=lambda x: x.index)
        ])

        # 2. Clustering
        if methode == "auto":
            clusterer = hdbscan.HDBSCAN(min_cluster_size=3)
            labels = clusterer.fit_predict(embs)
        else:
            km = KMeans(n_clusters=int(methode), random_state=42)
            labels = km.fit_predict(embs)

        # 3. Nommer les clusters
        noms = nommer_clusters(textes, labels)

        return {
            "labels": labels.tolist(),
            "noms": noms,
            "n_clusters": len(set(l for l in labels if l >= 0)),
            "n_outliers": list(labels).count(-1)
        }

pipeline = PipelineClustering()
resultats = pipeline.analyser(textes)

Résumé

  • K-Means quand vous connaissez le nombre de clusters
  • HDBSCAN pour détecter automatiquement les groupes et les outliers
  • Le score silhouette aide à choisir le bon K
  • Un LLM nomme automatiquement les clusters découverts
  • Les embeddings rendent le clustering sémantique, pas juste lexical