RAG分塊策略:主流方法(遞迴、jina-seg)+前沿推薦(Meta-chunking、Late chunking、SLM-SFT)

汀、人工智能發表於2024-12-10

RAG分塊策略:主流方法(遞迴、jina-seg)+前沿推薦(Meta-chunking、Late chunking、SLM-SFT)

大多數常用的資料分塊方法(chunking)都是基於規則的,採用 fixed chunk size(譯者注:將資料或文字按照固定的大小進行資料分塊)或 overlap of adjacent chunks(譯者注:讓相鄰的資料塊具有重疊內容,確保資訊不會丟失。) 等技術。對於具有多個層級結構的文件,可以使用 Langchain 提供的 RecursiveCharacterTextSplitter,這種方法允許將文件按照不同的層級進行分割。

然而,在實際應用中,由於預定義的規則(比如資料分塊大小(chunk size)或重疊部分的大小(size of overlapping parts))過於死板,基於規則的資料分塊方法很容易導致檢索到的上下文(retrieval contexts)不完整或包含 noise(譯者注:指不需要的、干擾性的資訊或資料,可能會對分析或處理造成干擾或誤導的資料。) 的資料塊過大等問題

  • 長文字向量化的挑戰

    在基於 Transformer 架構的向量化模型中,每個詞彙都會被對映為一個高維向量。為了表示整段文字的語義,通常採用對詞向量取平均,或使用特殊標記(如 [CLS])位置的向量作為整體表示。然而,當直接對過長的文字進行向量化時,會面臨以下挑戰:

    • 語義資訊稀釋:長文字往往涵蓋多個主題或觀點,整體向量難以準確捕捉細節語義,導致語義資訊被稀釋,無法充分體現文字的核心內容。

    • 計算開銷增大:處理長文字需要更多的計算資源和儲存空間,增加了模型的計算複雜度,影響系統的效能和效率。

    • 檢索效率降低:過長的向量在檢索過程中可能會降低匹配精度,導致檢索結果的相關性下降,同時也會降低檢索的速度和效率。

  • 提升檢索和生成質量的必要性

    為了克服上述挑戰,合理的文字分塊策略顯得尤為重要。透過對文字進行適當的切分,可以有效提升檢索和生成的質量:

    • 提高檢索準確率:將文字分塊後,得到的文字片段具有更精細的粒度,能夠更準確地匹配使用者的查詢意圖,提升檢索結果的相關性和準確度。

    • 最佳化系統效能:縮短單個文字塊的長度,減少了模型在向量化和檢索過程中的計算和儲存開銷,提高了系統的處理效率和響應速度。

    • 增強大模型的回答質量:為大語言模型提供更相關和精煉的文字塊,有助於模型更好地理解語境,從而生成更準確、連貫和貼切的回答。

透過對文字進行合理的分塊,不僅可以提高向量化過程中的語義表達能力,還能在檢索階段取得更高的匹配精度,最終使得 RAG 系統能夠為使用者提供更優質的服務。

1.文字分塊策略對大模型輸出的影響

1.1 文字分塊過長的影響

在構建 RAG(Retrieval-Augmented Generation)系統時,文字分塊的長度對大模型的輸出質量有著至關重要的影響。過長的文字塊會帶來一系列問題:

  1. 語義模糊:當文字塊過長時,在向量化過程中,細節語義資訊容易被平均化或淡化。這是因為向量化模型需要將大量的詞彙資訊壓縮成固定長度的向量表示,導致無法精準捕捉文字的核心主題和關鍵細節。結果就是,生成的向量難以代表文字的重要內容,降低了模型對文字理解的準確性。

  2. 降低召回精度:在檢索階段,系統需要根據使用者的查詢從向量資料庫中檢索相關文字。過長的文字塊可能涵蓋多個主題或觀點,增加了語義的複雜性,導致檢索模型難以準確匹配使用者的查詢意圖。這樣一來,召回的文字相關性下降,影響了大模型生成答案的質量。

  3. 輸入受限:大語言模型(LLM)對輸入長度有嚴格的限制。過長的文字塊會佔據更多的輸入空間,減少可供輸入的大模型的文字塊數量。這限制了模型能夠獲取的資訊廣度,可能導致遺漏重要的上下文或相關資訊,影響最終的回答效果。

1.2 文字分塊過短的影響

相反,過短的文字塊也會對大模型的輸出產生不利影響,具體表現為:

  1. 上下文缺失:短文字塊可能缺乏必要的上下文資訊。上下文對於理解語言的意義至關重要,缺乏上下文的文字塊會讓模型難以準確理解文字的含義,導致生成的回答不完整或偏離主題。

  2. 主題資訊丟失:段落或章節級別的主題資訊需要一定的文字長度來表達。過短的文字塊可能只包含片段資訊,無法完整傳達主要觀點或核心概念,影響模型對整體內容的把握。

  3. 碎片化問題:大量的短文字塊會導致資訊碎片化,增加檢索和處理的複雜度。系統需要處理更多的文字塊,增加了計算和儲存的開銷。同時,過多的碎片化資訊可能會干擾模型的判斷,降低系統效能和回答質量。

透過上述分析,可以得出結論:合理的文字分塊策略是提升 RAG 系統效能和大模型回答質量的關鍵。為了在實際應用中取得最佳效果,需要在以下方面進行權衡和最佳化:

  1. 根據文字內容選擇切分策略:不同型別的文字適合不同的切分方法。

    • 邏輯緊密的文字:對於論文、技術文件等段落內邏輯緊密的文字,應儘量保持段落的完整性,避免過度切分,以保留完整的語義和邏輯結構。

    • 語義獨立的文字:對於法規條款、產品說明書等句子間邏輯相對獨立的文字,可以按照句子進行切分。這種方式有助於精確匹配特定的查詢內容,提高檢索的準確性。

  2. 考慮向量化模型的效能:評估所使用的向量化模型對於不同長度文字的處理能力。

    • 長文字處理:如果向量化模型在處理長文字時容易丟失資訊,應適當縮短文字塊的長度,以提高向量表示的精確度。

    • 短文字最佳化:對於能夠有效處理短文字的模型,可以適當切分文字,但要注意保留必要的上下文資訊。

  3. 關注大模型的輸入限制:大語言模型對輸入長度有一定的限制,需要確保召回的文字塊能夠全部輸入模型。

    • 輸入長度最佳化:在切分文字塊時,控制每個塊的長度,使其既包含完整的語義資訊,又不超過模型的輸入限制。

    • 資訊覆蓋:確保切分後的文字塊能夠覆蓋知識庫中的關鍵資訊,避免遺漏重要內容。

  4. 實驗與迭代:沒有一種放之四海而皆準的最佳實踐,需要根據具體的應用場景進行實驗和調整。

    • 效能評估:透過實驗評估不同切分策略對檢索準確性和生成質量的影響,從而選擇最適合的方案。

    • 持續最佳化:根據模型的表現和使用者反饋,不斷最佳化切分策略,提升系統的整體效能。

2. 常見的文字分塊策略

在 RAG(Retrieval-Augmented Generation,檢索增強生成)系統中,文字分塊策略的選擇對系統效能和大模型的生成質量有著重要影響。合理的文字分塊能提高檢索準確性,併為大模型生成提供更好的上下文支援。下面,將深入探討幾種常見的文字分塊方法及其應用場景。

常用的文字分塊方法包括:固定大小分塊、基於 NTLK 分塊、特殊格式分塊、深度學習模型分塊、智慧體式分塊

2.1 固定大小文字切塊(遞迴方法)

固定大小文字切塊是最簡單直觀的文字分塊方法。它按照預先設定的固定長度,將文字劃分為若干塊。這種方法實現起來相對容易,但在實際應用中,需要注意以下幾點:

  • 問題與挑戰

    • 上下文割裂:簡單地按照固定字元數截斷文字,可能會打斷句子或段落,導致上下文資訊丟失。這會影響後續的文字向量化效果和語義理解。

    • 語義完整性受損:文字塊可能包含不完整的句子或思想,影響檢索階段的匹配精度,以及大模型生成回答的質量。

  • 改進方法

    • 引入重疊:在相鄰文字塊之間引入一定的重疊部分,確保上下文的連貫性。比如,每個文字塊與前一個塊有 50 個字元的重疊。這有助於保留句子的完整性和段落的連貫性。

    • 智慧截斷:在切分文字時,儘量選擇在標點符號或段落結束處進行截斷,而不是嚴格按照字元數。這可以避免打斷句子,保持語義的完整性。

  • 實踐工具:LangChain 的 RecursiveCharacterTextSplitter**

大模型應用開發框架 LangChain 提供了 RecursiveCharacterTextSplitter,最佳化了固定大小文字切塊的缺陷,推薦在通用文字處理中使用。

使用示例

from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=200,
    chunk_overlap=50,
    length_function=len,
    separators=["\n", "。", ""]
)
text = "..."  # 待處理的文字
texts = text_splitter.create_documents([text])
for doc in texts:
    print(doc)

引數說明

  • chunk_size:文字塊的最大長度(如 200 個字元)。

  • chunk_overlap:相鄰文字塊之間的重疊長度(如 50 個字元)。

  • length_function:用於計算文字長度的函式,預設為 len

  • separators:定義了一組分割符列表,用於在切分文字時優先選擇合適的位置。

工作原理

RecursiveCharacterTextSplitter 按照 separators 中的分割符順序,遞迴地對文字進行切分:

  1. 初步切分:使用第一個分割符(如 "\n",表示段落分隔)對文字進行初步切分。

  2. 檢查塊大小:如果得到的文字塊長度超過了 chunk_size,則使用下一個分割符(如 "。",表示句子分隔)進一步切分。

  3. 遞迴處理:依次使用剩餘的分割符,直到文字塊長度符合要求或無法再切分。

  4. 合併塊:如果相鄰的文字塊合併後長度不超過 chunk_size,則進行合併,確保塊的長度儘可能接近 chunk_size,同時保留上下文完整性。

