【问题标题】:best practices on floating point precision in pythonpython中浮点精度的最佳实践
【发布时间】:2019-04-12 23:01:59
【问题描述】:

今晚早些时候,我的一个朋友刚刚递给我这个可爱的问题。问题说:

MATLAB 中编写一个程序来检查一个点是否在三角形内。不要忘记检查该点是否也在边界上。 三角点是x=(0,0),y=(0,1)z=(1,0)

这个问题并不难解决。这个想法是找到斜边的方程并检查该点是否位于三角形的任何一条腿上。然而,检查内部和外部结果并没有那么困难。

我在MATLAB上编写了代码,逻辑似乎没问题。但问题是结果与那个逻辑不协调!我开始质疑我的代码,因为我对 MATLAB 不太熟练。尽管如此,我还是尝试了我的首选语言 Python。

这是我的代码:


def isInsideTriangle(x,y):
    if  x == 0 or y == 0 or y ==  1-x:
        print('on the border of the triangle')
    elif x > 1 or y > 1 or x < 0 or y < 0 or y > 1-x:
            print('outside of the triangle')
            print(1-x)  # check the value
    else:
        # verbose these values to double check
        print(1-x)
        print(y)
        print(type(y))
        print(type(1-x))
        print(y==(1-x))
        print('inside of the triangle')

isInsideTriangle(0.2,0.8)

尝试使用这两个值时,控制台上的结果应为on the border。然而程序却说是inside!我尝试在xy 之间切换,即isInsideTriangle(0.8,0.2),但这次程序输出了预期的结果。

这使我意识到与逻辑无关,而与浮点精度有关。我将 MATLAB 上的变量大小增加到 64 位 精度,并且程序运行良好。

我现在的问题

作为一名 Python 人,在 Python 中避免此类问题的最佳编程实践是什么?我们如何才能避免这种烦人的问题,特别是在生产环境中?

【问题讨论】:

  • 你试过用Decimal代替浮点数吗? docs.python.org/3.7/library/decimal.html
  • 谢谢,我会试试看的。但是,您认为在每个数值变量上选择小数是最佳做法吗?
  • Decimal 仍然是浮点数,并且仍然受到几乎所有二进制浮点数的限制。它只是 decimal 浮点数,因此它的行为更符合受过训练以将小数视为数字的自然表示的人类的直觉。它仍然不能表示像 1/3 这样的数字,并且仍然存在舍入误差。
  • 如果要检查一个点是否在多边形内,我建议使用缠绕数:geomalgorithms.com/a03-_inclusion.html

标签: python floating-point double precision


【解决方案1】:

前言

您的问题是“一个点在正多边形内吗?”的专业化,我的意思是不是我们经常在 GIS 系统中发现的自相交或多多边形。并且由于您要求三角形,因此我假设多边形是凸的。

有趣的是,叉积是解决它的关键。当您在 2D 平面中处理向量时,叉积与该平面正交。要提取的有用信息是:它是向上还是向下?

Float Arithmetic 会发生错误,当叉积接近于零但不等于时,它变得很关键,然后它将有符号而不是 null。

要检查您的点是否在多边形内,只需检查边缘和点之间的所有叉积是否具有相同的符号,例如:sign(h x w) = -1

同理,要检查多边形是否凸,就要检查所有连续边的叉积是否具有相同的符号,例如:sign(u x v) = -1

实施

让我们构建一个小类来检查一个点是否在正凸多边形的内部(在边缘或外部):

