構建 LLM 應用:數據準備(第二部分)

作者:Vipra Singh

編譯:ronghuaiyang

導讀

在系列博客中,我們通過檢索增強生成(RAG)應用的視角來學習大規模語言模型(LLM)

檢索增強生成(RAG)的數據準備工作流

在上一篇文章中,我們深入探討了檢索增強生成(Retrieval Augmented Generation, RAG)的流程,全面理解了它的各個組成部分。

任何機器學習應用的初始階段都涉及數據準備。這包括建立數據獲取流程和預處理數據,使其與推理流程兼容。

在這篇文章中,我們將關注 RAG 的數據準備方面。目標是高效地組織和構建數據,確保在我們的應用在找到答案時具備最佳性能。

下面讓我們做詳細的瞭解。

  1. 第 1 步:數據獲取 =============

構建面向消費者的聊天機器人首先需要明智的數據選擇。本篇博客將探討如何有效地收集、管理和清洗數據,從而打造一個成功的語言模型(LLM)應用。

  1. 明智選擇: 確定從門戶到 API 的數據來源,併爲您的 LLM 應用程序設置推送機制以實現持續更新。

  2. 治理至關重要: 提前實施數據治理政策。審覈並編輯文檔來源,對敏感數據進行脫敏處理,併爲上下文訓練建立基礎。

  3. 質量把控: 評估數據的多樣性、規模及噪聲水平。較低質量的數據集會稀釋響應質量,因此早期分類機制至關重要。

  4. 前瞻佈局: 即便在快速發展的 LLM 開發過程中,也要遵守數據治理原則。這能降低風險,並確保數據提取的可擴展性和健壯性。

  5. 實時淨化: 從 Slack 等平臺抓取數據時,實時過濾掉噪聲、錯別字及敏感內容,確保 LLM 應用的乾淨與高效。

  6. 第 2 步:數據清理 =============

我們的文件中的每一頁都被轉化爲一個名爲 “Document” 的對象,該對象包含兩個核心組成部分:page_contentmetadata

page_content 部分展示的是直接從文檔頁面中提取的文本內容。

metadata 則是一個至關重要的附加詳情集合,涵蓋了諸如文檔來源(即其原始文件)、頁碼、文件類型以及其他信息片段。這些元數據詳細跟蹤了在模型施展其功能、生成富有洞察力的答案時所依賴的具體來源信息。

爲了實現這一過程,我們採用了諸如 Data Loaders 這類強大工具,這些工具由 LangChain 和 Llamaindex 等開源庫提供。這些庫支持多種格式的數據加載,包括從 PDF 和 CSV 到 HTML、Markdown,甚至是數據庫等多種類型。通過這些工具,我們能夠靈活且高效地處理不同來源和格式的信息,確保文檔內容及其元數據被準確無誤地轉化和利用,進而增強語言模型的理解能力和響應質量。

!pip install pypdf
!pip install langchain

#for PDF file we need to import PyPDFLoader from langchain framework
from langchain_community.document_loaders import PyPDFLoader

# for CSV file we need to import csv_loader
# for Doc we need to import UnstructuredWordDocumentLoader
# for Text document we need to import TextLoader

filePath = "/content/A_miniature_version_of_the_course_can_be_found_here__1701743458.pdf"
loader = PyPDFLoader(filePath) 
#Load document 
pages = loader.load_and_split()
print(pages[0].page_content)
  1. 第 3 步:分塊 ===========

3.1. 爲什麼要分塊?

在應用程序領域,數據處理方式是改變遊戲規則的關鍵——不論是 Markdown、PDF 還是其他文本文件。想象一下:你手頭有一個龐大的 PDF 文件,急於就其內容提出問題。問題在於?傳統的做法是將整個文檔和你的問題一股腦兒扔給模型處理,但這種方法效果不佳。爲什麼呢?這就涉及到模型上下文窗口的侷限性了。

隨着 GPT-3.5 及其同類模型的出現,情況發生了變化。可以把上下文窗口想象成是對文檔的一瞥,通常只侷限於一頁或幾頁內容。如果一次性上傳整個文檔?這並不現實。但是不用擔心!

神奇的解決辦法在於數據分塊。將文檔拆解成易於處理的小部分,只向模型發送最相關的部分。這樣一來,既不會讓模型感到負擔過重,又能確保你獲得渴望的精確信息。

通過將結構化的文檔分解成易於管理的片段,我們使 LLM 能夠以前所未有的效率處理信息。不再受制於單頁的限制,這種策略確保了關鍵細節在處理過程中不會丟失,提高了回答的準確性和深度。

3.2. 分塊之前的考慮

文檔的結構與長度:

嵌入模型: 分塊大小決定所使用的嵌入模型類型。

預期查詢: 應用場景是什麼?

3.3. 分塊大小

3.3.1. 選擇分塊大小

LLM 上下文窗口

