Press "Enter" to skip to content

在ChatGPT中提升CSV文件查询性能

通过自定义的CSV加载器实现LangChain的自查询

先进的语言模型,如ChatGPT,为查询表格数据带来了一种新颖且有前景的方法。然而,由于令牌限制,如果没有像retriever这样的API的帮助,直接执行查询就变得具有挑战性。因此,查询的准确性在很大程度上取决于查询的质量,标准的检索器未能返回所需的确切信息并不罕见。

在本文中,我将深入探讨传统检索方法在某些用例中失败的原因。此外,我们提出了一种革命性的解决方案,即通过包含元数据信息的自定义CSV数据加载器。通过利用LangChain的自查询API以及新的CSV数据加载器,我们可以以显著提高的性能和精度提取信息。

有关本文中使用的详细代码,请查看此处的笔记本。我想强调的是,这个笔记本展示了使用LLM查询大型表格数据可以实现卓越的准确性的可能性。

在疾病人口数据集上进行检索

我们想要查询作者创建的以下合成SIR数据集:我们基于简单的SIR模型模拟了疾病在10个城市的90天期间人口的三个不同群体。为了简单起见,我们假设每个城市的人口范围从5e3到2e4,并且城市之间没有人口流动。此外,我们生成了10个介于500到2000之间的随机整数作为最初的感染人数。

作者提供的图像:10个城市的疾病人口

该表格具有以下形式,包括五列:“time”表示测量人口的时间,“city”表示测量数据的城市,“susceptible”、“infectious”和“removed”表示人口的三个群体。为了简单起见,数据已保存为本地的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

我们想要向ChatGPT提出与数据集相关的问题。为了让ChatGPT与这样的表格数据进行交互,我们按照以下标准步骤使用LangChain:

  1. 使用CSVLoader加载数据,
  2. 创建一个向量存储(这里我们使用Chroma)来存储带有OpenAI嵌入的嵌入数据,
  3. 使用检索器返回与给定的非结构化查询相关的文档。

您可以使用以下代码实现上述步骤。

# 从本地路径加载数据
loader = CSVLoader(file_path=LOCAL_PATH)
data = loader.load()
# 创建嵌入
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(data, embeddings)
# 创建检索器
retriever = vectorstore.as_retriever(search_kwargs={"k": 20})

现在我们可以定义一个ConversationalRetriverChain来查询SIR数据集。

llm = ChatOpenAI(model_name="gpt-4", temperature=0)
# 定义系统消息模板
system_template = """提供的{context}是一个包含10个城市90天内可疑、感染和移除人口的表格数据集。数据集包括以下列:'time':测量人口的时间,'city':测量人口的城市,'susceptible':疾病的易感人口,'infectious':疾病的感染人口,'removed':疾病的移除人口。----------------{context}"""
# 创建聊天提示模板
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)

在上面的代码中,我定义了一个对话链,它将使用前一步中定义的检索器在SIR数据集中搜索查询的相关信息,并根据检索到的信息给出一个答案。为了给ChatGPT提供更清晰的指示,我还提供了一个提示,澄清了数据集中所有列的定义。

现在让我们提一个简单的问题:“2018年2月3日哪个城市有最多的感染人数?”

令人惊讶的是,我们的聊天机器人说:“提供的数据集不包含2018年2月3日的数据。”

这怎么可能呢?

为什么检索失败了?

为了调查为什么聊天机器人无法回答一个答案明明在提供的数据集中,我看了一下它在问题“2018年2月3日哪个城市有最多的感染人数?”中检索到的相关文档。我得到了以下内容:

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

出乎意料的是,尽管我明确指定我想知道发生在2018年2月3日的事件,但没有返回任何这个日期的行。由于从未将任何关于那天的信息发送给ChatGPT,当然可以肯定它无法回答这样的问题。

深入研究检索器的源代码,我们可以看到get_relevant_documents默认调用similarity_search方法。该方法根据计算的距离度量(默认为余弦距离)从0到1返回前n个块(默认为4,但我在我的代码中将数字设置为20),该度量衡量查询向量和文档块向量之间的“相似度”。

回到SIR数据集,我们注意到每行几乎讲述了同样的故事:日期、城市和标记为哪个组的人数。毫无疑问,表示这些行的向量彼此相似。对相似度分数进行快速检查告诉我们,许多行的分数约为0.29。

因此,相似度分数不足以区分行之间与查询的相关性:这在表格数据中始终如此,其中行之间没有显著差异。我们需要能够处理元数据的更强大的筛选器。

具有自定义元数据的CSVLoader

很明显,聊天机器人性能的提升在很大程度上取决于检索器的效率。为此,我们首先定义了一个具有元数据信息的自定义CSVLoader,可以与检索器进行通信。

我们编写以下代码:

class MetaDataCSVLoader(BaseLoader):    """将CSV文件加载到文档列表中。    每个文档表示CSV文件的一行。每一行都被转换成一个键/值对,并输出到文档的page_content的新行中。    对于由于默认情况下,从csv加载的每个文档的源都设置为`file_path`参数的值。    您可以通过将`source_column`参数设置为CSV文件中的某一列的名称来覆盖此设置。    每个文档的源将设置为具有指定名称的列的值。    输出示例:        .. code-block:: txt            column1: value1            column2: value2            column3: value3    """    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,    ):        #  省略(保存原始代码)        self.metadata_columns = metadata_columns        # <新增    def load(self) -> List[Document]:        """将数据加载到文档对象中。"""        docs = []        with open(self.file_path, newline="", encoding=self.encoding) as csvfile:           #  省略(保存原始代码)                # 新增的代码                 if self.metadata_columns:                    for k, v in row.items():                        if k in self.metadata_columns:                            metadata[k] = v                # 新增代码的结束                doc = Document(page_content=content, metadata=metadata)                docs.append(doc)        return docs

