qinghuan
← 数学语言与概率基础 / 全部笔记

单元 02 / 数学语言与概率基础

P0 · 必会主线L0 · 零基础

向量、点积与范数:把相似度写成计算

先用一组有顺序的数表示对象,再计算点积、长度和余弦相似度,并处理零向量与极大、极小浮点数。

本篇目录 · 按需要跳转

这篇重点学什么

本篇重点:推导并计算 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 累加,用显式缩放观察大数仍能得到合理方向分数。

experiment.py · python
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["余弦及任务语义解释"]

按顺序看,每一步做了什么

  1. 确认坐标语义和两侧维度一致。
  2. 检查 NaN、无穷和零向量。
  3. 分别缩放到可计算范围。
  4. 累计点积和平方和。
  5. 检查输出误差并用任务样本解释结果。

03对照实现代码

OpenJDK 17u:FdLibm.Hypot 的缩放长度计算 (新标签页)↗

Microsoft OpenJDK 17.0.20.1;上游 jdk-17.0.20-ga · java.lang.FdLibm$Hypot#compute(double,double)

以下为原创精简教学实现,不是原样库源码;只演示本文说明的机制,省略真实实现的优化、调度和完整兼容边界。

核心机制代码 · python
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 后端的逐行源码。

代码先调用谁,再调用谁

  1. Math.hypot 公开入口
  2. StrictMath.hypot
  3. FdLibm.Hypot.compute 检查特殊值
  4. 按指数缩放并计算长度
  5. 恢复尺度或得到不可表示结果

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、无穷和零向量已经提前拒绝。双精度顺序累加仍有误差,自测使用容差而非把所有近似结果与精确实数逐位比较,极值案例用已知方向作为独立预期。

为什么这样写是对的完成第二遍前缀后,三个累加器分别为该前缀缩放分量的点积及两侧平方和。

时间 O(d)。额外空间 O(1) 额外空间。

两次线性扫描,每个坐标执行常数次固定宽度浮点运算;不分配归一化数组。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

Main.java · 完整可运行程序 · Java
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 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

能手算小向量点积与范数,并实现对有限非零向量稳定计算的余弦相似度。

本篇重点加练

点积同时编码长度和方向,需要通过信息是否充分与几何变换来检验理解。

  • 点积的代数定义
  • 长度与方向

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01原理问答

    对 u=(2,-1,3)、v=(-1,4,2),分别描述 u+v、3u 和 u·v 的输入输出类型,并给出坐标顺序改变时应保持的约束。

    提交内容
    计算纸与类型表
    怎样算完成
    • 坐标含义一致
    • 标量向量区分清楚

    练到的知识点向量分量和维度 · 逐元素加法与缩放 · 点积的代数定义

  2. 作业 02代码推演

    对 a=(2,0)、b=(1,1)、c=(-3,0),比较各对的点积、欧氏距离和余弦排序;说明三种排序各自保留了哪些信息。

    提交内容
    手算表与几何草图
    怎样算完成
    • 计算过程可核对
    • 几何解释符合所用度量

    练到的知识点欧氏范数 · 长度与方向 · 余弦相似度

  3. 作业 03原理问答

    已知二维未知向量 u 与 a=(1,0)、b=(0,1) 的点积,是否足以确定 u?将 b 改成 (2,0) 后重新分析,并自行给定两组可检验的观测。

    提交内容
    可恢复性论证与观测案例
    怎样算完成
    • 给定信息与待求量明确
    • 唯一性或不唯一性有证据

    练到的知识点点积的代数定义 · 长度与方向

  4. 作业 04代码推演

    对自行给定的三个三维向量 u、v、w,核验 u·(v+w) 与 (u·v)+(u·w),并审查把一个标量点积继续当向量相乘的错误表达式。

    提交内容
    逐项记录与类型审查
    怎样算完成
    • 运算输入输出类型明确
    • 等式使用条件完整

    练到的知识点点积的代数定义

综合应用

  1. 作业 05编程实践

    编写小向量数值测试器,对分量跨越 1e-200 至 1e200 的有限输入比较两种余弦实现;使用独立高精度计算作为参照并报告误差。

    提交内容
    CPU 脚本与误差表
    怎样算完成
    • 参照计算独立
    • 误差与尺度共同报告

    练到的知识点点积的代数定义 · 欧氏范数 · 余弦相似度 · 缩放避免中间溢出

  2. 作业 06设计评审

    为允许缺失特征的相似度接口设计契约,分别处理全零、空数组、长度不等、NaN 和仅一方缺失;说明这些状态能否混用同一个返回值。

    提交内容
    接口及异常状态表
    怎样算完成
    • 非法与合法数值可区分
    • 调用方处理规则完整

    练到的知识点零向量策略

  3. 作业 07排错实验

    生成二维样本,将一个坐标单位从米改成毫米,观察最近邻排序的变化;评估两种预处理方案,并写清统计量由哪份数据得到。

    提交内容
    数据、排序记录及实验说明
    怎样算完成
    • 单位变更可复现
    • 预处理没有使用保留集信息

    练到的知识点逐元素加法与缩放 · 欧氏范数 · 长度与方向

  4. 作业 08编程实践

    仅用 Python 标准库对五个自拟二维向量做同一角度旋转,比较旋转前后的点积、长度与向量分量;同时测试只旋转一侧的情况。

    提交内容
    变换程序与比较表
    怎样算完成
    • 变换对象范围明确
    • 误差容差及输入可复现

    练到的知识点点积的代数定义 · 长度与方向

进阶挑战 · 掌握前置后选做

  1. 作业 09原理问答

    审查“点积运算都是 O(d),因此顺序累加和不同归约树数值上等价”的论证;提供有限浮点输入的反例并界定该反例证明的范围。

    提交内容
    反例程序与论证
    怎样算完成
    • 复杂度和数值误差分开
    • 反例不依赖非法输入

    练到的知识点点积的代数定义 · 余弦相似度 · 缩放避免中间溢出

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 反例构造

    构造一组候选向量,使点积排名与仅比较方向的排名不一致;能否在保持所有方向不变时任意改变最高分候选?给出成立条件或阻碍。

    提交内容
    候选集、论证与边界说明
    怎样算完成
    • 向量非零且条件显式
    • 排名变化有计算证据

    练到的知识点点积的代数定义 · 长度与方向

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