我的学习笔记
LlamaIndex—正确使用索引

字数统计: 5.2k阅读时长: 21 min
2023/07/12

在第一篇文章构建PDF聊天机器人之后,我们需要在第二篇文章之前充分了解Llamaindex的重要性。

与其匆忙进入编码和实现阶段,不如优先深入理解Llamaindex及其基础概念。通过这样做,我们将能够更好地理解如何将这些原则应用到我们的具体项目中,并避免不必要的错误。

img

因此,在继续第2部分之前,我们花一些时间深入研究Llamaindex的复杂性及其在聊天机器人开发中的重要性。通过扩展我们的知识和掌握基础知识,我们将更好地准备创建一个强大而有效的PDF聊天机器人。

在这篇文章中,我将介绍Llamaindex的基本组件,它在聊天机器人开发中的实际应用,以及花时间了解它的好处。

我为什么不去看官方文件?

“你为什么不专注于开发应用程序,而不是解释一些我们可以通过官方文件轻松阅读的东西?”

值得注意的是,官方网站并不总是最新的,可能不会提供详细的信息解释。因此,可能有必要寻求额外的资源或进行进一步的研究,以充分了解内容。这可能是一个耗时的过程,但重要的是要确保获得的信息是准确和可靠的。

我不仅提供了详细的解释,还提供了真实的用例以及使用什么场景和索引,以便您更好地理解将要构建的基础。

LlamaIndex简介

LlamaIndex(也称为GPT Index)是一个用户友好的界面,它将您的外部数据连接到大型语言模型(Large Language Models, llm)。它提供了一系列工具来简化流程,包括可以与各种现有数据源和格式(如api、pdf、文档和SQL)集成的数据连接器。此外,LlamaIndex为结构化和非结构化数据提供索引,可以毫不费力地与大语言模型一起使用。

本文将讨论LlamaIndex提供的不同类型的索引以及如何使用它们。这可能包括列表索引、矢量存储索引、树索引和关键字表索引的分解,以及特殊索引,如图索引、Pandas索引、SQL索引和文档摘要索引。此外,我将详细介绍每个索引的情况,可能有必要讨论使用LlamaIndex的成本,并将其与其他选项进行比较。

为什么我们需要LlamaIndex ?

商业ChatGPT还不够好吗?

是的,它在一般用例中可能足够了,但请记住,我们的目标是在您的文档湖(类比数据湖)上构建通用聊天机器人应用程序。想想你的公司文档可能有超过1000页,那么ChatGPT广告将不足以分析你的东西。主要原因是token的限制。

  • GPT-3: 2000 tokens
  • GPT-3.5: 4000 tokens
  • GPT-4: 提升到32.000 tokens

1,000 tokens大概是750单词

img

GPT-3, GPT-3.5, GPT-4和LlamaIndex被Flyps接受的tokens数量

LlamaIndex是如何适应的?

如果可用的tokens不多,则无法在prompt中输入更大的数据集,这可能会限制您对模型的操作。然而,您仍然可以训练模型,尽管有一些优点和缺点需要考虑。不过别担心,LlamaIndex会帮你的!

使用LlamaIndex,您可以为各种数据集(如文档、pdf和数据库)建立索引,然后轻松地查询它们以查找所需的信息。

想象一下,只需点击几下就可以访问您需要的所有信息!您可以直接向知识库、Slack和其他通信工具以及数据库和几乎所有SaaS内容提出复杂的问题,而无需以任何特殊方式准备数据。最好的部分是什么?您将得到由GPT推理能力支持的答案,所有这些都在几秒钟内完成,甚至不必将任何内容复制和粘贴到prompts符中。

通过正确实现GPT Index,您可以使这一切成为可能!在下一节中,我们将深入研究不同类型的索引,以及为您的应用程序准备的适用代码。

用LlamaIndex索引

在能够有效地用自然语言提出问题并获得准确的答案之前,有必要对相关数据集进行索引。如前所述,LlamaIndex能够索引广泛的数据类型,随着GPT-4即将到来,多模式索引也将很快可用。这一部分,我们将研究LlamaIndex提供的不同类型的索引,看看什么索引用于什么用例。

在深入了解索引的细节之前,您应该知道LlamaIndex的核心是将文档分解为多个Node对象。节点是LlamaIndex中的一等公民。节点表示源文档的“块”,无论是文本块、图像块还是更多。它们还包含元数据以及与其他节点和索引结构的关系信息。当您创建索引时,它抽象了节点的创建,但是,如果您的需求需要,您可以手动为文档定义节点。

让我们先设置一些底层代码。

  • 安装库
1
2
pip install llama-index
pip install openai
  • 安装OpenAI API Key
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)

列表Index

列表索引是一种简单的数据结构,其中节点按顺序存储。在索引构建期间,文档文本被分块、转换为节点并存储在列表中。

img

来自LlamaIndex官方文件

