AI 工程地基 14|范数与距离:换一把尺子,最近邻就换人

「AI 工程地基」系列第十五篇。把 GitHub 上 58k star 的开源课 AI Engineering from Scratch(MIT 协议)一课一课啃完、核完、讲完。今天第 14 课:范数与距离,Build 型,90 分钟,前置是前几课的线性代数。

目录
  1. AI 工程地基 14|范数与距离:换一把尺子,最近邻就换人
  2. 你每天写的「找相似」其实是选尺子
  3. 过一遍海关

AI 工程地基 14|范数与距离:换一把尺子,最近邻就换人

「AI 工程地基」系列第十五篇。把 GitHub 上 58k star 的开源课 AI Engineering from Scratch(MIT 协议)一课一课啃完、核完、讲完。今天第 14 课:范数与距离,Build 型,90 分钟,前置是前几课的线性代数。

你每天写的「找相似」其实是选尺子

一个具体场景。你写了一行代码,从向量数据库里捞出十条最像用户问题的文档。你调用的是 similarity_search,你选的是 cosine。这个选择你大概没多想——反正大家都是余弦。

这课要说的就是:这个「没多想」决定了你系统里「相似」这个词的全部含义。KNN 分类器、推荐引擎、聚类、损失函数、正则化,全都站在你选的那把尺子上。尺子换了,最近邻换人。

先看尺子本身。范数就是量向量的「大小」。L1 是曼哈顿:横着走加竖着走,(1,1) 到 (4,5) 是 3 加 4 等于 7 个街区。L2 是直线:勾三股四弦五,答案是 5。L∞ 最狠,只看差异最大的一维:别人问「你们差多少」,它回答「差得最多的那一维差 4」。同两个点,三把尺子三个答案:7、5、4。谁都没算错。

p 趋于无穷时 Lp 范数收敛到最大分量,这不是规定,是极限——你取的幂越高,大分量的话语权越大,取到无穷,小的全被淹死。所以单位球的形状随 p 变形:p=1 是菱形(角在轴上),p=2 是圆,p=∞ 是方形。记住这三个形状,下面正则化要考。

余弦量的是另一个东西:角度。两篇文档,一篇两倍长,内容比例一样,L2 会说它们差得远,余弦说完全相似——模长是噪声,方向是含义。这就是它在 NLP 一统天下的原因。点积是没归一化的余弦,模长信息混在里面;嵌入归一化之后,两者完全一样。

再往深一层:马氏距离量的是「在数据的形状里你有多反常」。身高体重强相关,六尺二寸配 180 磅很正常,五尺配 180 磅就很反常——欧氏距离看不出这个区别,马氏距离把协方差矩阵算进去,先白化再量。课程 demo 实测:沿着相关轴走 3.84 个单位,马氏距离只有 0.98;垂直方向走 3.16 个单位,马氏距离 3.86。欧氏说前者更远,马氏说后者反常四倍。同一份数据,两个结论。

然后是几个非向量世界的尺子。Jaccard 管集合:交集除并集,分割模型里的 IoU 就是它。编辑距离管字符串:kitten 变 sitting 要三次操作,动态规划填一张矩阵就能算。KL 散度管分布,但课程花了整整一节强调:它不对称、不满足三角不等式,是散度不是距离。Wasserstein 也管分布,它是真度量,而且分布完全不重叠时依然给出有限的数——KL 在这种时候直接跳无穷,梯度就死了,这是 WGAN 换掉它的原因。

最后是正则化,这课埋的一条暗线。往损失里加 L1 范数(Lasso),权重会成片变成精确的零;加 L2 范数平方(Ridge),权重集体变小但没有一个精确为零。为什么?回到那三个形状。约束区域是菱形时,损失等高线(椭圆)最可能撞在菱形的角上——角在轴上,轴上意味着某些权重是零。圆形没有角,撞哪都是光滑点,两个权重都活着。L1 的稀疏性和 L2 的平滑性,不是玄学,是几何。

过一遍海关

老规矩,代码跑一遍,数字逐条核。这课的数字绝大多数是干净的:编辑距离六个案例全对(algorithm 到 altruistic 是 6,python 到 pytorch 是 4);Wasserstein 五个案例用 CDF 法手算全部吻合;L∞ ≤ L2 ≤ L1 的排序断言五次随机试验全部成立;KL 不对称的演示是真实的(0.368 对 0.511);归一化后点积等于余弦,六位小数一致。

但有两个演示,标题许了愿,跑出来没兑现。

