语义相似度是嵌入模型设计用来度量的指标,但这些度量会受到许多偏差因素的影响。在本文中,我们将探讨文本嵌入模型中一个普遍存在的偏差来源:输入文本的长度。
当与其他文本嵌入进行比较时,较长文本的嵌入通常会显示更高的相似度分数,这与实际内容的相似程度无关。虽然真正相似的文本仍然会有较高的相似度分数,但较长的文本会引入偏差——仅仅因为其长度就使其嵌入在平均上看起来更相似。
这会产生实际影响。这意味着嵌入模型本身并不能很好地衡量相关性。在基于嵌入的搜索中,总是存在最佳匹配,但由于长度偏差,你无法使用相似度分数来判断最佳匹配或任何次优匹配是否真正相关。你不能说,例如,任何余弦相似度高于 0.75 的匹配都是相关的,因为可能会有一个长文档在完全不相关的情况下也能达到这个水平。
我们将通过一些简单的例子来演示这一点,并向你展示为什么余弦相似度不能作为评估文本嵌入的通用方法
tag可视化长度偏差
为了展示长度偏差是如何表现的,我们将使用 Jina AI 最新的嵌入模型 jina-embeddings-v3,并选择 text-matching 任务选项。我们还将使用来自一个广泛使用的 IR 数据集:CISI 数据集,你可以从 Kaggle 下载。

这个数据集用于训练 IR 系统,所以它包含查询和用于匹配的文档。我们只会使用文档,它们都在 CISI.ALL 文件中。你可以从命令行通过 GitHub 上的另一个来源下载,使用以下命令:
wget https://raw.githubusercontent.com/GianRomani/CISI-project-MLOps/refs/heads/main/CISI.ALL
CISI 包含 1,460 个文档。下面的表格和直方图总结了文本大小及其分布的基本统计数据:
| 词数 | 句子数 | |
|---|---|---|
| 平均文档大小 | 119.2 | 4.34 |
| 标准差 | 63.3 | 2.7 |
| 最大大小 | 550 | 38 |
| 最小大小 | 8 | 1 |


让我们在 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 网站获取 API 密钥。你可以获得一个免费密钥,最多可用于 100 万个 token 的嵌入,这对本文来说已经足够了。
将你的密钥放在一个变量中:
api_key = "<Your Key>"
现在,使用 jina-embeddings-v3 的 text-matching 任务生成嵌入。此代码以每批 10 个的方式处理 docs 中的文本。
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"Got {len(embeddings)}...")
data = {
"model": "jina-embeddings-v3",
"task": "text-matching",
"late_chunking": False,
"dimensions": 1024,
"embedding_type": "float",
"input": docs[i:i+10]
}
response = requests.post(url, headers=headers, data=json.dumps(data))
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):
return float((a @ b.T) / (norm(a)*norm(b)))
然后,计算 1,460 个嵌入中的每一个与其他 1,459 个的余弦相似度:
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 个值的列表。它们的分布应该近似于相同语言和语域中"随机"文档之间的余弦相似度。下表和直方图总结了结果。
| 文本数量 | 1,460 |
|---|---|
| 余弦相似度数量 | 2,130,140 |
| 平均值 | 0.343 |
| 标准差 | 0.116 |

这些文档,即使彼此不相关,其余弦相似度通常也远高于零。我们可能会倾向于设置一个阈值 0.459(平均值加一个标准差),或者也许四舍五入到 0.5,并说任何余弦相似度小于该值的文档对大体上是不相关的。
但让我们在较短的文本上做同样的实验。我们将使用 nltk 库将每个文档分成句子:
import nltk
sentences = []
for doc in docs:
sentences.extend(nltk.sent_tokenize(doc))
这产生了 6,331 个句子,平均长度为 27.5 个单词,标准差为 16.6。在下面的直方图中,句子的大小分布用红色表示,完整文档的分布用蓝色表示,这样你可以比较它们。

我们将使用相同的模型和方法来获取每个句子的嵌入:
sentence_embeddings = []
i = 0
while i < len(sentences):
print(f"Got {len(sentence_embeddings)}...")
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
然后计算每个句子的 embedding 与其他每个句子的余弦相似度:
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))
结果得到了更多的余弦值:40,075,230,如下表所示:
| Number of sentences | 6,331 |
|---|---|
| Number of cosines | 40,075,230 |
| Average | 0.254 |
| Std. Deviation | 0.116 |
句子之间的余弦值平均来说比完整文档之间的余弦值要低得多。下面的直方图比较了它们的分布,你可以清楚地看到句子对形成了与文档对几乎相同但向左偏移的分布。

为了验证这种大小依赖性是否稳定,让我们获取句子和文档之间的所有余弦值,并将它们添加到直方图中。它们的信息总结在下表中:
| Number of texts | 6,331 sentences & 1,460 documents |
|---|---|
| Number of cosines | 9,243,260 |
| Average | 0.276 |
| Std. Deviation | 0.119 |
下图中的绿线是句子到文档余弦值的分布。我们可以看到,这个分布恰好位于文档间余弦值和句子间余弦值之间,表明大小效应同时涉及被比较的两个文本的大小。

