Ich erinnere mich noch sehr gut, dass bis vor einigen Monaten Prompt Engineering der letzte Schrei war. Der gesamte Arbeitsmarkt war mit der Rolle von Prompt Engineers gefüllt, aber das ist jetzt nicht mehr der Fall. Prompt Engineering war keine Kunst oder Wissenschaft, es war nur ein cleverer Hans-Phänomen, bei dem Menschen den notwendigen Kontext für das System bereitstellten, um eine bessere Antwort zu geben. Die Leute schrieben sogar Bücher/Blogs wie die Top 50 Prompts, um das Beste aus GPT herauszuholen, und so weiter und so fort. Aber groß angelegte Experimente haben gezeigt, dass es keine einzige Aufforderung oder Strategie gibt, die für alle Arten von Problemen funktioniert, einige Aufforderungen scheinen zwar in der Isolation besser zu sein, sind aber ein Volltreffer und ein Fehlschlag, wenn sie umfassend analysiert werden. Heute werden wir über DSPY: COMPILING DECLARATIVE LANGUAGE MODEL CALLS INTO SELF-IMPROVING PIPELINES sprechen, einen von Stanford entwickelten Rahmen für Selbstverbesserungs-Pipelines, bei dem LLMs als Modul behandelt werden, das von einem Compiler optimiert wird, ähnlich wie die Abstraktionen, die in PyTorch zu finden sind.
Einführung
Wie ich oben erwähnt habe, ist das Internet voll von Werbebüchern und -blogs. Und die meisten von ihnen verkaufen dir einen Haufen Mist. Jetzt, wie ich sagte, einige von ihnen mögen tatsächlich funktionieren, aber das ist keine sehr gute Art, unsere Apps aufzubauen. Nicht zu wissen, wann etwas nicht funktioniert, ist wichtig, wir müssen einen sicheren Hypothesenraum definieren, in dem das System funktioniert und nicht funktioniert.
Die ersten Suchergebnisse auf Google für Bücher über Prompt Engineering. Es gibt tatsächlich Aufforderungen in diesen Büchern geschrieben, nicht die Verwendung von Techniken wie CoT oder ReAct
Es gibt sogar Paper, in denen gezeigt wurde, dass die Leistung von LLMs mit bestimmten emotionalen Aufforderungen zunimmt. Für mich habe ich immer noch meine Bedenken hinsichtlich der Authentizität eines solchen Papiers. Wie lange hält es an? Gilt es für jedes Thema? Gibt es Themen, bei denen diese Art von emotionaler Aufforderung zu schlechteren Ergebnissen führen könnte? Es gibt so viele Paper wie dieses, die unbeabsichtigt halbfertige Forschung verbreiten. Ein weiteres Paper wie dieses war die Embers of Autoregression, bei dem viele Dinge später widerlegt wurden.
https://arxiv.org/pdf/2307.11760
Aber die größere Frage ist, was für eine wissenschaftliche/systematische Methode es ist, bei der ich einem System sagen muss, dass „Ich könnte gefeuert werden, wenn du mir nicht sofort die Antwort gibst oder meine Oma ist krank, und so weiter und so fort“. Das sind einfach Leute, die versuchen, sich in das Verhalten von LLMs zu hacken.
Das Problem mit der Aufforderung verstehen
Beispielsweise, wenn ich sage „Füge 5-Shot CoT mit RAG hinzu, mit harten negativen Beispielen“, ist es zwar konzeptionell klar, aber in der Praxis sehr schwer umzusetzen. LLMs sind sehr empfindlich gegenüber Aufforderungen, so dass das Hinzufügen dieser Art von Struktur in einer Aufforderung nicht funktioniert die meiste Zeit. Das Verhalten von LLMs ist sehr empfindlich gegenüber der Art und Weise, wie eine Aufforderung geschrieben wird, was es schwierig macht, sie zu steuern.
Daher, wenn wir eine Pipeline aufbauen, geht es nicht nur darum, ein LLM davon zu überzeugen, eine Ausgabe in einer bestimmten Weise zu geben, sondern die Ausgabe sollte so eingeschränkt sein, dass sie als Eingabe für andere Module in der größeren Pipeline funktionieren kann.
Um dieses Problem zu lösen, gibt es bereits viel Forschung, aber sie sind in vielerlei Hinsicht begrenzt. Die meisten von ihnen basieren auf String-Vorlagen, die zerbrechlich und nicht skalierbar sind. Das Sprachmodell ändert sich im Laufe der Zeit und die Aufforderung bricht. Wenn wir unser Modul in eine andere Pipeline einfügen wollen, funktioniert es nicht. Wir wollen, dass es mit neuen Tools, einer neuen Datenbank oder einem Abrufsystem interagiert, es funktioniert nicht.
Genau dieses Problem versucht DSPy zu lösen, indem LLM als Modul behandelt wird, dessen Verhalten automatisch angepasst wird, basierend auf der Art und Weise, wie es mit anderen Komponenten in der Pipeline interagiert.
DSPy Paradigma: Lass uns programmieren – nicht prompten – LMs
Das Ziel von DSPy ist es, den Fokus von der Feinabstimmung der LLMs auf ein gutes übergeordnetes Systemdesign zu verlagern.
Aber wie macht man das?
Um darüber auf einer mentalen Ebene nachzudenken, können wir die LLMs als Geräte betrachten: die Anweisungen ausführen und über eine Abstraktion operieren, die DNN ähnelt.
Beispielsweise definieren wir eine Schicht der Konvolution in PyTorch und sie kann auf einer Reihe von Eingaben betrieben werden, die von anderen Schichten kommen. Konzeptionell können wir diese Schichten stapeln und eine gewünschte Abstraktionsebene unserer ursprünglichen Eingaben erreichen, wir müssen keine CUDA-Kerne und viele andere Anweisungen definieren. Alles das ist bereits in der Definition der Konvolutionsschicht abstrahiert. Das wollen wir mit LLMs machen, bei denen LLMs abstrahierte Module sind, die in verschiedenen Kombinationen gestapelt werden, um ein bestimmtes Verhalten zu erreichen, sei es CoT, ReAct oder etwas anderes.
Um das gewünschte Verhalten zu erreichen, müssen wir einige Dinge ändern:
NLP-Signatur
Dies sind einfach die Deklarationen des Verhaltens, das wir von unseren LLMs wollen. Dies definiert nur, was erreicht werden soll, nicht die Spezifikationen, wie es erreicht werden soll. Eine Spezifikation, die DSPy sagt, was eine Transformation tut, anstatt wie man das LLM auffordert, es zu tun.
Beispiel für Signaturen
- Die Signatur behandelt das strukturierte Formatieren und Parsen der Logik.
- Signaturen können in selbstverbessernde und pipeline-adaptive Aufforderungen oder Feinabstimmungen kompiliert werden.
DSPY leitet die Rolle der Felder ab, indem es:
- Ihre Namen verwendet, z.B. verwendet DSPy in-context learning, um Fragen anders zu interpretieren als Antworten.
- Ihre Spuren (Eingabe/Ausgabe-Beispiele) verwendet
Hinweis: All dies wird nicht hart codiert, sondern das System ermittelt es während der Kompilierung
Module
Hier verwenden wir die Signaturen, um unsere Module aufzubauen, z.B. wenn wir ein CoT-Modul aufbauen wollen, verwenden wir diese Signaturen, um es aufzubauen. Dies erzeugt automatisch hochwertige Aufforderungen, um das Verhalten bestimmter Aufforderungstechniken zu erreichen.
Eine technischere Definition: Ein Modul ist eine parameterisierte Schicht, die eine Signatur durch Abstraktion einer Aufforderungstechnik ausdrückt.
Arten von Modulen
Nachdem es deklariert wurde, verhält sich ein Modul wie eine aufrufbare Funktion.
Parameter: Um eine bestimmte Signatur auszudrücken, muss jeder LLM-Aufruf spezifizieren:
- Das spezifische LLM zum Aufrufen
- Die Aufforderungsanweisungen
- Das String-Präfix für jedes Signaturfeld
- Die Demonstrationen, die als Few-Shot-Aufforderungen und/oder als Feinabstimmungsdaten verwendet werden
Optimierer
Um dieses System zum Laufen zu bringen, nimmt der Optimierer die gesamte Pipeline und optimiert sie anhand einer bestimmten Metrik und erzeugt im Prozess automatisch die besten Aufforderungen und aktualisiert sogar die Gewichte des Sprachmodells.
Die Idee auf hoher Ebene ist, dass wir einen Optimierer verwenden werden, um unseren Code zu kompilieren, der Sprachmodellaufrufe enthält, so dass jeder Modul in unserer Pipeline in eine Aufforderung kompiliert wird, die automatisch für uns generiert wird, oder in ein neues Satz von Gewichten für unser Sprachmodell, das an die Aufgabe angepasst ist, die wir versuchen zu lösen.
Praktisches Beispiel
Eine einzelne Suchanfrage ist oft nicht ausreichend für komplexe QA-Aufgaben. Beispielsweise enthält ein Beispiel innerhalb von HotPotQA
eine Frage über die Geburtsstadt des Autors von „Right Back At It Again“. Eine Suchanfrage identifiziert den Autor oft korrekt als „Jeremy McKinnon“, aber fehlt die Fähigkeit, die beabsichtigte Antwort zu komponieren, um herauszufinden, wann er geboren wurde.
Der Standardansatz für diese Herausforderung in der Literatur zur retrieval-gestützten NLP besteht darin, Multi-Hop-Suchsysteme aufzubauen, wie GoldEn (Qi et al., 2019) und Baleen (Khattab et al., 2021). Diese Systeme lesen die abgerufenen Ergebnisse und generieren dann zusätzliche Abfragen, um zusätzliche Informationen zu sammeln, wenn dies erforderlich ist, bevor sie zu einer endgültigen Antwort gelangen. Mit DSPy können wir solche Systeme in wenigen Zeilen Code simulieren.
Derzeit müssen wir sehr komplexe Aufforderungen schreiben und sie auf eine sehr unordentliche Weise strukturieren. Aber der schlechte Teil ist, dass sobald ich die Art der Fragen ändere, ich möglicherweise das gesamte Systemdesign ändern muss, aber nicht mit DSPy.
Konfiguration des Sprachmodells und des Abrufmodells
import dspy
turbo = dspy.OpenAI(model='gpt-3.5-turbo')
colbertv2_wiki17_abstracts = dspy.ColBERTv2(url='http://20.102.90.50:2017/wiki17_abstracts')
dspy.settings.configure(lm=turbo, rm=colbertv2_wiki17_abstracts)
Wenn Sie mehr über Abrufmodelle erfahren möchten:
Laden des Datasets
Wir verwenden das erwähnte HotPotQA
-Dataset, eine Sammlung von komplexen Frage-Antwort-Paaren, die normalerweise in einem Multi-Hop-Verfahren beantwortet werden. Wir können dieses Dataset, das von DSPy bereitgestellt wird, über die HotPotQA
-Klasse laden:
from dspy.datasets import HotPotQA
# Lade das Dataset.
dataset = HotPotQA(train_seed=1, train_size=20, eval_seed=2023, dev_size=50, test_size=0)
# Sag DSPy, dass das 'question'-Feld die Eingabe ist. Alle anderen Felder sind Labels und/oder Metadaten.
trainset = [x.with_inputs('question') for x in dataset.train]
devset = [x.with_inputs('question') for x in dataset.dev]
len(trainset), len(devset)
#Output
(20, 50)
Erstellen der Signatur
Nun, da wir die Daten geladen haben, lassen Sie uns mit dem Definieren der Signaturen für die Unteraufgaben unserer Baleen-Pipeline beginnen.
Wir beginnen damit, die GenerateAnswer
-Signatur zu erstellen, die context
und question
als Eingabe nimmt und answer
als Ausgabe gibt.
class GenerateAnswer(dspy.Signature):
"""Beantworte Fragen mit kurzen Fakten-Antworten.""""
context = dspy.InputField(desc="kann relevante Fakten enthalten")
question = dspy.InputField()
answer = dspy.OutputField(desc="oft zwischen 1 und 5 Wörtern")
class GenerateSearchQuery(dspy.Signature):
"""Schreibe eine einfache Suchanfrage, die bei der Beantwortung einer komplexen Frage hilft.""""
context = dspy.InputField(desc="kann relevante Fakten enthalten")
question = dspy.InputField()
query = dspy.OutputField()
Erstellen der Pipeline
Lassen Sie uns also die eigentliche Programm SimplifiedBaleen
definieren. Es gibt viele mögliche Möglichkeiten, dies zu tun, aber wir halten uns in dieser Version an die wesentlichen Elemente.
from dsp.utils import deduplicate
class SimplifiedBaleen(dspy.Module):
def __init__(self, passages_per_hop=3, max_hops=2):
super().__init__()
self.generate_query = [dspy.ChainOfThought(GenerateSearchQuery) for _ in range(max_hops)]
self.retrieve = dspy.Retrieve(k=passages_per_hop)
self.generate_answer = dspy.ChainOfThought(GenerateAnswer)
self.max_hops = max_hops
def forward(self, question):
context = []
for hop in range(self.max_hops):
query = self.generate_query[hop](context=context, question=question).query
passages = self.retrieve(query).passages
context = deduplicate(context + passages)
pred = self.generate_answer(context=context, question=question)
return dspy.Prediction(context=context, answer=pred.answer)
Wie man sehen kann, definiert die __init__
-Methode einige wichtige Submodule:
- generate_query: Für jeden Hop haben wir einen
dspy.ChainOfThought
-Vorhersager mit derGenerateSearchQuery
-Signatur. - retrieve: Dieses Modul führt die Suche mit den generierten Abfragen über unseren definierten ColBERT RM-Suchindex durch, indem es das
dspy.Retrieve
-Modul verwendet. - generate_answer: Dieses
dspy.Predict
-Modul wird mit derGenerateAnswer
-Signatur verwendet, um die endgültige Antwort zu produzieren.
Die forward
-Methode verwendet diese Submodule in einer einfachen Steuerungslogik.
- Zunächst führen wir eine Schleife bis zu
self.max_hops
mal aus. - In jeder Iteration generieren wir eine Suchanfrage mit dem Vorhersager an
self.generate_query[hop]
. - Wir rufen die Top-k-Passagen mit dieser Abfrage ab.
- Wir fügen die (deduplizierten) Passagen zu unserem
context
-Akkumulator hinzu. - Nach der Schleife verwenden wir
self.generate_answer
, um eine Antwort zu produzieren. - Wir geben eine Vorhersage mit dem abgerufenen
context
und der vorhergesagtenanswer
zurück.
Ausführen der Pipeline
Lassen Sie uns dieses Programm in seinem unkompilierten (Null-Shot) Setting ausführen.
Dies bedeutet nicht unbedingt, dass die Leistung schlecht sein wird, sondern vielmehr, dass wir durch die Zuverlässigkeit des zugrunde liegenden LM eingeschränkt sind, um unsere Subaufgaben aus minimalen Anweisungen zu verstehen. Oftmals ist dies völlig in Ordnung, wenn man die leistungsstärksten/teuersten Modelle (z.B. GPT-4) für die einfachsten und gängigsten Aufgaben (z.B. das Beantworten einfacher Fragen zu beliebten Entitäten) verwendet.
# Stelle eine beliebige Frage an dieses einfache RAG-Programm.
meine_frage = "Wie viele Stockwerke hat das Schloss, das David Gregory geerbt hat?"
# Hole die Vorhersage ab. Dies enthält `pred.context` und `pred.answer`.
uncompiled_baleen = SimplifiedBaleen() # unkompiliertes (d.h. Null-Shot) Programm
pred = uncompiled_baleen(meine_frage)
# Gib den Kontext und die Antwort aus.
print(f"Frage: {meine_frage}")
print(f"Vorhergesagte Antwort: {pred.answer}")
print(f"Abgerufene Kontexte (gekürzt): {[c[:200] + '...' for c in pred.context]}")
#Output
Frage: Wie viele Stockwerke hat das Schloss, das David Gregory geerbt hat?
Vorhergesagte Antwort: fünf
Abgerufene Kontexte (gekürzt): ['David Gregory (Arzt) | David Gregory (20. Dezember 1625 – 1720) war ein schottischer Arzt und Erfinder. Sein Nachname wird manchmal als Gregorie geschrieben, die ursprüngliche schottische Schreibweise. Er erbte Kinn...', 'The Boleyn Inheritance | The Boleyn Inheritance ist ein Roman der britischen Autorin Philippa Gregory, der 2006 zum ersten Mal veröffentlicht wurde. Es ist eine direkte Fortsetzung ihres früheren Romans "The Other Boleyn Girl," a...', 'Gregory of Gaeta | Gregory war von 963 bis zu seinem Tod Herzog von Gaeta. Er war der zweite Sohn von Docibilis II von Gaeta und seiner Frau Orania. Er folgte seinem Bruder John II, der nur Töchter hinterließ...', 'Kinnairdy Castle | Kinnairdy Castle ist ein Tower House mit fünf Stockwerken und einem Dachboden, zwei Meilen südlich von Aberchirder, Aberdeenshire, Schottland. Der alternative Name ist Old Kinnairdy....', 'Kinnaird Head | Kinnaird Head (Schottisch-Gälisch: "An Ceann Àrd" , "hoher Kopfland") ist ein Vorgebirge, das in die Nordsee ragt, innerhalb der Stadt Fraserburgh, Aberdeenshire an der Ostküste von Schottla...', 'Kinnaird Castle, Brechin | Kinnaird Castle ist eine Burg aus dem 15. Jahrhundert in Angus, Schottland. Die Burg ist seit über 600 Jahren die Heimat der Familie Carnegie, dem Earl of Southesk....']
Optimierung der Pipeline
Ein Null-Shot-Ansatz versagt jedoch schnell bei spezialisierteren Aufgaben, neuen Domänen/Einstellungen und effizienteren (oder offeneren) Modellen.
Um dies zu beheben, bietet DSPy die Kompilierung an. Lassen Sie uns unser Multi-Hop (SimplifiedBaleen
)-Programm kompilieren.
Lassen Sie uns zunächst unsere Validierungslogik für die Kompilierung definieren:
- Die vorhergesagte Antwort stimmt mit der Gold-Antwort überein.
- Der abgerufene Kontext enthält die Gold-Antwort.
- Keine der generierten Abfragen ist zu ausführlich (d.h. keine überschreitet 100 Zeichen in der Länge).
- Keine der generierten Abfragen wird grob wiederholt (d.h. keine ist innerhalb von 0,8 oder höher F1-Score von früheren Abfragen).
def validate_context_and_answer_and_hops(example, pred, trace=None):
if not dspy.evaluate.answer_exact_match(example, pred): return False
if not dspy.evaluate.answer_passage_match(example, pred): return False
hops = [example.question] + [outputs.query for *_, outputs in trace if 'query' in outputs]
if max([len(h) for h in hops]) > 100: return False
if any(dspy.evaluate.answer_exact_match_str(hops[idx], hops[:idx], frac=0.8) for idx in range(2, len(hops))): return False
return True
Wir werden einen der grundlegendsten Teleprompter in DSPy verwenden, nämlich BootstrapFewShot
, um die Vorhersager in der Pipeline mit Few-Shot-Beispielen zu optimieren.
from dspy.teleprompt import BootstrapFewShot
teleprompter = BootstrapFewShot(metric=validate_context_and_answer_and_hops)
compiled_baleen = teleprompter.compile(SimplifiedBaleen(), teacher=SimplifiedBaleen(passages_per_hop=2), trainset=trainset)
Bewertung der Pipeline
Lassen Sie uns nun unsere Bewertungsfunktion definieren und die Leistung der unkompilierten und kompilierten Baleen-Pipelines vergleichen. Obwohl dieses Devset nicht als zuverlässige Benchmark dient, ist es für dieses Tutorial anschaulich.
from dspy.evaluate.evaluate import Evaluate
# Definiere eine Metrik, um zu überprüfen, ob wir die richtigen Dokumente abgerufen haben
def gold_passages_retrieved(example, pred, trace=None):
gold_titles = set(map(dspy.evaluate.normalize_text, example["gold_titles"]))
found_titles = set(
map(dspy.evaluate.normalize_text, [c.split(" | ")[0] for c in pred.context])
)
return gold_titles.issubset(found_titles)
# Richte die `evaluate_on_hotpotqa`-Funktion ein. Wir werden diese Funktion mehrmals unten verwenden.
evaluate_on_hotpotqa = Evaluate(devset=devset, num_threads=1, display_progress=True, display_table=5)
uncompiled_baleen_retrieval_score = evaluate_on_hotpotqa(uncompiled_baleen, metric=gold_passages_retrieved, display=False)
compiled_baleen_retrieval_score = evaluate_on_hotpotqa(compiled_baleen, metric=gold_passages_retrieved)
print(f"## Retrieval-Score für unkompilierte Baleen: {uncompiled_baleen_retrieval_score}")
print(f"## Retrieval-Score für kompilierte Baleen: {compiled_baleen_retrieval_score}
Fazit
Diese Ergebnisse zeigen, dass die Kombination eines Multi-Hop-Settings in DSPy sogar das menschliche Feedback übertreffen kann. Sie haben sogar gezeigt, dass ein viel kleineres Modell, wie T5, mit einem DSPy-Setting mit GPT verglichen wurde. DSPy ist eines der coolsten Dinge, auf die ich gestoßen bin, nachdem lang chain veröffentlicht wurde, es verspricht, ein viel besseres und systematisch entworfenes System zu schaffen, anstatt wilde Stücke in eine große LLM-Pipeline zu werfen.