Zum Hauptinhalt springen
Lektion 17 von 22 · Embeddings, RAG und semantische Suche

Textklassifizierung mit Embeddings

Lektion 17 von 22 Aktualisiert am 29. Juli 2026 Entwickler
Auf dieser Seite
  1. Lernziele
  2. Zero-shot-Klassifizierung
  3. Few-shot-Klassifizierung
  4. Leichter überwachter Klassifikator
  5. Vergleich der Ansätze
  6. Zusammenfassung

Lernziele

  • Textklassifizierung ohne Modelltraining durchführen
  • Zero-shot- und Few-shot-Klassifizierung mit Embeddings implementieren
  • Die Leistung im Vergleich zu einem leichten überwachten Klassifikator bewerten

Zero-shot-Klassifizierung

Die semantische Suche findet Dokumente, die einer Abfrage unter Tausenden von Kandidaten am ähnlichsten sind. Die Klassifizierung kehrt dieses Setup um, ohne die zugrundeliegenden Mechaniken zu ändern: Ihre „Dokumente” sind nun Ihre Kategorien (fünf oder zehn Stück), und die „Abfrage” ist der Text, den Sie klassifizieren möchten. Die Kategorie, deren Embedding am nächsten liegt, wird als Vorhersage ausgewählt. Es wird kein Training durchgeführt, keine annotierten Daten benötigt, lediglich ein einziger API-Aufruf.

Zwei Implementierungsdetails im untenstehenden Code verdienen Aufmerksamkeit. Erstens gehen Text und Kategoriennamen in einen einzigen embeddings.create-Aufruf ein, was die Latenz um die Anzahl der Labels reduziert. Zweitens wandelt die Anwendung von Softmax auf die Ähnlichkeitswerte rohe Werte (die typischerweise zwischen 0,3 und 0,5 liegen) in Wahrscheinlichkeiten um, die sich zu 1 summieren. Diese Wahrscheinlichkeiten können einem Benutzer angezeigt oder einem Schwellenwert für das Vertrauen gegenübergestellt werden.

from openai import OpenAI
import numpy as np

client = OpenAI()

def classifier_zero_shot(
    texte: str,
    categories: list[str],
    model: str = "text-embedding-3-large"
) -> dict:
    """Klassifiziert Text in Kategorien ohne Training."""
    # Embeddings für Kategorien
    all_inputs = [texte] + categories
    response = client.embeddings.create(
        input=all_inputs,
        model=model
    )
    embs = [d.embedding for d in sorted(response.data, key=lambda x: x.index)]
    texte_emb = np.array(embs[0])
    cat_embs = np.array(embs[1:])

    # Ähnlichkeit mit jeder Kategorie
    scores = cat_embs @ texte_emb

    # Softmax zur Ermittlung von Wahrscheinlichkeiten
    exp_scores = np.exp(scores - np.max(scores))
    probas = exp_scores / exp_scores.sum()

    resultats = sorted(
        zip(categories, probas),
        key=lambda x: x[1], reverse=True
    )

    return {
        "prediction": resultats[0][0],
        "confiance": float(resultats[0][1]),
        "scores": {cat: float(p) for cat, p in resultats}
    }

# Nutzung
texte = "Le cours de l'action a chuté de 15% après l'annonce"
categories = ["Finance", "Sport", "Technologie", "Politique", "Santé"]

resultat = classifier_zero_shot(texte, categories)
print(f"Prédiction : {resultat['prediction']} "
      f"({resultat['confiance']:.1%})")
print("Scores :", resultat["scores"])

In diesem Beispiel landet der Satz „der Aktienkurs ist nach der Ankündigung um 15 % gefallen” erwartungsgemäß in der Kategorie Finance. Der Ansatz stößt an seine Grenzen, wenn die Kategorien interne Labels sind, deren Namen keinen Hinweis auf ihren Inhalt geben. Ein Label wie „P1” oder „Level-2-Eskalation” trägt keine ausnutzbare semantische Bedeutung, wodurch die Klassifizierung im Wesentlichen zufällig wird.

Few-shot-Klassifizierung

Genau das löst die Few-shot-Klassifizierung. Statt eine Kategorie durch ihren Namen darzustellen, wird sie durch den Schwerpunkt (Centroid) weniger echter Beispiele repräsentiert. Die Kategorie „Bug” ist nicht mehr das Wort „Bug”, sondern der Durchschnitt der Embeddings für „die App stürzt beim Start ab”, „Fehler 500 auf der Anmeldeseite” und „der Button funktioniert auf dem Mobilgerät nicht”. Drei bis fünf Beispiele pro Klasse reichen in der Regel aus, und die Kosten bleiben denen eines einzelnen API-Aufrufs gleich, da alle Texte gleichzeitig gesendet werden.

def classifier_few_shot(
    texte: str,
    exemples: dict[str, list[str]],
    model: str = "text-embedding-3-large"
) -> dict:
    """Klassifiziert nach Ähnlichkeit zu markierten Beispielen.

    Args:
        exemples: {"category": ["example1", "example2", ...]}
    """
    # Alle Texte vorbereiten
    tous_textes = [texte]
    mapping = []
    for cat, txts in exemples.items():
        for t in txts:
            tous_textes.append(t)
            mapping.append(cat)

    # Embeddings in einem einzigen Aufruf
    response = client.embeddings.create(
        input=tous_textes, model=model
    )
    embs = [d.embedding for d in sorted(response.data, key=lambda x: x.index)]
    texte_emb = np.array(embs[0])

    # Durchschnittliche Embeddings pro Kategorie
    cat_embeddings = {}
    for i, cat in enumerate(mapping):
        emb = np.array(embs[i + 1])
        if cat not in cat_embeddings:
            cat_embeddings[cat] = []
        cat_embeddings[cat].append(emb)

    cat_moyennes = {
        cat: np.mean(embs_list, axis=0)
        for cat, embs_list in cat_embeddings.items()
    }

    # Ähnlichkeit
    scores = {
        cat: float(np.dot(texte_emb, emb_moy) /
                    (np.linalg.norm(texte_emb) * np.linalg.norm(emb_moy)))
        for cat, emb_moy in cat_moyennes.items()
    }

    prediction = max(scores, key=scores.get)
    return {"prediction": prediction, "scores": scores}

