RAG實(shí)戰(zhàn)篇:優(yōu)化數(shù)據(jù)索引的四種高級方法,構(gòu)建完美的信息結(jié)構(gòu)
在構(gòu)建高效的檢索系統(tǒng)(RAG)時(shí),優(yōu)化索引是提升系統(tǒng)性能的關(guān)鍵步驟。這篇文章深入探討了如何通過高級技術(shù)手段對索引進(jìn)行優(yōu)化,以實(shí)現(xiàn)更快速、更準(zhǔn)確的信息檢索,供大家參考。
在《RAG實(shí)戰(zhàn)篇:構(gòu)建一個(gè)最小可行性的Rag系統(tǒng)》中,風(fēng)叔詳細(xì)介紹了RAG系統(tǒng)的實(shí)現(xiàn)框架,以及如何搭建一個(gè)最簡單的Naive Rag系統(tǒng)。
Indexing(索引)是搭建任何RAG系統(tǒng)的第一步,也是至關(guān)重要的一步,良好的索引意味著合理的知識(shí)或信息分類,召回環(huán)節(jié)就會(huì)更加精準(zhǔn)。在這篇文章中,圍繞Indexing(索引)環(huán)節(jié),如下圖藍(lán)色部分所示,風(fēng)叔詳細(xì)介紹一下如何對輸入文檔·構(gòu)建合理的索引。
在實(shí)際應(yīng)用場景中,文檔尺寸可能非常大,因此需要將長篇文檔分割成多個(gè)文本塊,以便更高效地處理和檢索信息。
Indexing(索引)環(huán)節(jié)主要面臨三個(gè)難題:
首先,內(nèi)容表述不完整,內(nèi)容塊的語義信息容易受分割方式影響,致使在較長的語境中,重要信息被丟失或被掩蓋。
其次,塊相似性搜索不準(zhǔn)確,隨著數(shù)據(jù)量增多,檢索中的噪聲增大,導(dǎo)致頻繁與錯(cuò)誤數(shù)據(jù)匹配,使得檢索系統(tǒng)脆弱且不可靠。
最后,參考軌跡不明晰,檢索到的內(nèi)容塊可能來自任何文檔,沒有引用痕跡,可能出現(xiàn)來自多個(gè)不同文檔的塊,盡管語義相似,但包含的卻是完全不同主題的內(nèi)容。
下面,我們結(jié)合源代碼,介紹Chunk optimization(塊優(yōu)化)、Multi-representation indexing(多層表達(dá)索引)、Specialized embeddings(特殊嵌入)和Hierachical Indexing(多級索引)這四種優(yōu)化索引的高級方法。
1. Chunk optimization(塊優(yōu)化)
在內(nèi)容分塊的時(shí)候,分塊大小對索引結(jié)果會(huì)有很大的影響。較大的塊能捕捉更多的上下文,但也會(huì)產(chǎn)生更多噪聲,需要更長的處理時(shí)間和更高的成本;而較小的塊噪聲更小,但可能無法完整傳達(dá)必要的上下文。
第一種優(yōu)化方式:固定大小重疊滑動(dòng)窗口
該方法根據(jù)字符數(shù)將文本劃分為固定大小的塊,實(shí)現(xiàn)簡單。但是其局限性包括對上下文大小的控制不精確、存在切斷單詞或句子的風(fēng)險(xiǎn)以及缺乏語義考慮。適用于探索性分析,但不推薦用于需要深度語義理解的任務(wù)。
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)化方式:遞歸感知
一種結(jié)合固定大小滑動(dòng)窗口和結(jié)構(gòu)感知分割的混合方法。它試圖平衡固定塊大小和語言邊界,提供精確的上下文控制。實(shí)現(xiàn)復(fù)雜度較高,存在塊大小可變的風(fēng)險(xiǎn),對于需要粒度和語義完整性的任務(wù)有效,但不推薦用于快速任務(wù)或結(jié)構(gòu)劃分不明確的任務(wù)。
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)化方式:結(jié)構(gòu)感知切分
該方法考慮文本的自然結(jié)構(gòu),根據(jù)句子、段落、節(jié)或章對其進(jìn)行劃分。尊重語言邊界可以保持語義完整性,但結(jié)構(gòu)復(fù)雜性的變化會(huì)帶來挑戰(zhàn)。對于需要上下文和語義的任務(wù)有效,但不適用于缺乏明確結(jié)構(gòu)劃分的文本
text = "..." # your text
docs = text.split(".")
第四種優(yōu)化方式:內(nèi)容感知切分
此方法側(cè)重于內(nèi)容類型和結(jié)構(gòu),尤其是在 Markdown、LaTeX 或 HTML 等結(jié)構(gòu)化文檔中。它確保內(nèi)容類型不會(huì)在塊內(nèi)混合,從而保持完整性。挑戰(zhàn)包括理解特定語法和不適用于非結(jié)構(gòu)化文檔。適用于結(jié)構(gòu)化文檔,但不推薦用于非結(jié)構(gòu)化內(nèi)容。以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)化方式:基于語義切分
一種基于語義理解的復(fù)雜方法,通過檢測主題的重大轉(zhuǎn)變將文本劃分為塊。確保語義一致性,但需要高級 NLP 技術(shù)。對于需要語義上下文和主題連續(xù)性的任務(wù)有效,但不適合高主題重疊或簡單的分塊任務(wù)
text = "..." # your text
from langchain.text_splitter import NLTKTextSplitter
text_splitter = NLTKTextSplitter()
docs = text_splitter.split_text(text)
2. 多層表達(dá)索引
多層表達(dá)索引是一種構(gòu)建多級索引的方法,在長上下文環(huán)境比較有用。
這種方法通過將原始數(shù)據(jù)生成 summary后,重新作為embedding再存到summary database中。檢索的時(shí)候,首先通過summary database找到最相關(guān)的summary,再回溯到原始文檔中去。
首先,我們使用 WebBaseLoader 加載兩個(gè)網(wǎng)頁的文檔,在這個(gè)例子中,我們加載了 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,設(shè)置為 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 兩個(gè)模塊,分別用于存儲(chǔ)原始文檔和總結(jié)文檔。InMemoryByteStore 是一個(gè)內(nèi)存中的存儲(chǔ)層,用于存儲(chǔ)原始文檔,而 Chroma 則是一個(gè)文檔向量數(shù)據(jù)庫,用于存儲(chǔ)文檔的向量表示。
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 類幫助我們在一個(gè)統(tǒng)一的接口中管理文檔和向量存儲(chǔ),使得檢索過程更加高效。
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]
將總結(jié)文檔添加到 Chroma 向量數(shù)據(jù)庫中,同時(shí)在 InMemoryByteStore 中關(guān)聯(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 進(jìn)行相似性檢索,k=1 表示只返回最相關(guān)的一個(gè)文檔。然后使用 retriever 進(jìn)行檢索,n_results=1 表示只返回一個(gè)文檔結(jié)果。
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是一種常用的特殊向量方法,它為段落中的每個(gè)標(biāo)記生成一個(gè)受上下文影響的向量,同時(shí)也會(huì)為查詢中的每個(gè)標(biāo)記生成向量。然后,每個(gè)文檔的得分是每個(gè)查詢嵌入與任何文檔嵌入的最大相似度之和。
可以使用RAGatouille工具來快速實(shí)現(xiàn)ColBERT,首先引入RAGatouille。
from ragatouille import RAGPretrainedModel
RAG = RAGPretrainedModel.from_pretrained("colbert-ir/colbertv2.0")
然后我們獲取文檔數(shù)據(jù),這里我們選擇了使用wiki頁面
最后,完成索引的構(gòu)建,自動(dòng)使用ColBERT方法完成索引。
RAG.index(
collection=[full_document],
index_name="Miyazaki-123",
max_document_length=180,
split_documents=True,)
4. 分層索引
分層索引,指的是帶層級結(jié)構(gòu)的去索引,比如可以先從關(guān)系數(shù)據(jù)庫里索引找出對應(yīng)的關(guān)系,然后再利用索引出的關(guān)系再進(jìn)一步去搜尋basic數(shù)據(jù)庫。前文介紹的多層表達(dá)索引也屬于分層索引的一種。
還有一種更有效的分層索引方法叫做Raptor,Recursive Abstractive Processing for Tree-Organized Retrieval,該方法核心思想是將doc構(gòu)建為一棵樹,然后逐層遞歸的查詢,如下圖所示:
RAPTOR 根據(jù)向量遞歸地對文本塊進(jìn)行聚類,并生成這些聚類的文本摘要,從而自下而上構(gòu)建一棵樹。聚集在一起的節(jié)點(diǎn)是兄弟節(jié)點(diǎn);父節(jié)點(diǎn)包含該集群的文本摘要。這種結(jié)構(gòu)使 RAPTOR 能夠?qū)⒋聿煌墑e文本的上下文塊加載到 LLM 的上下文中,以便它能夠有效且高效地回答不同層面的問題。
查詢有兩種方法,基于樹遍歷(tree traversal)和折疊樹(collapsed tree)。遍歷是從 RAPTOR 樹的根層開始,然后逐層查詢;折疊樹就是全部平鋪,用ANN庫查詢。
Raptor是一種非常高級和復(fù)雜的方法,源代碼也相對比較復(fù)雜,這里就不貼出來了,只從整體上介紹一下Raptor的邏輯。大家可以通過上文介紹的方法來獲取源碼。
首先,我們使用LangChain 的 LCEL 文檔作為輸入數(shù)據(jù),并對文檔進(jìn)行分塊以適合我們的 LLM 上下文窗口,生成全局嵌入列表,并將維度減少到2來簡化生成的聚類,并可視化。
然后,為每個(gè)Raptor步驟定義輔助函數(shù),并構(gòu)建樹。這一段代碼是整個(gè)Raptor中最復(fù)雜的一段,其主要做了以下事情:
- global_cluster_embeddings使用UAMP算法對所有的Embeddings進(jìn)行全局降維,local_cluster_embeddings則使用UAMP算法進(jìn)行局部降維。
- get_optimal_clusters函數(shù)使用高斯混合模型的貝葉斯信息準(zhǔn)則 (BIC) 確定最佳聚類數(shù)。
- GMM_cluster函數(shù)使用基于概率閾值的高斯混合模型 (GMM) 進(jìn)行聚類嵌入,返回包含聚類標(biāo)簽和確定的聚類數(shù)量的元組。
- Perform_clustering函數(shù)則對嵌入執(zhí)行聚類,首先全局降低其維數(shù),然后使用高斯混合模型進(jìn)行聚類,最后在每個(gè)全局聚類內(nèi)執(zhí)行局部聚類。
- Embed_cluster_texts函數(shù)則用于嵌入文本列表并對其進(jìn)行聚類,返回包含文本、其嵌入和聚類標(biāo)簽的 DataFrame。
- Embed_cluster_summarize_texts函數(shù)首先為文本生成嵌入,根據(jù)相似性對它們進(jìn)行聚類,擴(kuò)展聚類分配以便于處理,然后匯總每個(gè)聚類內(nèi)的內(nèi)容。
- recursive_embed_cluster_summarize函數(shù)遞歸地嵌入、聚類和匯總文本,直至指定級別或直到唯一聚類的數(shù)量變?yōu)?1,并在每個(gè)級別存儲(chǔ)結(jié)果。
接下來,生成最終摘要,有兩種方法:
- 樹遍歷檢索:樹的遍歷從樹的根級開始,并根據(jù)向量嵌入的余弦相似度檢索節(jié)點(diǎn)的前 k 個(gè)文檔。因此,在每一級,它都會(huì)從子節(jié)點(diǎn)檢索前 k 個(gè)文檔。
- 折疊樹檢索:折疊樹檢索是一種更簡單的方法。它將所有樹折疊成一層,并根據(jù)查詢向量的余弦相似度檢索節(jié)點(diǎn),直到達(dá)到閾值數(shù)量的標(biāo)記。
接下來,我們將提取數(shù)據(jù)框文本、聚類文本、最終摘要文本,并將它們組合起來,創(chuàng)建一個(gè)包含根文檔和摘要的大型文本列表。然后將該文本存儲(chǔ)到向量存儲(chǔ)中,構(gòu)建索引,并創(chuàng)建查詢引擎
最后,用一個(gè)實(shí)際問題進(jìn)行檢驗(yàn),可以看到實(shí)際的回復(fù)內(nèi)容還是比較準(zhǔn)確的。
# 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)化索引的四種高級方法就介紹完了。
總結(jié)
在這篇文章中,風(fēng)叔詳細(xì)介紹了優(yōu)化Indexing(索引)的具體方法,包括Chunk optimization(塊優(yōu)化)、Multi-representation indexing(多層表達(dá)索引)、Specialized embeddings(特殊嵌入)和Hierachical Indexing(多級索引)這四種優(yōu)化方案。
在下一篇文章中,風(fēng)叔將重點(diǎn)圍繞Query Translation(查詢轉(zhuǎn)換)環(huán)節(jié),介紹精準(zhǔn)識(shí)別用戶查詢意圖的五種高級優(yōu)化方法。
本文由人人都是產(chǎn)品經(jīng)理作者【風(fēng)叔】,微信公眾號(hào):【風(fēng)叔云】,原創(chuàng)/授權(quán) 發(fā)布于人人都是產(chǎn)品經(jīng)理,未經(jīng)許可,禁止轉(zhuǎn)載。
題圖來自Unsplash,基于 CC0 協(xié)議。
- 目前還沒評論,等你發(fā)揮!