词向量及向量知识库介绍
词向量
词向量定义
在机器学习和自然语言处理(NLP)中,词向量(Embeddings)是一种将非结构化数据,如单词、句子或者整个文档,转化为实数向量的技术。这些实数向量可以被计算机更好地理解和处理。
嵌入背后的主要想法是,相似或相关的对象在嵌入空间中的距离应该很近。
词向量优势
在RAG(Retrieval Augmented Generation,检索增强生成)方面词向量的优势主要有两点:
-
词向量比文字更适合检索。当我们在数据库检索时,如果数据库存储的是文字,主要通过检索关键词(词法搜索)等方法找到相对匹配的数据,匹配的程度是取决于关键词的数量或者是否完全匹配查询句的;但是词向量中包含了原文本的语义信息,可以通过计算问题与数据库中数据的点积、余弦距离、欧几里得距离等指标,直接获取问题与数据在语义层面上的相似度;
-
词向量比其它媒介的综合信息能力更强,当传统数据库存储文字、声音、图像、视频等多种媒介时,很难去将上述多种媒介构建起关联与跨模态的查询方法;但是词向量却可以通过多种向量模型将多种数据映射成统一的向量形式。
这里补充一下RAG的介绍
RAG(Retrieval Augmented Generation,检索增强生成)是一种结合了检索(Retrieval)和生成(Generation)的人工智能模型架构,旨在提高文本生成任务的性能和相关性。这种方法通过先检索相关信息,然后基于这些信息生成文本,从而允许模型在生成回答、文章或其他类型的文本内容时,利用大量的外部知识。RAG模型通常用于需要广泛背景知识的任务,如问答系统、内容推荐、文章撰写等。
一般构建词向量的方法
在搭建 RAG 系统时,我们往往可以通过使用嵌入模型来构建词向量,我们可以选择:
-
使用各个公司的 Embedding API;
-
在本地使用嵌入模型将数据构建为词向量。
向量数据库
向量数据库定义
向量数据库是用于高效计算和管理大量向量数据的解决方案。向量数据库是一种专门用于存储和检索向量数据(embedding)的数据库系统。它与传统的基于关系模型的数据库不同,它主要关注的是向量数据的特性和相似性。
在向量数据库中,数据被表示为向量形式,每个向量代表一个数据项。这些向量可以是数字、文本、图像或其他类型的数据。向量数据库使用高效的索引和查询算法来加速向量数据的存储和检索过程。
使用Embedding API(OpenAI)
我这里使用的是OpenAI的API,
GPT embedding mode使用的是text-embedding-3-small
def openai_embedding(text: str, model: str=None):
# 获取环境变量 OPENAI_API_KEY
api_key=os.environ['OPENAI_API_KEY']
client = OpenAI(api_key=api_key)
# embedding model:'text-embedding-3-small', 'text-embedding-3-large', 'text-embedding-ada-002'
if model == None:
model="text-embedding-3-small"
response = client.embeddings.create(
input=text,
model=model
)
return response
response = openai_embedding(text='要生成 embedding 的输入文本,字符串形式。')
API返回格式为json,除object
向量类型外还有存放数据的data
、embedding model 型号model
以及本次 token 使用情况usage
等数据,具体如下所示:
{
"object": "list",
"data": [
{
"object": "embedding",
"index": 0,
"embedding": [
-0.006929283495992422,
... (省略)
-4.547132266452536e-05,
],
}
],
"model": "text-embedding-3-small",
"usage": {
"prompt_tokens": 5,
"total_tokens": 5
}
}
我们可以调用response的object来获取embedding的类型。
print(f'返回的embedding类型为:{response.object}')Copy to clipboardErrorCopied
返回的embedding类型为:listCopy to clipboardErrorCopied
embedding存放在data中,我们可以查看embedding的长度及生成的embedding。
print(f'embedding长度为:{len(response.data[0].embedding)}')
print(f'embedding(前10)为:{response.data[0].embedding[:10]}')Copy to clipboardErrorCopied
embedding长度为:1536
embedding(前10)为:[0.03884002938866615, 0.013516489416360855, -0.0024250170681625605, -0.01655769906938076, 0.024130908772349358, -0.017382603138685226, 0.04206013306975365, 0.011498954147100449, -0.028245486319065094, -0.00674333656206727]Copy to clipboardErrorCopied
我们也可以查看此次embedding的模型及token使用情况。
print(f'本次embedding model为:{response.model}')
print(f'本次token使用情况为:{response.usage}')Copy to clipboardErrorCopied
本次embedding model为:text-embedding-3-small
本次token使用情况为:Usage(prompt_tokens=12, total_tokens=12)
数据处理
数据读取
pdf文档
我们可以使用 LangChain 的 PyMuPDFLoader 来读取知识库的 PDF 文件。PyMuPDFLoader 是 PDF 解析器中速度最快的一种,结果会包含 PDF 及其页面的详细元数据,并且每页返回一个文档。
from langchain.document_loaders.pdf import PyMuPDFLoader
# 创建一个 PyMuPDFLoader Class 实例,输入为待加载的 pdf 文档路径
loader = PyMuPDFLoader("../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf")
# 调用 PyMuPDFLoader Class 的函数 load 对 pdf 文件进行加载
pdf_pages = loader.load()
文档加载后储存在 pages
变量中:
-
page
的变量类型为List
-
打印
pages
的长度可以看到 pdf 一共包含多少页
print(f"载入后的变量类型为:{type(pdf_pages)},", f"该 PDF 一共包含 {len(pdf_pages)} 页")Copy to clipboardErrorCopied
载入后的变量类型为:<class 'list'>, 该 PDF 一共包含 196 页Copy to clipboardErrorCopied
page
中的每一元素为一个文档,变量类型为 langchain_core.documents.base.Document
, 文档变量类型包含两个属性
-
page_content
包含该文档的内容。 -
meta_data
为文档相关的描述性数据。
pdf_page = pdf_pages[1]
print(f"每一个元素的类型:{type(pdf_page)}.",
f"该文档的描述性数据:{pdf_page.metadata}",
f"查看该文档的内容:\n{pdf_page.page_content}",
sep="\n------\n")
每一个元素的类型:<class 'langchain_core.documents.base.Document'>.
------
该文档的描述性数据:{'source': './data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'file_path': './data_base/knowledge_db/pumkin_book/pumpkin_book.pdf', 'page': 1, 'total_pages': 196, 'format': 'PDF 1.5', 'title': '', 'author': '', 'subject': '', 'keywords': '', 'creator': 'LaTeX with hyperref', 'producer': 'xdvipdfmx (20200315)', 'creationDate': "D:20230303170709-00'00'", 'modDate': '', 'trapped': ''}
------
查看该文档的内容:
前言
……省略
md文档
from langchain.document_loaders.markdown import UnstructuredMarkdownLoader
loader = UnstructuredMarkdownLoader("../../data_base/knowledge_db/prompt_engineering/1. 简介 Introduction.md")
md_pages = loader.load()
读取出来的对象和pf文档读出来的是一致的,这里不再赘述
数据清理
我们期望知识库的数据尽量是有序的、优质的、精简的,因此我们要删除低质量的、甚至影响理解的文本数据。
不同的文本数据情况不同,所要采取的清理方法也不同,例如上文中读取的pdf文件不仅将一句话按照原文的分行添加了换行符\n
,也在原本两个符号中间插入了\n
,我们可以使用正则表达式匹配并删除掉\n
。
import re
pattern = re.compile(r'[^\u4e00-\u9fff](\n)[^\u4e00-\u9fff]', re.DOTALL)
pdf_page.page_content = re.sub(pattern, lambda match: match.group(0).replace('\n', ''), pdf_page.page_content)
print(pdf_page.page_content)
文档分割
由于单个文档的长度往往会超过模型支持的上下文,导致检索得到的知识太长超出模型的处理能力,因此,在构建向量知识库的过程中,我们往往需要对文档进行分割,将单个文档按长度或者按固定的规则分割成若干个 chunk,然后将每个 chunk 转化为词向量,存储到向量数据库中。
在检索时,我们会以 chunk 作为检索的元单位,也就是每一次检索到 k 个 chunk 作为模型可以参考来回答用户问题的知识,这个 k 是我们可以自由设定的。
Langchain 中文本分割器都根据 chunk_size
(块大小)和 chunk_overlap
(块与块之间的重叠大小)进行分割。
-
chunk_size 指每个块包含的字符或 Token (如单词、句子等)的数量
-
chunk_overlap 指两个块之间共享的字符数量,用于保持上下文的连贯性,避免分割丢失上下文信息
Langchain
提供多种文档分割方式,区别在怎么确定块与块之间的边界、块由哪些字符/token组成、以及如何测量块大小
-
RecursiveCharacterTextSplitter()
: 按字符串分割文本,递归地尝试按不同的分隔符进行分割文本。 -
CharacterTextSplitter()
: 按字符来分割文本。 -
MarkdownHeaderTextSplitter()
: 基于指定的标题来分割markdown 文件。 -
TokenTextSplitter()
: 按token来分割文本。 -
SentenceTransformersTokenTextSplitter()
: 按token来分割文本 -
Language(): 用于 CPP、Python、Ruby、Markdown 等。
-
NLTKTextSplitter()
: 使用 NLTK(自然语言工具包)按句子分割文本。 -
SpacyTextSplitter()
: 使用 Spacy按句子的切割文本。
注:如何对文档进行分割,其实是数据处理中最核心的一步,其往往决定了检索系统的下限。但是,如何选择分割方式,往往具有很强的业务相关性——针对不同的业务、不同的源数据,往往需要设定个性化的文档分割方式。
搭建并使用向量数据库
前面提到主流的向量数据库中有Chroma,其是一个轻量级向量数据库,拥有丰富的功能和简洁的API,具有简单、易用、轻量的优点,但功能相对简单且不支持GPU加速,适合初学者使用。
我这里就调用一下Chroma向量数据库
我使用的是AzureOpenAIEmbeddings
调用一些必要的库
import os
from langchain.document_loaders.pdf import PyMuPDFLoader
from langchain.document_loaders.markdown import UnstructuredMarkdownLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import AzureOpenAIEmbeddings
from langchain.vectorstores.chroma import Chroma
定义函数创建向量数据库
#定义持久化路径
persist_directory = '../../data_base/vector_db/chroma'
#设置embedding,可以按自己使用的API去进行调用
embeddings = AzureOpenAIEmbeddings(
azure_deployment="text-embedding-3-large",
model="text-embedding-3-large",
azure_endpoint="https://xxx.com/",
openai_api_key="xxxx",
api_version="2024-02-01"
)
def build_db():
# 获取folder_path下所有文件路径,储存在file_paths里
file_paths = []
folder_path = '../../data_base/knowledge_db'
for root, dirs, files in os.walk(folder_path):
for file in files:
file_path = os.path.join(root, file)
file_paths.append(file_path)
#遍历文件路径并把实例化的loader存放在loaders里
loaders = []
for file_path in file_paths:
file_type = file_path.split('.')[-1]
#根据不同文件进行不同的读取
if file_type == 'pdf':
loaders.append(PyMuPDFLoader(file_path))
elif file_type == 'md':
loaders.append(UnstructuredMarkdownLoader(file_path))
# 下载文件并存储到text
texts = []
for loader in loaders: texts.extend(loader.load())
# 切分文档
# chunk_size和chunk_over可以自定义
#创建了一个RecursiveCharacterTextSplitter的实例
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, chunk_overlap=50)
#用该实例调用split_documents方法,将长文本text按照text_splitter设置的规则进行分割
split_docs = text_splitter.split_documents(texts)
#使用Chroma库从文档中生成向量数据库
vectordb = Chroma.from_documents(
documents=split_docs[:20], # 为了速度,只选择前 20 个切分的 doc 进行生成;
embedding=embeddings,
persist_directory=persist_directory # 允许我们将persist_directory目录保存到磁盘上
)
#运行 vectordb.persist 来持久化向量数据库
vectordb.persist()
print(f"向量库中存储的数量:{vectordb._collection.count()}") #20
相似度检索函数
#相似度检索函数
#Chroma的相似度搜索使用的是余弦距离
#当你需要数据库返回严谨的按余弦相似度排序的结果时可以使用similarity_search函数。
def similar_search(question):
vectordb = Chroma(persist_directory=persist_directory,embedding_function=embeddings)
sim_docs = vectordb.similarity_search(question,k=3) #相似度最高的前三
print(f"检索到的内容数:{len(sim_docs)}")
for i, sim_doc in enumerate(sim_docs):
print(f"检索到的第{i}个内容: \n{sim_doc.page_content[:200]}", end="\n--------------\n")
MMR检索
#MMR检索
#如果只考虑检索出内容的相关性会导致内容过于单一,可能丢失重要信息。
#最大边际相关性 (MMR, Maximum marginal relevance) 可以帮助我们在保持相关性的同时,增加内容的丰富度。
#核心思想是在已经选择了一个相关性高的文档之后,再选择一个与已选文档相关性较低但是信息丰富的文档。这样可以在保持相关性的同时,增加内容的多样性,避免过于单一的结果。
def mmr_search(question):
vectordb = Chroma(persist_directory=persist_directory,embedding_function=embeddings)
mmr_docs = vectordb.max_marginal_relevance_search(question,k=3)
for i, sim_doc in enumerate(mmr_docs):
print(f"MMR 检索到的第{i}个内容: \n{sim_doc.page_content[:200]}", end="\n--------------\n")
pass
测试
#build_db() #build一次就够了 若要再次创建可以运用下面这条指令将原有的数据库文件删除
#rm -rf '../../data_base/vector_db/chroma'
similar_search("什么是大语言模型")
mmr_search("什么是大语言模型")