【问题标题】:inaccurate results for calculations using floats - Simple solution使用浮点数的计算结果不准确 - 简单的解决方案
【发布时间】:2020-11-13 11:21:06
【问题描述】:

在 StackOverflow 和其他地方已经提出了许多关于 Python 使用浮点数的计算令人困惑的行为的问题 - 通常返回的结果显然有少量错误。 explanation for this 总是链接到。但是,通常不会提供实用的简单解决方案。

这不仅仅是错误(通常可以忽略不计) - 它更多的是获得像 3.999999999999999 这样的结果来获得像 8.7 - 4.7 这样的简单总和的混乱和不雅。

我已经为此编写了一个简单的解决方案,我的问题是,为什么 Python 不会在后台自动实现这样的事情

基本概念是将所有浮点数转换为整数,进行运算,然后适当地转换回浮点数。上述链接文档中解释的困难仅适用于浮点数,而不适用于整数,这就是它起作用的原因。代码如下:

def justwork(x,operator,y):
    numx = numy = 0
    if "." in str(x):
        numx = len(str(x)) - str(x).find(".") -1
    if "." in str(y):
        numy = len(str(y)) - str(y).find(".") -1
    num = max(numx,numy)

    factor = 10 ** num
    newx = x * factor
    newy = y * factor

    if operator == "%":
        ans1 = x % y
        ans = (newx % newy) / factor
    elif operator == "*":
        ans1 = x * y
        ans = (newx * newy) / (factor**2)
    elif operator == "-":
        ans1 = x - y
        ans = (newx - newy) / factor
    elif operator == "+":
        ans1 = x + y
        ans = (newx + newy) / factor
    elif operator == "/":
        ans1 = x / y
        ans = (newx / newy)
    elif operator == "//":
        ans1 = x // y
        ans = (newx // newy)

    return (ans, ans1)

这无疑是相当不优雅的,可能需要稍加思考来改进,但它可以完成工作。该函数返回一个具有正确结果(通过转换为整数)和不正确结果(自动提供)的元组。以下是如何提供准确结果的示例,而不是正常执行。

#code                           #returns tuple with (correct, incorrect) result
print(justwork(0.7,"%",0.1))    #(0.0, 0.09999999999999992)
print(justwork(0.7,"*",0.1))    #(0.07, 0.06999999999999999)
print(justwork(0.7,"-",0.2))    #(0.5, 0.49999999999999994)
print(justwork(0.7,"+",0.1))    #(0.8, 0.7999999999999999)
print(justwork(0.7,"/",0.1))    #(7.0, 6.999999999999999)
print(justwork(0.7,"//",0.1))   #(7.0, 6.0)

TLDR:本质上的问题是,为什么浮点数可以存储为以 2 为底的二进制分数(本质上是不精确的),而它们可以以与整数相同的方式存储(可以工作)?

【问题讨论】:

  • 这只能在一个简单的域中“工作”,特别是带有短十进制数字的简单算术。当涉及更复杂的计算时,它将不起作用,例如产生无法用短十进制数字表示的结果的非小数分数或计算链。至于为什么使用浮点而不是固定的整数格式,这是因为点是浮动的:浮点数具有内置的比例,使其能够处理非常大或非常小的数字,例如物理学中发生的那样.这称为动态范围。
  • Decimal floating point 是一个东西。不过,在大多数系统上,您可以更好地支持二进制浮点(这更容易在硬件中有效实现)。
  • 主要原因是性能。对于进行数十亿次浮点计算的程序来说,这是一笔太大的开销。而且绝对不仅仅是 Python。
  • 看起来 python 有一个用于十进制浮点数的模块:docs.python.org/3/library/decimal.html
  • 如果我将其更改为return ans(您声称的“正确”结果),那么justwork(justwork(1, '/', 3), '*', 3) 将导致0.9999999999999998。而使用return ans1(您声称的“不正确”结果)会导致1.0

标签: python floating-point binary floating-accuracy ieee-754


【解决方案1】:

三点:

  1. 提出的问题/通用方法中的功能,虽然在很多情况下确实避免了该问题,但在许多其他情况下,即使是相对简单的情况,也存在相同的问题。
  2. 有一个decimal 模块总是提供准确的答案(即使问题中的justwork() 函数失败)
  3. 使用decimal 模块会大大减慢速度 - 大约要长 100 倍。默认方法会牺牲准确性来优先考虑速度。 [将其设为默认值是否正确尚有争议]。

为了说明这三点,请考虑以下功能,大致基于问题中的功能:

def justdoesntwork(x,operator,y):
    numx = numy = 0
    if "." in str(x):
        numx = len(str(x)) - str(x).find(".") -1
    if "." in str(y):
        numy = len(str(y)) - str(y).find(".") -1
    factor = 10 ** max(numx,numy)
    newx = x * factor
    newy = y * factor

    if operator == "+":     myAns = (newx + newy) / factor
    elif operator == "-":   myAns = (newx - newy) / factor
    elif operator == "*":   myAns = (newx * newy) / (factor**2)
    elif operator == "/":   myAns = (newx / newy)
    elif operator == "//":  myAns = (newx //newy)
    elif operator == "%":   myAns = (newx % newy) / factor

    return myAns

from decimal import Decimal
def doeswork(x,operator,y):
    if operator == "+":     decAns = Decimal(str(x)) + Decimal(str(y))
    elif operator == "-":   decAns = Decimal(str(x)) - Decimal(str(y))
    elif operator == "*":   decAns = Decimal(str(x)) * Decimal(str(y))
    elif operator == "/":   decAns = Decimal(str(x)) / Decimal(str(y))
    elif operator == "//":  decAns = Decimal(str(x)) //Decimal(str(y))
    elif operator == "%":   decAns = Decimal(str(x)) % Decimal(str(y))

    return decAns

然后循环遍历多个值以查找myAnsdecAns 的不同之处:

operatorlist = ["+", "-", "*", "/", "//", "%"]
for a in range(1,1000):
    x = a/10
    for b in range(1,1000):
        y=b/10
        counter = 0
        for operator in operatorlist:
            myAns, decAns = justdoesntwork(x, operator, y),  doeswork(x, operator, y)
            if (float(decAns) != myAns)   and     len(str(decAns)) < 5  :
                print(x,"\t", operator, " \t ", y, " \t=   ", decAns,  "\t\t{", myAns, "}")

=> 这会遍历所有值到 1 d.p。从 0.1 到 99.9 - 确实找不到任何 myAnsdecAns 不同的值。

但是如果改成 2d.p. (即x = a/100y = b/100),然后出现许多示例。例如,0.1+1.09 - 这可以通过在控制台中输入((0.1*100)+(1.09*100)) / (100) 轻松检查,它使用问题的基本方法,并返回1.1900000000000002 而不是1.19。错误的来源是1.09*100,它返回109.00000000000001。 [只需输入0.1+1.09 也会出现同样的错误]。所以问题中建议的方法并不总是有效。

但使用 Decimal() 会返回正确答案:Decimal('0.1')+Decimal('1.09') 返回Decimal('1.19')

[注意:不要忘记用引号将 0.1 和 1.09 括起来。如果不这样做,Decimal(0.1)+Decimal(1.09) 将返回 Decimal('1.190000000000000085487172896') - 因为它以浮点 0.1 开头,存储不准确,然后将 that 转换为十进制 - GIGO。 Decimal() 必须输入一个字符串。采用浮点数,将其转换为字符串,然后从那里转换为十进制,似乎确实有效,但问题仅在于直接从浮点数转换为十进制]。


在时间成本方面,运行这个:

import timeit
operatorlist = ["+", "-", "*", "/", "//", "%"]

for operator in operatorlist:
    for a in range(1,10):
        a=a/10
        for b in range(1,10):
            b=b/10
            
            DECtime  = timeit.timeit("Decimal('" +str(a)+ "') " +operator+ " Decimal('" +str(b)+ "')", setup="from decimal import Decimal")
            NORMtime = timeit.timeit(str(a) +operator+ str(b))
            timeslonger = DECtime // NORMtime
            print("Operation:  ", str(a) +operator +str(b) , "\tNormal operation time: ", NORMtime, "\tDecimal operation time: ", DECtime, "\tSo Decimal operation took ", timeslonger, " times longer")

这表明,对于所有测试的运算符,十进制运算的时间始终长约 100 倍。

[在运算符列表中包含取幂表明取幂可能需要 3000 - 5000 倍的时间。然而,这部分是因为 Decimal() 的计算精度远高于正常操作 - Decimal() 默认精度为 28 位 - Decimal("1.5")**Decimal("1.5") 返回 1.837117307087383573647963056,而 1.5**1.5 返回 1.8371173070873836。如果您通过将b=b/10 替换为b=float(b)b 限制为整数(这将阻止具有高SF 的结果),那么与其他运算符一样,Decimal 计算需要大约 100 倍的时间]。


仍然可以说,时间成本仅对执行数十亿次计算的用户来说很重要,并且大多数用户会优先考虑获得可理解的结果,而不是时间差,这在大多数普通应用程序中是微不足道的。

【讨论】:

  • 感谢@SayanipDutta @ EricPostpischil @ chtz 和其他所有人的帮助
猜你喜欢
  • 2017-06-08
  • 2018-12-20
  • 1970-01-01
  • 1970-01-01
  • 2013-01-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多