是否有可能在不诉诸任意精度数据类型的情况下删除浮点错误?

时间:2011-06-15 14:14:41

标签: floating-point rounding precision

我想知道在特定条件下是否可以在不使用任意精度数据类型的情况下删除浮点错误。

问题是通常的问题。语言是Ruby,但它以任何语言保存:

f = 1829.82
=> 1829.82

f / 12.0
=> 152.485

(f / 12.0).round(2)
=> 152.48

为什么不是152.49?因为浮点的精度有限:

format("%18.14f", f)
=> "1829.81999999999994"

format("%18.14f", f / 12.0)
=> "152.48499999999999"

所以四舍五入是正确的。现在我的问题是:如果有以下情况,有没有办法得到我想要的答案:使用float执行的操作数量有很大限制,所需的精度限制为两位小数(最多8位数)总共)和少量剩余的“错误”圆形答案是否可以接受?

用户可以输入有效的Ruby字符串,例如:

"foo / 12.0"

其中foo是在执行字符串的上下文中提供的数字,但是'12 .0'是用户输入的内容。想象一下带有一些免费公式字段的电子表格。字符串简单地被评估为Ruby,因此12.0变为Float。我可以使用ruby_parser + ruby​​2ruby gems构建一个解析树,将数据类型转换为Bignum,Rational,来自Flt库的东西,十进制浮点表示或者你有什么,但这很棘手,因为实际的字符串可以变成有点复杂,所以我不想这样做。如果没有其他可能的话,我会这样做,但这个问题在这里特别指出我是否可以避开这条道路。因此,12.0的数据类型是严格的Float,结果是严格的Float,我唯一能做的就是解释片段的最终答案,并尝试“纠正”它,如果它绕'错误'的方式。 / p>

用户唯一的计算涉及精度为两位小数的数字(总共最多8位)。使用'simple'我的意思是浮点错误没有机会累积:我可以添加其中两个数字并将一个除以整数,但随后计算完成,结果舍入并存储并随后进行计算基于该舍入数字。通常只涉及一个浮点错误,但我认为如果两个可以累积,问题不会显着改变,尽管根据定义残差率可能更大。

首先想到的是首先舍入到3位小数,然后是2位。但是,这不起作用。这将导致

152.48499999999999 => 152.485 => 152.49

但也

152.4846 => 152.485 => 152.49

这不是你想要的。

接下来我想到的是添加最小可能的增量(正如人们已经指出的那样,取决于所考虑的浮点值)到一个浮点,如果在它上面轻推它。 5边界。我主要想知道这可能导致“误报”的频率:添加最小增量的数字,即使它刚好低于.5边界的事实不是由于浮点错误,但是因为它只是计算的结果?

第二个选项是:只需始终将最小的增量添加到数字中,因为.5区域是唯一重要的区域。

编辑: 正如cdiggins建议的那样,我只是重写了一些问题,将我的部分答案纳入评论中。我将奖励给了Ira Baxter积极参与讨论,尽管我还不确定他是对的:Mark Ransom和Emilio M Bumachar似乎支持我的观点,即在实践中,可能会进行修正相对大多数情况下,会产生“正确”的结果。

我仍然需要进行实验,看看结果是多么频繁,我完全打算这样做,但我可以花在这上面的时间有点受限,所以我还没有完成它。这个实验并非无足轻重。

8 个答案:

答案 0 :(得分:6)

听起来你想要的是固定精度的十进制数。实现这些的好图书馆比自己一起黑客攻击更可靠。

对于Ruby,请查看Flt library

答案 1 :(得分:5)

“可以在不使用无限精度数据类型的情况下删除浮点错误。”?

没有。浮点错误是计算机唯一涉及数字运算的错误。如果删除所有错误,根据定义,您的精度是无限的。

这听起来很迂腐,这不是我的意图。我想指出在你明显的技术问题下面存在一个很大的概念问题。 除非您知道正确的值(即无限精度值或其他携带该信息的形式),否则无法根据正确的数值正确舍入错误数字。

您添加少量数字的想法可能会在统计上得出结论,但仅限于统计数据。我建议你写一个脚本来测试大量的案例,随机数不超过两位小数。 (该脚本还需要以无限精度进行数学运算,以便知道正确的比较答案)这样,您就可以测量校正和误报。

答案 2 :(得分:3)

如果你可以控制算术量(特别是乘法和除法),你可以尝试简单地将所有浮点值按一些幂 scale 十(比如scale = 4)。 你必须改变输入,输出,乘法和除法的代码。

