Padronare le ricerche su Arxiv una guida fai-da-te per costruire un chatbot di domande e risposte con Haystack

Come diventare un esperto di ricerca su Arxiv una guida pratica per costruire un chatbot di domande e risposte con Haystack

Introduzione

La domanda e risposta sui dati personalizzati è uno dei casi d’uso più ricercati dei modelli di linguaggio. Le abilità conversazionali simili a quelle umane dei modelli di linguaggio combinati con i metodi di recupero dei vettori rendono molto più facile estrarre risposte da documenti ampi. Con alcune variazioni, possiamo creare sistemi per interagire con qualsiasi tipo di dati (strutturati, non strutturati e semi-strutturati) archiviati come embedding in un database di vettori. Questo metodo di arricchire i modelli di linguaggio con dati recuperati in base a punteggi di similarità tra l’embedding della query e gli embedding del documento viene chiamato RAG o Retrieval Augmented Generation. Questo metodo può semplificare molte cose, come la lettura di articoli arXiv.

Se ti occupi di IA e Informatica, sicuramente hai sentito parlare almeno una volta di “arXiv”. ArXiv è un archivio di accesso aperto per preprint e postprint elettronici. Ospita articoli verificati ma non sottoposti a revisione paritaria su vari argomenti, come ML, IA, Matematica, Fisica, Statistica, elettronica, ecc. ArXiv ha svolto un ruolo fondamentale nel promuovere la ricerca aperta in IA e nelle scienze dure. Ma leggere i paper di ricerca è spesso faticoso e richiede molto tempo. Quindi, possiamo migliorare un po’ la situazione utilizzando un chatbot RAG che ci consente di estrarre contenuto rilevante dal paper e ottenere risposte utili?

In questo articolo, creeremo un chatbot RAG per i paper arXiv utilizzando uno strumento open source chiamato Haystack.

Obiettivi di apprendimento

  • Comprendere che cos’è Haystack e i suoi componenti per la creazione di applicazioni basate su modelli di linguaggio.
  • Creare un componente per recuperare articoli arXiv utilizzando la libreria “arxiv”.
  • Imparare come creare pipeline di indicizzazione e query con i nodi Haystack.
  • Imparare a creare un’interfaccia di chat con Gradio, coordinare le pipeline per recuperare documenti da un vector store e generare risposte da un modello di linguaggio.

Questo articolo è stato pubblicato come parte del Data Science Blogathon.

Cos’è Haystack?

Haystack è un framework NLP open source tutto in uno per la creazione di applicazioni scalabili basate su modelli di linguaggio. Haystack offre un approccio altamente modulare e personalizzabile per la creazione di applicazioni NLP pronte per la produzione, come la ricerca semantica, la domanda e risposta, il RAG, ecc. È costruito intorno al concetto di pipeline e nodi; le pipeline offrono un approccio molto strutturato per organizzare i nodi per creare applicazioni NLP efficienti.

  • Nodi: I nodi sono i mattoni fondamentali di Haystack. Un nodo svolge una sola funzione, come la pre-elaborazione dei documenti, il recupero dal vector store, la generazione di risposte dai modelli di linguaggio, ecc.
  • Pipeline: La pipeline aiuta a collegare un nodo ad un altro per creare una catena di nodi. Questo rende più facile la creazione di applicazioni con Haystack.

Haystack supporta anche fuori dalla scatola i principali vector store, come Weaviate, Milvus, Elastic Search, Qdrant, ecc. Consulta il repository pubblico di Haystack per ulteriori informazioni: https://github.com/deepset-ai/haystack.

Quindi, in questo articolo, utilizzeremo Haystack per creare un chatbot Q&A per i paper arXiv con un’interfaccia Gradio.

Gradio

Gradio è una soluzione open source di Huggingface per creare e condividere una demo di qualsiasi applicazione di machine learning. È alimentato da Fastapi sul backend e svelte per i componenti front-end. Ci consente di creare app web personalizzabili con Python. Ideale per la creazione e la condivisione di app demo per modelli di apprendimento automatico o prove di concetto. Per ulteriori informazioni, visita il GitHub ufficiale di Gradio. Per saperne di più sulla creazione di applicazioni con Gradio, consulta questo articolo, “Creiamo una chat GPT con Gradio.”

Costruire il chatbot

Prima di costruire l’applicazione, tracciamo brevemente il flusso di lavoro. Inizia con un utente che fornisce l’ID del documento Arxiv e termina con la ricezione delle risposte alle query. Quindi, ecco un semplice flusso di lavoro del nostro chatbot Arxiv.

