本笔记为【动手学大模型应用开发】https://datawhalechina.github.io/llm-universe/,DataWhale 六月组队学习的 llm-universe - 202406 的 任务三打卡笔记。
笔记首次发表在 我的个人网站 www.jiewong.com
词向量
大模型对于输入语言的处理 ,首先要经过分词,其次要经过向量化,输入的句子才会到大模型。
Tokenizer:文本处理的第一步
Tokenizer,即分词器,是将输入文本分解为更小单元(token)的工具。这些token可以是单词、子词(subword)、字符或其他单位。在大语言模型中,Tokenizer是文本预处理阶段的核心,因为模型不能直接处理原始文本字符串。
不同类型的Tokenizer包括:
- 词级别的Tokenizer:将文本分解为单词,如“Hello, world!”被分解为[“Hello”, “,”, “world”, “!”]。
- 子词级别的Tokenizer:如Byte Pair Encoding(BPE)和WordPiece,将文本分解为更小的子词单元。
- 字符级别的Tokenizer:将文本分解为单个字符,如“Hello”被分解为[“H”, “e”, “l”, “l”, “o”]。
词汇量(Vocabulary Size):模型输入的关键参数
在Tokenizer中,vocabulary size(词汇量)是指tokenizer使用的词汇表(vocabulary)中所包含的唯一token的总数。每个token都是一个独特的符号或字符串片段,代表输入文本的基本单元。词汇表中的token可以是单词、子词、字符或其他形式的表示。
- 词汇量与模型性能:较大的词汇量可以捕捉更多细节信息,但也会增加模型的复杂度和训练时间。适当的词汇量有助于模型在训练数据和测试数据上都能表现良好。
- 词汇量的选择:根据具体任务和数据集选择合适的词汇量,以平衡模型的表现和计算资源的需求。
Embedding Layer:从离散到连续
在文本经过Tokenizer处理后,得到的token序列会通过Embedding Layer转换为高维向量。Embedding Layer是一个查找表,将每个token ID映射到一个向量表示。这些向量捕捉了token的语义信息,使模型能够在高维空间中处理这些token。
在机器学习和自然语言处理(NLP)中,词向量(Embeddings)是一种将非结构化数据,如单词、句子或者整个文档,转化为实数向量的技术。这些实数向量可以被计算机更好地理解和处理。
嵌入背后的主要想法是,相似或相关的对象在嵌入空间中的距离应该很近。
举个例子,我们可以使用词嵌入(word embeddings)来表示文本数据。在词嵌入中,每个单词被转换为一个向量,这个向量捕获了这个单词的语义信息。例如,“king” 和 “queen” 这两个单词在嵌入空间中的位置将会非常接近,因为它们的含义相似。而 “apple” 和 “orange” 也会很接近,因为它们都是水果。而 “king” 和 “apple” 这两个单词在嵌入空间中的距离就会比较远,因为它们的含义不同。
RAG为什么要用词向量?
在RAG(Retrieval Augmented Generation,检索增强生成)方面词向量的优势主要有两点:
- 词向量比文字更适合检索。当我们在数据库检索时,如果数据库存储的是文字,主要通过检索关键词(词法搜索)等方法找到相对匹配的数据,匹配的程度是取决于关键词的数量或者是否完全匹配查询句的;但是词向量中包含了原文本的语义信息,可以通过计算问题与数据库中数据的点积、余弦距离、欧几里得距离等指标,直接获取问题与数据在语义层面上的相似度;
- 词向量比其它媒介的综合信息能力更强,当传统数据库存储文字、声音、图像、视频等多种媒介时,很难去将上述多种媒介构建起关联与跨模态的查询方法;但是词向量却可以通过多种向量模型将多种数据映射成统一的向量形式。
构建词向量的方法
在搭建 RAG 系统时,我们往往可以通过使用嵌入模型来构建词向量,我们可以选择:
- 使用各个公司的 Embedding API;
- 在本地使用嵌入模型将数据构建为词向量。
向量数据库
什么是向量数据库
向量数据库是用于高效计算和管理大量向量数据的解决方案。向量数据库是一种专门用于存储和检索向量数据(embedding)的数据库系统。它与传统的基于关系模型的数据库不同,它主要关注的是向量数据的特性和相似性。
在向量数据库中,数据被表示为向量形式,每个向量代表一个数据项。这些向量可以是数字、文本、图像或其他类型的数据。向量数据库使用高效的索引和查询算法来加速向量数据的存储和检索过程。
向量数据库的原理及核心优势
向量数据库中的数据以向量作为基本单位,对向量进行存储、处理及检索。向量数据库通过计算与目标向量的余弦距离、点积等获取与目标向量的相似度。当处理大量甚至海量的向量数据时,向量数据库索引和查询算法的效率明显高于传统数据库。
主流的向量数据库
- Chroma:是一个轻量级向量数据库,拥有丰富的功能和简单的 API,具有简单、易用、轻量的优点,但功能相对简单且不支持GPU加速,适合初学者使用。
- Weaviate:是一个开源向量数据库。除了支持相似度搜索和最大边际相关性(MMR,Maximal Marginal Relevance)搜索外还可以支持结合多种搜索算法(基于词法搜索、向量搜索)的混合搜索,从而搜索提高结果的相关性和准确性。
- Qdrant:Qdrant使用 Rust 语言开发,有极高的检索效率和RPS(Requests Per Second),支持本地运行、部署在本地服务器及Qdrant云三种部署模式。且可以通过为页面内容和元数据制定不同的键来复用数据。
使用腾讯混元Embedding API
import json
import types
from tencentcloud.common import credential
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
from tencentcloud.hunyuan.v20230901 import hunyuan_client, models
from dotenv import load_dotenv
import os
try:
# 实例化一个认证对象,入参需要传入腾讯云账户 SecretId 和 SecretKey,此处还需注意密钥对的保密
# 代码泄露可能会导致 SecretId 和 SecretKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考,建议采用更安全的方式来使用密钥,请参见:https://cloud.tencent.com/document/product/1278/85305
# 密钥可前往官网控制台 https://console.cloud.tencent.com/cam/capi 进行获取
load_dotenv() # 加载环境变量
SecretId = os.getenv("SecretId")
SecretKey = os.getenv("SecretKey")
cred = credential.Credential(SecretId, SecretKey)
# 实例化一个http选项,可选的,没有特殊需求可以跳过
httpProfile = HttpProfile()
httpProfile.endpoint = "hunyuan.tencentcloudapi.com"
# 实例化一个client选项,可选的,没有特殊需求可以跳过
clientProfile = ClientProfile()
clientProfile.httpProfile = httpProfile
# 实例化要请求产品的client对象,clientProfile是可选的
client = hunyuan_client.HunyuanClient(cred, "", clientProfile)
# 实例化一个请求对象,每个接口都会对应一个request对象
req = models.GetEmbeddingRequest()
params = {
"Input": "你好"
}
req.from_json_string(json.dumps(params))
# 返回的resp是一个GetEmbeddingResponse的实例,与请求对象对应
resp = client.GetEmbedding(req)
# 输出json格式的字符串回包
print(resp.to_json_string())
except TencentCloudSDKException as err:
print(err)
返回了一大堆内容
{"Data": [{"Embedding": [-0.0013065338134765625, -0.01026153564453125, 0.0185089111328125, -0.012054443359375, -0.03179931640625, -0.016082763671875, 0.0194854736328125, 0.019195556640625, -0.029754638671875, -0.00955963134765625, -0.000446319580078125, 0.03619384765625, -0.11041259765625, 0.029266357421875, -0.011444091796875, -0.028778076171875, -0.01357269287109375, 0.0029888153076171875, -0.028350830078125, 0.04351806640625, 省略
, 0.0304107666015625], "Index": 0, "Object": "embedding"}], "Usage": {"PromptTokens": 3, "TotalTokens": 3}, "RequestId": "063627d8-0b27-4285-a803-97143ad92a72"}
查看 Data 的 Object
data = json.loads(resp.to_json_string())
print(data["Data"][0]["Object"])
返回
embedding
查看embedding长度、前10、token使用情况
print(f'embedding长度为:{len(data["Data"][0]["Embedding"])}')
print(f'embedding(前10)为:{data["Data"][0]["Embedding"][:10]}')
print(f'本次token使用情况:{data["Usage"]}')
embedding长度为:1024
embedding(前10)为:[-0.0013065338134765625, -0.01026153564453125, 0.0185089111328125, -0.012054443359375, -0.03179931640625, -0.016082763671875, 0.0194854736328125, 0.019195556640625, -0.029754638671875, -0.00955963134765625]
本次token使用情况:{'PromptTokens': 3, 'TotalTokens': 3}
数据处理
pip install langchain
pip install -U langchain-community
pip install pymupdf # pdf
pip install unstructured # markdown
选用 Datawhale 一些经典开源课程作为示例
- 《机器学习公式详解》PDF版本
- 《面向开发者的LLM入门教程、第一部分Prompt Engineering》md版本
数据放置在./data_base/knowledge_db 目录下
pdf读取
#旧版 from langchain.document_loaders.pdf import PyMuPDFLoader
from langchain_community.document_loaders import PyMuPDFLoader
# 创建一个 PyMuPDFLoader Class 实例,输入为待加载的 pdf 文档路径
loader = PyMuPDFLoader("./data_base/knowledge_db/pumkin_book/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_community.document_loaders import UnstructuredMarkdownLoader
loader = UnstructuredMarkdownLoader("./data_base/knowledge_db/prompt_engineering/1. 简介 Introduction.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")
数据清洗
# pdf删除换行符\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·删除空格
pdf_page.page_content = pdf_page.page_content.replace('•', '')
pdf_page.page_content = pdf_page.page_content.replace(' ', '')
print(pdf_page.page_content)
# markdown删除多余换行符\n
md_page.page_content = md_page.page_content.replace('\n\n', '\n')
print(md_page.page_content)
文档分割
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 知识库中单段文本长度
CHUNK_SIZE = 500
# 知识库中相邻文本重合长度
OVERLAP_SIZE = 50
# 使用递归字符文本分割器
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=CHUNK_SIZE,
chunk_overlap=OVERLAP_SIZE
)
text_splitter.split_text(pdf_page.page_content[0:1000])
split_docs = text_splitter.split_documents(pdf_pages)
print(split_docs[3])#切分的文件内容
print(f"切分后的文件数量:{len(split_docs)}")
print(f"切分后的字符数(可以用来大致评估 token 数):{sum([len(doc.page_content) for doc in split_docs])}")
搭建并使用向量数据库
封装的混元Embedding api 接口T encentCloudEmbedding.py
from __future__ import annotations
import json
import logging
import os
from typing import Dict, List, Any
from dotenv import load_dotenv
from tencentcloud.common import credential
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
from tencentcloud.hunyuan.v20230901 import hunyuan_client, models
from langchain.embeddings.base import Embeddings
from langchain.pydantic_v1 import BaseModel
logger = logging.getLogger(__name__)
class TencentCloudEmbeddings(BaseModel, Embeddings):
"""腾讯云 Hunyuan Embeddings 封装"""
client: Any
"""腾讯云 Hunyuan 客户端"""
def __init__(self, **data: Any):
super().__init__(**data)
self.client = self.initialize_client()
def initialize_client(self):
"""初始化腾讯云 Hunyuan 客户端"""
try:
load_dotenv() # 加载环境变量
SecretId = os.getenv("SecretId")
SecretKey = os.getenv("SecretKey")
cred = credential.Credential(SecretId, SecretKey)
httpProfile = HttpProfile()
httpProfile.endpoint = "hunyuan.tencentcloudapi.com"
clientProfile = ClientProfile()
clientProfile.httpProfile = httpProfile
client = hunyuan_client.HunyuanClient(cred, "", clientProfile)
return client
except TencentCloudSDKException as err:
logger.error(f"初始化腾讯云客户端失败: {err}")
raise
def embed_query(self, text: str) -> List[float]:
"""生成输入文本的 embedding"""
try:
req = models.GetEmbeddingRequest()
params = {"Input": text}
req.from_json_string(json.dumps(params))
resp = self.client.GetEmbedding(req)
data = json.loads(resp.to_json_string())
return data["Data"][0]["Embedding"]
except TencentCloudSDKException as err:
logger.error(f"获取 embedding 失败: {err}")
return []
def embed_documents(self, texts: List[str]) -> List[List[float]]:
"""生成输入文本列表的 embedding"""
return [self.embed_query(text) for text in texts]
main.py
import os
from dotenv import load_dotenv, find_dotenv
from langchain_community.document_loaders import PyMuPDFLoader, UnstructuredMarkdownLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from TencentCloudEmbedding import TencentCloudEmbeddings
from langchain.vectorstores.chroma import Chroma
# 获取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)
print(file_paths[:3])
# 遍历文件路径并把实例化的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")
# 切分文档
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
split_docs = text_splitter.split_documents(texts)
embedding = TencentCloudEmbeddings()
# 定义持久化路径
persist_directory = './data_base/vector_db/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()}")
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")
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")