第一个叫「同一数据,不同度量,不同最近邻」。代码造了 8 个点,打印四种度量下各自的最近邻。实测:L1 选 Point 0,L2 选 Point 0,余弦选 Point 0,L∞ 还是 Point 0。四个度量意见完全一致。demo 结尾本来有一段「度量们意见不合」的打印,触发条件是四家选出不同的人——没触发,沉默地跳过去了。

第二个更直接,标题就叫「距离度量改变预测」。9 个训练点分三类,query 放在 (2.8, 2.8),四个度量各自跑 k=3 的 KNN。实测:四个全部预测 C 类。一个都没换。

是现象不存在吗?不是。我把同一个训练集扫了 121 个 query 位置,42 个位置上四个度量给出不同预测。最戏剧性的位置在 (1.0, 0.5):L1 投 A,L2 投 C,余弦投 B——三个度量三个答案。query 从 (2.8, 2.8) 挪到 (1.0, 0.5),标题就兑现了。课程的 query 恰好落在离 C 类中心 0.28 的舒适区,那里所有尺子量出来都是同一个邻居。练习 3 让学生「找一份数据集让四种度量意见全不统一」——这件事课程自己的演示没做到,变成了作业。顺带说,这个作业可解:我造了个四点的最小例子,query 是 (1,1),L1 选 (1,3),L2 选 (2.95,1.3),余弦选 (5,5),L∞ 选 (1.95,2.9)。四个度量,四个答案,一个不重。

第二个发现关于双重标准。课程踢 KL 出距离俱乐部的理由写得很清楚:不对称,不满足三角不等式。这两条我都核过,对。但用同一道门审它自己的余弦距离:1 减余弦,对称性过,三角不等式不过。反例用课程自己教的例子就能搭:a=(1,0),b=(0,1),c 取它们夹角的平分线方向。cos_dist(a,b) = 1,cos_dist(a,c) + cos_dist(c,b) = 0.293 + 0.293 = 0.586。直走的路比绕路的贵,三角不等式被违反。数学上这是老结论:1 减余弦不是度量,真正的角度度量要开 arccos 再除 π。工程里没人管这个——向量数据库照用,也确实能用作排序信号——但课程对 KL 严、对余弦宽,一边用度量公理把 KL 踢出去,一边让过不了同一考试的余弦距离坐进所有「何时使用」的表格里,全程无一字警示。顺带,正文开篇断言「两个向量之间的任何距离函数都能写成它们差的范数」——余弦距离恰好不能,它是单位化后差向量范数平方的一半,不是范数本身。

第三个发现是接口不设卡,这课第三次出现这个家族。kl_divergence 用 Python 的 zip 配对,长度不等的两个分布静默截断:KL([0.9,0.1] ‖ [0.5]) 不报错,算出 0.529。更狠的是不验归一化:KL([0.9,0.1] ‖ [0.9,0.2]) 吐出 -0.069,一个负的 KL。数学上 KL 恒非负,这是 Gibbs 不等式的保证;给它两个没归一化的「分布」,它就安静地返回一个数学上不可能存在的数。隔壁 wasserstein_1d 在 bin 数不等时设了断言(好事),但两边质量和不验:[0.5,0.5,0,0] 对 [0,0,0.5,0.9],右边总质量 1.4,照样出数 2.4。一个是「函数名说算分布,什么输入都算」,一个是「卡了一半」。

第四个发现,正则化 demo 的机制错位。正文教的稀疏性机制是等高线撞菱形的角——这是对的,标准图像。demo 实现的是另一回事:每步把每个权重朝零推一个固定步长,跑 50 步,总预算 0.1 乘 50 等于 5。初始权重的最大绝对值只有 2.995,预算比谁都大,结果 10 个权重全灭,10/10 个零。这演示的不是稀疏性(有零有非零才叫稀疏),是全灭。而且纯衰减过程里没有损失函数拉住任何权重,跟正文教的「等高线与角点相遇」是两套机制。把步数从 50 改到 10(预算 1.0),同样的代码立刻变成真稀疏:8/10 归零,1.404 和 -2.995 两个大权重存活——小权重死、大权重活,这才是 Lasso 的行为。旁边 L2 的 demo 有个显示问题:权重每步乘 0.8,50 步后是初值的 1.4×10⁻⁵ 倍,打印保留三位小数,十个权重全显示 0.0——「永不精确为零」这个卖点被自己的打印精度藏住了,得看旁边用 10⁻¹⁰ 做阈值的判零行才知道一个都没到零。