Abbiamo due pipeline: la pipeline di indicizzazione e la pipeline di query. Quando un utente inserisce un ID di articolo Arxiv, va al componente Arxiv, che recupera e scarica il documento corrispondente in una directory specificata e avvia la pipeline di indicizzazione. La pipeline di indicizzazione è composta da quattro nodi, ognuno responsabile di completare un singolo compito. Quindi, vediamo cosa fanno questi nodi.

Pipeline di indicizzazione

In una pipeline di Haystack, l’output del nodo precedente viene utilizzato come input del nodo corrente. In una pipeline di indicizzazione, l’input iniziale è il percorso del documento.

  • PDFToTextConverter: la libreria Arxiv ci consente di scaricare documenti in formato PDF. Ma abbiamo bisogno dei dati in formato testo. Quindi, questo nodo estrae i testi dal PDF.
  • Preprocessor: i dati estratti devono essere puliti e elaborati prima di essere archiviati nel database di vettori. Questo nodo è responsabile della pulizia e divisione dei testi.
  • EmbeddingRetriver: questo nodo definisce il negozio di vettori in cui devono essere archiviati i dati e il modello di embedding utilizzato per ottenere gli embedding.
  • InMemoryDocumentStore: questo è il negozio di vettori in cui vengono archiviati gli embedding. In questo caso, abbiamo utilizzato il negozio di documenti in memoria predefinito di Haystack. Ma è anche possibile utilizzare altri negozi di vettori, come Qdrant, Weaviate, Elastic Search, Milvus, ecc.

Pipeline di query

La pipeline di query viene attivata quando l’utente invia delle query. La pipeline di query recupera i “k” documenti più vicini agli embedding della query dal negozio di vettori e genera una risposta LLM. Anche qui abbiamo quattro nodi.

  • Retriever: recupera i “k” documenti più vicini agli embedding della query dal negozio di vettori.
  • Sampler: filtra i documenti in base alla probabilità cumulativa dei punteggi di similarità tra la query e i documenti utilizzando il campionamento di top p.
  • LostInTheMiddleRanker: questo algoritmo riordina i documenti estratti. Posiziona i documenti più rilevanti all’inizio o alla fine del contesto.
  • PromptNode: PromptNode è responsabile della generazione delle risposte alle query dal contesto fornito al LLM.

Questo era riguardo al flusso di lavoro del nostro chatbot Arxiv. Ora, immergiamoci nella parte di codifica.

Configura l’ambiente di sviluppo

Prima di installare qualsiasi dipendenza, creare un ambiente virtuale. Puoi utilizzare Venv e Poetry per creare un ambiente virtuale.

python -m venv my-env-nomefonte bin/activate

Ora, installa le seguenti dipendenze di sviluppo. Per scaricare articoli Arxiv, è necessario installare la libreria Arxiv.

farm-haystackarxivgradio

Ora, importeremo le librerie.

import arxivimport osfrom haystack.document_stores import InMemoryDocumentStorefrom haystack.nodes import (    EmbeddingRetriever,     PreProcessor,     PDFToTextConverter,     PromptNode,     PromptTemplate,     TopPSampler    )from haystack.nodes.ranker import LostInTheMiddleRankerfrom haystack.pipelines import Pipelineimport gradio as gr

Costruzione del componente Arxiv

Questo componente sarà responsabile del download e dell’archiviazione dei file PDF di Arxiv. Quindi, ecco come definiamo il componente.

classe ArxivComponente:    """    Questo componente è responsabile del recupero degli articoli arXiv in base a un ID arXiv.    """    def run(self, arxiv_id: str = None):        """        Recupera e archivia un articolo arXiv per il dato ID arXiv.        Args:            arxiv_id (str): ID arXiv dell'articolo da recuperare.        """        # Imposta il percorso della directory in cui gli articoli arXiv verranno archiviati        dir: str = DIR        # Crea un'istanza del client arXiv        arxiv_client = arxiv.Client()        # Controlla se viene fornito un ID arXiv; se non lo è, genera un errore        if arxiv_id is None:            raise ValueError("Fornisci l'ID arXiv dell'articolo da recuperare.")        # Cerca l'articolo arXiv utilizzando l'ID arXiv fornito        search = arxiv.Search(id_list=[arxiv_id])        response = arxiv_client.results(search)        paper = next(response)  # Ottieni il primo risultato        title = paper.title  # Estrai il titolo dell'articolo        # Controlla se la directory specificata esiste        if os.path.isdir(dir):            # Controlla se il file PDF per l'articolo esiste già            if os.path.isfile(dir + "/" + title + ".pdf"):                return {"file_path": [dir + "/" + title + ".pdf"]}        else:            # Se la directory non esiste, creala            os.mkdir(dir)        # Prova a scaricare il PDF per l'articolo arXiv        try:            paper.download_pdf(dirpath=dir, filename=title + ".pdf")            return {"file_path": [dir + "/" + title + ".pdf"]}        except:            # Se si verificano errori durante il download, genera un ConnectionError            raise ConnectionError(messaggio=f"Si è verificato un errore durante il download del PDF per \                                            l'articolo arXiv con ID: {arxiv_id}")