2.2 基於 NLTK 、spaCy 的文字切塊

NLTK(Natural Language Toolkit) 是廣泛使用的 Python 自然語言處理庫,提供了豐富的文字處理功能。其中,sent_tokenize 方法可用於自動將文字切分為句子。

原理sent_tokenize 基於論文《Unsupervised Multilingual Sentence Boundary Detection》的方法,使用無監督演算法為縮寫詞、搭配詞和句子開頭的詞建立模型,然後利用這些模型識別句子邊界。這種方法在多種語言(主要是歐洲語言)上都取得了良好效果。

  • 預訓練模型缺失:NLTK 官方並未提供中文分句模型的預訓練權重,需要使用者自行訓練。

  • 訓練介面可用:NLTK 提供了訓練介面,使用者可以基於自己的中文語料庫訓練分句模型。

  • 在 LangChain 中的應用

LangChain 整合了 NLTK 的文字切分功能,方便使用者直接呼叫。

使用示例

from langchain.text_splitter import NLTKTextSplitter
text_splitter = NLTKTextSplitter()
text = "..."  # 待處理的文字
texts = text_splitter.split_text(text)
for doc in texts:
    print(doc)

  • 擴充套件:基於 spaCy 的文字切塊

spaCy 是另一款強大的自然語言處理庫,具備更高階的語言分析能力。LangChain 也整合了 spaCy 的文字切分方法。

使用方法

只需將 NLTKTextSplitter 替換為 SpacyTextSplitter


from langchain.text_splitter import SpacyTextSplitter

text_splitter = SpacyTextSplitter()

text = "..."  # 待處理的文字
texts = text_splitter.split_text(text)

for doc in texts:
    print(doc)

提示:使用 spaCy 時,需要先下載對應語言的模型。例如,處理中文文字時,需要下載中文模型包。

2.3 特殊格式文字切塊(HTML、Markdown)

在實際應用中,常常需要處理具有特殊內在結構的文字,如 HTML、Markdown、LaTeX、程式碼檔案 等。這些文字的結構資訊對於理解其內容至關重要,簡單的文字切分方法可能會破壞其原有結構,導致上下文資訊丟失。

  • 保留結構資訊:在切分文字時,應儘量保留其內在的結構,如標籤、標題、程式碼塊等。

  • 減少上下文損失:避免在關鍵位置切分文字,以免丟失重要的上下文資訊。

  • LangChain 提供的特殊文字切塊方法

LangChain 為使用者提供了針對多種特殊格式文字的切塊類,方便使用者處理不同型別的文字。

表 4-1 LangChain 提供的特殊文字切塊方法

文字格式 類名
Python PythonTextSplitter
markdown MarkdownTextSplitter
latex LatexTextSplitter
html HTMLHeaderTextSplitter

以處理 Markdown 文字為例:


from langchain.text_splitter import MarkdownTextSplitter

text_splitter = MarkdownTextSplitter()

text = "..."  # 待處理的 Markdown 文字
texts = text_splitter.split_text(text)

for doc in texts:
    print(doc)

這些特殊文字切塊類針對不同的文字格式,預設了適合的分割符列表,然後呼叫 RecursiveCharacterTextSplitter 進行進一步的切分。例如:

  • PythonCodeTextSplitter:針對 Python 程式碼的結構,設定適合的分隔符,如函式定義、類定義、註釋等。

  • MarkdownTextSplitter:根據 Markdown 的結構,如標題、列表、段落等,進行切分。

  • LatexTextSplitter:識別 LaTeX 文件的章節、公式、環境等進行切分。

  • HTMLHeaderTextSplitter:針對 HTML 文件的標籤結構,按照元素層級進行切分。

  • 4.3.5 自定義擴充套件

LangChain 還預定義了其他程式語言(如 Go、C++、Java)等的分割符列表,方便使用者快速定義新的文字切塊類。如果需要處理未提供的文字格式,可以參照已有的類實現。

自定義示例:建立一個用於切分 Java 程式碼的文字切塊類。

from langchain.text_splitter import RecursiveCharacterTextSplitter

class JavaCodeTextSplitter(RecursiveCharacterTextSplitter):
    def __init__(self, **kwargs):
        separators = [
            "\n\n",  # 空行
            "\n",    # 換行
            ";",     # 語句結束
            " ",     # 空格
            ""       # 無分隔符
        ]
        super().__init__(separators=separators, **kwargs)

text_splitter = JavaCodeTextSplitter(chunk_size=200, chunk_overlap=50)

text = "..."  # 待處理的 Java 程式碼
texts = text_splitter.split_text(text)

for doc in texts:
    print(doc)

透過自定義分割符列表和引數設定,可以靈活地適應不同格式文字的切分需求。

2.4 基於語義文字切塊

  • Embedding-based(譯者注:基於嵌入的資料分塊方法,資料被對映到一個低維空間中,以便更好地捕捉其語義資訊。)

  • Model-based(譯者注:基於模型的資料分塊方法,使用了預先訓練好的模型來進行語義分塊。)

  • LLM-based(譯者注:基於大語言模型的資料分塊方法,使用 LLM 捕捉文字中的語義資訊。)

2.4.1 Embedding-based 方法

LlamaIndex[2] 和 Langchain[3] 都提供了基於嵌入(embedding)的 semantic chunker(譯者注:能夠將文字或資料按照語義資訊進行分塊的工具或演算法。) 。他們的演算法理念大致是差不多的,本文將以 LlamaIndex 為例進行解讀。

請注意,需要安裝最新版本的 LlamaIndex ,才能使用 LlamaIndex 中的 semantic chunker。我之前安裝的 LlamaIndex 版本號是 0.9.45,並沒有包含這個演算法。因此,我建立了一個新的 conda 虛擬環境,並安裝了更新版本的 LlamaIndex —— 0.10.12:

pip install llama-index-core

pip install llama-index-readers-file

pip install llama-index-embeddings-openai

值得一提的是,0.10.12 版本的 LlamaIndex 可以根據需求安裝所需的元件或模組,因此本文僅安裝一些關鍵元件。

(llamaindex_010) Florian:~ Florian$ pip list | grep llama
llama-index-core              0.10.12
llama-index-embeddings-openai 0.1.6
llama-index-readers-file 0.1.5
llamaindex-py-client          0.1.13

測試程式碼如下所示:

from llama_index.core.node_parser import (
    SentenceSplitter,
    SemanticSplitterNodeParser,
)
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.core import SimpleDirectoryReader


import os
os.environ["OPENAI_API_KEY"] = "YOUR_OPEN_AI_KEY"

# load documents
dir_path = "YOUR_DIR_PATH"
documents = SimpleDirectoryReader(dir_path).load_data()


embed_model = OpenAIEmbedding()
splitter = SemanticSplitterNodeParser(
    buffer_size=1, breakpoint_percentile_threshold=95, embed_model=embed_model
)

nodes = splitter.get_nodes_from_documents(documents)
for node in nodes:
 print('-' * 100)
 print(node.get_content())

splitter.get_nodes_from_documents[4] 函式的內部實現邏輯,其主要流程如圖 2 所示:

圖 2 中提到的 “sentences” 是一個 Python 列表,其中每個成員都是包含四個鍵值對的字典,各鍵的含義如下:

  • sentence:當前句子

  • index:當前句子的序號

  • combined_sentence:用於構建滑動視窗(sliding window),包括 [index - self.buffer_size, index, index + self.buffer_size] 三個句子(預設情況下,self.buffer_size = 1),能夠用於計算句子間的語義相關性。將當前句子與其前後的句子合併在一起,可以減少不必要的干擾資訊,從而更有效地抓取連續句子之間的關聯性。

  • combined_sentence_embedding:combined_sentence 的嵌入向量

透過以上分析可以明顯看出,基於嵌入向量的 semantic chunking(譯者注:根據語義資訊將文字或資料分成有意義的片段或塊的過程) 方法本質上是基於滑動視窗(combined_sentence)計算文字相似度。那些相鄰且符合閾值的句子會被歸入一個語義塊。

專案路徑中只有一份 BERT 論文文件 [5]。以下是執行結果:

(llamaindex_010) Florian:~ Florian$ python /Users/Florian/Documents/june_pdf_loader/test_semantic_chunk.py 
...
...
----------------------------------------------------------------------------------------------------
We argue that current techniques restrict the
power of the pre-trained representations, espe-
cially for the fine-tuning approaches. The ma-
jor limitation is that standard language models are
unidirectional, and this limits the choice of archi-
tectures that can be used during pre-training. For
example, in OpenAI GPT, the authors use a left-to-
right architecture, where every token can only at-
tend to previous tokens in the self-attention layers
of the Transformer (Vaswani et al., 2017). Such re-
strictions are sub-optimal for sentence-level tasks,
and could be very harmful when applying fine-
tuning based approaches to token-level tasks such
as question answering, where it is crucial to incor-
porate context from both directions.
In this paper, we improve the fine-tuning based
approaches by proposing BERT: Bidirectional
Encoder Representations from Transformers.
BERT alleviates the previously mentioned unidi-
rectionality constraint by using a “masked lan-
guage model” (MLM) pre-training objective, in-
spired by the Cloze task (Taylor, 1953). The
masked language model randomly masks some of
the tokens from the input, and the objective is to

predict the original vocabulary id of the
maskedarXiv:1810.04805v2  [cs.CL]  24 May 2019
----------------------------------------------------------------------------------------------------
word based only on its context. Unlike left-to-
right language model pre-training, the MLM ob-
jective enables the representation to fuse the left
and the right context, which allows us to pre-
train a deep bidirectional Transformer. In addi-
tion to the masked language model, we also use
a “next sentence prediction” task that jointly pre-
trains text-pair representations. The contributions
of our paper are as follows:
• We demonstrate the importance of bidirectional
pre-training for language representations. Un-
like Radford et al. (2018), which uses unidirec-
tional language models for pre-training, BERT
uses masked language models to enable pre-
trained deep bidirectional representations. This
is also in contrast to Peters et al. 
----------------------------------------------------------------------------------------------------
...
...

