RAG實戰(zhàn)篇:優(yōu)化數(shù)據(jù)索引的四種高級方法,構建完美的信息結構

0 評論 3068 瀏覽 4 收藏 19 分鐘

在構建高效的檢索系統(tǒng)(RAG)時,優(yōu)化索引是提升系統(tǒng)性能的關鍵步驟。這篇文章深入探討了如何通過高級技術手段對索引進行優(yōu)化,以實現(xiàn)更快速、更準確的信息檢索,供大家參考。

《RAG實戰(zhàn)篇:構建一個最小可行性的Rag系統(tǒng)》中,風叔詳細介紹了RAG系統(tǒng)的實現(xiàn)框架,以及如何搭建一個最簡單的Naive Rag系統(tǒng)。

Indexing(索引)是搭建任何RAG系統(tǒng)的第一步,也是至關重要的一步,良好的索引意味著合理的知識或信息分類,召回環(huán)節(jié)就會更加精準。在這篇文章中,圍繞Indexing(索引)環(huán)節(jié),如下圖藍色部分所示,風叔詳細介紹一下如何對輸入文檔·構建合理的索引。

在實際應用場景中,文檔尺寸可能非常大,因此需要將長篇文檔分割成多個文本塊,以便更高效地處理和檢索信息。

Indexing(索引)環(huán)節(jié)主要面臨三個難題:

首先,內容表述不完整,內容塊的語義信息容易受分割方式影響,致使在較長的語境中,重要信息被丟失或被掩蓋。

其次,塊相似性搜索不準確,隨著數(shù)據(jù)量增多,檢索中的噪聲增大,導致頻繁與錯誤數(shù)據(jù)匹配,使得檢索系統(tǒng)脆弱且不可靠。

最后,參考軌跡不明晰,檢索到的內容塊可能來自任何文檔,沒有引用痕跡,可能出現(xiàn)來自多個不同文檔的塊,盡管語義相似,但包含的卻是完全不同主題的內容。

下面,我們結合源代碼,介紹Chunk optimization(塊優(yōu)化)、Multi-representation indexing(多層表達索引)、Specialized embeddings(特殊嵌入)和Hierachical Indexing(多級索引)這四種優(yōu)化索引的高級方法。

1. Chunk optimization(塊優(yōu)化)

在內容分塊的時候,分塊大小對索引結果會有很大的影響。較大的塊能捕捉更多的上下文,但也會產(chǎn)生更多噪聲,需要更長的處理時間和更高的成本;而較小的塊噪聲更小,但可能無法完整傳達必要的上下文。

第一種優(yōu)化方式:固定大小重疊滑動窗口

該方法根據(jù)字符數(shù)將文本劃分為固定大小的塊,實現(xiàn)簡單。但是其局限性包括對上下文大小的控制不精確、存在切斷單詞或句子的風險以及缺乏語義考慮。適用于探索性分析,但不推薦用于需要深度語義理解的任務。

text = "..." # your text
from langchain.text_splitter import CharacterTextSplitter
text_splitter = CharacterTextSplitter(
    chunk_size = 256,
    chunk_overlap  = 20)
docs = text_splitter.create_documents([text])

第二種優(yōu)化方式:遞歸感知

一種結合固定大小滑動窗口和結構感知分割的混合方法。它試圖平衡固定塊大小和語言邊界,提供精確的上下文控制。實現(xiàn)復雜度較高,存在塊大小可變的風險,對于需要粒度和語義完整性的任務有效,但不推薦用于快速任務或結構劃分不明確的任務。

text = "..." # your text
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size = 256,
    chunk_overlap  = 20,
    separators = ["nn", "n"])
docs = text_splitter.create_documents([text])

第三種優(yōu)化方式:結構感知切分