Il componente sopra inizializza un client Arxiv, quindi recupera l’articolo Arxiv associato all’ID e verifica se è già stato scaricato; restituisce il percorso del PDF o lo scarica nella directory.

Costruzione del pipeline di indicizzazione

Ora, definiremo il pipeline di indicizzazione per elaborare e archiviare i documenti nel nostro database vettoriale.

document_store = InMemoryDocumentStore()embedding_retriever = EmbeddingRetriever(    document_store=document_store,     embedding_model="sentence-transformers/All-MiniLM-L6-V2",     model_format="sentence_transformers",     top_k=10    )def indexing_pipeline(file_path: str = None):    pdf_converter = PDFToTextConverter()    preprocessor = PreProcessor(split_by="word", split_length=250, split_overlap=30)        indexing_pipeline = Pipeline()    indexing_pipeline.add_node(        component=pdf_converter,         name="PDFConverter",         inputs=["File"]        )    indexing_pipeline.add_node(        component=preprocessor,         name="PreProcessor",         inputs=["PDFConverter"]        )    indexing_pipeline.add_node(        component=embedding_retriever,        name="EmbeddingRetriever",         inputs=["PreProcessor"]        )    indexing_pipeline.add_node(        component=document_store,         name="InMemoryDocumentStore",         inputs=["EmbeddingRetriever"]        )    indexing_pipeline.run(file_paths=file_path)

Prima di tutto, definiamo il nostro archivio di documenti in memoria e quindi il recuperatore dell’incorporamento. Nel recuperatore dell’incorporamento, specificiamo l’archivio di documenti, i modelli di incorporamento e il numero di documenti da recuperare.

Abbiamo anche definito i quattro nodi di cui abbiamo parlato in precedenza. Il convertitore pdf converte il pdf in testo, il preprocessore pulisce e crea frammenti di testo, il retrieval dell’incorporamento crea gli incorporamenti dei documenti e l’InMemoryDocumentStore memorizza gli incorporamenti vettoriali. Il metodo run con il percorso del file attiva il pipeline e ciascun nodo viene eseguito nell’ordine in cui sono stati definiti. Puoi anche notare come ogni nodo utilizzi gli output dei nodi precedenti come input.

Costruzione del pipeline di interrogazione

Il pipeline di interrogazione è composto anche da quattro nodi. Questo è responsabile della creazione dell’incorporamento dal testo interrogato, della ricerca di documenti simili negli archivi vettoriali e infine della generazione di risposte dal LLM.

def query_pipeline(query: str = None):    if not query:        raise gr.Error("Fornire una query.")    prompt_text = """Sintetizza una risposta esaustiva dai paragrafi forniti di un articolo Arxiv e dalla domanda data.\nConcentrati sulla domanda e evita informazioni superflue nella tua risposta.\n\n\n Paragrafi: {join(documents)} \n\n Domanda: {query} \n\n Risposta:"""    prompt_node = PromptNode(                         "gpt-3.5-turbo",                          default_prompt_template=PromptTemplate(prompt_text),                          api_key="api-key",                          max_length=768,                          model_kwargs={"stream": False},                         )    query_pipeline = Pipeline()    query_pipeline.add_node(        component = embedding_retriever,         name = "Retriever",         inputs=["Query"]        )    query_pipeline.add_node(        component=TopPSampler(        top_p=0.90),         name="Sampler",         inputs=["Retriever"]        )    query_pipeline.add_node(        component=LostInTheMiddleRanker(1024),         name="LostInTheMiddleRanker",         inputs=["Sampler"]        )    query_pipeline.add_node(        component=prompt_node,         name="Prompt",         inputs=["LostInTheMiddleRanker"]        )    pipeline_obj = query_pipeline.run(query = query)        return pipeline_obj["results"]