測試結果表明,使用這種資料分塊方法,得到的資料塊粒度相對較粗。

圖 2 還顯示,這種方法是 page-based 的(譯者注:這種方法將文字按照頁面進行資料分塊處理,而非其他更小的單位,比如句子或段落。),並沒有直接解決跨多個頁面的資料分塊問題。

通常情況下,基於嵌入的資料分塊方法,其效能嚴重依賴於嵌入模型。實際效果需要未來進一步評估。

2.4.2 Model-based 方法

基於 BERT 的文字切分方法

為了使 BERT 模型能夠學習兩個句子之間的關係,在其預訓練過程中設計了一個二分類任務:同時向 BERT 中輸入兩個句子,預測第二個句子是否是第一個句子的下一句。基於這一思想,可以設計一種最樸素的文字切分方法,其中最小的切分單位是句子

具體而言,在完整的文字上,採用滑動視窗的方式,將相鄰的兩個句子分別輸入 BERT 模型進行二分類預測。如果預測得分較低,說明這兩個句子之間的語義關係較弱,此處可作為文字的切分點。然而,這種方法存在一定的侷限性:

  • 上下文資訊有限:在判斷切分點時,只考慮了前後兩個相鄰句子,未利用更長距離的文字資訊,可能導致切分不準確。

  • 計算效率低:需要對文字中的每對相鄰句子進行預測,計算量較大,難以應對大規模文字處理的需求。

Cross-Segment 模型:引入更長的上下文依賴

針對上述方法的不足,Lukasik 等人在論文_《Text Segmentation by Cross Segment Attention》_中提出了 Cross-Segment 模型。該模型的核心是充分利用更長的上下文資訊,同時提升預測效率。

在 cross-segment BERT 模型(a)中,向模型輸入潛在文字分割點附近的區域性上下文:左邊 k 個tokens,右邊 k 個tokens。在 BERT+Bi-LSTM 模型(b)中,首先使用 BERT 模型對每個句子進行編碼,然後將句子表徵輸入到 Bi-LSTM 中。在 hierarchical BERT 模型(c)中,首先使用 BERT 對每個句子進行編碼,然後將輸出的句子表徵輸入另一個基於 transformer 的模型。Source: Text Segmentation by Cross Segment Attention.[6]

圖 4(a)中的 cross-segment BERT 模型,將文字分割定義為逐句分類任務(譯者注:sentence-by-sentence classification task,逐句判斷其是否是文字分割點)。潛在文字分割點附近的區域性上下文(兩側的 k 個 tokens)被輸入到模型中。與 [CLS] 相對應的隱藏狀態(hidden state)被傳遞給 softmax 分類器,由其決定是否在潛在斷句處進行分割。

主要步驟:

  1. 句子向量化:利用 BERT 模型分別獲取每個句子的向量表示,保留句子的語義資訊。

  2. 跨段落預測:將連續的多個句子的向量表示同時輸入到另一個 BERT 或 LSTM 模型中,一次性預測每個句子是否為文字分段的邊界

這種方法的優勢在於:

  • 綜合考慮上下文:透過同時處理多個句子,模型能夠捕獲更長距離的依賴關係,提高切分的準確性。

  • 提高效率:相比逐一預測相鄰句子的方式,批次處理多個句子能夠提升計算效率。

SeqModel 模型:自適應滑動視窗與上下文建模

雖然 Cross-Segment 模型引入了更長的上下文,但在句子向量化過程中,仍是對每個句子獨立進行的,未充分建模句子間的複雜依賴關係。為此,Zhang 等人在論文_《Sequence Model with Self-Adaptive Sliding Window for Efficient Spoken Document Segmentation》_中提出了 SeqModel 模型,對文字切分方法進行了進一步改進。

SeqModel 的特點:

  • 同時編碼多個句子:利用 BERT 對連續的多個句子進行聯合編碼,直接建模句子間的依賴關係,獲取包含上下文資訊的句子表示。

  • 預測切分邊界:在獲得句子的上下文表示後,模型預測每個句子後是否需要進行文字分割。

  • 自適應滑動視窗:引入了自適應滑動視窗的方法,根據文字內容動態調整視窗大小,在不犧牲準確性的前提下,加快推理速度

這種方法不僅提高了切分的準確性,還使得模型在處理長文字時具有更高的效率。

SeqModel 模型的應用與實現

值得關注的是,SeqModel 模型的預訓練權重已經在「魔搭社群(ModelScope)」上公開發布,支援中文文字的處理。這為開發者在實際專案中應用 SeqModel 模型,提供了便利的條件。

使用方法示例

from modelscope.outputs import OutputKeys
from modelscope.pipelines import pipeline
from modelscope.utils.constant import Tasks

#初始化文字分割任務的pipeline
p = pipeline(task=Tasks.document_segmentation, model='damo/nlp_bert_document-segmentation_chinese-base')

#輸入需要分割的長文字
documents = '這裡輸入您需要分割的長文字內容'

#執行文字分割
result = p(documents=documents)

#輸出分割後的文字結果
print(result[OutputKeys.TEXT])

SeqModel 可透過 ModelScope[10] 框架使用。程式碼如下:

from modelscope.outputs import OutputKeys
from modelscope.pipelines import pipeline
from modelscope.utils.constant import Tasks

p = pipeline(
    task = Tasks.document_segmentation,
    model = 'damo/nlp_bert_document-segmentation_english-base'
)

print('-' * 100)

result = p(documents='We demonstrate the importance of bidirectional pre-training for language representations. Unlike Radford et al. (2018), which uses unidirectional language models for pre-training, BERT uses masked language models to enable pretrained deep bidirectional representations. This is also in contrast to Peters et al. (2018a), which uses a shallow concatenation of independently trained left-to-right and right-to-left LMs. • We show that pre-trained representations reduce the need for many heavily-engineered taskspecific architectures. BERT is the first finetuning based representation model that achieves state-of-the-art performance on a large suite of sentence-level and token-level tasks, outperforming many task-specific architectures. Today is a good day')

print(result[OutputKeys.TEXT])

在測試資料的末尾新增了一句 "Today is a good day(今天是個好日子)",但文字分割處理後的 result 變數中並沒有以任何方式將 "Today is a good day(今天是個好日子)" 分割開。

We demonstrate the importance of bidirectional pre-training for language representations.Unlike Radford et al.(2018), which uses unidirectional language models for pre-training, BERT uses masked language models to enable pretrained deep bidirectional representations.This is also in contrast to Peters et al.(2018a), which uses a shallow concatenation of independently trained left-to-right and right-to-left LMs.• We show that pre-trained representations reduce the need for many heavily-engineered taskspecific architectures.BERT is the first finetuning based representation model that achieves state-of-the-art performance on a large suite of sentence-level and token-level tasks, outperforming many task-specific architectures.Today is a good day

上述方法還有很大改進空間,建議的一種改進方法是建立專門針對某個專案或任務定製的訓練資料,以便進行領域微,這樣可以提高模型的效能。此外,最佳化模型架構也是一個改進點

基於深度學習模型的文字切分方法,利用了預訓練模型對語言的深層次理解,從最初的樸素方法到 Cross-Segment,再到 SeqModel 模型的不斷改進:

  • 樸素方法:以句子為最小單位,利用 BERT 預測相鄰句子的聯絡,但上下文考慮有限,效率較低。

  • Cross-Segment 模型:引入更長的上下文,批次預測切分點,提高了準確性和效率。

  • SeqModel 模型:同時編碼多個句子,建模句子間的依賴關係,採用自適應滑動視窗,進一步提升效能。

這些方法的演進,體現了研究者們在文字切分領域的不懈探索和創新。透過選擇合適的模型和方法,可以更精確地對文字進行切分,提高下游任務的效果,滿足多樣化的實際應用需求。

2.5 LLM-based 方法

這篇題為《 Dense X Retrieval: What Retrieval Granularity Should We Use? 》的論文介紹了一種新的檢索單位,稱為 proposition 。proposition 被定義為文字中的 atomic expressions(譯者注:不能進一步分解的單個語義元素,可用於構成更大的語義單位) ,用於檢索和表達文字中的獨特事實或特定概念,能夠以簡明扼要的方式表達,使用自然語言完整地呈現一個獨立的概念或事實,不需要額外的資訊來解釋。

那麼,如何獲取所謂的 proposition 呢?本文透過構建提示詞並與 LLM 互動來獲取。

LlamaIndex 和 Langchain 都實現了相關演算法,下面將使用 LlamaIndex 進行演示。

LlamaIndex 的實現思路是使用論文中提供的提示詞來生成 proposition :

