Migliorare le prestazioni di interrogazione dei file CSV in ChatGPT

Migliorare le prestazioni di interrogazione dei file CSV in ChatGPT' can be condensed to 'Migliorare prestazioni interrogazione file CSV in ChatGPT

con LangChain’s Self-Querying basato su un caricatore CSV personalizzato

L’avvento di modelli linguistici sofisticati, come ChatGPT, ha portato a un approccio nuovo e promettente per l’interrogazione di dati tabulari. Tuttavia, a causa delle limitazioni dei token, l’esecuzione diretta di una query diventa difficile senza l’assistenza di API come il retriever. Di conseguenza, l’accuratezza delle query dipende fortemente dalla qualità della query stessa, e non è raro che i retriever standard non restituiscano le informazioni esatte richieste.

In questo articolo, approfondirò le ragioni del fallimento dei metodi di retrieval convenzionali in alcuni casi d’uso. Inoltre, proponiamo una soluzione rivoluzionaria sotto forma di un caricatore di dati CSV personalizzato che incorpora informazioni di metadati. Sfruttando l’API di Self-Querying di LangChain insieme al nuovo caricatore di dati CSV, possiamo estrarre informazioni con prestazioni e precisione significativamente migliorate.

Per il codice dettagliato utilizzato in questo articolo, si prega di consultare il notebook qui. Vorrei sottolineare il fatto che questo notebook illustra la possibilità di interrogare grandi volumi di dati tabulari con un LLM (Large Language Model) ottenendo un’accuratezza notevole.

Reperimento su Dataset di Popolazione di Malattia

Vorremmo interrogare il seguente dataset sintetico SIR creato dall’autore: simuliamo i tre diversi gruppi della popolazione durante 90 giorni di malattia in 10 città basandoci su un semplice modello SIR. Per semplicità, supponiamo che la popolazione di ogni città sia compresa tra 5e3 e 2e4 e non ci sia movimento di popolazione tra le città. Inoltre, generiamo dieci numeri interi casuali tra 500 e 2000 come numero originale di persone infette.

Immagine dell'autore: Popolazione di malattia in 10 città

La tabella ha la seguente forma con cinque colonne: “time” che indica il momento in cui è stata misurata la popolazione, “city” la città in cui sono state misurate le informazioni e “susceptible”, “infectious” e “removed” i tre gruppi della popolazione. Per semplicità, i dati sono stati salvati localmente come un file CSV.

time susceptible infectious removed city0 2018-01-01 8639 8639 0 city01 2018-01-02 3857 12338 1081 city02 2018-01-03 1458 13414 2405 city03 2018-01-04 545 12983 3749 city04 2018-01-05 214 12046 5017 city0

Vorremmo fare delle domande a ChatGPT pertinenti al dataset. Per consentire a ChatGPT di interagire con tali dati tabulari, seguiamo i seguenti passaggi standard utilizzando LangChain:

  1. Utilizzare il CSVLoader per caricare i dati,
  2. Creare un vectorstore (utilizziamo Chroma qui) per memorizzare i dati incorporati con gli embeddings di OpenAI,
  3. Utilizzare i retriever per restituire i documenti pertinenti a una determinata query non strutturata.

Puoi utilizzare il seguente codice per realizzare i passaggi sopra descritti.

# caricamento dei dati dal percorso locale
loader = CSVLoader(file_path=LOCAL_PATH)
data = loader.load()
# Creazione dell'embedding
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(data, embeddings)
# Creazione del retriever
retriever = vectorstore.as_retriever(search_kwargs={"k": 20})

Possiamo ora definire una ConversationalRetriverChain per interrogare il dataset SIR.

