Überspringen und zum Inhalt gehen →

30-Tage-DSPy-Challenge: Tag 12 – Eigene Module erstellen

Während einfache DSPy-Programme mit einzelnen Prädiktoren wie dspy.Predict oder dspy.ChainOfThought erstellt werden, erfordern komplexe Anwendungen eine andere Herangehensweise. DSPy bietet hierfür das Konzept der Module (dspy.Module). Eigene Module ermöglichen es, mehrstufige Logik, mehrere Prädiktoren und komplexe Datenflüsse in wiederverwendbaren, in sich geschlossenen Komponenten zu kapseln.

Kapselung von Logik mit dspy.Module

Ein dspy.Module ist konzeptionell an torch.nn.Module aus dem PyTorch-Framework angelehnt. Es dient als Basisklasse für alle programmatischen Blöcke, die eine erlernbare Transformation von Eingaben zu Ausgaben durchführen. Das Erstellen eines eigenen Moduls folgt einem klaren Muster und erfordert die Implementierung von zwei zentralen Methoden wie schon im SimpleClassifier in day11.ipynb gesehen:

__init__(self) (Konstruktor)
In dieser Methode wird die Architektur des Moduls deklariert. Hier werden alle Sub-Module instanziiert und als Attribute der Klasse gespeichert. Dieser Schritt definiert die „Bausteine“ des Moduls, deren Verhalten später durch einen Optimizer angepasst werden kann.

forward(self, ...) (Ausführungsmethode)
Diese Methode definiert den eigentlichen Kontroll- und Datenfluss. Sie legt fest, wie die Eingabedaten durch die in __init__ deklarierten Sub-Module verarbeitet werden. Die forward-Methode beschreibt die logische Abfolge der Operationen – welche Prädiktoren in welcher Reihenfolge und mit welchen Daten aufgerufen werden.

Durch die Kombination dieser beiden Methoden lassen sich komplexe Pipelines erstellen. Ein Modul kann beispielsweise zuerst Informationen extrahieren, diese dann klassifizieren und basierend auf dem Ergebnis eine Antwort generieren. Diese gesamte Logik wird innerhalb eines einzigen, wiederverwendbaren Moduls gekapselt. Der Vorteil liegt in der Modularität und der Abstraktion. Ein einmal definiertes Modul kann in verschiedenen Programmen eingesetzt werden, ohne dass dessen interne Funktionsweise offengelegt werden muss.

Erstellung eines Moduls zur Textzusammenfassung

Die folgende Umsetzung zeigt die Erstellung eines benutzerdefinierten Moduls namens SummarizationModule. Dieses Modul führt eine Textzusammenfassung in zwei Schritten durch: Zuerst extrahiert es relevante Schlüsselwörter aus einem langen Text. Anschließend nutzt es diese Schlüsselwörter als Kontext, um eine fokussierte und prägnante Zusammenfassung zu erstellen.

Zunächst werden die notwendigen DSPy-Komponenten importiert und das Sprachmodell konfiguriert.

import dspy

# 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=0.1,
    cache=False
)

dspy.configure(lm=local_llm)

Für die beiden logischen Schritte (Schlüsselwörter generieren, Zusammenfassung erstellen) werden zwei separate Signaturen definiert.

class GenerateKeywords(dspy.Signature):
    """Generiert eine Liste von 5-7 relevanten Schlüsselwörtern aus einem langen Text."""
    long_text = dspy.InputField(desc="Ein langer Text, aus dem Schlüsselwörter extrahiert werden sollen.")
    keywords = dspy.OutputField(desc="Eine durch Kommas getrennte Liste von 5-7 Schlüsselwörtern.")

class SummarizeWithKeywords(dspy.Signature):
    """Erstellt eine kurze Zusammenfassung eines Textes unter Berücksichtigung der vorgegebenen Schlüsselwörter."""
    long_text = dspy.InputField(desc="Der Originaltext, der zusammengefasst werden soll.")
    keywords = dspy.InputField(desc="Relevante Schlüsselwörter, auf die sich die Zusammenfassung konzentrieren soll.")
    summary = dspy.OutputField(desc="Eine prägnante Zusammenfassung in 2-3 Sätzen.")

Nun wird das SummarizationModule definiert. Es erbt von dspy.Module und implementiert die __init__– und forward-Methoden.

