在一个简化的序列评分示例中,小型语言模型为每个选定的词元赋予概率
0.1。包含 400 次此类选择的序列,其数学上的乘积并不为零:
400 factors0.1×0.1×⋯×0.1=(0.1)400=10−400.
再加入一个同样可能的词元,乘积就是 10−401。这是两个不同的正数,
但 NumPy 使用 64 位浮点数计算时,会将两个乘积都存储为 0.0。基于
存储后乘积的排序因此丢失了这个差异。
对数保留了一种更有用的表示:两个乘积变成有限的和
400ln(0.1)≈−921.034037 和 401ln(0.1)≈−923.336622。学完
本课,你将能够把对数读作逆幂,推导把乘积转为和的法则,手算一个短序列
的分数,并实现同样的思想,同时不假装对数空间消除了所有数值限制。
本课反转了第 7 课中介绍的指数运算——第 7 课:面向 AI 的幂,这是课程四语言翻译集中对应的本地化页面。官方的
Mathematics for Machine Learning companion
将内容分为数学基础和使用这些基础的机器学习问题。其导论章节(英文
PDF)说明了在依赖概率模型之前
建立基础的重要性;下面的对数数学是原创的概率衔接,而不是归因于第 1 章的
定义。
缺少的答案是指数
对数提出一个精确的问题:以选定的底数,哪个指数能产生这个正数?
例如,log2(8)=3,因为 23=8;而
log10(0.01)=−2,因为 10−2=0.01。负数答案是指数,而不是负的
输入。
计算中经常出现三种底数:
| 名称 | 本文记号 | 底数 | 一种有用的理解 |
|---|
| 常用对数 | log10(x) | 10 | 十进制数量级 |
| 自然对数 | ln(x)=loge(x) | e≈2.71828 | 概率与优化公式 |
| 二进制对数 | log2(x) | 2 | 加倍与二的幂 |
有些领域书写不带底数的 log,但这种约定并不普遍。本课用 ln 表示自然
对数,并为其他底数写出下标。NumPy 也采用同样的实用选择:
numpy.log
逐元素计算自然对数。
只要两个表达式都处于其实数定义域中,这个逆关系就提供了两项检验:
logb(by)=yandblogb(x)=x.
乘法留下加法轨迹
乘积法则不是需要孤立记忆的模式;它直接来自第 7 课的指数法则。
取正数 a 和 c,并使用有效的对数底数 b>0、b=1。令
u=logb(a)andv=logb(c).
根据定义,a=bu 且 c=bv。相乘并应用同底数指数法则得到
ac=bubv=bu+v.
因此,产生 ac 的指数是 u+v:
logb(ac)=logb(a)+logb(c).
在相同假设下,除法会减去指数:
ca=bvbu=bu−v⟹logb(ca)=logb(a)−logb(c).
将正数提升到实数幂 r 会乘以其指数:
ar=(bu)r=bru⟹logb(ar)=rlogb(a).
正性在这三条法则中都发挥着实际作用:a>0 和 c>0 使每个实数对数都有
定义,而 c>0 也保证商的分母不为零。
加法不遵循乘积法则。一个简单的反例是
ln(2+3)=ln(5)≈1.609438,
但是
ln(2)+ln(3)=ln(6)≈1.791759.
所以一般而言 log(a+c)=log(a)+log(c)。本课后面会介绍一种用于相加
已存储为对数的值的稳定操作;它有意区别于将乘积转为和。
四次词元选择:两种表示的完整演算
假设两个候选续写各包含四次词元选择。在这个小计算中,将列出的每个 pi
视为模型在位置 i 为所选词元给出的正条件概率。将四个因子相乘得到序列
分数;将它们的自然对数相加得到相应的对数概率。对于模型下固定的已观测
序列,这些分数也称为其似然和对数似然。后续课程将展开完整的概率与估计
工具;这里的任务只是计算并比较这两种表示。
候选 A 的乘积是候选 B 的两倍。它的对数分数也更大:
−7.824046>−8.517193。由于 ln 的底数 e>1 且严格递增,这种排序是有
保证的。负的对数分数中“更大”意味着更接近零。分数取对数后并没有变成
概率;它变成了便于比较的概率表示。
Binary64 变为零,而对数分数仍在变化
计算机无法存储每一个实数。JavaScript 的 Number 和 NumPy 的 float64
使用 binary64 浮点格式。NumPy 通过
numpy.finfo:
报告其机器限制:最小的正正规 float64 值约为
2.2250738585072014×10−308。该数与零之间的部分间隙由正非正规值填充,但精度有所降低;最小的正非正规数约为 4.9406564584124654×10−324。
numpy.nextafter
确认这是从零向正方向的下一个可表示浮点数。
用指针或方向键移动任一滑块。上方面板展示 binary64 直接乘法经过正规区域、
狭窄的非正规带并最终归零的过程;下方面板展示有限的加法值 nln(p)。
带圆点的实线和带方块的虚线即使没有颜色也能区分,表格则显示所选状态和
转换行。
将一个正概率 p 重复为最多 2,000 个因子。直接 binary64 乘积可能从正规值经过非正规值再变为零,而累积自然对数和保持有限。读数对微小乘积使用四位有效数字,以免暗示不存在的精度。直接乘积:实线和圆点对数和:虚线和方块所选数量:点状指引线
- 直接 binary64 乘积
- 0
- 累积对数和
- -921.034037
- 直接乘积状态
- 零
- 第一个非正规因子数量
- 308
- 第一个零因子数量
- 324
p = 0.10 重复 400 次时,直接 binary64 乘积为 0(零);累积对数和为 -921.034037。
p = 0.10 重复 400 次时,直接 binary64 乘积为 0(零);累积对数和为 -921.034037。
此显示中的微小直接乘积会四舍五入到四位有效数字。可表示性转换由表格和状态标签而非额外小数位来识别。
将所选值和边界值显示为表格
所选值和可表示性里程碑处的直接 binary64 乘积与累积自然对数和| 因子数量 n | 直接乘积 | 累积对数和 | 直接乘积状态 |
|---|
| 1 | 1.000e-1 | -2.302585 | 正规 |
|---|
| 307 | 1.000e-307 | -706.893624 | 正规 |
|---|
| 308 | 1.000e-308 | -709.196209 | 非正规 |
|---|
| 323 | 9.881e-324 | -743.734985 | 非正规 |
|---|
| 324 | 0 | -746.037570 | 零 |
|---|
| 400 | 0 | -921.034037 | 零 ← 所选 |
|---|
| 2,000 | 0 | -4,605.170186 | 零 |
|---|
在服务器渲染的初始状态下,p=0.10 且 n=400。直接乘法已经产生了零,
但 400ln(0.10)=−921.034037… 仍然是有限值。这个对数值保留了正
因子的加法分数。在 float64 中调用 exp(−921.034037…) 仍会返回零,
因为改变表示形式无法创造出低于该格式范围的正 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) 且
y=ln(c),那么
ln(a+c)=ln(ex+ey),
而不是 x+y。NumPy 的
numpy.logaddexp
直接计算这个和的对数。对于两个已经取对数的备选项 x=−1000 和
y=−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>0, b>0, b=1 | 有效 | 存在唯一的实数指数 |
| x=0 | 作为实数对数未定义 | 没有有限指数能让正底数等于零 |
| x<0 | 在实数设置下未定义 | 有效的实数对数底数提升到实数幂后始终为正 |
| b=1 | 无效底数 | 对任意 y 都有 1y=1,因此不存在逆函数 |
| b≤0 | 对此实数对数定义无效 | 实数幂无法一致地产生每个正的真数 |
| logb(a+c)=logb(a)+logb(c) | 一般为假 | 右侧等于 logb(ac) |
对于实数数组,NumPy 在这个边界附近遵循浮点约定:np.log(0.0) 返回
-inf 并发出除零信号,而 np.log(-1.0) 返回 nan 并发出无效操作
信号。这些是有用的机器值,但不能据此抹去数学上的区别。真正为零的概率
因子会对对数分数贡献 −∞;而舍入为浮点零的正乘积本应在乘法之前
取对数。一旦计算出的乘积已经为零,np.log(product) 就无法重建丢失的
因子。
默默地用一个小正数替换零,也会改变模型声明的数值。如果截断是有意的建模
决定,应记录这条规则,而不要把它呈现为精确算术。
最后,排序方向取决于底数。对于每个 b>1 的底数——包括 e、2 和
10——对数都是递增的,因此更大的正乘积拥有更大的对数值。介于零和一
之间的有效底数会给出递减的对数,并反转这一排序。对数概率实现通常使用
e>1,这就是较不负的对数分数排名更高的原因。
有限分数可能就是有用的结果
开头的序列不需要把微小概率转换回十进制形式就能保持可区分。它们的对数
分数将乘法比较保留为普通加法,而乘积、商和幂法则解释了这种表示为何有效。
这一基础将支持后续关于概率、基于似然的估计、熵和稳定模型计算的课程。下一
个课程步骤会把重点转向作为约束的方程,以及满足方程的值的集合。现在的
实用结论很明确:在组合和比较正因子时将它们保留在对数空间,只有在结果既
必要又可表示时才回到概率空间。
检查你的理解
问题 1
计算 log₂(32) 和 log₁₀(0.001),然后用幂检验每个答案。
显示分步解答
对于 log2(32),要问的是 2 的哪个指数能产生 32:
25=32⟹log2(32)=5.对于 log10(0.001),将小数改写为十的幂:
0.001=10001=10−3⟹log10(0.001)=−3.两个真数都为正,底数 2 和 10 也都为正且不等于 1,因此这些实数对数
满足所需假设。检验就是幂等式 25=32 和 10−3=0.001。
问题 2
对于 x > 0 且 y > 0,将 ln(x³/y²) 展开为 x 和 y 的对数,并在每一步指出所用法则。
显示分步解答
先使用商法则,因为最外层运算是将两个正量相除:
ln(y2x3)=ln(x3)−ln(y2).然后对每一项使用幂法则:
ln(x3)−ln(y2)=3ln(x)−2ln(y).假设 x>0 和 y>0 使 x3、y2 及其商都为正;它们也确保
y2=0。因此,在给定假设下,展开式与原表达式拥有相同的实数定义域。
问题 3
一位队友写下 ln(4 + 5) = ln(4) + ln(5)。请反驳这一断言,并写出正确的乘积恒等式。
显示分步解答
分别看两边代表什么。左边是
ln(4+5)=ln(9).右边的和使用的是乘积法则:
ln(4)+ln(5)=ln(4×5)=ln(20).由于 9=20,它们的自然对数不相等。数值上,
ln(9)≈2.197225,而 ln(20)≈2.995732。有效的恒等式是
ln(ac)=ln(a)+ln(c)其中 a>0 且 c>0。它把乘法(而非加法)转换成求和。
问题 4
一个三词元候选有正因子 0.5、0.2 和 0.1。请逐步计算其直接乘积和自然对数分数,然后将对数分数转换回来。
显示分步解答
从左到右相乘:
0.5×0.2=0.1,0.1×0.1=0.01.对于对数表示,计算并相加三个项:
ln(0.5)≈−0.693147,ln(0.2)≈−1.609438,ln(0.1)≈−2.302585,−0.693147−1.609438−2.302585=−4.605170.乘积法则说明这个和等于 ln(0.01)。应用逆指数得到
e−4.605170≈0.01.转换结果与直接乘积一致;显示小数所暗示的微小差异,仅来自将对数项四舍
五入到六位小数。
问题 5
两个候选的自然对数分数分别为 −40 和 −42。哪个乘积更大,相差多少倍?
显示分步解答
自然对数使用底数 e>1,因此保留排序。由于 −40>−42,对数分数为
−40 的候选拥有更大的正乘积。
令两个乘积为 A=e−40 和 B=e−42。它们的比值使用指数的商法则:
BA=e−42e−40=e−40−(−42)=e2≈7.389.因此,第一个候选的乘积约为第二个的 7.389 倍。对数空间中的两点差异,
就是原始空间中的乘法比值。
问题 6
当 p = 0.1 时,探索器显示 n = 323 的直接乘积为非正规值,n = 324 时为零。对数分数发生什么变化?这说明对它取指数时会怎样?
显示分步解答
加法分数是 nln(p)。取 ln(0.1)≈−2.302585093:
323ln(0.1)≈−743.734985,324ln(0.1)≈−746.037570.两者都是有限值且仍然不同,尽管在这个经过验证的示例中,直接 binary64
乘法在 324 个因子时达到零。因此,对数空间累积仍可以比较这些正因子序列。
然而,对 −746.037570 取指数要求 float64 表示约为 10−324 的数,在
舍入后低于其正数范围,因此存储结果为零。对数表示保留了有限分数,却不会
扩展 binary64 的输出范围。
问题 7
调试这段用于相加两个对数备选项的代码:np.log(np.exp(-1000) + np.exp(-1001))。它为什么返回 −inf?哪个 NumPy 运算能保留有限的组合对数分数?
显示分步解答
这段代码先取指数。在 float64 中,两个中间值都小到无法表示:
float64(e−1000)=0,float64(e−1001)=0.程序随后计算 ln(0+0)=ln(0),NumPy 将其表示为 -inf,并发出除零
信号。
使用专门为已处于对数空间的备选项设计的运算:
combined = np.logaddexp(-1000.0, -1001.0)
# -999.6867383124818
它计算 ln(e−1000+e−1001),而不会先将两个指数都舍入为零。这不是
乘积法则:正因子的乘积使用 np.log(factors).sum(),而本例相加的是两个
备选项。最后,np.exp(combined) 仍返回 0.0;有限的组合分数得以保留,
但其概率尺度的值在 float64 中仍不可表示。