news 2026/10/9 2:22:29

维度砍一半,检索到底差多少:自测 + 独立信源对账

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
维度砍一半,检索到底差多少:自测 + 独立信源对账

版权与内容来源声明
本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容,均在附表 A 中标注来源;引用官方原文保持原样,不作改写。文中命令、版本号与界面截图以本文成文时的实测/核验结果为准,标注「待验证」的部分请以你本地环境实际输出为判断依据。本文不推荐任何不合规的软件获取方式,也不对任何收益结果作承诺。转载请注明出处。

同一批候选向量、同一个查询,只把打分函数从余弦换成点积,余弦排第一的那个候选,被点积排到了第三。索引没重建,向量没重新生成,改的只有一行打分函数。这件小事在向量检索里天天发生:向量长度没有被统一的时候,点积和余弦量的根本不是同一样东西,而很多「排序不对」的排查,就卡在这一步没有往下走。

本文要验证的问题很具体:向量维度砍一半,检索到底差多少?以及,网上流传的「相似度数值会掉、排序基本不变」这句话,能不能自己跑一遍对出来。

一、测试条件先统一:三组小实验,一次只动一个变量

三组实验都用合成向量跑,目的不是得到某个模型或某个索引的分数,而是把「长度」「归一化」「维度」三个变量拆开,一次只动一个。

先把场地钉死。三组实验共用同一套骨架:numpy 生成浮点向量,查询向量与候选向量同维,候选 200 条,看 top-10。区别只在每组动哪一个变量。

实验动的那一个变量固定不变看什么
实验 1(第 2 章)候选向量的长度(其中两条分别乘 0.2、乘 3,方向不变)维度、查询向量、候选条数点积与余弦排出的名次是否一致
实验 2(第 2 章)是否把所有向量归一化到单位长度同上归一化后两种打分是否合流
实验 3(第 3 章)向量维度(64 维 → 32 维)归一化状态、查询向量、候选条数top-10 重合、相似度数值变化、全量排序相关

表下口径:向量为合成随机数据,固定随机种子(实验 1/2 用 42,实验 3 用 7),不含真实语料的语义结构,也不含任何索引实现的近似误差。因此本文的结论是机制层面的,不能直接当作某个嵌入模型或某个向量库的评测结果。

1.1 三种距离各自量的是什么

余弦相似度(cosine similarity)量的是两个向量的夹角:它先各自除掉向量长度,只看方向对不对得上,取值在 −1 到 1 之间。点积(dot product)量的是方向一致程度再乘上两个向量的长度——方向一样时,向量越长,点积越大,所以它没有上界。L2 距离(欧氏距离)量的是两个点在空间里的直线距离,方向和相似度相反,距离越小越像。

一句话记住区别:余弦只看方向,点积同时看方向和长度。

1.2 归一化是这里唯一的开关

归一化(normalization)做的事很朴素:把每个向量整体缩放到单位长度,方向一点不动。它之所以关键,是因为只有所有向量长度都是 1 的时候,点积才等于余弦。少了这一步,你用点积就等于把向量的长度悄悄当成了一个权重——长度大的候选天然占便宜,而这通常不是你想要的效果。

代码里换个打分函数只要一秒,但「换了之后结果变没变」这件事,必须自己跑一遍才算数。


二、自测结果:向量一长,点积和余弦就排出两个名次

未归一化时,点积的名次被向量长度带偏了:余弦排第一的候选,点积把它排到了第三。

2.1 只让候选的身长差一点,方向全都不动

🧪 实测环境:Python 3.13.12 / macOS / numpy 2.5.3