class SummarizationModule(dspy.Module):
    def __init__(self):
        super().__init__()
        # 1. Deklaration der Sub-Module im Konstruktor
        self.keyword_generator = dspy.Predict(GenerateKeywords)
        self.summarizer = dspy.Predict(SummarizeWithKeywords)

    def forward(self, long_text):
        # 2. Definition des Kontrollflusses in der forward-Methode
        # Schritt 1: Schlüsselwörter generieren
        keywords_prediction = self.keyword_generator(long_text=long_text)

        # Schritt 2: Zusammenfassung mit den generierten Schlüsselwörtern erstellen
        summary_prediction = self.summarizer(long_text=long_text, keywords=keywords_prediction.keywords)

        # Rückgabe der finalen Ergebnisse
        return dspy.Prediction(
            keywords=keywords_prediction.keywords,
            summary=summary_prediction.summary
        )

Abschließend wird eine Instanz des neuen Moduls erstellt und mit einem Beispieltext ausgeführt.

# Beispieltext zur Zusammenfassung
example_text = """
Die künstliche Intelligenz (KI) hat sich in den letzten Jahren rasant entwickelt.
Besonders große Sprachmodelle (LLMs) wie GPT-4 haben die Fähigkeiten von Maschinen,
menschliche Sprache zu verstehen und zu generieren, revolutioniert. Diese Modelle werden auf
riesigen Textmengen trainiert und können für eine Vielzahl von Aufgaben eingesetzt werden,
darunter Textzusammenfassung, Übersetzung, Beantwortung von Fragen und sogar das Schreiben
von kreativen Texten oder Code. Die zugrunde liegende Architektur, bekannt als Transformer,
ist entscheidend für diesen Erfolg, da sie es den Modellen ermöglicht, Kontexte und
Beziehungen zwischen Wörtern in langen Textsequenzen effektiv zu verarbeiten.
"""

# Instanziierung und Aufruf des Moduls
summarizer_module = SummarizationModule()
result = summarizer_module(long_text=example_text)

# Ausgabe der Ergebnisse
print(f"Generierte Schlüsselwörter: {result.keywords}")
print(f"Erstellte Zusammenfassung: {result.summary}")

Ergebnis

Die Ausführung des obigen Codes erzeugt eine Ausgabe, die sowohl die extrahierten Schlüsselwörter als auch die darauf basierende Zusammenfassung enthält. Das Ergebnis sieht wie folgt aus:

Generierte Schlüsselwörter: künstliche Intelligenz, Sprachmodelle, GPT-4, Transformer, Textgenerierung, KI-Entwicklung, maschinelles Lernen

Erstellte Zusammenfassung: Künstliche Intelligenz, insbesondere große Sprachmodelle wie GPT-4, hat durch ihre Fähigkeit zur Textgenerierung und -verarbeitung die KI-Entwicklung revolutioniert. Auf Basis der Transformer-Architektur können diese Modelle auf riesigen Datensätzen trainiert werden und vielfältige Aufgaben wie Übersetzung, Fragebeantwortung oder kreatives Schreiben übernehmen.

Zusammenfassung

Die praktische Umsetzung demonstriert erfolgreich die Kapselung einer zweistufigen Logik in einem einzigen, kohärenten dspy.Module.

  • Modularität
    Das SummarizationModule kann nun als eigenständiger Baustein in komplexeren DSPy-Programmen verwendet werden, ohne dass der aufrufende Code die interne Logik der Schlüsselwort-Extraktion kennen muss.
  • Abstraktion
    Die forward-Methode abstrahiert den mehrstufigen Prozess. Für den Anwender sieht der Aufruf des Moduls wie ein einfacher Funktionsaufruf aus, der einen langen Text entgegennimmt und eine Zusammenfassung zurückgibt.
  • Optimierbarkeit
    Das gesamte Modul kann als eine Einheit von einem DSPy-Optimizer (Teleprompter) optimiert werden. Der Optimizer würde lernen, die Prompts für beide internen Prädiktoren (keyword_generator und summarizer) so anzupassen, dass das Endziel – eine qualitativ hochwertige Zusammenfassung – bestmöglich erreicht wird.

Das Erstellen eigener Module ist somit der entscheidende Mechanismus in DSPy, um von einfachen Skripten zu skalierbaren, wartbaren und optimierbaren LLM-Anwendungen überzugehen. Es ermöglicht die Konstruktion von anspruchsvollen, hierarchischen Programmen, deren Komponenten logisch getrennt und wiederverwendbar sind.

Optimierung des gesamten Moduls

In einem vorherigen Schritt wurde gezeigt, wie man mit dspy.Module eine komplexe, mehrstufige Logik in einer wiederverwendbaren Komponente kapseln kann. Der wahre Vorteil dieses Ansatzes liegt jedoch in der Optimierbarkeit. DSPy-Optimizer, sogenannte „Teleprompter“, können ein gesamtes Modul als eine Einheit behandeln und die Prompts aller internen Prädiktoren so anpassen, dass das finale Ergebnis des Moduls verbessert wird.

Ganzheitliche Modul-Optimierung