然后,scale = 2的十进制分数(如5.10)与510完全相同。输入需要准确输入;例如,读取字符串mmm.nnnn,在字符串中移动小数位 scale 位置(例如,对于scale = 2 ==> mmmnn.nn,然后将字符串转换为float)。这些分数的加法/减法是精确的,不需要任何代码更改。乘法和除法会失去一些“十进制”精度,需要缩放;表示x * y需要改为x * y / scale的代码; x / y需要更改为x * scale / y。您可以在刻度点处对字符串进行舍入,然后输出它。

这个答案是使用真正的十进制算术包的俗气版,由另一张海报提到。

答案 3 :(得分:3)

您提到的最小增量通常称为epsilon。这是可以添加到1.0的最小值,以进行明显的更改。如果要将其添加到其他数字,则必须先将其缩放:x = x + (x * epsilon)

还有另一个definition of epsilon,它是浮点数的最大舍入误差。这个定义应该是第一个定义的一半。

理论上,在舍入之前添加epsilon值会产生与纠正时一样多的错误。在实践中情况并非如此,因为接近偶数十进制数的数字比随机机会所暗示的要多得多。

答案 4 :(得分:2)

我注意到在对one of the answers的评论中,有人认为改变数据类型很难。尽管如此,我将按照要求回答这个问题:

  

我想知道是否,在   具体条件,有可能   删除浮点错误   诉诸无限精准   数据类型。

为了获得准确的结果,您需要使用decimal floating point representations数字和相应的数学例程。请注意,如果定点数学库使用数字的二进制表示,它们仍然可能导致二进制浮点错误。

答案 5 :(得分:1)

在一般情况下,我会说不可能一直得到正确的答案。正如你自己发现的那样,四舍五入不是答案。相反,尽量保持最高精度。

但是,您确实拥有完整的功能库供您使用。你可以向上舍入,向下舍入,舍入到零,舍入到无穷大,所以如果你知道算法在做什么,你可以使用适当的函数。

我想说,添加一个“小”值,或者通常称为“epsilon”,是一种可行的方法。请记住,如果原始值为否定,则必须减去而不是添加它。另请注意,如果要处理全范围的浮点值,则epsilon可能取决于值。

答案 6 :(得分:0)

不,您无法防止浮点错误的累积,因为机器算术总是将运算结果舍入到适合给定的位数。除此之外,考虑到许多操作的结果需要精确表示无限数量的位(例如2/10 = 0.2;但它需要无限位数才能准确表示在基地2,这是计算机使用的。)

答案 7 :(得分:0)

不幸的是,这不是你的答案,但它可能会让你开始。

对象:

class Object
  # Return only the methods not present on basic objects
  def local_methods
    (self.methods - Object.new.methods).sort
  end
end

回调模块:

module Hooker
  module ClassMethods
  private
    def following(*syms, &block)
      syms.each do |sym| # For each symbol
        str_id = "__#{sym}__hooked__"
        unless private_instance_methods.include?(str_id)
          alias_method str_id, sym    # Backup original method
          private str_id         # Make backup private
          define_method sym do |*args|  # Replace method
            ret = __send__ str_id, *args # Invoke backup
            rval=block.call(self,       # Invoke hook
             :method => sym, 
             :args => args,
             :return => ret
            )
            if not rval.nil?
              ret=rval[:ret]
            end
            ret # Forward return value of method
          end
        end
      end
    end
  end

  def Hooker.included(base)
    base.extend(ClassMethods)
  end
end

对Float进行更改以实际完成工作:

if 0.1**2 != 0.01 # patch Float so it works by default
  class Float
    include Hooker
    0.1.local_methods.each do |op|
      if op != :round
        following op do |receiver, args|
          if args[:return].is_a? Float
            ret=args[:return].round Float::DIG
            ret=Hash[:ret => ret]
          end
          ret
        end
      end
    end
  end
end

编辑: 有点更好地使用Rational。 nmethods覆盖仍然不会始终打开(请参阅代码后的问题):

  class Float
    include Hooker
    0.1.local_methods.each do |op|
      if op != :round
        following op do |receiver, args|
          if args[:return].is_a? Float
            argsin=[]
            args[:args].each do |c|
              argsin=c.rationalize
            end
            rval=receiver.rationalize.send(
                args[:method], 
                argsin
               )
            ret=Hash[:ret => rval.to_f]
          end
          ret
        end
      end
    end
  end

问题:并非所有方法覆盖都有效,至少在1.9.3p0中:

pry(main)> 6543.21 % 137.24
=> 92.93
[... but ...]
pry(main)> 19.5.send(:-.to_sym, 16.8)
=> 2.7
pry(main)> 19.5 - 16.8
=> 2.6999999999999993