我们已经将文档切开成了更小的、且语义清晰的块,接下来要做的就是将这些块放入到一个索引中,这样当我们要答复某个数据集相关的问题时,就能轻松地检索到对应的块。
要完成这一政策,我们需求用到两个技术:嵌入(Embedding)和向量存储(Vector Store)。
我们在前面的课程里已经介绍过这两个技术,这儿先只做简略的复习。
向量存储和嵌入
嵌入是将一段文本转化为数值方式。具有相似内容的文本在数值空间中会有相似的向量,这就意味着我们可以经过比较这些向量,来找出相似的文本片段。
而向量存储是一种数据库,它用来存储切开后的文档片段以及它们对应的嵌入,便利我们后续根据问题查找相关的文档。
整个进程如下:
- 提出一个问题,并为它生成一个嵌入;
- 将它跟向量存储里的全部不同的向量进行比较;
- 选出最相似的前n个片段;
- 将选出的片段和问题一同输入到LLM里,得到一个答案。
为了帮忙了解,我们先看一个简略的比方:
进程1:供给一些例句,其间前两句非常相似,第三句则与前两句关联不大
sentence1 = "i like dogs"
sentence2 = "i like canines"
sentence3 = "the weather is ugly outside"
进程2:运用Embbeding类为每个语句生成一个嵌入
from langchain.embeddings.openai import OpenAIEmbeddings
embedding = OpenAIEmbeddings()
embedding1 = embedding.embed_query(sentence1)
embedding2 = embedding.embed_query(sentence2)
embedding3 = embedding.embed_query(sentence3)
进程3:用点积(dot product)来计算两两之间的嵌入相似度
import numpy as np
np.dot(embedding1, embedding2)
# 0.9631853877103518
np.dot(embedding1, embedding3)
# 0.7709997651294672
np.dot(embedding2, embedding3)
# 0.7596334120325523
点积的值越大,代表相似度就越高。
再来看一个实践的比方:
政策是为供给的全部PDF文档生成嵌入,并把它们存储在一个向量存储里。
进程1:加载PDF文档
from langchain.document_loaders import PyPDFLoader
# 加载 PDF
loaders = [
# 重复加载第一个文档,模拟一些脏数据
PyPDFLoader("docs/cs229_lectures/MachineLearning-Lecture01.pdf"),
PyPDFLoader("docs/cs229_lectures/MachineLearning-Lecture01.pdf"),
PyPDFLoader("docs/cs229_lectures/MachineLearning-Lecture02.pdf"),
PyPDFLoader("docs/cs229_lectures/MachineLearning-Lecture03.pdf")
]
docs = []
for loader in loaders:
docs.extend(loader.load())
进程2:用递归字符文本切开器来把文档分成块
# 切开
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size = 1500,
chunk_overlap = 150
)
splits = text_splitter.split_documents(docs)
进程3:为每个块生成嵌入,并创建Chroma向量存储
这儿用到的向量存储是Chroma。Chroma是一种轻量级、根据内存的向量存储,运用起来很便利。
from langchain.vectorstores import Chroma
# 可先用[rm -rf ./docs/chroma]移除或许存在的旧数据库数据
persist_directory = 'docs/chroma/'
# 传入之前创建的切开和嵌入,以及耐久化目录
vectordb = Chroma.from_documents(
documents=splits,
embedding=embedding,
persist_directory=persist_directory
)
进程4:用相似性查找方法来查找文档
question = "is there an email i can ask for help"
# K=3用于指定回来的文档数量
docs = vectordb.similarity_search(question,k=3)
可以打印文档的长度和内容来检查:
len(docs)
# 3
docs[0].page_content
进程5:耐久化向量数据库,以便今后运用
vectordb.persist()
接下来我们将谈论一些边际案例,展现几种或许出现失利情况:
失利情况1:重复的块导致重复的冗余信息
question = "what did they say about matlab?"
docs = vectordb.similarity_search(question,k=5)
docs[0]
docs[1]
其间,docs[0] 和 docs[1] 得到的效果是相同的,这是因为我们在一开始就有意重复加载了第一个文档。
这样做的效果是,我们把两个内容相同的分块都传给了言语模型。而第二个分块是没有价值的,假设换成一个内容不同的分块会更好,这样至少言语模型可以从中获取更多信息。
在下一课中,我们将谈论如何在保证检索到相关的块的一同,也能保证每个块都是唯一的。
失利情况2:无法完整捕捉到问题中的要害信息
比方下面这个问题,“第三堂课里他们讲了什么关于回归的内容?”
question = "what did they say about regression in the third lecture?"
docs = vectordb.similarity_search(question,k=5)
一般来说,我们应该能看出,问题的提问者是想要从第三堂课里找到答案的。
但实践上,当我们遍历全部文档,并打印出元数据后会发现,效果里实践上混合了多个文档的内容。
for doc in docs:
print(doc.metadata)
# {'source': 'docs/cs229_lectures/MachineLearning-Lecture03.pdf', 'page': 0}
# {'source': 'docs/cs229_lectures/MachineLearning-Lecture03.pdf', 'page': 14}
# {'source': 'docs/cs229_lectures/MachineLearning-Lecture02.pdf', 'page': 0}
# {'source': 'docs/cs229_lectures/MachineLearning-Lecture03.pdf', 'page': 6}
# {'source': 'docs/cs229_lectures/MachineLearning-Lecture01.pdf', 'page': 8}
这是因为,我们只是根据嵌入做了一个语义查找,它为整个语句生成了一个嵌入,而且或许会更重视于“回归”这个词。
当我们检查第五个文档时,就会发现它确实说到了“回归”这个词。
失利情况3:跟着检索文档数量的增加,相关性逐渐降低
当我们测验改动k值,也就是检索的文档数量时,我们会得到更多的文档,但效果列表后边的文档或许没有前面的那些相关性强。
检索
在这一课中,我们将深入探讨「检索」技术,并介绍一些更先进的方法来战胜上一课的边际情况。
检索是检索增强生成(RAG)流程的中心。
处理多样性:最大边际相关性
最大边际相关性(MMR, Maximum Marginal Relevance)背面的理念是,假设我们总是选择与查询在嵌入空间中最相似的文档,我们或许会错过一些多元化的信息。
MMR可以帮忙我们选择一个更多样化的文档调集。
MMR在保持查询相关性的一同,尽量增加效果之间的多样性,它的做法是:
- 首先发送一个查询,得到一组答复;
- 用”fetch_k”参数指定我们想要获取的照应数量,这彻底根据语义相似性;
- 然后,针对这个较小的文档调集,从多样性方面进行优化;
- 毕竟从这组文档中,选择”k”个照应回来给用户。
我们用一个简略的比方来帮忙了解:
政策是查询有指定特征的蘑菇信息。
进程1:创建Chroma向量存储
from langchain.vectorstores import Chroma
from langchain.embeddings.openai import OpenAIEmbeddings
persist_directory = 'docs/chroma/'
embedding = OpenAIEmbeddings()
vectordb = Chroma(
persist_directory=persist_directory,
embedding_function=embedding
)
进程2:用少量信息创建一个小型数据库
# 鹅膏菌有一个巨大而宏伟的子实体(地上部分)。
# 有大子实体的蘑菇是鹅膏菌。有些品种是全白色的。
# 鹅膏菌,又名去世帽,是全部已知蘑菇中毒性最强的一种。
texts = [
"""The Amanita phalloides has a large and imposing epigeous (aboveground) fruiting body (basidiocarp).""",
"""A mushroom with a large fruiting body is the Amanita phalloides. Some varieties are all-white.""",
"""A. phalloides, a.k.a Death Cap, is one of the most poisonous of all known mushrooms.""",
]
smalldb = Chroma.from_texts(texts, embedding=embedding)
进程3:进行相似性查找
# 告诉我有关带有大子实体的全白蘑菇的信息
question = "Tell me about all-white mushrooms with large fruiting bodies"
smalldb.similarity_search(question, k=2)
# [Document(page_content='A mushroom with a large fruiting body is the Amanita phalloides. Some varieties are all-white.', metadata={}),
# Document(page_content='The Amanita phalloides has a large and imposing epigeous (aboveground) fruiting body (basidiocarp).', metadata={})]
可以看到,它根据k值回来了两个最相关的文档,但没有说到它们有毒的事实。
进程4:进行MMR查找
smalldb.max_marginal_relevance_search(question,k=2, fetch_k=3)
# [Document(page_content='A mushroom with a large fruiting body is the Amanita phalloides. Some varieties are all-white.', metadata={}),
# Document(page_content='A. phalloides, a.k.a Death Cap, is one of the most poisonous of all known mushrooms.', metadata={})]
这儿我们传入了”k=2″,标明依然想回来两个文档,但我们设置了”fetch_k=3″,标明想获取三个文档。然后我们就可以看到,回来的文档中包含了它们有毒的事实。
现在我们试着用这个方法来处理上一节课中的失利情况1:
question = "what did they say about matlab?"
docs_mmr = vectordb.max_marginal_relevance_search(question,k=3)
docs_mmr[0].page_content[:100]
# 'those homeworks will be done in either MATLA B or in Octave, which is sort of — I \nknow some people '
docs_mmr[1].page_content[:100]
# 'algorithm then? So what’s different? How come I was making all that noise earlier about \nleast squa'
可以看到,第一个文档跟之前一样,因为它最相关。而第二个文档这次就不同了,这说明MMR让答复中增加了一些多样性。
处理特殊性:运用自查询检索器处理元数据
自查询运用言语模型将原始问题切开为两个独立的部分,一个过滤器和一个查找项。
查找项就是我们在语义上想要查找的问题内容。
过滤器则是包含我们想要过滤的元数据。
比方,“1980年制造的关于外星人的电影有哪些”,语义部分就是“关于外星人的电影”,元数据部分则是“电影年份应为1980年”。
我们先手动指定一个元数据过滤器来验证它的作用。
政策是处理上一节课的失利情况2:
question = "what did they say about regression in the third lecture?"
# 指定源为第三堂课的PDF文档
docs = vectordb.similarity_search(
question,
k=3,
filter={"source":"docs/cs229_lectures/MachineLearning-Lecture03.pdf"}
)
for d in docs:
print(d.metadata)
# {'source': 'docs/cs229_lectures/MachineLearning-Lecture03.pdf', 'page': 0}
# {'source': 'docs/cs229_lectures/MachineLearning-Lecture03.pdf', 'page': 14}
# {'source': 'docs/cs229_lectures/MachineLearning-Lecture03.pdf', 'page': 4}
可以看到,现在检索到的文档都来自那一堂课了。
我们还可以运用SelfQueryRetriever
,从问题本身推断出元数据。
进程1:供给元数据字段信息
from langchain.llms import OpenAI
from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain.chains.query_constructor.base import AttributeInfo
metadata_field_info = [
AttributeInfo(
name="source",
description="The lecture the chunk is from, should be one of `docs/cs229_lectures/MachineLearning-Lecture01.pdf`, `docs/cs229_lectures/MachineLearning-Lecture02.pdf`, or `docs/cs229_lectures/MachineLearning-Lecture03.pdf`",
type="string",
),
AttributeInfo(
name="page",
description="The page from the lecture",
type="integer",
),
]
这个比方中的元数据只要两个字段,源(source)和页(page)。我们需求填写每个字段的称谓、描绘和类型。这些信息会被传给言语模型,所以需求尽或许描绘得清楚。
进程2:初始化自查询检索器
# 指定文档实践内容的信息
document_content_description = "Lecture notes"
llm = OpenAI(temperature=0)
retriever = SelfQueryRetriever.from_llm(
llm,
vectordb,
document_content_description,
metadata_field_info,
verbose=True
)
进程3:运转自查询检索器查找问题
question = "what did they say about regression in the third lecture?"
docs = retriever.get_relevant_documents(question)
for d in docs:
print(d.metadata)
# {'source': 'docs/cs229_lectures/MachineLearning-Lecture03.pdf', 'page': 0}
# {'source': 'docs/cs229_lectures/MachineLearning-Lecture03.pdf', 'page': 14}
# {'source': 'docs/cs229_lectures/MachineLearning-Lecture03.pdf', 'page': 4}
可以看到,语义部分标明这是一个关于回归的查询。过滤器部分标明我们只想看那些source值为指定值的文档。
而从打印出的元数据看,它们都来自指定的那一堂课,说明自查询检索器确实可以用来精确地进行元数据过滤。
处理相关性:运用上下文紧缩提取出与查询最相关的部分
进步检索文档质量的另一种方法是紧缩。
当你提出一个问题时,你会得到整个存储的文档,但或许只要其间一小部分是跟问题相关的。
也就是说,与查询最相关的信息或许被隐藏在包含大量无关文本的文档里。
上下文紧缩就是为了处理这个问题的。
经过紧缩,你可以先让言语模型提取出最相关的片段,然后只把最相关的片段传给毕竟的言语模型调用。
这会增加言语模型调用的本钱,但也会让毕竟的答案更会集在最重要的内容上,这需求我们自己权衡。
进程1:创建上下文紧缩检索器
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
# 包装我们的向量存储
llm = OpenAI(temperature=0)
compressor = LLMChainExtractor.from_llm(llm)
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=vectordb.as_retriever()
)
进程2:提出问题,检索紧缩后的相关文档
question = "what did they say about matlab?"
compressed_docs = compression_retriever.get_relevant_documents(question)
pretty_print_docs(compressed_docs)
从紧缩后的文档我们可以看到, 一,它们比一般的文档短得多; 二,依然有重复的内容,这是因为底层我们仍是用的语义查找算法。
为了处理内容重复的问题,我们可以在创建检索器时,结合前面的内容,把查找类型设置为MMR。
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=vectordb.as_retriever(search_type = "mmr")
)
question = "what did they say about matlab?"
compressed_docs = compression_retriever.get_relevant_documents(question)
pretty_print_docs(compressed_docs)
从头运转之后,我们得到的就是一个没有任何重复信息的过滤后的效果集了。
下一步,我们将谈论如何运用这些检索到的文档来答复用户的问题。