AI知识库

53AI知识库

学习大模型的前沿技术与行业应用场景


如何使用 Llama 3 构建本地文件生成式搜索引擎(附源码&文档)
发布日期:2024-07-20 08:12:16 浏览次数: 2361


导读

通过阅读本文你将了解:

  1. 如何使用llama 3 构建生成式搜索引擎

  2. 获取所有源码

关注公众号,后台发送"llama3搜索引擎构建"获取所有源码



其余llama 3相关文章参见:

在 Visual Studio Code 中运行 Llama 3 :Step by Step

消费级 GPU 微调Llama 3 :百万规模数据 (附数据&源码)

使用 Llama3 + Groq构建新闻机器人(附源码)

运行最强开源LLM大模型:Llama3 70B,只需一个4GB GPU!


系统设计

为了构建一个本地生成式搜索引擎或助手,我们需要几个组建:

  1. 包含本地文件内容的索引,具有信息检索引擎,用于检索给定查询/问题的最相关文档。

  2. 大语言模型,用于从本地文档中选择内容并生成摘要答案

  3. 一个用户界面


这些组件的交互方式如下图所示。    


首先,我们需要将本地文件索引到可以查询本地文件内容的索引中。

然后,当用户提出问题时,我们将使用创建的索引,以及一些不对称段落或文档embeddings,以检索可能包含答案的最相关文档。

这些文档的内容和问题将传递给本地部署的大型语言模型,该模型将使用给定文档的内容生成答案。

在指令提示prompt中,我们还会要求大语言模型返回所使用文档的引用。

最终,所有内容将在用户界面上向用户可视化呈现。


现在,让我们更详细地看每个组件如何工作和使用。


语义索引

构建一个语义索引,它将根据文件内容的相似性和给定的查询为我们提供最相关的文档。

为了创建这样的索引,我们将使用 Qdrant 作为向量存储(当然也此处也可以使用其他, 例如ElasticSearch)

Qdrant 客户端库 不需要完整安装 Qdrant 服务器,并且可以对适合工作内存(RAM)的文档进行相似性计算。因此,我们所需要做的就是 pip 安装 Qdrant 客户端。

可以按照以下方式初始化 Qdrant(请注意,由于pipeline流程的原因,hf 参数稍后定义,但是使用 Qdrant 客户端时,您已经需要定义使用哪种向量化方法和度量标准):    


from qdrant_client import QdrantClient              from qdrant_client.models import Distance, VectorParams              client = QdrantClient(path="qdrant/")              collection_name = "MyCollection"              if client.collection_exists(collection_name):                  client.delete_collection(collection_name)              

client.create_collection(collection_name,vectors_config=VectorParams(size=768, distance=Distance.DOT)) qdrant = Qdrant(client, collection_name, hf)


为了创建向量索引,我们将不得不将文档embedding。对于embedding,我们将不得不选择正确的嵌入方法和正确的向量相似度度量标准。更多相似度度量参见:推荐系统中的相似度度量

可以使用多种段落、句子或单词嵌入方法,得到不同的结果。更多embeddings方法参见:最佳Embedding模型效果对比  - OpenAI / Cohere / Google / E5 / BGE 以及 如何使用 "套娃词向量" 模型提升RAG检索速度和效果(附论文+模型权重)


基于文档创建embedding搜索的主要问题是不对称搜索的问题。

不对称搜索问题在信息检索中很常见,当查询较短且文档较长时会出现。单词或句子嵌入通常被微调以根据相似大小的文档(句子或段落)提供相似性分数。一旦不是这种情况,适当的信息检索可能会失败。

然而,我们可以找到一种嵌入方法,可以很好地解决不对称搜索问题。例如,通常在 MSMARCO 数据集上进行微调的模型效果很好。

MSMARCO 数据集基于必应搜索查询和文档,并由微软发布。因此,它非常适合我们正在处理的问题。


对于本文特定的实现,我选择了一个已经微调的模型,称为:

sentence-transformers/msmarco-bert-base-dot-v5

