我应该如何进行浮点比较?


80

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

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答案的评论之一)。您将不得不选择您想要少的两个。
toochin 2011年

另一方面,如果仅处理有理数,则可以基于整数执行某种有理数算法,然后,如果可以将两个数中的一个取消为另一个,则认为两个数相等。
toochin 2011年

好吧,目前我正在模拟。我通常在进行这些比较的地方与可变的时间步长有关(用于解决某些问题)。在某些情况下,我需要检查一个对象的给定时间步长是否等于,小于或大于另一个对象的时间步长。
Mike Bailey


Answers:


68

除非您正处于浮动/双精度限制的边缘,否则比较较大/较小并不是真正的问题。

为了进行“模糊等于”比较, 经过大量工作并考虑了很多批评之后,我为《浮点指南》提出了这个(Java代码,应该很容易适应):

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,两个非常小的值为零的极小值或无穷大,它会失败。

一种替代方法(有关更多详细信息,请参见上面的链接)是将浮点数的位模式转换为整数,并接受固定整数范围内的所有内容。

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


1
@toochin:取决于您要允许的误差幅度,但是当您考虑最接近零,正数和负数的非规格化数时,这显然成为一个问题-除零之外,这两个数之和比其他两个数更近值,但许多基于相对误差的幼稚实现都会认为它们相距太远。
Michael Borgwardt'2

2
嗯 您有一个测试else if (a * b == 0),但是您对同一行的评论是a or b or both are zero。但是这不是两个不同的东西吗?例如,如果a == 1e-162b == 2e-162则条件a * b == 0为真。
马克·迪金森

1
@toochin:主要是因为该代码被认为可以轻松地移植到其他可能没有该功能的语言中(它也仅在1.5中被添加到Java中)。
Michael Borgwardt

1
如果该功能使用率很高(例如,视频游戏的每个帧),我将使用史诗般的优化将其重写为汇编形式。

1
很好的指南和很好的答案,尤其是在abs(a-b)<eps这里考虑答案。两个问题:(1)将所有<s更改为<=s,从而允许“ zero-eps”比较等于精确比较是否更好?(2)diff < epsilon * (absA + absB);代替diff / (absA + absB) < epsilon;(最后一行)使用会更好吗?
Franz D.

36

TL; DR

  • 使用以下函数代替当前接受的解决方案,以免在某些极限情况下产生某些不良结果,同时可能会提高效率。
  • 了解数字的预期不精确度,并在比较函数中相应地输入它们。
bool nearly_equal(
  float a, float b,
  float epsilon = 128 * FLT_EPSILON, float relth = 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(relth, epsilon * norm);
}

图形,好吗?

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

第一个是在相对模式,其中的区别xy相对认为它们的振幅|x| + |y|。以2D绘制时,它具有以下轮廓,其中绿色表示x和相等y。(epsilon出于说明目的,我选择了0.5)。

在此处输入图片说明

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

第二种是绝对模式,当我们简单地将它们的差异与固定数字进行比较时。它给出以下轮廓(再次以epsilon0.5和relth1表示)。

在此处输入图片说明

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

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

在Michael Borgwardt的答案中,切换基于的值diff,该值应低于relthFloat.MIN_NORMAL在他的答案中)。下图以阴影线显示了此切换区域。

在此处输入图片说明

因为该relth * epsilon值较小relth,所以绿色色块不会粘在一起,这反过来又给解决方案带来了不好的性质:我们可以找到这样的三元组,即x < y_1 < y_2,而x == y2x != y1

在此处输入图片说明

请看以下引人注目的示例:

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

我们拥有x < y1 < y2,实际上y2 - x比它大2000倍以上y1 - x。但是使用当前的解决方案

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

相反,在以上提出的解决方案中,切换区域基于的值|x| + |y|,该值由下面的阴影正方形表示。它可以确保两个区域正常连接。

在此处输入图片说明

另外,上面的代码没有分支,这可能会更有效率。考虑到操作,例如maxabs,其中先验需要支化,通常具有专用组装说明。出于这个原因,我认为这种方法优于另一种解决方案,该解决方案是nearlyEqual通过将开关从更改为来修复Michael的diff < relth方法diff < eps * relth,这将产生基本相同的响应模式。

在相对和绝对比较之间切换?

这些模式之间的切换是围绕进行的relth,这FLT_MIN与接受的答案相同。这种选择意味着的表示形式float32会限制我们的浮点数的精度。

这并不总是有意义。例如,如果您比较的数字是减法的结果,则可能在范围内FLT_EPSILON更有意义。如果它们是相减数的平方根,则数字不精确度可能更高。

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

