微信扫码
添加专属顾问
我要投稿
长度偏差如何影响文本向量模型的搜索结果?本文深入解析,并提供实际数据集演示。 核心内容: 1. 文本向量模型中的常见偏差:长度偏差 2. 长度偏差对搜索结果的影响和示例分析 3. 实际数据集CISI的可视化演示和代码实现
向量模型的核心功能是测量语义相似度,但这个测量结果很容易受到多种干扰因素的影响。在本文中,我们将着眼于文本向量模型中一个普遍存在的偏差来源:输入内容的长度。
通常情况下,当与其它文本向量进行比较时,长文本向量往往得分更高,哪怕它们的实际内容没那么相似。当然,内容真正相似的文本,得分还是会比不相关的文本高。但是,长文本本身就会带来一种偏差:仅仅因为文本更长,它们的向量看起来(平均而言)就好像更相似。
这会在实际应用中带来问题。单靠向量模型本身,其实很难准确评估内容的“相关性”。在基于向量的搜索里,虽然总能找到一个“最佳匹配”的结果,但长度偏差的存在,导致我们无法单凭相似度分数,就判断这个“最佳匹配”或者其他得分较低的结果,内容是否真的相关。
比如,你不能简单地认为“余弦相似度高于 0.75 就代表内容相关”,因为一个完全不相关的长文档,可能仅仅因为够长,相似度得分就能达到这个水平。
? 要点: 比较向量只能反映相对相似度,无法直接判断相关性。
下面,我们会用一些简单的例子演示这个问题,并说明为什么不能把文本 embedding 之间的余弦相似度,当作评估相关性的通用标准。
为了具体展示长度偏差的影响,我们将使用 Jina AI 最新的文本向量模型 jina-embeddings-v3
,并指定采用 text-matching
选项。我们还会用到一个信息检索领域广泛使用的数据集:CISI 数据集中的文本文档,你可以从 Kaggle 下载这个数据集。
数据集链接:https://www.kaggle.com/datasets/dmaso01dsta/cisi-a-dataset-for-information-retrieval
这个数据集经常被用来训练信息检索系统,所以里面既有查询 (queries),也有用来匹配的文档 (documents)。本文只用到其中的文档,它们都在 CISI.ALL 这个文件里。你可以用下面的命令下载它:
wget https://raw.githubusercontent.com/GianRomani/CISI-project-MLOps/refs/heads/main/CISI.ALL
CISI 包含 1,460 个文档。下面的表格和直方图总结了文本大小及其分布的基本统计数据:
接下来,我们用 Python 读取这些文档,并生成它们的向量。下面的代码假定 CISI.ALL 文件位于当前目录下:
with open("CISI.ALL", "r", encoding="utf-8") as inp:
cisi_raw = inp.readlines()
docs = []
current_doc = ""
in_text = False
for line in cisi_raw:
if line.startswith("."):
in_text = False
if current_doc:
docs.append(current_doc.strip())
current_doc = ""
if line.startswith(".W"):
in_text = True
else:
if in_text:
current_doc += line
执行这段代码后,docs 列表中会包含 1,460 个文档。你可以查看一下文档内容:
print(docs[0])
# 输出示例
The present study is a history of the DEWEY Decimal
Classification. The first edition of the DDC was published
in 1876, the eighteenth edition in 1971, and future editions
will continue to appear as needed. In spite of the DDC's
long and healthy life, however, its full story has never
been told. There have been biographies of Dewey
that briefly describe his system, but this is the first
attempt to provide a detailed history of the work that
more than any other has spurred the growth of
librarianship in this country and abroad.
现在,我们来为每个文本构建向量,这里使用 jina-embeddings-v3
模型。为此,你需要先从 Jina AI 官网(https://jina.ai/embeddings/#apiform)获取一个 API 密钥。我们提供免费额度,可以处理 100 万 token,足够完成本文的实验了。
将你的密钥放在一个变量中:
api_key = "<Your Key>"
接下来,调用 jina-embeddings-v3
模型,并指定 text-matching
任务来生成向量。下面的代码每次处理 docs 列表中的 10 个文本。
import requests
import json
from numpy import array
embeddings = []
url = "https://api.jina.ai/v1/embeddings"
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer " + api_key
}
i = 0
while i < len(docs):
print(f"已获取 {len(embeddings)} 个 embedding...") # 打印进度信息
data = {
"model": "jina-embeddings-v3",
"task": "text-matching",
"late_chunking": False, # 不启用延迟分块
"dimensions": 1024, # 指定 embedding 维度
"embedding_type": "float", # 指定 embedding 类型
"input": docs[i:i+10] # 输入当前批次的文本
}
response = requests.post(url, headers=headers, data=json.dumps(data))
# 解析响应,并将 embedding 添加到列表中
for emb in response.json()['data']:
embeddings.append(array(emb['embedding']))
i += 10
这样,embeddings
列表里就存好了每个文本对应的 1024 维向量。我们可以看一下其中一个向量的样子:
print(embeddings[0])
# 输出示例
array([ 0.0352382 , -0.00594871, 0.03808545, ..., -0.01147173,
-0.01710563, 0.01109511], shape=(1024,))),
现在,我们计算这些向量两两之间的余弦相似度。首先,用 numpy
定义余弦函数 cos_sim
:
from numpy import dot
from numpy.linalg import norm
def cos_sim(a, b):
# 计算 a 和 b 的点积,再除以它们各自范数的乘积
return float((a @ b.T) / (norm(a)*norm(b)))
然后计算这 1,460 个 embedding 两两之间的余弦相似度(不与自身比较):
all_cosines = []
for i, emb1 in enumerate(embeddings):
for j, emb2 in enumerate(embeddings):
if i != j: # 避免与自身比较
all_cosines.append(cos_sim(emb1, emb2))
结果会得到一个包含 2,130,140 个余弦相似度值的列表。这个列表值的分布,可以近似看作同一语言和写作风格下,“随机”抽取的文档之间余弦相似度的分布情况。下表和直方图总结了这些计算结果。
我们看到,即使这些文档内容上没什么关联,它们之间的余弦相似度通常也明显大于零。这样一来,我们可能就想设一个阈值,比如 0.459(平均值 + 1 倍标准差),或者干脆四舍五入到 0.5,然后规定:任何相似度低于这个值的文档对,内容就基本不相关。
不过,我们换成用更短的文本(也就是句子)来做同样的实验。我们用 nltk 库把每个文档拆成句子:
import nltk
sentences = []
for doc in docs:
sentences.extend(nltk.sent_tokenize(doc))
这样我们得到了 6,331 个句子,平均长度为 27.5 个词,标准差为 16.6。在下面的直方图(图 6)中,红色代表句子的长度分布,蓝色代表完整文档的长度分布,方便大家比较。
我们还是用相同的模型和方法,给每个句子生成向量:
sentence_embeddings = []
i = 0
while i < len(sentences):
print(f"已获取 {len(sentence_embeddings)} 个句子 embedding...") # 打印进度
data = {
"model": "jina-embeddings-v3",
"task": "text-matching",
"late_chunking": False,
"dimensions": 1024,
"embedding_type": "float",
"input": sentences[i:i+10] # 输入当前批次的句子
}
response = requests.post(url, headers=headers, data=json.dumps(data))
for emb in response.json()['data']:
sentence_embeddings.append(array(emb['embedding']))
i += 10
然后计算向量两两之间的余弦相似度(不与自身比较):
sent_cosines = []
for i, emb1 in enumerate(sentence_embeddings):
for j, emb2 in enumerate(sentence_embeddings):
if i != j:
sent_cosines.append(cos_sim(emb1, emb2))
这次得到的余弦相似度值数量更多了,总计 40075230 个。结果总结在下表中:
很明显,句子之间的平均余弦相似度 (0.254) 比完整文档之间的平均值 (0.343) 低了不少。下面的直方图比较了这两种情况的分布。大家可以清楚地看到,句子间相似度的分布形状和文档间的几乎一样,只是整体向左平移了。
为了检验这种长度依赖性是否普遍存在,我们再计算所有句子与文档之间的余弦相似度,并把结果画在直方图里。相关数据总结在下表:
下图中的绿线代表句子与文档间余弦相似度的分布。我们能看到,这个分布正好夹在文档间(蓝色)和句子间(红色)的分布中间。这表明长度效应同时受到比较双方中较长文本和较短文本的影响。
我们再做个测试:把原始文档每十个拼接到一起,得到 146 个更长的文档,然后测量它们之间的余弦相似度。结果总结如下:
这个分布远远地跑到了其他分布的右边。如果用 0.5 作为余弦相似度阈值,那我们几乎会把所有这些长文档都判断为相关的。要想在这种长文档里排除掉不相关的,就必须把阈值设得非常高,比如 0.9,但这样做,肯定又会把短文档里很多好的匹配结果给错杀了。
这说明,我们根本没法用一个固定的最小余弦相似度阈值来评估匹配的好坏,至少得想办法把文档长度考虑进去才行。
向量中的长度偏差,不同于长上下文模型中的位置偏差。它不是模型架构造成的。说到底,这个偏差的根源其实也不是长度本身。举个例子,如果我们把同一份文档复制粘贴很多次来加长它,是不会出现这种长度偏差的。
问题在于,长文本承载的信息点更多。即使文章主题和目的明确,作者写更多字就是为了表达更多内容。
所以,较长的文本(至少是大家平时写的那种),自然会生成在语义空间上更“分散”的向量。一个文本表达的内容点越多,它的向量跟其他向量的平均夹角就越小(也就是余弦相似度更高),这跟文本具体讲什么主题关系不大。
这篇文章想告诉大家的是:我们不能只靠语义向量之间的余弦相似度,来判断一个结果是不是真的好,它最多只能告诉你哪个是现有选项里相对最像的。我们必须在计算余弦相似度之外,再做点别的,来检查找出的最佳匹配项到底有没有用、靠不靠谱。
一个思路是尝试归一化 (normalization)。如果我们能通过实验测出长度偏差具体有多大,也许就能设法消除它的影响。不过,这种方法可能不够通用。在一个数据集上管用的方法,换个数据集很可能就失灵了。
非对称查询-文档编码 (Asymmetric query-document encoding)(比如 jina-embeddings-v3
就提供了这个功能)有助于减少向量模型中的长度偏差,但不能完全消除。非对称编码的目的,是让文档向量在语义空间中不那么“分散”,而让查询向量更“分散”一些。
下面的直方图里,红线是用 jina-embeddings-v3
做非对称编码后,文档之间的余弦相似度分布。我们给每个文档分别用 retrieval.query
和 retrieval.passage
标记进行编码,然后拿每个文档的查询向量去跟所有其他文档的段落向量作比较。算出来的平均余弦相似度是 0.200,标准差是 0.124。
可以看到,这些相似度值比我们之前用 text-matching
算出来的(蓝色分布)要低很多。
但是,非对称编码并没有消除长度偏差。下面的直方图比较了用非对称编码时,完整文档之间(蓝色)和句子之间(红色)的余弦相似度分布。
句子间余弦相似度的平均值是 0.124。这样算下来,用非对称编码时,句子和文档的平均相似度差值是 0.076 (0.200 - 0.124)。而之前用对称编码(text-matching)时,这个差值是 0.089 (0.343 - 0.254)。可见,长度偏差几乎没什么变化。
所以说,虽然非对称编码能改进用于信息检索任务的向量效果,但在衡量匹配结果的相关性这方面,它并没有太多帮助。
重排器 (reranker) 方法,比如 jina-reranker-v2-base-multilingual
和 jina-reranker-m0
,是另一种给查询-文档匹配打分的方法,并且实践证明,它能提高查询精度。不过,重排器的分数没有经过归一化处理,所以也不能直接当作客观的相似度分数来用。但是,它们的计算方式不一样,也许可以通过某种方式将重排器得分归一化,让它变成一个评估相关性的好指标。
另一个可选方案是利用大语言模型 (LLM),最好是推理能力比较强的模型,来直接评估候选结果跟查询的匹配程度。简单来说,我们可以直接问一个针对性训练过的大模型:“从 1 到 10 分,这份文档跟这个查询匹配得怎么样?” 目前的大模型可能还不太擅长干这个,但通过专门的训练和更巧妙的提示词设计,这方面还是很有潜力的。
让大模型去衡量相关性并非天方夜谭,但这需要一套和向量模型不同的思路和方法。
我们上面详细讨论的长度偏差效应,说明了向量模型的一个根本局限:它们擅长比较相似性,但在衡量绝对的相关性上并不可靠。这个局限不是设计上有缺陷,而是这类模型工作原理的固有特性。
那这对我们实际应用有什么启示呢?
首先,我们要审慎看待固定的余弦相似度阈值。单纯依赖一个数值门槛来判断内容是否相关,往往是行不通的。余弦相似度虽然看起来客观,但很容易受到文本长度等因素的影响,并不能直接等同于我们通常理解的“内容相关”。
第二,考虑混合解决方案。Embeddings 技术进行高效的初步筛选,快速从大量候选项中找出“可能相关”的条目。然后再结合更精细(通常计算成本也更高)的方法,比如使用重排器 (Reranker)、大型语言模型 (LLM) 进行评估,或者引入人工审核,来进一步确认真正的相关性。
第三,设计系统时,重心应该放在最终要解决的任务上,而不是过度关注模型本身的技术指标。一个模型就算在各种基准测试中得分再高,如果但如果它无法胜任预期的工作,那么投入也是无效的。
最后,认识到模型的局限性,是一种务实的态度,这对于构建可靠、高效的系统至关重要。了解你所使用的工具的强项和弱项,才能更好地扬长避短。这就像我们不会用锤子去拧螺丝一样,我们也不应该让向量模型去承担它本身就不擅长的任务。尊重工具的适用范围,用好它的长处,用在合适的场景
53AI,企业落地大模型首选服务商
产品:场景落地咨询+大模型应用平台+行业解决方案
承诺:免费场景POC验证,效果验证后签署服务协议。零风险落地应用大模型,已交付160+中大型企业
2025-04-19
RAG升级-基于知识图谱+deepseek打造强大的个人知识库问答机器人
2025-04-19
RAG vs. CAG vs. Fine-Tuning:如何为你的大语言模型选择最合适的“脑力升级”?
2025-04-19
低代码 RAG 只是信息搬运工,Graph RAG 让 AI 具备垂直深度推理能力!
2025-04-18
微软PIKE-RAG全面解析:解锁工业级应用领域知识理解与推理
2025-04-18
AI 记忆不等于 RAG:对话式 AI 为何需要超越检索增强
2025-04-18
Firecrawl:颠覆传统爬虫的AI黑科技,如何为LLM时代赋能
2025-04-18
什么是RAG与为什么要RAG?
2025-04-18
Anthropic工程师揭秘高效AI Agent的三大秘诀
2024-10-27
2024-09-04
2024-07-18
2024-05-05
2024-06-20
2024-06-13
2024-07-09
2024-07-09
2024-05-19
2024-07-07
2025-04-19
2025-04-18
2025-04-16
2025-04-14
2025-04-13
2025-04-11
2025-04-09
2025-04-07