importnumpyasnp np.set_printoptions(precision=4,suppress=True)rng=np.random.default_rng(42);d=8q=rng.normal(size=d);q/=np.linalg.norm(q)dirs=rng.normal(size=(4,d));dirs/=np.linalg.norm(dirs,axis=1,keepdims=True)C=dirs*np.array([1.0,0.2,1.0,3.0])[:,None]# 候选1缩到0.2倍、候选3放大3倍,方向不变order=lambdas:np.argsort(-s).tolist()dot_raw=C @ q cos_raw=(C @ q)/(np.linalg.norm(C,axis=1)*np.linalg.norm(q))print("未归一化 点积:",np.round(dot_raw,4),"排序",order(dot_raw))print("未归一化 余弦:",np.round(cos_raw,4),"排序",order(cos_raw))Cn=C/np.linalg.norm(C,axis=1,keepdims=True)dot_n=Cn @(q/np.linalg.norm(q))cos_n=(Cn @ q)/(np.linalg.norm(Cn,axis=1)*np.linalg.norm(q))print("归一化后 点积:",np.round(dot_n,4),"排序",order(dot_n))print("归一化后 余弦:",np.round(cos_n,4),"排序",order(cos_n))

真实输出(原样照贴,未作任何改动):

未归一化 点积: [0.1686 0.1128 0.0022 1.6039] 排序 [3, 0, 1, 2] 未归一化 余弦: [0.1686 0.5639 0.0022 0.5346] 排序 [1, 3, 0, 2] 归一化后 点积: [0.1686 0.5639 0.0022 0.5346] 排序 [1, 3, 0, 2] 归一化后 余弦: [0.1686 0.5639 0.0022 0.5346] 排序 [1, 3, 0, 2]

对着前两行看:候选 1 的余弦分 0.5639 是四个里最高的,但它被缩到 0.2 倍之后,点积分只剩 0.1128,名次从第一掉到第三;候选 3 的余弦分 0.5346 只排第二,被放大 3 倍后点积分 1.6039,直接顶到第一。长度一动,点积的名次就跟着动;余弦纹丝不动,因为它在算之前已经把长度除掉了。

2.2 归一化补齐后,两种打分合流

后两行是同一批向量归一化之后的结果:点积和余弦给出了完全相同的四个数值,排序也都是[1, 3, 0, 2]——和未归一化时的余弦排序一致。

这不是巧合,是恒等关系:单位向量的点积就等于夹角的余弦。所以「点积和余弦差不多」这句话,准确的说法是「归一化之后两者等价」。少了归一化这一步,它们量的是两个不同的量,排名不一致是必然,不是 bug。

这也解释了排查时的第一步该看什么:排序不对,先打一行日志看向量长度是不是 1。不查这一步就换模型、换索引、调参数,等于在错的分岔口上继续往前走。

三、一个反直觉现象:数值腰斩、整体排序看着没塌,top-10 却被换掉七成

维度从 64 维截断到 32 维,平均相似度从 0.7891 掉到 0.4316,掉了将近一半;但整个候选集排序的相关系数还有 0.9210,看上去「没怎么乱」——真正被改掉的是头部:top-10 只重合 3/10。

3.1 先把「数值」和「名次」分成两个指标量

🧪 实测环境:Python 3.13.12 / macOS / numpy 2.5.3

importnumpyasnp np.set_printoptions(precision=4,suppress=True)rng=np.random.default_rng(7)N,D,K=200,64,10c=rng.normal(size=D);c/=np.linalg.norm(c)# 公共主题方向 = 查询方向a=rng.uniform(0.05,1.5,size=N)# 每个候选离主题的远近不同X=c+(a/np.sqrt(D))[:,None]*rng.normal(size=(N,D))X*=np.array([1.0,0.2,1.0,3.0])[np.arange(N)%4][:,None]# 身长不一,同实测块 1q=c Xn=X/np.linalg.norm(X,axis=1,keepdims=True);qn=q/np.linalg.norm(q)full_cos=Xn @ qn h=D//2Xt=Xn[:,:h]trunc_cos=(Xt/np.linalg.norm(Xt,axis=1,keepdims=True))@(qn[:h]/np.linalg.norm(qn[:h]))trunc_dot=Xt @ qn[:h]P=rng.normal(size=(h,D))/np.sqrt(h)Xp=Xn @ P.T;qp=qn @ P.T proj_cos=(Xp/np.linalg.norm(Xp,axis=1,keepdims=True))@(qp/np.linalg.norm(qp))topk=lambdas,k=K:set(np.argsort(-s)[:k].tolist())spear=lambdax,y:np.corrcoef(np.argsort(np.argsort(x)),np.argsort(np.argsort(y)))[0,1]print("方案, top10重合, 平均相似度, 平均|Δ|, Spearman")print(f"基线 64D 余弦, 10/10,{full_cos.mean():.4f}, 0.0000, 1.0000")forname,sin[("截断32D+重归一化 余弦",trunc_cos),("截断32D 点积(未重归一化)",trunc_dot),("随机投影32D 余弦",proj_cos)]:print(f"{name},{len(topk(full_cos)&topk(s))}/{K},{s.mean():.4f}, "f"{np.abs(s-full_cos).mean():.4f},{spear(full_cos,s):.4f}")