L’embedding_retriever recupera “k” documenti simili dalla memoria vettoriale. Il Sampler è responsabile del campionamento dei documenti. Il LostInTheMiddleRanker classifica i documenti all’inizio o alla fine del contesto in base alla loro rilevanza. Infine, il prompt_node, in cui il LLM è “gpt-3.5-turbo”. Abbiamo anche aggiunto un modello di prompt per aggiungere più contesto alla conversazione. Il metodo run restituisce un oggetto di pipeline, un dizionario.

Questo era il nostro backend. Ora, progettiamo l’interfaccia.

Interfaccia Gradio

Questo ha una classe Blocks per creare un’interfaccia web personalizzabile. Quindi, per questo progetto, abbiamo bisogno di una casella di testo che prenda l’ID Arxiv come input dell’utente, un’interfaccia di chat e una casella di testo che prenda le query dell’utente. Ecco come possiamo farlo.

with gr.Blocks() as demo:    with gr.Row():        with gr.Column(scale=60):            text_box = gr.Textbox(placeholder="Input Arxiv ID",                                   interactive=True).style(container=False)        with gr.Column(scale=40):            submit_id_btn = gr.Button(value="Submit")    with gr.Row():        chatbot = gr.Chatbot(value=[]).style(height=600)        with gr.Row():        with gr.Column(scale=70):            query = gr.Textbox(placeholder = "Enter query string",                                interactive=True).style(container=False)

Esegui il comando gradio app.py dalla tua linea di comando e visita l’URL localhost visualizzato.

Ora dobbiamo definire gli eventi di trigger.

submit_id_btn.click(        fn = embed_arxiv,         inputs=[text_box],        outputs=[text_box],        )query.submit(            fn=add_text,             inputs=[chatbot, query],             outputs=[chatbot, ],             queue=False            ).success(            fn=get_response,            inputs = [chatbot, query],            outputs = [chatbot,]            )demo.queue()demo.launch()

Per far funzionare gli eventi, abbiamo bisogno di definire le funzioni menzionate in ciascun evento. Fai clic su submit_id_btn, invia l’input dalla casella di testo come parametro alla funzione embed_arxiv. Questa funzione coordinerà il recupero e l’archiviazione del PDF di Arxiv nel vettore di archiviazione.

arxiv_obj = ArxivComponent()def embed_arxiv(arxiv_id: str):    """        Args:            arxiv_id: ID di Arxiv dell'articolo da recuperare.                   """    global FILE_PATH    dir: str = DIR       file_path: str = None    if not arxiv_id:        raise gr.Error("Fornisci un ID di Arxiv")    file_path_dict = arxiv_obj.run(arxiv_id)    file_path = file_path_dict["file_path"]    FILE_PATH = file_path    indexing_pipeline(file_path=file_path)    return"File incorporato con successo"

Abbiamo definito un oggetto ArxivComponent e la funzione embed_arxiv. Esegue il metodo “run” e utilizza il percorso del file restituito come parametro per la Pipeline di indicizzazione.

Ora, passiamo all’evento di invio con la funzione add_text come parametro. Questa è responsabile per la visualizzazione della chat nell’interfaccia di chat.

def add_text(history, text: str):    if not text:         raise gr.Error('inserisci il testo')    history = history + [(text,'')]     return history

Ora, definiamo la funzione get_response, che recupera e trasmette le risposte LLM nell’interfaccia di chat.

def get_response(history, query: str):    if not query:        gr.Error("Fornisci una query.")        response = query_pipeline(query=query)    for text in response[0]:        history[-1][1] += text        yield history, ""

Questa funzione prende la stringa di query e la passa alla Pipeline di query per ottenere una risposta. Infine, iteriamo sulla stringa di risposta e la restituiamo al chatbot.

Mettilo tutto insieme.