import numpy as np
class cpoly:

    def __init__(self, points=[[0,0], [0,1], [1,0]], assert_convexity=True):
        """
        Initialize 2D Polygon with a sequence of 2D points
        """
        self._points = np.array(points)
        assert self.p.shape[0] >= 3
        assert self.p.shape[1] == 2
        assert self.is_convex or not(assert_convexity)

    @property
    def n(self):
        return self.p.shape[0]

    @property
    def p(self):
        return self._points

    @property
    def is_convex(self):
        """
        Check convexity of the polygon (operational for a non intersecting polygon)
        """
        return self.contains()

    def contains(self, p=None, debug=False, atol=2e-16):
        """
        Check if a 2D convex polygon contains a point (also used to assess convexity)
        Returns:
           -1: Point is oustide the polygon
            0: Point is close to polygon edge (epsilon ball)
           +1: Point is inside the polygon
        """
        s = None
        c = False
        n = self.n
        for k in range(n):
            # Vector Differences:
            d1 = self.p[(k+1)%n,:] - self.p[k%n,:]
            if p:
                d2 = p - self.p[k%n,:]
            else:
                d2 = self.p[(k+2)%n,:] - self.p[(k+1)%n,:]
            # Cross Product:
            z = np.cross(d1, d2)
            if np.allclose(z, 0, atol=atol):
                s_ = 0
                c = True
            else:
                s_ = np.sign(z)
            # Debug Helper:
            if debug:
                print("k = %d, d1 = %s, d2 = %s, z = %.32f, s = %d" % (k, d1, d2, z, s_))
            # Check if cross product sign change (excluded null, when point is colinear with the segment)
            if s and (s_ != s) and not(s_ == 0):
                # Nota: Integer are exact if float representable, therefore comparizons are correct
                return -1
            s = s_
        if c:
            return 0
        else:
            return 1

    def plot(self, axe=None):
        import matplotlib.pyplot as plt
        from matplotlib.patches import Polygon
        if not(axe):
            fig, axe = plt.subplots()
            axe.plot(self.p[:,0], self.p[:,1], 'x', markersize=10, label='Points $p_i$')
            axe.add_patch(Polygon(self.p, alpha=0.4, label='Area'))
            axe.set_xlabel("$x$")
            axe.set_ylabel("$y$")
            axe.set_title("Polygon")
            axe.set_aspect('equal')
            axe.legend(bbox_to_anchor=(1,1), loc='upper left')
            axe.grid()
        return axe.get_figure(), axe

这个类被初始化为一个二维点列表(默认是你的)。

p = cpoly()

在我的设置中,浮点精度大约是:

e = np.finfo(np.double).eps # 2.220446049250313e-16

我们创建一个试验数据集用于测试目的:

p = cpoly()
r = [
    [0,0], [0,1], [1,0], # Polygon vertices
    [0,0.5], [-e,0.6], [e,0.4], [0.1, 0.1], [1,1],
    [0.5+e,0.5], [0.3-e,0.7], [0.7+e/10,0.3],
    [0, 1.2], [1.2, 0.], # Those points make your logic fails
    [0.2,0.8], [0.1,0.9],
    [0.8+10*e,0.2], [0.9+10*e,0.1]
]

如果我们调整您的函数以获得合规的输出:

def isInsideTriangle(x,y):
    if  x == 0 or y == 0 or y ==  1-x:
        return 0
    elif x > 1 or y > 1 or x < 0 or y < 0 or y > 1-x:
        return -1
    else:
        return 1

然后我们检查试验点,看看我们的两个函数是否表现良好:

_, axe = p.plot()
cols = {-1: 'red', 0:'orange', 1:'green'}
for x in r:
    q1 = p.contains(x)
    q2 = isInsideTriangle(*x)
    print(q1==q2)
    axe.plot(*x, 'o', markersize=4, color=cols[q1])

使用此设置,所有点都被正确分类。但是你可以看到你的算法是有缺陷的。主要是以下几行:

if  x == 0 or y == 0 or y ==  1-x:

未能拒绝[0, 1.2][1.2, 0.]

靠近边缘的点和浮点运算错误

即使对于像[0.2,0.8] 这样的点正好在边缘上,浮点错误也会导致点错误分类。以下几点将使叉积不完全等于零。详情见下文:

p.contains([0.2,0.8], debug=True) # True
# k = 0, d1 = [0 1], d2 = [0.2 0.8], z = -0.20000000000000001110223024625157, s = -1
# k = 1, d1 = [ 1 -1], d2 = [ 0.2 -0.2], z = 0.00000000000000005551115123125783, s = 0
# k = 2, d1 = [-1  0], d2 = [-0.8  0.8], z = -0.80000000000000004440892098500626, s = -1

这就是为什么我们必须添加一个半径为atol 的球来检查给定公差是否实际上为零:

if np.allclose(z, 0, atol=atol):
    s_ = 0
    # ...
else:
    s_ = np.sign(z)

这意味着我们必须接受距离边缘足够近的点(从两侧)被认为包含在多边形中。这是 Float Arithmetic 所固有的,您能做的最好的事情就是将 atol 调整为您的应用程序可接受的值。或者你可以找出另一个不存在这个问题的逻辑或数据模型。

缩放到精确比例

如果我们放大到精确比例以查看靠近边缘的情况,我们得到:

n = 40
lim = np.array([0.5,0.5]) + [-n*e/2, +n*e/2]
x = np.linspace(lim[0], lim[1], 30)
X, Y = np.meshgrid(x, x)
x, y = X.reshape(-1), Y.reshape(-1)

_, axe = p.plot()
axe.set_xlim(lim)
axe.set_ylim(lim)
for r in zip(x,y):
    q = p.contains(r)
    axe.plot(*r, 'o', color=cols[q], markersize=2)

我们看到一些非常靠近边缘但在多边形内部或外部的点被归类为“在边缘上”。这是因为 epsilon 球准则。您还可以观察到点的间距不相等(无论我是否使用linspace),因为不可能将10 表示为2 的整数幂。

结论

上面的解决方案是您的问题的概括,在O(n) 中执行。它可能看起来有点矫枉过正,但它是通用(适用于任何正多边形)和全面(它依赖于众所周知的几何概念)。

实际上,该算法只是检查点在通过路径时是否位于多边形所有边的同一侧。如果是,那么它就断定该点在多边形内部,即!

上述解决方案当然会受到浮点算术错误的影响,因为它依赖于浮点计算(见点10)。幸运的是,使用 epsilon 球测试,我们可以缓解它。

参考

如果你想更深入地理解有限精度算术,我建议你阅读这本优秀的书:Accuracy and Stability of Numerical Algorithms, J. Higham

奖金

下面是所有答案与试验数据集的比较:

我们可以就软检查强调的不同类型的“错误”提供一些背景信息:

  • 由于设计缺陷,1112 点被 @MagedSaeed 错误分类;
  • @MahmoudElshahat 在其逻辑中使用直接浮点相等,这就是为什么它为等价类的点返回不同的结果(例如:点 02,多边形点);
  • @SamMason 使用最简单的逻辑和 epsilon 球测试。它似乎有最大的错误率,因为它不区分内部和边界上(我们可以忽略确切答案为0 并且其函数返回1 的所有点)。 IMO,这是在O(1) 中运行的最佳if-then 算法。此外,它可以轻松更新以考虑 3 种状态逻辑;
  • 10 是故意设计来挑战边界处的逻辑,从距离上看,该点明显位于多边形之外,其数量级与机器精度相当。这是浮点算术误差变得显着并且逻辑更加模糊的地方:离边缘多远是可以接受和易于处理的?

【讨论】:

  • 感谢这个详细的回答。真的很喜欢。但是,我有两个问题:我的算法是否正确?其次,关于浮点精度的最佳实践如何?你没有提到任何。我想要你的cmets。谢谢
  • @MagedSaeed 我已经更新了我的帖子来回答你的问题。您的算法不正确(请参阅上面的详细信息)。我添加了有关浮点算术错误和参考的内容。如果我已经充分解决了您的问题,请告诉我。干杯。
  • 感谢您的回答。它确实增加了很大的价值。
  • @MagedSaeed 欢迎您,我很高兴回答您的问题。
  • 根据您的比较,与其他想法相比,我并没有那么糟糕。顺便说一句,这是个好消息。 ::)))
【解决方案2】:

首先,您的逻辑不正确。考虑x=0.9y=0.9 的情况。这显然在三角形之外,但不满足任何条件x &gt; 1 or y &gt; 1 or x &lt; 0 or y &lt; 0

其次,任何涉及相等比较的浮点算术——比如测试一个点是否在形状的“边界”上——都可能受到精度问题的影响。重新编写逻辑以测试一个点是否在边界的小范围内可能会更好。

我建议不要将Decimal 类用于本机不是十进制数字的任何事物,例如货币。对 Decimal(例如 math.sqrt)执行除基本算术之外的任何操作都会在内部将其转换为浮点数。

【讨论】:

  • 哦,感谢@duskwuff 对逻辑的说明。我只是相应地更新了。我想我错过了这个条件or y &gt; 1-x 还有什么其他的吗?
  • 再次感谢您在边框上的说明。但是,增加准确度裕度可能不会让事情变得更好。考虑把这个数字作为一个测试用例,很可能结果是错误的。
