Überspringen und zum Inhalt gehen →

30-Tage-DSPy-Challenge: Tag 14 – Wochenrückblick und Vertiefung

Die zweite Woche der Challenge konzentrierte sich auf die Prinzipien, die DSPy von traditionellem Prompt Engineering unterscheiden: die systematische Optimierung und Evaluation von Sprachmodell-Programmen.

Wiederholung der Konzepte Optimierung und Evaluation

Die Entwicklung robuster LLM-Anwendungen erfordert einen methodischen Ansatz zur Leistungssteigerung. DSPy formalisiert diesen Prozess durch zwei eng miteinander verbundene Konzepte: Evaluation und Optimierung.

Die Evaluation ist der Prozess der objektiven Messung der Programmleistung. Sie erfordert drei Komponenten. Ein Programm (dspy.Module), das eine definierte Aufgabe ausführt. Eine Evaluationsmetrik, eine Funktion, die die Qualität der Programmausgabe im Vergleich zu einem erwarteten Ergebnis (Gold-Label) quantifiziert. Ein Evaluationsdatensatz (devset), eine Sammlung von Beispielen, die das Programm zuvor nicht gesehen hat, um eine unverfälschte Leistungsmessung zu gewährleisten.

Optimierung (Kompilierung): Die Optimierung, in DSPy auch als Kompilierung bezeichnet, ist der automatisierte Prozess zur Verbesserung der Prompts eines Programms. Anstatt Prompts manuell zu formulieren, übernimmt ein sogenannter Teleprompter (Optimizer) diese Aufgabe. Der Optimizer, wie z.B. BootstrapFewShot, nutzt einen Trainingsdatensatz (trainset), um verschiedene Prompt-Strategien zu erproben. Mithilfe der Evaluationsmetrik bewertet der Optimizer die Leistung dieser Strategien auf den Trainingsdaten. Das Ergebnis ist ein optimiertes Programm, dessen Prompts so konstruiert sind, dass sie die definierte Metrik maximieren.

Zusammenfassend lässt sich sagen, dass die Evaluation die Zielvorgabe liefert, während die Optimierung der Mechanismus ist, der das Programm systematisch an dieses Ziel heranführt.

Experiment zur Auswirkung der Beispielanzahl

In diesem praktischen Teil wird untersucht, wie sich die Leistung eines Intent-Klassifikators ändert, wenn die Anzahl der Few-Shot-Beispiele im Prompt variiert wird. Als Optimizer wird BootstrapFewShot verwendet, dessen Parameter max_bootstrapped_demos die Anzahl dieser Beispiele steuert.

Zunächst wird ein Datensatz für Kundenanfragen geladen, gefiltert und in ein Trainings- sowie ein Evaluationsset aufgeteilt.

from datasets import load_dataset
import dspy

# Laden und Vorbereiten des Datasets
dataset = load_dataset("bitext/Bitext-customer-support-llm-chatbot-training-dataset", split="train")
dataset = dataset.filter(lambda x: x["category"] == "ACCOUNT")
dataset = dataset.shuffle(seed=42)

# Aufteilung in Trainings- (1000) und Evaluationssamples (100)
train_samples = dataset.select(range(1000))
dev_samples = dataset.select(range(1000, 1100))

# Konvertierung in dspy.Example Objekte
trainset = [dspy.Example(instruction=ex['instruction'], intent=ex['intent']).with_inputs('instruction') for ex in train_samples]
devset = [dspy.Example(instruction=ex['instruction'], intent=ex['intent']).with_inputs('instruction') for ex in dev_samples]

print(f"Anzahl der Beispiele im Trainingsset: {len(trainset)}")
print(f"Anzahl der Beispiele im Evaluationsset: {len(devset)}")

Anschließend werden die Signatur und das Modul für die Intent-Klassifizierung definiert.

class IntentSignature(dspy.Signature):
    """
    Identify the intent of the customer instruction. 
    Possible values are recover_password, switch_account, create_account, delete_account, registration_problems, edit_account.
    """
    instruction = dspy.InputField(desc="Instruction: a user request from the Customer Service domain.")
    intent = dspy.OutputField(desc="Intent: the intent corresponding to the user instruction.")