# Crea un'istanza della classe ArxivComponentarxiv_obj = ArxivComponent()def embed_arxiv(arxiv_id: str):    """    Recupera e incorpora un articolo arXiv per l'ID arXiv specificato.    Args:        arxiv_id (str): ID arXiv dell'articolo da recuperare.    """    # Accedi alla variabile globale FILE_PATH    global FILE_PATH        # Imposta la directory in cui vengono archiviati gli articoli arXiv    dir: str = DIR        # Inizializza il file_path a None    file_path: str = None        # Controlla se è stato fornito l'ID arXiv    if not arxiv_id:        raise gr.Error("Fornisci un ID di Arxiv")        # Chiama il metodo "run" di ArxivComponent per recuperare e archiviare l'articolo arXiv    file_path_dict = arxiv_obj.run(arxiv_id)        # Estrai il percorso del file dal dizionario    file_path = file_path_dict["file_path"]        # Aggiorna la variabile globale FILE_PATH    FILE_PATH = file_path        # Chiama la funzione indexing_pipeline per elaborare l'articolo scaricato    indexing_pipeline(file_path=file_path)    return "Articolo incorporato con successo"def get_response(history, query: str):    if not query:        gr.Error("Fornisci una query.")        # Chiama la funzione query_pipeline per elaborare la query dell'utente    response = query_pipeline(query=query)        # Aggiungi la risposta alla cronologia della chat    for text in response[0]:        history[-1][1] += text        yield historydef add_text(history, text: str):    if not text:        raise gr.Error('Inserisci il testo')        # Aggiungi il testo fornito dall'utente alla cronologia della chat    history = history + [(text, '')]    return history# Crea un'interfaccia Gradio usando Blocks       	gr.Blocks() as demo:    with gr.Row():        with gr.Column(scale=60):            # Casella di testo per l'ID di Arxiv            text_box = gr.Textbox(placeholder="Inserisci l'ID di Arxiv",                                   interactive=True).style(container=False)        with gr.Column(scale=40):            # Pulsante per inviare l'ID di Arxiv            submit_id_btn = gr.Button(value="Invia")        with gr.Row():        # Interfaccia del chatbot        chatbot = gr.Chatbot(value=[]).style(height=600)        with gr.Row():        with gr.Column(scale=70):            # Casella di testo per le query dell'utente            query = gr.Textbox(placeholder="Inserisci la stringa di query",                                interactive=True).style(container=False)        # Definisci le azioni per il clic del pulsante e l'invio della query    submit_id_btn.click(        fn=embed_arxiv,         inputs=[text_box],        outputs=[text_box],    )    query.submit(        fn=add_text,         inputs=[chatbot, query],         outputs=[chatbot, ],         queue=False    ).success(        fn=get_response,        inputs=[chatbot, query],        outputs=[chatbot,]    )# Accoda e avvia l'interfacciademo.queue()demo.launch()

Esegui l’applicazione utilizzando il comando gradio app.py e visita l’URL per interagire con l’Arxic Chatbot.

Ecco come apparirà.

Ecco il repository GitHub dell’app sunilkumardash9/chat-arxiv.

Possibili miglioramenti

Abbiamo creato con successo un’applicazione semplice per chattare con qualsiasi paper Arxiv, ma è possibile apportare alcuni miglioramenti.

  • Standalone Vector store: Invece di utilizzare un vector store predefinito, è possibile utilizzare vector store autonomi disponibili con Haystack, come Weaviate, Milvus, ecc. Ciò ti darà non solo maggiore flessibilità, ma anche significativi miglioramenti delle prestazioni.
  • Citazioni: Possiamo aggiungere certezza alle risposte LLM aggiungendo citazioni adeguate.
  • Altre funzionalità: Invece di avere solo un’interfaccia di chat, possiamo aggiungere funzionalità per visualizzare pagine di PDF utilizzati come fonti per le risposte LLM. Dai un’occhiata a questo articolo, “Build a ChatGPT for PDFs with Langchain“, e al repository GitHub di un’applicazione simile.
  • Frontend: Un frontend migliore e più interattivo sarebbe molto meglio.

Conclusione

Quindi, questo era tutto su come costruire un’app di chat per i paper Arxiv. Questa applicazione non è limitata solo a Arxiv. Possiamo estenderla anche ad altri siti, come PubMed. Con alcune modifiche, possiamo utilizzare un’architettura simile per chattare con qualsiasi sito web. Quindi, in questo articolo, abbiamo creato un componente Arxiv per scaricare i paper Arxiv, li abbiamo incorporati utilizzando le pipeline di haystack e infine abbiamo recuperato le risposte dal LLM.

Punti chiave

  • Haystack è una soluzione open source per la creazione di applicazioni NLP scalabili e pronte per la produzione.
  • Haystack offre un approccio altamente modulare per la creazione di app reali. Fornisce nodi e pipeline per semplificare il recupero delle informazioni, la preelaborazione dei dati, l’embedding e la generazione delle risposte.
  • È una libreria open source sviluppata da Huggingface per prototipare rapidamente qualsiasi applicazione. Fornisce un modo facile per condividere modelli di ML con chiunque.
  • Utilizza un workflow simile per creare app di chat per altri siti, come PubMed.

Domande frequenti

I media mostrati in questo articolo non appartengono ad Analytics Vidhya e vengono utilizzati a discrezione dell’autore.