Überspringen und zum Inhalt gehen →

30-Tage-DSPy-Challenge: Tag 8 & 9 – Daten zur Optimierung

Heute mache ich mal Tag 8 und 9 zusammen, da es mir doch etwas wenig erscheint, einfach nur ein Datensatz zu erstellen und ihn nicht zu benutzen. Dafür habe ich dann morgen frei 🙂 – Los geht’s.

Anstatt Prompts manuell zu verfeinern, nutzt DSPy sogenannte „Teleprompter“, um diese Aufgabe automatisiert durchzuführen. Die Grundlage für jede dieser Optimierungen ist ein Datensatz, der aus Beispielen besteht mit denen die Prompts optimiert werden.

Die Rolle von Beispieldaten (dspy.Example)

In DSPy ist ein Datenpunkt kein einfacher Text-String oder ein Dictionary, sondern ein strukturiertes Objekt vom Typ dspy.Example. Ein dspy.Example kapselt alle zu einem Datenpunkt gehörenden Informationen, wie Eingabefelder (z. B. eine Frage) und Ausgabefelder (z. B. die erwartete Antwort). Diese strukturierten Beispiele erfüllen zwei wesentliche Funktionen im Optimierungsprozess:

Grundlage für die Optimierung
Teleprompter wie BootstrapFewShot verwenden einen Trainingsdatensatz, um „Few-shot“-Demonstrationen zu generieren. Dabei werden Beispiele aus dem Datensatz ausgewählt und in den Kontext des Prompts eingefügt, um dem Sprachmodell (LLM) durch konkrete Beispiele zu zeigen, wie es die gestellte Aufgabe lösen soll. Die Qualität dieser Demonstrationen beeinflusst direkt die Leistungsfähigkeit des optimierten Programms.

