Überspringen und zum Inhalt gehen →

30-Tage-DSPy-Challenge: Tag 11 – Eigene Metriken zur Evaluation

Die datengesteuerte Optimierung von LLM-Anwendungen erfordert eine Methode zur Bewertung der Leistung der Prompts. Im DSPy-Framework wird diese Funktion durch Metriken erfüllt. Eine Metrik definiert, was als „gutes“ oder „korrektes“ Ergebnis für eine bestimmte Aufgabe gilt. Während DSPy einige Standard-Metriken bereitstellt, ist die Fähigkeit, eigene, aufgabenspezifische Metriken zu definieren, für eine präzise Evaluation und Optimierung unerlässlich.

Aufbau und Funktion von Evaluationsmetriken

Eine Metrik in DSPy ist im Kern eine Python-Funktion, die eine vom Modell generierte Vorhersage mit dem erwarteten „Goldstandard“-Ergebnis vergleicht. Die Signatur dieser Funktion ist standardisiert und lautet:

metric(gold, pred, trace=None)

Die Parameter haben folgende Bedeutung:

  • gold
    Dies ist das dspy.Example-Objekt aus dem Evaluationsdatensatz (devset). Es enthält die Eingabedaten sowie die korrekten, erwarteten Ausgabewerte (Labels). Zum Beispiel gold.sentiment würde auf den korrekten Sentiment-Wert zugreifen.
  • pred
    Dies ist das dspy.Prediction-Objekt, das vom DSPy-Modul als Antwort generiert wurde. Es enthält die vom Modell vorhergesagten Ausgabewerte. Entsprechend würde pred.sentiment auf den vorhergesagten Wert zugreifen.
  • trace
    Ein optionaler Parameter, der die vollständige Aufzeichnung der LLM-Aufrufe für die gegebene Vorhersage bereitstellt. Dieser Parameter ist für fortgeschrittene Metriken nützlich, beispielsweise wenn eine Bewertung durch ein weiteres LLM („LLM-as-judge“) erfolgen soll, das den „Gedankengang“ des zu bewertenden Modells analysieren soll.

Der Rückgabewert der Metrik-Funktion bestimmt das Ergebnis der Evaluation für einen einzelnen Datenpunkt. Üblicherweise ist dies:

  • Ein boolescher Wert: True für eine korrekte Vorhersage, False für eine inkorrekte.
  • Ein numerischer Wert (Integer oder Float), der einen Score repräsentiert, oft im Bereich von 0.0 bis 1.0.

Das dspy.Evaluate-Werkzeug führt diese Metrik für jeden Datenpunkt im Evaluationsset aus und berechnet den Durchschnitt der zurückgegebenen Werte, um einen Gesamtleistungsscore für das Programm zu ermitteln.

Implementierung einer Genauigkeitsmetrik

Im Folgenden wird eine Metrik implementiert, welche die Genauigkeit (Accuracy) für die bereits bekannte Sentiment-Analyse-Aufgabe berechnet. Die Genauigkeit ist definiert als der Anteil der korrekt klassifizierten Beispiele an der Gesamtzahl aller Beispiele. Die Implementierung basiert auf dem optimized_classifier aus dem Jupyter Notebook von Tag 10 (day10.ipynb). Den vollständigen Code kann man im GitHub Repository finden.

Die Funktion sentiment_accuracy vergleicht den vorhergesagten Sentiment-Wert mit dem tatsächlichen Wert aus dem dspy.Example.

import dspy

def sentiment_accuracy(gold, pred, trace=None):
    """
    Berechnet die Genauigkeit für die Sentiment-Klassifizierung.
    Gibt True zurück, wenn die Vorhersage mit dem Gold-Label übereinstimmt, andernfalls False.

    Args:
        gold (dspy.Example): Das Beispiel mit der korrekten Antwort.
        pred (dspy.Prediction): Die Vorhersage des Modells.
        trace (optional): Der Ausführungstrace. Wird hier nicht verwendet.

    Returns:
        bool: True bei Übereinstimmung, andernfalls False.
    """
    # Zugriff auf die relevanten Felder in den gold- und pred-Objekten
    gold_sentiment = gold.sentiment
    predicted_sentiment = pred.sentiment

    # Normalisierung und Vergleich der Werte (Groß-/Kleinschreibung ignorieren)
    return gold_sentiment.lower() == predicted_sentiment.lower()

