【问题标题】:rounding errors in Python floor divisionPython地板除法中的舍入错误
【发布时间】:2021-12-26 16:22:30
【问题描述】:

我知道浮点运算中会发生舍入错误,但有人可以解释一下原因:

>>> 8.0 / 0.4  # as expected
20.0
>>> floor(8.0 / 0.4)  # int works too
20
>>> 8.0 // 0.4  # expecting 20.0
19.0

这发生在 x64 上的 Python 2 和 3 上。

在我看来,这要么是一个错误,要么是 // 的一个非常愚蠢的规范,因为我看不出最后一个表达式应该评估为 19.0 的任何理由。

为什么a // b 不简单定义为floor(a / b)

编辑8.0 % 0.4 也计算为0.3999999999999996。至少这是随之而来的,因为那时8.0 // 0.4 * 0.4 + 8.0 % 0.4 评估为8.0

编辑:这不是 Is floating point math broken? 的重复,因为我在问为什么这个特定的操作会出现(也许可以避免的)舍入错误,以及为什么 a // b 没有被定义为 /等于floor(a / b)

REMARK:我想这不起作用的更深层原因是楼层划分是不连续的,因此有一个无限的condition number,使其成为一个不适定问题。楼层除法和浮点数根本不兼容,你永远不应该在浮点数上使用//。只需使用整数或分数即可。

【问题讨论】:

  • 有趣的是,'%.20f'%0.4 给出了'0.40000000000000002220',所以0.4 显然比0.4 略高一点。
  • @khelwood floor(8.0/0.4) 如何产生正确的结果?
  • 首先,float 类型的浮点数通常是错误的。其次,//% 对于负数和 float 数字非常不可靠(意思是意外行为)。 documentation on Decimal objects 简要讨论了带有负整数的 // 以及 Decimal 库如何以不同的方式处理它。
  • @AlexanderVogt 不是吗?问题不在于为什么浮点结果不准确,而在于为什么 python 为floor(8.0/0.4) 和“floor-division”8.0//0.4 做了两种不同的事情。

标签: python python-2.7 python-3.x floating-point rounding


【解决方案1】:

正如您和 khelwood 已经注意到的,0.4 不能完全表示为浮点数。为什么?它是五分之二 (4/10 == 2/5),它没有有限二进制分数表示。

试试这个:

from fractions import Fraction
Fraction('8.0') // Fraction('0.4')
    # or equivalently
    #     Fraction(8, 1) // Fraction(2, 5)
    # or
    #     Fraction('8/1') // Fraction('2/5')
# 20

然而

Fraction('8') // Fraction(0.4)
# 19

这里,0.4 被解释为浮点字面量(因此是浮点二进制数),需要(二进制)舍入,并且只有 then 转换为有理数 Fraction(3602879701896397, 9007199254740992),这几乎但不完全是 4 / 10。然后执行地板除法,因为

19 * Fraction(3602879701896397, 9007199254740992) < 8.0

20 * Fraction(3602879701896397, 9007199254740992) > 8.0

结果是 19,而不是 20。

同样的情况也可能发生在

8.0 // 0.4

也就是说,似乎下限除法是原子确定的(但在解释的浮点文字的唯一近似浮点值上)。

为什么会这样

floor(8.0 / 0.4)

给出“正确”的结果?因为在那里,两个舍入误差相互抵消。 首先 1) 执行除法,产生的结果略小于 20.0,但不能表示为浮点数。它被四舍五入到最接近的浮点数,恰好是20.0。只有然后,执行了floor 操作,但现在完全作用于20.0,因此不再更改数字。


1) 作为 Kyle Strand points out,确定了确切的结果然后四舍五入不是实际上 /em> 发生在2) 级别(CPython 的 C 代码甚至 CPU 指令)。但是,它可能是确定预期 3) 结果的有用模型。

2) 但是,在最低 4) 级别上,这可能不会太远。一些芯片组通过首先计算更精确(但仍然不精确,只是有更多二进制数字)的内部浮点结果然后四舍五入到 IEEE 双精度来确定浮点结果。

3) Python 规范“预期”的,不一定是我们的直觉。

4) 嗯,最低级别高于逻辑门。我们不必考虑使半导体能够理解这一点的量子力学。

【讨论】:

  • “似乎地板分割是原子确定的”——很好的猜测,我想在语义上是正确的,但就实现必须做的事情而言,它有点倒退:因为没有支持的硬件支持“原子”// 语义,余数被预先计算并从分子中减去,以确保浮点除法(当它最终发生时)立即计算正确的值,无需进一步调整。
  • 是的,我在这里从用户(即 Python 程序员)的角度使用术语“原子”。类似于如何某些数据库操作可能被描述为“原子”,它们也不映射到单个硬件指令。所以我说的是效果,而不是实现。
  • 适当的实现,硬件是否支持与 Python 的 // 运算符等效的本机指令当然取决于硬件和操作数类型。早期的 CPU 肯定支持整数操作数的整数除法。可能没有任何芯片组具有原生支持浮点划分的本机支持,但这也不是不可想象的,因为它只是不切实际,并非不可能。
  • 8.0//0.4 可能也会发生同样的情况”。不是真的,至少对于 cpython 来说。他们实际上宁愿做round((8.0 - fmod(8.0, 0.4)) / 0.4),它给出19,因为(至少对于我的机器/编译器版本)fmod(8.0/0.4)导致0.4(也在纯C中)。详情见我的回答。
