可视化您的红绿蓝(RAG)数据 — 检索增强生成的EDA

如何使用UMAP降维技术将Question、Answer及其与源文档的关系展示在OpenAI、Langchain和ChromaDB中。

大型语言模型(LLMs)如GPT-4在文本理解和生成方面表现出令人印象深刻的能力。但是,它们在处理领域特定信息时面临挑战。当查询超出训练数据时,它们往往会产生错误答案的幻觉[1]。此外,LLMs的推理过程缺乏透明度,使用户难以理解达成的结论是如何得出的。

为解决这些挑战,我们开发了一种名为检索辅助生成(Retrieval-Augmented Generation,RAG)的技术。RAG 在 LLM 的工作流程中添加了一个检索步骤,使其能够在回答问题时从其他源(例如您的私人文本文档)查询相关数据。这些文档可以事先划分为小片段,并利用类似于OpenAI的embedding-ada-002的 ML 模型生成嵌入(紧凑的向量表示)。具有相似内容的片段将具有相似的嵌入。当 RAG 应用程序收到一个问题时,它将该查询投射到相同的嵌入空间,并检索与查询相关的相邻文档片段。然后,LLM将这些文档片段作为上下文来回答问题。这种方法可以提供回答查询所需的信息,并通过向用户呈现使用的片段来实现透明度。

Retrieval-Augmented Generation from [2]: Yixuan Tang, Yi Yang: MultiHop-RAG: Benchmarking Retrieval-Augmented Generation for Multi-Hop Queries (2021), arXiv — CC BY-SA 4.0

在开发RAG应用程序时,正如在许多其他领域中所认可的,了解数据的整体情况非常重要。对于RAG来说,可视化嵌入空间尤为有用,因为RAG应用程序使用此空间来查找相关信息。由于查询与文档片段共享空间,因此相关文档片段与查询之间的接近度尤其重要。我们建议使用UMAP等方法进行可视化,将高维嵌入降低到更易处理的2D可视化,同时保留关键属性,例如片段和查询之间的关系和接近度。尽管高维嵌入只缩减到两个分量,但问题及其相关文档片段在嵌入空间中形成的簇仍然能够被识别出来。这有助于找到数据的本质洞见。

UMAP dimensionality reduction of the embeddings of document snippets, colored by to their relevance to the question “‘”Who built the Nürburgring?” — created by author

在本文中,您将学习如何

  • 准备文档:首先收集数据。本教程使用维基百科的HTML格式的一级方程式数据作为示例,用于构建我们的RAG应用程序的数据集。你也可以在这里使用自己的数据!
  • 拆分和创建嵌入向量:将收集到的文档拆分为较小的片段,并使用嵌入模型将它们转换为紧凑的向量表示。这涉及使用分割器、OpenAI的文本嵌入模型(text-embedding-ada-002)以及ChromaDB作为向量存储器。
  • 构建一个LangChain:通过结合一个用于创建上下文的提示生成器、一个用于提取相关片段的检索器和一个用于回答查询的LLM(GPT-4)来建立LangChain。
  • 询问问题:学习如何向RAG应用程序提问。
  • 可视化:使用Renumics-Spotlight在2D中可视化嵌入,并分析查询和文档片段之间的关系和相似度。

这个简化的教程将引导您完成RAG应用程序开发的每个阶段,并特别关注结果可视化的作用。

代码可以在Github上找到

准备好

首先,安装所有所需的软件包:

!pip install langchain langchain-openai chromadb renumics-spotlight 

本教程使用Langchain,Renumics-Spotlight Python包。

  • Langchain: 一个整合语言模型和RAG组件的框架,使设置流程更加顺畅。
  • Renumics-聚焦: 一款可交互地探索无结构机器学习数据集的可视化工具。

免责声明:本文作者同时也是Spotlight的开发人员之一。

所需的ML模型将从OpenAI使用

  • GPT-4:一种以其先进的文本理解和生成能力而闻名的最先进的语言模型。
  • 嵌入-ada-002:一种专门用于创建文本嵌入表示的模型。

设置您的 OPENAI_API_KEY; 例如,您可以在笔记本中使用笔记本行魔法来设置它。

%env OPENAI_API_KEY=<your-api-key>

准备文件

对于此示例,您可以使用我们准备的维基百科所有一级方程式文章的数据集。该数据集是使用维基百科 API 和 BeautifulSoup 创建的。您可以下载数据集。

此数据集基于维基百科上的文章,其授权方式为知识共享署名-相同方式共享许可协议。原始文章和作者列表可在相应的维基百科页面上找到。

将提取的HTML文件放入docs/子文件夹中。

或者你可以通过创建docs/子文件夹并将你自己的文件复制到其中来使用你自己的数据集。

