AI 数学课程 · 第 8 课,共 180

面向 AI 的对数:让微小的概率乘积不再消失

学习把对数看作逆幂,推导乘积转求和的法则,并使用 NumPy 对数分数区分被 float64 舍入为零的概率乘积。

分享这篇文章

在一个简化的序列评分示例中,小型语言模型为每个选定的词元赋予概率 0.10.1。包含 400 次此类选择的序列,其数学上的乘积并不为零:

0.1×0.1××0.1400 factors=(0.1)400=10400.\underbrace{0.1\times0.1\times\cdots\times0.1}_{400\text{ factors}} =(0.1)^{400}=10^{-400}.

再加入一个同样可能的词元,乘积就是 1040110^{-401}。这是两个不同的正数, 但 NumPy 使用 64 位浮点数计算时,会将两个乘积都存储为 0.0。基于 存储后乘积的排序因此丢失了这个差异。

对数保留了一种更有用的表示:两个乘积变成有限的和 400ln(0.1)921.034037400\ln(0.1)\approx-921.034037401ln(0.1)923.336622401\ln(0.1)\approx-923.336622。学完 本课,你将能够把对数读作逆幂,推导把乘积转为和的法则,手算一个短序列 的分数,并实现同样的思想,同时不假装对数空间消除了所有数值限制。

本课反转了第 7 课中介绍的指数运算——第 7 课:面向 AI 的幂,这是课程四语言翻译集中对应的本地化页面。官方的 Mathematics for Machine Learning companion 将内容分为数学基础和使用这些基础的机器学习问题。其导论章节(英文 PDF)说明了在依赖概率模型之前 建立基础的重要性;下面的对数数学是原创的概率衔接,而不是归因于第 1 章的 定义。

缺少的答案是指数

对数提出一个精确的问题:以选定的底数,哪个指数能产生这个正数?

例如,log2(8)=3\log_2(8)=3,因为 23=82^3=8;而 log10(0.01)=2\log_{10}(0.01)=-2,因为 102=0.0110^{-2}=0.01。负数答案是指数,而不是负的 输入。

计算中经常出现三种底数:

名称本文记号底数一种有用的理解
常用对数log10(x)\log_{10}(x)1010十进制数量级
自然对数ln(x)=loge(x)\ln(x)=\log_e(x)e2.71828e\approx2.71828概率与优化公式
二进制对数log2(x)\log_2(x)22加倍与二的幂

有些领域书写不带底数的 log,但这种约定并不普遍。本课用 ln\ln 表示自然 对数,并为其他底数写出下标。NumPy 也采用同样的实用选择: numpy.log 逐元素计算自然对数。

只要两个表达式都处于其实数定义域中,这个逆关系就提供了两项检验:

logb(by)=yandblogb(x)=x.\log_b(b^y)=y \qquad\text{and}\qquad b^{\log_b(x)}=x.

乘法留下加法轨迹

乘积法则不是需要孤立记忆的模式;它直接来自第 7 课的指数法则。

取正数 aacc,并使用有效的对数底数 b>0b>0b1b\ne1。令

u=logb(a)andv=logb(c).u=\log_b(a)\quad\text{and}\quad v=\log_b(c).

根据定义,a=bua=b^uc=bvc=b^v。相乘并应用同底数指数法则得到

ac=bubv=bu+v.ac=b^u b^v=b^{u+v}.

因此,产生 acac 的指数是 u+vu+v

logb(ac)=logb(a)+logb(c).\boxed{\log_b(ac)=\log_b(a)+\log_b(c)}.

在相同假设下,除法会减去指数:

ac=bubv=buvlogb ⁣(ac)=logb(a)logb(c).\frac{a}{c}=\frac{b^u}{b^v}=b^{u-v} \quad\Longrightarrow\quad \boxed{\log_b\!\left(\frac{a}{c}\right)=\log_b(a)-\log_b(c)}.

将正数提升到实数幂 rr 会乘以其指数:

ar=(bu)r=brulogb(ar)=rlogb(a).a^r=(b^u)^r=b^{ru} \quad\Longrightarrow\quad \boxed{\log_b(a^r)=r\log_b(a)}.