【解决方案2】:

在 github (https://github.com/python/cpython/blob/966b24071af1b320a1c7646d33474eeae057c20f/Objects/floatobject.c) 上查看了 cpython 中 float 对象的半官方来源后,可以理解这里发生了什么。

对于普通除法float_div 被调用(第 560 行),它在内部将 python floats 转换为 c-doubles,进行除法,然后将生成的 double 转换回 python float .如果您只是在 c 中使用 8.0/0.4 来执行此操作,您会得到:

#include "stdio.h"
#include "math.h"

int main(){
    double vx = 8.0;
    double wx = 0.4;
    printf("%lf\n", floor(vx/wx));
    printf("%d\n", (int)(floor(vx/wx)));
}

// gives:
// 20.000000
// 20

对于楼层划分,发生了其他事情。在内部,float_floor_div(第 654 行)被调用,然后它调用float_divmod,该函数应该返回一个 python 元组 floats 包含地板除法,以及 mod/remainder,即使后者被PyTuple_GET_ITEM(t, 0) 扔掉了。这些值按以下方式计算(转换为 c-doubles 后):

  1. 使用double mod = fmod(numerator, denominator)计算余数。
  2. 在进行除法时,分子会减少 mod 以获得整数值。
  3. 通过有效计算floor((numerator - mod) / denominator) 来计算下除法的结果
  4. 之后,@Kasramvd 的回答中已经提到的检查完成。但这只会将(numerator - mod) / denominator 的结果捕捉到最接近的整数值。

这给出不同结果的原因是,fmod(8.0, 0.4) 由于浮点运算给出了0.4 而不是0.0。因此,计算的结果实际上是floor((8.0 - 0.4) / 0.4) = 19,将(8.0 - 0.4) / 0.4) = 19 捕捉到最接近的整数值并不能解决由fmod 的“错误”结果引入的错误。你也可以在 c 中轻松地破解它:

#include "stdio.h"
#include "math.h"

int main(){
    double vx = 8.0;
    double wx = 0.4;
    double mod = fmod(vx, wx);
    printf("%lf\n", mod);
    double div = (vx-mod)/wx;
    printf("%lf\n", div);
}

// gives:
// 0.4
// 19.000000