【解决方案3】:

您的代码似乎在不必要地检查条件,我将其实现为更接近于:

def isInsideTriangle(x, y):
    return (
        x >= 0 and x <= 1 and
        y >= 0 and y <= 1 - x
    )

这感觉更接近我作为一个人的想法:首先确保 x 轴在界限内,然后检查 y 轴。

然后您可以在代码中添加一些测试:

tests = [
    (0, 0, True),
    (0, 1, True),
    (1, 0, True),
    (1, 1, False),
    (-1, 1, False),
    (2, 1, False),
    (1, 1, False),
    (0.5, 0.5, True),
    (0.1, 0.9, True),
    (0.2, 0.9, False),
    (0.9, 0.9, False),
]

for x, y, expected in tests:
    result = isInsideTriangle(x,y)
    if result != expected:
        print(f"failed with ({x},{y}) = {result}")

为确保其正常运行,请注意周围有一些不错的框架可以在您的代码上自动运行类似这样的测试

正如@duskwuff 所指出的,浮点数只是近似值(大约 15 个十进制数字,Python 使用双精度/64 位浮点数),但舍入错误很容易导致事情以“错误的一面”告终的比较。这也是为什么数学库包含像 log1p 这样的“冗余”操作,当 x“接近零”时,它会正确计算 log(1 + x),例如,尝试:

from math import log, log1p

print(log(1.0 + 1e-20))
print(log1p(1e-20))

这些应该是“相同的”,但由于四舍五入,幼稚版本会遭受“灾难性的精度损失”,因此打印0.0

处理这个问题的一种方法是允许一定数量的预期错误(通常称为 epsilon),例如上面的函数可以重写为:

def isInsideTriangleEps(x, y, epsilon=1e-10):
    assert epsilon >= 0
    return (
        x >= -epsilon and x - 1 <= epsilon and
        y >= -epsilon and x - 1 + y <= epsilon
    )

允许某些用户指定的容差

【讨论】:

  • 感谢您提供浮点精度方面的 cmets。真的很有帮助!但是,我可以看到您对问题的解决方案仅检查该点是否在三角形内部或外部。边界上呢?该问题需要检查给定点是在边界上、三角形内部还是外部。
  • 对我来说,问题的措辞是确保您使用 &gt;=&lt;= 运算符,而不仅仅是 &gt;&lt;。他们/你也没有定义任何边框宽度,所以说一个点是否在一条无限宽的线上是没有意义的,浮点(im)精度也意味着这几乎肯定会做错事晦涩的案例
【解决方案4】:

回答您的问题:

我的算法正确吗?其次,关于浮点精度的最佳实践如何?

  • 不,你的逻辑不正确。 已编辑:见 cmets
  • 其次,为了让它工作,我们应该避免有损操作,其中 1-0.8 将被四舍五入,例如 '0.199999999999999996' 并损失其实际值的一小部分,

所以我们应该将条件“x == 1-y”替换为“x + y == 1”并将“y > 1-x”更改为“x + y > 1”

编辑:删除 x==1 或 y==1 以及多余的 x>1 或 y>1

然后它会工作:

def isInsideTriangle(x,y):
    if  x+y==1:
        print('on the border of the triangle')
    elif x < 0 or y < 0 or x+y>1:
            print('outside of the triangle')
            print(1-x)  # check the value
    else:
        # verbose these values to double check
        print(1-x)
        print(y)
        print(type(y))
        print(type(1-x))
        print(y==(1-x))
        print('inside of the triangle')

isInsideTriangle(0.8,0.2)
isInsideTriangle(0.2,0.8)

输出:

on the border of the triangle
on the border of the triangle

【讨论】:

  • 将浮点数与相等性进行比较不是一个好的选择,您应该改用 epsilon 球标准。 OP 逻辑和您的逻辑无法拒绝(0,10)等显然在多边形之外的点。
  • 你说得对,我修改了答案中的逻辑,只将 x+y==1 用于检查边界上的点
  • 当处理有限精度算术检查直接相等时,一般来说不是一个好的选择,最好使用 epsilon 球测试。 x+y==1有点敏感,那x+y-1&lt;=eps呢?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-03-27
  • 2023-03-09
  • 2019-03-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多