PROPOSITIONS_PROMPT = PromptTemplate(
    """Decompose the "Content" into clear and simple propositions, ensuring they are interpretable out of
context.
1. Split compound sentence into simple sentences. Maintain the original phrasing from the input
whenever possible.
2. For any named entity that is accompanied by additional descriptive information, separate this
information into its own distinct proposition.
3. Decontextualize the proposition by adding necessary modifier to nouns or entire sentences
and replacing pronouns (e.g., "it", "he", "she", "they", "this", "that") with the full name of the
entities they refer to.
4. Present the results as a list of strings, formatted in JSON.

Input: Title: ¯Eostre. Section: Theories and interpretations, Connection to Easter Hares. Content:
The earliest evidence for the Easter Hare (Osterhase) was recorded in south-west Germany in
1678 by the professor of medicine Georg Franck von Franckenau, but it remained unknown in
other parts of Germany until the 18th century. Scholar Richard Sermon writes that "hares were
frequently seen in gardens in spring, and thus may have served as a convenient explanation for the
origin of the colored eggs hidden there for children. Alternatively, there is a European tradition
that hares laid eggs, since a hare’s scratch or form and a lapwing’s nest look very similar, and
both occur on grassland and are first seen in the spring. In the nineteenth century the influence
of Easter cards, toys, and books was to make the Easter Hare/Rabbit popular throughout Europe.
German immigrants then exported the custom to Britain and America where it evolved into the
Easter Bunny."
Output: [ "The earliest evidence for the Easter Hare was recorded in south-west Germany in
1678 by Georg Franck von Franckenau.", "Georg Franck von Franckenau was a professor of
medicine.", "The evidence for the Easter Hare remained unknown in other parts of Germany until
the 18th century.", "Richard Sermon was a scholar.", "Richard Sermon writes a hypothesis about
the possible explanation for the connection between hares and the tradition during Easter", "Hares
were frequently seen in gardens in spring.", "Hares may have served as a convenient explanation
for the origin of the colored eggs hidden in gardens for children.", "There is a European tradition
that hares laid eggs.", "A hare’s scratch or form and a lapwing’s nest look very similar.", "Both
hares and lapwing’s nests occur on grassland and are first seen in the spring.", "In the nineteenth
century the influence of Easter cards, toys, and books was to make the Easter Hare/Rabbit popular
throughout Europe.", "German immigrants exported the custom of the Easter Hare/Rabbit to
Britain and America.", "The custom of the Easter Hare/Rabbit evolved into the Easter Bunny in
Britain and America." ]

Input: {node_text}
Output:"""
)

在前面的章節 “01 Embedding-based 方法” 中,已經安裝了 LlamaIndex 0.10.12 的關鍵元件。但如果想要使用 DenseXRetrievalPack ,還需要執行 pip install llama-index-llms-openai 安裝。安裝完成後,當前的 LlamaIndex 相關元件如下所示:

(llamaindex_010) Florian:~ Florian$ pip list | grep llama
llama-index-core                    0.10.12
llama-index-embeddings-openai       0.1.6
llama-index-llms-openai             0.1.6
llama-index-readers-file            0.1.5
llamaindex-py-client                0.1.13

在 LlamaIndex 中,DenseXRetrievalPack 是一個需要單獨下載的軟體包。這裡直接在測試程式碼中下載。測試程式碼如下:

from llama_index.core.readers import SimpleDirectoryReader
from llama_index.core.llama_pack import download_llama_pack

import os
os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_KEY"

# Download and install dependencies
DenseXRetrievalPack = download_llama_pack(
    "DenseXRetrievalPack", "./dense_pack"
)

# If you have already downloaded DenseXRetrievalPack, you can import it directly.
# from llama_index.packs.dense_x_retrieval import DenseXRetrievalPack

# Load documents
dir_path = "YOUR_DIR_PATH"
documents = SimpleDirectoryReader(dir_path).load_data()

# Use LLM to extract propositions from every document/node
dense_pack = DenseXRetrievalPack(documents)

response = dense_pack.run("YOUR_QUERY")

這段測試程式碼主要使用 DenseXRetrievalPack 類的建構函式。因此,有必要分析 DenseXRetrievalPack 類的原始碼 [11]。

class DenseXRetrievalPack(BaseLlamaPack):
    def __init__(
        self,
        documents: List[Document],
        proposition_llm: Optional[LLM] = None,
        query_llm: Optional[LLM] = None,
        embed_model: Optional[BaseEmbedding] = None,
        text_splitter: TextSplitter = SentenceSplitter(),
        similarity_top_k: int = 4,
    ) -> None:
        """Init params."""
        self._proposition_llm = proposition_llm or OpenAI(
            model="gpt-3.5-turbo",
            temperature=0.1,
            max_tokens=750,
        )

        embed_model = embed_model or OpenAIEmbedding(embed_batch_size=128)

        nodes = text_splitter.get_nodes_from_documents(documents)
        sub_nodes = self._gen_propositions(nodes)

        all_nodes = nodes + sub_nodes
        all_nodes_dict = {n.node_id: n for n in all_nodes}

        service_context = ServiceContext.from_defaults(
            llm=query_llm or OpenAI(),
            embed_model=embed_model,
            num_output=self._proposition_llm.metadata.num_output,
        )

        self.vector_index = VectorStoreIndex(
            all_nodes, service_context=service_context, show_progress=True
        )

        self.retriever = RecursiveRetriever(
            "vector",
            retriever_dict={
                "vector": self.vector_index.as_retriever(
                    similarity_top_k=similarity_top_k
                )
            },
            node_dict=all_nodes_dict,
        )

        self.query_engine = RetrieverQueryEngine.from_args(
            self.retriever, service_context=service_context
        )

如程式碼所示,該建構函式的思路是首先使用 text_splitter 將文件劃分為 nodes(譯者注:將文件按照其最初的格式分割而成的最小單元。) ,然後呼叫 self._gen_propositions 生成 propositions 來獲取相應的 sub_nodes(譯者注:根據 nodes 生成的 propositions 所對應的文件片段或子集) 。然後,使用 nodes + sub_nodes 構建 VectorStoreIndex,並透過 RecursiveRetriever 進行檢索。遞迴檢索器(recursive retriever)可以透過處理文件的小資料塊(small chunks)來找到所需資訊,就像可以直接去書籍中的某個小節或段落查詢一樣。但是,如果在這些小資料塊(small chunks)中找不到完整的資訊,遞迴檢索器(recursive retriever)會將相關的大資料塊(larger chunks)傳遞到生成階段(generation stage)進一步處理,就像在書中某個小節或段落查詢資料時,如果需要更多資訊,就會翻到相關的章節或整本書一樣。

專案路徑中只有一份 BERT 論文文件。透過除錯,發現 sub_nodes[].text 的內容並非原始文字,裡面的內容已經被改寫過了:

> /Users/Florian/anaconda3/envs/llamaindex_010/lib/python3.11/site-packages/llama_index/packs/dense_x_retrieval/base.py(91)__init__()
     90 
---> 91         all_nodes = nodes + sub_nodes
     92         all_nodes_dict = {n.node_id: n for n in all_nodes}


ipdb> sub_nodes[20]
IndexNode(id_='ecf310c7-76c8-487a-99f3-f78b273e00d9', embedding=None, metadata={}, excluded_embed_metadata_keys=[], excluded_llm_metadata_keys=[], relationships={}, text='Our paper demonstrates the importance of bidirectional pre-training for language representations.', start_char_idx=None, end_char_idx=None, text_template='{metadata_str}\n\n{content}', metadata_template='{key}: {value}', metadata_seperator='\n', index_id='8deca706-fe97-412c-a13f-950a19a594d1', obj=None)
ipdb> sub_nodes[21]
IndexNode(id_='4911332e-8e30-47d8-a5bc-ed7cbaa8e042', embedding=None, metadata={}, excluded_embed_metadata_keys=[], excluded_llm_metadata_keys=[], relationships={}, text='Radford et al. (2018) uses unidirectional language models for pre-training.', start_char_idx=None, end_char_idx=None, text_template='{metadata_str}\n\n{content}', metadata_template='{key}: {value}', metadata_seperator='\n', index_id='8deca706-fe97-412c-a13f-950a19a594d1', obj=None)
ipdb> sub_nodes[22]
IndexNode(id_='83aa82f8-384a-4b06-92c8-d6277c4162bf', embedding=None, metadata={}, excluded_embed_metadata_keys=[], excluded_llm_metadata_keys=[], relationships={}, text='BERT uses masked language models to enable pre-trained deep bidirectional representations.', start_char_idx=None, end_char_idx=None, text_template='{metadata_str}\n\n{content}', metadata_template='{key}: {value}', metadata_seperator='\n', index_id='8deca706-fe97-412c-a13f-950a19a594d1', obj=None)
ipdb> sub_nodes[23]
IndexNode(id_='2ac635c2-ccb0-4e62-88c7-bcbaef3ef38a', embedding=None, metadata={}, excluded_embed_metadata_keys=[], excluded_llm_metadata_keys=[], relationships={}, text='Peters et al. (2018a) uses a shallow concatenation of independently trained left-to-right and right-to-left LMs.', start_char_idx=None, end_char_idx=None, text_template='{metadata_str}\n\n{content}', metadata_template='{key}: {value}', metadata_seperator='\n', index_id='8deca706-fe97-412c-a13f-950a19a594d1', obj=None)
ipdb> sub_nodes[24]
IndexNode(id_='e37b17cf-30dd-4114-a3c5-9921b8cf0a77', embedding=None, metadata={}, excluded_embed_metadata_keys=[], excluded_llm_metadata_keys=[], relationships={}, text='Pre-trained representations reduce the need for many heavily-engineered task-specific architectures.', start_char_idx=None, end_char_idx=None, text_template='{metadata_str}\n\n{content}', metadata_template='{key}: {value}', metadata_seperator='\n', index_id='8deca706-fe97-412c-a13f-950a19a594d1', obj=None)

sub_nodes 和 nodes 之間的關係如圖 7 所示,這個索引結構按照 small-to-big 的方式進行排序組織。

圖 7:按照 small-to-big 的方式進行排序組織的索引結構。

