【问题标题】:How should I do floating point comparison?我应该如何进行浮点比较?
【发布时间】:2011-06-22 09:01:04
【问题描述】:

我目前正在编写一些代码,其中包含以下内容:

double a = SomeCalculation1();
double b = SomeCalculation2();

if (a < b)
    DoSomething2();
else if (a > b)
    DoSomething3();

然后在其他地方我可能需要做平等:

double a = SomeCalculation3();
double b = SomeCalculation4();

if (a == 0.0)
   DoSomethingUseful(1 / a);
if (b == 0.0)
   return 0; // or something else here

简而言之,我有很多浮点数学运算,我需要对条件进行各种比较。我无法将其转换为整数数学,因为这样的事情在这种情况下毫无意义。

我之前读过浮点比较可能不可靠,因为您可能会遇到这样的事情:

double a = 1.0 / 3.0;
double b = a + a + a;
if ((3 * a) != b)
    Console.WriteLine("Oh no!");

简而言之,我想知道:如何可靠地比较浮点数(小于、大于、相等)?

我使用的数字范围大致从 10E-14 到 10E6,所以我确实需要处理小数字和大数字。

我已将此标记为与语言无关,因为无论我使用哪种语言,我都对如何实现这一点感兴趣。

【问题讨论】:

  • 使用浮点数时无法可靠地做到这一点。总会有一些数字对于计算机来说是相等的,但实际上并不相等(比如 1E+100、1E+100+1),而且您通常也会得到计算机不相等的计算结果,尽管实际上是相等的(参见nelhage 的答案之一)。您将不得不选择您不想要的两者中的哪一个。
  • 另一方面,如果你,比如说,只处理有理数,你可能会基于整数实现一些有理数算术,然后如果两个数中的一个可以是,则认为两个数相等取消到另一个。
  • 嗯,目前我正在模拟。我通常进行这些比较的地方与可变时间步长有关(用于解决一些颂歌)。在某些情况下,我需要检查一个对象的给定时间步长是否等于、小于或大于另一个对象的时间步长。
  • 为什么不使用数组? stackoverflow.com/questions/28318610/…

标签: language-agnostic comparison floating-point


【解决方案1】:

除非您工作在浮点/双精度限制的边缘,否则比较更大/更小并不是真正的问题。

对于“模糊等于”比较,这个(Java 代码,应该很容易适应)是我经过大量工作并考虑到很多批评后为The Floating-Point Guide 提出的:

public static boolean nearlyEqual(float a, float b, float epsilon) {
    final float absA = Math.abs(a);
    final float absB = Math.abs(b);
    final float diff = Math.abs(a - b);

    if (a == b) { // shortcut, handles infinities
        return true;
    } else if (a == 0 || b == 0 || diff < Float.MIN_NORMAL) {
        // a or b is zero or both are extremely close to it
        // relative error is less meaningful here
        return diff < (epsilon * Float.MIN_NORMAL);
    } else { // use relative error
        return diff / (absA + absB) < epsilon;
    }
}

它带有一个测试套件。您应该立即放弃任何不支持的解决方案,因为它几乎可以保证在某些边缘情况下会失败,例如有一个值 0、两个与零相反的非常小的值或无穷大。

另一种方法(有关详细信息,请参阅上面的链接)是将浮点数的位模式转换为整数并接受固定整数距离内的所有内容。

无论如何,可能没有适合所有应用程序的解决方案。理想情况下,您应该使用涵盖实际用例的测试套件来开发/调整自己的测试套件。

【讨论】:

  • @toochin:取决于您希望允许的误差幅度有多大,但是当您考虑最接近零、正数和负数的非规范化数字时,它会成为最明显的问题 - 除了零,它们比任何其他两个值都更接近,但许多基于相对误差的幼稚实现会认为它们相距太远。
  • 嗯。你有一个测试else if (a * b == 0),但是你在同一行的评论是a or b or both are zero。但这不是两个不同的东西吗?例如,如果 a == 1e-162b == 2e-162 则条件 a * b == 0 将为真。
  • @toochin:主要是因为该代码应该很容易移植到可能没有该功能的其他语言(它也仅在 1.5 中添加到 Java)。
  • 如果该功能被大量使用(例如视频游戏的每一帧),我会在汇编中使用史诗优化重写它。
  • 很好的指南和很好的答案,特别是考虑到这里的abs(a-b)&lt;eps 答案。两个问题: (1) 将所有&lt;s 更改为&lt;=s 会不会更好,从而允许“零-eps”比较,相当于精确比较? (2) 使用diff &lt; epsilon * (absA + absB);而不是diff / (absA + absB) &lt; epsilon;(最后一行)不是更好吗——?
【解决方案2】:

TL;DR

  • 使用以下函数代替当前接受的解决方案,以避免在某些限制情况下出现一些不良结果,同时可能更有效。
  • 了解您对数字的预期不精确度,并在比较函数中相应地提供它们。
bool nearly_equal(
  float a, float b,
  float epsilon = 128 * FLT_EPSILON, float abs_th = FLT_MIN)
  // those defaults are arbitrary and could be removed
{
  assert(std::numeric_limits<float>::epsilon() <= epsilon);
  assert(epsilon < 1.f);

  if (a == b) return true;

  auto diff = std::abs(a-b);
  auto norm = std::min((std::abs(a) + std::abs(b)), std::numeric_limits<float>::max());
  // or even faster: std::min(std::abs(a + b), std::numeric_limits<float>::max());
  // keeping this commented out until I update figures below
  return diff < std::max(abs_th, epsilon * norm);
}

请提供图形?

比较浮点数时,有两种“模式”。

第一个是相对模式,其中xy之间的差异被认为是相对于它们的幅度|x| + |y|。在 2D 中绘制时,它给出以下配置文件,其中绿色表示 xy 相等。 (出于说明目的,我将epsilon 设为 0.5)。

相对模式用于“正常”或“足够大”的浮点值。 (稍后会详细介绍)。

第二种是绝对模式,当我们简单地将它们的差异与一个固定数字进行比较时。它给出了以下配置文件(再次使用 0.5 的 epsilon 和 1 的 abs_th 进行说明)。

这种绝对比较模式用于“微小”浮点值。

现在的问题是,我们如何将这两种响应模式拼接在一起。

在 Michael Borgwardt 的回答中,开关基于diff 的值,该值应低于abs_th(他的回答中为Float.MIN_NORMAL)。该开关区域如下图阴影所示。

因为abs_th * epsilonabs_th 小,所以绿色补丁不会粘在一起,这反过来又给解决方案带来了不好的属性:我们可以找到三组数字,例如x &lt; y_1 &lt; y_2x == y2 但@ 987654347@.

以这个惊人的例子为例:

x  = 4.9303807e-32
y1 = 4.930381e-32
y2 = 4.9309825e-32

我们有x &lt; y1 &lt; y2,实际上y2 - xy1 - x 大2000 多倍。然而,使用当前的解决方案,

nearlyEqual(x, y1, 1e-4) == False
nearlyEqual(x, y2, 1e-4) == True

相比之下,在上面提出的解决方案中,切换区域是基于|x| + |y|的值,由下面的阴影方块表示。它确保两个区域都能优雅地连接。

此外,上面的代码没有分支,这可能更有效。考虑到诸如maxabs 之类的先验 需要分支的操作​​,通常具有专门的汇编指令。出于这个原因,我认为这种方法优于另一种解决方案,即通过将开关从 diff &lt; abs_th 更改为 diff &lt; eps * abs_th 来修复 Michael 的 nearlyEqual,这将产生基本相同的响应模式。

相对比较和绝对比较在哪里切换?

这些模式之间的切换是围绕abs_th 进行的,在接受的答案中被视为FLT_MIN。这种选择意味着float32 的表示限制了我们的浮点数的精度。

这并不总是有意义的。例如,如果您比较的数字是减法的结果,那么FLT_EPSILON 范围内的某个值可能更有意义。如果它们是减数的平方根,则数值的不精确性可能会更高。

当您考虑将浮点数与0 进行比较时,这一点相当明显。在这里,任何相对比较都会失败,因为|x - 0| / (|x| + 0) = 1。因此,当x 处于计算的不精确量级时,比较需要切换到绝对模式——而且很少低于FLT_MIN

这就是上面引入abs_th参数的原因。

此外,通过不将abs_thepsilon 相乘,此参数的解释很简单,并且对应于我们对这些数字的预期数值精度级别。

数学隆隆声

(保留在这里主要是为了我自己的乐趣)

更一般地,我假设行为良好的浮点比较运算符=~ 应该具有一些基本属性。

以下是相当明显的:

  • 自我平等:a =~ a
  • 对称:a =~ b 暗示b =~ a
  • 反对不变性:a =~ b 暗示 -a =~ -b

(我们没有a =~ bb =~ c暗示a =~ c=~不是等价关系)。

我将添加以下更特定于浮点比较的属性

  • 如果a &lt; b &lt; c,则a =~ c 暗示a =~ b(更接近的值也应该相等)
  • 如果a, b, m &gt;= 0a =~ b 暗示a + m =~ b + m(具有相同差异的较大值也应该相等)
  • 如果0 &lt;= λ &lt; 1a =~ b 隐含λa =~ λb(可能不那么明显可以论证)。