Die Implementierung stellt durch die Verwendung von .lower() sicher, dass der Vergleich unabhängig von der Groß- und Kleinschreibung ist, was die Metrik robuster macht.

Um die definierte Metrik zu verwenden, wird sie an das dspy.Evaluate-Werkzeug übergeben. Es wird angenommen, dass ein Evaluationsdatensatz (devset) und ein zu evaluierendes DSPy-Programm (z.B. optimized_classifier) bereits vorhanden sind.

from dspy.evaluate import Evaluate

# Annahme: 'devset' und 'optimized_classifier' sind bereits definiert.
# devset = [...] # Liste von dspy.Example Objekten
# optimized_classifier = ... # Ein kompiliertes DSPy-Modul

# Instanziierung des Evaluators
evaluator = Evaluate(devset=devset, num_threads=1, display_progress=True)

# Ausführung der Evaluation mit der benutzerdefinierten Metrik
evaluation_score = evaluator(optimized_classifier, metric=sentiment_accuracy)

Ergebnis

Die Ausführung des evaluator mit der sentiment_accuracy-Metrik liefert einen numerischen Score. Wenn beispielsweise 3 von 4 Beispielen im devset korrekt klassifiziert wurden, gibt die Metrik dreimal True (was als 1 gewertet wird) und einmal False (gewertet als 0) zurück. dspy.Evaluate berechnet den Durchschnitt dieser Werte.

Die Konsolenausgabe zeigt folgende Informationen.

Average Metric: 2.00 / 2 (100.0%): 100%|█████████████████████████████████████████████████| 2/2 [00:03<00:00,  1.62s/it]
INFO dspy.evaluate.evaluate: Average Metric: 2 / 2 (100.0%)

Interpretation und Zusammenfassung

Das bedeutet, dass das evaluierte Programm optimized_classifier 100 % der Beispiele im devset korrekt gemäß der in sentiment_accuracy definierten Logik klassifiziert hat. Für dieses Beispiel ist es recht einfach, da das Modell keine komplexe Aufgabe zu bewältigen hat.

Die Fähigkeit, eigene Metriken zu definieren, ist von entscheidender Bedeutung, da Standard-Metriken wie die exakte Übereinstimmung (exact match) für viele Anwendungsfälle nicht ausreichen. Komplexe Aufgaben, wie die Bewertung der Qualität einer Zusammenfassung, die Validierung von JSON-Strukturen oder die Messung der Faktenkonsistenz, erfordern eine spezifische Bewertungslogik. Durch das gezeigte Muster kann man den Evaluationsprozess exakt auf die Erfolgsfaktoren einer spezifischen Anwendung zuschneiden. Dies ermöglicht eine zielgerichtete und aussagekräftige Optimierung von DSPy-Programmen.

Ergänzung: Komplexeres Beispiel

Das Beispiel mit den 8 synthetischen Daten ist doch relativ einfach. Daher hier noch ein etwas komplizierteres Beispiel mit dem Huggingface imdb Datensatz. Um den Huggingface imdb Datensatz laden zu können mus man noch das python Modul datasets installieren. Das geht mit folgendem Befehl.

pip install datasets

Nach der Installation kann man die imdb Daten mit folgendem Code laden:

from datasets import load_dataset

# Datensatz laden – hier der IMDb Sentiment-Datensatz
dataset = load_dataset("imdb", split="train") 
dataset = dataset.shuffle(seed=42)
dataset = dataset.select(range(500))

Damit werden zufällige 500 Einträge aus dem imdb Datensatz von Huggingface geladen (Achtung, der Datensatz ist relativ groß und der Download dauert eine Weile). Mit dem folgenden Code kann man sich dann noch mal die „Label Distribution“ ansehen. Es sollte so ca. 50/50 sein.

from collections import Counter

# Check label distribution
label_counts = Counter(dataset["label"])

total = sum(label_counts.values())
for label, count in label_counts.items():
    print(f"Label {label}: {count} ({count/total:.2%})")

Wenn man eine einigermaßen gute Verteilung der Label hat, (In meinem Fall ist es 49.20% / 50.80%) kann man die 500 Beispiele in DSPy Examples überführen.

