Una delle parti più frustranti della creazione di applicazioni gen-AI è il processo manuale di ottimizzazione dei prompt. In un pubblicazione realizzato da LinkedIn all'inizio di quest'anno, hanno descritto ciò che hanno imparato dopo aver distribuito un'applicazione RAG con agenti. Una delle sfide principali era ottenere una qualità costante. Hanno trascorso 4 mesi a modificare varie parti dell'applicazione, compresi i suggerimenti, per mitigare problemi come le allucinazioni.
DSPy è una libreria open source che tenta di parametrizzare i prompt in modo che diventi un problema di ottimizzazione. IL carta originale definisce il prompt engineering “fragile e non scalabile” e lo paragona alla “regolazione manuale dei pesi per un classificatore”.
Pagliaio è una libreria open source per creare applicazioni LLM, incluse le pipeline RAG. È indipendente dalla piattaforma e offre un gran numero di integrazioni con diversi fornitori LLM, database di ricerca e altro ancora. Ha anche il suo metriche di valutazione.
In questo articolo, esamineremo brevemente gli aspetti interni di DSPy e mostreremo come può essere utilizzato per insegnare a un LLM a preferire risposte più concise quando si risponde a domande su un set di dati medici accademici.
Questo articolo da TDS fornisce un'esplorazione approfondita di DSPy. Riassumeremo e utilizzeremo alcuni dei loro esempi.
Per costruire un'applicazione LLM che possa essere ottimizzata, DSPy offre due astrazioni principali: firme E moduli. Una firma è un modo per definire l'input e l'output di un sistema che interagisce con gli LLM. La firma viene tradotta internamente in un prompt da DSPy.
class Emotion(dspy.Signature):
# Describe the task
"""Classify emotions in a sentence."""sentence = dspy.InputField()
# Adding description to the output field
sentiment = dspy.OutputField(desc="Possible choices: sadness, joy, love, anger, fear, surprise.")
Quando si utilizza DSPy Predict
modulo (ne parleremo più avanti), questa firma viene trasformata nel seguente prompt:
Classify emotions in a sentence.---
Follow the following format.
Sentence: ${sentence}
Sentiment: Possible choices: sadness, joy, love, anger, fear, surprise.
---
Sentence:
Quindi, anche DSPy ha moduli che definiscono i “predittori” che hanno parametri che possono essere ottimizzati, come la selezione di esempi a pochi scatti. Il modulo più semplice è dspy.Predict
che non modifica la firma. Più avanti in questo articolo utilizzeremo il modulo dspy.ChainOfThought
che chiede al LLM di fornire una motivazione.
Le cose iniziano a diventare interessanti quando proviamo a ottimizzare un modulo (o come DSPy lo chiama “compilare” un modulo). Quando si ottimizza un modulo, in genere è necessario specificare 3 cose:
- il modulo da ottimizzare,
- un set di addestramento, che potrebbe avere etichette,
- e alcuni parametri di valutazione.
Quando si utilizza il dspy.Predict
o il dspy.ChainOfThought
moduli, DSPy effettua una ricerca nel set di training e seleziona gli esempi migliori da aggiungere al prompt come esempi di poche riprese. Nel caso di RAG, può includere anche il contesto utilizzato per ottenere la risposta finale. Chiama questi esempi “dimostrazioni”.
È inoltre necessario specificare il tipo di per ottimizzare che desideri utilizzare per effettuare la ricerca nello spazio dei parametri. In questo articolo utilizziamo il file BootstrapFewShot
ottimizzatore. Come funziona questo algoritmo internamente? In realtà è molto semplice e il documento fornisce alcuni pseudo-codici semplificati:
class SimplifiedBootstrapFewShot ( Teleprompter ) :
def __init__ ( self , metric = None ) :
self . metric = metricdef compile ( self , student , trainset , teacher = None ) :
teacher = teacher if teacher is not None else student
compiled_program = student . deepcopy ()
# Step 1. Prepare mappings between student and teacher Predict modules .
# Note : other modules will rely on Predict internally .
assert student_and_teacher_have_compatible_predict_modules ( student , teacher )
name2predictor , predictor2name = map_predictors_recursively ( student , teacher )
# Step 2. Bootstrap traces for each Predict module .
# We ’ll loop over the training set . We ’ll try each example once for simplicity .
for example in trainset :
if we_found_enough_bootstrapped_demos () : break
# turn on compiling mode which will allow us to keep track of the traces
with dspy . setting . context ( compiling = True ) :
# run the teacher program on the example , and get its final prediction
# note that compiling = True may affect the internal behavior here
prediction = teacher (** example . inputs () )
# get the trace of the all interal Predict calls from teacher program
predicted_traces = dspy . settings . trace
# if the prediction is valid , add the example to the traces
if self . metric ( example , prediction , predicted_traces ) :
for predictor , inputs , outputs in predicted_traces :
d = dspy . Example ( automated = True , ** inputs , ** outputs )
predictor_name = self . predictor2name (id( predictor ) )
compiled_program ( predictor_name ). demonstrations . append ( d )
return compiled_program
L'algoritmo di ricerca esamina ogni input di addestramento nel file trainset
ottiene una previsione e quindi controlla se “supera” la metrica esaminandola self.metric(example, prediction, predicted_traces)
. Se la metrica supera, gli esempi vengono aggiunti al file demonstrations
del programma compilato.
L'intero codice lo trovate qui libro di cucina con colab associataquindi qui esamineremo solo alcuni dei passaggi più importanti. Per l'esempio, usiamo a set di dati derivato da Set di dati PubMedQA (entrambi sotto licenza MIT). Contiene domande basate su abstract di documenti di ricerca medica e le relative risposte. Alcune delle risposte fornite possono essere piuttosto lunghe, quindi utilizzeremo DSPy per “insegnare” al LLM a preferire risposte più concise, mantenendo elevata l'accuratezza della risposta finale.
Dopo aver aggiunto i primi 1000 esempi a un archivio documenti in memoria (che può essere sostituito da un numero qualsiasi di recuperatori), ora possiamo costruire la nostra pipeline RAG:
from haystack.components.retrievers.in_memory import InMemoryBM25Retriever
from haystack.components.generators import OpenAIGenerator
from haystack.components.builders import PromptBuilder
from haystack import Pipelineretriever = InMemoryBM25Retriever(document_store, top_k=3)
generator = OpenAIGenerator(model="gpt-3.5-turbo")
template = """
Given the following information, answer the question.
Context:
{% for document in documents %}
{{ document.content }}
{% endfor %}
Question: {{question}}
Answer:
"""
prompt_builder = PromptBuilder(template=template)
rag_pipeline = Pipeline()
rag_pipeline.add_component("retriever", retriever)
rag_pipeline.add_component("prompt_builder", prompt_builder)
rag_pipeline.add_component("llm", generator)
rag_pipeline.connect("retriever", "prompt_builder.documents")
rag_pipeline.connect("prompt_builder", "llm")
Proviamolo!
question = "What effects does ketamine have on rat neural stem cells?"response = rag_pipeline.run({"retriever": {"query": question}, "prompt_builder": {"question": question}})
print(response("llm")("replies")(0))
La risposta alla domanda precedente:
La ketamina inibisce la proliferazione delle cellule staminali neurali del ratto in modo dose-dipendente a concentrazioni di 200, 500, 800 e 1000μM. Inoltre, la ketamina diminuisce la concentrazione intracellulare di Ca(2+), sopprime l'attivazione della proteina chinasi C-α (PKCα) e la fosforilazione delle chinasi extracellulari segnale-regolate 1/2 (ERK1/2) nelle cellule staminali neurali del ratto. Questi effetti non sembrano essere mediati dall’apoptosi dipendente dalla caspasi-3.
Possiamo vedere come le risposte tendano ad essere molto dettagliate e lunghe.
Iniziamo creando una firma DSPy dei campi di input e output:
class GenerateAnswer(dspy.Signature):
"""Answer questions with short factoid answers."""context = dspy.InputField(desc="may contain relevant facts")
question = dspy.InputField()
answer = dspy.OutputField(desc="short and precise answer")
Come possiamo vedere, specifichiamo già nella nostra descrizione che ci aspettiamo una risposta breve.
Quindi, creiamo un modulo DSPy che verrà successivamente compilato:
class RAG(dspy.Module):
def __init__(self):
super().__init__()
self.generate_answer = dspy.ChainOfThought(GenerateAnswer)# this makes it possible to use the Haystack retriever
def retrieve(self, question):
results = retriever.run(query=question)
passages = (res.content for res in results('documents'))
return Prediction(passages=passages)
def forward(self, question):
context = self.retrieve(question).passages
prediction = self.generate_answer(context=context, question=question)
return dspy.Prediction(context=context, answer=prediction.answer)
Stiamo utilizzando il retriever Haystack precedentemente definito per cercare i documenti nell'archivio documenti results = retriever.run(query=question)
. La fase di previsione viene eseguita con il modulo DSPy dspy.ChainOfThought
che insegna al LM a pensare passo dopo passo prima di impegnarsi nella risposta.
Durante la compilazione, il prompt che verrà ottimizzato avrà il seguente aspetto:
Infine, dobbiamo definire le metriche che vorremmo ottimizzare. Il valutatore sarà composto da due parti:
SASEvaluator
: la metrica di somiglianza della risposta semantica è un punteggio compreso tra 0 e 1 che calcola la somiglianza tra l'output fornito e l'output effettivo.- Per le risposte più lunghe di 20 parole applicheremo una penalità che crescerà proporzionalmente al numero di parole fino ad un massimo di 0,5.
from haystack.components.evaluators import SASEvaluatorsas_evaluator = SASEvaluator()
sas_evaluator.warm_up()
def mixed_metric(example, pred, trace=None):
semantic_similarity = sas_evaluator.run(ground_truth_answers=(example.answer), predicted_answers=(pred.answer))("score")
n_words=len(pred.answer.split())
long_answer_penalty=0
if 20<n_words<40:
long_answer_penalty = 0.025 * (n_words - 20)
elif n_words>=40:
long_answer_penalty = 0.5
return semantic_similarity - long_answer_penalty
Il nostro set di dati di valutazione è composto da 20 esempi di formazione e 50 esempi nel set di sviluppo.
Se valutiamo l'attuale pipeline RAG ingenua con il codice seguente, otteniamo un punteggio medio di 0,49.
Guardare alcuni esempi può darci qualche intuizione su cosa sta facendo la partitura:
Domanda: L’aumento del tempo dalla chemioradioterapia neoadiuvante alla chirurgia è associato a tassi di risposta patologica completa più elevati nel cancro esofageo?
Risposta prevista: Sì, l'aumento del tempo dalla chemioradioterapia neoadiuvante alla chirurgia è associato a tassi di risposta patologica completa più elevati nel cancro esofageo.
Punteggio: 0,78
Ma
Domanda: La localizzazione del focus epilettico basata sulle registrazioni MEG interictali in stato di riposo è fattibile indipendentemente dalla presenza o assenza di picchi?
Risposta prevista: sì.
Punteggio: 0,089
Come possiamo vedere dagli esempi, se la risposta è troppo breve, ottiene un punteggio basso perché la sua somiglianza con la risposta basata sulla verità diminuisce.
Compiliamo quindi la pipeline RAG con DSPy:
from dspy.teleprompt import BootstrapFewShotoptimizer = BootstrapFewShot(metric=mixed_metric)
compiled_rag = optimizer.compile(RAG(), trainset=trainset)
Dopo averlo fatto e rivalutato la pipeline compilata, il punteggio ora è 0,69!
Ora è il momento di ottenere il prompt ottimizzato finale e di aggiungerlo alla nostra pipeline Haystack.
Possiamo vedere gli esempi di pochi scatti selezionati da DSPy guardando il file demos
campo nel compiled_rag
oggetto:
compiled_rag.predictors()(0).demos
Nel prompt finale vengono forniti due tipi di esempi: esempi con pochi scatti e demo bootstrap, come nel prompt mostrato sopra. Gli esempi con pochi scatti sono coppie domanda-risposta:
Example({'question': 'Does increased Syk phosphorylation lead to overexpression of TRAF6 in peripheral B cells of patients with systemic lupus erythematosus?', 'answer': 'Our results suggest that the activated Syk-mediated TRAF6 pathway leads to aberrant activation of B cells in SLE, and also highlight Syk as a potential target for B-cell-mediated processes in SLE.'})
Considerando che la demo bootstrap ha la traccia completa del LLM, incluso il contesto e il ragionamento fornito (nel file rationale
campo sottostante):
Example({'augmented': True, 'context': ('Chronic rhinosinusitis (CRS) …', 'Allergic airway …', 'The mechanisms and ….'), 'question': 'Are group 2 innate lymphoid cells ( ILC2s ) increased in chronic rhinosinusitis with nasal polyps or eosinophilia?', 'rationale': 'produce the answer. We need to consider the findings from the study mentioned in the context, which showed that ILC2 frequencies were associated with the presence of nasal polyps, high tissue eosinophilia, and eosinophil-dominant CRS.', 'answer': 'Yes, ILC2s are increased in chronic rhinosinusitis with nasal polyps or eosinophilia.'})
Tutto quello che dobbiamo fare ora è estrarre questi esempi trovati da DSPy e inserirli nella nostra pipeline Haystack:
static_prompt = lm.inspect_history(n=1).rpartition("---\n")(0)
La nostra nuova pipeline diventa:
from haystack.components.retrievers.in_memory import InMemoryBM25Retriever
from haystack.components.generators import OpenAIGenerator
from haystack.components.builders import PromptBuilder, AnswerBuilder
from haystack import Pipelinetemplate = static_prompt+"""
---
Context:
{% for document in documents %}
«{{ document.content }}»
{% endfor %}
Question: {{question}}
Reasoning: Let's think step by step in order to
"""
new_prompt_builder = PromptBuilder(template=template)
new_retriever = InMemoryBM25Retriever(document_store, top_k=3)
new_generator = OpenAIGenerator(model="gpt-3.5-turbo")
answer_builder = AnswerBuilder(pattern="Answer: (.*)")
optimized_rag_pipeline = Pipeline()
optimized_rag_pipeline.add_component("retriever", new_retriever)
optimized_rag_pipeline.add_component("prompt_builder", new_prompt_builder)
optimized_rag_pipeline.add_component("llm", new_generator)
optimized_rag_pipeline.add_component("answer_builder", answer_builder)
optimized_rag_pipeline.connect("retriever", "prompt_builder.documents")
optimized_rag_pipeline.connect("prompt_builder", "llm")
optimized_rag_pipeline.connect("llm.replies", "answer_builder.replies")
Controlliamo la stessa domanda che abbiamo provato prima.
Domanda:
Quali effetti ha la ketamina sulle cellule staminali neurali dei ratti?
Prima:
La ketamina inibisce la proliferazione delle cellule staminali neurali del ratto in modo dose-dipendente a concentrazioni di 200, 500, 800 e 1000μM. Inoltre, la ketamina diminuisce la concentrazione intracellulare di Ca(2+), sopprime l'attivazione della proteina chinasi C-α (PKCα) e la fosforilazione delle chinasi extracellulari segnale-regolate 1/2 (ERK1/2) nelle cellule staminali neurali del ratto. Questi effetti non sembrano essere mediati dall’apoptosi dipendente dalla caspasi-3.
Dopo:
La ketamina a concentrazioni più elevate inibisce la proliferazione delle cellule staminali neurali del ratto, senza influenzare l’apoptosi. Inoltre, diminuisce la concentrazione di calcio intracellulare e sopprime l’attivazione di PKCα e la fosforilazione di ERK1/2 in queste cellule.
Lavoro fatto!
Alcune parole di conclusione
In questo post, abbiamo utilizzato DSPy per ottimizzare il prompt utilizzato in una pipeline RAG Haystack. Lo abbiamo fatto utilizzando una metrica personalizzata basata sul quadro di valutazione di Haystack che penalizzava il LLM per le risposte lunghe mantenendo alta la somiglianza con la risposta corretta. Con questo approccio, siamo riusciti a migliorare le nostre prestazioni di quasi il 40% senza dover eseguire alcuna attività di ingegneria manuale.
Fonte: towardsdatascience.com