向量检索

什么是向量

在数学中,向量(也称为欧⼏⾥得向量、⼏何向量),指具有⼤⼩(magnitude)和⽅向的量。它可以形象化地表示为带箭头的线段。箭头所指代表向量的⽅向;线段⻓度代表向量的⼤⼩

它可以表示为从一个点到另一个点的有向线段。例如,二维空间中的向量可以表示为 ,表示从原点 到点 的有向线段

以此类推,我可以用一组坐标 表示一个 维空间中的向量, 叫向量的维度

文本向量 (Text Embeddings)

  1. 将文本转成一组 维浮点数,即文本向量又叫 Embeddings
  2. 向量之间可以计算距离,距离远近对应语义相似度大小

文本向量是怎么得到的

  1. 构建相关(正例)与不相关(负例)的句子对样本
  2. 训练双塔式模型,让正例间的距离小,负例间的距离大

例如:

扩展阅读:https://www.sbert.net

向量间的相似度计算

  • 欧氏距离:两向量点之间的距离,越小越相似
  • 余弦距离:两向量点夹角的 cos 值,越大越相似
import numpy as np
from numpy import dot
from numpy.linalg import norm
 
def cos_sim(a, b):
    '''余弦距离 -- 越大越相似'''
    return dot(a, b)/(norm(a)*norm(b))
 
 
def l2(a, b):
    '''欧氏距离 -- 越小越相似'''
    x = np.asarray(a)-np.asarray(b)
    return norm(x)

嵌入模型的选择

找需求相关的语料库(如:法律、医学)来进行文本向量转换测试,进行评估

大多数场景下,开源的嵌入模型使用都很一般。要提升检索召回率,建议对模型进行微调

下文中使用到的 text-embedding-ada-002 嵌入模型是 OpenAI 闭源商业模型,对多语言支持很好,也是 LangChainLlamaIndex 的默认嵌入模型

# dimensions 维度,这里使用默认维度
# 嵌入模型的维度越大,表示特征细节提取越丰富
def get_embeddings(texts, model="text-embedding-ada-002", dimensions=None):
	'''封装 OpenAI 的 Embedding 模型接口'''
	if model == "text-embedding-ada-002":
	    dimensions = None
	if dimensions:
	    data = client.embeddings.create(
	        input=texts, model=model, dimensions=dimensions).data
	else:
	    data = client.embeddings.create(input=texts, model=model).data
	return [x.embedding for x in data]
test_query = ["测试文本"]
vec = get_embeddings(test_query)[0]
print(f"Total dimension: {len(vec)}")
print(f"First 10 elements: {vec[:10]}")
 
# Total dimension: 1536
# First 10 elements: [-0.007304091472178698, -0.006229960359632969, -0.010646641254425049, 0.0014391535660251975, -0.010704899206757545, 0.029274623841047287, -0.019807705655694008, 0.005487171467393637, -0.016865678131580353, -0.011979292146861553]

评估嵌入模型

query = "国际争端"
 
# 且能支持跨语言
# query = "global conflicts"
 
documents = [
    "联合国就苏丹达尔富尔地区大规模暴力事件发出警告",
    "土耳其、芬兰、瑞典与北约代表将继续就瑞典“入约”问题进行谈判",
    "日本岐阜市陆上自卫队射击场内发生枪击事件 3人受伤",
    "国家游泳中心(水立方):恢复游泳、嬉水乐园等水上项目运营",
    "我国首次在空间站开展舱外辐射生物学暴露实验",
]
 
query_vec = get_embeddings([query])[0]
doc_vecs = get_embeddings(documents)
 
print("Query 与自己的余弦距离: {:.2f}".format(cos_sim(query_vec, query_vec)))
print("Query 与 Documents 的余弦距离:")
for vec in doc_vecs:
    print(cos_sim(query_vec, vec))
 
print()
 
print("Query 与自己的欧氏距离: {:.2f}".format(l2(query_vec, query_vec)))
print("Query 与 Documents 的欧氏距离:")
for vec in doc_vecs:
    print(l2(query_vec, vec))
 
