我实现了一个函数(
这个函数在大多数情况下运行良好,但有一个奇怪的情况我不理解:
深入研究我的函数,
angle_between
)来计算两个向量之间的夹角。它利用类似于针尖的三角形,基于“针尖三角形的面积和角度计算错误”以及“计算向量之间角度的数值稳定方法”的相关问题。这个函数在大多数情况下运行良好,但有一个奇怪的情况我不理解:
import numpy as np
vectorA = np.array([0.008741225033460295, 1.1102230246251565e-16], dtype=np.float64)
vectorB = np.array([1, 0], dtype=np.float64)
angle_between(vectorA, vectorB) # is np.nan
深入研究我的函数,
np.nan
通过对负数求平方根产生,负数似乎是方法增加精度的结果:foo = 1.0 # np.linalg.norm(vectorA)
bar = 0.008741225033460295 # np.linalg.norm(vectorB)
baz = 0.9912587749665397 # np.linalg.norm(vectorA- vectorB)
# algebraically equivalent ... numerically not so much
order1 = baz - (foo - bar)
order2 = bar - (foo - baz)
assert order1 == 0
assert order2 == -1.3877787807814457e-17
根据Kahan的论文,这意味着三元组(foo, bar, baz)实际上并不代表三角形的边长。然而,鉴于我构造三角形的方式(请参见代码中的注释),事实上应该是这样的。
从这里开始,我有点迷失方向,不知道错误的源头在哪里。有人可以解释一下发生了什么吗?
为了完整起见,这是我的函数的完整代码:
import numpy as np
from numpy.typing import ArrayLike
def angle_between(
vec_a: ArrayLike, vec_b: ArrayLike, *, axis: int = -1, eps=1e-10
) -> np.ndarray:
"""Computes the angle from a to b
Notes
-----
Implementation is based on this post:
https://scicomp.stackexchange.com/a/27694
"""
vec_a = np.asarray(vec_a)[None, :]
vec_b = np.asarray(vec_b)[None, :]
if axis >= 0:
axis += 1
len_c = np.linalg.norm(vec_a - vec_b, axis=axis)
len_a = np.linalg.norm(vec_a, axis=axis)
len_b = np.linalg.norm(vec_b, axis=axis)
mask = len_a >= len_b
tmp = np.where(mask, len_a, len_b)
np.putmask(len_b, ~mask, len_a)
len_a = tmp
mask = len_c > len_b
mu = np.where(mask, len_b - (len_a - len_c), len_c - (len_a - len_b))
numerator = ((len_a - len_b) + len_c) * mu
denominator = (len_a + (len_b + len_c)) * ((len_a - len_c) + len_b)
mask = denominator > eps
angle = np.divide(numerator, denominator, where=mask)
np.sqrt(angle, out=angle)
np.arctan(angle, out=angle)
angle *= 2
np.putmask(angle, ~mask, np.pi)
return angle[0]
编辑:问题肯定与float64
有关,当使用更大的浮点数进行计算时,问题就会消失:
import numpy as np
vectorA = np.array([0.008741225033460295, 1.1102230246251565e-16], dtype=np.float128)
vectorB = np.array([1, 0], dtype=np.float128)
assert angle_between(vectorA, vectorB) == 0
bar + baz == 1 == foo
。 - Lukas Sbar + baz == 1 == foo
是由于浮点数不精确造成的?三个向量vectorA
、vectorB
和vectorA - vectorB
的长度应该始终形成一个有效的三角形,对吧?除此之外,函数应正确处理两种退化情况:vectorA == vectorB
和vectorA == -vectorB
。前者通过len_c
为0来处理,后者通过np.putmask(angle, ~mask, np.pi)
来处理。 - FirefoxMetzger1.1102230246251565e-16
,它们就会是倍数关系。我想1.1102230246251565e-16
与零之间的差异不足以避免问题。 - Lukas SvectorB
设置为vectorA
的倍数的情况 - 有趣的是,它有时会产生nan
,有时会产生0
,有时会失败并产生一个大小为1e-8
的小角度...你有什么想法吗? - FirefoxMetzger