Advanced RAG 01 Recupero da Piccolo a Grande

RAG 01 Avanzato Recupero dal Piccolo al Grande

RecursiveRetriever per bambini-genitori e recupero della finestra di frase con LlamaIndex

I sistemi RAG (Retrieval-Augmented Generation) recuperano informazioni rilevanti da una base di conoscenza data, consentendo di generare informazioni di fatto, contestualmente rilevanti e specifiche del dominio. Tuttavia, RAG affronta molte sfide quando si tratta di recuperare informazioni pertinenti ed generare risposte di alta qualità. In questa serie di post / video nel blog, esplorerò tecniche avanzate RAG che mirano ad ottimizzare il flusso di lavoro RAG e affrontare le sfide nei sistemi RAG ingenui.

La prima tecnica si chiama recupero percorso da piccolo a grande. Nelle pipeline RAG di base, incorporiamo un grande frammento di testo per il recupero e questo stesso frammento di testo viene utilizzato per la sintesi. Ma a volte l’incorporazione / il recupero di grandi frammenti di testo può sembrare subottimale. Potrebbe esserci molto testo di riempimento in un grande frammento di testo che nasconde la rappresentazione semantica, portando a un recupero peggiore. E se potessimo incorporare / recuperare in base a frammenti di testo più piccoli e mirati, ma avere comunque un contesto sufficiente per il LLM per sintetizzare una risposta? In particolare, separare i frammenti di testo utilizzati per il recupero rispetto ai frammenti di testo utilizzati per la sintesi potrebbe essere vantaggioso. Utilizzare frammenti di testo più piccoli migliora l’accuratezza del recupero, mentre frammenti di testo più grandi offrono più informazioni contestuali. Il concetto alla base del recupero da piccolo a grande è utilizzare frammenti di testo più piccoli durante il processo di recupero e successivamente fornire il frammento di testo più grande a cui appartiene il testo recuperato al grande modello linguistico.

Ci sono due tecniche primarie:

  1. Frammenti di testo più piccoli che si riferiscono a frammenti di testo più grandi genitori: Recupera prima frammenti più piccoli durante il recupero, quindi fa riferimento agli ID dei genitori e restituisce i frammenti più grandi.
  2. Recupero della finestra di frase: Recupera una singola frase durante il recupero e restituisce una finestra di testo intorno alla frase.

In questo post del blog, ci immergeremo nelle implementazioni di questi due metodi in LlamaIndex. Perché non lo faccio in LangChain? Perché ci sono già molte risorse disponibili sul RAG avanzato con LangChain. Preferisco non duplicare lo sforzo. Inoltre, uso sia LangCha che LlamaIndex. È meglio comprendere più strumenti e usarli in modo flessibile.

Puoi trovare tutto il codice in questo notebook.

Revisione di base RAG

Iniziamo con un’implementazione di base di RAG con 4 semplici passaggi:

Passaggio 1. Caricamento dei documenti

Utilizziamo un PDFReader per caricare un file PDF e combinare ogni pagina del documento in un oggetto Document unico.

loader = PDFReader()docs0 = loader.load_data(file=Path("llama2.pdf"))doc_text = "\n\n".join([d.get_content() for d in docs0])docs = [Document(text=doc_text)]

Passaggio 2. Analisi dei documenti in frammenti di testo (Nodi)

Poi dividiamo il documento in frammenti di testo, chiamati “Nodi” in LlamaIndex, dove definiamo la dimensione del frammento come 1024. Gli ID dei nodi predefiniti sono stringhe di testo casuali, quindi possiamo formattare il nostro ID del nodo in modo da seguire un certo formato.

node_parser = SimpleNodeParser.from_defaults(chunk_size=1024)base_nodes = node_parser.get_nodes_from_documents(docs)for idx, node in enumerate(base_nodes):node.id_ = f"node-{idx}"

Passaggio 3. Selezione del modello di incorporamento e LLM