Wenn die compile-Methode eines Optimizers auf ein dspy.Module angewendet wird, findet ein ganzheitlicher Optimierungsprozess statt. Der Optimizer betrachtet nicht nur einen einzelnen Prädiktor isoliert, sondern die gesamte in der forward-Methode definierte Kette von Operationen. Der Prozess funktioniert wie folgt:

Zieldefinition
Das Ziel der Optimierung ist die Maximierung des Scores, der von einer benutzerdefinierten Evaluationsmetrik berechnet wird. Diese Metrik vergleicht die finale Ausgabe der forward-Methode mit einem erwarteten Ergebnis (dem „Gold-Label“) aus einem Trainingsdatensatz.

Analyse der Sub-Module
Der Optimizer identifiziert alle in der __init__-Methode des Moduls deklarierten lernbaren Sub-Module (z. B. dspy.Predict, dspy.ChainOfThought).

Generierung von Demonstrationen
Für jedes dieser Sub-Module generiert der Optimizer „Few-shot“-Demonstrationen. Er simuliert dazu Durchläufe mit den Daten des Trainingssets und leitet ab, welche Beispiele (Eingabe/Ausgabe-Paare) für jeden internen Schritt am lehrreichsten sind, um das Endziel zu erreichen.

Erstellung optimierter Prompts
Die generierten Demonstrationen werden in die Prompts der jeweiligen Sub-Module eingefügt.

Das Ergebnis ist ein neues, „kompiliertes“ Modul, bei dem die internen Prädiktoren durch die Hinzunahme von Beispielen effektiver zusammenarbeiten, um die Gesamtleistung zu steigern.

Optimierung des SummarizationModule

Das folgende Python-Skript führt den gesamten Prozess für das zuvor erstellte SummarizationModule durch. Es nutzt das arxiv-summarization-Dataset, um dem Modul beizubringen, wie man wissenschaftliche Artikel zusammenfasst.

Vor der Ausführung des Skripts müssen die erforderlichen Bibliotheken installiert werden.

pip install evaluate rouge_score

Das nachfolgende Skript implementiert den gesamten Optimierungs-Workflow.

import dspy

# 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)

import heapq
from datasets import Dataset, load_dataset

# Lade die kürzesten Dokumente, da die längeren Dokumente nicht in das 128k Kontext Fenster des LLM passen
# Aber der Artikel muss mindestesn 10.000 Zeichen lang sein, da in dem Dataset doch einiges an Schrott drin ist :-(
def shortest_n(dataset, n, min_length=10000):
    """
    Wählt die n kürzesten Artikel aus, die mindestens `min_length` Zeichen lang sind.
    """
    # Filter auf Mindestlänge anwenden
    filtered = (x for x in dataset if len(x["article"]) >= min_length)
    
    # n kürzeste behalten
    return heapq.nsmallest(n, filtered, key=lambda x: len(x["article"]))

dataset = load_dataset("ccdv/arxiv-summarization", split="train")
shortest_125 = shortest_n(dataset, 125)
shortest_dataset = Dataset.from_list(shortest_125)

train_samples = shortest_dataset.select(range(100))
dev_samples = shortest_dataset.select(range(100, 125))

trainset = [dspy.Example(long_text=ex['article'], summary=ex['abstract']).with_inputs('long_text') for ex in train_samples]
devset = [dspy.Example(long_text=ex['article'], summary=ex['abstract']).with_inputs('long_text') for ex in dev_samples]

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

# Diese mal sind die Sgnaturen auf englich, da das Dataset auch auf englisch ist und 
# die rouge_metric nur funktioniert, wenn wie Sprache von gold unf pred identisch ist.
class GenerateKeywords(dspy.Signature):
    """Generates a list of 10 relevant keywords from a long scientific text."""
    long_text = dspy.InputField(desc="A long scientific article from which keywords should be extracted.")
    keywords = dspy.OutputField(desc="A comma-separated list of 5–7 keywords.")

class SummarizeWithKeywords(dspy.Signature):
    """Creates a short summary of a scientific text while taking the provided keywords into account."""
    long_text = dspy.InputField(desc="The original scientific text.")
    keywords = dspy.InputField(desc="Relevant keywords that the summary should focus on.")
    summary = dspy.OutputField(desc="A concise summary of the article in 3–4 sentences.")

class SummarizationModule(dspy.Module):
    def __init__(self):
        super().__init__()
        self.keyword_generator = dspy.Predict(GenerateKeywords)
        self.summarizer = dspy.Predict(SummarizeWithKeywords)

    def forward(self, long_text):
        keywords_prediction = self.keyword_generator(long_text=long_text)
        summary_prediction = self.summarizer(long_text=long_text, keywords=keywords_prediction.keywords)
        return dspy.Prediction(summary=summary_prediction.summary)