正性在这三条法则中都发挥着实际作用:a>0a>0c>0c>0 使每个实数对数都有 定义,而 c>0c>0 也保证商的分母不为零。

加法遵循乘积法则。一个简单的反例是

ln(2+3)=ln(5)1.609438,\ln(2+3)=\ln(5)\approx1.609438,

但是

ln(2)+ln(3)=ln(6)1.791759.\ln(2)+\ln(3)=\ln(6)\approx1.791759.

所以一般而言 log(a+c)log(a)+log(c)\log(a+c)\ne\log(a)+\log(c)。本课后面会介绍一种用于相加 已存储为对数的值的稳定操作;它有意区别于将乘积转为和。

四次词元选择:两种表示的完整演算

假设两个候选续写各包含四次词元选择。在这个小计算中,将列出的每个 pip_i 视为模型在位置 ii 为所选词元给出的正条件概率。将四个因子相乘得到序列 分数;将它们的自然对数相加得到相应的对数概率。对于模型下固定的已观测 序列,这些分数也称为其似然和对数似然。后续课程将展开完整的概率与估计 工具;这里的任务只是计算并比较这两种表示。

候选 A 的乘积是候选 B 的两倍。它的对数分数也更大: 7.824046>8.517193-7.824046>-8.517193。由于 ln\ln 的底数 e>1e>1 且严格递增,这种排序是有 保证的。负的对数分数中“更大”意味着更接近零。分数取对数后并没有变成 概率;它变成了便于比较的概率表示。

Binary64 变为零,而对数分数仍在变化

计算机无法存储每一个实数。JavaScript 的 Number 和 NumPy 的 float64 使用 binary64 浮点格式。NumPy 通过 numpy.finfo: 报告其机器限制:最小的正正规 float64 值约为 2.2250738585072014×103082.2250738585072014\times10^{-308}。该数与零之间的部分间隙由正非正规值填充,但精度有所降低;最小的正非正规数约为 4.9406564584124654×103244.9406564584124654\times10^{-324}numpy.nextafter 确认这是从零向正方向的下一个可表示浮点数。

用指针或方向键移动任一滑块。上方面板展示 binary64 直接乘法经过正规区域、 狭窄的非正规带并最终归零的过程;下方面板展示有限的加法值 nln(p)n\ln(p)。 带圆点的实线和带方块的虚线即使没有颜色也能区分,表格则显示所选状态和 转换行。

将一个正概率 p 重复为最多 2,000 个因子。直接 binary64 乘积可能从正规值经过非正规值再变为零,而累积自然对数和保持有限。读数对微小乘积使用四位有效数字,以免暗示不存在的精度。

将 p 选择在 0.01 到 0.50 之间。方向键每次改变 0.01。

0.10

选择 1 到 2,000 个重复因子。方向键每次改变一个。

400
直接乘积:实线和圆点对数和:虚线和方块所选数量:点状指引线
直接概率乘积与累积对数和两个上下排列的图共享横轴上的因子数量。上方带圆点的实线穿过正规和非正规 binary64 区域,然后继续为点状零线。下方带方块的虚线保持为有限的直线累积自然对数和。点状竖直指引线标出所选数量。直接 binary64 乘积与可表示性0-100-200-300正规值非正规值:高于下限的狭窄区间零:点状下限pⁿ 的 log₁₀ 数量级累积自然对数和-0-1,151-2,303-3,454-4,60505001,0001,5002,000n ln(p)因子数量 n
直接 binary64 乘积
0
累积对数和
-921.034037
直接乘积状态
第一个非正规因子数量
308
第一个零因子数量
324

p = 0.10 重复 400 次时,直接 binary64 乘积为 0(零);累积对数和为 -921.034037。

p = 0.10 重复 400 次时,直接 binary64 乘积为 0(零);累积对数和为 -921.034037。

此显示中的微小直接乘积会四舍五入到四位有效数字。可表示性转换由表格和状态标签而非额外小数位来识别。