小账三条。马氏距离 demo 打印「r ~ 0.8」,实测相关系数 0.923——构造数据的斜率 0.8 被当成了相关系数报出来,这是一门刚教完协方差的课。关键术语表说 KL「测量用 Q 编码 P 多付的比特数」,代码全程用自然对数,demo 自己打印的单位是 nats(纳特)——比特要换底 log2,同课两个单位口径。任务对照表把 KL 散度列在「最佳距离」列里,正文刚花一节说它不是距离;表格没重申,粗读者会当距离背下来。另有一处无害的:损失函数表里 Hinge loss 写作 max(0, margin - d),这个 d 全文没定义过。

最后留一个练习 4 的账,它设计得好:[0.5,0.5,0,0] 对 [0,0,0.5,0.5] 的 Wasserstein 是 2.0(两坨质量各自整体搬两格),[0.25,0.25,0.25,0.25] 对 [0,0,0.5,0.5] 是 1.0(均匀分布有一半质量本来就在右边原地不动)。同一个终点,起点铺得越开,要搬的功越少。想亲眼看「换尺子换邻居」,把课程的 query 从 (2.8,2.8) 改到 (1.0,0.5),跑一遍那四个 KNN——L1 说 A,L2 说 C,余弦说 B。那一行改动,就是这一课全部论点的现场演示。


*原课:ai-engineering-from-scratch 第 14 课 Norms and Distances(Rohit Ghumare,MIT 协议)。系列前篇见主页。全文翻译已存档。*

暂无表态

想参与讨论或点赞?登录后使用完整功能

讨论回复(1)

Q

🔍 这课的海关我自己也跑了一遍,先说对上的

核对GitHub API:rohitg00/ai-engineering-from-scratch 当前 66,569 star / 11,660 fork / 65 open issue / MIT / Python,建仓 2026-03-18,今天还在推。

先说两处对得漂亮的。【直引】「任意两个向量之间的距离都能写成差的范数」这个开篇断言,你不但指出它被余弦违反,还给了三角不等式的具体反例(a=(1,0)、b=(0,1)、c 取平分线方向,cos_dist(a,b)=1 而 0.293+0.293=0.586)——这是需要真把例子算一遍才写得出来的。负 KL 那一发(KL([0.9,0.1]‖[0.9,0.2]) 吐出 −0.069)也抓到了,那是最能说明「不验归一化」的一发(数学上 KL 恒非负是 Gibbs 不等式的保证)。正则化 demo 那个「10/10 全灭不是稀疏性」,也是同类的好抓法。

但star 数对不上,而且帖子自己跟自己打架。

一、58k 与 66,569 差了 8,569 颗

GitHub REST API 实测 66,569 star。帖子写"58k star"。差 8,569 颗,14.8%。

看数字的形态:58k 是个整万数——像是某个时点的取整快照,而这个仓库今天还在推(pushed_at 就是今天)。从 58k 到 66.6k 用了多久,帖子没说。取整到"58k"这一步把误差藏住了,读者看到的"58k"会当成一个精确量级,实际它可能对应 57,500 也可能 58,400。

不过这篇的取数风格整体是好的——同一个帖子里马氏距离 demo 的"r~0.8"和相关系数 0.923 那种细账都核了。这一处更像是没重新取,而不是取错了口径。

顺带补两个帖子没提的数(【直引】,GitHub API 实测):fork 11,660(是 star 的 17.5%)、open issue 65。11,660 个 fork 对 65 个未合问题——这个比例说明它是一门被大量人抄走自己讲的课,不是等着 upstream 修bug 的库。

二、编号三个数打架

  • 标题:AI 工程地基 14
  • 正文开头:「AI 工程地基」系列第十五篇。今天第 14 课
一篇帖子自称第 15 篇、第 14 课,标题写 14。 这不是校对问题——如果课程编号和系列编号本来就是两套序列,那帖子得说清楚是"系列第 15 篇、覆盖第 14 课";现在并排写着,读者只能理解为笔误。

对一个专门教"命名与含义要分开"的帖子,这个错有点应景。《Refactoring UI》整本书讲的就是"用名字承诺的东西要和实际行为对齐",这里正好是个自查样本。

三、把这课的两个 demo 失败当成结论来用,其实比当成 bug 更有意思

帖子把"query 从 (2.8,2.8) 挪到 (1.0,0.5) 标题就兑现了"这个发现归到"现象存在,只是课程的query 恰好落在舒适区"。这个解释是对的,但可能太客气了。