该模型基于 BERT,并且使用点积作为相似性度量进行了微调。我们已经初始化了 qdrant 客户端,以在以下行中使用点积作为相似性度量(请注意,此模型的维度为 768):

client.create_collection(collection_name,vectors_config=VectorParams(size=768, distance=Distance.DOT))


我们可以使用其他度量标准,例如余弦相似度,但是由于该模型是使用点积进行微调的,因此使用该度量标准将获得最佳效果。

此外,从几何角度来看:余弦相似度仅关注角度的差异,而点积同时考虑角度和大小。通过将数据归一化为具有统一大小,这两种度量变得等效。

在忽略大小有益的情况下,余弦相似度是有用的。然而,如果大小很重要,点积是更适合的相似性度量。

初始化 MSMarco 模型的代码如下(如果您有可用的 GPU,请使用它,尽管如此):


    model_name = "sentence-transformers/msmarco-bert-base-dot-v5"                  model_kwargs = {'device': 'cpu'}                  encode_kwargs = {'normalize_embeddings': True}                  hf = HuggingFaceEmbeddings(                      model_name=model_name,                      model_kwargs=model_kwargs,                      encode_kwargs=encode_kwargs                  )  

  

接下来的问题是:由于transformer模型的二次内存要求,BERT 等模型的上下文大小受到限制。

在许多类似 BERT 的模型中,上下文大小设置为 512 个标记。有两种选择:

  1. 我们可以仅基于前 512 个标记的答案,并忽略文档的其余部分

  2. 创建一个索引,其中一个文档将被分成多个块并存储在索引中。


在第一种情况下,我们将丢失许多重要信息,因此我们选择了第二种变体。为了将文档分块,我们可以使用 LangChain 中的预构建块分割器:


from langchain_text_splitters import TokenTextSplitter              text_splitter = TokenTextSplitter(chunk_size=500, chunk_overlap=50)              texts = text_splitter.split_text(file_content)              metadata = []              for i inrange(0,len(texts)):                  metadata.append({"path":file})              qdrant.add_texts(texts,metadatas=metadata)

在提供的代码部分中,我们将文本分块为 500 个tokens大小(一般系统默认是1024个),重叠 50 个tokens的窗口

这样我们可以在块结束或开始的地方保留一些上下文。在代码的其余部分,我们创建了包含用户硬盘上文档路径的元数据,并将这些带有元数据的块添加到索引中。

然而,在将文件内容添加到索引之前,我们需要读取它。甚至在读取文件之前,我们需要获取需要索引的所有文件。

为了简单起见,在这个项目中,用户可以定义一个要索引的文件夹。索引器以递归方式检索该文件夹及其子文件夹中的所有文件,并索引支持的文件(我们将看看如何支持 PDF、Word、PPT 和 TXT)。


我们可以以递归方式检索给定文件夹及其子文件夹中的所有文件:


def get_files(dir):                  file_list = []                  for f in listdir(dir):                      if isfile(join(dir,f)):                          file_list.append(join(dir,f))                      elif isdir(join(dir,f)):                          file_list= file_list + get_files(join(dir,f))                  return file_list 

  

 

一旦在列表中检索到所有文件,我们可以读取包含文本的文件内容。在这个工具中,我们将支持 MS Word 文档(扩展名为“.docx”)、PDF 文档、MS PowerPoint 演示文稿(扩展名为“.pptx”)和纯文本文件(扩展名为“.txt”)。

为了读取 MS Word 文档,我们可以使用 docx-python 库。将文档读入字符串变量的函数如下:


import docx              def getTextFromWord(filename):                  doc = docx.Document(filename)                  fullText = []                  for para in doc.paragraphs:                      fullText.append(para.text)                  return'\n'.join(fullText)

类似的方法也可以用于 MS PowerPoint 文件。为此,我们需要下载并安装 pptx-python 库,并编写以下函数:


from pptx import Presentation              def getTextFromPPTX(filename):                  prs = Presentation(filename)                  fullText = []                  for slide in prs.slides:                      for shape in slide.shapes:                          fullText.append(shape.text)                  return'\n'.join(fullText)