这些属性已经对可能的近似相等函数提供了强大的约束。上面提出的功能验证了它们。也许缺少一个或几个明显的属性。

当人们将=~ 视为一组由Ɛabs_th 参数化的等式关系=~[Ɛ,t] 时,还可以添加

  • 如果Ɛ1 &lt; Ɛ2a =~[Ɛ1,t] b 暗示a =~[Ɛ2,t] b(给定容差的平等意味着更高容差的平等)
  • 如果t1 &lt; t2a =~[Ɛ,t1] b 暗示a =~[Ɛ,t2] b(给定不精确度的相等意味着更高不精确度的相等)

建议的解决方案也验证了这些。

【讨论】:

  • 这是一个很好的答案!
  • c++ 实现问题:(std::abs(a) + std::abs(b)) 可以大于std::numeric_limits&lt;float&gt;::max() 吗?
  • @anneb 可以,可以是+INF。
  • 很好的答案!这些图形真的很有帮助!
  • 代码中的参数名称似乎颠倒了。 'relth' 参数用作绝对阈值,而 'epsilon' 参数用作相对阈值。
【解决方案3】:

我遇到了比较浮点数 A &lt; BA &gt; B 的问题 以下是似乎有效的方法:

if(A - B < Epsilon) && (fabs(A-B) > Epsilon)
{
    printf("A is less than B");
}

if (A - B > Epsilon) && (fabs(A-B) > Epsilon)
{
    printf("A is greater than B");
}

晶圆厂——绝对价值——会考虑它们是否基本相等。

【讨论】:

  • 完全不需要fabs,如果你第一次测试if (A - B &lt; -Epsilon)
【解决方案4】:

我们必须选择一个容差水平来比较浮点数。例如,

final float TOLERANCE = 0.00001;
if (Math.abs(f1 - f2) < TOLERANCE)
    Console.WriteLine("Oh yes!");

一个音符。你的例子很有趣。

double a = 1.0 / 3.0;
double b = a + a + a;
if (a != b)
    Console.WriteLine("Oh no!");

这里有一些数学

a = 1/3
b = 1/3 + 1/3 + 1/3 = 1.

1/3 != 1

哦,是的..

你是说

if (b != 1)
    Console.WriteLine("Oh no!")

【讨论】:

    【解决方案5】:

    我对 swift 中的浮点比较的想法

    infix operator ~= {}
    
    func ~= (a: Float, b: Float) -> Bool {
        return fabsf(a - b) < Float(FLT_EPSILON)
    }
    
    func ~= (a: CGFloat, b: CGFloat) -> Bool {
        return fabs(a - b) < CGFloat(FLT_EPSILON)
    }
    
    func ~= (a: Double, b: Double) -> Bool {
        return fabs(a - b) < Double(FLT_EPSILON)
    }
    

    【讨论】:

      【解决方案6】:

      Michael Borgwardt & bosonix 对 PHP 的适应:

      class Comparison
      {
          const MIN_NORMAL = 1.17549435E-38;  //from Java Specs
      
          // from http://floating-point-gui.de/errors/comparison/
          public function nearlyEqual($a, $b, $epsilon = 0.000001)
          {
              $absA = abs($a);
              $absB = abs($b);
              $diff = abs($a - $b);
      
              if ($a == $b) {
                  return true;
              } else {
                  if ($a == 0 || $b == 0 || $diff < self::MIN_NORMAL) {
                      return $diff < ($epsilon * self::MIN_NORMAL);
                  } else {
                      return $diff / ($absA + $absB) < $epsilon;
                  }
              }
          }
      }
      

      【讨论】:

        【解决方案7】:

        您应该问自己为什么要比较这些数字。如果您知道比较的目的,那么您还应该知道您的数字所需的准确性。这在每种情况和每种应用程序上下文中都是不同的。但在几乎所有实际情况下,都需要绝对准确度。很少有相对准确度适用。

        举个例子:如果您的目标是在屏幕上绘制图形,那么如果浮点值映射到屏幕上的相同像素,您可能希望它们比较相等。如果您的屏幕尺寸为 1000 像素,并且您的数字在 1e6 范围内,那么您可能希望 100 与 200 进行比较。

        给定所需的绝对精度,那么算法变为:

        public static ComparisonResult compare(float a, float b, float accuracy) 
        {
            if (isnan(a) || isnan(b))   // if NaN needs to be supported
                return UNORDERED;    
            if (a == b)                 // short-cut and takes care of infinities
                return EQUAL;           
            if (abs(a-b) < accuracy)    // comparison wrt. the accuracy
                return EQUAL;
            if (a < b)                  // larger / smaller
                return SMALLER;
            else
                return LARGER;
        }
        

        【讨论】:

          【解决方案8】:

          标准建议是使用一些小的“epsilon”值(可能根据您的应用程序选择),并考虑彼此在 epsilon 内的浮点数相等。例如像

          #define EPSILON 0.00000001
          
          if ((a - b) < EPSILON && (b - a) < EPSILON) {
            printf("a and b are about equal\n");
          }
          

          更完整的答案很复杂,因为浮点错误非常微妙且难以推理。如果您真的关心任何精确意义上的平等,那么您可能正在寻找不涉及浮点的解决方案。

          【讨论】:

          • 如果他使用非常小的浮点数,比如 2.3E-15 怎么办?
          • 我正在使用大约 [10E-14, 10E6] 的范围,不是机器 epsilon,但非常接近它。
          • 如果您牢记必须处理 relative 错误,则处理小数字不是问题。如果您不关心相对较大的误差容限,如果您将条件替换为if ((a - b) &lt; EPSILON/a &amp;&amp; (b - a) &lt; EPSILON/a)
          • 上面给出的代码在处理非常大的数字c时也有问题,因为一旦你的数字足够大,EPSILON将小于c的机器精度。例如。假设c = 1E+22; d=c/3; e=d+d+d;。那么e-c 很可能远大于 1。
          • 例如,尝试double a = pow(8,20); double b = a/7; double c = b+b+b+b+b+b+b; std::cout&lt;&lt;std::scientific&lt;&lt;a-c;(根据 pnt 和 nelhage,a 和 c 不相等)或double a = pow(10,-14); double b = a/2; std::cout&lt;&lt;std::scientific&lt;&lt;a-b;(根据 pnt 和 nelhage,a 和 b 相等)
          【解决方案9】:

          我尝试在考虑上述 cmets 的情况下编写一个相等函数。这是我想出的:

          编辑:从 Math.Max(a, b) 更改为 Math.Max(Math.Abs​​(a), Math.Abs​​(b))

          static bool fpEqual(double a, double b)
          {
              double diff = Math.Abs(a - b);
              double epsilon = Math.Max(Math.Abs(a), Math.Abs(b)) * Double.Epsilon;
              return (diff < epsilon);
          }
          

          想法?我仍然需要计算出大于和小于。

          【讨论】:

          • epsilon 应该是Math.abs(Math.Max(a, b)) * Double.Epsilon;,否则对于负的ab,它将始终小于diff。而且我认为您的 epsilon 太小,该函数可能不会返回与 == 运算符不同的任何内容。大于a &lt; b &amp;&amp; !fpEqual(a,b)
          • 当两个值都完全为零时失败,对于 Double.Epsilon 和 -Double.Epsilon 失败,对于无穷大失败。
          • 无穷大的情况在我的特定应用程序中不是问题,但应予以注意。
          【解决方案10】:

          我想出了一个简单的方法来调整 epsilon 的大小以适应被比较的数字的大小。所以,不要使用:

          iif(abs(a - b) < 1e-6, "equal", "not")
          

          如果 ab 可以很大,我将其更改为:

          iif(abs(a - b) < (10 ^ -abs(7 - log(a))), "equal", "not")
          

          我想这并不能满足其他答案中讨论的所有理论问题,但它的优点是只需一行代码,因此可以在 Excel 公式或 Access 查询中使用,而无需 VBA 函数.

          我搜索了其他人是否使用过这种方法,但我没有找到任何东西。我在我的应用程序中对其进行了测试,它似乎运行良好。因此,对于不需要其他答案的复杂性的上下文来说,这似乎是一种足够的方法。但我想知道它是否有我没有想到的问题,因为似乎没有其他人在使用它。

          如果有原因使用日志的测试对于简单比较各种大小的数字无效,请在评论中说明原因。

          【讨论】:

            【解决方案11】:

            您需要考虑到截断错误是相对错误。如果两个数字的差异与它们的 ulp(最后一个单位)一样大,则两个数字大致相等。

            但是,如果您进行浮点计算,您的错误可能性会随着每次操作而增加(尤其是减法时要小心!),因此您的错误容忍度需要相应增加。

            【讨论】:

              【解决方案12】:

              比较双精度是否相等/不等的最佳方法是获取它们差异的绝对值并将其与足够小的(取决于您的上下文)值进行比较。

              double eps = 0.000000001; //for instance
              
              double a = someCalc1();
              double b = someCalc2();
              
              double diff = Math.abs(a - b);
              if (diff < eps) {
                  //equal
              }
              

              【讨论】:

                猜你喜欢
                • 2015-02-18
                • 1970-01-01
                • 2021-05-28
                • 1970-01-01
                • 2013-11-19
                • 1970-01-01
                • 2023-03-12
                • 1970-01-01
                相关资源
                最近更新 更多