推薦閱讀列表:
LLM本地知識庫問答系統(tǒng)(一):使用LangChain和LlamaIndex從零構(gòu)建PDF聊天機(jī)器人指南
? ? ? ?上一篇文章我們介紹了使用LlamaIndex構(gòu)建PDF聊天機(jī)器人,本文將介紹一下LlamaIndex的基本概念和原理。
LlamaIndex簡介
? ? ? ?LlamaIndex(也稱為GPT Index)是一個用戶友好的界面,可將外部數(shù)據(jù)連接到大型語言模型(LLM)。它提供了一系列工具來簡化流程,包括可以與各種現(xiàn)有數(shù)據(jù)源和格式(如API、PDF、文檔和SQL)集成的數(shù)據(jù)連接器。此外,LlamaIndex為結(jié)構(gòu)化和非結(jié)構(gòu)化數(shù)據(jù)提供索引,可以輕松地與LLM一起使用。
? ? ? ?本文將討論LlamaIndex提供的不同類型的索引以及如何使用它們。LlamaIndex索引包括列表索引、矢量存儲索引、樹索引和關(guān)鍵字表索引,當(dāng)然也包括一些特殊索引,比如圖索引、Pandas索引、SQL索引和文檔摘要索引。
? ? ? ?有了ChatGPT這么強(qiáng)大的LLM,搭建PDF聊天機(jī)器人不夠嗎?為什么還需要LlamaIndex?其實,如果想搭建企業(yè)級的聊天機(jī)器人,那么ChatGPT的上下文是不夠的,常見LLM上下文大致如下:
- GPT-3:約2000個tokens
- GPT-3.5:約4000個tokens
- GPT-4:最多32.000個tokens
Note:1000個tokens大約有750個words
? ? ? ?LlamaIndex將文檔分解為多個Node對象,Node表示源文檔的“塊”,這些源文檔可以是文本塊、圖像或者其他內(nèi)容。它們還包含元數(shù)據(jù)以及與其他節(jié)點和索引結(jié)構(gòu)的關(guān)系信息。創(chuàng)建索引,其實就是創(chuàng)建這些Node,也可以手動為文檔定義Node。
使用LlamaIndex構(gòu)建索引
下面我們看一下如何創(chuàng)建這些索引:
安裝LlamaIndex相關(guān)庫
pip install llama-index
pip install openai
設(shè)置OpenAI API Key
import os
os.environ['OPENAI_API_KEY'] = '<YOUR_OPENAI_API_KEY>'
?
import logging
import sys
?
## showing logs
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))
?
?
## load the PDF
from langchain.text_splitter import RecursiveCharacterTextSplitter
from llama_index import download_loader
?
# define loader
UnstructuredReader = download_loader('UnstructuredReader', refresh_cache=True)
loader = UnstructuredReader()
?
# load the data
documents = loader.load_data('../notebooks/documents/Apple-Financial-Report-Q1-2022.pdf',split_documents=False)
列表索引(List Index)
? ? ? ?列表索引是一個簡單的數(shù)據(jù)結(jié)構(gòu),其中Node存儲在一個序列中。文檔文本被分塊,轉(zhuǎn)換為節(jié)點,并在索引構(gòu)建過程中存儲在列表中。如下圖所示:
? ? ? ?在查詢期間,如果沒有指定其他查詢參數(shù),LlamaIndex只需將列表中的所有節(jié)點加載到Response Synthesis模塊中。
? ? ? ?列表索引提供了多種查詢列表索引方式,比如通過embedding查詢最相關(guān)的top-k塊,或者使用關(guān)鍵字過濾,如下所示:
? ? ? ?LlamaIndex為列表索引提供embedding支持。每個節(jié)點存儲文本之外還可以選擇存儲embedding。在查詢期間,我們可以使用embedding對節(jié)點進(jìn)行最大相似度檢索,然后調(diào)用LLM來生成答案。由于使用embedding的相似性查找(例如,使用余弦相似性)不需要LLM調(diào)用,因此embedding是一種代價更低的查詢機(jī)制,而不需要遍歷LLM節(jié)點。這意味著在索引構(gòu)建過程中,LlamaIndex不會調(diào)用LLM來生成embedding,而是在查詢時生成embedding。這種設(shè)計避免了在索引構(gòu)建期間為所有文本塊生成embedding,這對于大數(shù)據(jù)來說可能代價高昂。
下面是一個具體的使用案例:
from llama_index import GPTKeywordTableIndex, SimpleDirectoryReader
from IPython.display import Markdown, display
from langchain.chat_models import ChatOpenAI
?
## by default, LlamaIndex uses text-davinci-003 to synthesise response
# and text-davinci-002 for embedding, we can change to
# gpt-3.5-turbo for Chat model
index = GPTListIndex.from_documents(documents)
?
query_engine = index.as_query_engine()
response = query_engine.query("What is net operating income?")
display(Markdown(f"<b>{response}</b>"))
?
## Check the logs to see the different between th
## if you wish to not build the index during the index construction
# then need to add retriever_mode=embedding to query engine
# query with embed_model specified
query_engine = new_index.as_query_engine(
retriever_mode="embedding",
verbose=True
)
response = query_engine.query("What is net operating income?")
display(Markdown(f"<b>{response}</b>"))
向量存儲索引(Vector Store Index)
向量存儲索引是最常見且使用簡單的,允許在大量數(shù)據(jù)中回答查詢。
? ? ? 默認(rèn)情況下,GPTVectorStoreIndex使用內(nèi)存中的SimpleVectorStore。與List Index不同,Vector Store Index在構(gòu)建索引過程中就生成了embedding,這意味著在構(gòu)建索引以生成embedding數(shù)據(jù)期間就調(diào)用了LLM。
? ? ? ?查詢Vector Store Index前k個最相似的節(jié)點,并將它們傳遞到我們的Response Synthesis模塊中。
from llama_index import GPTVectorStoreIndex
?
index = GPTVectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine()
response = query_engine.query("What did the author do growing up?")
response
樹索引(Tree Index)
? ? ? ?樹索引是一個樹結(jié)構(gòu)索引,其中每個節(jié)點都是子節(jié)點的摘要。在索引構(gòu)建過程中,樹是以自下而上的方式構(gòu)建的,直到我們最終得到一組根節(jié)點。樹索引從一組節(jié)點(成為該樹中的葉節(jié)點)構(gòu)建一個層次樹。
? ? ?查詢樹索引涉及從根節(jié)點向下遍歷到葉節(jié)點。默認(rèn)情況下,(child_branch_factor=1),查詢在給定父節(jié)點的情況下選擇一個子節(jié)點。如果child_branch_factor=2,則查詢會在每個級別選擇兩個子節(jié)點。
? ? ? ?與Vector Index不同,Tree Index是在查詢時生成embedding,當(dāng)然如果在查詢時指定retriever_mode=“embedding”,那么embedding會延遲生成并被緩存起來。
from llama_index import GPTTreeIndex
?
new_index = GPTTreeIndex.from_documents(documents)
response = query_engine.query("What is net operating income?")
display(Markdown(f"<b>{response}</b>"))
?
## if you want to have more content from the answer,
# you can add the parameters child_branch_factor
# let's try using branching factor 2
query_engine = new_index.as_query_engine(
child_branch_factor=2
)
response = query_engine.query("What is net operating income?")
display(Markdown(f"<b>{response}</b>"))
? ? ? ?要在查詢期間構(gòu)建Tree Index,我們需要向查詢引擎添加retrier_mode和response_mode,并將GPTTreeIndex中的build_Tree參數(shù)設(shè)置為False。
index_light = GPTTreeIndex.from_documents(documents, build_tree=False)
query_engine = index_light.as_query_engine(
retriever_mode="all_leaf",
response_mode='tree_summarize',
)
query_engine.query("What is net operating income?")
關(guān)鍵詞表索引(Keyword Table Index)
? ? ? ?關(guān)鍵字表索引從每個節(jié)點提取關(guān)鍵字,并構(gòu)建從每個關(guān)鍵字到該關(guān)鍵字的相應(yīng)節(jié)點的映射。
? ? ? ?在查詢期間,我們從查詢中提取相關(guān)關(guān)鍵字,并將這些關(guān)鍵字與預(yù)先提取的Node關(guān)鍵字進(jìn)行匹配,以獲取相應(yīng)的Node。提取的節(jié)點被傳遞到我們的Response Synthesis模塊。
? ? ? ? 對于GPTKeywordTableIndex,一般情況是使用LLM從每個文檔中提取關(guān)鍵字,這意味著它在構(gòu)建時確實需要LLM調(diào)用。但是,如果使用GPTSimpleKeywordTableIndex(使用正則表達(dá)式關(guān)鍵字提取器從每個文檔中提取關(guān)鍵字),則在構(gòu)建時不會調(diào)用LLM。
from llama_index import GPTKeywordTableIndex
index = GPTKeywordTableIndex.from_documents(documents)
query_engine = index.as_query_engine()
response = query_engine.query("What is net operating income?")
可組合性圖索引(Composability Graph Index)
? ? ? ?LlamaIndex通過在現(xiàn)有Index的基礎(chǔ)上composite indices,此功能能夠高效地索引完整的文檔層次結(jié)構(gòu),并為GPT提供量身定制的知識。通過利用可組合性,您可以在多個級別定義索引,例如單個文檔的較低級別索引和文檔組的較高級別索引??紤]以下示例:
- 可以為每個文檔中的文本創(chuàng)建一個樹索引。
- 生成一個列表索引,該索引覆蓋整個文檔集合的所有樹索引。
下面通過一個實例來說明一下可組合性圖索引的能力:
-
從多個文檔創(chuàng)建樹索引
-
從樹索引生成摘要。如前所述,樹索引對于匯總文檔集合非常有用。
-
接下來,我們將創(chuàng)建一個Graph,它在3個樹索引之上有一個列表索引。為什么?因為列表索引適合于合成組合多個數(shù)據(jù)源上的信息的答案。
-
最后查詢圖形。
? ? ? 我們加載了蘋果從Q1–2022和Q1–2023 兩個季度10k的金融數(shù)據(jù),并進(jìn)行問答,代碼如下:
## re
years = ['Q1-2023', 'Q2-2023']
UnstructuredReader = download_loader('UnstructuredReader', refresh_cache=True)
?
loader = UnstructuredReader()
doc_set = {}
all_docs = []
?
for year in years:
year_docs = loader.load_data(f'../notebooks/documents/Apple-Financial-Report-{year}.pdf', split_documents=False)
for d in year_docs:
d.extra_info = {"quarter": year.split("-")[0],
"year": year.split("-")[1],
"q":year.split("-")[0]}
doc_set[year] = year_docs
all_docs.extend(year_docs)
為每個季度數(shù)據(jù)創(chuàng)建索引
## setting up vector indicies for each year
#---
# initialize simple vector indices + global vector index
# this will use OpenAI embedding as default with text-davinci-002
service_context = ServiceContext.from_defaults(chunk_size_limit=512)
index_set = {}
for year in years:
storage_context = StorageContext.from_defaults()
cur_index = GPTVectorStoreIndex.from_documents(
documents=doc_set[year],
service_context=service_context,
storage_context=storage_context
)
index_set[year] = cur_index
# store index in the local env, so you don't need to do it over again
storage_context.persist(f'./storage_index/apple-10k/{year}')
從樹索引生成摘要。如前所述,樹索引對于匯總文檔集合非常有用。
# describe summary for each index to help traversal of composed graph
index_summary = [index_set[year].as_query_engine().query("Summary this document in 100 words").response for year in years]
接下來,我們將在3個樹索引之上創(chuàng)建一個包括列表索引Graph。
### Composing a Graph to Synthesize Answers
from llama_index.indices.composability import ComposableGraph
?
from langchain.chat_models import ChatOpenAI
from llama_index import LLMPredictor
?
# define an LLMPredictor set number of output tokens
llm_predictor = LLMPredictor(llm=ChatOpenAI(temperature=0, max_tokens=512, model_name='gpt-3.5-turbo'))
service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor)
storage_context = StorageContext.from_defaults()\
?
## define a list index over the vector indicies
## allow us to synthesize information across each index
graph = ComposableGraph.from_indices(
GPTListIndex,
[index_set[y] for y in years],
index_summaries=index_summary,
service_context=service_context,
storage_context=storage_context
)
?
root_id = graph.root_id
?
#save to disk
storage_context.persist(f'./storage_index/apple-10k/root')
?
## querying graph
custom_query_engines = {
index_set[year].index_id: index_set[year].as_query_engine() for year in years
}
?
query_engine = graph.as_query_engine(
custom_query_engines=custom_query_engines
)
?
response = query_engine.query("Outline the financial statement of Q2 2023")
response.response
Pandas Index and SQL Index
這個相對比較好理解一些,我們直接展示一下實例代碼:
Pandas Index:
from llama_index.indices.struct_store import GPTPandasIndex
import pandas as pd
?
df = pd.read_csv("titanic_train.csv")
?
index = GPTPandasIndex(df=df)
?
query_engine = index.as_query_engine(
verbose=True
)
response = query_engine.query(
"What is the correlation between survival and age?",
)
response
SQL Index:
? ? ? ?可以使用LLM去訪問傳統(tǒng)數(shù)據(jù)庫了,對于傳統(tǒng)行業(yè)是不有些激動呢?下面展示一個例子,更多例子可以參考(https://gpt-index.readthedocs.io/en/latest/examples/index_structs/struct_indices/SQLIndexDemo.html)
# install wikipedia python package
!pip install wikipedia
?
from llama_index import SimpleDirectoryReader, WikipediaReader
from sqlalchemy import create_engine, MetaData, Table, Column, String, Integer, select, column
?
wiki_docs = WikipediaReader().load_data(pages=['Toronto', 'Berlin', 'Tokyo'])
?
engine = create_engine("sqlite:///:memory:")
metadata_obj = MetaData()
?
# create city SQL table
table_name = "city_stats"
city_stats_table = Table(
table_name,
metadata_obj,
Column("city_name", String(16), primary_key=True),
Column("population", Integer),
Column("country", String(16), nullable=False),
)
metadata_obj.create_all(engine)
?
from llama_index import GPTSQLStructStoreIndex, SQLDatabase, ServiceContext
from langchain import OpenAI
from llama_index import LLMPredictor
?
llm_predictor = LLMPredictor(llm=LLMPredictor(llm=ChatOpenAI(temperature=0, max_tokens=512, model_name='gpt-3.5-turbo')))
service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor)
?
sql_database = SQLDatabase(engine, include_tables=["city_stats"])
sql_database.table_info
?
# NOTE: the table_name specified here is the table that you
# want to extract into from unstructured documents.
index = GPTSQLStructStoreIndex.from_documents(
wiki_docs,
sql_database=sql_database,
table_name="city_stats",
service_context=service_context
)
?
# view current table to verify the answer later
stmt = select(
city_stats_table.c["city_name", "population", "country"]
).select_from(city_stats_table)
?
with engine.connect() as connection:
results = connection.execute(stmt).fetchall()
print(results)
?
query_engine = index.as_query_engine(
query_mode="nl"
)
response = query_engine.query("Which city has the highest population?")
文檔摘要索引(Document Summary Index)
? ? ? ?Document Summary Index是LlamaIndex全新的數(shù)據(jù)結(jié)構(gòu),非常適合QA系統(tǒng)開發(fā)的。到目前為止,我們已經(jīng)研究了單個索引,我們可以通過使用單個索引或?qū)⒍鄠€索引組合在一起來構(gòu)建LLM QA應(yīng)用程序。
一般來說,強(qiáng)大的LLM-QA系統(tǒng)通常由以下方式開發(fā):
-
獲取源文檔并將其劃分為文本塊;
-
然后將文本塊存儲在矢量數(shù)據(jù)庫中;
-
在查詢時間期間,通過利用用于嵌入的相似性和/或關(guān)鍵字過濾器來檢索文本塊;
-
執(zhí)行Response synthesis;
然而,這種方法有幾個限制,會影響檢索性能。
當(dāng)前方法的缺點:
-
文本塊沒有完整的全局上下文,這通常會限制問答過程的有效性。
-
需要仔細(xì)調(diào)整top-k/相似性得分閾值,因為太小的值可能導(dǎo)致錯過相關(guān)上下文,而太大的值可能會增加不相關(guān)上下文的成本和延遲。
-
embedding可能并不總是為問題選擇最合適的上下文,因為這個過程本質(zhì)上是分別確定文本和上下文的。
? ? ? ?為了增強(qiáng)檢索結(jié)果,添加了關(guān)鍵字過濾器。然而,這種方法有其自身的一系列挑戰(zhàn),例如通過手動工作或使用NLP關(guān)鍵字提取/主題標(biāo)記模型來為每個文檔識別適當(dāng)?shù)年P(guān)鍵字,以及從查詢中推斷正確的關(guān)鍵字。
? ? ? ?這就是LlamaIndex引入文檔摘要索引的地方,該索引可以為每個文檔提取和索引非結(jié)構(gòu)化文本摘要,從而提高了現(xiàn)有方法之外的檢索性能。這個索引比單個文本塊包含更多的信息,并且比關(guān)鍵字標(biāo)簽具有更多的語義。它還允許靈活的檢索,包括LLM和基于embedding的方法。
? ? ? ?在構(gòu)建期間,該索引加載文檔,并使用LLM從每個文檔中提取摘要。在查詢期間,它根據(jù)摘要檢索要查詢的相關(guān)文檔,方法如下:
- 基于LLM的檢索:獲取文檔摘要集合,并請求LLM識別相關(guān)文檔+相關(guān)性得分
- 基于embedding的檢索:利用摘要嵌入相似性來檢索相關(guān)文檔,并對檢索結(jié)果的數(shù)量施加top-k限制。
注意:Document Summary Index的檢索類檢索任何選定文檔的所有節(jié)點,而不是在節(jié)點級別返回相關(guān)塊。
下面來看一個例子:
import nest_asyncio
nest_asyncio.apply()
?
from llama_index import (
SimpleDirectoryReader,
LLMPredictor,
ServiceContext,
ResponseSynthesizer
)
from llama_index.indices.document_summary import GPTDocumentSummaryIndex
from langchain.chat_models import ChatOpenAI
?
wiki_titles = ["Toronto", "Seattle", "Chicago", "Boston", "Houston"]
?
from pathlib import Path
?
import requests
for title in wiki_titles:
response = requests.get(
'https://en.wikipedia.org/w/api.php',
params={
'action': 'query',
'format': 'json',
'titles': title,
'prop': 'extracts',
# 'exintro': True,
'explaintext': True,
}
).json()
page = next(iter(response['query']['pages'].values()))
wiki_text = page['extract']
?
data_path = Path('data')
if not data_path.exists():
Path.mkdir(data_path)
?
with open(data_path / f"{title}.txt", 'w') as fp:
fp.write(wiki_text)
?
# Load all wiki documents
city_docs = []
for wiki_title in wiki_titles:
docs = SimpleDirectoryReader(input_files=[f"data/{wiki_title}.txt"]).load_data()
docs[0].doc_id = wiki_title
city_docs.extend(docs)
?
# # LLM Predictor (gpt-3.5-turbo)
llm_predictor_chatgpt = LLMPredictor(llm=ChatOpenAI(temperature=0, model_name="gpt-3.5-turbo"))
service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor_chatgpt, chunk_size_limit=1024)
?
# default mode of building the index
response_synthesizer = ResponseSynthesizer.from_args(response_mode="tree_summarize", use_async=True)
doc_summary_index = GPTDocumentSummaryIndex.from_documents(
city_docs,
service_context=service_context,
response_synthesizer=response_synthesizer
)
?
doc_summary_index.get_document_summary("Boston")
知識圖譜索引(Knowledge Graph Index)
? ? ? ?它通過在一組文檔上提取形式為(主語、謂語、賓語)的知識三元組來構(gòu)建索引,了解知識圖譜的讀者不陌生。
? ? ? ?在查詢期間,它可以只使用知識圖作為上下文進(jìn)行查詢,也可以利用每個實體的底層文本作為上下文。通過利用底層文本,我們可以針對文檔的內(nèi)容提出更復(fù)雜的查詢。
? ? ? ?把一個圖想象成相互連接的邊和頂點。
更多例子,可以參考(https://gpt-index.readthedocs.io/en/latest/examples/index_structs/knowledge_graph/KnowledgeGraphDemo.html)
各種Index對比
文章來源:http://www.zghlxwxcb.cn/news/detail-684049.html
?文章來源地址http://www.zghlxwxcb.cn/news/detail-684049.html
到了這里,關(guān)于LLM本地知識庫問答系統(tǒng)(二):如何正確使用LlamaIndex索引的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!