读取文本文件非常简单:


f = open(file,'r')              file_content = f.read()              f.close()

在这种情况下,我们将使用 PyPDF2 库来读取 PDF 文件:


reader = PyPDF2.PdfReader(file)              for i inrange(0,len(reader.pages)):                  file_content = file_content + " "+reader.pages[i].extract_text()

最后,整个索引函数将如下所示:


file_content = ""                  forfile in onlyfiles:                      file_content = ""                      iffile.endswith(".pdf"):                          print("indexing "+file)                          reader = PyPDF2.PdfReader(file)                          for i inrange(0,len(reader.pages)):                              file_content = file_content + " "+reader.pages[i].extract_text()                      eliffile.endswith(".txt"):                          print("indexing " + file)                          f = open(file,'r')                          file_content = f.read()                          f.close()                      eliffile.endswith(".docx"):                          print("indexing " + file)                          file_content = getTextFromWord(file)                      eliffile.endswith(".pptx"):                          print("indexing " + file)                          file_content = getTextFromPPTX(file)                      else:                          continue                      text_splitter = TokenTextSplitter(chunk_size=500, chunk_overlap=50)                      texts = text_splitter.split_text(file_content)                      metadata = []                      for i inrange(0,len(texts)):                          metadata.append({"path":file})                      qdrant.add_texts(texts,metadatas=metadata)                  print(onlyfiles)                  print("Finished indexing!")

生成式搜索 API

我们将使用 FastAPI 创建一个网络服务来托管我们的生成式搜索引擎。API 将访问 Qdrant 客户端,使用我们在上一节中创建的索引数据执行搜索,使用向量相似度度量来生成答案,并最终将答案提供给用户。

为了初始化和导入生成式搜索组件的库,我们可以使用以下代码:



from fastapi import FastAPI              from langchain_community.embeddings import HuggingFaceEmbeddings              from langchain_qdrant import Qdrant              from qdrant_client import QdrantClient              from pydantic import BaseModel              import torch              from transformers import AutoTokenizer, AutoModelForCausalLM              import environment_var              import os              from openai import OpenAI              


class Item(BaseModel): query: str def__init__(self, query: str) -> None: super().__init__(query=query)

  

 

如前所述,我们使用 FastAPI 创建 API 接口。我们将利用 qdrant_client 库访问我们创建的索引数据,并利用 langchain_qdrant 库进行额外支持。对于嵌入和本地加载 Llama 3 模型,我们将使用 PyTorch 和 Transformers 库。此外,我们将使用 OpenAI 库调用 NVIDIA NIM API,并将 API 密钥存储在我们创建的 environment_var(用于 Nvidia 和 HuggingFace)文件中。

我们创建了一个名为 Item 的类,它派生自 Pydantic 中的 BaseModel,用于传递给请求函数的参数。它将有一个名为 query 的字段。

现在,我们可以开始初始化我们的机器学习模型


model_name = "sentence-transformers/msmarco-bert-base-dot-v5"              model_kwargs = {'device': 'cpu'}              encode_kwargs = {'normalize_embeddings': True}              hf = HuggingFaceEmbeddings(                  model_name=model_name,                  model_kwargs=model_kwargs,                  encode_kwargs=encode_kwargs              )

           
             

os.environ["HF_TOKEN"] = environment_var.hf_token              use_nvidia_api = False              use_quantized = True              if environment_var.nvidia_key !="":                  client_ai = OpenAI(                      base_url="https://integrate.api.nvidia.com/v1",                      api_key=environment_var.nvidia_key                  )                  use_nvidia_api = True              elif use_quantized:                  model_id = "Kameshr/LLAMA-3-Quantized"                  tokenizer = AutoTokenizer.from_pretrained(model_id)                  model = AutoModelForCausalLM.from_pretrained(                      model_id,                      torch_dtype=torch.float16,                      device_map="auto",                  )              else:                  model_id = "meta-llama/Meta-Llama-3-8B-Instruct"                  tokenizer = AutoTokenizer.from_pretrained(model_id)                  model = AutoModelForCausalLM.from_pretrained(                      model_id,                      torch_dtype=torch.float16,                      device_map="auto",                  )

 