在查询期间,如果没有指定其他查询参数,LlamaIndex只是将列表中的所有node加载到Response Synthesis模块中。

img

来自LlamaIndex官方文件

列表索引确实提供了许多查询列表索引的方法,从基于嵌入的查询中获取前k个邻居,或者添加一个关键字过滤器,如下所示:

img

来自LlamaIndex官方文件

此列表索引对于综合跨多个数据源的信息的答案非常有用

LlamaIndex为列表索引提供Embedding支持。除了每个节点存储文本之外,每个节点还可以选择存储Embedding。在查询期间,我们可以在调用LLM合成答案之前,使用Embeddings对节点进行最大相似度检索。

由于使用Embeddings的相似性查找(例如使用余弦相似性)不需要LLM调用,Embeddings作为一种更便宜的查找机制,而不是使用大语言模型来遍历节点

这意味着在索引构建过程中,LlamaIndex不会调用LLM来生成Embedding,而是在查询时生成。这种设计选择避免了在索引构建期间为所有文本块生成Embeddings的需要,这可能会导致大量数据的开销。

您很快就会发现,将多个索引组合在一起可以帮助您避免高昂的Embedding成本。但这还不是全部——它还可以提高应用程序的整体性能!另一种方法是使用自定义Embedding(而不是使用OpenAI),但我们不会在本文中研究这种方法,因为它值得另一种方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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>"))

向量存储索引

它是最常见且易于使用的,允许对大型数据语料库回答查询

img

来自LlamaIndex官方文件

默认情况下,GPTVectorStoreIndex 使用内存中的 SimpleVectorStore作为默认存储上下文的一部分初始化。

与列表索引不同,基于向量存储的索引在索引构建期间生成Embeddings

这意味着在索引构建期间将调用LLM端点以生成Embeddings数据。

Query(查询)向量存储索引包括获取top-k最相似的节点,并将它们传递到我们的响应合成模块。

img

来自LlamaIndex官方文件
1
2
3
4
5
6
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

树状索引

它对总结一组文件很有用

树状索引是树结构索引,其中每个节点是子节点的摘要。在索引构建期间,树以自下而上的方式构建,直到我们最终得到一组根节点。

树状索引从一组节点(成为该树中的叶节点)构建层次树。

img

来自LlamaIndex官方文件

查询树状索引涉及从根节点向下遍历到叶节点。默认情况下(child_branch_factor=1 ),查询在给定父节点的情况下选择一个子节点。如果child_branch_factor=2,则查询在每个级别选择两个子节点。

img

来自LlamaIndex官方文件

与向量索引不同,LlamaIndex不会调用LLM来生成Embedding,而是在查询时生成。Embeddings被惰性地生成,然后缓存(如果retriver_mode ="Embedding" query(…)期间指定),而不是在索引构建期间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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>"))

为了在查询期间构建树状索引,我们需要将retriver_moderesponse_mode添加到查询引擎,并将GPTTreeIndex中的build_tree参数设置为False

1
2
3
4
5
6
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?")

关键词表索引

这对于将查询路由到不同的数据源非常有用

关键字表索引从每个Node提取关键字,并构建从每个关键字到该关键字对应的Node的映射。

img

来自LlamaIndex官方文件

在查询时,我们从查询中提取相关关键字,并将其与预提取的Node关键字进行匹配,获取相应的Node。提取的节点被传递到响应合成模块。

img

来自LlamaIndex官方文件

注意到 GPTKeywordTableIndex-使用LLM从每个文档中提取关键字,这意味着它确实需要在构建期间调用LLM

但是,如果您使用GPTSimpleKeywordTableIndex,它使用regex关键字提取器从每个文档中提取关键字,则在构建期间不会调用LLM

1
2
3
4
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?")

可组合性图索引

它对于构建知识图谱很有用

使用LlamaIndex,您可以通过在现有索引之上构建索引来创建复合索引。该特性使您能够有效地索引完整的文档层次结构,并为GPT提供量身定制的知识。

通过利用可组合性,您可以在多个级别定义索引,例如为单个文档定义低级索引,为文档组定义高级索引。考虑下面的例子:

  • 您可以为每个文档中的文本创建树索引。

  • 生成一个列表索引,涵盖所有的树索引为您的整个文档集合。

通过一个场景编写代码:我们将执行以下步骤来演示可组合性图索引的能力:

  • 从多个文档创建树索引

  • 从树索引生成摘要。如前所述,Tree Index对于总结文档集合很有用。

  • 接下来,我们将在3个树索引的顶部创建一个列表索引的图。为什么?因为列表索引适合于合成跨多个数据源组合信息的答案。

  • 最后查询图。

实现:

我会阅读苹果2022年和2023年的10k报告,并在两个季度之间提出财务问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
## 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)

为每个季度创建矢量指数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
## 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}')

从树索引生成摘要。如前所述,Tree Index对于总结文档集合很有用。

1
2
# 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个树索引之上创建一个列表索引的Graph

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
### 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