# ROUGE-L ist eine Standardmetrik zur Bewertung von Zusammenfassungen.
import evaluate as hf_evaluate

rouge = hf_evaluate.load('rouge')

def rouge_metric(gold, pred, trace=None):
    """Berechnet den ROUGE-L F1-Score."""
    if not pred.summary or not gold.summary:
        return 0.0
    results = rouge.compute(predictions=[pred.summary], references=[gold.summary])
    return results['rougeL']

# Der Optimizer generiert für jeden Prädiktor im Modul bis zu 3 "Few-Shot"-Beispiele.
from dspy.teleprompt import BootstrapFewShot

optimizer = BootstrapFewShot(metric=rouge_metric, max_bootstrapped_demos=3)

# Der compile-Prozess wendet den Optimizer auf das Modul an.
print("\nBeginne die Kompilierung des Moduls. Dies kann einige Minuten dauern...")
compiled_summarizer = optimizer.compile(SummarizationModule(), trainset=trainset)
print("Kompilierung abgeschlossen.")

# --- 6. Evaluation des unoptimierten und optimierten Moduls ---
from dspy.evaluate import Evaluate

evaluator = Evaluate(devset=devset, num_threads=1, display_progress=True, display_table=5)

print("\n--- Evaluation des unoptimierten Moduls (Zero-Shot) ---")
unoptimized_summarizer = SummarizationModule()
evaluator(unoptimized_summarizer, metric=rouge_metric)

print("\n--- Evaluation des optimierten Moduls (Few-Shot) ---")
evaluator(compiled_summarizer, metric=rouge_metric)

# --- 7. Inspektion der optimierten Prompts ---
print("\n--- Inspektion eines optimierten Prompts ---")
# Ein Beispieldurchlauf, um die internen Prompts zu inspizieren.
example_dev_text = devset[0].long_text
compiled_summarizer(long_text=example_dev_text)

Ergebnis und Interpretation

Nach der Ausführung des Skripts werden zwei Evaluationsdurchläufe durchgeführt und deren Ergebnisse angezeigt. Die Ausgabe der Evaluation zeigt eine Tabelle, die die durchschnittliche Punktzahl der ROUGE-Metrik für beide Module zeigt. Das Optimiert-Modul hat eine etwas höhere Punktzahl erreicht als das Unoptimiert-Modul. Ein höherer ROUGE-Score bedeutet, dass die generierten Zusammenfassungen eine größere lexikalische Überlappung mit den Referenz-Zusammenfassungen aus dem Dataset aufweisen, was auf eine höhere Qualität hindeutet.

--- Evaluation des unoptimierten Moduls (Zero-Shot) ---
Average Metric: 5.15 / 25 (20.6%): 100%| 25/25 [03:31<00:00, 8.47s/it]
--- Evaluation des optimierten Moduls (Few-Shot) ---
Average Metric: 5.56 / 25 (22.3%): 100%| 25/25 [34:11<00:00, 82.05s/it]

Von 20.6% auf 22.3% ist nicht viel aber auch nicht zu vernachlässigen.

Der letzte Teil des Skripts gibt einen der Prompts aus, die innerhalb des optimierten Moduls verwendet werden. Anstatt einer einfachen Anweisung (wie im unoptimierten Fall) enthält dieser Prompt nun mehrere vollständige Beispiele, die der Optimizer aus dem Trainingsdatensatz generiert hat. Dadurch wird der Prompt extrem lang – er enthält immerhin mehrere arxiv.org Paper und die dazu gehörenden Zusammenfassungen aus dem Huggingface Dataset. Aber der Prompt leitet das Sprachmodell etwas präziser an. Er zeigt dem Modell nicht nur, was es tun soll, sondern auch, wie eine gute Lösung aussieht. Aus dem Grund musste ich auch das Modell

Praktisch ist, dass der Optimizer für beide internen Prädiktoren (keyword_generator und summarizer) funktioniert, um deren Zusammenspiel zu verbessern und das bestmögliche Endergebnis zu erzielen.

Zusammenfassung

Das programmatische Optimieren ganzer Module ist eine der Stärken von DSPy. Anstatt manuell komplexe Prompt-Ketten zu entwerfen, definiert der Entwickler die logische Struktur der Anwendung in einem dspy.Module. Anschließend nutzt ein datengesteuerter Optimizer (Teleprompter) ein Trainingsset, um die internen Prompts aller Komponenten automatisch zu verfeinern. Dieser Ansatz macht die Entwicklung von LLM-Anwendungen systematischer, reproduzierbarer und führt zu besseren Ergebnissen.

Veröffentlicht in Allgemein