在前几行中,我们加载了在 MSMARCO 数据上微调的基于 BERT 的模型的权重,我们还使用它来索引我们的文档。

然后,我们检查是否提供了 nvidia_key,如果提供了,我们将使用 OpenAI 库调用 NVIDIA NIM API。

当我们使用 NVIDIA NIM API 时,我们可以使用一个包含 70B 参数的 Llama 3 指令模型。如果未提供 nvidia_key,我们将在本地加载 Llama 3。

然而,在本地,至少对于大多数消费类电子产品来说,不可能加载 70B 参数模型。因此,我们将加载 Llama 3 8B 参数模型或额外进行量化的 Llama 3 8B 参数模型。

通过量化,我们节省了空间,并使模型能够在较少的 RAM 上执行。

例如,Llama 3 8B 通常需要约 14GB 的 GPU RAM,而 Llama 3 8B 量化后可以在 6GB 的 GPU RAM 上运行。因此,我们根据参数加载完整或量化模型。

现在我们可以初始化 Qdrant 客户端


client = QdrantClient(path="qdrant/")              collection_name = "MyCollection"              qdrant = Qdrant(client, collection_name, hf)

还有 FastAPI 并创建一个第一个模拟 GET 函数


app = FastAPI()                            @app.get("/")              asyncdef root():                  return {"message": "Hello World"}

这个函数将以 JSON 格式返回 {"message":"Hello World"}

然而,为了使此 API 起作用,我们将创建两个函数,一个仅执行语义搜索,而另一个将执行搜索,然后将前 10 个块作为上下文,并生成一个答案,引用它使用的文档。


@app.post("/search")              def search(Item:Item):                  query = Item.query                  search_result = qdrant.similarity_search(                      query=query, k=10                  )                  i = 0                  list_res = []                  for res in search_result:                      list_res.append({"id":i,"path":res.metadata.get("path"),"content":res.page_content})                  return list_res

           
             

@app.post("/ask_localai")              asyncdef ask_localai(Item:Item):                  query = Item.query                  search_result = qdrant.similarity_search(                      query=query, k=10                  )                  i = 0                  list_res = []                  context = ""                  mappings = {}                  i = 0                  for res in search_result:                      context = context + str(i)+"\n"+res.page_content+"\n\n"                      mappings[i] = res.metadata.get("path")                      list_res.append({"id":i,"path":res.metadata.get("path"),"content":res.page_content})                      i = i +1              


rolemsg = {"role": "system", "content": "Answer user's question using documents given in the context. In the context are documents that should contain an answer. Please always reference document id (in squere brackets, for example [0],[1]) of the document that was used to make a claim. Use as many citations and documents as it is necessary to answer question."} messages = [ rolemsg, {"role": "user", "content": "Documents:\n"+context+"\n\nQuestion: "+query}, ] if use_nvidia_api: completion = client_ai.chat.completions.create( model="meta/llama3-70b-instruct", messages=messages, temperature=0.5, top_p=1, max_tokens=1024, stream=False ) response = completion.choices[0].message.content else: input_ids = tokenizer.apply_chat_template( messages, add_generation_prompt=True, return_tensors="pt" ).to(model.device)
terminators = [ tokenizer.eos_token_id, tokenizer.convert_tokens_to_ids("<|eot_id|>") ]


outputs = model.generate( input_ids, max_new_tokens=256, eos_token_id=terminators, do_sample=True, temperature=0.2, top_p=0.9, ) response = tokenizer.decode(outputs[0][input_ids.shape[-1]:]) return {"context":list_res,"answer":response}

   