Image created by the author using Midjourney v6.0

拆分并为数据集创建嵌入

您可以跳过此部分,直接下载包含一级方程数据集嵌入的数据库。

要自己创建嵌入,首先需要设置嵌入模型和向量存储。在这里,我们使用来自OpenAIEmbeddings的text-embedding-ada-002和使用ChromaDB的向量存储。

from langchain_openai import OpenAIEmbeddings
from langchain.vectorstores.chroma import Chroma

embeddings_model = OpenAIEmbeddings(model="text-embedding-ada-002")
docs_vectorstore = Chroma(
collection_name="docs_store",
embedding_function=embeddings_model,
persist_directory="docs-db",
)

向量存储将持久化在 docs-db/ 文件夹中。

为了填充向量存储,我们使用BSHTMLLoader加载HTML文档:

from langchain_community.document_loaders import BSHTMLLoader, DirectoryLoader
loader = DirectoryLoader(
"docs",
glob="*.html",
loader_cls=BSHTMLLoader,
loader_kwargs={"open_encoding": "utf-8"},
recursive=True,
show_progress=True,
)
docs = loader.load()

并将它们分成较小的块

from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, chunk_overlap=200, add_start_index=True
)
splits = text_splitter.split_documents(docs)

此外,您可以创建一个可以从元数据重构的ID。这样,即使只有文档和元数据,您也可以在数据库中找到嵌入。您可以将所有内容都添加到数据库并进行存储。

import hashlib
import json
from langchain_core.documents import Document

def stable_hash(doc: Document) -> str:
"""
Stable hash document based on its metadata.
"""
return hashlib.sha1(json.dumps(doc.metadata, sort_keys=True).encode()).hexdigest()

split_ids = list(map(stable_hash, splits))
docs_vectorstore.add_documents(splits, ids=split_ids)
docs_vectorstore.persist()

您可以在本教程中找到有关拆分和整个过程的更多信息。

构建LangChain

首先,您需要选择一个LLM模型。在这里,我们使用GPT-4。此外,您还需要准备检索器以使用向量存储。

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4", temperature=0.0)
retriever = docs_vectorstore.as_retriever(search_kwargs={"k": 20})

将ChatOpenAI模型在初始化时将温度参数设为0.0,可确保确定性的输出。

现在,让我们为RAG创建一个提示。LLM将会提供用户的问题和检索到的文档作为回答问题的上下文。它还被要求提供回答所依据的来源。

from langchain_core.prompts import ChatPromptTemplate

template = """
You are an assistant for question-answering tasks.
Given the following extracted parts of a long document and a question, create a final answer with references ("SOURCES").
If you don't know the answer, just say that you don't know. Don't try to make up an answer.
ALWAYS return a "SOURCES" part in your answer.

QUESTION: {question}
=========
{source_documents}
=========
FINAL ANSWER: """
prompt = ChatPromptTemplate.from_template(template)

接下来,建立一个处理流程,首先将获取的文档进行格式化,使其包含页面内容和源文件路径。然后,将格式化的输入传入语言模型(LLM)步骤,根据用户提问和文档上下文生成答案。

from typing import List

from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser


def format_docs(docs: List[Document]) -> str:
return "\n\n".join(
f"Content: {doc.page_content}\nSource: {doc.metadata['source']}" for doc in docs
)


rag_chain_from_docs = (
RunnablePassthrough.assign(
source_documents=(lambda x: format_docs(x["source_documents"]))
)
| prompt
| llm
| StrOutputParser()
)
rag_chain = RunnableParallel(
{
"source_documents": retriever,
"question": RunnablePassthrough(),
}
).assign(answer=rag_chain_from_docs)

提问

RAG 应用现已准备好回答问题。

question = "Who built the nuerburgring"
response = rag_chain.invoke(question)
response["answer"]

这将打印出一个正确的答案。

'The Nürburgring was built in the 1920s, with the construction of the track beginning in September 1925. The track was designed by the Eichler Architekturbüro from Ravensburg, led by architect Gustav Eichler. The original Nürburgring was intended to be a showcase for German automotive engineering and racing talent (SOURCES: data/docs/Nürburgring.html).'

我们将坚持一个问题。这个问题也将用于下一部分的进一步调查。

可视化

为了探索,我们使用Pandas DataFrame来组织我们的数据。让我们从向量存储中提取文本片段及其嵌入开始。此外,让我们标记出正确的答案。

import pandas as pd

response = docs_vectorstore.get(include=["metadatas", "documents", "embeddings"])
df = pd.DataFrame(
{
"id": response["ids"],
"source": [metadata.get("source") for metadata in response["metadatas"]],
"page": [metadata.get("page", -1) for metadata in response["metadatas"]],
"document": response["documents"],
"embedding": response["embeddings"],
}
)
df["contains_answer"] = df["document"].apply(lambda x: "Eichler" in x)
df["contains_answer"].to_numpy().nonzero()