這種索引結構是透過 self._gen_propositions[12] 構建的,程式碼如下:

 async def _aget_proposition(self, node: TextNode) -> List[TextNode]:
 """Get proposition."""
        inital_output = await self._proposition_llm.apredict(
            PROPOSITIONS_PROMPT, node_text=node.text
 )
        outputs = inital_output.split("\n")

        all_propositions = []

 for output in outputs:
 if not output.strip():
 continue
 if not output.strip().endswith("]"):
 if not output.strip().endswith('"') and not output.strip().endswith(
 ","
 ):
                    output = output + '"'
                output = output + " ]"
 if not output.strip().startswith("["):
 if not output.strip().startswith('"'):
                    output = '"' + output
                output = "[ " + output

 try:
                propositions = json.loads(output)
 except Exception:
 # fallback to yaml
 try:
                    propositions = yaml.safe_load(output)
 except Exception:
 # fallback to next output
 continue

 if not isinstance(propositions, list):
 continue

            all_propositions.extend(propositions)

 assert isinstance(all_propositions, list)
        nodes = [TextNode(text=prop) for prop in all_propositions if prop]

 return [IndexNode.from_text_node(n, node.node_id) for n in nodes]

 def _gen_propositions(self, nodes: List[TextNode]) -> List[TextNode]:
 """Get propositions."""
        sub_nodes = asyncio.run(
            run_jobs(
 [self._aget_proposition(node) for node in nodes],
                show_progress=True,
                workers=8,
 )
 )

 # Flatten list
 return [node for sub_node in sub_nodes for node in sub_node]

對每個原始節點(original node),都非同步呼叫 self._aget_proposition,透過 PROPOSITIONS_PROMPT 獲取 LLM 返回的 inital_output,然後根據 inital_output 獲取 propositions 並構建 TextNode。最後,將這些 TextNode 與原始節點(original node)關聯起來,即使用 [IndexNode.from_text_node(n, node.node_id) for n in nodes] 。

有一件事需要多說一句:原論文使用 LLM 生成的 propositions 作為訓練資料,微調了一個文字生成模型。這個文字生成模型目前是公開可訪問的 [13],總體來看,這種利用 LLM 構建 propositions 的資料分塊方法能夠實現更精細的資料分塊。能夠與原始節點(original node)構成一個 small-to-big 的索引結構,從而為 semantic chunking(譯者注:將文字分成具有語義相關性的片段。) 方法提供了一種新思路。

3. 文字分塊的最佳化策略

3.1 保持語義完整性

3.1.1 避免句子拆分

在文字切分過程中,應儘量避免將句子拆分。句子是表達完整語義的基本單位,拆分句子可能導致語義破碎,影響向量化表示的準確性和模型對文字的理解。例如,句子中包含的主謂賓結構或修飾關係在被截斷後,會失去原有的含義,使得模型難以準確捕捉文字的核心內容。

實踐建議

  • 按標點切分:使用句號、問號、感嘆號等標點符號作為切分點,確保每個文字塊包含完整的句子。

  • 分割符的優先順序:在設定分割符時,將句子結束符號置於高優先順序,保證切分過程中優先考慮句子邊界。

3.1.2 考慮段落關聯性

除了句子層面,段落也是表達完整思想的重要單位。段落內的句子通常圍繞一個主題展開,具有緊密的邏輯關聯。將邏輯關聯緊密的段落拆分到不同的文字塊,可能導致上下文割裂,影響模型對整體語義的把握。

實踐建議

  • 段落完整切分:儘量將同一段落的內容保留在同一個文字塊中,避免因切分導致的語義斷裂。

  • 結合主題分割:對於長段落,可以根據主題或語義轉折點進行切分,確保每個文字塊內部主題統一。

3.2 控制文字塊長度

3.2.1 設定合理的長度閾值

文字塊的長度對向量化模型和大語言模型(LLM)的處理效能有直接影響。過長的文字塊可能導致:

  • 向量表示稀釋:重要的語義資訊被淹沒,降低檢索精度。

  • 模型輸入限制:超過大模型的輸入長度,無法處理全部內容。

過短的文字塊則可能缺乏上下文,導致語義不完整。

實踐建議

  • 評估模型效能:根據向量化模型和大模型對不同長度文字的處理效果,設定適當的文字塊長度閾值。

  • 常用長度參考:通常,文字塊長度可以設定為 200~500 個字元,根據具體情況調整。

3.2.2 動態調整

不同型別的文字對文字塊長度的要求可能不同。靈活調整長度閾值,可以更好地適應多樣化的文字內容。

實踐建議

  • 文字型別分析:針對新聞、法律文件、技術手冊等不同型別的文字,分析其結構特點,設定合適的長度。

  • 自適應切分:開發動態調整機制,根據文字內容和結構實時調整文字塊長度,實現個性化處理。

3.3 重疊切分

3.3.1 方法

重疊切分是指在文字塊之間引入一定的重疊部分,使相鄰文字塊共享部分內容。具體方法是:

  • 設定重疊長度:如設定每個文字塊之間有 50 個字元的重疊。

  • 滑動視窗切分:採用滑動視窗的方式切分文字,每次移動的步長小於視窗大小。

3.3.2 優點

  • 保留上下文連線:重疊部分使得相鄰文字塊之間的上下文資訊得到保留,避免因切分導致的語義斷裂。

  • 增強模型理解:大模型在生成回答時,能夠參考前後文資訊,產生更加連貫和準確的結果。

實踐建議

  • 合理設定重疊量:根據文字特點和模型需求,設定適當的重疊長度,既保留上下文,又不增加過多的冗餘資訊。

  • 效率考慮:注意重疊切分會增加文字塊的數量,需權衡處理效率和上下文保留之間的關係。

3.4 結合向量化模型效能

3.4.1 適配模型特性

不同的向量化模型(如 BERT、GPT、Sentence Transformers)在處理文字長度和語義資訊方面有不同的表現。

實踐建議

  • 模型特性研究:深入瞭解所使用向量化模型的特點,對其在不同文字長度下的效能進行評估。

  • 選擇合適策略:根據模型的優勢,選擇短文字或長文字的分塊策略。例如,如果模型對短文字的向量表示效果更好,則應傾向於較短的文字塊。

3.4.2 最佳化向量表示

針對長文字可能出現的語義稀釋問題,可以採用更高階的向量表示方法。

實踐建議

  • 加權平均:對文字中的重要詞彙(如關鍵詞、專有名詞)賦予更高權重,增強它們在向量表示中的影響。

  • 注意力機制:利用注意力機制(Attention)聚焦文字中的關鍵部分,生成更具代表性的向量。

  • 分層編碼:對文字進行層次化編碼,先編碼句子,再編碼段落,逐層組合,保持語義結構。

3.5 考慮大模型的輸入限制

3.5.1 輸入長度控制

大語言模型對輸入文字的長度有嚴格限制(如 GPT-3 的最大輸入長度為 2048 個標記)。在召回階段,需要確保選取的文字塊總長度不超過模型的限制。

實踐建議

  • 統計文字塊長度:在將文字塊輸入模型前,計算其總長度,確保不超出模型限制。

  • 截斷策略:如長度超出限制,可考慮截斷低相關性的內容,保留核心文字塊。

3.5.2 優先順序排序

不同的文字塊對回答的貢獻度不同,應根據其與查詢的相關性進行排序,優先輸入最重要的內容。

實踐建議

  • 相關性評分:為每個召回的文字塊計算與查詢的相關性得分。

  • 擇優選擇:根據相關性得分,從高到低選擇文字塊,直至達到模型的輸入長度限制。

3.5.3 內容精煉

當重要的文字塊過長時,可以對其進行壓縮或摘要,確保核心資訊得以保留。

實踐建議

  • 自動摘要:利用摘要模型對長文字塊生成精簡的摘要。

  • 關鍵句提取:提取文字塊中最能代表核心內容的句子,供大模型參考。

最佳化文字分塊策略需要綜合考慮語義完整性、文字塊長度、向量化模型效能以及大模型的輸入限制等因素。透過避免句子拆分、保持段落關聯、引入重疊切分、適配模型特性和合理控制輸入長度,可以有效提升 RAG 系統的檢索效果和大模型回答的質量。

4. 前沿方法推薦

4.1 Meta-Chunking (2024.8.16)

Meta-Chunking旨在最佳化檢索增強生成(RAG)中的文字塊處理,從而提升知識密集型任務的質量。研究提出在句子和段落之間引入一種新的粒度,即Meta-Chunking,它由段落內具有深層語言邏輯聯絡的多個句子組成。為實現這一概念,研究者設計了基於困惑度(PPL)的分割方法,該方法在效能和速度之間取得平衡,並能精確確定文字塊的邊界。同時,考慮到不同文字的複雜性,研究還提出了結合PPL分割和動態合併的策略,以實現細粒度和粗粒度文字分割之間的平衡。在11個資料集上的實驗表明,Meta-Chunking能有效提升基於RAG的單跳和多跳問答效能。此外,研究還發現PPL分割在不同規模和型別的模型上表現出顯著的靈活性和適應性

  • 標題:Meta-Chunking: Learning Efficient Text Segmentation via Logical Perception,

  • github:https://github.com/IAAR-Shanghai/Meta-Chunking/tree/386dc29b9cfe87da691fd4b0bd4ba7c352f8e4ed

基於一個核心原則:允許塊大小變化,以更有效地捕獲和維護內容的邏輯完整性。這種動態的粒度調整可確保每個分段塊都包含完整且獨立的思想表達,從而避免在分段過程中邏輯鏈中斷。這不僅增強了文件檢索的相關性,還提高了內容清晰度。

如下圖所示,例句呈現出遞進關係,但語義相似度較低,可能導致例句完全分離。

  • 第一種分塊,稱為Margin Sampling Chunking,大概思路是讓LLM來做二分類,大模型輸出是個詞表的機率分佈,這裡他們做了一個對“是” 、 “否”的機率差,判斷是否符合閾值。
  • 第二種分塊,稱為Perplexity Chunking,計算每個句子在上下文下的困惑度(如果困惑度高,說明模型對這段文字比較懵逼,所以不建議切分)。每次找到序列中困惑度最小的句子,並且如果這個句子前後2句都小於當前這個句子,那就可以切分了。算困惑度可以利用固定長度的kv-cache,來保證視訊記憶體問題