class IntentClassifier(dspy.Module):
    def __init__(self):
        super().__init__()
        self.predictor = dspy.Predict(IntentSignature)

    def forward(self, instruction):
        return self.predictor(instruction=instruction)

Für dieses Experiment wird ein lokal ausgeführtes Sprachmodell konfiguriert. Die Evaluationsmetrik ist die exakte Übereinstimmung.

# Annahme: Ein lokales LLM (z.B. Llama.cpp Server) läuft auf dem angegebenen Endpunkt
# Konfiguration des lokalen Sprachmodells
local_llm = dspy.LM(
    "openai/gemma-3-4b-it-Q4_K_M.gguf", 
    api_base="http://localhost:8080/v1", 
    api_key="no_key_needed",
    temperature=0.1,
    cache=False
)
dspy.configure(lm=local_llm)

from dspy.evaluate import Evaluate
from dspy.teleprompt import BootstrapFewShot

# Metrik-Funktion: Vergleicht Vorhersage und Label (case-insensitive)
def exact_match_metric(gold, pred, trace=None):
    return gold.intent.lower() == pred.intent.lower()

# Evaluator instanziieren
evaluator = Evaluate(devset=devset, metric=exact_match_metric, num_threads=1, display_progress=True)

Das Experiment wird in zwei Phasen durchgeführt: Zuerst wird die Baseline-Leistung des unoptimierten Modells (Zero-Shot) gemessen. Anschließend wird das Modell mit einer unterschiedlichen Anzahl von Beispielen (max_bootstrapped_demos = 2, 4, 8) optimiert und jeweils evaluiert.

# Evaluation des unoptimierten Modells (Zero-Shot)
print("Evaluation vor Optimierung (Zero-Shot):")
unoptimized_program = IntentClassifier()
zero_shot_score = evaluator(unoptimized_program, display_table=0)

# Iterative Optimierung und Evaluation
results = {"0 (Zero-Shot)": zero_shot_score}
demo_counts = [2, 4, 8]

for count in demo_counts:
    print(f"\nStarte Optimierung mit max_bootstrapped_demos = {count}...")

    # Konfiguration des Optimizers mit der jeweiligen Beispielanzahl
    optimizer = BootstrapFewShot(metric=exact_match_metric, max_bootstrapped_demos=count)

    # Kompilierung des Programms
    optimized_program = optimizer.compile(IntentClassifier(), trainset=trainset)

    print(f"\nEvaluation nach Optimierung (Demos={count}):")
    score = evaluator(optimized_program, display_table=0)
    results[str(count)] = score

print("\n--- Experiment-Ergebnisse ---")
for demos, score in results.items():
    print(f"Anzahl Demos: {demos}, Accuracy: {score:.2f}%")

Ergebnisse

Die Ausführung des Experiments liefert eine quantitative Messung der Auswirkung der Beispielanzahl auf die Genauigkeit der Intent-Klassifizierung. Die folgenden Ergebnisse sind repräsentativ für einen typischen Durchlauf:

Anzahl der Demos (max_bootstrapped_demos)Accuracy (Exact Match)
0 (Zero-Shot)81.0%
299.0%
497.0%
896.0%

Interpretation der Ergebnisse

Die vorgelegten Ergebnisse zeigen eine deutliche, aber nicht-lineare Beziehung zwischen der Anzahl der im Prompt bereitgestellten Beispiele (Demos) und der Klassifizierungsgenauigkeit. Der auffälligste Effekt ist der massive Anstieg der Genauigkeit von 81,0 % im Zero-Shot-Szenario (0 Demos) auf 99,0 % bei der Verwendung von nur zwei Beispielen. Dieser Sprung von 18 Prozentpunkten unterstreicht die immense Wirkung des „In-Context Learning“. Ohne Beispiele muss das Modell die Anweisung in der Signatur abstrakt interpretieren. Bereits zwei konkrete Demonstrationen reichen jedoch aus, um die Aufgabe zu konkretisieren. Sie zeigen dem Modell unmissverständlich den Zusammenhang zwischen einer typischen Benutzeranweisung und dem korrekten Intent. Diese Beispiele reduzieren die Ambiguität der Aufgabe drastisch und führen zu einer wesentlich zuverlässigeren und präziseren Klassifizierung.