将所选值和边界值显示为表格
所选值和可表示性里程碑处的直接 binary64 乘积与累积自然对数和
因子数量 n直接乘积累积对数和直接乘积状态
11.000e-1-2.302585正规
3071.000e-307-706.893624正规
3081.000e-308-709.196209非正规
3239.881e-324-743.734985非正规
3240-746.037570
4000-921.034037 ← 所选
2,0000-4,605.170186

在服务器渲染的初始状态下,p=0.10p=0.10n=400n=400。直接乘法已经产生了零, 但 400ln(0.10)=921.034037400\ln(0.10)=-921.034037\ldots 仍然是有限值。这个对数值保留了正 因子的加法分数。在 float64 中调用 exp(921.034037)\exp(-921.034037\ldots) 仍会返回零, 因为改变表示形式无法创造出低于该格式范围的正 float64 值。

确切的转换取决于数据类型、数值以及浮点运算的顺序。这个探索器有意模拟重复 的 binary64 乘法;它不是所有概率程序的通用阈值。

NumPy 复现安全路径

下面的程序使用 NumPy 2.5.2 运行。直接乘法使用 np.prod;而对数空间累积 使用 numpy.log 所记录的逐元素自然对数,再进行求和。

import numpy as np

for n in (323, 324, 400, 401):
    factors = np.full(n, 0.1, dtype=np.float64)
    direct = np.prod(factors)
    log_score = np.log(factors).sum()
    print(f"n={n}: direct={direct:.4e}, log_score={log_score:.6f}")

limits = np.finfo(np.float64)
print(f"smallest normal:    {limits.smallest_normal:.4e}")
print(f"smallest subnormal: {limits.smallest_subnormal:.4e}")
print(f"next after zero:    {np.nextafter(0.0, 1.0):.4e}")
n=323: direct=9.8813e-324, log_score=-743.734985
n=324: direct=0.0000e+00, log_score=-746.037570
n=400: direct=0.0000e+00, log_score=-921.034037
n=401: direct=0.0000e+00, log_score=-923.336622
smallest normal:    2.2251e-308
smallest subnormal: 4.9407e-324
next after zero:    4.9407e-324

400 因子和 401 因子的乘积在存储时都变成零,而它们的对数分数仍保持顺序。 在实践中,需要比较候选乘积的系统可以保留对数分数并选择较大的一个,而不必 对每个分数取指数。这里所说的是狭义的数值收益:正的乘法因子变成有限的 加法分数。它无法修复无效输入、其他运算中的舍入,或结果对于所选类型而言 过小的最终转换。

相加备选项的概率需要另一个恒等式。如果 x=ln(a)x=\ln(a)y=ln(c)y=\ln(c),那么

ln(a+c)=ln(ex+ey),\ln(a+c)=\ln(e^x+e^y),

而不是 x+yx+y。NumPy 的 numpy.logaddexp 直接计算这个和的对数。对于两个已经取对数的备选项 x=1000x=-1000y=1001y=-1001

x, y = -1000.0, -1001.0

with np.errstate(divide="ignore"):
    naive = np.log(np.exp(x) + np.exp(y))

stable = np.logaddexp(x, y)
print(naive)
print(stable)
print(np.exp(stable))
-inf
-999.6867383124818
0.0

朴素路径过早取指数,将两个备选项都变成零。np.logaddexp 保留了它们有限 的组合对数分数。对这个分数取指数仍会下溢,正如最后一行所示。对于正因子的 乘积,使用 sum(log_factors);当相加备选项已经由对数值表示时, 使用 logaddexp

值得明确说明的边界

实数对数的定义域和底数条件可以避免几个常见错误:

输入或断言实数对数结果原因
x>0x>0, b>0b>0, b1b\ne1有效存在唯一的实数指数
x=0x=0作为实数对数未定义没有有限指数能让正底数等于零
x<0x<0在实数设置下未定义有效的实数对数底数提升到实数幂后始终为正
b=1b=1无效底数对任意 yy 都有 1y=11^y=1,因此不存在逆函数
b0b\le0对此实数对数定义无效实数幂无法一致地产生每个正的真数
logb(a+c)=logb(a)+logb(c)\log_b(a+c)=\log_b(a)+\log_b(c)一般为假右侧等于 logb(ac)\log_b(ac)

