檢索增強生成 (RAG) 應用中,始終存在兩種方法之間的權衡:嵌入整個文檔以獲得更好的上下文,或將其分解成較小的塊以實現更精確的檢索。
嵌入整個文檔可以捕捉全局信息,但可能會丟失重要細節;而較短的塊可以保留細節,但常常會忽略整體上下文。
延遲分塊提供了一種解決方案,它在保持完整文檔上下文的同時,將其分割成更小、更容易處理的塊。
本文將介紹延遲分塊作為傳統樸素分塊方法的更好替代方案,並逐步演示其實現方法。
使用檢索增強生成 (RAG) 和 LangChain 集成外部數據與大型語言模型 (LLM)。探索課程
在 RAG 管道中,文檔在嵌入並存儲到向量數據庫之前會被分解成較小的塊。每個塊都獨立處理,並在查詢時用於檢索。然而,這種“樸素分塊”方法常常會丟失重要的長距離上下文。
問題在於,傳統的分塊方法在分割文檔時沒有考慮信息的關聯方式。例如,在關於巴黎的文檔中,“這座城市”這個短語可能最終與“巴黎”所在的塊不同。如果沒有完整的上下文,檢索模型可能難以關聯這些引用,從而導致結果不夠準確。在長文檔中,關鍵上下文分散在多個部分,這個問題更加嚴重。
延遲分塊通過更改分割文檔的時間來解決這個問題。延遲分塊不是首先將文檔分解成塊,而是使用長上下文模型嵌入整個文檔。只有在此之後,它才會將文檔分割成較小的塊。
延遲分塊的主要優點:
使用像 Jina 的 jinaai/jina-embeddings-v2-base-en 這樣的長上下文模型(支持最多 8192 個標記),延遲分塊允許在將大型文本部分分割成塊之前有效地嵌入它們。
這是一個使用 Jina 的長上下文嵌入模型實現延遲分塊的分步指南。您可以在此處免費獲取 Jina 的 API 密鑰,我們將使用以下輸入文本作為演示:
<code>input_text = """Berlin is the capital and largest city of Germany, both by area and by population. Its more than 3.85 million inhabitants make it the European Union's most populous city, as measured by population within city limits. The city is also one of the states of Germany, and is the third smallest state in the country in terms of area."""</code>
首先,使用您的 Jina API 密鑰和下面的輔助函數將輸入文本分解成塊。這些塊帶有跨度註釋,有助於稍後分割文檔嵌入。 Jina 的 API 使用段落或句子中斷等自然邊界來確保塊有意義並保留其含義。
<code>import json import requests def custom_tokenize_jina_api(input_text: str): url = '<https:></https:>' headers = { 'Content-Type': 'application/json', 'Authorization': 'Bearer ENTER_YOUR_JINA_API_KEY' } data = { "content": input_text, "tokenizer": "o200k_base", "return_tokens": "true", "return_chunks": "true", "max_chunk_length": "1000" } # Make the API request response = requests.post(url, headers=headers, json=data) response_data = response.json() chunks = response_data.get("chunks", []) i = 1 j = 1 span_annotations = [] for x in response_data['tokens']: if j == 1: j = len(x) else: j = len(x) + i span_annotations.append((i, j)) i = j return chunks, span_annotations chunks, span_annotations = custom_tokenize_jina_api(input_text) print(chunks) print(span_annotations)</code>
<code>['Berlin is the capital and largest city of Germany, both by area and by population.\n\n', "Its more than 3.85 million inhabitants make it the European Union's most populous city, as measured by population within city limits.\n\n", 'The city is also one of the states of Germany, and is the third smallest state in the country in terms of area.'] [(1, 17), (17, 44), (44, 69)]</code>
首先,使用與長上下文模型兼容的標記器,例如 Jina 的 embeddings-v2-base-en,將整個文檔分解成標記。接下來,使用長上下文轉換器模型為每個標記創建嵌入。這意味著文檔中的每個單詞或標記都會獲得其獨特的嵌入,以捕捉其含義。
<code>from transformers import AutoModel from transformers import AutoTokenizer # load model and tokenizer tokenizer = AutoTokenizer.from_pretrained('jinaai/jina-embeddings-v2-base-en', trust_remote_code=True) model = AutoModel.from_pretrained('jinaai/jina-embeddings-v2-base-en', trust_remote_code=True) inputs = tokenizer(input_text, return_tensors='pt') model_output = model(**inputs) model_output[0].shape</code>
<code>torch.Size([1, 71, 768]) # 71 代表整个文档中的标记数</code>
一旦您擁有整個文檔的標記嵌入,您就可以進行延遲分塊了。使用步驟一中的跨度註釋將這些標記嵌入分割成較小的塊。然後,應用平均池化來平均每個塊內的嵌入,為每個塊創建一個單一嵌入。我們現在有了包含整個文檔強大上下文信息的塊嵌入。
<code>def late_chunking( model_output: 'BatchEncoding', span_annotation: list, max_length=None ): token_embeddings = model_output[0] outputs = [] for embeddings, annotations in zip(token_embeddings, span_annotation): if ( max_length is not None ): # remove annotations which go bejond the max-length of the model annotations = [ (start, min(end, max_length - 1)) for (start, end) in annotations if start = 1 ] pooled_embeddings = [ embedding.detach().cpu().numpy() for embedding in pooled_embeddings ] outputs.append(pooled_embeddings) return outputs</code>
<code>embeddings = late_chunking(model_output, [span_annotations])[0] len(embeddings)</code>
<code>3 # 与步骤 1 中的块数匹配</code>
為了了解延遲分塊的優勢,讓我們將其與傳統分塊進行比較:
<code>embeddings_traditional_chunking = model.encode(chunks)</code>
<code>import numpy as np cos_sim = lambda x, y: np.dot(x, y) / (np.linalg.norm(x) * np.linalg.norm(y)) q = "Berlin" berlin_embedding = model.encode(q) print(q) print('\n') for chunk, new_embedding, trad_embeddings in zip(chunks, embeddings, embeddings_traditional_chunking): print(chunk.strip()) print(f'Late chunking:', cos_sim(berlin_embedding, new_embedding)) print(f'Traditional chunking:', cos_sim(berlin_embedding, trad_embeddings)) print("------------------------------------------------------------------")</code>
<code>Berlin Berlin is the capital and largest city of Germany, both by area and by population. Late chunking: 0.84954596 Traditional chunking: 0.84862185 ------------------------------------------------------------------ Its more than 3.85 million inhabitants make it the European Union's most populous city, as measured by population within city limits. Late chunking: 0.82489026 Traditional chunking: 0.70843375 ------------------------------------------------------------------ The city is also one of the states of Germany, and is the third smallest state in the country in terms of area. Late chunking: 0.84980094 Traditional chunking: 0.7534553 ------------------------------------------------------------------</code>
正如您在第二個和第三個塊中看到的,與單詞“Berlin”相比,傳統分塊顯示相似度評分為 70-75%。然而,使用延遲分塊(保持整個文檔的上下文),這些分數上升到 82-84%。這表明延遲分塊在保留上下文和創建更有意義的嵌入方面做得更好,從而產生更準確的搜索結果。
延遲分塊是對文檔檢索系統(尤其是在 RAG 管道中)的重大改進。通過等到文檔完全嵌入後再分割文檔,延遲分塊在每個塊中保留了完整的上下文。這導致更準確和更有意義的嵌入。
使用 LangChain 實現 RAG 以創建用於回答有關技術文檔問題的聊天機器人。探索項目
以上是拉格的晚期:與Jina AI實施的詳細內容。更多資訊請關注PHP中文網其他相關文章!