Das optimale Ergebnis von 99,0 % Genauigkeit wird mit nur zwei Demos erreicht. Dies ist ein entscheidendes Ergebnis, da es der intuitiven Annahme „mehr Daten sind immer besser“ widerspricht. Eine geringe Anzahl von qualitativ hochwertigen, repräsentativen Beispielen ist oft ausreichend, um die Aufgabe für das Modell vollständig zu definieren. Die vom BootstrapFewShot-Optimizer ausgewählten zwei Beispiele scheinen ein besonders effektives Paar gewesen zu sein, das die Kernanforderungen der Klassifizierungsaufgabe perfekt abdeckt.

Interessanterweise führt die Erhöhung der Anzahl der Demos von zwei auf vier (97,0 %) und weiter auf acht (96,0 %) zu einer leichten, aber messbaren Verschlechterung der Leistung. Dieses Phänomen kann mehrere Ursachen haben: Es ist möglich, dass die zusätzlichen Beispiele, die für die 4- und 8-Demo-Prompts ausgewählt wurden, weniger eindeutig oder sogar irreführend waren. Wenn der Optimizer weniger ideale oder mehrdeutige Beispiele aus dem trainset auswählt, können diese das Modell eher verwirren als anleiten. Eine zu große Anzahl von Beispielen kann dazu führen, dass das Modell versucht, zu spezifische Muster aus den Demos zu lernen, anstatt die allgemeine logische Regel zu abstrahieren. Es konzentriert sich dann zu sehr auf die Eigenheiten der Beispiele im Prompt und verliert an Generalisierungsfähigkeit für die eigentliche Anfrage. Bei längeren Prompts kann die Aufmerksamkeit des Modells nachlassen („Lost in the Middle“-Problem). Die ursprüngliche, klare Anweisung und die entscheidende neue Anfrage am Ende des Prompts können durch eine große Menge an dazwischenliegenden Beispielen an Gewicht verlieren.

Da mir das Ergebnis etwas komisch vorkommt, lasse ich das Skript day14.ipynb noch ein paar mal laufen und ändere dabei jedes mal den seed vom shuffle Aufruf um das Ergebnis mit anderen Daten zu vergleichen. Hier die Ergebnisse

# Seed = 55
--- Experiment-Ergebnisse ---
Anzahl Demos: 0 (Zero-Shot), Accuracy: 70.0%
Anzahl Demos: 2, Accuracy: 99.0%
Anzahl Demos: 4, Accuracy: 95.0%
Anzahl Demos: 8, Accuracy: 96.0%
# Seed = 70
--- Experiment-Ergebnisse ---
Anzahl Demos: 0 (Zero-Shot), Accuracy: 72.0%
Anzahl Demos: 2, Accuracy: 99.0%
Anzahl Demos: 4, Accuracy: 99.0%
Anzahl Demos: 8, Accuracy: 98.0%
# Seed = 99
--- Experiment-Ergebnisse ---
Anzahl Demos: 0 (Zero-Shot), Accuracy: 75.0%
Anzahl Demos: 2, Accuracy: 96.0%
Anzahl Demos: 4, Accuracy: 96.0%
Anzahl Demos: 8, Accuracy: 96.0%
# Seed = 122
--- Experiment-Ergebnisse ---
Anzahl Demos: 0 (Zero-Shot), Accuracy: 75.0%
Anzahl Demos: 2, Accuracy: 99.0%
Anzahl Demos: 4, Accuracy: 96.0%
Anzahl Demos: 8, Accuracy: 98.0%

Trotz unterschiedlicher Daten immer das gleiche Ergebnis. Mit 2 Demos erhält man die beste Accuracy. Mit 4 Demos ist das Ergebnis in einigen Fällen genauso gut, aber warum den Prompt unnötig aufblähen, wenn man mit 2 Demos ein besseres Ergebnis erzielen kann als mit 4.

Zusammenfassung

Das Experiment demonstriert, dass die Anzahl der Few-Shot-Beispiele ein kritischer Hyperparameter ist, der die Modellleistung direkt steuert. Das Beispiel zeigt, dass bereits wenige Beispiele eine drastische Leistungssteigerung im Vergleich zum Zero-Shot-Ansatz bewirken können.

Veröffentlicht in Allgemein