llm = ChatOpenAI(model_name="gpt-4", temperature=0)
# Definizione dei template di messaggio di sistema
system_template = """Il {context} fornito è un dataset tabulare che contiene la popolazione suscettibile, infetta e rimossa durante 90 giorni in 10 città. Il dataset include le seguenti colonne: 'time': momento in cui è stata misurata la popolazione, 'city': città in cui è stata misurata la popolazione, 'susceptible': popolazione suscettibile alla malattia, 'infectious': popolazione infetta dalla malattia, 'removed': popolazione rimossa dalla malattia. ---------------- {context}"""
# Creazione dei template di prompt di chat
messages = [
    SystemMessagePromptTemplate.from_template(system_template),
    HumanMessagePromptTemplate.from_template("{question}")
]
qa_prompt = ChatPromptTemplate.from_messages(messages)
memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)
qa = ConversationalRetrievalChain.from_llm(
    llm=llm,
    retriever=vectorstore.as_retriever(),
    return_source_documents=False,
    combine_docs_chain_kwargs={"prompt": qa_prompt},
    memory=memory,
    verbose=True
)

Nel codice sopra, ho definito una catena di conversazione che cercherà le informazioni rilevanti della query nel dataset SIR utilizzando il recuperatore definito nel passaggio precedente e fornirà una risposta basata sulle informazioni recuperate. Per fornire istruzioni più chiare a ChatGPT, ho anche fornito un prompt che chiarisce la definizione di tutte le colonne nel dataset.

Chiediamoci ora una semplice domanda: “Quale città ha il maggior numero di persone infette il 2018-02-03?”

Sorprendentemente, il nostro chatbot ha detto: “Il dataset fornito non include dati per la data 2018-02-03.”

Come è possibile?

Perché il recupero è fallito?

Per indagare su perché il chatbot non è riuscito a rispondere a una domanda la cui risposta si trova solo nel dataset fornito, ho dato un’occhiata al documento rilevante che ha recuperato con la domanda “Quale città ha il maggior numero di persone infette il 2018-02-03?”. Ho ottenuto le seguenti righe:

[Documento(page_content=': 31\ntime: 2018-02-01\nsusceptible: 0\ninfectious: 1729\nremoved: 35608\ncity: city3', metadata={'source': 'sir.csv', 'row': 301}), Document(page_content=': 1\ntime: 2018-01-02\nsusceptible: 3109\ninfectious: 9118\nremoved: 804\ncity: city8', metadata={'source': 'sir.csv', 'row': 721}), Document(page_content=': 15\ntime: 2018-01-16\nsusceptible: 1\ninfectious: 2035\nremoved: 6507\ncity: city7', metadata={'source': 'sir.csv', 'row': 645}), Document(page_content=': 1\ntime: 2018-01-02\nsusceptible: 3481\ninfectious: 10873\nremoved: 954\ncity: city5', metadata={'source': 'sir.csv', 'row': 451}), Document(page_content=': 23\ntime: 2018-01-24\nsusceptible: 0\ninfectious: 2828\nremoved: 24231\ncity: city9', metadata={'source': 'sir.csv', 'row': 833}), Document(page_content=': 1\ntime: 2018-01-02\nsusceptible: 8081\ninfectious: 25424\nremoved: 2231\ncity: city6', metadata={'source': 'sir.csv', 'row': 541}), Document(page_content=': 3\ntime: 2018-01-04\nsusceptible: 511\ninfectious: 9733\nremoved: 2787\ncity: city8', metadata={'source': 'sir.csv', 'row': 723}), Document(page_content=': 24\ntime: 2018-01-25\nsusceptible: 0\ninfectious: 3510\nremoved: 33826\ncity: city3', metadata={'source': 'sir.csv', 'row': 294}), Document(page_content=': 33\ntime: 2018-02-03\nsusceptible: 0\ninfectious: 1413\nremoved: 35924\ncity: city3', metadata={'source': 'sir.csv', 'row': 303}), Document(page_content=': 25\ntime: 2018-01-26\nsusceptible: 0\ninfectious: 3173\nremoved: 34164\ncity: city3', metadata={'source': 'sir.csv', 'row': 295}), Document(page_content=': 1\ntime: 2018-01-02\nsusceptible: 3857\ninfectious: 12338\nremoved: 1081\ncity: city0', metadata={'source': 'sir.csv', 'row': 1}), Document(page_content=': 23\ntime: 2018-01-24\nsusceptible: 0\ninfectious: 1365\nremoved: 11666\ncity: city8', metadata={'source': 'sir.csv', 'row': 743}), Document(page_content=': 16\ntime: 2018-01-17\nsusceptible: 0\ninfectious: 2770\nremoved: 10260\ncity: city8', metadata={'source': 'sir.csv', 'row': 736}), Document(page_content=': 3\ntime: 2018-01-04\nsusceptible: 487\ninfectious: 6280\nremoved: 1775\ncity: city7', metadata={'source': 'sir.csv', 'row': 633}), Document(page_content=': 14\ntime: 2018-01-15\nsusceptible: 0\ninfectious: 3391\nremoved: 9639\ncity: city8', metadata={'source': 'sir.csv', 'row': 734}), Document(page_content=': 20\ntime: 2018-01-21\nsusceptible: 0\ninfectious: 1849\nremoved: 11182\ncity: city8', metadata={'source': 'sir.csv', 'row': 740}), Document(page_content=': 28\ntime: 2018-01-29\nsusceptible: 0\ninfectious: 1705\nremoved: 25353\ncity: city9', metadata={'source': 'sir.csv', 'row': 838}), Document(page_content=': 23\ntime: 2018-01-24\nsusceptible: 0\ninfectious: 3884\nremoved: 33453\ncity: city3', metadata={'source': 'sir.csv', 'row': 293}), Document(page_content=': 16\ntime: 2018-01-17\nsusceptible: 1\ninfectious: 1839\nremoved: 6703\ncity: city7', metadata={'source': 'sir.csv', 'row': 646}), Document(page_content=': 15\ntime: 2018-01-16\nsusceptible: 1\ninfectious: 6350\nremoved: 20708\ncity: city9', metadata={'source': 'sir.csv', 'row': 825})]