評估各分塊大小的性能 —— 爲了測試不同的分塊大小,你可以使用多個索引或單個索引中的多個命名空間。使用代表性數據集,爲想要測試的分塊大小創建嵌入,並保存在索引(或索引中)。然後,運行一系列查詢,評估質量,並比較不同分塊大小的性能。這很可能是一個迭代過程,你需對不同查詢測試不同的分塊大小,直到確定出最適合你的內容和預期查詢的最佳分塊大小。

高上下文長度的侷限性:

在 LlamaIndex 發佈的這個示例中,可以看到下表隨着分塊大小的增加,平均響應時間有輕微上升。有趣的是,平均忠實度似乎在分塊大小爲 1024 時達到峯值,而平均相關性則隨着分塊大小的增大表現出持續改善,同樣在 1024 達到頂峯。這表明,1024 個 tokens 的分塊大小可能在響應時間和以忠實度及相關性衡量的響應質量之間達到了最優平衡。

3.4. 分塊方法

在進行分塊時,有不同的方法,每種方法可能適用於不同的情況。通過考察每種方法的優缺點,我們的目標是確定應用它們的正確場景。

3.4.1. 固定大小分塊

我們決定每個分塊中的 tokens 數量,並可選擇性地加入重疊部分以確保語義上下文的豐富性在分塊間得以保留。爲何要重疊?這是爲了確保分塊之間的語義上下文完整性不受影響。

爲何選擇固定大小分塊?對於大多數場景而言,這是一種理想的方案。它不僅計算成本低,節省處理能力,而且使用起來也非常簡單。無需複雜的 NLP 庫;只需利用固定大小分塊的優雅特性,就能輕鬆分解數據。

以下是使用 LangChain 執行固定大小分塊的一個示例:

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

3.4.2. “上下文感知” 分塊

這是一系列方法,旨在利用我們正在分塊的內容特性,並對其應用更復雜的分塊技術。以下是一些示例:

3.4.2.1. 句子分割

如前所述,許多模型針對句子級內容的嵌入進行了優化。自然而然地,我們會使用句子分塊,爲此有幾種方法和工具可供選擇,包括:

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

3.4.2.2. 遞歸分塊

介紹我們的祕密武器:來自 LangChain 的 RecursiveCharacterTextSplitter。這個多功能工具根據選定的字符優雅地分割文本,同時保留語義上下文。想象一下雙換行、單換行和空格——它就像將信息雕琢成易於處理的、有意義的部分。

它是如何工作的?很簡單。只需傳遞Document並指定所需的分塊長度(假設爲 1000 個單詞)。你甚至可以微調分塊之間的重疊量。

以下是使用 LangChain 進行遞歸分塊的示例:

text = "..." # your text
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
    # Set a really small chunk size, just to show.
    chunk_size = 256,
    chunk_overlap  = 20
)
docs = text_splitter.create_documents([text])

3.4.2.3. 專業分塊

Markdown 和 LaTeX 是兩種可能遇到的結構化和格式化內容示例。在這種情況下,你可以使用專門的分塊方法,在分塊過程中保留內容的原始結構。

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

3.5. 多模態分塊

從文檔中提取表格和圖像: 使用 LayoutPDFReader 或 Unstructured 等工具,表格和圖像可以被提取出來,並能附上元數據標籤,如標題、描述和摘要。

多模態方法:

在處理包含多樣化內容(如文本、表格、圖像)的文檔時,採用多模態方法尤爲重要,這不僅能提取和理解文本信息,還能有效整合圖像和表格內容。例如,通過文本嵌入,可以爲圖像和表格生成描述性文本,使得這些非文本元素也能被語言模型理解並融入到後續的分析或查詢響應中。而多模態嵌入技術則更進一步,它允許模型直接對圖像這類非文本數據進行編碼,結合文本信息形成統一的表示,有利於提升整體內容的理解精度和上下文關聯性。

  1. 第 4 步:分詞 ===========

大多數的分詞方法的總結

分詞(Tokenization)是指將短語、句子、段落或整個文本文檔切分成更小的單位,如單個單詞或術語。在本文中,我們將瞭解主要的分詞方法以及它們當前的應用場景。我建議您也參考一下 Hugging Face 製作的這個分詞器概述,以便獲得更深入的指南。

4.1. 單詞級分詞

單詞級分詞涉及將文本切分爲單詞單位。爲了正確執行這一操作,需要考慮一些預防措施。

空格與標點符號分詞

將文本切分成更小的部分比看起來要複雜,並且有多種方法可以實現。例如,讓我們看下面這個句子:

"Don't you like science? We sure do."

一種簡單的分詞方法是通過空格來分割這段文本,這樣做會得到:

["Don't""you""like""science?""We""sure""do."]

如果我們觀察分詞結果 "science?""do.",會注意到標點符號與單詞 "science""do" 連在一起,這並不理想。我們應該考慮標點符號,這樣一來,模型就不必爲一個單詞及其可能跟隨的所有標點符號學習不同的表示形式,否則會使模型需要學習的表示數量激增。

考慮到標點符號,對我們的文本進行分詞會得到:

["Don""'""t""you""like""science""?""We""sure""do""."]

基於規則的分詞