对于实数数组,NumPy 在这个边界附近遵循浮点约定:np.log(0.0) 返回 -inf 并发出除零信号,而 np.log(-1.0) 返回 nan 并发出无效操作 信号。这些是有用的机器值,但不能据此抹去数学上的区别。真正为零的概率 因子会对对数分数贡献 -\infty;而舍入为浮点零的正乘积本应在乘法之前 取对数。一旦计算出的乘积已经为零,np.log(product) 就无法重建丢失的 因子。

默默地用一个小正数替换零,也会改变模型声明的数值。如果截断是有意的建模 决定,应记录这条规则,而不要把它呈现为精确算术。

最后,排序方向取决于底数。对于每个 b>1b>1 的底数——包括 ee221010——对数都是递增的,因此更大的正乘积拥有更大的对数值。介于零和一 之间的有效底数会给出递减的对数,并反转这一排序。对数概率实现通常使用 e>1e>1,这就是较不负的对数分数排名更高的原因。

有限分数可能就是有用的结果

开头的序列不需要把微小概率转换回十进制形式就能保持可区分。它们的对数 分数将乘法比较保留为普通加法,而乘积、商和幂法则解释了这种表示为何有效。

这一基础将支持后续关于概率、基于似然的估计、熵和稳定模型计算的课程。下一 个课程步骤会把重点转向作为约束的方程,以及满足方程的值的集合。现在的 实用结论很明确:在组合和比较正因子时将它们保留在对数空间,只有在结果既 必要又可表示时才回到概率空间。

检查你的理解

问题 1

计算 log₂(32) 和 log₁₀(0.001),然后用幂检验每个答案。

显示分步解答

对于 log2(32)\log_2(32),要问的是 22 的哪个指数能产生 3232

25=32log2(32)=5.2^5=32 \quad\Longrightarrow\quad \log_2(32)=5.

对于 log10(0.001)\log_{10}(0.001),将小数改写为十的幂:

0.001=11000=103log10(0.001)=3.0.001=\frac{1}{1000}=10^{-3} \quad\Longrightarrow\quad \log_{10}(0.001)=-3.

两个真数都为正,底数 221010 也都为正且不等于 11,因此这些实数对数 满足所需假设。检验就是幂等式 25=322^5=32103=0.00110^{-3}=0.001

问题 2

对于 x > 0 且 y > 0,将 ln(x³/y²) 展开为 x 和 y 的对数,并在每一步指出所用法则。

显示分步解答

先使用商法则,因为最外层运算是将两个正量相除:

ln ⁣(x3y2)=ln(x3)ln(y2).\ln\!\left(\frac{x^3}{y^2}\right) =\ln(x^3)-\ln(y^2).

然后对每一项使用幂法则:

ln(x3)ln(y2)=3ln(x)2ln(y).\ln(x^3)-\ln(y^2) =3\ln(x)-2\ln(y).

假设 x>0x>0y>0y>0 使 x3x^3y2y^2 及其商都为正;它们也确保 y20y^2\ne0。因此,在给定假设下,展开式与原表达式拥有相同的实数定义域。

问题 3

一位队友写下 ln(4 + 5) = ln(4) + ln(5)。请反驳这一断言,并写出正确的乘积恒等式。

显示分步解答

分别看两边代表什么。左边是

ln(4+5)=ln(9).\ln(4+5)=\ln(9).

右边的和使用的是乘积法则:

ln(4)+ln(5)=ln(4×5)=ln(20).\ln(4)+\ln(5)=\ln(4\times5)=\ln(20).

由于 9209\ne20,它们的自然对数不相等。数值上, ln(9)2.197225\ln(9)\approx2.197225,而 ln(20)2.995732\ln(20)\approx2.995732。有效的恒等式是

ln(ac)=ln(a)+ln(c)\ln(ac)=\ln(a)+\ln(c)

其中 a>0a>0c>0c>0。它把乘法(而非加法)转换成求和。

问题 4