Sorprendentemente, anche se ho specificato che volevo sapere cosa è successo nella data 2018-02-03, nessuna riga di quella data è stata restituita. Dato che nessuna informazione su quella data è mai stata inviata a ChatGPT, non c’è ovviamente dubbio che non possa rispondere a una domanda del genere.

Approfondendo nel codice sorgente del retriever, possiamo vedere che la chiamata a get_relevant_documents richiama similarity_search di default. Il metodo restituisce i primi n frammenti (4 di default, ma ho impostato il numero a 20 nel mio codice) in base alla metrica di distanza calcolata (distanza coseno di default) che misura la “similarità” tra il vettore della query e il vettore dei frammenti del documento.

Tornando al dataset SIR, notiamo che ogni riga racconta praticamente la stessa storia: in quale data, in quale città e quante persone sono segnate come quale gruppo. Non sorprende che i vettori che rappresentano queste righe siano simili tra loro. Un rapido controllo del punteggio di similarità ci dà il fatto che molte righe finiscono con un punteggio intorno a 0,29.

Pertanto, un punteggio di similarità non è abbastanza forte per distinguere le righe in base a quanto sono rilevanti per la query: questo è sempre il caso dei dati tabulari in cui le righe non hanno differenze significative tra loro. Abbiamo bisogno di filtri più robusti che possano lavorare sui metadati.

CSVLoader con Metadati Personalizzati

Sembra evidente che il miglioramento delle prestazioni del chatbot dipenda in gran parte dall’efficienza del retriever. Per fare ciò, iniziamo definendo un CSVLoader personalizzato che può comunicare le informazioni sui metadati con il retriever.

Scriviamo il seguente codice:

class MetaDataCSVLoader(BaseLoader):    """Carica un file CSV in una lista di documenti.    Ogni documento rappresenta una riga del file CSV. Ogni riga viene convertita in una    coppia chiave/valore e viene inserita in una nuova riga nel contenuto della pagina del documento.    La fonte di ogni documento caricato da CSV è impostata sul valore dell'argomento    `file_path` per tutti i documenti di default.    È possibile sovrascrivere questa opzione impostando l'argomento `source_column` sul    nome di una colonna nel file CSV.    La fonte di ogni documento verrà quindi impostata sul valore della colonna    con il nome specificato in `source_column`.    Esempio di output:        .. code-block:: txt            colonna1: valore1            colonna2: valore2            colonna3: valore3    """    def __init__(        self,        file_path: str,        source_column: Optional[str] = None,        metadata_columns: Optional[List[str]] = None,           content_columns: Optional[List[str]] =None ,          csv_args: Optional[Dict] = None,        encoding: Optional[str] = None,    ):        #  omesso (salvato come codice originale)        self.metadata_columns = metadata_columns        # < AGGIUNTO    def load(self) -> List[Document]:        """Carica i dati negli oggetti documento."""        docs = []        with open(self.file_path, newline="", encoding=self.encoding) as csvfile:           #  omesso (salvato come codice originale)                # CODICE AGGIUNTO                 if self.metadata_columns:                    for k, v in row.items():                        if k in self.metadata_columns:                            metadata[k] = v                # FINE DEL CODICE AGGIUNTO                doc = Document(page_content=content, metadata=metadata)                docs.append(doc)        return docs

Per risparmiare spazio, ho omesso il codice che è uguale all’API originale e ho incluso solo le poche linee aggiuntive che vengono principalmente utilizzate per aggiungere determinate colonne che richiedono particolare attenzione ai metadati. Infatti, nei dati stampati sopra, è possibile notare due parti: i contenuti della pagina e i metadati. Il CSVLoader standard scrive tutte le colonne della tabella nei contenuti della pagina e solo le risorse dati e i numeri di riga nei metadati. Il “MetaDataCSVLoader” definito ci consente di scrivere altre colonne nei metadati.

Ricreiamo ora il vector store, con gli stessi passaggi della sezione precedente, ad eccezione del caricatore di dati in cui aggiungiamo due metadata_columns “time” e “city”.

# Carica i dati e imposta il caricatore di embedding loader = MetaDataCSVLoader(file_path="sir.csv",metadata_columns=['time','city']) #<= modificato data = loader.load()

SelfQuerying sul Vectorstore Informato dai Metadati

Ora siamo pronti a utilizzare l’API di SelfQuerying di LangChain:

Secondo la documentazione di LangChain: un retriever auto-interrogativo è quello che, come suggerisce il nome, può interrogarsi da solo. … Questo consente al retriever non solo di utilizzare la query dell’utente per confrontare la similarità semantica con i contenuti dei documenti memorizzati, ma anche di estrarre filtri dalla query dell’utente sui metadati dei documenti memorizzati ed eseguire tali filtri.

Immagine di LangChain: illustrazione dell'auto-interrogazione

Ora puoi capire perché sottolineo i metadati nell’ultimo capitolo: si basa su di essi che ChatGPT o altri LLM possono costruire il filtro per ottenere le informazioni più rilevanti dal dataset. Utilizziamo il seguente codice per costruire un retriever auto-interrogativo descrivendo i metadati e la descrizione del contenuto del documento, in base ai quali può essere costruito un filtro ben eseguito per estrarre le informazioni accurate. In particolare, forniamo un metadata_field_info al retriever, specificando il tipo e la descrizione delle due colonne a cui vogliamo che il retriever presti maggiore attenzione.

llm=ChatOpenAI(model_name="gpt-4",temperature=0)metadata_field_info=[     AttributeInfo(        name="time",        description="tempo in cui è stato misurato la popolazione",         type="datetime",     ),    AttributeInfo(        name="city",        description="città in cui è stata misurata la popolazione",         type="string",     ),]document_content_description = "Popolazione suscettibile, infettiva ed eliminata durante 90 giorni in 10 città"retriever = SelfQueryRetriever.from_llm(    llm, vectorstore, document_content_description, metadata_field_info, search_kwargs={"k": 20},verbose=True)

Possiamo ora definire una ConversationalRetriverChain simile per interrogare il dataset SIR, ma questa volta con il SelfQueryRetriever. Vediamo ora cosa succederà quando facciamo la stessa domanda: “Quale città ha il maggior numero di persone infette il 2018-02-03?”

