Alex:Lewis,完成文本分块之后,下一步应该就是 Embedding,也就是嵌入技术。我知道,向量嵌入是现代 AI 系统的知识核心,当然也是 RAG 系统的核心。它的重要性不言而喻。
Lewis:当然。今天,大模型吸收世界级知识的效率和速度,可能已经超过了我们人类。你可以把一个训练良好的大模型想象成一座大到难以想象的图书馆,或者一个知识水晶球,里面装满了它能够从人类那里收集到的所有书籍、博客、音频和视频信息。每一本书、每一个网页、每一段视频,都被神奇地转换成一串数字,这一切都要归功于 embedding 技术。
Embedding 是对外部信息的编码
在深入技术细节之前,我们先用人脑来做一个类比:我们是如何感知外部信息、对它进行编码并存储的?
这是一个复杂的、多层次的生物与神经过程。首先,外部信息通过感官进入大脑,例如视觉、听觉、触觉、嗅觉和味觉。每一种感官都有专门的受体,负责将物理或化学刺激,例如光、声波、压力、化学分子,转换成神经信号。这些神经信号会通过周围神经系统传输,经过脊髓或其他中继站,例如丘脑,到达大脑中对应的区域。
一旦神经信号到达大脑,就会由皮层区域进行处理。这一步被称为信息编码,主要包括以下两种编码方式。
空间编码:大脑通过激活不同位置的神经元来表示不同刺激。例如,视觉皮层的不同部分对应视野中的不同区域。
时间编码:神经元放电的频率和时间模式也携带信息。神经元会按照特定速率和序列发出电脉冲,以表示不同的感官特征,例如音高、强度等。
经过大脑皮层处理和编码之后,信息会被短期存储,然后被巩固到长期记忆中。大脑可以通过记忆回忆,从长期记忆中检索信息,并在工作记忆中重新激活它。每一次记忆被提取时,大脑都会重新处理这些信息,这也可能导致信息被重新编码和更新。
随着外部信息不断流入,大脑中的神经网络也会发生变化。这种变化被称为神经可塑性,意思是大脑的结构和功能可以根据经验和学习不断调整。在这个过程中,神经元之间的突触连接可能被增强或削弱,甚至会形成新的突触连接。整个过程涉及神经元之间复杂的连接和突触可塑性,使我们能够感知、理解并记住来自外部世界的信息。
类似地,embedding 就是基于大模型的 AI 系统“感知”和内化外部世界信息的过程。它是 RAG 系统中负责感知、理解和表达外部数据的核心部分,也是理解和处理高维数据的关键工具。
图 3.1:人类感知系统与 AI embedding 系统的对比,展示了并行的信息流
就像大脑通过视觉、听觉、触觉等方式感知外部信息一样,embedding 模型通过将文本、图像、音频等数据从自然语言转换为向量表示,使系统能够“感知”数据中的语义内容。
感知系统不仅具有感觉功能,还可以整合不同类型的感官输入——眼睛看到的东西和耳朵听到的东西,会被大脑综合处理。类似地,embedding 模型不仅能够处理文本,也能够处理图像、音频和其他多模态数据,并将这些不同模态转换到同一个向量空间中,使来自不同来源的信息能够被一致地处理。
感知信息和记忆,甚至由这些信息和记忆内化形成的潜意识,会影响一个人的行为。同样,embedding 模型的质量也会直接影响后续的检索和生成过程。一个优秀的 embedding 模型可以帮助向量数据库找到最相关的信息,从而引导系统生成更准确的答案;反过来,一个较差的 embedding 模型可能导致系统对查询的“感知”产生偏差,影响生成结果的准确性。
一个人的感知系统会随着经验积累变得更加敏感和精确。同样,embedding 模型也可以通过训练和微调不断提升性能,使 AI embedding 系统能够更准确地捕捉语义含义,从而增强 RAG 系统的表现。
embedding 模型的选择,可能是 RAG 系统开发过程中最重要、最具决定性的步骤之一。没有强大的 embedding 模型,后续的检索和生成阶段几乎不可能产生令人满意的结果。不过,这并不意味着只要选择排行榜第一的 embedding 模型,就一定能保证 RAG 系统表现优异,因为索引方式、检索策略等因素也必须被考虑进去。
在实际应用中,embedding 模型的选择往往会受到成本、速度和隐私等因素的约束。例如,虽然 OpenAI 的 embedding 模型表现非常好,但如果由于隐私问题,数据不能发送到外部,就可能需要使用本地部署的开源模型。在这种情况下,关键就不再是寻找“最好”的模型,而是在可用选项之间取得平衡,并选择最适合的本地模型。
从技术角度看,通过将离散数据,例如单词、句子或图像,映射到一个连续向量空间中,embedding 模型能够捕捉数据之间的语义关系和句法关系。
这些数字并不只是简单的 0 和 1,而是包含丰富语义信息以及世界级知识的复杂高维向量。
图 3.2:三层输入进入 embedding 模型,并生成不同向量作为输出
embedding 的维度可以是 512、768、1024 等任意值,就像为每个词在多维空间中分配一个位置。每一个维度都可以被看作一种特征,用来帮助描述含义,例如主题、情感、实体类型,或者其他语义特征。每个词都通过这些特征来定义自己。
维度越高,能够表示的细节越丰富,但计算复杂度也会随之增加。选择合适的 embedding 维度,是让大模型准确理解词义、同时又不造成性能下降的关键。
每个 embedding 维度本质上是一组坐标,也就是向量,通常位于一个高维空间中。在这个空间里,每个点,也就是 embedding 的位置,反映了它所对应文本的含义。就像同义词词典中含义相近的词往往彼此靠近一样,相似概念在 embedding 空间中也会靠近。这使我们能够直观地比较不同文本。
通过将文本简化为这些数值表示,我们可以使用简单的数学运算快速衡量两段文本之间的相似度,而不必关心它们原始的长度或结构。这样,我们就把两个文本、两张图像或两段视频之间的比较,转换成了数值差异上的运算——这正是 RAG 系统能够实现检索的基本原理。这也呼应了毕达哥拉斯那句名言:万物皆数。
衡量向量之间的相似度
一些常见指标如下:
欧氏距离:衡量两个点之间的直线距离。
曼哈顿距离:衡量两个点沿坐标轴方向的距离,而不是直线距离;这个指标较少用于比较向量相似度。
余弦相似度:衡量两个向量之间夹角的余弦值。
点积或内积(IP) :衡量一个向量在另一个向量上的投影。在欧氏空间中,内积和点积是等价的。
图 3.3:四个面板分别解释欧氏距离、曼哈顿距离、余弦相似度和内积
相似度指标的选择应基于具体应用场景。例如,OpenAI 推荐使用余弦相似度来衡量其 embeddings。代码示例如下:
import numpy as np
def cosine_similarity(vec1, vec2):
dot_product = np.dot(vec1, vec2)
norm_vec1 = np.linalg.norm(vec1)
norm_vec2 = np.linalg.norm(vec2)
return dot_product / (norm_vec1 * norm_vec2)
similarity = cosine_similarity(query_result, document_result)
print("Cosine Similarity:", similarity)
以上就是 embeddings 的基础知识。欢迎来到数据向量的世界,在这个世界中,脉动的信息构成了 RAG 系统的核心组件。
从早期词嵌入模型到大模型 embeddings
多年来,embedding 模型的格局发生了巨大变化。从最早的 Word2Vec、GloVe 等词向量模型,到基于深度学习的动态 embedding 方法,embedding 技术一直在快速发展和演进。早期模型主要关注词级别 embeddings,但随着需求变得更加复杂,重点逐渐转向能够处理句子和段落的模型。2018 年出现了一个关键转折点——Google 推出了 BERT,也就是 Bidirectional Encoder Representations from Transformers。通过利用 Transformer 模型,BERT 将文本转换为简洁的向量表示,并在各种自然语言处理任务中实现了前所未有的性能提升。
不过,BERT 并不是为了高效生成句子级 embeddings 而优化的。这一局限催生了 Sentence-BERT,后来它演进为广为人知的 SentenceTransformers 框架。这个框架如今支持训练和部署多种开源 embedding 模型。Sentence-BERT 修改了原始 BERT 架构,以生成语义丰富的句子 embeddings,大幅降低了查找相似句子等任务的计算成本。
如今,embedding 模型生态呈现出多样化特征,许多服务提供商都推出了自己的 embedding 方案。为了应对这种多样性,研究人员和实践者通常会依赖大规模文本 embedding 基准等评估标准,客观比较各种 embedding 模型。
早期词嵌入模型
Word2Vec 是 Google 提出的一种词嵌入模型,包含两种架构:Continuous Bag-of-Words,简称 CBOW,以及 Skip-Gram。该模型通过预测目标词和上下文词之间的关系,学习词的低维向量表示。尽管机制相对简单,Word2Vec 仍然能够有效捕捉词与词之间的语义相似性,并在训练后生成包含语义信息的词向量。这标志着我们进入迷人的词向量世界的重要一步,也为大规模模型时代奠定了基础。
图 3.4:带箭头的散点图,展示人生阶段,标注有 grandfather、man、adult、woman、boy、girl、child 和 infant
GloVe,也就是 Global Vectors for Word Representation,是斯坦福大学提出的另一种词嵌入方法。与 Word2Vec 不同,GloVe 利用全局共现矩阵,并基于词共现概率构建损失函数来训练模型。这种方法在处理稀疏数据和捕捉全局语义信息方面表现出色。
FastText 是 Facebook AI Research 团队提出的一种词嵌入模型。它的独特之处在于考虑了子词信息,即将单词分解成字符 n-gram。这种策略使 FastText 在处理未登录词和词形变化时更加有效,从而增强了模型的泛化能力。
上下文词嵌入模型
虽然传统词嵌入模型,例如 Word2Vec、GloVe、FastText,在捕捉词与词之间的语义相似性方面取得了显著成功,但它们生成的是静态 embeddings。这意味着无论上下文如何,每个词都会被赋予相同的向量表示,因此在处理多义词和复杂上下文时表现较差。为了解决这一限制,研究人员开发了上下文词嵌入模型,使词的表示能够根据其具体上下文动态变化,从而更准确地反映词义。
ELMo,也就是 Embeddings from Language Models,由 AllenNLP 团队提出,是一种基于双向 LSTM 的语言模型。它可以根据一个词的上下文动态生成词 embeddings,使同一个词在不同上下文中拥有不同的向量表示。这为捕捉词语多义性提供了更好的方式。
图 3.5:词嵌入模型的流程图,分支为上下文无关方法和上下文相关方法
如果你对 Transformer 等大模型基础架构,或者各种词嵌入模型的技术细节感兴趣,可以参考每种模型对应的相关论文来进一步学习。此外,也可以阅读 Lewis 的课程《GPT Illustrated: How Large Models Are Built》。这门课程以通俗但深入的方式解释了自然语言处理中的各种核心技术。
句子嵌入模型与 SentenceTransformers 框架
Sentence-BERT,也就是 SBERT,是 BERT 的扩展,旨在为句子生成固定长度的向量表示。通过在 BERT 上添加双塔架构和对比学习训练方法,SBERT 可以高效计算句子之间的语义相似度,适用于信息检索、问答系统等场景。
图 3.6:流程图展示用户查询经过 embedding 模型处理,并作为向量绘制在搜索空间中
直到今天,SBERT 仍然在工业界被广泛使用,不过它后来已经更名为 SentenceTransformers 框架。这个框架是下载、微调和训练文本与图像 embedding 模型时最常用的基础 Python 库。
SentenceTransformers 框架由 UKPLab 创建,并由 Hugging Face 维护。Hugging Face 提供了超过 5000 个预训练 SentenceTransformers 模型,其中包括许多出现在 MTEB 排行榜上的先进模型。使用 SentenceTransformers 框架,也可以轻松训练或微调模型,以满足特定用例的需求。
在 SentenceTransformers 框架中,常用的开源 embedding 模型包括 all-MiniLM-L6-v2 和 all-mpnet-base-v2。这两个模型分别基于 MiniLM 架构,一种轻量级 Transformer 模型,以及 MPNet 架构,一种结合 BERT 和 XLNet 优点的架构,并且都使用 Hugging Face 的 transformers 库进行优化。它们常被用作 RAG 系统中的基线模型,其性能可以作为评估其他模型优势或改进效果的基础。其中,all-mpnet-base-v2 模型提供更高的 embedding 质量,而 all-MiniLM-L6-v2 模型则以速度约为前者 5 倍而著称。
| 模型名称 | 参数量 | Embedding 维度 | 特点 / 优势 | 弱点 / 局限 | 应用场景 |
|---|---|---|---|---|---|
| paraphrase-MiniLM-L6-v2 | 33M | 384 | 专门针对文本相似度和语义检索优化;高效,适合低资源环境。 | 较低的 embedding 维度可能限制其在更复杂语义任务中的表现。 | 比较句子或段落相似度;适合常见自然语言处理任务。 |
| all-MiniLM-L6-v2 | 33M | 384 | 针对不同用例进行广泛训练;推理速度快,整体表现良好。 | 与更大模型或多语言模型相比,多语言支持较弱。 | 搜索引擎优化、语义搜索、句子相似度和轻量级检索任务。 |
| all-mpnet-base-v2 | 110M | 768 | 基于 MPNet 的更大模型,语义表示能力更强,准确率更高。 | 比基于 MiniLM 的模型需要更多计算资源。 | 高精度语义检索、排序、聚类,以及需要更高准确率的应用。 |
表 3.1:部分 SentenceTransformer embedding 模型对比,包括模型大小、embedding 维度、优势、限制和应用场景
计算两个句子之间的语义相似度
现在我们将使用 sentence-transformers 库中的模型来比较两个句子的相似度。
首先,安装 sentence-transformers 库:
pip install sentence-transformers
接下来,使用以下代码计算两个句子之间的语义相似度:
from sentence_transformers import SentenceTransformer, util
model = SentenceTransformer('paraphrase-MiniLM-L6-v2')
sentence1 = "How is the gameplay of this game?"
sentence2 = "I want to learn about the combat system of this one."
embedding1 = model.encode(sentence1, convert_to_tensor=True)
embedding2 = model.encode(sentence2, convert_to_tensor=True)
cosine_similarity = util.pytorch_cos_sim(embedding1, embedding2)
print(f"The similarity between the two sentences is: {cosine_similarity.item():.4f}")
第一次运行程序时,SentenceTransformers 框架会将指定预训练模型的配置和参数下载到本地缓存。请确保有足够的存储空间用于下载模型文件。
sentence-transformers 库依赖 Hugging Face 的模型存储机制,默认使用 Hugging Face 的缓存文件夹路径。
Windows 操作系统中的缓存文件夹路径:
C:\Users<username>.cache\huggingface\transformers
macOS 或 Linux 操作系统中的缓存文件夹路径:
C:\Users<username>.cache\huggingface\transformers
第一次加载某个模型,例如 paraphrase-MiniLM-L6-v2 时,模型文件会被下载并存储到上述目录。如果同一个模型之前已经通过 Hugging Face 下载过,则不会再次下载。你可以通过设置 TRANSFORMERS_CACHE 环境变量来自定义缓存路径:
export TRANSFORMERS_CACHE="your custom cache path"
多语言 embedding 模型
虽然早期词嵌入和句子嵌入模型在单语言场景下表现良好,但它们在跨语言任务中面临挑战,也很难直接应用。为了解决这个问题,一些专门面向多语言环境设计的 embedding 模型应运而生,例如 MUSE 和 XLM-R。
MUSE,也就是 Multilingual Unsupervised or Supervised Embeddings,是 Facebook,也就是现在的 Meta,开发的一种多语言词嵌入工具。它支持无监督和有监督两种模式,可以将不同语言的词 embeddings 映射到同一个向量空间中,从而实现跨语言语义比较。
XLM-R,也就是 XLM-RoBERTa,是 Facebook AI Research 团队开发的一种跨语言模型。它基于 RoBERTa 架构,并在覆盖 100 种语言的大规模数据集上训练。这种大规模且多样化的训练,使 XLM-R 在多语言理解和翻译任务中表现出色。
Google 提出的 Universal Sentence Encoder,简称 USE,是另一种基于 Transformer 的句子嵌入模型,旨在为句子和段落生成通用向量表示。USE 在多语言环境中也表现良好,适合文本分类、聚类和语义相似度计算等任务。
现代大模型通常具备强大的多语言能力。例如,OpenAI 的 text-embedding-3-small 和 text-embedding-3-large,BGE 的 M3-Embedding,以及 Cohere 的多语言 embedding 模型,都支持接近或超过 100 种语言。
面向不同数据类型的 embedding 模型
embedding 模型并不限于文本。它们也可以用来将图像、音频、视频、图以及知识图谱表示为向量。这使得不同类型的数据都能够使用类似的数学运算进行搜索、比较和检索。
图像、音频和视频 embedding 模型
图像数据的反向搜索是许多应用中的核心功能。例如,如果用户想找到更多与悟空这一中国神话著名角色相关的图片,可以上传一张参考图,并要求搜索引擎识别视觉上相似的图像。传统上,这类任务依赖卷积神经网络,也就是 CNN 模型,例如 ResNet50。ResNet50 是微软在 2015 年基于 ImageNet 数据集训练的模型,能够高效进行图像特征提取和相似度匹配。
类似地,在视频反向搜索中,也可以使用 ResNet50 进行视频 embeddings。通过将视频帧转换为特征向量,并在视频帧数据库中进行相似度检索,可以定位与输入视频最相似的其他视频片段。
对于音频数据,可以通过 PANN,也就是 Pretrained Audio Neural Network,进行处理。PANN 在大规模音频数据集上进行了预训练。它允许学习者通过输入一段音频片段来执行反向音频搜索,并被广泛应用于音频检索任务和声音分类系统。
图文联合 embedding 模型
图文联合 embedding 模型的出现,为 embedding 技术打开了新的方向。这些多模态 embedding 模型可以同时捕捉文本以及图像、音频甚至视频等各种非结构化数据形式的语义表示。此类模型不仅允许用户用文本搜索图像,还可以生成图像描述、执行反向图像搜索等功能。
2021 年,OpenAI 的 CLIP,也就是 Contrastive Language-Image Pretraining,成为这类联合 embedding 模型的标准架构。OpenAI 收集了一个包含超过 4 亿个图文对的数据集,并在这些图文对上使用对比学习,将图像和文本映射到同一个向量空间中,从而建立文本和图像之间的联系。这种跨模态 embedding 使机器在处理图像和文本数据时,能够更有效地理解并关联不同模态的信息,为多模态学习带来了突破。
图 3.7:图示展示一张狗的照片和文本分别经过编码器处理,并在矩阵中结合
2024 年,Google 发布了 SigLIP,也就是 sigmoidal-CLIP 模型,这是 CLIP 模型的改进版本,在零样本任务中表现出色,并且更容易使用。随着硬件技术进步和模型优化能力提升,小尺寸模型变得越来越受欢迎。例如,Unum 提供了小规模多模态 embedding 模型。由于内存占用低,这类小模型可以在笔记本电脑等设备上流畅运行,实现低延迟和更快处理速度。这不仅扩大了多模态技术的应用范围,也让它的应用更加方便和普及。
图和知识图谱 embedding 模型
Graph Embedding 和 Knowledge Graph Embedding 在处理涉及复杂关系和结构的数据时具有重要价值。图 embedding 主要关注节点之间的结构关系,而知识图谱 embedding 则强调实体之间的语义关系。
图 3.8:图示展示图结构和知识图谱结构如何通过箭头转换到向量空间
图 embedding 会将图中的节点、边及其关系映射到低维向量空间中,同时保留图的结构信息。例如,Node2Vec 和 GraphSAGE 等模型会通过随机游走或聚合邻居信息等方式,将图的局部和全局结构编码为向量表示。这些向量可以直接用于节点分类、聚类或图上的关系预测等下游任务。
图 embedding 被广泛用于社交网络分析、推荐系统、金融网络、医疗网络等领域,并且可以有效处理这些数据中存在的复杂关系结构和网络特征。通过图 embedding,系统可以学习节点的语义和结构信息,使相似节点在向量空间中更接近,从而提升节点分类和链接预测等任务的表现。
知识图谱 embedding,例如 TransE、TransR、DistMult 等,会将实体和关系映射到同一个向量空间中,从而有效捕捉实体之间的语义关系。这些模型被广泛用于知识图谱补全和链接预测任务,帮助发现知识图谱中的潜在关系。例如,TransE 模型使用简单的平移操作来捕捉实体之间的关系,而 TransR、DistMult 等模型进一步增强了对复杂、多维关系的建模能力。
知识图谱 embedding 在智能问答、推荐系统、语义搜索等场景中发挥着重要作用。例如,在 QA 系统中,embeddings 可以帮助识别问题与知识库实体之间的复杂关系,从而提供更准确的答案。
大语言模型时代的 embedding 模型
随着大模型时代到来,以 BERT 和 GPT 为代表的模型彻底改变并重塑了传统 embedding 架构,形成了统一化格局。这些大模型不仅在文本生成任务中表现优秀,其隐藏层输出也可以作为强大的文本 embedding 工具,有效捕捉文本中的深层语义信息。这一能力极大提升了相似度计算、分类和搜索等任务的表现。
除了将 BERT 和 GPT “裁剪”后用作 embedding 模型之外,OpenAI 以及其他 AI 提供商或开源组织,也提供了大量专门为此目的设计的 embedding 模型,开发者可以通过 API 或开源代码使用。这些模型在各种自然语言处理应用中展现出优秀适应性,为文本提供丰富语义表示,适用于相似度计算、分类、搜索等任务。
因此,和其他 AI 技术一样,embedding 技术在过去几年也经历了重大突破和发展。机器对语言和图像的理解能力得到增强,为各种下游任务提供了坚实基础,也推动了 Scaling Law 的发展。
OpenAI 联合创始人、前首席科学家 Ilya Sutskever 指出,高质量的人类生成文本数据基本已经耗尽,Scaling Law 的进一步发展需要探索新方向。在文本数据方面,我们可能已经使用了大部分现有资源,但仍然有大量视频和多模态信息等待大模型分析和理解。
此外,Scaling Law 未来的发展还会涉及几个方向,包括硬件扩展、资源和性能优化,以及 AI 系统主动创造高质量训练数据。这些视角为未来探索提供了有价值的方向。
接下来,我们将进一步聚焦 RAG 系统中使用的现代 embedding 模型。
现代 embedding 模型:OpenAI、jina、cohere、voyage
在现代 embedding 模型领域,许多公司都提供商业 embedding 模型。这些商业 embedding 模型通常并不完全开源,而是通过 API 调用的方式提供给用户。虽然有时需要付费,但总体而言,这些服务的成本并不高,并且随着技术进步和服务竞争加剧,价格正在逐步下降,这与大模型的定价趋势类似。
尽管如此,从使用习惯上看,Lewis 最常用的模型仍然是 OpenAI 的 text-embedding-3-small 和 text-embedding-3-large 模型。不过,在这个竞争激烈的市场中,OpenAI 在 embedding 模型上的优势并不明显。例如,Google 已经在其 Gemini 系列中推出了 text-embedding-005 模型,一些初创公司如 Jina、Cohere 和 Voyage 也提供最新的商业 embedding 模型。这些公司的模型在语义相似度计算、搜索和推荐等场景中表现出色,为开发者提供了多样化选择。
使用 OpenAI 的 text-embedding-3-small 进行产品推荐
OpenAI 提供了多种强大的商业 embedding 模型,包括较早的 text-embedding-ada-002,以及当前旗舰模型 text-embedding-3-small 和 text-embedding-3-large。text-embedding-3-large 模型维度更高,达到 3072 维,因此能够捕捉更细致的语义信息,但这也意味着更高的计算成本。
| 模型名称 | 维度 | MTEB 表现 | 每 100 万 token 价格 | 特点 |
|---|---|---|---|---|
| text-embedding-3-small | 1536 | 62.3% | $0.02 | 性价比高 |
| text-embedding-3-large | 3072 | 64.6% | $0.13 | 性能强 |
| text-embedding-ada-002 | 1536 | 61.0% | $0.08 | 早期模型 |
表 3.2:OpenAI embedding 模型对比,包括向量维度、MTEB 表现、价格和关键特点
价格和基准分数可能变化。在生产环境使用这些数字之前,请查看提供商的官方文档。
使用 embeddings 的产品推荐示例场景
接下来,让我们使用 embedding 技术构建一个模拟场景。假设有一组玩家,他们为各种动作类游戏提供了简短评价和评分。首先,使用 OpenAI 的 text-embedding-3-small 模型为用户评论以及每个游戏的描述生成 embedding 向量。然后,计算用户评论 embedding 向量与游戏《Myth of the Vanquished Gods: Monkey King》的 embedding 向量之间的余弦相似度。这样,得到的相似度分数就成为该游戏对用户的推荐评分,从而识别出可能喜欢玩《Myth of the Vanquished Gods: Monkey King》的用户群体。
import os
import openai
import pandas as pd
import numpy as np
import json
from sklearn.metrics.pairwise import cosine_similarity
### 读取用户评论数据集
df = pd.read_csv("data/Chronicles of Godslaying/user_reviews.csv")
### 读取游戏描述文件
with open("data/Chronicles of Godslaying/game_description.json", "r") as f:
game_descriptions = json.load(f)
### 定义获取 embedding 向量的函数
def get_embedding(text, model="text-embedding-3-small"):
response = openai.embeddings.create(
input=[text],
model=model
)
return response.data[0].embedding
## 获取所有游戏的 embedding 向量
unique_games = df['game_title'].unique().tolist()
target_game = "Killing God: Hu Sun" # 修改目标游戏名称
if target_game not in unique_games:
unique_games.append(target_game) # 确保目标游戏在列表中
game_embeddings = {}
for game in unique_games:
description = game_descriptions[game]
game_embeddings[game] = np.array(get_embedding(description))
计算用户评论的 embedding 向量,也就是用户评论过的所有游戏 embedding 向量的平均值:
user_vectors = {}
for user_id, group in df.groupby("user_id"):
user_game_vecs = []
for idx, row in group.iterrows():
g_title = row['game_title']
g_vec = game_embeddings[g_title]
user_game_vecs.append(g_vec)
user_vectors[user_id] = np.mean(np.array(user_game_vecs), axis=0)
## 获取 "Killing God: Hu Sun" 的 embedding 向量
target_vector = game_embeddings[target_game]
## 计算每个用户评论 embedding 与目标游戏 embedding 之间的余弦相似度
results = []
for user_id, u_vec in user_vectors.items():
u_vec_reshaped = u_vec.reshape(1, -1)
t_vec = target_vector.reshape(1, -1)
similarity = cosine_similarity(u_vec_reshaped, t_vec)[0,0]
results.append((user_id, similarity))
## 排序并找出最可能喜欢 Killing God: Hu Sun 的用户
result_df = pd.DataFrame(results, columns=["user_id", f"similarity_to_{target_game}"])
result_df = result_df.sort_values(by=f"similarity_to_{target_game}", ascending=False)
print(f"\nTop 5 users most likely to like {target_game}:")
print(result_df.head())
输出如下:
Top 5 users most likely to like Killing God: Hu Sun:
user_id similarity_to_Killing God: Hu Sun
U001 0.923190
U004 0.922800
U005 0.917884
U002 0.910476
U003 0.909157
如果某个用户过去喜欢的游戏,例如 Souls-like、动作 RPG,风格与 Killing God: Hu Sun 相似,那么他的相似度分数应该更高。
使用 jina embeddings 进行跨语言聚类
jina-embeddings-v3 是一种先进的多语言文本 embedding 模型,基于 XLM-RoBERTa,并经过多项改进,可以高效编码更长的文本序列。该模型拥有 5.7 亿参数,支持 89 种语言,包括阿拉伯语、中文、法语、德语和日语,并在多种多语言任务中表现出色。除了对多语言的强大支持之外,jina-embeddings-v3 模型还特别增强了长上下文处理能力,支持最长 8192 token 的输入,非常适合长文档检索和长文本 embeddings 等任务。
jina-embeddings-v3 模型采用面向任务的低秩适配技术,为检索、聚类、分类和文本匹配等任务生成高质量 embeddings。这种灵活性使模型能够根据不同任务需求生成最优 embeddings。为了进一步提升效率并降低资源消耗,该模型利用 FlashAttention2 和 DeepSpeed 框架,显著提升了分布式训练效率,并降低了内存使用。
此外,jina-embeddings-v3 模型集成了 Matryoshka 表示学习方法,使学习者可以灵活调整 embedding 维度,最低可降至 32 维,而且不会对整体性能产生负面影响。这对于生产环境中需要大规模向量存储的应用,或者边缘设备上的应用,具有显著优势。
jina-embeddings-v3 模型提供了三个主要 API 参数,供学习者自定义:
task:根据下游任务,例如检索、分类或文本匹配,调整信息嵌入策略,以优化特定任务的性能。
dimensions:学习者可以根据需要灵活调整 embedding 维度,在保证性能的同时优化存储成本。
late_chunking:在 embedding 之后对文本应用分块,尤其优化长文档表示,并有助于提升检索性能。
接下来,我们将使用 jina-embeddings-v3 模型演示多语言游戏描述的语义检索和聚类,展示如何在多种语言中找到与《Legend of Deicide: Monkey King》风格相似的游戏描述,并执行简单文本聚类。
首先,安装相关库 einops。这是一个用于简化和增强向量,也就是多维数组操作的 Python 库。
pip install einops
然后,配置 Jina API,你需要从 Jina 官方网站获取,并读取游戏数据。
import pandas as pd
import numpy as np
import requests
from sklearn.cluster import KMeans
### 配置 Jina API
url = 'https://api.jina.ai/v1/embeddings'
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer your Jina API'
}
### 读取游戏描述数据
df = pd.read_csv("data/Chronicles of Godslaying/game_descriptions.csv")
texts = df['description'].tolist()
### 获取文本 embeddings
data = {
"model": "jina-embeddings-v3",
"task": "text-matching",
"dimensions": 1024,
"normalized": True,
"input": texts
}
response = requests.post(url, headers=headers, json=data)
if response.status_code != 200:
raise RuntimeError(f"API call failed: {response.status_code} - {response.text}")
embeddings = [item['embedding'] for item in response.json().get('data', [])]
if not embeddings:
raise RuntimeError("API did not return embedding vectors")
embeddings = np.array(embeddings)
### 聚类分析
kmeans = KMeans(n_clusters=3, random_state=42)
labels = kmeans.fit_predict(embeddings)
print("\nClustering results:")
for i, lbl in enumerate(labels):
print(f"Cluster {lbl}: {texts[i]}")
输出如下:
Cluster 2: "Killing God: Hu Sun" is an action RPG game adapted from the classic *Journey to the West*, known for its high-quality graphics and hardcore combat style.
Cluster 2: Killing God: Hu Sun is an action RPG game inspired by the classic *Journey to the West*, known for its stunning visuals and challenging combat.
Cluster 2: Ein actiongeladenes RPG, Killing God: Hu Sun, inspiriert von der klassischen chinesischen Literatur, kombiniert mythische Kreaturen und intensiven Nahkampf.
Cluster 2: *Sekiro: Shadows Die Twice* is a challenging action-adventure game set in a reimagined Sengoku-era Japan, focusing on precise sword combat and stealth.
Cluster 2: The player explores a vast open world full of mysteries, terrifying enemies, and hidden treasures in *Elden Ring*.
Cluster 0: *Elden Ring* is an open-world action RPG developed by FromSoftware and George R.R. Martin, known for its vast world and diverse builds.
Cluster 0: *Bloodborne* is an action RPG set in a Victorian-style horror world, known for its fast-paced offensive and defensive combat and eerie atmosphere.
Cluster 0: *Dark Souls Ⅲ* is a dark fantasy action RPG game, renowned for its extreme difficulty and deep world-building.
Cluster 1: **Organic skin care**: A segment unrelated to gaming, used to test clustering with an irrelevant topic.
Cluster 1: This text is unrelated to games and is only used to test the effect of text clustering.
接下来,使用 t-SNE 算法将高维 embedding 向量,也就是 1024 维,降到 2 维,然后将聚类结果可视化。由于本章篇幅限制,这里不展示具体代码。完整代码可以访问 Lewis 的 GitHub 仓库。完整代码可参考 github.com/PacktPublis…
图 3.9:散点图展示三个游戏描述聚类
可以看到,无论是中文、英文还是其他语言,只要游戏与 “Legend of Destroying Gods: Monkey King” 相似,就会被分配到同一个类别标签。现代 embedding 模型对多语言的支持非常实用。
jina-embeddings-v3、OpenAI 的商业 embedding 模型,以及 Cohere 的 embed-english-v3.0 等模型,都已经集成到 AWS SageMaker 和 Azure Marketplace 等云平台中。Pinecone、Qdrant、Milvus 等向量数据库,以及 LangChain 和 LlamaIndex 等开源框架,也都与这些模型无缝集成,使用户能够高效执行相似度检索和搜索任务。
使用 MTEB 评估 embedding 模型
Alex:许多开源和商业 embedding 模型在不同场景中的表现各不相同。哪个 embedding 模型最好?谁来决定?
Lewis:MTEB,也就是 Massive Text Embedding Benchmark,是由 Hugging Face 和大规模文本检索社区共同提出的。它是一个专门用于评估文本 embedding 模型性能的基准框架。它覆盖多种语言和任务,提供了统一的大规模基准,可以系统评估 embedding 模型在不同应用场景中的表现。
Hugging Face 社区网站上的 MTEB 页面,提供了各种任务中最佳文本 embedding 模型的排行榜,为模型使用者提供参考。
图 3.10:英文 AI 模型排行榜表格,包含排名、分数和带注释的列标签
MTEB 的一大特点是任务多样性。考虑到 embedding 模型在真实世界中的应用多种多样,并且需要在检索、聚类和分类等任务上验证,MTEB 覆盖了八大类任务。
| 任务类别 | 任务示例 | 描述 |
|---|---|---|
| 检索 | 信息检索(BEIR) | 检索与查询语义匹配的文档 |
| 文本匹配 | 语义相似度(STS-B) | 判断两个句子之间的语义相似度 |
| 分类 | 句子分类(Amazon Reviews) | 预测句子类别,例如情感分析 |
| 聚类 | 主题聚类(TREC) | 将相似文本归为一组 |
| 排序 | 语义排序(MS MARCO) | 根据查询和语义相似度对文档排序 |
| 摘要评估 | 摘要质量评估(SummEval) | 评估机器生成摘要与参考摘要的匹配程度 |
| 回归 | 句子回归(STS-B) | 对句子进行回归分析,输出相似度分数 |
| 生成任务辅助 | 评估输入和输出质量 | 辅助评估生成模型输入和生成输出的 embedding 质量 |
表 3.3:MTEB 任务类别概览、代表性基准示例,以及每类任务评估的 embedding 能力
每当一个 embedding 模型完成训练后,负责该模型的组织、机构或公司通常会使用 MTEB 提供的各种数据集和任务来评估他们的模型。之后,他们会通过 GitHub Repo 将模型分数和代码提交到 MTEB 排行榜。
当然,如前所述,尽管 MTEB 的评估任务覆盖广泛且具有相当挑战性,但由于它使用的数据集大多是公开的,如果某个组织为了提升排名,专门针对这些任务和数据集优化模型,那么它在 MTEB 排行榜上的表现自然会非常突出。然而,这类模型在其他没有被专门优化过的任务上是否能取得同样效果,仍然不确定。
选择 embedding 模型的最佳方式,是在你自己的用例上评估它。为了找到最符合具体需求的 embedding 模型,有必要构建一个针对自身应用场景的评估数据集,并反复测试和比较不同 embedding 模型,包括各种商业 embedding 模型,以及 Hugging Face 和 SentenceTransformers 提供的开源模型。这个过程并不容易,但它是确保选出最佳模型的有效方式。接下来,我们将介绍一个个人独立完成 embedding 模型评估的优秀案例,供学习者学习和参考。
比较和选择 embedding 模型
Jonathan Ellis 在他的文章 “Best Embedding Models for Information Retrieval in 2025” 中,对常用 embedding 模型进行了评估和比较。
测试数据来自 ViDoRe 图像搜索基准数据集,并使用 Gemini Flash 1.5 OCR 工具转换为文本。Jonathan Ellis 之所以选择这些数据集,是因为大多数经典文本搜索数据集已经被开发者反复用于模型训练,因此不适合再用于评估。通过对图像搜索数据集进行 OCR 处理,他认为这些模型接触到的是它们从未见过的数据。考虑到许多大模型都是在已知公开数据集上训练的,他主动创建自己的测试数据,是一种非常出色的创新实践。
图 3.11:不同数据集和模型上的 NDCG@5 分数对比
评估结果揭示了一些有趣发现。例如,ModernBERT 和 Gemini 的 text-embedding-004 模型只支持英语,而大多数其他模型支持多种语言。在性能方面,voyage-3-large 模型表现突出,是最优秀的模型,而 voyage-3-lite 模型则在低成本领域表现出色。
在开源模型中,Stella 模型表现令人印象深刻。不过,尽管 stella-1.5b 的规模是 stella-400m 的四倍,但它的准确率并没有显著提升。
Gemini 的 text-embedding-004 模型被认为是性价比最高的模型,在免费条件下提供中等水平表现,并且有合理的每分钟 1500 次请求限制。唯一缺点是无法通过付费提升吞吐量。
相比之下,jina-embeddings-v3 和 Cohere 的 embed-v3 模型在这次测试中表现相对普通,被性能更好且成本更低的模型超越。
当然,单次独立测试结果并不能代表一切。embedding 模型格局变化很快。选择最合适的 embedding 模型,无论是商业模型还是开源模型,都需要深入理解每个模型,并基于具体应用场景、使用量、性能要求和预算限制进行判断。
这些结果反映的是某个特定时间点的一次独立评估。模型性能、价格、限制和可用性都可能变化,因此读者在选择模型前应核实当前细节。
稀疏 embedding、稠密 embedding 与 BM25
Alex:Lewis,我一直在整理《Chronicles of Godslaying: The Monkey》里的战斗日志,也经常听朋友们讨论 sparse embedding 和 dense embedding 这样的概念,但我总是容易混淆。你能解释一下它们之间的区别吗?
Lewis:在大模型时代,当我们谈到 embeddings,大多数时候指的是稠密 embeddings,也称为稠密向量。有时,我们也会使用稀疏 embeddings,也称为稀疏向量。如果你听到技术人员在项目中讨论 sparse embedding,通常意味着他们在做混合检索。我们后面会更深入地讨论这个主题。
接下来,让我们用一个简单类比解释这两种 embedding 方法的区别。想象你在描述一个人。稀疏 embedding 就像用标签来描述这个人:
[tall=1, glasses=1, long hair=0, round face=0, ...]
而稠密 embedding 则像用一组抽象特征来描述这个人:
[0.8, -0.2, 0.5, 0.3, ...]
例如,稀疏 embedding 会直接列出悟空的特征清单,而稠密 embedding 会将这些特征压缩成一种抽象的数值表示。
图 3.12:稀疏特征和稠密特征的对比图
理解稀疏和稠密 embeddings
Alex:哦?这个类比很形象!它们各自有什么特点?
Lewis:我们来看它们的特点。稀疏 embeddings 通常维度非常高,可能达到数万维,但大多数值都是 0。每个维度都有清晰含义,例如对应某个具体词。由于稀疏 embeddings 中大多数值都是 0,因此它们的存储和计算非常高效,因为只需要处理非零值。相比之下,稠密 embeddings 的维度相对较小,通常是几百到几千维,而且每个维度都有值。不过,这些维度的含义更加抽象,表示模型学习到的特征。与稀疏 embeddings 不同,稠密 embeddings 需要存储和计算所有维度,因此对计算资源要求更高。
使用 BM25 实现稀疏 embeddings
BM25 可以被看作一种用于信息检索和搜索引擎的稀疏词汇表示方法。它的核心思想是基于词的重要性来匹配文档和查询。下面详细解释 BM25 如何实现稀疏 embedding。
首先,构建一个包含语料库中所有词的词表。词表中的每个词都对应向量中的一个维度。假设我们整理了游戏《Chronicle of Godslaying: Sun Wukong》中孙悟空的战斗日志,那么词表就相当于日志中出现过的“战斗术语集合”。
Divine weapon
Monster
Flame Fist
所有词都会形成一个大型词表,每个术语都会被分配一个唯一索引,也就是向量中的一维。例如,如果词表大小为 10000,而 “Wukong” 在词表中的索引是 103,那么它就对应稀疏向量的第 103 维。
对于每个词,BM25 会基于公式计算其权重。
其中 TF,也就是 Term Frequency,指的是该词在文档中出现的次数。例如,“Flame Fist” 出现了三次。
IDF,也就是 Inverse Document Frequency,用于衡量某个词的全局重要性;稀有词具有更高权重。例如,Diamond Body 可能出现在大多数日志中,因此权重较低,而 Destructive Roar 出现得较少,因此权重更高。IDF 的计算如下。
k 和 b 是超参数,分别控制词频对权重的影响,以及文档长度归一化的程度。
基于词表生成稀疏向量,其维度等于词表大小。对于一个文档或查询,只有出现过的词会被赋予权重,而未出现词对应的维度为 0。
下面的 Python 代码示例演示了如何使用 BM25 分析和检索孙悟空的战斗日志。
from collections import Counter
import math
## 孙悟空的战斗日志
battle_logs = [
"Sun Wukong uses Flame Fist to repel the monster; then activates Diamond Body to block attacks from divine weapons.",
"The monster uses an Ice Arrow to attack Sun Wukong, but is countered and defeated by the Flame Fist.",
"Sun Wukong summons Flame Fist and Destructive Roar to defeat the monster and then collects the monster's essence."
]
## 超参数
k1 = 1.5
b = 0.75
## 构建词表
vocabulary = set(word for log in battle_logs for word in log.split(","))
vocab_to_idx = {word: idx for idx, word in enumerate(vocabulary)}
## 计算 IDF
N = len(battle_logs)
df = Counter(word for log in battle_logs for word in set(log.split(",")))
idf = {word: math.log((N - df[word] + 0.5) / (df[word] + 0.5) + 1) for word in vocabulary}
## 日志长度信息
avg_log_len = sum(len(log.split(",")) for log in battle_logs) / N
## BM25 稀疏信息嵌入
def bm25_sparse_embedding(log):
tf = Counter(log.split(","))
log_len = len(log.split(","))
embedding = {}
for word, freq in tf.items():
if word in vocabulary:
idx = vocab_to_idx[word]
score = idf[word] * (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * log_len / avg_log_len))
embedding[idx] = score
return embedding
## 生成稀疏向量
for log in battle_logs:
sparse_embedding = bm25_sparse_embedding(log)
print(f"Sparse embedding: {sparse_embedding}")
输出如下:
Sparse Embedding: {0: 0.9285957424963089, 2: 0.9285957424963089, 5: 0.9285957424963089}
BM25 的亮点在于其高效的存储方式。在存储齐天大圣的战斗日志时,不需要保存所有维度的权重。如果只有 5 个术语具有非零权重值,那么只需要存储这些信息。每个非零维度都清晰对应某个具体战斗术语;例如,Blazing Fist 的得分是基于其出现频率和重要性计算得到的。当你需要检索所有使用 Diamond Body 阻挡攻击的日志时,只需要计算查询和日志稀疏向量之间的点积即可。这种方法不仅速度快,而且能保证结果准确。
通过稀疏 embeddings 高效表示和检索关键战斗信息,BM25 结合了战斗术语的局部频率和全局稀有性。这既保证了存储效率,也能够基于局部频率和全局稀有性捕捉关键词重要性。
使用 BGE-M3 进行混合检索
理解了稀疏和稠密 embeddings 的概念之后,我们来介绍 BGE-M3 模型。这是北京智源人工智能研究院,也就是 BAAI,开发的开源文本 embedding 模型。BGE-M3 可以通过 Hugging Face 和 FlagEmbedding 包使用。BGE-M3 模型通过自知识蒸馏提升性能,使其在多种检索任务中表现出色。此外,该模型采用优化后的批处理策略和缓存机制,可以高效处理长文本,满足长文档检索需求。BGE-M3 模型旨在支持多语言、多功能和多粒度的文本检索与表示任务。
为什么叫 M3?因为 BGE-M3 模型主要有 3 个特点:
Multi-functionality,多功能性:BGE-M3 模型集成了三种检索能力:稠密检索、稀疏检索和多向量检索,可以灵活满足不同检索需求。
Multi-linguality,多语言性:BGE-M3 模型支持超过 100 种语言,提供强大的多语言和跨语言检索能力。
Multi-granularity,多粒度:BGE-M3 模型可以处理从短语到 8192 token 长文档的文本,满足不同长度文本的处理需求。
BGE-M3 如何生成稀疏和稠密 embeddings
BGE-M3 模型的独特之处在于,它的输出接口可以同时返回稀疏向量和稠密向量。稀疏 embeddings 主要用于捕捉关键词特征,而稠密 embeddings 用于捕捉语义特征。此外,BGE-M3 模型还提供了结合稀疏和稠密 embeddings 进行混合重排序的功能,可以进一步提升检索结果的准确性和相关性。
要使用 BGE-M3 模型,应先安装 BAAI 的 FlagEmbedding 包。
pip install flagembedding
接下来,初始化模型并输入文本。
from FlagEmbedding import BGEM3FlagModel
model = BGEM3FlagModel("BAAI/bge-m3", use_fp16=False)
passage = ["The monkey uses the Blazing Flame Fist to drive away the monster; then activates the Vajra Body to resist attacks from divine weapons."]
通过设置 return_sparse、return_dense 和 return_colbert_vecs 等参数,可以从模型中获得不同类型的向量表示。关于 ColBERT 的多向量重排序机制,更多细节见第 7 章。
### 编码文本并获得稀疏和稠密 embeddings
passage_embeddings = model.encode(
passage,
return_sparse=True, # 返回稀疏 embeddings
return_dense=True, # 返回稠密 embeddings
return_colbert_vecs=True # 返回多向量 embeddings
)
### 分别提取稀疏 embeddings、稠密 embeddings 和多向量 embeddings
dense_vecs = passage_embeddings["dense_vecs"]
sparse_vecs = passage_embeddings["lexical_weights"]
colbert_vecs = passage_embeddings["colbert_vecs"]
### 展示稀疏和稠密 embeddings 示例
print(dense_vecs[0][:10]) # 只显示前 10 个维度
print(list(sparse_vecs[0].items())[:10]) # 只显示前 10 个非零值
print(colbert_vecs[0][:2]) # 只显示前两个多向量 embeddings
现在,我们已经获得了多种类型的向量表示。相关计算过程和这些向量本身这里不再展开。在第 4.6 节中,我们将演示如何使用混合 embedding 模型构建混合检索系统,使它既能够精确匹配关键词,又能够理解查询的语义意图。
多模态 embedding 模型 Visualized_BGE
Anna:Lewis,单一模态的 embedding 模型,例如文本或图像,已经相对成熟。不过,在我们的实际应用场景中,往往需要同时处理文本、图像,甚至音频和视频等多模态数据,以实现更丰富和更灵活的功能。
Lewis:多模态 embedding 模型正是为满足这类需求而出现的。它们可以将不同模态的数据映射到同一个向量空间中,从而支持跨模态检索、跨模态生成和多模态分析。
一个代表性工作是 BAAI 的 Visualized_BGE 模型,这是基于 BGE 模型推出的视觉化版本,用来满足多模态处理需求。Visualized_BGE 模型主要关注图像和文本模态的联合处理。通过衡量同一 embedding 空间中图像向量和文本向量之间的相似度,该模型实现了跨模态检索和图文搜索。例如,学习者可以基于文本描述检索最相关的图像,或者根据图像找到最匹配的文本描述。
Visualized_BGE 模型的技术架构主要包括以下模块:
视觉编码器:通常基于 ViT 或其他深度卷积网络,用于提取图像特征。通过将输入图像切分为 patches,或者使用卷积操作,模型可以提取多层次视觉信息,最终生成图像的全局向量表示。
文本编码器:基于 BERT 或 RoBERTa 等大模型架构,用于提取文本语义。对于输入文本序列,模型会输出一个表示整体语义的向量。
对比学习:模型使用对比学习方法进行训练,使相关,也就是匹配的图文对,在向量空间中更接近;而不相关,也就是不匹配的图文对,则距离更远,从而实现跨模态对齐和检索能力。
Visualized_BGE 模型目前处于研究阶段。建议访问 FlagEmbedding 官方 GitHub 仓库,获取最新安装说明。目前安装方式如下。完整代码可参考 github.com/PacktPublis…
git clone https://github.com/FlagOpen/FlagEmbedding.git
cd FlagEmbedding/research/visual_bge
pip install -e .
下面的代码示例演示了如何使用 FlagEmbedding 导入并可视化一个多模态 embedding。
from visual_bge.modeling import Visualized_BGE
model = Visualized_BGE(
model_name_bge="BAAI/bge-base-en-v1.5",
model_weight="path/to/weights"
)
### 图文联合编码
embedding = model.encode(image="image.jpg", text="description")
### 仅图像编码
img_embedding = model.encode(image="image.jpg")
### 仅文本编码
text_embedding = model.encode(text="description")
这里,我们只执行了图像和文本的联合编码、向量提取和表示。要实现完整的图像相似度检索或跨模态检索功能,通常还需要结合向量数据库,例如 Milvus,或者其他高效相似度检索工具。
在 LangChain 和 LlamaIndex 等框架中使用 embedding 模型
在前面的示例中,我们要么直接调用商业 embedding 模型 API,要么通过 Hugging Face 平台下载开源 embedding 模型。本节将展示如何使用 LangChain 和 LlamaIndex 等主流应用框架加载和使用 embedding 模型。这种方式的一个显著优势是,开发者可以在统一开发框架内无缝切换不同 embedding 模型。开发者不需要为各种 embedding 模型重复编写适配代码,而可以更专注于业务逻辑实现。此外,这些框架提供的缓存机制可以避免重复 embedding 计算,大幅提升检索和查询效率。
LangChain 中的 embedding 接口
LangChain 提供了多种组件和工具,可以方便地将大语言模型集成到应用中。它也为 embeddings 提供了设计良好的抽象,使开发者可以轻松在不同 embedding 模型之间切换,包括 OpenAI Embeddings、Hugging Face Embeddings 和本地模型。
LangChain 中常见的 embedding 类如下:
OpenAIEmbeddings:用于调用 OpenAI 提供的文本 embedding API。
HuggingFaceEmbeddings:用于在本地或远程调用 Hugging Face 模型。
SelfHostedPipelineEmbeddings:用于调用自定义部署的 Pipelines。
CacheBackedEmbeddings:可以将任何 embedding 模型“包装”为带缓存支持的版本,以避免重复计算 embeddings。
下面的代码示例演示了如何使用 OpenAIEmbeddings 对文本编码,并查看返回向量的维度或数值。完整代码可参考 github.com/PacktPublis…
from langchain.embeddings import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small", # 可以指定其他 OpenAI 模型
)
text = "This is a sample sentence for embedding."
text_vector = embeddings.embed_query(text)
print(len(text_vector))
print(text_vector[:10]) # 查看向量的前 10 个元素
传入一段文本之后,OpenAIEmbeddings 会返回一个浮点数列表,作为编码后的向量,例如 1536 维。这些向量可以存储到向量数据库中,例如 Faiss、Milvus 等,用于相似度搜索。
如果你想切换到另一个 embedding 模型,只需要做如下调整:
from langchain.embeddings import HuggingFaceEmbeddings
embed_model = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2" # 可以指定任意 Hugging Face 模型
)
text = "This is a sample sentence for embedding."
text_vector = embed_model.embed_query(text)
print(len(text_vector))
print(text_vector[:10]) # 查看向量的前 10 个元素
LlamaIndex 中的 embedding 接口
与 LangChain 类似,LlamaIndex 也提供了统一的 embedding 接口,使开发者可以根据需要轻松切换不同 embedding 模型。OpenAI 等 embedding 模型的使用前面已经描述过,这里不再重复。下面是通过 Settings 设置全局 embedding 模型的代码示例。
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex, Settings
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
## 设置全局 embedding 模型
Settings.embed_model = HuggingFaceEmbedding(model_name="BAAI/bge-small-zh")
## 加载文档
documents = SimpleDirectoryReader("data/Chronicles of Godslaying").load_data()
## 创建索引,会自动使用 Settings 中设置的 embedding 模型
index = VectorStoreIndex.from_documents(documents)
## 创建查询引擎
query_engine = index.as_query_engine()
response = query_engine.query("Please briefly summarize the content of the file")
print(response)
类似地,你也可以使用 Settings.llm 设置全局大语言模型。
通过 LangChain cache 进行 embedding 缓存
在处理大量文本,或者对同一批文本进行频繁重复查询时,每次都重新计算 embeddings 会消耗大量算力和时间。为了解决这个问题,LangChain 提供了 CacheBackedEmbeddings 功能,允许将 embedding 结果持久化存储在本地文件系统或其他键值数据库中。当再次对相同文本进行编码时,框架会直接获取缓存结果,而不是重新计算。
LangChain 使用输入文本的 hash 作为 key,对应的 embedding 作为 value,并将它们存储在指定的 ByteStore 中。下一次计算相同文本的 embedding 时,会先检查缓存是否存在;如果存在,就直接返回缓存结果。如果不存在,才会调用底层 embedding 模型生成。
下面的代码示例演示了如何使用 LocalFileStore 作为文件系统缓存,并结合 Faiss 进行 embedding 检索。
from langchain.storage import LocalFileStore
from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import FAISS
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_text_splitters import CharacterTextSplitter
### 初始化 embedding 模型
embed_model = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh")
### 初始化本地缓存存储路径
store = LocalFileStore("./cache/")
### 使用 CacheBackedEmbeddings.from_bytes_store 创建包装器
from langchain.embeddings import CacheBackedEmbeddings
cached_embedder = CacheBackedEmbeddings.from_bytes_store(
underlying_embeddings=embed_model,
document_embedding_cache=store, # 用于缓存文档向量
namespace=embed_model.model_name # 防止不同模型之间发生冲突
)
### 准备测试文本并切分
raw_documents = TextLoader("state_of_the_union.txt").load()
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
documents = text_splitter.split_documents(raw_documents)
### 构建 Faiss embedding 检索库;第一次运行需要实际计算 embeddings
db = FAISS.from_documents(documents, cached_embedder)
### 下一次运行使用缓存结果,处理速度显著提升
db2 = FAISS.from_documents(documents, cached_embedder)
第一次构建 embedding 检索数据库可能需要较长时间,因为它确实需要调用 OpenAI 的 embedding 模型为所有文本 chunks 计算 embeddings。然而,当第二次执行相同操作时,embeddings 可以从本地缓存中读取,从而显著减少运行时间。
当然,你也可以选择其他缓存存储机制,例如 InMemory、Redis 等。如果你的应用只需要在程序执行期间缓存数据,并且不会跨进程或跨机器使用,就可以使用内存缓存。
from langchain.storage import InMemoryByteStore
store = InMemoryByteStore()
cached_embedder = CacheBackedEmbeddings.from_bytes_store(
underlying_embeddings,
store,
namespace=underlying_embeddings.model
)
对于 Redis、SQLite、云存储等更稳健的持久化存储,只需要实现或使用对应的 ByteStore 接口,就可以将 embeddings 缓存到你选择的存储介质中。这样可以满足大规模分布式部署或跨团队共享的需求。
微调 embedding 模型
在本节中,我们将讨论如何微调 embedding 模型。
微调是指调整预训练模型以适配特定任务的过程。它的核心在于确保任务和现有数据之间存在紧密联系。重要的是,在开始微调之前,必须明确是否真的需要微调。如果任务目标不清晰,或者与可用数据特征不匹配,微调可能会浪费资源,甚至产生负面效果。例如,如果目标只是让大模型回答几个专业问题,而现有开源模型已经能够给出令人满意的答案,那么就没有必要微调。
可以考虑微调的场景包括以下几类:
高度专业化任务:例如生成某个特定子领域的医疗报告。
特定格式要求:例如生成符合内部文档标准的答案。
本地化需求:例如为某种特定语言或文化语境生成内容。
总结来说,微调应该围绕实际任务和已有数据展开,避免“为了微调而微调”。
接下来,我们将以开源 embedding 模型 Stella 为例,介绍如何微调。首先,安装以下工具和库:Hugging Face 的 transformers 库、PyTorch,最好是 GPU 版本,以及用于数据加载的 Datasets 库。
pip install transformers datasets torch
Stella 模型有如下几个版本:
stella-400m:参数量适中,大约 4 亿参数,适合资源受限环境。
stella-1.5b:高性能版本,大约 15 亿参数,需要更多计算资源,至少需要一张 16GB 显存的 GPU。
下面的示例代码使用一个提前准备好的数据集,批量输入两段文本,分别生成 embedding 向量,并计算余弦相似度。然后,将计算结果与实际相似度标签进行比较,并使用梯度下降拟合模型参数。通过这种训练,模型可以让数据集中语义相似的文本对在 embedding 空间中距离更近。
首先,导入必要库。
from transformers import AutoModel, AutoTokenizer, TrainingArguments, Trainer
import torch
import torch.nn as nn
from datasets import Dataset, DatasetDict
示例数据以文本对形式存储,每一对代表两个需要计算相似度的句子。similarity 表示两段文本之间人工标注的相似度分数,范围从 0 到 1,用于监督训练。注意:实际微调所需的数据量会远大于这里展示的数据。
## 示例数据以文本对形式存在
train_data = [
{
"text1": "How is the gameplay of 'Chronicles of the God Slayer: Monkey'?",
"text2": "I want to know about the combat system of 'Chronicles of the God Slayer: Monkey'.",
"similarity": 0.8 # 相似度分数
},
{
"text1": "Are the visual details of 'Chronicles of the God Slayer: Monkey' good?",
"text2": "How is the open world design of this game?",
"similarity": 0.3 # 不太相似
}
]
val_data = [
{
"text1": "How is the combat experience of 'Chronicles of the God Slayer: Monkey'?",
"text2": "How is the game's fighting system?",
"similarity": 0.9
}
]
SentenceEmbeddingModel 类封装了一个基础预训练模型,例如 stella-400m-v5,用于生成句子 embeddings。两个文本都会输入基础模型,并提取 CLS token embedding 作为句子的向量表示。同时,使用 nn.CosineSimilarity 计算两个文本的余弦相似度,并与 similarity 标签计算均方误差,也就是 MSE loss,进行监督训练。
## 自定义模型类
class SentenceEmbeddingModel(nn.Module):
def __init__(self, base_model):
super().__init__()
self.model = base_model
def forward(self, input_ids1, attention_mask1, input_ids2, attention_mask2, similarity=None):
# 获取两段文本的 embeddings
outputs1 = self.model(input_ids=input_ids1, attention_mask=attention_mask1)
outputs2 = self.model(input_ids=input_ids2, attention_mask=attention_mask2)
### 使用 CLS Token 的 embedding 作为句子向量表示
embeddings1 = outputs1.last_hidden_state[:, 0]
embeddings2 = outputs2.last_hidden_state[:, 0]
### 计算余弦相似度
cos = nn.CosineSimilarity(dim=1)
pred_similarity = cos(embeddings1, embeddings2)
### 如果提供了相似度标签,则计算 MSE Loss
loss = None
if similarity is not None:
loss_fn = nn.MSELoss()
loss = loss_fn(pred_similarity, similarity)
return {"loss": loss, "predictions": pred_similarity} if loss is not None else pred_similarity
数据预处理函数会分别对两段文本,也就是 text1 和 text2,执行 tokenization、truncation 和 padding,使它们长度一致。然后返回 tokenization 结果,包括 input_ids 和 attention_mask,供模型使用。
### 数据预处理函数
def preprocess_function(examples):
## 分别编码两段文本
text1_encodings = tokenizer(
examples["text1"],
truncation=True,
padding="max_length",
max_length=128
)
text2_encodings = tokenizer(
examples["text2"],
truncation=True,
padding="max_length",
max_length=128
)
## 合并编码结果
return {
"input_ids1": text1_encodings["input_ids"],
"attention_mask1": text1_encodings["attention_mask"],
"input_ids2": text2_encodings["input_ids"],
"attention_mask2": text2_encodings["attention_mask"],
"similarity": examples["similarity"]
}
### 将数据转换成 Dataset
train_dataset = Dataset.from_list(train_data)
val_dataset = Dataset.from_list(val_data)
raw_datasets = DatasetDict({
"train": train_dataset,
"validation": val_dataset
})
接下来,加载 Hugging Face 预训练模型 stella_en_400M_v5 及其 tokenizer,并使用预训练模型初始化自定义的 SentenceEmbeddingModel。
## 加载预训练模型和 tokenizer
model_name = "NovaSearch/stella_en_400M_v5" # 可以替换为 stella-400m-v5 模型
tokenizer = AutoTokenizer.from_pretrained(model_name)
base_model = AutoModel.from_pretrained(model_name, trust_remote_code=True)
model = SentenceEmbeddingModel(base_model)
将 preprocess_function 应用于原始数据集,也就是 raw_datasets,进行预处理。处理后的数据集包含 tokenized input IDs、attention masks,以及 similarity 标签。
## 数据预处理
tokenized_datasets = raw_datasets.map(preprocess_function, batched=True)
通过 TrainingArguments 配置训练参数。主要参数说明见代码注释。
## 设置训练参数
training_args = TrainingArguments(
output_dir="./results", # 输出目录
evaluation_strategy="epoch", # 每个 epoch 结束时进行评估
learning_rate=2e-5, # 学习率
per_device_train_batch_size=8, # 训练 batch size
per_device_eval_batch_size=8, # 评估 batch size
num_train_epochs=3, # 训练 epoch 数
weight_decay=0.01, # 权重衰减
save_steps=10, # 每 10 步保存一次模型
logging_dir='./logs', # 日志目录
logging_steps=10, # 每 10 步记录一次日志
save_total_limit=2, # 最多保存两个 checkpoint
)
接下来,开始训练模型。使用 Trainer 类封装训练逻辑。模型是自定义的 SentenceEmbeddingModel,train_dataset 和 eval_dataset 是预处理后的训练集和验证集,training_args 是前面定义的训练参数。
## 定义 Trainer 类并开始训练
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["validation"],
)
## 开始训练
trainer.train()
输出如下:
100%|████████████████| 3/3 [00:02<00:00, 1.49it/s].
{'eval_runtime': 0.0412, 'eval_samples_per_second': 24.288, 'eval_steps_per_second': 24.288, 'epoch': 3.0}
{'train_runtime': 7.4859, 'train_samples_per_second': 0.802, 'train_steps_per_second': 0.401, 'train_loss': 0.04166938861211141, 'epoch': 3.0}
微调模型之后,可以保存模型参数和 tokenizer。
model.save_pretrained("./my_stella_model")
tokenizer.save_pretrained("./my_stella_model")
你可以像使用 HuggingFace 开源模型一样,使用这个模型生成微调后的 embedding 向量;也可以通过 LangChain 或 LlamaIndex 等框架提供的自定义 embedding 模型接口进行操作,具体请参考各框架文档。
通过微调 Stella 模型,可以生成更高质量的文本 embeddings,使语义相似的文本在 embedding 空间中更加接近。为了进一步提升模型性能,建议提供更多高质量训练数据,以增强模型泛化能力。
需要强调的是,现代大模型在各方面已经非常强大。只有当现有模型无法满足任务需求时,才应该考虑微调。
总结
Embedding 是大模型的核心技术之一。Embedding 指的是对外部信息进行编码的过程。没有 embedding 技术,大模型就无法感知世界、理解知识或处理知识。为了管理非结构化数据的复杂性,我们应用 embedding 技术将数据转换为数值向量,以捕捉其基本特征。
本节从 embedding 技术的起源到最新发展,覆盖了 embedding 技术的多个方面,并提供了许多具体实践示例,帮助学习者理解和练习。
目前,文本 embedding 技术已经非常成熟。然而,多模态非结构化数据,例如图像、音频和视频,格式多样,并承载着丰富的底层语义。
未来,embedding 技术的重点将放在多模态的协同发展上,例如文本、图像、音频 / 视频等。通过跨模态 embedding 技术,可以更有效地捕捉不同数据类型之间的关联性和共性,从而增强模型理解复杂真实世界场景的能力。