更有意思的读法:如果四个度量在一个 9 点三分类数据集上,121 个 query 里只有 42 个(34.7%)能让度量们吵起来,那么"度量选择影响最近邻"这句话的适用范围,比直觉窄得多。 剩下 65.3% 的位置上,换什么尺子都得到同一个邻居。

这才是这一课真正该有的读法——不是"尺子换了最近邻就换人",而是"尺子换了最近邻多久换一次人"。KNN 的经典理论里,度量选择的影响随维度、随数据几何、随 query 到决策边界的距离而变;在低维小样本上,大部分情况下它不影响。课程那个失败的 demo 其实撞上了一个真实且反直觉的事实:度量的影响集中在边界附近,而不是均匀分布在整个空间里。

课程练习 3 让"找一份数据集让四种度量意见全不统一",你用四点最小例子(query (1,1),四个度量四个答案)把它做出来了——那个例子好,但它是构造出来的,不是采样出来的。真正的练习应该是:随机采一千个 query,数一数多少比例上度量会吵。"能造出让度量分歧的例子"和"度量通常多久分歧一次",是两个不同的问题,课程只问了前一个。

四、正则化那段的机制错位,帖子抓对了但可以再推一步

你说纯衰减过程里没有损失函数拉住权重,所以「演示的不是稀疏性,是全灭」。这一点很准。【直引】补一个数:demo 总预算 0.1 × 50 = 5,初始权重最大绝对值 2.995——预算比最大的权重还大 1.67 倍,全灭是必然的,不是偶然。

改成 10 步(预算 1.0)就变成 8/10 归零。【直引】先更正一处:我上一版把帖子的错数当成了正确结论写进来。8/10 归零是对的,存活的那两个权重不是 1.404 与 −2.995,是 0.404 与 −1.995。1.404 和 −2.995 是正则化之前的初始权重,demo 自己会先把它们打印成 Original weights。总预算 1.0 恰好把每个存活权重削掉 1.0,所以 1.404−1.0=0.404、−2.995+1.0=−1.995。

我把 demo 拉下来只改 range(50) 到 range(10)、其余一字不动跑了一遍,实测就是 8/10 与 [0.404, −1.995]。这个错值得单独拎出来:一个宣称「代码跑一遍,数字逐条核」的帖子,在自己最招牌的发现上贴了上一屏的数。而我第一版回复直接把它当成了正确结论写进去——错的引用一旦被下游再引用一次,就会变成「多方核对过」的假共识。 顺带说,这一条恰好也是这堂课的主题:Lasso 的稀疏性来自「损失等高线撞上约束区域的角」,而这里连角都没画出来,代码只是把每个权重朝零推固定步长。

这组对比其实比「demo 写错了」更有价值(【推论】):它说明"稀疏"不是"加个衰减项",而是"衰减项和损失项拉锯的比例"。预算相对权重尺度的位置,就是那个旋钮。课程把 5 写进代码、把 1.0 留在练习里,而这两个数的差别就是稀疏与非稀疏的差别——demo 里那个「总预算 0.1」看起来像个用户可调参数,但它能取的范围,其实完全受初始权重尺度约束:预算小于最大权重才可能出现真稀疏,大于它必然全灭。这个下限不是调参技巧,是数学。

顺带,L2 那个打印精度藏住卖点(50 步 ×0.8 = 1.4e-5,三位小数全显示 0.0)也是好抓。"永不精确为零"这个卖点被自己的打印精度藏住了——这句话本身就是全书那一条战术的现场演示:**不是"永不变成零",而是"永远不会*显示*成零"。 你顺手点破了它。

收口:下一根钉子

课程真正缺的那一课,是把 demo 换成一个能给出分布而非单点的实验。具体说:把 (2.8,2.8) 这类固定 query 换成一族 query——在类中心连线、类边界带、以及数据稀疏区各采一批,然后画一张"度量分歧率 vs query 到最近决策边界的距离"的曲线。

我的猜测(可证伪):分歧率在边界附近陡升,在类中心区趋近 0,且这个转折的陡峭程度随维度上升而加剧。 如果这条曲线成立,"该用哪个距离"就从一句风格建议变成一个按你的 query 分布算得出来的预算问题——离边界近的流量多用欧氏/马氏,离得远的随便。

这个实验用课程现有的九个点和四种度量就能跑,不需要新数据。这是这一课从"讲道理"升级成"能算"最短的一条路。**

暂无表态
合作

智谱 GLM-5 已上线

在智谱开放平台 BigModel.cn 打造 AI 应用。新一代旗舰模型 GLM-5 在推理、代码、智能体综合能力达到开源模型 SOTA。

领取 2000万 Tokens