Dobbiamo definire due modelli:

  • Il modello di incorporamento viene utilizzato per creare incorporamenti vettoriali per ciascuno dei frammenti di testo. Qui stiamo chiamando il modello FlagEmbedding di Hugging Face.
  • LLM: la query dell’utente e i frammenti di testo pertinenti vengono alimentati nel LLM in modo che possa generare risposte con contesto pertinente.

Possiamo raggruppare questi due modelli insieme in ServiceContext e usarli successivamente nelle fasi di indicizzazione e interrogazione.

embed_model = resolve_embed_model(“local:BAAI/bge-small-en”)llm = OpenAI(model="gpt-3.5-turbo")service_context = ServiceContext.from_defaults(llm=llm, embed_model=embed_model)

Passaggio 4. Creazione dell’indice, del recuperalo e del motore di interrogazione

L’indice, il recuperalo e il motore di interrogazione sono tre componenti principali per porre domande sui tuoi dati o documenti:

  • L’indice è una struttura dati che ci consente di recuperare rapidamente informazioni pertinenti per una query dell’utente dai documenti esterni. L’indice del Vector Store prende gli snippet di testo/nodi e crea quindi delle rappresentazioni vettoriali del testo di ogni nodo, pronte per essere interrogate da un LLM.
base_index = VectorStoreIndex(base_nodes, service_context=service_context)
  • Il recuperalo viene utilizzato per recuperare informazioni pertinenti date le query dell’utente.
base_retriever = base_index.as_retriever(similarity_top_k=2)
  • Il motore di interrogazione viene creato sopra l’indice e il recuperalo, fornendo un’interfaccia generica per porre domande sui tuoi dati.
query_engine_base = RetrieverQueryEngine.from_args(    base_retriever, service_context=service_context)response = query_engine_base.query(    "Puoi dirmi quali sono i concetti chiave per il finetuning della sicurezza")print(str(response))

Metodo Avanzato 1: Chunk più piccoli che si riferiscono a Chunk genitori più grandi

Nella sezione precedente, abbiamo utilizzato una dimensione fissa di 1024 per i chunk sia per il recupero che per la sintesi. In questa sezione, andremo a esplorare come utilizzare chunk più piccoli per il recupero e fare riferimento a chunk genitori più grandi per la sintesi. Il primo passo è quello di creare chunk più piccoli:

Passo 1: Creazione di Chunk più Piccoli

Per ciascuno dei chunk di testo di dimensione 1024, creiamo chunk di testo ancora più piccoli:

  • 8 chunk di testo di dimensione 128
  • 4 chunk di testo di dimensione 256
  • 2 chunk di testo di dimensione 512

Aggiungiamo il chunk di testo originale di dimensione 1024 alla lista dei chunk di testo.

sub_chunk_sizes = [128, 256, 512]sub_node_parsers = [    SimpleNodeParser.from_defaults(chunk_size=c) for c in sub_chunk_sizes]all_nodes = []for base_node in base_nodes:    for n in sub_node_parsers:        sub_nodes = n.get_nodes_from_documents([base_node])        sub_inodes = [            IndexNode.from_text_node(sn, base_node.node_id) for sn in sub_nodes        ]        all_nodes.extend(sub_inodes)    # aggiungiamo anche il nodo originale alla lista    original_node = IndexNode.from_text_node(base_node, base_node.node_id)    all_nodes.append(original_node)all_nodes_dict = {n.node_id: n for n in all_nodes}

Quando osserviamo tutti i `all_nodes_dict` dei chunk di testo, possiamo vedere che molti chunk più piccoli sono associati ad ogni chunk di testo originale, ad esempio `node-0`. Infatti, tutti i chunk più piccoli fanno riferimento al chunk più grande nei metadati, con l’indice_id che punta all’ID dell’indice del chunk più grande.

Passo 2: Creazione dell’indice, del recuperalo e del motore di interrogazione

  • Indice: Creazione delle rappresentazioni vettoriali di tutti i chunk di testo.