Margin Sampling Chunking:

  1. 將文字分割成一系列句子。
  2. 對於相鄰的句子,使用 LLM 進行二元分類,判斷是否需要分割。
  3. LLM 輸出兩個選項的機率,計算機率差異 Margin。
  4. 將 Margin 與預設閾值進行比較,如果 Margin 大於閾值,則分割句子;否則,合併句子。

Perplexity Chunking:

  1. 將文字分割成一系列句子。
  2. 使用 LLM 計算每個句子基於其上下文的 PPL 值。
  3. 分析 PPL 值的分佈特徵,識別潛在的文字塊邊界(即 PPL 值的區域性最小值)。
  4. 將句子分割成多個文字塊,每個文字塊包含一個或多個連續的句子。
  • 兩種策略的優缺點:

    • Margin Sampling Chunking:

      • 優點:可以有效地降低對模型規模的需求,使得小模型也能勝任文字分塊任務。
      • 缺點:分割結果可能受到 LLM 模型的影響,且效率相對較低。
    • Perplexity Chunking:

      • 優點:分割結果更加客觀,效率更高,並能夠有效地捕捉文字的邏輯結構。
      • 缺點:需要分析 PPL 值的分佈特徵,可能需要一定的計算量。

4.2 Late Chunking(jina 2024.8.4)

  • 論文Late Chunking: Contextual Chunk Embeddings Using Long-Context Embedding Models:https://arxiv.org/abs/2409.04701
  • github:https://github.com/jina-ai/late-chunking
  • jina部落格:https://jina.ai/news/late-chunking-in-long-context-embedding-models/
  • 專案連結:https://colab.research.google.com/drive/15vNZb6AsU7byjYoaEtXuNu567JWNzXOz?usp=sharing

想象一下,你在整理一大堆檔案。傳統的方法是先把檔案分成小堆,然後再逐一處理。而後期分塊卻反其道而行之:先對整個檔案進行全面處理,然後再進行分類整理。這聽起來可能有點反直覺,但在AI的世界裡,這種方法卻顯示出了驚人的效果。具體來說,後期分塊是這樣工作的:

  1. 先用一個長上下文嵌入模型(比如 jina-embeddings-v2-small-en,具有 8K 上下文長度)"讀懂"整個文件
  2. 然後再把這個"理解"切成小塊

Late Chunking is now available in jina-embeddings-v3 API. 8192-token

傳統方法涉及使用句子、段落或最大長度限制來預先分割文字。之後,會反覆應用嵌入模型到這些結果塊中。為了為每個塊生成一個嵌入,許多嵌入模型會在這些詞級別嵌入上使用均值池化,以輸出一個單一的嵌入向量。
而“後期分塊”方法首先將嵌入模型的轉換層應用於整個文字或儘可能多的文字。這為文字中的每個標記生成了一個包含整個文字資訊的向量表示序列。隨後,對這個標記向量序列的每一部分應用平均池化,產生考慮整個文字上下文的每個部分的嵌入。與生成獨立同分布(i.i.d.)分塊嵌入的簡單編碼方法不同,後期分塊建立了一組分塊嵌入,其中每個嵌入都“依賴於”之前的嵌入,從而為每個分塊編碼了更多的上下文資訊。

為了解決這個問題,利用了最近的嵌入模型可以處理的長輸入序列jina-embeddings-v2-base-en。這些模型支援更長的輸入文字,例如 8192 個標記jina-embeddings-v2-base-en或大約十頁標準文字。這種大小的文字段不太可能具有隻能透過更大的上下文來解決的上下文依賴關係。然而,仍然需要更小的文字塊的向量表示,部分原因是 LLM 的輸入大小有限,但主要是因為短嵌入向量的資訊容量有限。

簡單的編碼方法(如上圖左側所示)在處理文字之前先對文字進行分塊,使用句子、段落和最大長度限制來先驗地分割文字,然後將嵌入模型應用於生成的塊。而後期分塊則首先將嵌入模型的轉換器部分應用於整個文字或儘可能大的部分。這會為每個標記生成一個向量表示序列,其中包含來自整個文字的文字資訊。為了為文字生成單個嵌入,許多嵌入模型會將這些標記表示應用於均值池化以輸出單個向量。而後期分塊則將均值池化應用於這個標記向量序列的較小段,從而為每個塊生成考慮整個文字的嵌入。

程式碼部分:

#!pip install transformers==4.43.4
#載入要用於嵌入的模型。選擇 jinaai/jina-embeddings-v2-base-en 但任何其他支援均值池的模型都是可能的。然而,具有較大最大上下文長度的模型是首選。
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)

#定義要編碼的文字並將其分割成塊。 chunk_by_sentences函式還返回跨度註釋。這些指定了分塊池所需的每個塊的令牌數量。
def chunk_by_sentences(input_text: str, tokenizer: callable):
    """
    Split the input text into sentences using the tokenizer
    :param input_text: The text snippet to split into sentences
    :param tokenizer: The tokenizer to use
    :return: A tuple containing the list of text chunks and their corresponding token spans
    """
    inputs = tokenizer(input_text, return_tensors='pt', return_offsets_mapping=True)
    punctuation_mark_id = tokenizer.convert_tokens_to_ids('.')
    sep_id = tokenizer.convert_tokens_to_ids('[SEP]')
    token_offsets = inputs['offset_mapping'][0]
    token_ids = inputs['input_ids'][0]
    chunk_positions = [
        (i, int(start + 1))
        for i, (token_id, (start, end)) in enumerate(zip(token_ids, token_offsets))
        if token_id == punctuation_mark_id
        and (
            token_offsets[i + 1][0] - token_offsets[i][1] > 0
            or token_ids[i + 1] == sep_id
        )
    ]
    chunks = [
        input_text[x[1] : y[1]]
        for x, y in zip([(1, 0)] + chunk_positions[:-1], chunk_positions)
    ]
    span_annotations = [
        (x[0], y[0]) for (x, y) in zip([(1, 0)] + chunk_positions[:-1], chunk_positions)
    ]
    return chunks, span_annotations

#在生產中,您應該使用更先進、更強大的分割方法,例如 Jina AI Tokenizer API https://jina.ai/tokenizer#apiform 。

https://jina.ai/segmenter/#apiform

import requests

def chunk_by_tokenizer_api(input_text: str, tokenizer: callable):
    #Define the API endpoint and payload
    url = 'https://tokenize.jina.ai/'
    payload = {
        "content": input_text,
        "return_chunks": "true",
        "max_chunk_length": "1000"
    }

    #Make the API request
    response = requests.post(url, json=payload)
    response_data = response.json()

    #Extract chunks and positions from the response
    chunks = response_data.get("chunks", [])
    chunk_positions = response_data.get("chunk_positions", [])

    #Adjust chunk positions to match the input format
    span_annotations = [(start, end) for start, end in chunk_positions]

    return chunks, span_annotations


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."

#determine chunks
chunks, span_annotations = chunk_by_sentences(input_text, tokenizer)
print('Chunks:\n- "' + '"\n- "'.join(chunks) + '"')


Chunks:
- "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."

#使用傳統的和上下文相關的 Late_chunking 方法對塊進行編碼:

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 < (max_length - 1)
            ]
        pooled_embeddings = [
            embeddings[start:end].sum(dim=0) / (end - start)
            for start, end in annotations
            if (end - start) >= 1
        ]
        pooled_embeddings = [
            embedding.detach().cpu().numpy() for embedding in pooled_embeddings
        ]
        outputs.append(pooled_embeddings)

    return outputs


#chunk before
embeddings_traditional_chunking = model.encode(chunks)

#chunk afterwards (context-sensitive chunked pooling)
inputs = tokenizer(input_text, return_tensors='pt')
model_output = model(**inputs)
embeddings = late_chunking(model_output, [span_annotations])[0]

#比較單詞“Berlin”與詞塊的相似度。上下文相關的分塊池方法的相似度應該更高
import numpy as np

cos_sim = lambda x, y: np.dot(x, y) / (np.linalg.norm(x) * np.linalg.norm(y))

berlin_embedding = model.encode('Berlin')

for chunk, new_embedding, trad_embeddings in zip(chunks, embeddings, embeddings_traditional_chunking):
    print(f'similarity_new("Berlin", "{chunk}"):', cos_sim(berlin_embedding, new_embedding))
    print(f'similarity_trad("Berlin", "{chunk}"):', cos_sim(berlin_embedding, trad_embeddings))
    
similarity_new("Berlin", "Berlin is the capital and largest city of Germany, both by area and by population."): 0.849546
similarity_trad("Berlin", "Berlin is the capital and largest city of Germany, both by area and by population."): 0.84862185
similarity_new("Berlin", " Its more than 3.85 million inhabitants make it the European Union's most populous city, as measured by population within city limits."): 0.82489026
similarity_trad("Berlin", " Its more than 3.85 million inhabitants make it the European Union's most populous city, as measured by population within city limits."): 0.7084338
similarity_new("Berlin", " The city is also one of the states of Germany, and is the third smallest state in the country in terms of area."): 0.84980094
similarity_trad("Berlin", " The city is also one of the states of Germany, and is the third smallest state in the country in terms of area."): 0.7534553

Text Similarity Traditional Similarity Late Chunking
Berlin is the capital and largest city of Germany, both by area and by population." 0.84862185 0.849546
Its more than 3.85 million inhabitants make it the European Union's most populous city, as measured by population within city limits. 0.7084338 0.82489026
The city is also one of the states of Germany, and is the third smallest state in the country in terms of area. 0.7534553 0.84980094