# Nutzung
exemples = {
    "bug": [
        "L'application crash au démarrage",
        "Erreur 500 sur la page de connexion",
        "Le bouton ne fonctionne pas sur mobile",
    ],
    "feature": [
        "Pourriez-vous ajouter un mode sombre ?",
        "Il serait utile d'exporter en PDF",
        "Suggestion : intégrer un calendrier",
    ],
    "question": [
        "Comment changer mon mot de passe ?",
        "Où trouver les paramètres de notification ?",
        "Quelle est la limite de stockage ?",
    ],
}

texte = "L'export CSV ne contient pas toutes les colonnes"
resultat = classifier_few_shot(texte, exemples)
print(f"Catégorie : {resultat['prediction']}")

Der Test-Ticket „die CSV-Exportfunktion enthält nicht alle Spalten” enthält weder das Wort „Bug” noch „Fehler”, ähnelt aber den drei bereitgestellten Funktionsstörungsbeispielen, weshalb er dort landet. Beachten Sie, dass die Mittelung von Embeddings eine homogene Kategorie voraussetzt. Wenn Ihre „Frage”-Beispiele Mischungen aus Abrechnungsanfragen und API-Fragen enthalten, wird sich ihr Schwerpunkt in einem semantischen Niemandsland befinden. In diesem Fall ist es besser, die Kategorie aufzuteilen.

Leichter überwachter Klassifikator

Wenn Sie pro Klasse mehrere Dutzend Beispiele haben, wird das Mittelung eine Verschwendung von Informationen: Sie glätten die Form der Punktwolke. Ein linearer Klassifikator, der auf denselben Embeddings trainiert wird, lernt eine Entscheidungsgrenze. Das Embedding übernimmt das Sprachverständnis, während LogisticRegression einfach eine geometrische Linie in diesem Raum zeichnet. Dies führt zu Trainingszeiten von wenigen Sekunden auf einer Standard-CPU, wohingegen das Feinabstimmen eines Sprachmodells eine GPU erfordern würde.

Die fünfteilige Kreuzvalidierung dient nicht nur der Show. Sie liefert die Standardabweichung der Genauigkeit und gibt einen Hinweis darauf, wie zuverlässig Ihre Schätzung ist. Ein Wert von 0,91 ± 0,02 ist nutzbar; 0,91 ± 0,15 signalisiert einen Datensatz, der zu klein oder unausgewogen ist.

from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
import numpy as np

def entrainer_classifieur(
    textes: list[str],
    labels: list[str],
    model: str = "text-embedding-3-large"
) -> LogisticRegression:
    """Trainiert einen Klassifikator auf Embeddings."""
    # Generiere Embeddings
    response = client.embeddings.create(
        input=textes, model=model
    )
    X = np.array([d.embedding for d in sorted(response.data, key=lambda x: x.index)])

    # Trainiere einen Klassifikator
    clf = LogisticRegression(max_iter=1000)
    
    # Kreuzvalidierung
    scores = cross_val_score(clf, X, labels, cv=5)
    print(f"Accuracy (5-fold CV) : {scores.mean():.3f} ± {scores.std():.3f}")

    # Trainiere auf dem vollständigen Datensatz
    clf.fit(X, labels)
    return clf

def predire(clf, texte: str, model: str = "text-embedding-3-large") -> str:
    """Vorhersage der Kategorie eines Textes."""
    emb = client.embeddings.create(
        input=texte, model=model
    ).data[0].embedding
    return clf.predict([emb])[0]

Vergleich der Ansätze

AnsatzBenötigte BeispieleGenauigkeitLatenz
Zero-shot0~70-80 %1 API-Aufruf
Few-shot (3-5/Klasse)15-25~80-90 %1 API-Aufruf
Überwacht (50+/Klasse)250+~90-95 %1 API-Aufruf + Inferenz

In dieser Reihenfolge gelesen, beschreiben diese drei Zeilen einen Fortschritt, den Sie während des gesamten Produktlebenszyklus verfolgen können. Beginnen Sie am ersten Tag mit Zero-shot, ohne jegliche Daten. Sobald Sie manuell etwa zwanzig Vorhersagen korrigiert haben, recyclen Sie diese als Few-shot-Beispiele. Wenn das Annotierungsvolumen pro Klasse fünfzig überschreitet, trainieren Sie den überwachenden Klassifikator. Denken Sie daran, dass die Zeile „Latenz” daran erinnert, dass selbst das überwachte Modell schnell bleibt; die lokale Inferenz fügt im Vergleich zum API-Aufruf für Embeddings eine vernachlässigbare Overhead hinzu.

Zusammenfassung

  • Zero-shot-Klassifizierung vergleicht Text mit Labels über Embeddings
  • Few-shot verwendet Beispiele, um jede Kategorie zu definieren
  • Ein überwachter Klassifikator (LogisticRegression) erzielt die beste Leistung
  • Beginnen Sie mit Zero-shot und fügen Sie Beispiele hinzu, wenn die Genauigkeit unzureichend ist