Il chatbot ha detto: “La città con il numero massimo di persone infette il 2018-02-03 è la città3 con 1413 persone infette.”

Signore e signori, è corretto! Il chatbot sta facendo il suo lavoro con un retriever migliore!

Non fa male vedere quali documenti rilevanti restituisce il retriever questa volta e restituisce:

[Documento (contenuto_pagina=': 33\ntempo: 2018-02-03\nsuscettibile: 0\ninfettiva: 1413\neliminata: 35924\ncittà: city3', metadati={'fonte': 'sir.csv', 'riga': 303, 'tempo': '2018-02-03', 'città': 'city3'}), Documento (contenuto_pagina=': 33\ntempo: 2018-02-03\nsuscettibile: 0\ninfettiva: 822\neliminata: 20895\ncittà: city4', metadati={'fonte': 'sir.csv', 'riga': 393, 'tempo': '2018-02-03', 'città': 'city4'}), Documento (contenuto_pagina=': 33\ntempo: 2018-02-03\nsuscettibile: 0\ninfettiva: 581\neliminata: 14728\ncittà: city5', metadati={'fonte': 'sir.csv', 'riga': 483, 'tempo': '2018-02-03', 'città': 'city5'}), Documento (contenuto_pagina=': 33\ntempo: 2018-02-03\nsuscettibile: 0\ninfettiva: 1355\neliminata: 34382\ncittà: city6', metadati={'fonte': 'sir.csv', 'riga': 573, 'tempo': '2018-02-03', 'città': 'city6'}), Documento (contenuto_pagina=': 33\ntempo: 2018-02-03\nsuscettibile: 0\ninfettiva: 496\neliminata: 12535\ncittà: city8', metadati={'fonte': 'sir.csv', 'riga': 753, 'tempo': '2018-02-03', 'città': 'city8'}), Documento (contenuto_pagina=': 33\ntempo: 2018-02-03\nsuscettibile: 0\ninfettiva: 1028\neliminata: 26030\ncittà: city9', metadati={'fonte': 'sir.csv', 'riga': 843, 'tempo': '2018-02-03', 'città': 'city9'}), Documento (contenuto_pagina=': 33\ntempo: 2018-02-03\nsuscettibile: 0\ninfettiva: 330\neliminata: 8213\ncittà: city7', metadati={'fonte': 'sir.csv', 'riga': 663, 'tempo': '2018-02-03', 'città': 'city7'}), Documento (contenuto_pagina=': 33\ntempo: 2018-02-03\nsuscettibile: 0\ninfettiva: 1320\neliminata: 33505\ncittà: city2', metadati={'fonte': 'sir.csv', 'riga': 213, 'tempo': '2018-02-03', 'città': 'city2'}), Documento (contenuto_pagina=': 33\ntempo: 2018-02-03\nsuscettibile: 0\ninfettiva: 776\neliminata: 19753\ncittà: city1', metadati={'fonte': 'sir.csv', 'riga': 123, 'tempo': '2018-02-03', 'città': 'city1'}), Documento (contenuto_pagina=': 33\ntempo: 2018-02-03\nsuscettibile: 0\ninfettiva: 654\neliminata: 16623\ncittà: city0', metadati={'fonte': 'sir.csv', 'riga': 33, 'tempo': '2018-02-03', 'città': 'city0'})]

Potresti notare immediatamente che ci sono “tempo” e “città” nei “metadati” nei documenti recuperati ora.

Conclusioni

In questo post del blog, ho esplorato i limiti di ChatGPT durante l’interrogazione di set di dati in formato CSV, utilizzando l’SIR dataset proveniente da 10 città nel corso di un periodo di 90 giorni come esempio. Per affrontare questi limiti, ho proposto un nuovo approccio: un caricatore di dati CSV consapevole dei metadati che ci consente di sfruttare l’API di auto-interrogazione, migliorando significativamente l’accuratezza e le prestazioni del Chatbot.