Valuta i RAG rigorosamente o perisci |  di Jarek Grygolec, Ph.D.  |  Aprile 2024

 | Intelligenza-Artificiale

I risultati presentati nella Tabella 1 sembrano molto allettanti, almeno a me. IL semplice l'evoluzione funziona molto bene. Nel caso dell'evoluzione del ragionamento la prima parte della domanda riceve una risposta perfetta, ma la seconda parte rimane senza risposta. Osservando la pagina di Wikipedia (3) è evidente che non c'è risposta alla seconda parte della domanda nel documento vero e proprio, per cui può essere interpretata anche come astensione dalle allucinazioni, cosa di per sé positiva. IL multi-contesto la coppia domanda-risposta sembra molto buona. Il tipo di evoluzione condizionale è accettabile se guardiamo la coppia domanda-risposta. Un modo di guardare a questi risultati è che c’è sempre spazio per una migliore ingegneria tempestiva che sta dietro le evoluzioni. Un altro modo è utilizzare LLM migliori, soprattutto per il ruolo critico come è predefinito nella libreria ragas.

Metrica

La libreria ragas è in grado non solo di generare set di valutazione sintetici, ma ci fornisce anche metriche integrate per la valutazione a livello di componente e per la valutazione end-to-end dei RAG.

Immagine 2: Metriche di valutazione RAG in RAGAS. Immagine creata dall'autore in draw.io.

Al momento della stesura di questo documento RAGAS fornisce otto parametri pronti all'uso per la valutazione RAG, vedere l'immagine 2, e probabilmente ne verranno aggiunti di nuovi in ​​futuro. In generale stai per scegliere le metriche più adatte al tuo caso d'uso. Tuttavia, consiglio di selezionare la metrica più importante, ovvero:

Risposta Correttezza — la metrica end-to-end con punteggi compresi tra 0 e 1, più alto è, migliore è, che misura l'accuratezza della risposta generata rispetto alla verità di base.

Concentrarsi su una metrica end-to-end aiuta ad avviare l'ottimizzazione del sistema RAG il più rapidamente possibile. Una volta ottenuti alcuni miglioramenti nella qualità, puoi esaminare le metriche per componente, concentrandoti su quella più importante per ciascun componente RAG:

Fedeltà — la metrica di generazione con punteggi compresi tra 0 e 1, più alto è, migliore è, che misura la coerenza fattuale della risposta generata rispetto al contesto fornito. Si tratta di radicare il più possibile la risposta generata nel contesto fornito e, così facendo, prevenire le allucinazioni.

Rilevanza del contesto — la metrica di recupero con punteggi compresi tra 0 e 1, più alto è, meglio è, misurando la pertinenza del contesto recuperato rispetto alla domanda.

La fabbrica RAG

OK, quindi abbiamo un RAG pronto per l'ottimizzazione… non così velocemente, questo non è sufficiente. Per ottimizzare RAG abbiamo bisogno della funzione factory per generare catene RAG con un determinato set di iperparametri RAG. Qui definiamo questa funzione di fabbrica in 2 passaggi:

Passo 1: Una funzione per archiviare documenti nel database vettoriale.

# Defining a function to get document collection from vector db with given hyperparemeters
# The function embeds the documents only if collection is missing
# This development version as for production one would rather implement document level check
def get_vectordb_collection(chroma_client,
documents,
embedding_model="text-embedding-ada-002",
chunk_size=None, overlap_size=0) -> ChromaCollection:

if chunk_size is None:
collection_name = "full_text"
docs_pp = documents
else:
collection_name = f"{embedding_model}_chunk{chunk_size}_overlap{overlap_size}"

text_splitter = CharacterTextSplitter(
separator=".",
chunk_size=chunk_size,
chunk_overlap=overlap_size,
length_function=len,
is_separator_regex=False,
)

docs_pp = text_splitter.transform_documents(documents)

embedding = OpenAIEmbeddings(model=embedding_model)

langchain_chroma = Chroma(client=chroma_client,
collection_name=collection_name,
embedding_function=embedding,
)

existing_collections = (collection.name for collection in chroma_client.list_collections())

if chroma_client.get_collection(collection_name).count() == 0:
langchain_chroma.from_documents(collection_name=collection_name,
documents=docs_pp,
embedding=embedding)
return langchain_chroma

Passo 2: Una funzione per generare RAG in LangChain con raccolta di documenti o l'apposita funzione RAG factory.

# Defininig a function to get a simple RAG as Langchain chain with given hyperparemeters
# RAG returns also the context documents retrieved for evaluation purposes in RAGAs

def get_chain(chroma_client,
documents,
embedding_model="text-embedding-ada-002",
llm_model="gpt-3.5-turbo",
chunk_size=None,
overlap_size=0,
top_k=4,
lambda_mult=0.25) -> RunnableSequence:

vectordb_collection = get_vectordb_collection(chroma_client=chroma_client,
documents=documents,
embedding_model=embedding_model,
chunk_size=chunk_size,
overlap_size=overlap_size)

retriever = vectordb_collection.as_retriever(top_k=top_k, lambda_mult=lambda_mult)

template = """Answer the question based only on the following context.
If the context doesn't contain entities present in the question say you don't know.

{context}

Question: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
llm = ChatOpenAI(model=llm_model)

def format_docs(docs):
return "\n\n".join((doc.page_content for doc in docs))

chain_from_docs = (
RunnablePassthrough.assign(context=(lambda x: format_docs(x("context"))))
| prompt
| llm
| StrOutputParser()
)

chain_with_context_and_ground_truth = RunnableParallel(
context=itemgetter("question") | retriever,
question=itemgetter("question"),
ground_truth=itemgetter("ground_truth"),
).assign(answer=chain_from_docs)

return chain_with_context_and_ground_truth

La prima funzione get_vettoredb_collection è incorporato in quest'ultimo funzione get_chainche genera la nostra catena RAG per un dato insieme di parametri, ovvero: embedding_model, llm_model, Chunk_size,overlay_size, top_k, lambda_mult. Con la nostra funzione di fabbrica stiamo solo grattando la superficie delle possibilità quali iperparmetri del nostro sistema RAG ottimizziamo. Nota anche che la catena RAG richiederà 2 argomenti: domanda E realtà di basedove quest'ultimo viene semplicemente fatto passare attraverso la catena RAG poiché è richiesto per la valutazione utilizzando i RAGA.

# Setting up a ChromaDB client
chroma_client = chromadb.EphemeralClient()

# Testing full text rag

with warnings.catch_warnings():
rag_prototype = get_chain(chroma_client=chroma_client,
documents=news,
chunk_size=1000,
overlap_size=200)

rag_prototype.invoke({"question": 'What happened in Minneapolis to the bridge?',
"ground_truth": "x"})("answer")

Valutazione RAG

Per valutare il nostro RAG utilizzeremo il diverso set di dati di articoli di notizie della CNN e del Daily Mail, disponibile su Volto che abbraccia (4). La maggior parte degli articoli in questo set di dati contiene meno di 1000 parole. Inoltre utilizzeremo il minuscolo estratto dal set di dati di soli 100 articoli di notizie. Tutto questo viene fatto per limitare i costi e il tempo necessari per eseguire la demo.

# Getting the tiny extract of CCN Daily Mail dataset
synthetic_evaluation_set_url = "https://gist.github.com/gox6/0858a1ae2d6e3642aa132674650f9c76/raw/synthetic-evaluation-set-cnn-daily-mail.csv"
synthetic_evaluation_set_pl = pl.read_csv(synthetic_evaluation_set_url, separator=",").drop("index")
# Train/test split
# We need at least 2 sets: train and test for RAG optimization.

shuffled = synthetic_evaluation_set_pl.sample(fraction=1,
shuffle=True,
seed=6)
test_fraction = 0.5

test_n = round(len(synthetic_evaluation_set_pl) * test_fraction)
train, test = (shuffled.head(-test_n),
shuffled.head( test_n))

Poiché prenderemo in considerazione molti prototipi RAG diversi oltre a quello definito sopra, abbiamo bisogno di una funzione per raccogliere le risposte generate dal RAG sul nostro set di valutazione sintetico:

# We create the helper function to generate the RAG ansers together with Ground Truth based on synthetic evaluation set
# The dataset for RAGAS evaluation should contain the columns: question, answer, ground_truth, contexts
# RAGAs expects the data in Huggingface Dataset format

def generate_rag_answers_for_synthetic_questions(chain,
synthetic_evaluation_set) -> pl.DataFrame:

df = pl.DataFrame()

for row in synthetic_evaluation_set.iter_rows(named=True):
rag_output = chain.invoke({"question": row("question"),
"ground_truth": row("ground_truth")})
rag_output("contexts") = (doc.page_content for doc
in rag_output("context"))
del rag_output("context")
rag_output_pp = {k: (v) for k, v in rag_output.items()}
df = pl.concat((df, pl.DataFrame(rag_output_pp)), how="vertical")

return df

Ottimizzazione RAG con RAGA e Optuna

Innanzitutto, vale la pena sottolineare che la corretta ottimizzazione del sistema RAG dovrebbe comportare un’ottimizzazione globale, in cui tutti i parametri sono ottimizzati contemporaneamente, in contrasto con l’approccio sequenziale o greedy, in cui i parametri sono ottimizzati uno per uno. L'approccio sequenziale ignora il fatto che possono esserci interazioni tra i parametri, che possono risultare in una soluzione non ottimale.

Ora finalmente siamo pronti per ottimizzare il nostro sistema RAG. Utilizzeremo il framework di ottimizzazione degli iperparametri Optare. A tal fine definiamo la funzione obiettivo per lo studio di Optuna specificando lo spazio iperparametrico consentito e calcolando la metrica di valutazione, vedere il codice seguente:

def objective(trial):

embedding_model = trial.suggest_categorical(name="embedding_model",
choices=("text-embedding-ada-002", 'text-embedding-3-small'))

chunk_size = trial.suggest_int(name="chunk_size",
low=500,
high=1000,
step=100)

overlap_size = trial.suggest_int(name="overlap_size",
low=100,
high=400,
step=50)

top_k = trial.suggest_int(name="top_k",
low=1,
high=10,
step=1)

challenger_chain = get_chain(chroma_client,
news,
embedding_model=embedding_model,
llm_model="gpt-3.5-turbo",
chunk_size=chunk_size,
overlap_size= overlap_size ,
top_k=top_k,
lambda_mult=0.25)

challenger_answers_pl = generate_rag_answers_for_synthetic_questions(challenger_chain , train)
challenger_answers_hf = Dataset.from_pandas(challenger_answers_pl.to_pandas())

challenger_result = evaluate(challenger_answers_hf,
metrics=(answer_correctness),
)

return challenger_result('answer_correctness')

Infine, avendo la funzione obiettivo, definiamo ed eseguiamo lo studio per ottimizzare il nostro sistema RAG in Optuna. Vale la pena notare che con questo metodo possiamo aggiungere allo studio le nostre ipotesi plausibili sugli iperparametri enqueue_trialnonché limitare lo studio in base al tempo o al numero di prove, vedere il I documenti di Optuna per ulteriori suggerimenti.

sampler = optuna.samplers.TPESampler(seed=6)
study = optuna.create_study(study_name="RAG Optimisation",
direction="maximize",
sampler=sampler)
study.set_metric_names(('answer_correctness'))

educated_guess = {"embedding_model": "text-embedding-3-small",
"chunk_size": 1000,
"overlap_size": 200,
"top_k": 3}

study.enqueue_trial(educated_guess)

print(f"Sampler is {study.sampler.__class__.__name__}")
study.optimize(objective, timeout=180)

Nel nostro studio l'ipotesi plausibile non è stata confermata, ma sono sicuro che con un approccio rigoroso come quello proposto sopra le cose miglioreranno.

Best trial with answer_correctness: 0.700130617593832
Hyper-parameters for the best trial: {'embedding_model': 'text-embedding-ada-002', 'chunk_size': 700, 'overlap_size': 400, 'top_k': 9}

Limitazioni dei RAGA

Dopo aver sperimentato la libreria raga per sintetizzare i set di valutazioni e valutare i RAG, ho alcuni avvertimenti:

  • La domanda può contenere la risposta.
  • La verità fondamentale è solo l’estratto letterale del documento.
  • Problemi con RateLimitError e overflow di rete su Colab.
  • Le evoluzioni integrate sono poche e non esiste un modo semplice per aggiungerne di nuove.
  • C'è spazio per miglioramenti nella documentazione.

I primi 2 avvertimenti sono legati alla qualità. La causa principale potrebbe risiedere nel LLM utilizzato e ovviamente GPT-4 fornisce risultati migliori rispetto a GPT-3.5-Turbo. Allo stesso tempo sembra che ciò potrebbe essere migliorato mediante una tempestiva ingegneria delle evoluzioni utilizzata per generare set di valutazione sintetici.

Per quanto riguarda i problemi relativi alla limitazione della velocità e agli overflow della rete, è consigliabile utilizzare: 1) checkpoint durante la generazione di set di valutazione sintetici per prevenire la perdita dei dati creati e 2) backoff esponenziale per assicurarsi di completare l'intera attività.

Infine, cosa più importante, ulteriori evoluzioni integrate sarebbero un'aggiunta gradita al pacchetto ragas. Per non parlare della possibilità di creare più facilmente evoluzioni personalizzate.

Altre caratteristiche utili dei RAGA

  • Suggerimenti personalizzati. Il pacchetto ragas fornisce la possibilità di modificare i prompt utilizzati nelle astrazioni fornite. Viene descritto l'esempio di richieste personalizzate per le metriche nell'attività di valutazione nei documenti. Di seguito utilizzo istruzioni personalizzate per modificare le evoluzioni e mitigare i problemi di qualità.
  • Adattamento automatico della lingua. RAGAs ti copre per le lingue diverse dall'inglese. Ha un'ottima funzionalità chiamata adattamento automatico della lingua che supporta la valutazione RAG in lingue diverse dall'inglese, consulta la documentazione per maggiori informazioni.

Conclusioni

Nonostante le limitazioni dei RAGA NON manca la cosa più importante:

RAGAs è già uno strumento molto utile nonostante la sua giovane età. Consente la generazione di un set di valutazione sintetico per una rigorosa valutazione RAG, un aspetto critico per uno sviluppo RAG di successo.

Fonte: towardsdatascience.com

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *