
在进行科学计算时,尤其是在处理浮点数时,看似等价的操作有时会产生极其微小的数值差异,这些差异在默认的输出显示中可能被隐藏。本节将通过一个具体的numpy示例来揭示这种现象,并深入分析其背后的原因。
假设我们有以下两个NumPy数组:
import numpy as np
a = np.array([[ 0, 1, 10, 2, 5]])
b = np.array([[ 0, 1, 18, 15, 5],
[13, 9, 23, 3, 22],
[ 2, 10, 17, 4, 8]])我们希望计算 a 中每个向量与 b 中每个向量之间的欧氏距离的平方,并取负号后除以2。我们尝试两种不同的方法。
方法一:使用 np.linalg.norm 这种方法首先计算向量差的L2范数(即欧氏距离),然后将其平方。
m1 = -np.linalg.norm(a[:, np.newaxis, :] - b[np.newaxis, :, :], axis=-1) ** 2 / 2
print("m1:", m1)方法二:手动计算平方和 这种方法直接计算向量差的平方和,这正是欧氏距离平方的定义。
m2 = -np.sum(np.square(a[:, np.newaxis, :] - b[np.newaxis, :, :]), axis=-1) / 2
print("m2:", m2)当我们打印 m1 和 m2 的结果时,它们看起来是完全相同的:
m1: [[-116.5 -346. -73.5]] m2: [[-116.5 -346. -73.5]]
然而,当我们使用 np.array_equal 来检查这两个数组是否完全相等时,结果却出人意料:
print(f"np.array_equal(m1, m2): {np.array_equal(m1, m2)}")
# 输出: np.array_equal(m1, m2): False这表明 m1 和 m2 之间存在差异。更令人困惑的是,如果我们创建一个字面量数组 sanity_check,并与 m1 和 m2 进行比较,会发现:
sanity_check = np.array([[-116.5, -346. , -73.5]])
print(f"np.array_equal(sanity_check, m1): {np.array_equal(sanity_check, m1)}")
print(f"np.array_equal(sanity_check, m2): {np.array_equal(sanity_check, m2)}")
# 输出:
# np.array_equal(sanity_check, m1): False
# np.array_equal(sanity_check, m2): True这表明 m1 是“异常”的一个,它与我们期望的精确值不符,而 m2 却与精确值匹配。
要理解 m1 产生差异的原因,我们需要深入了解浮点数运算的精度问题。np.linalg.norm 函数在计算向量的L2范数时,其内部逻辑是计算 sqrt(sum(v_i^2))。当我们将 np.linalg.norm 的结果再次平方时,实际上执行了 (sqrt(sum(v_i^2)))^2。
在浮点数算术中,sqrt(X)**2 并不总是严格等于 X。由于计算机内部表示浮点数的限制,开方和平方这两个逆操作可能会引入微小的精度损失。
我们可以通过一个简单的例子来验证这一点:
val_squared = 8**2 + 13**2
print(f"8**2 + 13**2: {val_squared}")
print(f"np.sqrt(8**2 + 13**2)**2: {np.sqrt(val_squared)**2}")
# 输出:
# 8**2 + 13**2: 233
# np.sqrt(8**2 + 13**2)**2: 232.99999999999997可以看到,np.sqrt(233)**2 的结果略小于 233。正是这种微小的精度损失,导致了 m1 与 m2 之间的差异。m2 直接计算了平方和,避免了中间的开方操作,因此保留了更高的精度。
为了直观地看到 m1 和 m2 之间实际的数值差异,我们可以将它们转换为列表,以显示完整的浮点数精度:
print(f"m1.tolist(): {m1.tolist()}")
print(f"m2.tolist(): {m2.tolist()}")
# 输出:
# m1.tolist(): [[-116.49999999999999, -346.0, -73.5]]
# m2.tolist(): [[-116.5, -346.0, -73.5]]现在,m1 在第一个元素上的微小差异清晰可见,而 m2 则精确地保持了期望值。
为什么 print(m1) 和 print(m2) 的输出看起来完全相同,却在 np.array_equal 中表现出不同呢?这与NumPy的打印选项有关。NumPy通过 np.set_printoptions 函数控制数组的打印格式,包括浮点数的显示精度。
默认情况下,NumPy的打印选项可能设置了较低的显示精度(例如,precision=3),这意味着它只会显示小数点后几位,从而隐藏了那些超出显示精度的微小差异。
我们可以通过 np.get_printoptions() 查看当前的打印设置:
print(np.get_printoptions())
# 默认输出可能类似:
# {'edgeitems': 3, 'threshold': 1000, 'floatmode': 'maxprec', 'precision': 3, 'suppress': False, 'linewidth': 75, 'nanstr': 'nan', 'infstr': 'inf', 'sign': '-', 'formatter': None, 'legacy': False}其中 precision 参数控制了浮点数的显示精度。当 precision 设置为较小的值时,例如 3,像 232.99999999999997 这样的数字在打印时就会被四舍五入显示为 233.0。
如果我们将打印精度调高,例如设置为 17 位小数,这些隐藏的差异就会显现出来:
np.set_printoptions(precision=17)
print("m1 (高精度):", m1)
print("m2 (高精度):", m2)
# 输出:
# m1 (高精度): [[-116.4999999999999858 -346.0000000000000000 -73.5000000000000000]]
# m2 (高精度): [[-116.5000000000000000 -346.0000000000000000 -73.5000000000000000]]此时,m1 和 m2 之间的差异在打印输出中也变得可见。
通过上述分析,我们可以得出以下结论和最佳实践:
# 示例:使用 np.allclose 进行浮点数比较
print(f"np.allclose(m1, m2): {np.allclose(m1, m2)}")
# 输出: np.allclose(m1, m2): True (默认容差下认为相等)
# 我们可以通过调整 rtol 和 atol 参数来控制容差
# np.allclose(m1, m2, rtol=1e-05, atol=1e-08)通过理解这些浮点数计算的细微之处和NumPy的工具特性,我们可以更准确地进行数值分析和编程,避免潜在的精度陷阱。
以上就是NumPy中np.linalg.norm的数值精度与浮点数打印陷阱解析的详细内容,更多请关注php中文网其它相关文章!
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号