余弦相似度:同向向量为何得不到1
两个非零向量完全同向,cosine_similarity的结果仍可能小于1。先检查向量范数是否低于eps:按PyTorch 2.8文档的公式,每个范数分别设下限,极小向量不会被放大成单位长度。此时分数同时包含方向和小范数衰减。
文章目录
这会影响“先normalize再点积”的等价条件,也会改变检索阈值和候选顺序。下面用可复算的二维例子区分零向量、极小非零向量和正常单位向量,不把数值保护返回的0当成方向关系。Read in English。
分母分别截断,不能只看函数名字
PyTorch 2.8 cosine_similarity定义为s=(x·y)/(max(‖x‖₂,eps)max(‖y‖₂,eps)),默认eps=10⁻⁸。关键是两个max分别作用在两个范数上。
对非零x、y,可以把它重写为s=cos(x,y)×r(x)×r(y),其中r(v)=min(1,‖v‖₂/eps)。当两个范数都不低于eps,r都是1,才回到通常的几何余弦;有一个低于下限,分数幅度就受到衰减。这是公式推导,不是某个嵌入模型的经验结论。
为便于看清数字,先用人为选取的eps=0.01。令x=(0.005,0),y=(1,0),方向一致,但s=0.5;让两端都用x,自相似度变成0.25。若把x放大到(0.01,0),与y的分数恢复为1。放大前后方向没变,分数却变了,说明在这一段不能继续假设正比例缩放不改变结果。
零分数有几种不同来源
零向量没有可定义的几何方向,通常的余弦公式在这里分母为0。加入正的eps后,数值公式可以为零向量返回0;这是对异常输入的延拓约定,不意味着它与任意向量形成直角。
真正正交的非零向量也得到0。例如(1,0)与(0,1)点积为0;但前者与零向量的平方距离为1,与(0,1)的平方距离为2。相似度同为0,距离与数据质量含义都不一样,不能只凭分数判断“无关但有效”。
工程上应另行记录向量是否有效:空输入、上游处理失败和模型确实生成零值是不同状态。这里只建议保存有授权、非敏感的状态与统计,不需要为了排错记录原始私人文本。无效向量是拒绝、跳过还是使用明确的回退,应由应用契约决定。小范数本身并不自动等于无效;应把数值状态与业务有效性分别记录,避免为了得到漂亮分数就删除有效样本。
normalize加点积,何时才是同一个数
normalize参考给出的变换是v/max(‖v‖ₚ,eps),默认p=2、eps=10⁻¹²。因此,在相同维度、p=2并显式使用同一个eps的实数公式下,分别normalize后点积,与上面的分别截断公式相同。
直接使用两个函数的默认参数,却没有满足“同一个eps”。取x=(5×10⁻⁹,0)、y=(1,0):按10⁻⁸的下限得到0.5,按10⁻¹²归一化后点积得到1。它们都遵守各自定义,差异不是靠补一行浮点容差就能解释掉的。
还要区分另一种写法:把范数乘积整体设下限。即便为保持量纲使用max(‖x‖‖y‖,eps²),也不等同于两个范数分别截断。对x=(0.005,0)、y=(1,0)、eps=0.01,整体乘积规则得到1,分别截断得到0.5。重构打分函数时,不能只保留一个叫eps的参数名就认为语义没变。
方向更接近的候选,为什么排到后面
查询q=(1,0),候选A=(0.005,0),B=(0.8,0.6)。严格几何余弦为A=1、B=0.8,A更接近查询方向。若采用eps=0.01的分别截断,A被衰减到0.5,B仍为0.8,顺序反过来。
这不是近似索引漏召回:只用精确算术定义就能发生。若系统本来设计为严格方向检索,应先检查小范数策略是否符合意图;若模型和服务明确采用这一带保护的打分,则要用相同规则构建基准,不能拿另一种公式去判索引错误。
示例中的0.01仅用于让比例直观,不是线上推荐阈值,也不表示真实模型经常产生这样的范数。上线前应检查有授权的代表性样本和真实数据类型,确认小范数情况是否存在,再决定验收规则,不能把教学反例当成发生频率。
十三项检查,把默认值差异单独测出来
代码以标准库浮点数实现文档实数公式,验证同向0.5、自相似0.25、边界、零向量、排名反转、默认eps差异和整体乘积反例,并拒绝NaN与无穷坐标。它不调用PyTorch,也不运行检索索引。
# Real-number formula reference with Python floats; no torch/index runtime.
from math import hypot,isfinite,isclose
def normalized(v,eps):
if not v or not isfinite(eps) or eps<=0:
raise ValueError('positive finite eps and nonempty vector required')
if not all(isfinite(x) for x in v):
raise ValueError('nonfinite coordinate')
scale=max(hypot(*v),eps)
return tuple(x/scale for x in v)
def score(a,b,eps):
if len(a)!=len(b):
raise ValueError('dimension mismatch')
return sum(x*y for x,y in zip(normalized(a,eps),normalized(b,eps)))
eps=.01; q=(1.,0.); a=(eps/2,0.); b=(.8,.6)
assert isclose(score(a,q,eps),.5)
assert isclose(score(a,a,eps),.25)
assert isclose(score((eps,0.),q,eps),1.)
assert score((0.,0.),q,eps)==0.
assert score((0.,0.),(0.,0.),eps)==0.
assert score(a,q,eps)<score(b,q,eps)
assert score(a,q,1e-12)>score(b,q,1e-12)
assert isclose(score((5e-9,0.),q,1e-8),.5)
assert isclose(score((5e-9,0.),q,1e-12),1.)
# Clamping the product, even using eps squared, is a different formula.
product_floor=sum(x*y for x,y in zip(a,q))/max(hypot(*a)*hypot(*q),eps**2)
assert isclose(product_floor,1.)
assert not isclose(product_floor,score(a,q,eps))
for bad in [(float('nan'),0.),(float('inf'),0.)]:
try:
score(bad,q,eps)
except ValueError:
pass
else:
raise AssertionError('nonfinite coordinate accepted')
print('13 numerical and rejection checks passed')
这里的输入检查是示例自己的契约,不代表框架自动拒绝所有非有限值。代码只覆盖展示的适中数值和边界,不是任意精度、任意维度或极端浮点范围的生产实现;实际低精度下eps的表示、运算顺序和舍入还需要在目标设备单独测试。
迁移打分规则时,保留哪些证据
先明确系统要比较严格几何余弦,还是带特定eps的数值分数。保存公式、eps、归一化轴、输入维度和计算类型,并让查询端、入库端与离线基准使用同一约定。不要把“已经normalize”当成完整配置描述。
对真实API还要显式检查归一化轴和输出形状,防止把按行比较误写成跨批次比较;本文的tuple例子只表示一对等维向量,没有验证广播或张量维度语义。测试集应至少区分正常非零、零向量、小于下限、恰在下限及非有限值。对小范数案例同时检查分数与业务状态:若无效查询仍走top-k,即使所有得分都是0,也可能被排序流程选出若干条记录;返回了结果不代表查询有效。
旧文余弦与内积的等价条件讨论了单位向量情形;这里补的是“归一化结果何时并非单位向量”。在受保护的小范数区间,不应继续套用单位向量的平方L2=2−2×内积;应回到含两端实际范数的距离展开式。以教学例子为例,保护后的A变成(0.5,0),查询仍为(1,0),平方距离是0.25;误套单位向量公式却得到1。正确展开为0.25+1−2×0.5=0.25,差异来自实际长度,而不是浮点容差。
若还要比较近似索引,先固定本篇的精确打分契约,再观察近似搜索误差;并列结果的处理可参阅top-k同分与召回率。这样能把数值定义、数据有效性与搜索算法分开定位。
如果这些小向量反例帮你发现归一化或阈值迁移中的差异,欢迎通过文末自愿支持。可复用的成果是这一组把公式、异常状态和排序分开检查的测试样例。
来源与补发说明
按原计划前已有的PyTorch 2.8版本文档核对。图示、向量和数值为原创教学内容,实际执行标准库检查,没有运行模型、PyTorch或索引。本次补发保留2026年10月21日19:00北京时间文章日期,实际公开时间另行记录。
支持
如果文中的解释或示例帮你解决了问题、节省了时间,欢迎自愿打赏支持本站。
二维码可点击放大。更多支持方式见支持页面。