让我们通过将文档按十个一组连接起来进行另一个测试,创建 146 个更大的文档并测量它们的余弦值。结果总结如下:
| Number of texts | 146 documents |
|---|---|
| Number of cosines | 21,170 |
| Average | 0.658 |
| Std. Deviation | 0.09 |

这远在其他分布的右侧。0.5 的余弦阈值会告诉我们,几乎所有这些文档都是相关的。要排除这种大小的不相关文档,我们必须将阈值设置得更高,可能高达 0.9,这无疑会排除较小文档之间的良好匹配。
这表明我们根本不能使用最小余弦阈值来估计匹配的好坏程度,至少在不考虑文档大小的情况下是不行的。
tag是什么导致了大小偏差?
embedding 中的大小偏差与长上下文模型中的位置偏差不同。它不是由架构造成的。它本质上也不是关于大小的。例如,如果我们通过反复连接同一文档的副本来创建较长的文档,它就不会显示大小偏差。
问题在于长文本说了更多的内容。即使它们受到主题和目的的限制,写更多的词的全部目的就是说更多的内容。
较长的文本,至少是人们通常创建的那种,自然会产生在语义空间中更"分散"的 embedding。如果一个文本说了更多的东西,它的 embedding 与其他向量的平均角度会更小,这与文本的主题无关。
tag测量相关性
这篇文章的教训是,你不能仅仅用语义向量之间的余弦值来判断某个东西是否是好的匹配,只能判断它是否是可用选项中最好的匹配。除了计算余弦值之外,你还必须做一些其他的事情来检查最佳匹配的实用性和有效性。
你可以尝试归一化。如果你能够经验性地测量大小偏差,可能可以抵消它。然而,这种方法可能不太稳健。适用于一个数据集的方法可能不适用于另一个数据集。
jina-embeddings-v3 提供的非对称查询-文档编码减少了 embedding 模型中的大小偏差,但并没有消除它。非对称编码的目的是将文档编码得不那么"分散",而将查询编码得更分散。
下面直方图中的红线是使用非对称编码的文档间余弦值分布 —— 每个文档都使用 retrieval.query 和 retrieval.passage 标志进行编码,每个文档查询 embedding 都与不是来自同一文档的每个文档段落 embedding 进行比较。平均余弦值为 0.200,标准差为 0.124。
这些余弦值比我们上面使用 text-matching 标志对同样的文档找到的余弦值要小得多,如下面的直方图所示。

然而,非对称编码并没有消除大小偏差。下面的直方图比较了使用非对称编码的完整文档和句子的余弦值。

句子余弦值的平均值是 0.124,所以使用非对称编码时,句子平均余弦值和文档平均余弦值之间的差异是 0.076。对于对称编码,平均值的差异是 0.089。大小偏差的变化并不显著。
虽然非对称编码改进了信息检索的 embedding,但在测量匹配的相关性方面并没有更好。
tag未来的可能性
重排序器方法,例如 jina-reranker-v2-base-multilingual 和 jina-reranker-m0,是对查询-文档匹配进行评分的另一种方式,我们已经知道它提高了查询精度。重排序器分数没有归一化,所以它们也不能作为客观的相似度度量。然而,它们的计算方式不同,可能可以通过某种方式将重排序器分数归一化,使其成为相关性的良好估计器。
另一种选择是使用大型语言模型,最好是具有强大推理能力的模型,直接评估候选项是否与查询匹配。简单来说,我们可以问一个针对特定任务的大型语言模型:"在 1 到 10 的范围内,这个文档与这个查询的匹配程度如何?"现有的模型可能不太适合这个任务,但有针对性的训练和更复杂的提示技术是有希望的。
模型测量相关性并非不可能,但它需要与 embedding 模型不同的范式。
tag让模型做它擅长的事
我们上面记录的大小偏差效应显示了 embedding 模型的一个基本限制:它们擅长比较事物,但在测量绝对相关性方面不可靠。这个限制不是设计上的缺陷 —— 这是这些模型工作方式的固有特征。
那么这对你意味着什么?
首先,对余弦阈值持怀疑态度。它们就是不起作用。余弦相似度度量产生看起来很客观的浮点数。但仅仅因为某个东西输出数字并不意味着它在客观地测量某些东西。
第二,考虑混合解决方案。embedding 可以有效地将大量项目缩小到有希望的候选项,之后你可以应用更复杂(和计算密集)的技术,如重排序器或 LLM,甚至人工评估员来确定实际相关性。
第三,在设计系统时,要从任务而不是能力的角度思考。在基准测试中得分最高、客观上最聪明的模型,如果不能完成你购买它的工作,仍然是在浪费钱。
理解我们模型的局限性并不是悲观 —— 它反映了应用中的一个更广泛的原则:理解你的模型擅长什么,不擅长什么,对于构建可靠和有效的系统至关重要。就像我们不会用锤子来拧螺丝一样,我们也不应该让 embedding 模型去处理它们无法处理的任务。尊重你的工具所擅长的领域。







