Clustering: Automatische Gruppierung
Auf dieser Seite
Lernziele
- Texte thematisch gruppieren, ohne vordefinierte Labels
- Einsatz von K-Means und HDBSCAN auf Embeddings
- Automatische Benennung von Clustern
Warum Clustering?
Die Lektion zur Klassifizierung ging davon aus, dass Sie bereits wussten, welche Kategorien existieren. Clustering adressiert die entgegengesetzte Situation, die beim Erkunden eines Korpus ebenfalls am häufigsten vorkommt: Niemand weiß, was es enthält. Sie beziehen achttausend Verbatim aus einer Zufriedenheitsumfrage oder sechs Monate Support-Tickets, und die erste Frage lautet nicht „wie viele Bugs”, sondern „worum sprechen die Leute eigentlich”. Clustering entdeckt thematische Gruppen, ohne dass man ihm etwas Vorgegebenes mitteilt.
Drei Anwendungsfälle tauchen ständig auf: die Analyse von Tausenden Kundenfeedback-Einträgen zur Extraktion dominanter Muster, die Identifikation wiederkehrender Themen in Support-Tickets zur Besetzung einer Wissensdatenbank und die Erkundung eines unbekannten Dokumentenkörpers, bevor entschieden wird, wie dieser indiziert werden soll.
K-Means auf Embeddings
K-Means ist der Standardalgorithmus: Sie geben ihm eine Anzahl von Gruppen vor, er platziert diese Anzahl an Zentren und verschiebt iterativ jedes Zentrum in Richtung des Durchschnitts der Punkte, die es anzieht. Angewendet auf Embeddings statt auf eine Wortmenge gruppiert er nach Bedeutung: „Die App stürzt beim Start ab” und „Seit gestern kann ich mich nicht anmelden” landen zusammen, obwohl sie keine gemeinsamen Wörter teilen.
Der nachfolgende Beispielkorpus enthält absichtlich vier Familien – Bugs, Feature-Anfragen, Fragen und Komplimente –, sodass Sie die resultierende Gruppierung visuell überprüfen können. Die Einstellung random_state=42 garantiert, dass Sie von einem Lauf zum nächsten dieselben Cluster erhalten, was essenziell ist, wenn Sie zwei Konfigurationen vergleichen müssen.
from openai import OpenAI
from sklearn.cluster import KMeans
import numpy as np
client = OpenAI()
# Beispielkorpus
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",
]
# Embeddings generieren
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)]
)
# K-Means Clustering
n_clusters = 4
kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
labels = kmeans.fit_predict(embeddings)
# Cluster anzeigen
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]}")
Die richtige Anzahl an Clustern finden
Bei zwölf vorbereiteten Sätzen war die Wahl von vier offensichtlich. Bei achttausend Verbatim weiß niemand, welcher Wert richtig ist. Zwei Metriken helfen Ihnen bei der Schätzung. Die Trägheit (Inertia), also die verbleibende Streuung innerhalb der Gruppen, nimmt mechanisch ab, wenn Sie Cluster hinzufügen: Sie suchen nach dem „Ellenbogen”, dem Punkt, ab dem sie nicht mehr signifikant sinkt. Der Silhouette-Score ist entscheidender, da er etwas anderes misst: wie viel näher jeder Punkt an seiner eigenen Gruppe liegt als an der nächstgelegenen Nachbargruppe. Im Gegensatz zur Trägheit hat er ein Maximum, und dieses Maximum weist direkt auf einen Kandidatenwert hin.
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("Number of clusters")
ax1.set_ylabel("Inertia")
ax1.set_title("Elbow method")
ax2.plot(K_range, silhouettes, "ro-")
ax2.set_xlabel("Number of clusters")
ax2.set_ylabel("Silhouette score")
ax2.set_title("Silhouette score")
plt.tight_layout()
plt.savefig("choix_k.png", dpi=150)
plt.show()
meilleur_k = K_range[np.argmax(silhouettes)]
print(f"Best k according to silhouette: {meilleur_k}")
HDBSCAN: Clustering ohne K
K-Means leidet unter einem strukturellen Mangel bei realen Daten: Er muss jeden Punkt einem Cluster zuweisen. Ein isoliertes Verbatim, das nichts anderem ähnelt, wird zwangsweise der am wenigsten entfernten Gruppe angehängt, wo es die Interpretation verschmutzt. HDBSCAN denkt in Dichten und akzeptiert die Verneinung: Er bestimmt die Anzahl der Gruppen selbst und platziert isolierte Punkte in einer separaten Kategorie, die mit -1 beschriftet ist.
Diese Ausreißer sind kein Müll. In einem Ticket-Korpus sind es oft seltene Vorfälle, Anfragen außerhalb des Geltungsbereichs oder schwache Signale – genau das, was einer menschlichen Prüfung bedarf. Der nachfolgende Code zeigt sie separat an, statt sie zu verwerfen.
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 found: {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]}")
Automatische Benennung von Clustern
Ein Clustering-Algorithmus liefert Ihnen Zahlen. „Cluster 2” gehört nicht in einen Bericht oder ein Dashboard. Der Schritt, der das Ergebnis nutzbar macht, besteht darin, ein Dutzend repräsentativer Mitglieder dem LLM vorzulegen und es nach einem kurzen Namen zu fragen: „Verbindungsprobleme”, „Integrationsanfragen”, „Rechnungsfragen”. Die Begrenzung auf zehn Beispiele kontrolliert die Kosten, die Einstellung der Temperatur auf Null stellt sicher, dass ein Cluster immer denselben Namen erhält, und der Loop ignoriert das Label -1, da Ausreißer per Definition kein gemeinsames Thema haben.
def nommer_clusters(
textes: list[str],
labels: list[int]
) -> dict[int, str]:
"""Names each cluster using an 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 examples
response = client.chat.completions.create(
model="gpt-5.6-terra",
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
# Usage
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]}")
Komplette Clustering-Pipeline
Die drei Schritte – Embedding, Clustering, Benennung – kommen in einer einzigen Klasse natürlich zusammen. Der Parameter methode akzeptiert "auto", was die Wahl der Gruppenanzahl an HDBSCAN delegiert, oder einen numerischen Wert, der als String übergeben wird, wenn Sie einem K-Means ein K auferlegen möchten. Das zurückgegebene Wörterbuch enthält die Labels, lesbare Namen und zwei Zähler, die sofort anzeigen, ob das Ergebnis nutzbar ist: Ein n_outliers, das nahe am Gesamtwert liegt, bedeutet, dass Ihr Korpus keine dichte Struktur aufweist, und Sie sollten zu K-Means zurückkehren.
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:
"""Complete pipeline: 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. Name 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)
Zusammenfassung
- Verwenden Sie K-Means, wenn Sie die Anzahl der Cluster kennen
- Verwenden Sie HDBSCAN, um Gruppen und Ausreißer automatisch zu erkennen
- Der Silhouette-Score hilft bei der Wahl des richtigen K
- Ein LLM benennt entdeckte Cluster automatisch
- Embeddings machen Clustering semantisch, nicht nur lexikalisch