为了节省空间,我省略了与原始API相同的代码,并只包含了额外的几行代码,这些代码主要用于将需要特殊注意的某些列添加到元数据中。确实,在上面的打印数据中,您可以注意到两个部分:页面内容和元数据。标准的CSVLoader将表格的所有列都写入页面内容中,只将数据资源和行号写入元数据中。定义的“MetaDataCSVLoader”允许我们将其他列写入元数据。

现在,我们重新创建向量存储,步骤与上面的部分相同,除了数据加载器,在其中我们添加了两个元数据列“time”和“city”。

# 加载数据并设置嵌入加载器 = MetaDataCSVLoader(file_path="sir.csv",metadata_columns=['time','city']) #<= 修改的 data = loader.load()

在元数据通知的向量存储上进行自查询

现在我们准备使用LangChain的SelfQuerying API:

根据LangChain的文档:自查询检索器是可以自己查询的检索器。…这使得检索器不仅可以将用户输入的查询与存储文档的内容进行语义相似性比较,还可以从用户查询中提取存储文档的元数据并执行这些过滤器。

LangChain的自查询示意图

现在你可以理解为什么我在最后一章强调元数据了:它是ChatGPT或其他LLM从数据集中获取最相关信息的基础。我们使用以下代码来建立这样一个自查询检索器,通过描述元数据和文档内容来构建性能良好的过滤器以提取准确的信息。特别地,我们向检索器提供metadata_field_info,指定我们希望检索器更加关注的两列的类型和描述。

llm=ChatOpenAI(model_name="gpt-4",temperature=0)metadata_field_info=[     AttributeInfo(        name="time",        description="测量人口的时间",         type="datetime",     ),    AttributeInfo(        name="city",        description="测量人口的城市",         type="string",     ),]document_content_description = "在10个城市中的90天内的易感染、感染和移除人口"retriever = SelfQueryRetriever.from_llm(    llm, vectorstore, document_content_description, metadata_field_info, search_kwargs={"k": 20},verbose=True)

现在我们可以定义一个类似的ConversationalRetriverChain来查询SIR数据集,但这次使用SelfQueryRetriever。让我们来看看当我们问同样的问题:“2018-02-03哪个城市有最多的感染人数?”会发生什么。

聊天机器人回答:“2018-02-03有最多感染人数的城市是城市3,有1413人感染。”

女士们先生们,答对了!聊天机器人使用更好的检索器完成了它的工作!

这次没有关系看一下检索器返回了哪些相关文档,它给出了:

[Document(page_content=': 33\ntime: 2018-02-03\nsusceptible: 0\ninfectious: 1413\nremoved: 35924\ncity: city3', metadata={'source': 'sir.csv', 'row': 303, 'time': '2018-02-03', 'city': 'city3'}), Document(page_content=': 33\ntime: 2018-02-03\nsusceptible: 0\ninfectious: 822\nremoved: 20895\ncity: city4', metadata={'source': 'sir.csv', 'row': 393, 'time': '2018-02-03', 'city': 'city4'}), Document(page_content=': 33\ntime: 2018-02-03\nsusceptible: 0\ninfectious: 581\nremoved: 14728\ncity: city5', metadata={'source': 'sir.csv', 'row': 483, 'time': '2018-02-03', 'city': 'city5'}), Document(page_content=': 33\ntime: 2018-02-03\nsusceptible: 0\ninfectious: 1355\nremoved: 34382\ncity: city6', metadata={'source': 'sir.csv', 'row': 573, 'time': '2018-02-03', 'city': 'city6'}), Document(page_content=': 33\ntime: 2018-02-03\nsusceptible: 0\ninfectious: 496\nremoved: 12535\ncity: city8', metadata={'source': 'sir.csv', 'row': 753, 'time': '2018-02-03', 'city': 'city8'}), Document(page_content=': 33\ntime: 2018-02-03\nsusceptible: 0\ninfectious: 1028\nremoved: 26030\ncity: city9', metadata={'source': 'sir.csv', 'row': 843, 'time': '2018-02-03', 'city': 'city9'}), Document(page_content=': 33\ntime: 2018-02-03\nsusceptible: 0\ninfectious: 330\nremoved: 8213\ncity: city7', metadata={'source': 'sir.csv', 'row': 663, 'time': '2018-02-03', 'city': 'city7'}), Document(page_content=': 33\ntime: 2018-02-03\nsusceptible: 0\ninfectious: 1320\nremoved: 33505\ncity: city2', metadata={'source': 'sir.csv', 'row': 213, 'time': '2018-02-03', 'city': 'city2'}), Document(page_content=': 33\ntime: 2018-02-03\nsusceptible: 0\ninfectious: 776\nremoved: 19753\ncity: city1', metadata={'source': 'sir.csv', 'row': 123, 'time': '2018-02-03', 'city': 'city1'}), Document(page_content=': 33\ntime: 2018-02-03\nsusceptible: 0\ninfectious: 654\nremoved: 16623\ncity: city0', metadata={'source': 'sir.csv', 'row': 33, 'time': '2018-02-03', 'city': 'city0'})]

你可能一下子就注意到了现在在检索到的文档中,“metadata” 中有“time” 和“city”。

结论

在这篇博客文章中,我探讨了 ChatGPT 在查询 CSV 格式数据集时的限制,以一个例子来自 10 个城市、为期 90 天的 SIR 数据集为例。为了解决这些限制,我提出了一种新颖的方法: 一种元数据感知的 CSV 数据加载器,使我们能够利用自查询 API,显著提高 Chatbot 的准确性和性能。

Leave a Reply

Your email address will not be published. Required fields are marked *