該方法考慮文本的自然結構,根據(jù)句子、段落、節(jié)或章對其進行劃分。尊重語言邊界可以保持語義完整性,但結構復雜性的變化會帶來挑戰(zhàn)。對于需要上下文和語義的任務有效,但不適用于缺乏明確結構劃分的文本

text = "..." # your text
docs = text.split(".")

第四種優(yōu)化方式:內容感知切分

此方法側重于內容類型和結構,尤其是在 Markdown、LaTeX 或 HTML 等結構化文檔中。它確保內容類型不會在塊內混合,從而保持完整性。挑戰(zhàn)包括理解特定語法和不適用于非結構化文檔。適用于結構化文檔,但不推薦用于非結構化內容。以markdown為例

from langchain.text_splitter import MarkdownTextSplitter
markdown_text = "..."
markdown_splitter = MarkdownTextSplitter(chunk_size=100, chunk_overlap=0)
docs = markdown_splitter.create_documents([markdown_text])

第五種塊優(yōu)化方式:基于語義切分

一種基于語義理解的復雜方法,通過檢測主題的重大轉變將文本劃分為塊。確保語義一致性,但需要高級 NLP 技術。對于需要語義上下文和主題連續(xù)性的任務有效,但不適合高主題重疊或簡單的分塊任務

text = "..." # your text
from langchain.text_splitter import NLTKTextSplitter
text_splitter = NLTKTextSplitter()
docs = text_splitter.split_text(text)

2. 多層表達索引

多層表達索引是一種構建多級索引的方法,在長上下文環(huán)境比較有用。

這種方法通過將原始數(shù)據(jù)生成 summary后,重新作為embedding再存到summary database中。檢索的時候,首先通過summary database找到最相關的summary,再回溯到原始文檔中去。

首先,我們使用 WebBaseLoader 加載兩個網(wǎng)頁的文檔,在這個例子中,我們加載了 Lilian Weng 的兩篇博客文章:

from langchain_community.document_loaders import WebBaseLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
loader = WebBaseLoader("https://lilianweng.github.io/posts/2023-06-23-agent/")
docs = loader.load()
loader = WebBaseLoader("https://lilianweng.github.io/posts/2024-02-05-human-data-quality/")
docs.extend(loader.load())

模型使用 ChatOpenAI,設置為 gpt-3.5-turbo 版本,利用 chain.batch 批量處理文檔,使用 max_concurrency 參數(shù)限制并發(fā)數(shù)。

import uuid
from langchain_core.documents import Document
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
chain = (
    {"doc": lambda x: x.page_content}
    | ChatPromptTemplate.from_template("Summarize the following document:nn{doc}")
    | ChatOpenAI(model="gpt-3.5-turbo",max_retries=0)
    | StrOutputParser())
summaries = chain.batch(docs, {"max_concurrency": 5})

我們引入了 InMemoryByteStore 和 Chroma 兩個模塊,分別用于存儲原始文檔和總結文檔。InMemoryByteStore 是一個內存中的存儲層,用于存儲原始文檔,而 Chroma 則是一個文檔向量數(shù)據(jù)庫,用于存儲文檔的向量表示。

from langchain.storage import InMemoryByteStore
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain.retrievers.multi_vector import MultiVectorRetriever
#The vector store to use to index the child chunks
vectorstore = Chroma(collection_name="summaries",                     embedding_function=OpenAIEmbeddings())
#The storage layer for the parent documents
store = InMemoryByteStore()

MultiVectorRetriever 類幫助我們在一個統(tǒng)一的接口中管理文檔和向量存儲,使得檢索過程更加高效。

id_key = "doc_id"
#The retriever
retriever = MultiVectorRetriever(
    vectorstore=vectorstore,
    byte_store=store,
    id_key=id_key,)
doc_ids = [str(uuid.uuid4()) for _ in docs]

將總結文檔添加到 Chroma 向量數(shù)據(jù)庫中,同時在 InMemoryByteStore 中關聯(lián)原始文檔和 doc_id。