Basis für die Evaluation
Ein separater Evaluationsdatensatz wird verwendet, um die Leistung des Programms objektiv zu messen. Eine definierte Metrik vergleicht die vom Programm generierte Ausgabe mit der erwarteten Ausgabe (dem „Label“) aus dem dspy.Example und berechnet daraus einen Score. Dieser Prozess ermöglicht einen datengesteuerten Vergleich verschiedener Programmversionen.

    Die Trennung der Daten in ein Trainings- (trainset) und ein Evaluationsset (devset) ist entscheidend, um Overfitting zu vermeiden und sicherzustellen, dass die gemessene Leistung des Programms auf neuen, ungesehenen Daten generalisierbar ist.

    Erstellung eines Datensatzes für Sentiment-Analyse

    Die folgende Umsetzung zeigt, wie ein kleiner Datensatz für eine Sentiment-Klassifizierungsaufgabe erstellt wird. Ziel ist es, Texte in die Kategorien „positiv“, „negativ“ oder „neutral“ einzuordnen.

    Zuerst werden die notwendigen Bibliotheken importiert und die Rohdaten definiert.

    import dspy
    import os
    
    # Definition der Rohdaten für die Sentiment-Analyse
    raw_data = [
        ("Die Bildqualität ist hervorragend und die Bedienung intuitiv.", "positiv"),
        ("Ich bin sehr zufrieden mit dem Produkt, es übertrifft meine Erwartungen.", "positiv"),
        ("Das Preis-Leistungs-Verhältnis ist unschlagbar.", "positiv"),
        ("Leider hat das Gerät nach kurzer Zeit den Geist aufgegeben.", "negativ"),
        ("Der Kundenservice war überhaupt nicht hilfreich und unfreundlich.", "negativ"),
        ("Die Akkulaufzeit ist enttäuschend kurz.", "negativ"),
        ("Das Produkt wurde pünktlich geliefert.", "neutral"),
        ("Die Verpackung war angemessen.", "neutral"),
        ("Die Farbe des Produkts entspricht der Abbildung online.", "neutral"),
        ("Ein wirklich tolles Erlebnis von Anfang bis Ende!", "positiv"),
        ("Ich würde dieses Produkt niemandem empfehlen.", "negativ"),
        ("Die Anleitung ist schwer zu verstehen.", "negativ"),
    ]

    Die Rohdaten werden in eine Liste von dspy.Example-Objekten umgewandelt. Die Methode .with_inputs('text') kennzeichnet das Feld text explizit als Eingabe für das Modell.

    # Umwandlung der Rohdaten in dspy.Example-Objekte
    dspy_examples = [
        dspy.Example(text=text, sentiment=sentiment).with_inputs("text")
        for text, sentiment in raw_data
    ]
    
    # Aufteilung des Datensatzes in Trainings- und Evaluationsset (80/20-Split)
    split_index = int(len(dspy_examples) * 0.8)
    trainset = dspy_examples[:split_index]
    devset = dspy_examples[split_index:]
    
    print(f"Anzahl der Beispiele im Trainingsset: {len(trainset)}")
    print(f"Anzahl der Beispiele im Evaluationsset: {len(devset)}")

    Definition des DSPy-Programms und der Evaluation

    Nachdem die Daten vorbereitet sind, werden die Komponenten des DSPy-Programms definiert: die Signatur, das Modul und die Evaluationsmetrik.

    Eine Signatur (dspy.Signature) definiert die Aufgabe, die das LLM ausführen soll. Das Modul (dspy.Module) strukturiert das Programm. Hier wird ein einfaches dspy.Predict-Modul verwendet.

    # Signatur für die Sentiment-Klassifizierung
    class SentimentSignature(dspy.Signature):
        """Klassifiziert den Sentiment eines gegebenen Textes als positiv, negativ oder neutral."""
        text = dspy.InputField(desc="Der zu klassifizierende Text.")
        sentiment = dspy.OutputField(desc="Das Ergebnis der Klassifizierung: positiv, negativ oder neutral.")
    
    # Modul, das die Klassifizierung durchführt
    class SimpleClassifier(dspy.Module):
        def __init__(self):
            super().__init__()
            self.predictor = dspy.Predict(SentimentSignature)
    
        def forward(self, text):
            return self.predictor(text=text)

    Für die Ausführung wird ein Sprachmodell benötigt. Hier wird beispielhaft ein Modell von OpenAI konfiguriert. Der API-Schlüssel muss in den Umgebungsvariablen gesetzt sein.

    # Konfiguration des lokalen Sprachmodells
    local_llm = dspy.LM(
        "openai/Qwen3-VL-8B-Instruct-Q4_K_M.gguf", 
        api_base="http://localhost:8080/v1", 
        api_key="no_key_needed",
        temperature=2,
        cache=False
    )
    
    dspy.configure(lm=local_llm)

    Mit den vorbereiteten Daten und dem definierten Programm kann nun der Optimierungsprozess gestartet werden. Eine Metrik ist eine Funktion, die den Erfolg einer Vorhersage bewertet. Hier wird die exakte Übereinstimmung (`exact match`) zwischen der Vorhersage und dem Gold-Label verwendet.

    # Metrik zur Evaluation: Exakte Übereinstimmung
    def validation_metric(gold, pred, trace=None):
        # 'gold' ist das Beispiel aus dem devset, 'pred' ist die Vorhersage des Modells
        return gold.sentiment.lower() == pred.sentiment.lower()

    Der BootstrapFewShot-Optimizer wird instanziiert und mit der compile-Methode auf das Modul angewendet. Der Optimizer verwendet das trainset, um Demonstrationen zu erstellen, und die validation_metric, um die Qualität der generierten Prompts zu bewerten.

    from dspy.teleprompt import BootstrapFewShot
    
    # Konfiguration des Optimizers
    config = dict(max_bootstrapped_demos=2, max_labeled_demos=2)
    
    # Instanziierung des Optimizers
    optimizer = BootstrapFewShot(metric=validation_metric, **config)
    
    # Kompilierung des Modells
    optimized_classifier = optimizer.compile(SimpleClassifier(), trainset=trainset)

    Nach Abschluss des Kompilierungsprozesses liegt ein optimiertes Programm vor. Die Leistung kann nun bewertet und mit der des unoptimierten Programms verglichen werden Die dspy.Evaluate-Klasse wird verwendet, um die Leistung beider Programmversionen (vor und nach der Optimierung) auf dem devset zu messen.

    from dspy.evaluate import Evaluate
    
    # Evaluator instanziieren
    evaluator = Evaluate(devset=devset, num_threads=1, display_progress=True, display_table=5)
    
    # Evaluation des unoptimierten Klassifikators
    unoptimized_classifier = SimpleClassifier()
    evaluator(unoptimized_classifier, metric=validation_metric)
    
    # Evaluation des optimierten Klassifikators
    evaluator(optimized_classifier, metric=validation_metric)

    Ein Blick auf die Historie des Sprachmodells nach einer Vorhersage mit dem optimierten Modell zeigt den finalen Prompt. Dieser enthält nun die vom Optimizer erstellten „Few-shot“-Beispiele.

    # Testvorhersage mit dem optimierten Modell, um den Prompt zu inspizieren
    test_text = "Der Versand hat etwas länger gedauert, aber das Produkt ist gut."
    optimized_classifier(text=test_text)
    
    # Anzeige des letzten generierten Prompts
    local_llm.inspect_history(n=1)
    System message:

    Your input fields are:
    1. `text` (str): Der zu klassifizierende Text.
    Your output fields are:
    1. `sentiment` (str): Das Ergebnis der Klassifizierung: positiv, negativ oder neutral.
    All interactions will be structured in the following way, with the appropriate values filled in.

    [[ ## text ## ]]
    {text}

    [[ ## sentiment ## ]]
    {sentiment}

    [[ ## completed ## ]]
    In adhering to this structure, your objective is:
    Klassifiziert den Sentiment eines gegebenen Textes als positiv, negativ oder neutral.


    User message:

    [[ ## text ## ]]
    Die Bildqualität ist hervorragend und die Bedienung intuitiv.


    Assistant message:

    [[ ## sentiment ## ]]
    positiv

    [[ ## completed ## ]]


    User message:

    [[ ## text ## ]]
    Ich bin sehr zufrieden mit dem Produkt, es übertrifft meine Erwartungen.


    Assistant message:

    [[ ## sentiment ## ]]
    positiv

    [[ ## completed ## ]]


    User message:

    [[ ## text ## ]]
    Der Versand hat etwas länger gedauert, aber das Produkt ist gut.

    Respond with the corresponding output fields, starting with the field `[[ ## sentiment ## ]]`, and then ending with the marker for `[[ ## completed ## ]]`.


    Response:

    [[ ## sentiment ## ]]
    neutral

    [[ ## completed ## ]]

    Analyse des resultierenden Prompts

    Der generierte Prompt ist ein sogenannter „Few-Shot“-Prompt. Er liefert dem Modell nicht nur die Anweisung, was zu tun ist, sondern auch konkrete Beispiele (Demonstrationen), wie die Aufgabe zu lösen ist. Diese Struktur nutzt die Fähigkeit von LLMs zum „In-Context Learning“.

    Systemnachricht (System Message)

    Die Systemnachricht legt die grundlegenden Regeln und das Format der Interaktion fest.

    • Definition der Ein- und Ausgabefelder
      • Your input fields are: 1. text (str)
      • Your output fields are: 1. sentiment (str)
        Diese Sektion definiert das Schema. Es wird explizit gemacht, dass ein text als Eingabe erwartet und ein sentiment als Ausgabe generiert werden soll.
    • Strukturvorgabe
      • [[ ## field ## ]] {value}
      • [[ ## completed ## ]]
        Hier wird ein rigides, maschinenlesbares Format vorgegeben. Jedes Feld wird durch doppelte eckige Klammern und Rauten ([[ ## ... ## ]]) markiert. Dies stellt sicher, dass die Ausgabe des LLMs vom DSPy-Framework zuverlässig geparst werden kann.
    • Aufgabenbeschreibung
      • In adhering to this structure, your objective is: Klassifiziert den Sentiment eines gegebenen Textes als positiv, negativ oder neutral.
        Dies ist die eigentliche Anweisung, die direkt aus der SentimentSignature des DSPy-Programms übernommen wurde. Sie formuliert das Ziel der Aufgabe.
    Few-Shot-Beispiele (Demonstrationen)

    Der Prompt enthält zwei vollständige Beispiele, die vom BootstrapFewShot-Optimizer aus dem trainset ausgewählt wurden. Jedes Beispiel besteht aus einer Nutzer- und einer Assistentennachricht.

    • Beispiel 1
      • Input
        [[ ## text ## ]] Die Bildqualität ist hervorragend und die Bedienung intuitiv.
      • Output
        [[ ## sentiment ## ]] positiv
        Dies ist ein eindeutig positives Beispiel, das dem Modell zeigt, wie ein solcher Text klassifiziert werden soll.
    • Beispiel 2
      • Input
        [[ ## text ## ]] Ich bin sehr zufrieden mit dem Produkt, es übertrifft meine Erwartungen.
      • Output
        [[ ## sentiment ## ]] positiv
        Ein weiteres klares positives Beispiel, das die Anforderung verstärkt.

    Die Funktion dieser Beispiele ist es, dem Modell den gewünschten Lösungsweg zu demonstrieren. Anstatt die abstrakte Anweisung interpretieren zu müssen, lernt das Modell am konkreten Fall.

    Finale Anfrage

    Die letzte Nutzernachricht enthält den eigentlichen Text, der klassifiziert werden soll.

    • Input
      [[ ## text ## ]] Der Versand hat etwas länger gedauert, aber das Produkt ist gut.
    • Anweisung
      Respond with the corresponding output fields...
      Die Anfrage schließt mit einer expliziten Wiederholung der Formatierungsanweisung, um die Wahrscheinlichkeit einer korrekten strukturellen Antwort zu maximieren.

    Analyse der LLM-Ausgabe

    Die Antwort des LLMs lautet:

    [[ ## sentiment ## ]]
    neutral
    
    [[ ## completed ## ]]

    Die Ausgabe hält sich exakt an die im Prompt definierte Struktur. Sie beginnt mit dem sentiment-Feld, enthält den Wert und schließt mit dem [[ ## completed ## ]]-Marker. Diese Konformität ist ein direktes Ergebnis der klaren Anweisungen und Beispiele im Prompt und für die automatisierte Weiterverarbeitung im DSPy-Framework essenziell.

    • Eingabetext
      „Der Versand hat etwas länger gedauert, aber das Produkt ist gut.“
      Dieser Satz enthält eine gemischte Tonalität. Der erste Teil („Versand hat etwas länger gedauert“) ist leicht negativ, während der zweite Teil („das Produkt ist gut“) klar positiv ist.
    • Klassifizierung als „neutral“
      Die Entscheidung des Modells für „neutral“ ist eine plausible und logische Schlussfolgerung. Angesichts der widersprüchlichen Signale im Text hat das Modell eine Abwägung vorgenommen und sich für die mittlere Kategorie entschieden.
    • Einfluss der Few-Shot-Beispiele
      Es ist interessant, dass die bereitgestellten Beispiele (trainset) nur eindeutig positive Fälle enthielten. Das Modell wurde also nicht explizit darauf trainiert, wie es mit gemischtem Sentiment umgehen soll. Dennoch war es in der Lage, aus den vorhandenen Kategorien („positiv“, „negativ“, „neutral“) die passendste für diesen uneindeutigen Fall zu wählen. Dies zeigt eine gewisse Generalisierungs- und Abstraktionsfähigkeit. Hätte der Optimizer ein Beispiel für gemischtes Sentiment in den Prompt aufgenommen (sofern im trainset vorhanden), wäre die Zuverlässigkeit für solche Fälle potenziell noch höher.

    Zusammenfassung

    Die Analyse zeigt, dass der von DSPy und dem BootstrapFewShot-Optimizer generierte Prompt gut strukturiert ist. Er kombiniert eine klare Aufgabenbeschreibung mit konkreten Beispielen, um das LLM präzise anzuleiten. Die resultierende Ausgabe des Modells ist sowohl strukturell korrekt als auch inhaltlich nachvollziehbar. Die Klassifizierung eines gemischten Sentiments als „neutral“ belegt die Fähigkeit des Modells, auch ohne spezifische Beispiele für solche Grenzfälle eine logische Inferenz zu treffen. Dieser Prozess illustriert, wie DSPy die manuelle Prompt-Erstellung durch einen datengesteuerten, automatisierten Ansatz ersetzt, um robuste und zuverlässige Ergebnisse zu erzielen.

    Führt man den unoptimized_classifier aus, erhält man im Prompt keine Beispiele.

    # Testvorhersage mit dem nicht optimierten Modell, um den Prompt zu inspizieren
    test_text = "Der Versand hat etwas länger gedauert, aber das Produkt ist gut."
    unoptimized_classifier(text=test_text)
    
    # Anzeige des letzten generierten Prompts
    local_llm.inspect_history(n=1)

    Das Ergebnis ist zwar das gleiche, aber im Prompt fehlen die Beispiele, so dass bei komplexeren Aufgeben ein schlechteres Ergebnis zu erwarten ist.

    System message:

    Your input fields are:
    1. `text` (str): Der zu klassifizierende Text.
    Your output fields are:
    1. `sentiment` (str): Das Ergebnis der Klassifizierung: positiv, negativ oder neutral.
    All interactions will be structured in the following way, with the appropriate values filled in.

    [[ ## text ## ]]
    {text}

    [[ ## sentiment ## ]]
    {sentiment}

    [[ ## completed ## ]]
    In adhering to this structure, your objective is:
    Klassifiziert den Sentiment eines gegebenen Textes als positiv, negativ oder neutral.


    User message:

    [[ ## text ## ]]
    Der Versand hat etwas länger gedauert, aber das Produkt ist gut.

    Respond with the corresponding output fields, starting with the field `[[ ## sentiment ## ]]`, and then ending with the marker for `[[ ## completed ## ]]`.


    Response:

    [[ ## sentiment ## ]]
    neutral
    [[ ## completed ## ]]

    Zusammenfassung

    • Unoptimiertes Programm (SimpleClassifier)
      Dieses Programm verwendet einen „Zero-shot“-Prompt, der nur die Aufgabenbeschreibung enthält. Seine Leistung hängt stark von der Fähigkeit des Basis-LLMs ab, die Anweisung korrekt zu interpretieren.
    • Optimiertes Programm (optimized_classifier)
      Der BootstrapFewShot-Optimizer hat das Programm analysiert und einen Prompt erstellt, der „Few-shot“-Demonstrationen enthält. Diese Beispiele leiten das LLM an und zeigen ihm durch konkrete Fälle, wie die Aufgabe zu lösen ist. Dies führt zu konsistenteren und genaueren Ergebnissen.

    Dieser Prozess demonstriert den zentralen Vorteil von DSPy: Anstatt manuell mit Prompts zu experimentieren, wird ein Datensatz genutzt, um systematisch und reproduzierbar ein leistungsfähiges Programm zu erstellen. Die dspy.Example-Objekte sind dabei das entscheidende Bindeglied, das sowohl die automatische Optimierung als auch die objektive Messung des Erfolgs ermöglicht. So kann man auch bei einem Wechsel des LLMs schnell wieder Prompts erhalten die für gute Ergebnisse sorgen.

    Veröffentlicht in Allgemein