词向量及向量知识库
词向量
在机器学习和自然语言处理(NLP)中,词向量(Embeddings)是一种将非结构化数据转化为实数向量的技术。实数向量可以被计算机更好地理解和处理
嵌入背后的主要想法是,相似或相关的对象在嵌入空间中的距离应该很近
//学了线性代数,但没学好空间思维
在 RAG 方面词向量的优势主要有
- 比文字更适合检索,检索语义而非关键词
- 综合多模态信息
使用嵌入模型来构建词向量,可以使用 Embedding API 和本地的嵌入模型
向量数据库
向量数据库是一种专门用于存储和检索向量数据(embedding)的数据库系统,它主要关注的是向量数据的特性和相似性
向量数据库中的数据以向量作为基本单位,对向量进行存储、处理及检索。
向量数据库通过计算与目标向量的余弦距离、点积等获取与目标向量的相似度。
当处理大量甚至海量的向量数据时,向量数据库索引和查询算法的效率明显高于传统数据库。
主流的向量数据库:
- Chroma:轻量,简单
- Weaviate:开源
- Qdrant:高效,支持多种部署模式
使用 Embedding API
使用智谱 API
import os
from zhipuai import ZhipuAI
def zhipu_embedding(text: str):
api_key = os.environ['ZHIPUAI_API_KEY']
client = ZhipuAI(api_key=api_key)
response = client.embeddings.create(
model="embedding-2",
input=text,
)
return response
text = '要生成 embedding 的输入文本,字符串形式。'
response = zhipu_embedding(text=text)
print(f'response类型为:{type(response)}')
print(f'embedding类型为:{response.object}')
print(f'生成embedding的model为:{response.model}')
print(f'生成的embedding长度为:{len(response.data[0].embedding)}')
print(f'embedding(前10)为: {response.data[0].embedding[:10]}')
"""
response类型为:<class 'zhipuai.types.embeddings.EmbeddingsResponded'>
embedding类型为:list
生成embedding的model为:embedding-2
生成的embedding长度为:1024
embedding(前10)为: [0.017893229, 0.064432174, -0.009351327, 0.027082685, 0.0040648775, -0.05599671, -0.042226028, -0.030019397, -0.01632937, 0.067769825]
"""
数据处理
本节以一些实际示例入手,来讲解如何将本地文档的内容转化为词向量,来构建向量数据库
数据读取
pdf 使用《机器学习公式详解》
使用 LangChain 的 PyMuPDFLoader 来读取知识库的 PDF 文件。PyMuPDFLoader 是 PDF 解析器中速度最快的一种,结果会包含 PDF 及其页面的详细元数据,并且每页返回一个文档。
from langchain.document_loaders.pdf import PyMuPDFLoader
# 创建一个 PyMuPDFLoader Class 实例,输入为待加载的 pdf 文档路径
loader = PyMuPDFLoader("data_base\knowledge_db\pumpkin_book.pdf")
# 调用 PyMuPDFLoader Class 的函数 load 对 pdf 文件进行加载
pdf_pages = loader.load()
print(f"载入后的变量类型为:{type(pdf_pages)},", f"该 PDF 一共包含 {len(pdf_pages)} 页")
pdf_page = pdf_pages[1]
print(f"每一个元素的类型:{type(pdf_page)}.",
f"该文档的描述性数据:{pdf_page.metadata}",
f"查看该文档的内容:\n{pdf_page.page_content}",
sep="\n------\n")
markdown
from langchain.document_loaders.markdown import UnstructuredMarkdownLoader
loader = UnstructuredMarkdownLoader("README.md")
md_pages = loader.load()
print(f"载入后的变量类型为:{type(md_pages)},", f"该 Markdown 一共包含 {len(md_pages)} 页")
md_page = md_pages[0]
print(f"每一个元素的类型:{type(md_page)}.",
f"该文档的描述性数据:{md_page.metadata}",
f"查看该文档的内容:\n{md_page.page_content[0:][:200]}",
sep="\n------\n")
数据清洗
要删除低质量的、甚至影响理解的文本数据,使得知识库的数据尽量是有序的、优质的、精简的
from langchain.document_loaders.pdf import PyMuPDFLoader
# 创建一个 PyMuPDFLoader Class 实例,输入为待加载的 pdf 文档路径
loader = PyMuPDFLoader("data_base\knowledge_db\pumpkin_book.pdf")
# 调用 PyMuPDFLoader Class 的函数 load 对 pdf 文件进行加载
pdf_pages = loader.load()
pdf_page = pdf_pages[1]
# 使用正则表达式,匹配并删除掉在两个符号中间插入的\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)
# 删除'•'和空格
pdf_page.page_content = pdf_page.page_content.replace('•', '')
pdf_page.page_content = pdf_page.page_content.replace(' ', '')
print(pdf_page.page_content)
md_page.page_content = md_page.page_content.replace('\n\n', '\n')
print(md_page.page_content)
文档分割
单个文档长度往往会超过模型支持的上下文,需要对文档进行分割
将单个文档按长度或者按固定的规则分割成若干个 chunk,然后将每个 chunk 转化为词向量,存储到向量数据库中
//想起 Monkey 大模型介绍中,谈到 patch 造成图像含义割裂。这里怎么解决?
检索以 chunk 作为元单位
Langchain 中,文本分割器都根据 chunk_size
(块大小) 和 chunk_overlap
(块与块之间的重叠大小) 进行分割
Langchain 提供多种文档分割方式
- RecursiveCharacterTextSplitter(): 按字符串分割文本,递归地尝试按不同的分隔符进行分割文本。
- CharacterTextSplitter(): 按字符来分割文本。
- MarkdownHeaderTextSplitter(): 基于指定的标题来分割 markdown 文件。
- TokenTextSplitter(): 按 token 来分割文本。
- SentenceTransformersTokenTextSplitter(): 按 token 来分割文本
- Language(): 用于 CPP、Python、Ruby、Markdown 等。
- NLTKTextSplitter(): 使用 NLTK(自然语言工具包)按句子分割文本。
- SpacyTextSplitter(): 使用 Spacy 按句子的切割文本。
from langchain.document_loaders.pdf import PyMuPDFLoader
# 创建一个 PyMuPDFLoader Class 实例,输入为待加载的 pdf 文档路径
loader = PyMuPDFLoader("data_base\knowledge_db\pumpkin_book.pdf")
# 调用 PyMuPDFLoader Class 的函数 load 对 pdf 文件进行加载
pdf_pages = loader.load()
pdf_page = pdf_pages[1]
# 使用正则表达式,匹配并删除掉在两个符号中间插入的\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)
pdf_page.page_content = pdf_page.page_content.replace('•', '')
pdf_page.page_content = pdf_page.page_content.replace(' ', '')
# print(pdf_page.page_content)
'''
* RecursiveCharacterTextSplitter 递归字符文本分割
RecursiveCharacterTextSplitter 将按不同的字符递归地分割(按照这个优先级["\n\n", "\n", " ", ""]),
这样就能尽量把所有和语义相关的内容尽可能长时间地保留在同一位置
RecursiveCharacterTextSplitter需要关注的是4个参数:
* separators - 分隔符字符串数组
* chunk_size - 每个文档的字符数量限制
* chunk_overlap - 两份文档重叠区域的长度
* length_function - 长度计算函数
'''
#导入文本分割器
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 知识库中单段文本长度
CHUNK_SIZE = 500
# 知识库中相邻文本重合长度
OVERLAP_SIZE = 50
# 使用递归字符文本分割器
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=CHUNK_SIZE,
chunk_overlap=OVERLAP_SIZE
)
# print(text_splitter.split_text(pdf_page.page_content[0:1000]))
print(text_splitter.split_text(pdf_page.page_content[0:200]))
split_docs = text_splitter.split_documents(pdf_pages)
print(f"切分后的文件数量:{len(split_docs)}")
print(f"切分后的字符数(可以用来大致评估 token 数):{sum([len(doc.page_content) for doc in split_docs])}")
//如何对文档进行分割,其实是数据处理中最核心的一步,其往往决定了检索系统的下限。但是,如何选择分割方式,往往具有很强的业务相关性
搭建并使用向量数据库
前序配置
省略数据清洗等环节
构建 Chroma 向量库
LangChain 可以直接使用 OpenAI 和百度千帆的 Embedding,另外,也可基于 LangChain 提供的接口,封装一个 zhupuai_embedding,来将智谱的 Embedding API 接入到 LangChain 中。在 附LangChain自定义Embedding封装讲解 中,介绍了如何将其他 Embedding API 封装到 LangChain 中
教程原本注记:如果你使用智谱 API,你可以参考讲解内容实现封装代码,也可以直接使用封装好的代码 zhipuai_embedding.py,将该代码同样下载到本 Notebook 的同级目录,就可以直接导入我们封装的函数。在下面的代码 Cell 中,我们默认使用了智谱的 Embedding,将其他两种 Embedding 使用代码以注释的方法呈现,如果你使用的是百度 API 或者 OpenAI API,可以根据情况来使用下方 Cell 中的代码。
import os
from dotenv import load_dotenv, find_dotenv
# 读取本地/项目的环境变量。
# find_dotenv()寻找并定位.env文件的路径
# load_dotenv()读取该.env文件,并将其中的环境变量加载到当前的运行环境中
# 如果你设置的是全局的环境变量,这行代码则没有任何作用。
_ = load_dotenv(find_dotenv())
# 如果你需要通过代理端口访问,你需要如下配置
# os.environ['HTTPS_PROXY'] = 'http://127.0.0.1:7890'
# os.environ["HTTP_PROXY"] = 'http://127.0.0.1:7890'
# 获取folder_path下所有文件路径,储存在file_paths里
file_paths = []
folder_path = 'data_base/knowledge_db'
# ../../data_base/knowledge_db
for root, dirs, files in os.walk(folder_path):
for file in files:
# print("yeah!")
file_path = os.path.join(root, file)
file_paths.append(file_path)
# print(file_paths)
# string = input()
from langchain.document_loaders.pdf import PyMuPDFLoader
from langchain.document_loaders.markdown import UnstructuredMarkdownLoader
# 遍历文件路径并把实例化的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())
text = texts[1]
# print(f"每一个元素的类型:{type(text)}.",
# f"该文档的描述性数据:{text.metadata}",
# f"查看该文档的内容:\n{text.page_content[0:]}",
# sep="\n------\n")
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 切分文档
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, chunk_overlap=50)
split_docs = text_splitter.split_documents(texts)
# print(f"切分后的文件数量:{len(split_docs)}")
# print(f"切分后的字符数(可以用来大致评估 token 数):{sum([len(doc.page_content) for doc in split_docs])}")
# 使用 OpenAI Embedding
# from langchain.embeddings.openai import OpenAIEmbeddings
# 使用百度千帆 Embedding
# from langchain.embeddings.baidu_qianfan_endpoint import QianfanEmbeddingsEndpoint
# 使用我们自己封装的智谱 Embedding,需要将封装代码下载到本地使用
from zhipuai_embedding import ZhipuAIEmbeddings
# 定义 Embeddings
# embedding = OpenAIEmbeddings()
embedding = ZhipuAIEmbeddings()
# embedding = QianfanEmbeddingsEndpoint()
# 定义持久化路径
persist_directory = 'data_base/vector_db/chroma'
# 删除旧的数据库文件(如果文件夹中有文件的话)
# !rm -rf 'data_base/vector_db/chroma'
import shutil
folder_path = 'data_base/vector_db/chroma'
shutil.rmtree(folder_path)
from langchain.vectorstores.chroma import Chroma
vectordb = Chroma.from_documents(
documents=split_docs[:20], # 为了速度,只选择前 20 个切分的 doc 进行生成;使用千帆时因QPS限制,建议选择前 5 个doc
embedding=embedding,
persist_directory=persist_directory # 允许我们将persist_directory目录保存到磁盘上
)
vectordb.persist()
print(f"向量库中存储的数量:{vectordb._collection.count()}")
from __future__ import annotations
import logging
from typing import Dict, List, Any
from langchain.embeddings.base import Embeddings
from langchain.pydantic_v1 import BaseModel, root_validator
logger = logging.getLogger(__name__)
class ZhipuAIEmbeddings(BaseModel, Embeddings):
"""`Zhipuai Embeddings` embedding models."""
client: Any
"""`zhipuai.ZhipuAI"""
@root_validator()
def validate_environment(cls, values: Dict) -> Dict:
"""
实例化ZhipuAI为values["client"]
Args:
values (Dict): 包含配置信息的字典,必须包含 client 的字段.
Returns:
values (Dict): 包含配置信息的字典。如果环境中有zhipuai库,则将返回实例化的ZhipuAI类;否则将报错 'ModuleNotFoundError: No module named 'zhipuai''.
"""
from zhipuai import ZhipuAI
values["client"] = ZhipuAI()
return values
def embed_query(self, text: str) -> List[float]:
"""
生成输入文本的 embedding.
Args:
texts (str): 要生成 embedding 的文本.
Return:
embeddings (List[float]): 输入文本的 embedding,一个浮点数值列表.
"""
embeddings = self.client.embeddings.create(
model="embedding-2",
input=text
)
return embeddings.data[0].embedding
def embed_documents(self, texts: List[str]) -> List[List[float]]:
"""
生成输入文本列表的 embedding.
Args:
texts (List[str]): 要生成 embedding 的文本列表.
Returns:
List[List[float]]: 输入列表中每个文档的 embedding 列表。每个 embedding 都表示为一个浮点值列表。
"""
return [self.embed_query(text) for text in texts]
async def aembed_documents(self, texts: List[str]) -> List[List[float]]:
"""Asynchronous Embed search docs."""
raise NotImplementedError("Please use `embed_documents`. Official does not support asynchronous requests")
async def aembed_query(self, text: str) -> List[float]:
"""Asynchronous Embed query text."""
raise NotImplementedError("Please use `aembed_query`. Official does not support asynchronous requests")
向量检索
Chroma 的相似度搜索使用的是余弦距离
s i m i l a r i t y = c o s ( A , B ) = A ⋅ B ∥ A ∥ ∥ B ∥ = ∑ 1 n a i b i ∑ 1 n a i 2 ∑ 1 n b i 2 similarity = cos(A, B) = \frac{A \cdot B}{\parallel A \parallel \parallel B \parallel} = \frac{\sum_1^n a_i b_i}{\sqrt{\sum_1^n a_i^2}\sqrt{\sum_1^n b_i^2}} similarity=cos(A,B)=∥A∥∥B∥A⋅B=∑1nai2∑1nbi2∑1naibi 其中 a i a_i ai、 b i b_i bi 分别是向量 A A A、 B B B 的分量
需要数据库返回严谨的按余弦相似度排序的结果时,可以使用 similarity_search
函数
# 接上文
question="什么是大语言模型"
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")
一开始不是很能理解输出,后来发现是没看仔细,检索了pdf,一个是内容不对,一个是效果不如md
但是zhipuai检索结果的匹配度不如教程中使用的openai吧
检索到的第0个内容:
如上所见,模型实际上并不知道我的名字。
因此,每次与语言模型的交互都互相独立,这意味着我们必须提供所有相关的消息,以便模型在当前对话中进行引用。如果想让模型
引用或 “记住” 对话的早期部分,则必须在模型的输入中提供早期的交流。我们将其称为上下文 (context) 。尝试以下示例。
```python
中文
messages = [
{'role':'system', 'content
--------------
检索到的第1个内容:
第八章 聊天机器人
大型语言模型带给我们的激动人心的一种可能性是,我们可以通过它构建定制的聊天机器人(Chatbot),而且只需很少的工作量。在
这一章节的探索中,我们将带你了解如何利用会话形式,与具有个性化特性(或专门为特定任务或行为设计)的聊天机器人进行深度
对话。
像 ChatGPT 这样的聊天模型实际上是组装成以一系列消息作为输入,并返回一个模型生成的消息作为输出的。这种聊天格式原本的设
计
--------------
检索到的第2个内容:
现在我们已经给模型提供了上下文,也就是之前的对话中提到的我的名字,然后我们会问同样的问题,也就是我的名字是什么。因为
模型有了需要的全部上下文,所以它能够做出回应,就像我们在输入的消息列表中看到的一样。
三、订餐机器人
在这一新的章节中,我们将探索如何构建一个 “点餐助手机器人”。这个机器人将被设计为自动收集用户信息,并接收来自比萨饼店的
订单。让我们开始这个有趣的项目,深入理解它如何帮助简化日常
--------------
(p2s) PS D:\desktop_temp\python-coding>
MMR 检索
最大边际相关性 (MMR, Maximum marginal relevance
) 可以帮助我们在保持相关性的同时,增加内容的丰富度
选择了一个相关性高的文档,再选择一个与已选文档相关性较低但是信息丰富的文档,避免过于单一的结果
# 接上文
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")
课后思考
可以尝试对我的 obsidian 库构建向量数据库
为之后的知识库构建做好准备
现在的网页端知识库助手,无法吞吐 GB 级的 obsidian 库吧