# Query 与自己的余弦距离: 1.00
# Query 与 Documents 的余弦距离:
# 0.8224810779975097
# 0.8299968969406545
# 0.798096878742543
# 0.7669367418371253
# 0.7933908049643592
# 
# Query 与自己的欧氏距离: 0.00
# Query 与 Documents 的欧氏距离:
# 0.5958505343143035
# 0.5831005284209486
# 0.6354574886516438
# 0.6827345862824111
# 0.6428206511188914

不用太看具体的值,要看语义相同的句子(上文中第一二句)是挨着的,就表示嵌入模型可用

OpenAI 新发布的两个 Embedding 模型

2024 年 1 月 25 日,OpenAI 新发布了两个 Embedding 模型

  • text-embedding-3-large
  • text-embedding-3-small

其最大特点是,支持自定义的缩短向量维度,从而在几乎不影响最终效果的情况下降低向量检索与相似度计算的复杂度

通俗的说:越大越准、越小越快。 官方公布的评测结果:

注:MTEB 是一个大规模多任务的 Embedding 模型公开评测集

model = "text-embedding-3-large"
dimensions = 256
 
query = "国际争端"
# 且能支持跨语言
# query = "global conflicts"
 
documents = [
    "联合国就苏丹达尔富尔地区大规模暴力事件发出警告",
    "土耳其、芬兰、瑞典与北约代表将继续就瑞典“入约”问题进行谈判",
    "日本岐阜市陆上自卫队射击场内发生枪击事件 3人受伤",
    "国家游泳中心(水立方):恢复游泳、嬉水乐园等水上项目运营",
    "我国首次在空间站开展舱外辐射生物学暴露实验",
]
 
query_vec = get_embeddings([query], model=model, dimensions=dimensions)[0]
doc_vecs = get_embeddings(documents, model=model, dimensions=dimensions)
 
print("向量维度: {}".format(len(query_vec)))
print()
print("Query 与 Documents 的余弦距离:")
for vec in doc_vecs:
    print(cos_sim(query_vec, vec))
print()
print("Query 与 Documents 的欧氏距离:")
for vec in doc_vecs:
    print(l2(query_vec, vec))
 
# 向量维度: 256
# 
# Query 与 Documents 的余弦距离:
# 0.2780946254748859
# 0.3359874398407345
# 0.12442062921732622
# 0.16765957304055518
# 0.1235534717848908
# 
# Query 与 Documents 的欧氏距离:
# 1.2015867924548178
# 1.1523996958849365
# 1.3233135541273946
# 1.2902251323704432
# 1.3239686813653393

扩展阅读:这种可变长度的 Embedding 技术背后的原理叫做 Matryoshka Representation Learning

向量数据库

向量数据库,是专门为向量检索设计的中间件!向量数据库其实最早在传统的人工智能和机器学习场景中就有所应用

在大模型兴起后,由于目前大模型的 token 数限制,很多开发者倾向于将数据量庞大的知识、新闻、文献、语料等先通过嵌入(embedding)算法转变为向量数据,然后存储在 Chroma 等向量数据库中

当用户在大模型输入问题后,将问题本身也 embedding,转化为向量,在向量数据库中查找与之最匹配的相关知识,组成大模型的上下文,将其输入给大模型,最终返回大模型处理后的文本给用户,这种方式不仅降低大模型的计算量,提高响应速度,也降低成本,并避免了大模型的 tokens 限制,是一种简单高效的处理手段

此外,向量数据库还在大模型记忆存储等领域发挥其不可替代的作用

Chroma 向量数据库

官方文档:https://docs.trychroma.com/docs/overview/introduction

pip install chromadb
# 为了演示方便,我们只取两页(第一章)
paragraphs = extract_text_from_pdf(
    "llama2.pdf",
    page_numbers=[2, 3],
    min_line_length=10
)
import chromadb
from chromadb.config import Settings
 