為了透過幾個簡單的示例驗證此方法的有效性,使用BeIR中的一些檢索基準對其進行了測試。這些檢索任務包括一個查詢集、一個文字文件語料庫和一個 QRels 檔案,該檔案儲存了與每個查詢相關的文件 ID 的資訊。為了識別查詢的相關文件,可以對文件進行分塊,將其編碼為嵌入索引,並確定每個查詢嵌入的最相似塊 (kNN)。由於每個塊對應一個文件,因此可以將塊的 kNN 排名轉換為文件的 kNN 排名(對於在排名中多次出現的文件,僅保留第一次出現的文件)。之後,可以將結果排名與對應於真實 QRels 檔案的排名進行比較,並計算 nDCG@10 等檢索指標。

Dataset AVG Document Length (characters) Traditional Chunking (nDCG@10) Late Chunking (nDCG@10) No Chunking (nDCG@10)
SciFact 1498.4 64.20% 66.10% 63.89%
TRECCOVID 1116.7 63.36% 64.70% 65.18%
FiQA2018 767.2 33.25% 33.84% 33.43%
NFCorpus 1589.8 23.46% 29.98% 30.40%
Quora 62.2 87.19% 87.19% 87.19%

文件越長,後期分塊策略就越有效。

後期分塊不需要對嵌入模型進行額外的訓練。它可以應用於任何使用均值池的長上下文嵌入模型

Late Chunking is now available in jina-embeddings-v3 API. 8192-token

import requests
import json

url = 'https://api.jina.ai/v1/embeddings'
headers = {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer jina_321dd2MDu0hLW2Gozj1fTd'
}
data = {
    "model": "jina-embeddings-v3",
    "task": "text-matching",
    "late_chunking": True,
    "dimensions": 1024,
    "embedding_type": "float",
    "input": [
        "針對敏感肌專門設計的天然有機護膚產品:體驗由蘆薈和洋甘菊提取物帶來的自然呵護。的護膚產品特別為敏感肌設計,溫和滋潤,保護您的肌膚不受刺激。讓您的肌膚告別不適,迎來健康光彩。",
        "新しいメイクのトレンドは鮮やかな色と革新的な技術に焦點を當てています: 今シーズンのメイクアップトレンドは、大膽な色彩と革新的な技術に注目しています。ネオンアイライナーからホログラフィックハイライターまで、クリエイティビティを解き放ち、毎回ユニークなルックを演出しましょう。"
    ]
}

response = requests.post(url, headers=headers, data=json.dumps(data))
print(response.text)


4.3 Anthropic (2024.9.20)

連結:https://www.anthropic.com/news/contextual-retrieval

Anthropic 引入了一種稱為上下文檢索的單獨策略。 Anthropic 的方法是一種解決上下文丟失問題的強力方法,其工作原理如下:

  • 每個塊都會與完整的文件一起傳送給LLM 。

  • LLM為每個塊新增相關上下文。

  • 這會產生更豐富、資訊更豐富的嵌入。

這本質上是上下文豐富,其中全域性上下文使用LLM顯式硬編碼到每個塊中,這在成本、時間和儲存方面都很昂貴。此外,尚不清楚這種方法是否能夠適應塊邊界,因為LLM依賴於準確且可讀的塊來有效地豐富上下文

4.4 Small Language Model, SLM--qwen0.5

  • simple-qwen-0.5: 這是最簡單的模型,主要根據文件的結構元素識別邊界。它簡單高效,適合基本的分塊需求。https://huggingface.co/jinaai/text-seg-lm-qwen2-0.5b

  • topic-qwen-0.5: 這個模型受到了思維鏈 (Chain-of-Thought) 推理的啟發,它會先識別文字中的主題,比如“第二次世界大戰的開始”,然後用這些主題來定義分塊邊界。它能保證每個文字塊在主題上保持連貫,特別適合處理複雜的多主題文件。初步測試表明,它分塊的效果很不錯,很接近人類的直覺。https://huggingface.co/jinaai/text-seg-lm-qwen2-0.5b-cot-topic-chunking

  • summary-qwen-0.5: 這個模型不僅能識別文字邊界,還能給每個文字塊生成摘要。在 RAG 應用裡,文字塊摘要很有用,尤其是在長文件問答這種任務上。當然,代價就是訓練時需要更多的資料。https://huggingface.co/jinaai/text-seg-lm-qwen2-0.5b-summary-chunking

這三個模型都只返回文字塊的頭部,也就是每個文字塊的截斷版本。它們不生成完整的文字塊,而是輸出關鍵點或子主題。簡單來說,因為它只提取關鍵點或子主題,這就相當於抓住了片段的核心意思,透過文字的語義轉換,來更準確地識別邊界,保證文字塊的連貫性。檢索的時候,會根據這些 “文字塊頭部” 把文件文字切片,然後再根據切片結果重建完整的片段。相當於先用 “文字塊頭部” 做了個索引,需要的時候再根據索引找到對應的完整片段。這樣既能保證檢索的精準度,又能提高效率,避免處理過多的冗餘資訊。

4.4.1 資料集構造