# DSPy-Examples erzeugen
examples = [
    dspy.Example(
        input=text["text"],
        sentiment="positiv" if text["label"] == 1 else "negativ"
    ).with_inputs("input")  # Definiert, welches Feld als Eingabe genutzt wird
    for text in dataset
]

# Testausgabe
print(examples[0])
print(f"{len(examples)} DSPy Examples geladen.")

Die Testausgabe sollte in etwa wie folgt aussehen:

Example({'input': 'There is no relation at all between Fortier and Profiler but the fact that both are police series about violent crimes. Profiler looks crispy, Fortier looks classic. Profiler plots are quite simple. Fortier\'s plot are far more complicated... Fortier looks more like Prime Suspect, if we have to spot similarities... The main character is weak and weirdo, but have "clairvoyance". People like to compare, to judge, to evaluate. How about just enjoying? Funny thing too, people writing Fortier looks American but, on the other hand, arguing they prefer American series (!!!). Maybe it\'s the language, or the spirit, but I think this series is more English than American. By the way, the actors are really good and funny. The acting is not superficial at all...', 'sentiment': 'positiv'}) (input_keys={'input'})
500 DSPy Examples geladen.

Zum Schluss wird der Datensatz in ein Trainings-Datensatz und ein Test-Datensatz umgewandelt.

# Aufteilung des Datensatzes
split_index = int(len(examples) * 0.8)  # 80/20-Split

trainset = examples[:split_index]
devset = examples[split_index:]

# Anzeige der Größe der erstellten Sets
print(f"Anzahl der Beispiele im Trainingsset: {len(trainset)}")
print(f"Anzahl der Beispiele im Evaluationsset: {len(devset)}")

Damit kann man dann wieder den Optimizer laufen lassen und die Metrik mit dem Evaluator ermitteln.

Average Metric: 92.00 / 100 (92.0%): 100%|███████████████████████████████████████████| 100/100 [01:07<00:00,  1.48it/s]
INFO dspy.evaluate.evaluate: Average Metric: 92 / 100 (92.0%)

Wie man sieht, habe ich 92% der IMDB Bewertungen korrekt zugeordnet. Zum Vergleich lasse ich den unoptimized_classifier von Tag 8 auch mal durch den Evaluator laufen um zu prüfen, wie viel besser der optimized_classifier geworden ist. Schließlich sagt so eine Metrik ja nicht aus, wie viel besser der optimized_classifier gegenüber einem SimpleClassifier ohne Beispiele im Prompt ist.

Das Ergebnis ist unerwartet und verblüffend zugleich, da der unoptimized_classifier sogar 94% der IMDB Bewertungen korrekt zuordnet.

Average Metric: 94.00 / 100 (94.0%): 100%|███████████████████████████████████████████| 100/100 [01:06<00:00,  1.50it/s]
INFO dspy.evaluate.evaluate: Average Metric: 94 / 100 (94.0%)

Ein Grund dafür könnte sein, das der Parameter temperature noch recht hoch ist. Also reduziere ich die Kreativität des Modells und setze die temperature auf 0.1 und lasse alles noch mal laufen. Das Ergebnis ist:

Ergebnis optimized_classifier:

Average Metric: 92.00 / 100 (92.0%): 100%|███████████████████████████████████████████| 100/100 [01:06<00:00,  1.51it/s]
INFO dspy.evaluate.evaluate: Average Metric: 92 / 100 (92.0%)

Ergebnis unoptimized_classifier:

Average Metric: 94.00 / 100 (94.0%): 100%|███████████████████████████████████████████| 100/100 [01:13<00:00,  1.37it/s]
INFO dspy.evaluate.evaluate: Average Metric: 94 / 100 (94.0%)

Wie es scheint haben die Beispiel das LLM eher gestört als dabei geholfen das Ergebnis zu verbessern. Denn auch mit der reduzierten temperature ändert sich nichts daran, dass der unoptimized_classifier 2% besser ist als der optimized_classifier. Das komplette Python Script zu dem Test befindet sich in day11-addon.ipynb. Evtl. lohnt es sich das mal mit anderen Modellen als dem Qwen3-VL-8B-Instruct-Q4_K_M.gguf zu testen…..

https://github.com/msoftware/DSPy

Veröffentlicht in Allgemein