这两个函数都是 POST 方法,我们使用我们的 Item 类通过 JSON 主体传递查询。第一个方法返回最相似的 10 个文档块,带有路径,并将文档 ID 从 0 到 9 进行分配。因此,它只是使用点积作为相似度度量进行普通语义搜索(这是在 Qdrant 中定义的,记住包含 distance=Distance.DOT 的行)。

第二个名为 ask_localai 的函数稍微复杂。它包含了第一个方法中的搜索机制(因此可能更容易通过那里的代码来理解语义搜索),但添加了一个生成部分。它创建了一个 Llama 3 的提示,其中包含系统提示消息中的指令:

使用上下文中提供的文档回答用户的问题。上下文中应包含应该包含答案的文档。请始终引用文档 ID(例如 [0],[1] 中的方括号)用于提出主张的文档。使用尽可能多的引用和文档来回答问题。

用户的消息包含一个文档列表,结构化为 ID(0-9),后跟文档块。为了保持 ID 和文档路径之间的映射,我们创建了一个名为 list_res 的列表,其中包括 ID、路径和内容。用户提示以单词 "Question" 结尾,后跟用户的查询。

响应包含上下文和生成的答案。然而,答案再次由 Llama 3 70B 模型(使用 NVIDIA NIM API)、本地 Llama 3 8B 或本地 Llama 3 8B 量化生成,具体取决于传递的参数。

API 可以从包含以下代码行的单独文件中启动(假设我们的生成式组件在名为 api.py 的文件中,因为 Uvicorn 中的第一个参数映射到文件名):


import uvicorn              

if__name__=="__main__": uvicorn.run("api:app",host='0.0.0.0', port=8000, reload=False, workers=3)


用户界面

我们本地生成式搜索引擎的最终组件是用户界面。我们将使用 Streamlit 构建一个简单的用户界面,其中包括一个输入栏,一个搜索按钮,一个用于显示生成的答案的部分,以及一个可以打开或下载的引用文档列表。

Streamlit 中用户界面的整个代码不到 45 行(确切地说是 44 行):

import re              import streamlit as st              import requests              import json              st.title('_:blue[Local GenAI Search]_ :sunglasses:')              question = st.text_input("基于您的本地文件提出问题", "")              if st.button("提出问题"):                  st.write("当前问题是 \"", question+"\"")                  url = "http://127.0.0.1:8000/ask_localai"              

payload = json.dumps({ "query": question }) headers = { 'Accept': 'application/json', 'Content-Type': 'application/json' }

response = requests.request("POST", url, headers=headers, data=payload)

answer = json.loads(response.text)["answer"] rege = re.compile("\[Document\ [0-9]+\]|\[[0-9]+\]") m = rege.findall(answer) num = [] for n in m: num = num + [int(s) for s in re.findall(r'\b\d+\b', n)]



st.markdown(answer) documents = json.loads(response.text)['context'] show_docs = [] for n in num: for doc in documents: ifint(doc['id']) == n: show_docs.append(doc) a = 1244 for doc in show_docs: with st.expander(str(doc['id'])+" - "+doc['path']): st.write(doc['content']) withopen(doc['path'], 'rb') as f: st.download_button("下载文件", f, file_name=doc['path'].split('/')[-1],key=a ) a = a + 1

最终效果如下图所示:

结论

本文展示了如何利用Qdrant使用生成式人工智能和语义搜索。它通常是一个在本地文件上引用索赔到本地文档的检索辅助生成(RAG)管道。整个代码大约有300行,我们甚至通过为用户提供在3种不同的Llama 3模型之间进行选择来增加了复杂性。对于这个用例,8B和70B参数模型都表现得非常好。


关注公众号,后台发送"llama3搜索引擎构建"获取所有源码             

 


53AI,企业落地应用大模型首选服务商

产品:大模型应用平台+智能体定制开发+落地咨询服务

承诺:先做场景POC验证,看到效果再签署服务协议。零风险落地应用大模型,已交付160+中大型企业

联系我们

售前咨询
186 6662 7370
预约演示
185 8882 0121

微信扫码

与创始人交个朋友

回到顶部

 
扫码咨询