使用了 wiki727k (https://github.com/koomri/text-segmentation) 資料集,這是一個從維基百科文章中提取的大規模結構化文字片段集合。它包含超過 727,000 個文字塊,每個片段代表維基百科文章的不同部分,例如引言、章節或子章節。

  • 資料增強

為了豐富每個模型變體的訓練資料,利用 GPT-4o 對資料集中的每篇文章進行了增強。具體來說,向 GPT-4o 提供了以下 prompt:

f"""
為這段文字生成一個五到十個單詞的主題和一個句子的摘要。

{text}

確保主題簡潔,摘要儘可能涵蓋主要主題。
請使用以下格式回覆:

主題:...
摘要:...

直接回復所需的主題和摘要,不要包含任何其他細節,也不要用引號、反引號或其他分隔符括住您的回覆。
   """.strip()

每篇文章按三個換行符 (\n\n\n) 分割成多個文字塊,文字塊內部再按兩個換行符 (\n\n) 進行子文字塊分割。例如,一篇關於 CGI 的文章會被分割成如下文字塊:

[
    [
      "在計算領域,通用閘道器介面 ( CGI ) 為 Web 伺服器提供了一種標準協議,用於執行像控制檯應用程式一樣執行的程式 ( 也稱為命令列介面程式 ) ,這些程式在伺服器上執行,動態生成網頁。",
      "此類程式稱為 \\"CGI 指令碼\\" 或簡稱為 \\"CGI\\"。",
      "指令碼如何由伺服器執行的具體細節由伺服器決定。",
      "通常情況下,CGI 指令碼在發出請求時執行並生成 HTML。"
    ],
    [
      "1993 年,國家超級計算應用中心 ( NCSA ) 團隊在 www-talk 郵件列表上編寫了呼叫命令列可執行檔案的規範;但是,NCSA 不再託管該規範。",
      "其他 Web 伺服器開發人員採用了它,並且從那時起它就一直是 Web 伺服器的標準。",
      "RFC 中特別提到了以下貢獻者: \\n1. Alice Johnson\\n2. Bob Smith\\n3. Carol White\\n4. David Nguyen\\n5. Eva Brown\\n6. Frank Lee\\n7. Grace Kim\\n8. Henry Carter\\n9. Ingrid Martinez\\n10. Jack Wilson"
    ]

然後,將原文的文字塊、GPT-4 生成的主題和摘要整理成 JSON 格式,方便模型學習。

{
  "sections": [
      [
      "在計算領域,通用閘道器介面 ( CGI ) 為 Web 伺服器提供了一種標準協議,用於執行像控制檯應用程式一樣執行的程式 ( 也稱為命令列介面程式 ) ,這些程式在伺服器上執行,動態生成網頁。",
       // ...分割好的章節內容 (上一步驟的結果)
    ]
  ],
  "topics": [
    "Web 伺服器中的通用閘道器介面",
    "CGI 的歷史和標準化",
    // ...其他主題
  ],
  "summaries": [
    "CGI 為 Web 伺服器提供了一個執行生成動態網頁程式的協議。",
    "NCSA 於 1993 年首次定義了 CGI,隨後它成為 Web 伺服器的標準...",
    // ...其他摘要
  ]
}

接著,對資料新增了噪聲,包括打亂資料、插入隨機字元 / 單詞 / 字母、隨機刪除標點符號以及去除所有換行符。

這些資料增強方法雖然有一定效果,但仍有侷限性,的最終目標是使模型能夠生成連貫的文字,並且正確處理程式碼片段等結構化內容。

所以還使用了 GPT-4o 生成程式碼、公式和列表來進一步豐富資料集,以提升模型處理這些元素的能力。

  • 訓練設定

    訓練模型過程中,的設定如下:

    • 框架:用了 Hugging Face 的 transformers 庫,還整合了Unsloth 來最佳化模型。這對於最佳化記憶體使用和加速訓練非常重要,讓可以用大型資料集高效地訓練小型模型。

    • 最佳化器和排程器:用了 AdamW 最佳化器,加上線性學習率排程策略和 warmup 機制,可以幫在訓練初期穩定訓練過程。

    • 實驗跟蹤:用 Weights & Biases 跟蹤所有訓練實驗,並記錄了訓練損失、驗證損失、學習率變化、整體模型效能等等關鍵指標。這種實時追蹤可以讓瞭解模型的進展情況,在需要時快速調整引數,獲得最好的學習效果。

  • 訓練過程

    是用 qwen2-0.5b-instruct 作為基礎模型,用 Unsloth 訓練了三個 SLM 變體,每個變體對應不同的分塊策略。訓練資料來自 wiki727k,除了文章全文外,還包含了先前 “資料增強” 步驟中提取的章節、主題和摘要等資訊。

    1. simple-qwen-0.5:用了 10,000 個樣本,訓練了 5,000 步,很快就收斂了,可以有效地檢測文字連貫分塊之間的邊界。訓練損失是 0.16。

    2. topic-qwen-0.5:跟 simple-qwen-0.5 類似,也用了 10,000 個樣本,訓練了 5,000 步,訓練損失是 0.45。

    3. summary-qwen-0.5:用了 30,000 個樣本,訓練了 15,000 步。這個模型很有潛力,但是訓練損失比較高(0.81),說明還需要更多資料,大概兩倍的原始樣本數量才能完全發揮它的實力。

5. 不同分塊策略的效果對比

5.1 jina vs qwen0.5

來看一下不同分塊策略的效果。下面是每種策略生成的三個連續分塊示例,還有 Jina 的 Segmenter API 的結果。為了生成這些文字塊,先用 Jina Reader 從 Jina AI 部落格抓取了一篇文章的純文字(包括頁首、頁尾等所有頁面資料),然後分別用不同的分塊方法處理。

https://jina.ai/news/can-embedding-reranker-models-compare-numbers/

  • Jina Segmenter API

Jina Segmenter API 的文字分塊非常細粒度,它會根據 \n\t 等字元進行分塊,所以切出來的文字塊通常都很小。只看前三個分塊的話,它從網站的導航欄提取了search\\nnotifications\\nNEWS\\n,但完全沒有提取到和文章內容相關的東西:

再往後,總算能看到一些部落格文章的內容了,但每個分塊保留的上下文資訊都很少:

  • simple-qwen-0.5

simple-qwen-0.5 會根據語義結構把部落格文章分成更長的文字塊,每個文字塊的含義都比較連貫:

  • topic-qwen-0.5

topic-qwen-0.5 會先根據文件內容識別主題,然後再根據這些主題進行分塊:

  • summary-qwen-0.5

summary-qwen-0.5 不僅能識別文字塊邊界,還會為每個文字塊生成摘要:

5.2 later chunk

Weaviate 研究人員做了一系列測試, 結果相當驚人。我們以 weaviate 部落格文章的一段內容來測試後期分塊與樸素分塊。

使用固定大小分塊策略(token 數 = 128)導致以下句子被分成了兩個不同的分塊:

Weaviate 的原生、多租戶架構為需要在保持快速檢索和準確性的同時優先考慮資料隱私的客戶提供優勢。

如下:

分塊 內容
chunk1 ... 技術堆疊的演變。這種靈活性,結合易於使用,幫助團隊更快地將 AI 原型部署到生產環境中。在架構方面,靈活性也至關重要。不同的應用場景有不同的需求。例如,我們與許多軟體公司以及在受監管行業中運營的公司合作。他們通常需要多租戶功能來隔離資料並保持合規性。在構建檢索增強生成(RAG)應用程式時,使用特定於帳戶或使用者的資料顯示結果的上下文,資料必須保留在為其使用者組專用的租戶內。Weaviate 的原生、多租戶架構對於需要優先考慮此類需求的客戶來說表現卓越。
chunk2 .. 在保持資料隱私的同時,實現快速檢索和準確性。另一方面,我們支援一些非常大規模的單租戶使用案例,側重於實時資料訪問。這些案例中的許多都涉及電子商務和以速度和客戶體驗為競爭點的行業。

假設有人問:"客戶最關心什麼?"

方法 AI 的回答
傳統方法 1. "產品更新, 參加我們的網路研討會。" (得分: 75.6)2. "在保證速度和準確性的同時注重資料隱私。我們支援一些大規模單租戶用例, 主要在電子商務和那些速度和客戶體驗至關重要的行業。" (得分: 70.1)
後期分塊 1. "客戶需求多樣, 我們引入了不同的儲存層。看到客戶的產品越來越受歡迎真是令人驚歎。但隨著使用者增長, 成本可能會急劇上升..." (得分: 74.8)2. "技術選擇至關重要。靈活性和易用性有助於團隊更快地實施 AI。不同的用例有不同的需求。例如, 軟體公司和受監管行業通常需要多租戶來隔離資料並確保合規。在構建 AI 應用時, 使用特定使用者資料來個性化結果很重要。Weaviate 的多租戶架構在這方面表現出色。" (得分: 68.9)

顯然, 後期分塊方法給出的答案更加全面和貼切, 更能抓住問題的核心。

  • colab連結: https://colab.research.google.com/drive/15vNZb6AsU7byjYoaEtXuNu567JWNzXOz?usp=sharing&ref=jina-ai-gmbh.ghost.io_

  • 測試連結: https://weaviate.io/blog/late-chunking_

  • 文章: https://weaviate.io/blog/launching-into-production_

6. 結合業務場景與文字特點選擇合適分塊策略

  • 不同應用需求

    不同的業務場景對文字處理有著獨特的需求和挑戰。在選擇文字分塊策略時,首先要充分理解具體應用的目標和要求

    • 專業領域應用:如在法律、醫學等專業領域,文字往往包含大量的術語和複雜的句法結構。此時,需要確保文字分塊能夠保留專業術語的完整性和上下文關聯,避免因切分不當導致語義誤解。

    • 長文件處理:對於需要處理長篇文件的應用,如技術文件、研究報告,應採用能夠保持段落和章節結構的分塊策略,確保大模型能夠理解文件的整體邏輯和主題發展。

    • 實時響應場景:在聊天機器人、智慧客服等需要實時響應的場景中,文字分塊策略需要兼顧處理速度和語義完整性,可能需要在分塊大小和計算資源之間尋找平衡。

  • 分析文字的內在結構

    不同型別的文字具有不同的結構特徵,在選擇分塊策略時,應充分考慮這些特性。

    • 結構化文字:如 HTML、Markdown 格式的文字,具有明確的標籤和層次結構。採用針對性的分塊方法(如 HTMLHeaderTextSplitterMarkdownTextSplitter),能夠保留文字的結構資訊,提升模型的理解能力。

    • 非結構化文字:對於散文、對話等非結構化文字,可考慮基於語義的分塊方法,利用自然語言處理技術識別句子邊界、主題變化點,確保語義的完整性和連貫性。

    • 多語言文字:在處理包含多種語言的文字時,需要選擇支援相應語言特性的分塊工具,並可能需要針對不同語言調整分塊引數。

  1. 多方案對比

    實踐中,應對不同的文字分塊策略進行實驗驗證,以確定最適合特定應用場景的方法。

    • 策略多樣性:嘗試固定大小切分、基於標點的切分、重疊切分、語義切分等多種策略。

    • 工具選擇:使用不同的文字切塊工具(如 RecursiveCharacterTextSplitterNLTKTextSplitterSpacyTextSplitter)進行比較,瞭解其在具體應用中的表現。

  2. 制定評估指標

    為客觀評估不同分塊策略的效果,需要制定明確的評估指標

    • 檢索效能:評估向量化和檢索階段的效果,如召回率、準確率、平均檢索時間等。

    • 生成質量:評估大模型生成回答的準確性、完整性、連貫性,以及與使用者查詢的相關性。

    • 使用者反饋:收集使用者對系統回答的滿意度評價,作為質量評估的重要參考。

  3. 資料驅動的最佳化

    基於實驗資料和評估結果,對分塊策略進行最佳化

    • 引數調優:根據實驗結果,調整文字塊長度、重疊長度、分割符等引數,尋求最佳配置。

    • 問題定位:透過分析模型錯誤案例,找出分塊策略中導致效能下降的原因,針對性地改進。

    • 漸進式最佳化:採用迭代的方法,逐步改進分塊策略和引數設定,每次最佳化後進行驗證,確保最佳化方向正確。

參考文獻

[1]https://github.com/langchain-ai/langchain/blob/v0.1.9/libs/langchain/langchain/text_splitter.py#L851C1-L851C6

[2]https://docs.llamaindex.ai/en/stable/examples/node_parsers/semantic_chunking.html

[3]https://python.langchain.com/docs/modules/data_connection/document_transformers/semantic-chunker

[4]https://github.com/run-llama/llama_index/blob/v0.10.12/llama-index-core/llama_index/core/node_parser/text/semantic_splitter.py

[5]https://arxiv.org/pdf/1810.04805.pdf

[6]https://arxiv.org/abs/2004.14535

[7]https://github.com/aakash222/text-segmentation-NLP/

[8]https://arxiv.org/pdf/2107.09278.pdf

[9]https://github.com/alibaba-damo-academy/SpokenNLP

[10]https://github.com/modelscope/modelscope/

[11]https://github.com/run-llama/llama_index/blob/v0.10.12/llama-index-packs/llama-index-packs-dense-x-retrieval/llama_index/packs/dense_x_retrieval/base.py

[12]https://github.com/run-llama/llama_index/blob/v0.10.12/llama-index-packs/llama-index-packs-dense-x-retrieval/llama_index/packs/dense_x_retrieval/base.py#L161

[13]https://github.com/chentong0/factoid-wiki

  1. Lewis, P., et al. (2020). Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. Advances in Neural Information Processing Systems_, 33, 9459–9474.

  2. Guu, K., et al. (2020). REALM: Retrieval-Augmented Language Model Pre-Training. Proceedings of the 37th International Conference on Machine Learning_, 1-41.

  3. Chen, D., et al. (2017). Reading Wikipedia to Answer Open-Domain Questions. Association for Computational Linguistics_, 1870–1879.

  4. 《Text Segmentation by Cross Segment Attention》: https://arxiv.org/abs/2004.14535_

  5. 訓練實現: https://github.com/aakash222/text-segmentation-NLP/

  6. 《Sequence Model with Self-Adaptive Sliding Window for Efficient Spoken Document Segmentation》: https://arxiv.org/abs/2107.09278_

  7. ModelScope 框架: https://github.com/modelscope/modelscope/

  8. 《Dense X Retrieval: What Retrieval Granularity Should We Use?》: https://arxiv.org/pdf/2312.06648.pdf

  9. Advanced RAG 05:探討基於文字內在語義資訊的資料分塊方法:https://mp.weixin.qq.com/s/ejLY3vmEW3yEaJT0qfdnUQ

相關文章