真实输出:

方案, top10重合, 平均相似度, 平均|Δ|, Spearman 基线 64D 余弦, 10/10, 0.7891, 0.0000, 1.0000 截断32D+重归一化 余弦, 9/10, 0.7993, 0.0281, 0.9757 截断32D 点积(未重归一化), 3/10, 0.4316, 0.3575, 0.9210 随机投影32D 余弦, 9/10, 0.8009, 0.0372, 0.9579

3.2 数值掉得多不多,和名次乱不乱,是两件事

第三行就是「数值腰斩」那一行:截断到 32 维之后没有重新归一化,直接和已归一化的查询做点积,平均相似度从 0.7891 掉到 0.4316,平均绝对变化 0.3575。这一行的全量排序相关系数还有 0.9210,光看这个数会得出「排序基本没变」的结论;可它的 top-10 只重合 3/10——头部七成被换掉了,检索首屏已经不能用了。

第二行是把它补齐归一化之后的结果:数值几乎没变(0.7891 → 0.7993,平均绝对变化 0.0281),top-10 重合 9/10,相关系数 0.9757。第四行换成确定性随机投影,结论和第二行接近(9/10、0.9579),在本文这份合成数据上,截断和投影没有拉开差距。

所以,网上常被转述的那句「降维后相似度数值会掉很多,但排序基本不变」,属于社区讨论帖里的说法(非官方文档,未经官方确认)。在本文的合成数据上,它只对了一半:数值确实会掉,但只有在先把归一化补齐的前提下,排序才基本不变;不补归一化,数值和 top-10 一起崩。至于「数值大幅下降、top-10 完全不变」这种理想情况,在本文的合成数据上没有观察到。


四、找独立信源对一遍:三份一手口径各自量的是什么

自测只能说明机制,不能替你回答「该用哪个模型」;要对账必须回到一手口径——而这几份官方口径各自量的是不同的东西,不能互相代答。

下面四份来源都核到了原文或摘要页原句(取数日期截至 2026-10-08,引用保持原样):

一手来源它量的是什么口径边界(它不管什么)
sentence-transformers 文档归一化参数的定义、以及归一化后可以用点积算分不谈维度压缩;不承诺归一化后排序一定与余弦逐位相同,只说可以这么算分
pgvector 仓库文档三种算子在数据库里的语义:L2 距离、负内积、余弦距离不定义「相似度」本身,也不比较模型好坏;它是检索算子的口径
Matryoshka Representation Learning 论文一种训练方式:让同一个嵌入在不同截断维度下都能用不保证任意模型截断后都能用;它描述的是按这种目标训练出来的表示
MTEB 论文文本嵌入的评测覆盖面:8 类任务、58 个数据集、112 种语言摘要给出的判断是没有任何一种方法在全部任务上占优,所以榜单名次不能当通用结论

4.1 sentence-transformers 与 pgvector:归一化之后,点积可以顶替余弦

sentence-transformers 文档在encode()的参数表里对归一化的定义只有一句:

normalize_embeddings (bool, optional) – Whether to normalize returned vectors to have length 1.

而在语义检索的「Speed Optimization」一节,它把用途说得很直白:

Further, we can normalize the corpus embeddings so that each corpus embeddings is of length 1. In that case, we can use dot-product for computing scores.

这两句正好对上实测块 1 的结论:归一化补齐之后点积与余弦同分同名次,不是经验,是官方文档里写明的等价关系。

pgvector 这一侧给的是算子口径——三种距离函数的原文定义分别是<->- L2 distance、<#>- (negative) inner product、<=>- cosine distance,并且专门注明<#>返回的是负内积:

Note:<#>returns the negative inner product since Postgres only supportsASCorder index scans on operators

也就是说,在数据库层面,「余弦」和「内积」本来就是两个不同的算子、两条不同的索引路径。你在应用层把打分函数从余弦改成内积,等于换了一个量纲,两者不可能自动对齐——除非先把向量归一化。

4.2 MRL 与 MTEB:一个讲截断维度仍然可用,一个讲评测覆盖多任务多语言

Matryoshka Representation Learning 论文(arXiv 2205.13147)摘要里,对这种「可变维度」的表示是这么描述的:

MRL learns coarse-to-fine representations that are at least as accurate and rich as independently trained low-dimensional representations.

它同时给了一个成本侧的结论:

(a) up to 14x smaller embedding size for ImageNet-1K classification at the same level of accuracy

注意这条的口径边界:它说的是按这种目标训练出来的表示可以截断使用,不是「任何嵌入模型砍掉一半维度都还能用」。本文实测块 2 里,未补归一化的截断把 top-10 打掉七成,恰好说明「截断」这个动作本身不提供任何保证——保证来自训练目标和归一化处理。

MTEB 论文(arXiv 2210.07316)摘要里对它的定位写得很清楚:

MTEB spans 8 embedding tasks covering a total of 58 datasets and 112 languages.

以及一条对本文最有用的话:

We find that no particular text embedding method dominates across all tasks.

这两条合起来读,恰恰说明「榜单名次」不能当通用结论:它量的是多任务、多语言的整体覆盖,不是一个模型在你这批向量上的 top-k 稳定性。你要判断降维该不该做,只能在自己那批向量上量 top-k 重合,而不是看谁排在前面。

本篇涉及的官方文档与论文口径:把这几份一手来源的原文摘录、以及两组实测脚本整理进了资料包,配合视频课看更顺。扫码即可获取:

五、差异来自哪里:降维省的是存储和延迟,代价在 top-k 排序

自测结果和官方口径看起来对不上,是因为它们回答的不是同一个问题:文档回答「这么算等不等价」,论文回答「这么训练能不能截断」,你要回答的是「我这批向量的首屏还准不准」。

5.1 差异的三个来源

最容易搞错的是归一化状态。实测块 1、块 2 里所有的名次翻转,根源都是向量长度没被统一。文档说的等价关系有前提,前提没做,结论就不成立。

紧接着会踩的是评估指标选错。平均相似度、全量排序相关系数,都会让人得出「变化不大」的印象;而检索真正交付出去的是 top-k 列表,头部错了就是错了。相关系数 0.9210 配 top-10 重合 3/10,就是这种偏差最典型的样子。

还有一层容易被忽略的是数据分布。本文实验 3 里,候选与查询的相似度是有区分度的(top-10 的量级在 0.9946 以上,均值 0.7891);如果候选之间的分差本来就很小、几乎挤在一起,那降维带来的扰动会更容易改写名次。这也是为什么第一手判断只能在自己数据上做。

5.2 选型清单:什么时候可以降维,什么时候必须停手

把上面的账收敛成一张表。结论落在「先补归一化,再测 top-k」这两步上,而不是「降多少维划算」。

场景可以降维吗落地动作
向量只用来算相似度、且全链路统一归一化可以,先小步试只测 top-k 重合,重合不达标就退回原维度
用点积索引、但向量长度没统一先停手先补归一化再谈降维;否则长度和降维的影响会叠在一起,分不清
用模型原生的截断能力(如按 MRL 目标训练)可以,按训练支持的档位截截断档位不要自己拍,用模型给出的档位,同时重新归一化
检索结果要进 rerank 或人工复核可以放宽top-k 重合只要不把该召回的漏出候选集即可
相似度分布很平(top-k 之间分差接近噪声)谨慎这类数据对维度最敏感,优先保维度,先做量化或换索引
降维目的是省存储/省延迟,但 top-k 已达标可以做把「top-k 重合达标」写成回归用例,改维度就跑一次

