我的学习笔记

土猛的员外

LlamaIndex—正确使用索引

在第一篇文章构建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


我们的创业项目已经上线!!!

TorchV AI,帮助企业快速进入AI时代!

具体详情,请点击官网咨询


最新内容,关注“土猛的员外”公众号