这就是引入relth以上参数的原因。

而且,通过不relth与乘epsilon,该参数的解释很简单,并且与我们期望的这些数字的数值精度相对应。

数学隆隆声

(保留在这里主要是出于我的荣幸)

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

以下内容非常明显:

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

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

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

  • 如果a < b < c,则a =~ c表示a =~ b(更接近的值也应相等)
  • 如果a, b, m >= 0a =~ b暗示a + m =~ b + m(具有相同差异的较大值也应相等)
  • 如果0 <= λ < 1然后a =~ b暗示λa =~ λb(可能不那么明显)。

这些属性已经对可能的近等函数给出了强大的约束。上面建议的功能可以验证它们。可能缺少一个或几个其他明显的属性。

当一个人认为是由和参数化=~的一族平等关系时,也可以添加=~[Ɛ,t]Ɛrelth

  • 如果Ɛ1 < Ɛ2然后a =~[Ɛ1,t] b暗示a =~[Ɛ2,t] b(给定公差的相等意味着更高公差的相等)
  • 如果t1 < t2然后a =~[Ɛ,t1] b暗示a =~[Ɛ,t2] b(给定不精确度的相等意味着较高不精确度的相等)

提出的解决方案也对此进行了验证。


1
这是一个很好的答案!
大卫

1
C ++实现问题:可以(std::abs(a) + std::abs(b))大于std::numeric_limits<float>::max()吗?
Anneb

15

我有浮点数比较的问题A < BA > 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");
}

晶圆厂-绝对价值-负责它们是否基本相等。


1
fabs如果您进行了首次测试if (A - B < -Epsilon)
根本

10

我们必须选择一个公差等级来比较浮点数。例如,

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!")

3

我快速想到浮点比较的想法

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)
}

1

从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;
            }
        }
    }
}

1

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

举个例子:如果您的目标是在屏幕上绘制图形,那么如果浮点值映射到屏幕上的相同像素,则可能希望它们比较相等。如果屏幕尺寸为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;
}

0

标准建议是使用一些小的“ε”值(可能取决于您的应用程序来选择),并认为彼此在ε内的浮点数是相等的。例如类似

#define EPSILON 0.00000001

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

一个更完整的答案很复杂,因为浮点错误非常微妙,并且令人费解。如果您真的在乎精确意义上的平等,那么您可能正在寻找一种不涉及浮点的解决方案。


如果他正在使用非常小的浮点数(例如2.3E-15)怎么办?
toochin 2011年

1
我正在使用大约[10E-14,10E6]的射程,不是完全的机器epsilon,而是非常接近它。
Mike Bailey

2
如果要牢记必须处理相对错误,那么处理小数字并不是问题。如果您不关心相对较大的错误容忍度,则将其替换为诸如if ((a - b) < EPSILON/a && (b - a) < EPSILON/a)
toochin

2
当处理非常大的数字时c,上面给出的代码也有问题,因为一旦您的数字足够大,EPSILON就会小于的机器精度c。例如c = 1E+22; d=c/3; e=d+d+d;。然后e-c可能比1大得多
toochin

1
例如,尝试double a = pow(8,20); double b = a/7; double c = b+b+b+b+b+b+b; std::cout<<std::scientific<<a-c;(a和c根据pnt和nelhage不相等)或double a = pow(10,-14); double b = a/2; std::cout<<std::scientific<<a-b;(a和b根据pnt和nelhage不相等)
toochin 2011年

0

我试着在写上面的注释时写一个相等函数。这是我想出的:

编辑:从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;,否则它总是小于diff负数ab。而且我认为您epsilon太小了,该函数可能不会返回与==运算符不同的任何内容。大于是a < b && !fpEqual(a,b)
toochin 2011年

1
当两个值都完全为零时失败,对于Double.Epsilon和-Double.Epsilon失败,对于无穷大失败。
Michael Borgwardt

1
在我的特定应用程序中,无穷大的情况不是要关注的问题,但应引起注意。
Mike Bailey

-1

您需要考虑到截断错误是相对的。如果两个数字之差大约等于其ulp(最后一个单位),则两个数字近似相等。

但是,如果执行浮点计算,则每次操作都会增加潜在的错误(尤其是要注意减法!),因此您的错误容忍度需要相应提高。


-1

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

double eps = 0.000000001; //for instance

double a = someCalc1();
double b = someCalc2();

double diff = Math.abs(a - b);
if (diff < eps) {
    //equal
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.