什么时候必须停手:只跑了平均相似度、或者只看了全量排序相关系数,就宣布「降维没什么损失」——这是最容易踩的坑。停手条件很硬:top-k 重合率是你的验收线,达标才上,未达标就退回原维度。这条线各场景不同,但至少要跑一次基线对照,取你自己数据上的重合情况,不要用别人的数。

把「维度账」落地成用例:上面这组「归一化前后对照 + 降维 top-k 回归」的脚本,连同向量检索相关的官方文档整理进了资料包,扫码即可获取:

附表 A:本文引用事实与出处对照表

事实出处本文位置
「normalize_embeddings (bool, optional) – Whether to normalize returned vectors to have length 1.」《SentenceTransformer.encode》参数表;Sentence Transformers 官方文档;https://www.sbert.net/docs/package_reference/sentence_transformer/model.html第 4 章
「Further, we can normalize the corpus embeddings so that each corpus embeddings is of length 1. In that case, we can use dot-product for computing scores.」《Semantic Search → Speed Optimization》;Sentence Transformers 官方文档;https://www.sbert.net/examples/sentence_transformer/applications/semantic-search/README.html第 4 章
「MRL learns coarse-to-fine representations that are at least as accurate and rich as independently trained low-dimensional representations.」《Matryoshka Representation Learning》摘要页;arXiv:2205.13147;https://arxiv.org/abs/2205.13147第 4 章
「(a) up to 14x smaller embedding size for ImageNet-1K classification at the same level of accuracy」同上(摘要页)第 4 章
「MTEB spans 8 embedding tasks covering a total of 58 datasets and 112 languages.」《MTEB: Massive Text Embedding Benchmark》摘要页;arXiv:2210.07316;https://arxiv.org/abs/2210.07316第 4 章
「We find that no particular text embedding method dominates across all tasks.」同上(摘要页)第 4 章
「<->- L2 distance」「<#>- (negative) inner product」「<=>- cosine distance」pgvector 官方仓库 README;https://github.com/pgvector/pgvector第 4 章
「Note:<#>returns the negative inner product since Postgres only supportsASCorder index scans on operators」同上(Querying → Supported distance functions)第 4 章
未归一化下点积排序[3, 0, 1, 2]与余弦排序[1, 3, 0, 2];归一化后两者同值同为[1, 3, 0, 2];64 维截断到 32 维时 top-10 重合 3/10(未重归一化点积)与 9/10(重归一化余弦),平均相似度 0.7891 → 0.4316 / 0.7993本文实测,脚本见第 2、3 章(Python 3.13.12 / numpy 2.5.3,固定种子 42 与 7)第 2、3 章

表下口径:第 3 章所有指标均来自合成随机向量,不是任何真实语料或线上检索服务的采样结果;「平均相似度」指查询与 200 条候选的打分均值,「top-10 重合」指降维前后 top-10 候选集合的交集大小。真实项目请用你自己的向量批次复跑。


写在最后:这篇用到的资料

写这篇文章时,把相关的官方文档和源码又翻了一遍,顺手也整理了几份配套的东西:

  • 大模型学习路线图:从零基础到能自己动手做 Agent,按阶段说明每一步该学什么、哪些可以先跳过
  • 《LangChain + LangGraph + MCP 智能体开发实战》视频课:7 个模块,从私有化部署、Embedding+RAG 到 MCP+Agent 全流程
  • AI 大模型知识库(在线可查):Agent Skills 从入门到落地、Claude Skills 完全指南等专题,按目录浏览即可
  • 640 套 AI 大模型行业报告 + 经典 PDF 书籍:看行业落地案例和别人怎么做的时候用得上
  • 大模型零基础到精通教学视频:跟着敲一遍,比只读文档快得多

资料是我自己整理的,放在下面这个码上,扫码即可获取:






添加时备注「AI」,优先通过。

资料按「先路线、再动手、最后查漏」的顺序整理好了,建议先看学习路线那一份,照着它挑一条适合自己当前基础的路径再往下看。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 2:20:47

谁说GLM5.2部署不到A100上?用TaoToken统一Key打通推理链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/9 2:18:54

基于BERT的文本纠错模型实战:从数据构造到推理调优

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/9 2:16:29

RS485老电表不换表上云:DTU、采集器、边缘网关三条路径对比

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华