这篇重点学什么
本篇重点:推导并计算 L1 / L2 / L∞ 范数、点积和余弦,再用相近数相减的例子检查浮点抵消误差,学习稳定计算。
开始读正文 ↓学过相近内容?查看哪些可以跳过
两条路线有些基础相同。已经学会的部分可以跳过,另一条路线只用来补你当前需要的内容。
向量与相似度
共同基础点积、范数、余弦与稳定计算统一以数学篇为主读;掌握后不重复做同类数值练习。
另一条路线补充什么?
先核对查询和文档的编码模型版本、归一化方式与索引是否兼容;换模型或迁移索引后检查检索结果,再用标注样例确定并检验相似度阈值。
读完这篇,你能做什么
能手算小向量点积与范数,并实现对有限非零向量稳定计算的余弦相似度。
学习理由、准备要求与知识点
为什么学它
Embedding、梯度和输入特征都常用向量表示。看清相似度在比较什么,才能避免检索和训练中的误用。
先读这些内容
知识点清单
- 向量分量和维度
- 逐元素加法与缩放
- 点积的代数定义
- 欧氏范数
- 长度与方向
- 余弦相似度
- 零向量策略
- 缩放避免中间溢出
运行前先确认Java 17;Python 3.14 CPU 教学实验,无外部数据、预训练权重、GPU 或多机训练。源代码基线与局限见源码模块。
01先弄懂它是什么
要用数字描述一个对象,往往需要不止一个数。例如用“一天学习几小时、做了几道题”记录学习情况,就得到一组有固定顺序的数,这叫向量。x=(3,4) 有两个分量,维度 d=2。比较两个向量时,同一位置必须表示同一件事、使用相同单位;不能拿一边的年龄去和另一边的金额相乘,再把结果解释为相似程度。
如果想把两个向量对应位置的数一起比较,可以先逐项相乘,再把结果相加,这就是点积:dot(x,y)=sum(i=1..d,x_i*y_i)。两个向量必须有相同数量的分量,算出的结果是一个数,也叫标量。例如 x=(1,2)、y=(3,4) 的点积为十一。但点积也受数值大小影响:把 x 放大十倍,点积也放大十倍,所以点积高不一定只是方向更接近。
欧氏范数 norm(x)=sqrt(sum(x_i^2)) 表示长度,(3,4) 的长度为五。余弦相似度 cos(x,y)=dot(x,y)/(norm(x)*norm(y)) 去掉正比例缩放的影响,主要比较方向。对于非零实向量,结果在负一到一之间;相同方向为一,垂直为零,相反方向为负一。零向量没有方向,公式分母为零,必须由接口另行决定策略。
文中这几个词,先认一认
- 分量
- 向量里某个位置上的数。例如 (3,4) 的第一个分量是三。
- 范数
- 按规定的方法计算向量的长度。本篇主要用欧氏范数,也就是由勾股定理推广出的长度。
- 归一化
- 按约定调整数值尺度。本篇是把非零向量除以它的长度,使长度变为一、方向保持不变。
02看看它怎么工作
把点积和范数代入小例子:x=(1,0)、y=(0,1) 的点积为零,所以正交;x=(3,4)、y=(6,8) 的点积为五十,两个长度分别为五和十,所以余弦为一。正比例缩放不会改变方向,负比例缩放则反转方向。不要把余弦一解释成两个向量逐项相等,它只说明方向一致。
除了欧氏长度,还可以用 L1 范数 sum(abs(x_i)) 或最大分量范数 max(abs(x_i)) 衡量向量大小。同一个向量 (3,4),用 L1、欧氏和最大分量范数计算,分别得到七、五、四。训练中的正则化、稀疏性和距离搜索可能选择不同范数,不能看到一个 norm 名称就默认全是欧氏长度;还要检查维度轴与是否保留维度。
浮点实现不能简单照抄数学表达式。x=(1e308,1e308) 的两个分量都有限,但先平方会溢出为无穷。数学上的余弦仍可能完全正常,因为分子和分母的尺度本可抵消。先把每个向量除以自身最大绝对分量,再计算归一化后的点积与长度,可以避免这些不必要的巨大中间值。
缩放解决的是部分范围问题,并不消除全部误差。很小的分量相对于最大分量可能下溢为零,大量相反符号项相加也可能发生抵消。对重要内积可以考虑更高精度累加、补偿求和或库提供的稳定归约;算法复杂度相同不意味着误差表现相同。本篇在双精度小实验中比较已知结果,不声称对任意高维输入实现正确舍入。
相似度也不是概率。余弦为零点八不代表两段文本有百分之八十概率表达同一意思;阈值需要在具体任务与数据上评估。若模型输出特征的模长包含置信度等信息,归一化会丢弃这部分信号。选择点积还是余弦,应先明确要保留大小还是只比较方向。
先跑通一个最小例子
Python stdlib CPU 小实验用 math.fsum 累加,用显式缩放观察大数仍能得到合理方向分数。
import math
def cosine(x,y):
if len(x)!=len(y) or not x:
raise ValueError("shape")
if not all(math.isfinite(v) for v in x+y):
raise ValueError("finite values required")
sx=max(map(abs,x)); sy=max(map(abs,y))
if sx==0 or sy==0:
raise ValueError("zero vector") # 明确拒绝没有方向的输入。
a=[v/sx for v in x]; b=[v/sy for v in y]
dot=math.fsum(u*v for u,v in zip(a,b)) # 先缩放再累加,避免原分量平方溢出。
den=math.sqrt(math.fsum(v*v for v in a))*math.sqrt(math.fsum(v*v for v in b))
return max(-1.0,min(1.0,dot/den))
assert math.isclose(cosine([3.,4.],[6.,8.]),1.)
assert cosine([1.,0.],[0.,1.])==0.
assert cosine([1.,0.],[-1.,0.])==-1.
assert math.isclose(cosine([1e308,1e308],[1e308,1e308]),1.)
assert math.isfinite(math.hypot(1e308,1e308))
try:
cosine([0.,0.],[1.,2.])
raise AssertionError("zero accepted")
except ValueError:
pass
print("PASS 6: direction, orthogonal, opposite, large values, norm, zero")
核对运行结果打印 PASS 6;大分量向量与自身余弦接近一,零向量按契约拒绝。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart LR
A["对齐坐标与维度"] --> B["检查有限且非零"]
B --> C["按各自最大分量缩放"]
C --> D["点积与长度计算"]
D --> E["余弦及任务语义解释"]按顺序看,每一步做了什么
- 确认坐标语义和两侧维度一致。
- 检查
NaN、无穷和零向量。 - 分别缩放到可计算范围。
- 累计点积和平方和。
- 检查输出误差并用任务样本解释结果。
03对照实现代码
Microsoft OpenJDK 17.0.20.1;上游 jdk-17.0.20-ga · java.lang.FdLibm$Hypot#compute(double,double)
以下为原创精简教学实现,不是原样库源码;只演示本文说明的机制,省略真实实现的优化、调度和完整兼容边界。
import math
def scaled_length(x,y):
a,b=abs(x),abs(y)
if not math.isfinite(a) or not math.isfinite(b):
raise ValueError("finite model only") # 教学模型收窄输入域。
scale=max(a,b)
if scale==0:
return 0.0
# 比例先平方,再恢复尺度,避免不必要的中间溢出。
return scale*math.sqrt((a/scale)**2+(b/scale)**2)
本机 Math.hypot 委托 StrictMath.hypot,进一步使用 FdLibm.Hypot.compute。真实实现先处理非有限值和分量大小顺序,再按指数范围对过大或过小值进行二进制缩放,最后恢复尺度。它不是简单地执行 sqrt(x*x+y*y),因此能避免许多中间平方溢出或下溢。
真实实现还对浮点数拆分高低部分,降低平方和计算的舍入误差,并针对分量数量级差异设置分支。原创模型只保留按尺度缩小再恢复的思想,不复现完整特殊值规则或精度保证;模型拒绝非有限输入,而真实库有自己明确的无穷与 NaN 契约。
长度结果本身若超出 double 的可表示范围,缩放也不能把它变成有限数。需要区分数学结果太大与中间表达式无端溢出:前者是输出类型限制,后者可以通过重排计算改善。余弦因为尺度可以抵消,可以直接在缩放空间计算,而不必先恢复巨大范数。
从二维 hypot 推广到高维时,不能声称调用一次二维函数就已经实现全部矩阵归约。库可能使用向量化、分块和不同精度策略。本文算法明确对一对一维数组做顺序扫描,借鉴数值范围处理原则,不冒称是实际 Tensor 后端的逐行源码。
代码先调用谁,再调用谁
Math.hypot公开入口StrictMath.hypotFdLibm.Hypot.compute检查特殊值- 按指数缩放并计算长度
- 恢复尺度或得到不可表示结果
04动手写一个例子
避免巨大平方的余弦相似度
输入同维、非空、有限 double 数组,两侧都不得为零向量;输出余弦。使用每侧最大绝对值缩放,不创建额外向量。
- 输入 1
[3,4] 与 [6,8]- 输出 1
约 1
- 输入 2
[1,0] 与 [0,1]- 输出 2
0
- 输入 3
[1e308,1e308] 与自身- 输出 3
约 1
第一遍验证长度和有限值,并找到两边各自的最大绝对分量 sx、sy。只要向量非零,缩放后至少有一个分量绝对值为一,其余不超过一,因此长度计算不会因为所有分量太小而整体变为零。
第二遍使用 x_i/sx 与 y_i/sy 累计点积和平方和。两个正比例尺度在余弦分子分母中抵消,数学结果不变。中间平方不超过一,和的上界受数组长度控制,避开原始 1e308 的平方;不必实际保存缩放后的数组。
最后将数值舍入造成的轻微越界限制到负一到一,但这不是修复错误输入的工具:NaN、无穷和零向量已经提前拒绝。双精度顺序累加仍有误差,自测使用容差而非把所有近似结果与精确实数逐位比较,极值案例用已知方向作为独立预期。
为什么这样写是对的完成第二遍前缀后,三个累加器分别为该前缀缩放分量的点积及两侧平方和。
两次线性扫描,每个坐标执行常数次固定宽度浮点运算;不分配归一化数组。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
public class Main {
static double cosine(double[] x,double[] y){
if(x==null||y==null||x.length==0||x.length!=y.length)throw new IllegalArgumentException("shape");
double sx=0,sy=0;
for(int i=0;i<x.length;i++){
if(!Double.isFinite(x[i])||!Double.isFinite(y[i]))throw new IllegalArgumentException("finite");
sx=Math.max(sx,Math.abs(x[i]));sy=Math.max(sy,Math.abs(y[i]));
}
if(sx==0||sy==0)throw new IllegalArgumentException("zero"); // 零向量没有余弦方向。
double dot=0,xx=0,yy=0;
for(int i=0;i<x.length;i++){
double a=x[i]/sx,b=y[i]/sy; // 各自缩放,正尺度在最后比值中抵消。
dot+=a*b;xx+=a*a;yy+=b*b;
}
double value=dot/(Math.sqrt(xx)*Math.sqrt(yy));
return Math.max(-1,Math.min(1,value)); // 只校正舍入导致的微小区间越界。
}
static void near(double a,double b){if(!Double.isFinite(a)||Math.abs(a-b)>1e-12)throw new AssertionError(a+" != "+b);}
static void bad(Runnable r){try{r.run();}catch(IllegalArgumentException e){return;}throw new AssertionError();}
public static void main(String[] args){
near(cosine(new double[]{3,4},new double[]{6,8}),1);
near(cosine(new double[]{1,0},new double[]{0,1}),0);
near(cosine(new double[]{1},new double[]{-2}),-1);
near(cosine(new double[]{1e308,1e308},new double[]{1e308,1e308}),1);
bad(()->cosine(new double[]{0,0},new double[]{1,2}));
bad(()->cosine(new double[]{Double.NaN},new double[]{1}));
bad(()->cosine(new double[]{1},new double[]{1,2}));
System.out.println("PASS 7");
}
}这些特殊情况也要试一试
- 零向量拒绝,不能把分母设为一而不说明。
- 两侧维度必须一致。
- 非有限输入在计算前失败。
- 真实浮点误差仍用容差处理。
进阶选读 · 大规模相似度检索
场景需要对百万向量找最相近结果。
做法预先归一化并用矩阵乘或近似索引检索,再用精确分数重排;固定距离定义与召回评估集。
复杂度直接计算 n 个 d 维向量为 O(nd),近似方法成本取决于索引和误差目标。
好处与代价速度提升可能牺牲召回,归一化也可能丢失模长信息,应以任务指标验证。
05项目里怎么用
不同特征的单位差得很大时,直接算距离,数值大的一项可能盖过其他项。例如金额和年龄放在一起,就要先弄清它们各自的定义和分布,再决定怎样调整尺度。需要从数据计算均值等统计量时,只用训练数据估计。文本 Embedding 应按模型说明并结合任务测试选择评分方式,不要假定所有模型都必须采用同一种归一化。
线上阈值需要考虑分布变化与业务错误成本。即便余弦始终在固定区间内,不同模型、语言和文本长度的分数分布仍可能不同。升级编码器后要重建索引并评估阈值兼容性,不能仅因为接口仍返回一个小数就默认含义不变。
数值测试应覆盖不同方向、规模和不合法输入。对于跨平台向量库,不同归约顺序可能造成末位差异,比较时使用合理绝对与相对容差;过宽容差又可能掩盖错误维度或符号。大型性能结果需要在目标设备和实际批量上测量,本篇小向量正确性实验不提供吞吐结论。
容易踩的坑
- 点积大就认为语义更相近。
- 把余弦当作概率。
- 忽略零向量和
NaN。 - 直接平方巨大值导致中间溢出。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 点积 | 保留大小和方向信息 | 分数受模长影响 | 模型按点积训练的评分 |
| 余弦 | 正尺度不影响分数 | 零向量需定义且模长丢失 | 方向主导的相似度 |
| 欧氏距离 | 几何意义直观 | 受尺度和单位影响 | 明确坐标空间的近邻 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01余弦一是否意味着向量相同?
展开参考答案与推导
不意味着。两个非零向量只要同方向且是正比例关系,余弦就是一。若业务还关心大小,需要同时检查范数或使用其他距离。
02零向量为何不能直接套公式?
展开参考答案与推导
它的范数为零,分母为零且没有唯一方向。接口可以拒绝或定义特殊约定,但应明确这不是原始数学余弦的正常值。
03为什么先缩放不会改变余弦?
展开参考答案与推导
每个向量除以正尺度后,点积除以两个尺度的乘积,范数乘积也除以相同值,两者相除抵消。负尺度会改变方向,不能忽略符号。
换个情况,还会处理吗
01两个模型相同余弦阈值表现不同怎么办?
展开参考答案与推导
比较各自验证集上的分数分布和错误成本,重新选择阈值。相同数值区间不保证跨模型概率或业务语义一致。
02大向量与自身算出 NaN,如何诊断?
展开参考答案与推导
检查是否原始分量平方或点积先溢出,导致无穷除无穷。采用缩放或成熟稳定归约,并区分输出本身越界与中间计算问题。
03特征金额压过年龄,怎样处理?
展开参考答案与推导
先确认两者确实应共同定义距离,再考虑基于训练集的尺度变换。不能盲目归一化后假定所有单位和业务影响已经对齐。
写代码试一试
01求小向量长度。
- 输入 / 条件
[3,4]- 输出 / 预期
5
提示 平方和开根号。
展开参考答案与推导
九加十六等于二十五,平方根为五;L1 范数则是七,因此题目必须明确采用哪种范数。
02计算相反方向。
- 输入 / 条件
[1,2] 与 [-2,-4]- 输出 / 预期
-1
提示 后一向量为负比例。
展开参考答案与推导
点积为负十,范数乘积为十,所以余弦为负一。正比例缩放不变性的条件不能省略正号。
03改进大数计算。
- 输入 / 条件
[1e308,1e308] 与自身- 输出 / 预期
约 1
提示 先除最大绝对分量。
展开参考答案与推导
缩放后两侧都是 [1,1],点积为二,两个长度乘积也为二,因此比值接近一,无需计算不可表示的原始平方。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能手算小向量点积与范数,并实现对有限非零向量稳定计算的余弦相似度。
本篇重点加练
点积同时编码长度和方向,需要通过信息是否充分与几何变换来检验理解。
- 点积的代数定义
- 长度与方向
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01原理问答
对 u=(2,-1,3)、v=(-1,4,2),分别描述 u+v、3u 和 u·v 的输入输出类型,并给出坐标顺序改变时应保持的约束。
- 提交内容
- 计算纸与类型表
- 怎样算完成
- 坐标含义一致
- 标量向量区分清楚
练到的知识点向量分量和维度 · 逐元素加法与缩放 · 点积的代数定义
- 作业 02代码推演
对 a=(2,0)、b=(1,1)、c=(-3,0),比较各对的点积、欧氏距离和余弦排序;说明三种排序各自保留了哪些信息。
- 提交内容
- 手算表与几何草图
- 怎样算完成
- 计算过程可核对
- 几何解释符合所用度量
练到的知识点欧氏范数 · 长度与方向 · 余弦相似度
- 作业 03原理问答
已知二维未知向量 u 与 a=(1,0)、b=(0,1) 的点积,是否足以确定 u?将 b 改成 (2,0) 后重新分析,并自行给定两组可检验的观测。
- 提交内容
- 可恢复性论证与观测案例
- 怎样算完成
- 给定信息与待求量明确
- 唯一性或不唯一性有证据
练到的知识点点积的代数定义 · 长度与方向
- 作业 04代码推演
对自行给定的三个三维向量 u、v、w,核验 u·(v+w) 与 (u·v)+(u·w),并审查把一个标量点积继续当向量相乘的错误表达式。
- 提交内容
- 逐项记录与类型审查
- 怎样算完成
- 运算输入输出类型明确
- 等式使用条件完整
练到的知识点点积的代数定义
综合应用
- 作业 05编程实践
编写小向量数值测试器,对分量跨越 1e-200 至 1e200 的有限输入比较两种余弦实现;使用独立高精度计算作为参照并报告误差。
- 提交内容
- CPU 脚本与误差表
- 怎样算完成
- 参照计算独立
- 误差与尺度共同报告
练到的知识点点积的代数定义 · 欧氏范数 · 余弦相似度 · 缩放避免中间溢出
- 作业 06设计评审
为允许缺失特征的相似度接口设计契约,分别处理全零、空数组、长度不等、
NaN和仅一方缺失;说明这些状态能否混用同一个返回值。- 提交内容
- 接口及异常状态表
- 怎样算完成
- 非法与合法数值可区分
- 调用方处理规则完整
练到的知识点零向量策略
- 作业 07排错实验
生成二维样本,将一个坐标单位从米改成毫米,观察最近邻排序的变化;评估两种预处理方案,并写清统计量由哪份数据得到。
- 提交内容
- 数据、排序记录及实验说明
- 怎样算完成
- 单位变更可复现
- 预处理没有使用保留集信息
练到的知识点逐元素加法与缩放 · 欧氏范数 · 长度与方向
- 作业 08编程实践
仅用 Python 标准库对五个自拟二维向量做同一角度旋转,比较旋转前后的点积、长度与向量分量;同时测试只旋转一侧的情况。
- 提交内容
- 变换程序与比较表
- 怎样算完成
- 变换对象范围明确
- 误差容差及输入可复现
练到的知识点点积的代数定义 · 长度与方向
进阶挑战 · 掌握前置后选做
- 作业 09原理问答
审查“点积运算都是
O(d),因此顺序累加和不同归约树数值上等价”的论证;提供有限浮点输入的反例并界定该反例证明的范围。- 提交内容
- 反例程序与论证
- 怎样算完成
- 复杂度和数值误差分开
- 反例不依赖非法输入
练到的知识点点积的代数定义 · 余弦相似度 · 缩放避免中间溢出
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 反例构造
构造一组候选向量,使点积排名与仅比较方向的排名不一致;能否在保持所有方向不变时任意改变最高分候选?给出成立条件或阻碍。
- 提交内容
- 候选集、论证与边界说明
- 怎样算完成
- 向量非零且条件显式
- 排名变化有计算证据
练到的知识点点积的代数定义 · 长度与方向
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读参考文档英文
OpenJDK 17u:FdLibm.Hypot 的缩放长度计算 (新标签页)↗
阅读
Hypot.compute的指数缩放、特殊值与误差处理,理解模型只保留其中的范围控制思想。 - 深入参考文档英文
MIT 18.06:线性代数 (新标签页)↗
阅读向量几何、点积、长度与正交相关课程。
- 深入参考文档英文
Python 3.14 math (新标签页)↗
核对 hypot、fsum 与浮点结果的契约。
- 深入参考文档英文
PyTorch 2.9 cosine_similarity (新标签页)↗
对照维度归约与 epsilon 约定,注意库策略与本篇零向量拒绝策略不同。