summary_docs = [
    Document(page_content=s, metadata={id_key: doc_ids[i]})
    for i, s in enumerate(summaries)]#Add
retriever.vectorstore.add_documents(summary_docs)
retriever.docstore.mset(list(zip(doc_ids, docs)))

執(zhí)行檢索操作,對于給定的查詢 query = “Memory in agents”,我們使用 vectorstore 進行相似性檢索,k=1 表示只返回最相關的一個文檔。然后使用 retriever 進行檢索,n_results=1 表示只返回一個文檔結果。

query = "Memory in agents"
sub_docs=vectorstore.similarity_search(query,k=1)
#打印sub_docs[0]
retrieved_docs=retriever.get_relevant_documents(query,n_results=1)
#打印retrieved_docs[0].page_content[0:500]

3. 特殊向量

特殊向量方法常用于多模態(tài)數(shù)據(jù),比如圖片數(shù)據(jù),利用特殊的向量去做索引。

ColBERT是一種常用的特殊向量方法,它為段落中的每個標記生成一個受上下文影響的向量,同時也會為查詢中的每個標記生成向量。然后,每個文檔的得分是每個查詢嵌入與任何文檔嵌入的最大相似度之和。

可以使用RAGatouille工具來快速實現(xiàn)ColBERT,首先引入RAGatouille。

from ragatouille import RAGPretrainedModel
RAG = RAGPretrainedModel.from_pretrained("colbert-ir/colbertv2.0")

然后我們獲取文檔數(shù)據(jù),這里我們選擇了使用wiki頁面

最后,完成索引的構建,自動使用ColBERT方法完成索引。

RAG.index(
    collection=[full_document],
    index_name="Miyazaki-123",
    max_document_length=180,
    split_documents=True,)

4. 分層索引

分層索引,指的是帶層級結構的去索引,比如可以先從關系數(shù)據(jù)庫里索引找出對應的關系,然后再利用索引出的關系再進一步去搜尋basic數(shù)據(jù)庫。前文介紹的多層表達索引也屬于分層索引的一種。

還有一種更有效的分層索引方法叫做Raptor,Recursive Abstractive Processing for Tree-Organized Retrieval,該方法核心思想是將doc構建為一棵樹,然后逐層遞歸的查詢,如下圖所示:

RAPTOR 根據(jù)向量遞歸地對文本塊進行聚類,并生成這些聚類的文本摘要,從而自下而上構建一棵樹。聚集在一起的節(jié)點是兄弟節(jié)點;父節(jié)點包含該集群的文本摘要。這種結構使 RAPTOR 能夠將代表不同級別文本的上下文塊加載到 LLM 的上下文中,以便它能夠有效且高效地回答不同層面的問題。

查詢有兩種方法,基于樹遍歷(tree traversal)和折疊樹(collapsed tree)。遍歷是從 RAPTOR 樹的根層開始,然后逐層查詢;折疊樹就是全部平鋪,用ANN庫查詢。

Raptor是一種非常高級和復雜的方法,源代碼也相對比較復雜,這里就不貼出來了,只從整體上介紹一下Raptor的邏輯。大家可以通過上文介紹的方法來獲取源碼。

首先,我們使用LangChain 的 LCEL 文檔作為輸入數(shù)據(jù),并對文檔進行分塊以適合我們的 LLM 上下文窗口,生成全局嵌入列表,并將維度減少到2來簡化生成的聚類,并可視化。