一个三词元候选有正因子 0.5、0.2 和 0.1。请逐步计算其直接乘积和自然对数分数,然后将对数分数转换回来。

显示分步解答

从左到右相乘:

0.5×0.2=0.1,0.1×0.1=0.01.0.5\times0.2=0.1, \qquad 0.1\times0.1=0.01.

对于对数表示,计算并相加三个项:

ln(0.5)0.693147,\ln(0.5)\approx-0.693147,ln(0.2)1.609438,ln(0.1)2.302585,\ln(0.2)\approx-1.609438, \qquad \ln(0.1)\approx-2.302585,0.6931471.6094382.302585=4.605170.-0.693147-1.609438-2.302585=-4.605170.

乘积法则说明这个和等于 ln(0.01)\ln(0.01)。应用逆指数得到

e4.6051700.01.e^{-4.605170}\approx0.01.

转换结果与直接乘积一致;显示小数所暗示的微小差异,仅来自将对数项四舍 五入到六位小数。

问题 5

两个候选的自然对数分数分别为 −40 和 −42。哪个乘积更大,相差多少倍?

显示分步解答

自然对数使用底数 e>1e>1,因此保留排序。由于 40>42-40>-42,对数分数为 40-40 的候选拥有更大的正乘积。

令两个乘积为 A=e40A=e^{-40}B=e42B=e^{-42}。它们的比值使用指数的商法则:

AB=e40e42=e40(42)=e27.389.\frac{A}{B}=\frac{e^{-40}}{e^{-42}}=e^{-40-(-42)}=e^2\approx7.389.

因此,第一个候选的乘积约为第二个的 7.3897.389 倍。对数空间中的两点差异, 就是原始空间中的乘法比值。

问题 6

当 p = 0.1 时,探索器显示 n = 323 的直接乘积为非正规值,n = 324 时为零。对数分数发生什么变化?这说明对它取指数时会怎样?

显示分步解答

加法分数是 nln(p)n\ln(p)。取 ln(0.1)2.302585093\ln(0.1)\approx-2.302585093

323ln(0.1)743.734985,323\ln(0.1)\approx-743.734985,324ln(0.1)746.037570.324\ln(0.1)\approx-746.037570.

两者都是有限值且仍然不同,尽管在这个经过验证的示例中,直接 binary64 乘法在 324 个因子时达到零。因此,对数空间累积仍可以比较这些正因子序列。

然而,对 746.037570-746.037570 取指数要求 float64 表示约为 1032410^{-324} 的数,在 舍入后低于其正数范围,因此存储结果为零。对数表示保留了有限分数,却不会 扩展 binary64 的输出范围。

问题 7

调试这段用于相加两个对数备选项的代码:np.log(np.exp(-1000) + np.exp(-1001))。它为什么返回 −inf?哪个 NumPy 运算能保留有限的组合对数分数?

显示分步解答

这段代码先取指数。在 float64 中,两个中间值都小到无法表示:

float64(e1000)=0,float64(e1001)=0.\operatorname{float64}(e^{-1000})=0, \qquad \operatorname{float64}(e^{-1001})=0.

程序随后计算 ln(0+0)=ln(0)\ln(0+0)=\ln(0),NumPy 将其表示为 -inf,并发出除零 信号。

使用专门为已处于对数空间的备选项设计的运算:

combined = np.logaddexp(-1000.0, -1001.0)
# -999.6867383124818

它计算 ln(e1000+e1001)\ln(e^{-1000}+e^{-1001}),而不会先将两个指数都舍入为零。这不是 乘积法则:正因子的乘积使用 np.log(factors).sum(),而本例相加的是两个 备选项。最后,np.exp(combined) 仍返回 0.0;有限的组合分数得以保留, 但其概率尺度的值在 float64 中仍不可表示。

资料来源

  1. Mathematics for Machine Learning companion website
  2. Mathematics for Machine Learning book PDF
  3. NumPy documentation: numpy.log
  4. NumPy documentation: numpy.logaddexp
  5. NumPy documentation: numpy.finfo
  6. NumPy documentation: numpy.nextafter