之前的分詞方法比純粹基於空格的分詞有所改進。然而,我們還可以進一步優化如何處理單詞 "Don't""Don't" 代表 "do not",因此更好的分詞方式應該是 ["Do", "n't"]。其他一些特定的規則可以進一步提升分詞的效果。

但是,根據我們應用於文本分詞的規則不同,即使是相同的文本也會產生不同的分詞輸出。因此,預訓練模型只有在輸入按照與其訓練數據相同的分詞規則進行分詞時,才能正常工作。

詞級分詞的問題

詞級分詞對於大規模文本語料庫可能會導致問題,因爲它會產生非常大的詞彙量。例如,Transformer XL 語言模型使用空格和標點符號進行分詞,導致詞彙量達到 267,000。

由於如此龐大的詞彙量,模型的輸入和輸出層擁有一個巨大的嵌入矩陣,這不僅增加了內存需求,也提高了時間複雜度。作爲參考,Transformer 模型的詞彙量很少超過 50,000。

4.2. 字符級分詞

如果詞級分詞存在不足,爲何不直接採用字符級分詞呢?

儘管字符級分詞能夠顯著減少內存佔用和降低時間複雜度,但它使模型學習到有含義的輸入表示變得更爲艱難。例如,相比學習單詞 "today" 的上下文無關表示,學習單個字母 "t" 的上下文無關表示要困難得多。

因此,字符級分詞常導致性能損失。爲了兼顧兩方面的優點,Transformer 模型通常採取一種詞級和字符級分詞相結合的方法,即子詞分詞(Subword Tokenization)。這種方法通過創建既考慮了整個詞彙單元,又包含了字符序列信息的子詞單位,來平衡模型學習的有效性和效率,從而在保留詞彙語義完整性的同時,有效控制詞彙表的大小。

4.3. 子詞分詞

子詞分詞算法基於這樣的原則:常用詞不應拆分成更小的子詞,而罕見詞則應分解爲有意義的子詞。

例如,單詞 "annoyingly" 可能被視爲一個罕見詞,可以分解爲 "annoying""ly"。獨立來看,"annoying"和"ly"作爲子詞會更頻繁地出現,同時,"annoyingly"的含義通過組合"annoying"和"ly" 的意義得以保留。

除了能使模型的詞彙表保持在一個合理的規模外,子詞分詞還允許模型學習到有意義的、獨立於上下文的表示。此外,子詞分詞還能通過將模型從未見過的單詞拆解爲已知的子詞來處理這些新詞。

接下來,讓我們瞭解幾種不同的子詞分詞方法。

字節對編碼(BPE)

字節對編碼(BPE)依賴於一個預分詞器,該預分詞器將訓練數據按單詞分割(如使用空格分詞,在 GPT-2 和 RoBERTa 中)。

預分詞後,BPE 構建一個基本詞彙表,包含語料庫中所有唯一單詞出現過的所有符號,並學習合併規則以從基礎詞彙表中的兩個符號形成一個新的符號。這一過程迭代進行,直到詞彙表達到期望的大小。

WordPiece

WordPiece,用於 BERT、DistilBERT 和 Electra,與 BPE 非常相似。WordPiece 首先初始化詞彙表以包含訓練數據中出現的每個字符,然後逐步學習一定數量的合併規則。與 BPE 不同的是,WordPiece 並不選擇最頻繁出現的符號對,而是選擇一旦加入詞彙表後最能提高訓練數據概率的那個。

直觀上,WordPiece 與 BPE 稍有不同,它評估合併兩個符號所損失的信息,確保這一操作是有價值的。

Unigram

與 BPE 或 WordPiece 不同,Unigram 將基礎詞彙表初始化爲大量符號,並逐步縮減每個符號以獲得較小的詞彙表。基礎詞彙可能包括所有預分詞的單詞和最常見的子串。Unigram 常與 SentencePiece 一起使用。

SentencePiece

迄今爲止描述的所有分詞算法都有一個共同的問題:假設輸入文本使用空格來分隔單詞。然而,並非所有語言都用空格分隔單詞。

爲普遍解決這一問題,SentencePiece 將輸入視爲原始輸入流,因此將空格也包括在使用的字符集中。然後,它使用 BPE 或 Unigram 算法構建合適的詞彙表。

使用 SentencePiece 的模型示例包括 ALBERT、XLNet、Marian 和 T5。

OpenAI 分詞可視化:https://platform.openai.com/tokenizer

總結

在本篇博文中,我們深入探討了檢索增強生成(RAG)應用中的數據預處理流程,着重強調了爲了達到最佳性能而進行的有效數據結構化方法。內容涵蓋了將原始數據轉化爲結構化文檔、創建相關數據塊以及諸如子詞分詞等分詞方法。文章突出了選擇合適的數據塊大小的重要性,並對每種分詞方法的考慮因素進行了說明。通過這些討論,爲針對特定應用場景定製數據預處理工作提供了深刻見解。

英文原文:https://medium.com/@vipra_singh/building-llm-applications-data-preparation-part-2-b7306d224245

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/INLHQp2FEJbmyno3g3mvLw