然后,為每個Raptor步驟定義輔助函數(shù),并構建樹。這一段代碼是整個Raptor中最復雜的一段,其主要做了以下事情:

  • global_cluster_embeddings使用UAMP算法對所有的Embeddings進行全局降維,local_cluster_embeddings則使用UAMP算法進行局部降維。
  • get_optimal_clusters函數(shù)使用高斯混合模型的貝葉斯信息準則 (BIC) 確定最佳聚類數(shù)。
  • GMM_cluster函數(shù)使用基于概率閾值的高斯混合模型 (GMM) 進行聚類嵌入,返回包含聚類標簽和確定的聚類數(shù)量的元組。
  • Perform_clustering函數(shù)則對嵌入執(zhí)行聚類,首先全局降低其維數(shù),然后使用高斯混合模型進行聚類,最后在每個全局聚類內執(zhí)行局部聚類。
  • Embed_cluster_texts函數(shù)則用于嵌入文本列表并對其進行聚類,返回包含文本、其嵌入和聚類標簽的 DataFrame。
  • Embed_cluster_summarize_texts函數(shù)首先為文本生成嵌入,根據(jù)相似性對它們進行聚類,擴展聚類分配以便于處理,然后匯總每個聚類內的內容。
  • recursive_embed_cluster_summarize函數(shù)遞歸地嵌入、聚類和匯總文本,直至指定級別或直到唯一聚類的數(shù)量變?yōu)?1,并在每個級別存儲結果。

接下來,生成最終摘要,有兩種方法:

  1. 樹遍歷檢索:樹的遍歷從樹的根級開始,并根據(jù)向量嵌入的余弦相似度檢索節(jié)點的前 k 個文檔。因此,在每一級,它都會從子節(jié)點檢索前 k 個文檔。
  2. 折疊樹檢索:折疊樹檢索是一種更簡單的方法。它將所有樹折疊成一層,并根據(jù)查詢向量的余弦相似度檢索節(jié)點,直到達到閾值數(shù)量的標記。

接下來,我們將提取數(shù)據(jù)框文本、聚類文本、最終摘要文本,并將它們組合起來,創(chuàng)建一個包含根文檔和摘要的大型文本列表。然后將該文本存儲到向量存儲中,構建索引,并創(chuàng)建查詢引擎

最后,用一個實際問題進行檢驗,可以看到實際的回復內容還是比較準確的。

# Question
response =rag_chain.invoke("What is LCEL?")
print(str(response))
############# Response ######################################
LangChain Expression Language (LCEL) is a declarative way to easily compose chains together in LangChain. It was designed from day 1 to support putting prototypes in production with no code changes, from the simplest "prompt + LLM" chain to complex chains with hundreds of steps. Some reasons why one might want to use LCEL include streaming support (allowing for the best possible time-to-first-token), async support (enabling use in both synchronous and asynchronous APIs), optimized parallel execution (automatically executing parallel steps with the smallest possible latency), retries and fallbacks (a great way to make chains more reliable at scale), access to intermediate results (useful for letting end-users know something is happening or debugging), input and output schemas (providing Pydantic and JSONSchema schemas inferred from chain structure for validation), seamless LangSmith tracing integration (maximum observability and debuggability), and seamless LangServe deployment integration (easy chain deployment).

到這里,優(yōu)化索引的四種高級方法就介紹完了。

總結

在這篇文章中,風叔詳細介紹了優(yōu)化Indexing(索引)的具體方法,包括Chunk optimization(塊優(yōu)化)、Multi-representation indexing(多層表達索引)、Specialized embeddings(特殊嵌入)和Hierachical Indexing(多級索引)這四種優(yōu)化方案。

在下一篇文章中,風叔將重點圍繞Query Translation(查詢轉換)環(huán)節(jié),介紹精準識別用戶查詢意圖的五種高級優(yōu)化方法。

本文由人人都是產(chǎn)品經(jīng)理作者【風叔】,微信公眾號:【風叔云】,原創(chuàng)/授權 發(fā)布于人人都是產(chǎn)品經(jīng)理,未經(jīng)許可,禁止轉載。

題圖來自Unsplash,基于 CC0 協(xié)議。

更多精彩內容,請關注人人都是產(chǎn)品經(jīng)理微信公眾號或下載App
評論
評論請登錄
  1. 目前還沒評論,等你發(fā)揮!