class MyVectorDBConnector:
    def __init__(self, collection_name, embedding_fn):
        # 内存模式
        chroma_client = chromadb.Client(Settings(allow_reset=True))
        # 持久化模式
        # client = chromadb.PersistentClient(path="/path/to/save/to")
        # Client-Server 模式
        # chroma_client = chromadb.HttpClient(host='localhost', port=8000)
        # Server 提前跑起来:chroma run --path /db_path
 
        # 注意:为了演示,实际不需要每次 reset()
		# 清空数据库,并且是不可逆的!
        chroma_client.reset()
 
        # 创建一个 collection
        self.collection = chroma_client.get_or_create_collection(name=collection_name)
        self.embedding_fn = embedding_fn
 
    def add_documents(self, documents):
        '''向 collection 中添加文档与向量'''
        self.collection.add(
            embeddings=self.embedding_fn(documents),  # 每个文档的向量
            documents=documents,  # 文档的原文
            ids=[f"id{i}" for i in range(len(documents))]  # 每个文档的 id
        )
 
    def search(self, query, top_n):
        '''检索向量数据库'''
        results = self.collection.query(
            query_embeddings=self.embedding_fn([query]),
            n_results=top_n
        )
        return results
# 创建一个向量数据库对象
vector_db = MyVectorDBConnector("demo", get_embeddings)
# 往向量数据库中添加文档
vector_db.add_documents(paragraphs)
 
user_query = "Llama 2 有多少参数"
# user_query = "Does Llama 2 have a conversational variant"
results = vector_db.search(user_query, 2)
 
for para in results['documents'][0]:
    print(para+"\n")
 
# Llama 2, an updated version of Llama 1, trained on a new mix of publicly available data...
# In this work, we develop and release Llama 2, a family of pretrained and fine-tuned LLMs...

澄清几个关键概念:

  • 向量数据库的意义是快速的检索
  • 向量数据库本身不生成向量,向量是由 Embedding 模型产生的
  • 向量数据库与传统的关系型数据库是互补的,不是替代关系,在实际应用中根据实际需求经常同时使用

主流向量数据库功能对比

名称介绍Web GUIGPU 支持远程调用 (HTTP/gRPC)云原生开源元数据 (hybrid search)
FAISSMeta 开源的向量检索引擎
Pinecone商用向量数据库,只有云服务N/A
Milvus开源向量数据库,同时有云服务
Weaviate开源向量数据库,同时有云服务
Qdrant开源向量数据库,同时有云服务
PGVectorPostgres 的开源向量检索引擎
RediSearchRedis 的开源向量检索引擎

ElasticSearch 也支持向量检索

扩展阅读:https://guangzhengli.com/blog/zh/vector-database

如何选型向量数据库

​在选择适合项⽬的向量数据库时,需要根据项⽬的具体需求、团队的技术背景和资源情况来综合评估。以下是⼀些建议和注意事项:

向量嵌⼊的⽣成

  • 如果已经有了⾃⼰的向量嵌⼊⽣成模型,那么需要的是⼀个能够⾼效存储和查询这些向量的数据库
  • 如果需要数据库服务来⽣成向量嵌⼊,那么应该选择提供这类功能的产品

延迟要求

  • 对于需要实时响应的应⽤程序,低延迟是关键,需要选择能够提供快速查询响应的数据库
  • 如果应⽤程序允许批量处理,那么可以选择那些优化了⼤批量数据处理的数据库

开发⼈员的经验

  • 根据团队的技术栈和经验,选择⼀个易于集成和使⽤的数据库
  • 如果团队成员对某些技术或框架更熟悉,那么选择⼀个能够与之⽆缝集成的数据库会更有利

基于向量检索的 RAG

class RAG_Bot:
	def __init__(self, vector_db, llm_api, n_results=2):
	    self.vector_db = vector_db
	    self.llm_api = llm_api
	    self.n_results = n_results
	
	def chat(self, user_query):
	    # 1. 检索
	    search_results = self.vector_db.search(user_query, self.n_results)
	
	    # 2. 构建 Prompt
	    prompt = build_prompt(
	        prompt_template, context=search_results['documents'][0], query=user_query)
	
	    # 3. 调用 LLM
	    response = self.llm_api(prompt)
	    return response
# 创建一个 RAG 机器人
bot = RAG_Bot(
    vector_db,
    llm_api=get_completion
)
 
user_query = "llama 2 有多少参数?"
response = bot.chat(user_query)
print(response)
 
# Llama 2 有 7B、13B 和 70B 参数的变体。