想知道我们如何利用Langchain Agent作为聊天机器人,请关注/订阅未来的更多更新:)

Pandas索引和SQL索引

它对结构化数据很有用

简单和非常直接,我将直接进入演示。

Pandas Index:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

img

SQL Index:

考虑一个很酷的应用程序,你可以将你的LLM应用程序附加到你的数据库,并在它上面提问。这个示例代码取自这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# 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?")

img

在底层,有一个Langchain库插件可以使用。我们将在另一篇文章中介绍Langchain。

文档摘要索引

这是一个全新的LlamaIndex数据结构,它是为了问答而制作的。到目前为止,我们已经讨论了单个索引,当然我们可以通过使用单个索引或将多个索引组合在一起来构建LLMQA应用程序。

通常,大多数用户以以下方式开发基于llm的QA系统:

  1. 它们获取源文档并将其分成文本块。

  2. 然后将文本块存储在矢量数据库中。

  3. 在查询期间,通过使用相似度和/或关键字过滤器进行Embedding来检索文本块。

  4. 执行响应综合。

然而,这种方法存在一些影响检索性能的局限性。

现有方法的缺点:

  1. 文本块没有完整的全局上下文,这通常限制了问答过程的有效性。
  2. 需要仔细调优top-k /相似性分数阈值,因为过小的值可能会导致错过相关上下文,而过大的值可能会增加不相关上下文的成本和延迟。
  3. Embeddings可能并不总是为一个问题选择最合适的上下文,因为这个过程本质上是分别决定文本和上下文的。

为了增强检索结果,一些开发人员添加了关键字过滤器。然而,这种方法有其自身的挑战,例如通过手工或使用NLP关键字提取/主题标记模型为每个文档确定适当的关键字,以及从查询中推断正确的关键字。

img

这就是LlamaIndex引入文档摘要索引的地方,它可以为每个文档提取和索引非结构化文本摘要,从而提高了现有方法的检索性能。该索引包含比单个文本块更多的信息,并且比关键字标签具有更多的语义含义。它还允许灵活的检索,包括LLM和基于嵌入的方法。

在构建期间,该索引摄取文档并使用LLM从每个文档提取摘要。在查询期间,根据摘要检索相关文档进行查询,使用以下方法:

  • - **基于LLM的检索:**获取文档摘要集合,请求LLM识别相关文档+相关性评分

    - **基于嵌入的检索:**利用摘要Embedding相似度检索相关文档,并对检索结果的数量施加top-k限制。

注意:文档摘要索引的检索类为任何选定的文档检索所有节点,而不是在节点级返回相关块。

看看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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")

知识图谱索引

它通过在一组文档上以以下形式提取知识三元组(主题、谓词、对象)来构建索引。

在查询期间,它可以只使用知识图作为上下文进行查询,也可以利用来自每个实体的底层文本作为上下文进行查询。通过利用底层文本,我们可以对文档的内容进行更复杂的查询。

把一个图想象成这样,你可以看到所有的边和顶点都是相互连接的。

img

来自LlamaIndex官方文件

你可以看看这个页面作为参考。

需要考虑的事实

在我们的PDF聊天机器人实施大语言模型期间,我提请注意我们想与您分享的重要方面,即:索引成本和索引时间(速度)。

索引的成本

索引费用是需要考虑的一个关键因素,正如我在本文前面所强调的那样。这在处理大量数据集时尤为重要,这也是我提倡使用LlamaIndex的原因。

你可以找到各个OpenAI模型的价格(https://openai.com/pricing)。

索引速度

第二个重要问题是文档索引的时间,即为操作准备整个解决方案的时间。根据我的实验,索引时间各不相同,但这是一次性的,也取决于OpenAI服务器。

通常,40页的pdf大约需要5秒。想象一下,一个拥有超过10万页的庞大数据集,可能需要几天的时间。我们可以利用async方法来减少索引时间。我将在另一篇文章中写这一点。

总结

img


联系我



原文作者:yuanwai

原文链接:https://www.luxiangdong.com/2023/07/12/llamaindex/

发表日期:July 12th 2023, 10:20:10 am

更新日期:September 4th 2023, 9:09:23 am

版权声明:

CATALOG
  1. 1. 我为什么不去看官方文件?
  2. 2. LlamaIndex简介
  3. 3. 为什么我们需要LlamaIndex ?
  4. 4. LlamaIndex是如何适应的?
  5. 5. 用LlamaIndex索引
    1. 5.1. 列表Index
    2. 5.2. 向量存储索引
    3. 5.3. 树状索引
    4. 5.4. 关键词表索引
    5. 5.5. 可组合性图索引
    6. 5.6. Pandas索引和SQL索引
    7. 5.7. 文档摘要索引
    8. 5.8. 知识图谱索引
  6. 6. 需要考虑的事实
    1. 6.1. 索引的成本
    2. 6.2. 索引速度
    3. 6.3. 总结