我猜,他们选择了这种计算底除法的方式来保持(numerator//divisor)*divisor + fmod(numerator, divisor) = numerator 的有效性(如@0x539 答案中的链接中所述),即使现在这会导致@987654348 的一些意外行为@。

【讨论】:

  • 你似乎是唯一有正确答案的人。道具!但是,由于您必须深入研究源代码才能找到它,我想知道这是否是所有 Python 实现的强制性部分?
  • PEP 238 开始,预计floor(a/b) == a // b 将是正确的,因为这已明确说明为“地板除法”的语义。
  • 在@0x539已经引用的问题报告(bugs.python.org/issue27463)中,似乎并没有被认为是错误的。这是python bugtracker。所以我猜“楼层划分”更像是一个名称,而不是用来定义实现。
  • "通过有效计算 floor((numerator - mod) / denominator) 来计算下除法的结果 - 不,它更像 round((numerator - mod) / denominator)。源代码确实使用了floor,但是如果floor 舍入错误的方式,它会立即向上调整结果。它依赖于- mod 部分来“有效地板”numerator / denominator
  • @user2357112 你是对的。实际上,结果是rounded 而不仅仅是floored。尽管如此,-mod 导致了奇怪的结果。
【解决方案3】:

@jotasi 解释了背后的真正原因。

但是,如果您想阻止它,您可以使用 decimal 模块,该模块基本上旨在表示十进制浮点数,与二进制浮点表示完全相反。

因此,在您的情况下,您可以执行以下操作:

>>> from decimal import *
>>> Decimal('8.0')//Decimal('0.4')
Decimal('20')

参考:https://docs.python.org/2/library/decimal.html

【讨论】:

  • 虽然这不是问题的答案,但decimal的正确用法也不是,因为我们可以简单地使用真正的除法来得到这个结果。
  • fractions 模块似乎也在做这项工作。
  • @0x539 的解释实际上并不正确。请参阅 jotasi 的回答和我在 0x539 回答下方的评论。
  • @KyleStrand 保留当然也适用于my answer,因为我已经对其进行了一些修改。
  • @das-g 啊,太好了!
【解决方案4】:

好的,经过一些研究,我发现了这个issue。 似乎正在发生的事情是,正如@khelwood 建议的那样,0.4 在内部评估为0.40000000000000002220,当除以8.0 时,产生的结果略小于20.0。然后/ 运算符舍入到最接近的浮点数,即20.0,但// 运算符立即截断结果,产生19.0

这应该更快,我认为它“接近处理器”,但我仍然不是用户想要/期望的。

【讨论】:

  • 好发现,那个。但是用户在这里想要什么?纠正本来就不正确的数字的数学行为? (其中同样的平均“典型用户”是usually blissfully unaware。)
  • @RadLexus 用户希望此操作的最佳近似值。在这种情况下是20.0
  • @0x539: 那些依赖// 将比20.0 略少于20.0 的内容截断到19.0 的可怜用户呢?这里的问题是用户想要做精确的算术并且使用了错误的工具来完成这项工作。
  • 实际上,截断不会发生,至少如果我正确理解了 cpython 的来源。他们通过实际计算floor((8.0 - fmod(8.0, 0.4)) / 0.4) 并通过fmod(8.0, 0.4)=0.4 引入错误来保持链接中提到的身份经历了相当大的考验。 (有关链接和更多解释,请参阅我的答案)。
  • 从数学上讲,8.0 / 0.40000000000000002220 “产生的结果比 20.0 略小”是正确的。但是,将浮点运算视为一系列步骤是不正确的,在这些步骤中计算实际的数学值,然后然后四舍五入(你暗示当你说“/ 运算符 then 循环...”)。当然,这是不可能的,因为计算机必须有办法在内部表示计算的所有中间步骤!请参阅@jotasi 的回答。
【解决方案5】:

那是因为 python 中没有 0.4(浮点有限表示),它实际上是一个像 0.4000000000000001 这样的浮点数,它使得除法的下限为 19。

>>> floor(8//0.4000000000000001)
19.0

但真正的除法 (/) returns a reasonable approximation of the division result if the arguments are floats or complex. 这就是为什么 8.0/0.4 的结果是 20。它实际上取决于参数的大小(在 C 双参数中)。 (不四舍五入到最近的浮点数

阅读 Guido 本人关于 pythons integer division floors 的更多信息。

有关浮点数的完整信息,您可以阅读这篇文章https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

有兴趣的朋友可以看一下Cpython源码中的float_div,它对浮点数做真正的除法任务:

float_div(PyObject *v, PyObject *w)
{
    double a,b;
    CONVERT_TO_DOUBLE(v, a);
    CONVERT_TO_DOUBLE(w, b);
    if (b == 0.0) {
        PyErr_SetString(PyExc_ZeroDivisionError,
                        "float division by zero");
        return NULL;
    }
    PyFPE_START_PROTECT("divide", return 0)
    a = a / b;
    PyFPE_END_PROTECT(a)
    return PyFloat_FromDouble(a);
}

最终结果将由函数PyFloat_FromDouble计算:

PyFloat_FromDouble(double fval)
{
    PyFloatObject *op = free_list;
    if (op != NULL) {
        free_list = (PyFloatObject *) Py_TYPE(op);
        numfree--;
    } else {
        op = (PyFloatObject*) PyObject_MALLOC(sizeof(PyFloatObject));
        if (!op)
            return PyErr_NoMemory();
    }
    /* Inline PyObject_New */
    (void)PyObject_INIT(op, &PyFloat_Type);
    op->ob_fval = fval;
    return (PyObject *) op;
}

【讨论】:

  • @Kasramvd 感谢您的广泛回答。也许我只是很密集,但我不明白“捕捉到下一个整数值”是什么意思。显然,并非所有浮点除法都会舍入到下一个整数值(3./4. 不会给出1)。因此,据我所知,对此的决定不可能像您提出的那样简单。我没听错吗?
  • 其实我自己查了源码后,我猜浮点除法是在函数float_div中完成的,而float_divmod只被float_floor_div调用,它在turn 给出“错误”结果19 而不是20
  • @jotasi 是的,完全正确。它比简单的捕捉更复杂。是的,它是 float_div 函数,它完成了真正的潜水任务。似乎它以某种方式根据参数大小计算最终结果。我更新了答案。感谢您的关注。
  • 我通过简单地检查 c 中的重要行进行了双重检查,显然重要的部分是它们通过 c 中的简单 double 除法计算 8.0/0.4 = 20 而地板除法实际上计算 floor((8.0 - fmod(8.0, 0.4)) / 0.4) = 19 因为@ 987654343@ 由于浮点运算。请参阅下面的答案以获取更多信息。
  • “事实是它取决于可用PyFloatObjects 的大小” - 什么?不,它没有。 PyFloatObjects 的大小都是一样的,PyFloatObjects 的存储细节与这种行为几乎没有任何关系。
猜你喜欢
  • 2019-02-13
  • 1970-01-01
  • 2013-04-02
  • 1970-01-01
  • 2010-10-31
  • 1970-01-01
相关资源
最近更新 更多