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