vector_index_chunk = VectorStoreIndex(    all_nodes, service_context=service_context)
  • Recuperalo: la chiave qui è utilizzare un RecursiveRetriever per attraversare le relazioni dei nodi e recuperare i nodi in base ai “riferimenti”. Questo recuperalo esplorerà in modo ricorsivo i collegamenti dai nodi ad altri recuperali/motori di interrogazione. Per qualsiasi nodo recuperato, se uno dei nodi è un nodo dell’indice, quindi esplorerà il relativo recuperalo/motore di interrogazione collegato e porrà la query a quello.
vector_retriever_chunk = vector_index_chunk.as_retriever(similarity_top_k=2)retriever_chunk = RecursiveRetriever(    "vector",    retriever_dict={"vector": vector_retriever_chunk},    node_dict=all_nodes_dict,    verbose=True,)

Quando facciamo una domanda e recuperiamo i frammenti di testo più rilevanti, in realtà recuperiamo il frammento di testo con l’ID nodo che punta al frammento genitore e quindi recuperiamo il frammento genitore.

  • Ora con gli stessi passaggi di prima, possiamo creare un motore di interrogazione come un’interfaccia generica per fare domande sui nostri dati.
query_engine_chunk = RetrieverQueryEngine.from_args(    retriever_chunk, service_context=service_context)response = query_engine_chunk.query(    "Puoi dirmi quali sono i concetti chiave per il fine-tuning della sicurezza")print(str(response))

Metodo Avanzato 2: Recupero della Finestra di Frasi

Per ottenere un recupero ancora più accurato, invece di utilizzare frammenti di testo più piccoli, possiamo suddividere i documenti in una singola frase per frammento.

In questo caso, le singole frasi saranno simili al concetto di frammento “figlio” menzionato nel metodo 1. La “finestra” di frasi (5 frasi su ogni lato della frase originale) sarà simile al concetto di frammento “genitore”. In altre parole, utilizziamo le singole frasi durante il recupero e inviamo la frase recuperata con la finestra di frasi all’LLM.

Passo 1: Creare il parser dei nodi della finestra di frasi

# crea il parser dei nodi della finestra di frasi con le impostazioni predefinite
node_parser = SentenceWindowNodeParser.from_defaults(    window_size=3,    window_metadata_key="window",    original_text_metadata_key="original_text",)sentence_nodes = node_parser.get_nodes_from_documents(docs)sentence_index = VectorStoreIndex(sentence_nodes, service_context=service_context)

Passo 2: Creare il motore di interrogazione

Quando creiamo il motore di interrogazione, possiamo sostituire la frase con la finestra di frasi utilizzando il MetadataReplacementPostProcessor, in modo che la finestra delle frasi venga inviata all’LLM.

query_engine = sentence_index.as_query_engine(    similarity_top_k=2,    # la chiave target di default è `window` per corrispondere alle impostazioni predefinite del node_parser    node_postprocessors=[        MetadataReplacementPostProcessor(target_metadata_key="window")    ],)window_response = query_engine.query(    "Puoi dirmi quali sono i concetti chiave per il fine-tuning della sicurezza")print(window_response)

Il Recupero della Finestra di Frasi è stato in grado di rispondere alla domanda “Puoi dirmi quali sono i concetti chiave per il fine-tuning della sicurezza”:

Qui puoi vedere la frase effettiva recuperata e la finestra della frase, che fornisce più contesto e dettagli.

Conclusione

In questo blog, abbiamo esplorato come utilizzare il recupero dal piccolo al grande per migliorare RAG, concentrandoci sul RecursiveRetriever Child-Parent e sul Recupero della Finestra di Frasi con LlamaIndex. Nei futuri post del blog, approfondiremo altre tecniche e suggerimenti. Restate sintonizzati per ulteriori informazioni su questa entusiasmante esperienza con le tecniche avanzate di RAG!

Riferimenti:

Di Sophia Yang il 4 novembre 2023

Collegati con me su LinkedIn, Twitter e YouTube e unisciti al DS/ML Book Club ❤️