问题及其相关答案也被投射到嵌入空间中。它们以与文本片段相同的方式进行处理。

question_row = pd.DataFrame(
{
"id": "question",
"question": question,
"embedding": embeddings_model.embed_query(question),
}
)
answer_row = pd.DataFrame(
{
"id": "answer",
"answer": answer,
"embedding": embeddings_model.embed_query(answer),
}
)
df = pd.concat([question_row, answer_row, df])

此外,还可以确定问题和文档摘录之间的距离。

import numpy as np
question_embedding = embeddings_model.embed_query(question)
df["dist"] = df.apply(
lambda row: np.linalg.norm(
np.array(row["embedding"]) - question_embedding
),
axis=1,
)

这还可以用于可视化,并且将存储在距离列中。

+----+------------------------------------------+----------------------------+------------------------------------------------------------------------+----------------------------------------------------+----------------------------------------+--------+------------------------------+-------------------+------------+
| | id | question | embedding | answer | source | page | document | contains_answer | dist |
|----+------------------------------------------+----------------------------+------------------------------------------------------------------------+----------------------------------------------------+----------------------------------------+--------+------------------------------+-------------------+------------|
| 0 | question | Who built the nuerburgring | [0.005164676835553928, -0.011625865528385777, ... | nan | nan | nan | nan | nan | nan |
| 1 | answer | nan | [-0.007912757349432444, -0.021647867427574807, ... | The Nürburgring was built in the 1920s in the town | nan | nan | nan | nan | 0.496486 |
| 2 | 000062fd07a090c7c84ed42468a0a4b7f5f26bf8 | nan | [-0.028886599466204643, 0.006249633152037859, ... | nan | data/docs/Hamilton–Vettel rivalry.html | -1 | Media reception... | 0 | 0.792964 |
| 3 | 0003de08507d7522c43bac201392929fb2e26b86 | nan | [-0.031988393515348434, -0.002095212461426854, ... | nan | data/docs/Cosworth GBA.html | -1 | Team Haas[edit]... | 0 | 0.726574 |
| 4 | 000543bb633380334e742ec9e0c15a188dcb0bf2 | nan | [-0.007886063307523727, 0.007812486961483955, ... | nan | data/docs/Interlagos Circuit.html | -1 | Grand Prix motorcycle racing.| 0 | 0.728354 |
| | | | | | | | Brazilian motorcycle... | | |
+----+------------------------------------------+----------------------------+------------------------------------------------------------------------+----------------------------------------------------+----------------------------------------+--------+------------------------------+-------------------+------------

点亮可以通过以下方式启动:


from renumics import spotlight
spotlight.show(df)

它将打开一个新的浏览器窗口。左上方的表格部分显示数据集的所有字段。您可以使用“可见列”按钮选择列“问题”、“答案”、“来源”、“文档”和“距离”。按“距离”对表格进行排序,将问题、答案和最相关的文档片段显示在顶部。选择前14行以在右上方的相似性图上突出显示它们。

您可以观察到与问题和答案最相关的文档都在紧密的接近位置。其中包括了包含正确答案的单个文档摘录。

接下来呢?

单个问题、答案和相关文档的良好可视化展示展示了RAG的巨大潜力。使用降维技术可以使嵌入空间对用户和开发人员可访问。本文中特定演示的实用性仍然非常有限。探索这些方法在展示多个问题以及通过评估问题来说明RAG系统的使用或检查嵌入空间覆盖范围的可能性仍然令人兴奋。请继续关注更多接下来的文章。

使用像Spotlight这样增强数据科学工作流程的工具,可以更轻松地进行RAG的可视化。使用您自己的数据尝试一下这段代码,并在评论中告诉我们您的结果!

我是一位专业人士,具有在创建用于交互式探索非结构化数据的先进软件解决方案方面的专业知识。我撰写关于非结构化数据的文章,并使用强大的可视化工具进行分析和做出明智的决策。

参考文献

[1] 高云帆,熊云,高新宇,贾康翔,潘金柳,毕雨溪,戴轶,孙佳伟,郭倩宇,王萌,王浩芬:用于大语言模型的检索增强生成技术:综述(2024),arxiv

【2】唐一轩,杨毅: MultiHop-RAG: 多跳查询的检索增强生成方式的基准测试 (2021), arXiv

[3] Leland McInnes, John Healy, James Melville:UMAP: 统一流形近似和投影用于降维 (2018),arXiv

2024-02-11 01:02:13 AI中文站翻译自原文