【问题标题】:Evaluate if two doubles are equal based on a given precision, not within a certain fixed tolerance根据给定的精度评估两个双精度值是否相等,而不是在某个固定的容差范围内
【发布时间】:2011-06-14 19:12:25
【问题描述】:

我正在运行 NUnit 测试来评估一些已知的测试数据和计算结果。这些数字是浮点双精度数,所以我不希望它们完全相等,但我不确定如何在给定的精度下将它们视为相等。

在 NUnit 中,我们可以与固定容差进行比较:

double expected = 0.389842845321551d;
double actual   = 0.38984284532155145d; // really comes from a data import
Expect(actual, EqualTo(expected).Within(0.000000000000001));

这对于零以下的数字效果很好,但随着数字的增长,容差确实需要改变,所以我们总是关心相同的精度位数。

具体来说,这个测试失败了:

double expected = 1.95346834136148d;
double actual   = 1.9534683413614817d; // really comes from a data import
Expect(actual, EqualTo(expected).Within(0.000000000000001));

当然更大的数字会因公差而失败..

double expected = 1632.4587642911599d;
double actual   = 1632.4587642911633d; // really comes from a data import
Expect(actual, EqualTo(expected).Within(0.000000000000001));

评估两个浮点数在给定精度下相等的正确方法是什么?在 NUnit 中是否有内置方法可以做到这一点?

【问题讨论】:

    标签: c# nunit floating-point


    【解决方案1】:

    来自 msdn:

    默认情况下,Double 值包含 15 位小数精度,但内部维护最多 17 位。

    我们假设 15,那么。

    因此,我们可以说我们希望容差达到相同的程度。

    小数点后有多少个精确数字?我们需要知道最高位到小数点的距离,对吧?幅度。我们可以用 Log10 得到这个。

    然后我们需要将 1 除以 10 ^ 精度来得到一个接近我们想要的精度的值。

    现在,你需要做比我更多的测试用例,但这似乎可行:

      double expected = 1632.4587642911599d;
      double actual = 1632.4587642911633d; // really comes from a data import
    
      // Log10(100) = 2, so to get the manitude we add 1.
      int magnitude = 1 + (expected == 0.0 ? -1 : Convert.ToInt32(Math.Floor(Math.Log10(expected))));
      int precision = 15 - magnitude ;
    
      double tolerance = 1.0 / Math.Pow(10, precision);
    
      Assert.That(actual, Is.EqualTo(expected).Within(tolerance));
    

    已经很晚了——这里可能有问题。我根据您的三组测试数据对其进行了测试,并且每组都通过了。将 pricision 更改为 16 - magnitude 导致测试失败。将其设置为14 - magnitude 显然会导致它通过,因为容差更大。

    【讨论】:

    • 这是一个很好的解决方案,但是在比较0.00.0 时会产生异常。不过,expected 周围的一个简单的if 条件可以解决这个问题。
    • 啊,是的。我以为会有什么。 :) 我已经更新了我的答案以考虑到这一点。
    • 这非常有效。我运行了 105 个单元测试,大约一半是正面的,一半是负面的,只有 8 个通过了。 Michael Borgwardt 的答案通过并没有通过完全相同的测试。
    • @Samuel Neff,失败的测试用例是什么?
    • int magnitude = 1 + (Math.Abs(expected) < 1E-15 ? -1 : Convert.ToInt32(Math.Floor(Math.Log10(Math.Abs(expected))))); — 这将修复具有负期望值和比较为零的情况。之前有没有人用过这个带有负值的代码?
    【解决方案2】:

    这是我为The Floating-Point Guide 想出的(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 == 0) { // a or b or both are zero
            // relative error is not meaningful here
            return diff < (epsilon * epsilon);
        } else { // use relative error
            return diff / (absA + absB) < epsilon;
        }
    }
    

    真正棘手的问题是当要比较的数字之一为零时该怎么办。最好的答案可能是,这种比较应该始终考虑被比较数字的领域含义,而不是试图具有普遍性。

    【讨论】:

    • @Michael Borgwardt,epsilon 到底是什么?在比较中,0.0001 的值实际上是什么?通过测试,我可以找到适合我的测试数据的 epsilon 值(预期为真,预期为假),但我对这个参数不是 100% 清楚。
    • @Samuel:epsilon 是一个相对误差范围,即 epsilon 为 0.01 意味着这两个值之间的差异必须小于约 1%。但是如果一个值为 0,这将变得毫无意义,所以在这种情况下,我要求另一个值小于 epsilon 的平方。这很随意;因此得出的结论是,可能不存在普遍有用的比较函数。
    • @Michael Bordwardt,谢谢,这是一个很好的解释。我理解并同意您关于普遍有用的比较功能的说法,但不幸的是,这是针对库,而不是特定的企业应用程序,所以我正在寻找尽可能通用的东西。我还在做更多的测试,你的解决方案和 Bretts(更简单,不太整洁)的解决方案似乎在相同的情况下工作并且在相同的情况下失败。
    • @Samuel:从概念上讲,这两种方法测试的东西或多或少是一样的,尽管我没想到它们在那个程度上表现相同。只是为了确保,我注意到我的测试套件中缺少一些东西:你有比较正负零、无穷大和 NaN 的测试用例吗?
    • @Samuel:谢谢,我的地址是 brazzy@gmail.com - 这些情况很可能可以通过添加一个额外的if(a==b) return true 分支来解决。
    【解决方案3】:

    如何将每个项目转换为字符串并比较字符串?

    string test1 = String.Format("{0:0.0##}", expected);
    string test2 = String.Format("{0:0.0##}", actual);
    Assert.AreEqual(test1, test2);
    

    【讨论】:

    • 我已经想到了这一点并同意它可以转换为字符串。不像您所说的那样,您希望使用每个值可用的完整精度转换为字符串,然后以相同的长度截断它们。但是,我希望得到一个数学上合适的结果,而不是一些不那么老套的东西。
    • 此解决方案意味着 0.99999999999999999999 将不等于 1.00000000000000000000 - 但这不是故意的,是吗?
    【解决方案4】:

    我不知道是否有使用 nunit 的内置方法,但我建议将每个浮点数乘以您所寻求的精度的 10 倍,将结果存储为长整数,并将两个长整数与彼此。
    例如:

    double expected = 1632.4587642911599d;
    double actual   = 1632.4587642911633d;
    //for a precision of 4
    long lActual = (long) 10000 * actual;
    long lExpected = (long) 10000 * expected;
    
    if(lActual == lExpected) {  // Do comparison
       // Perform desired actions
    }
    

    【讨论】:

    • 这在数学上与使用公差相同,只是以不同的方式应用。
    【解决方案5】:
    Assert.That(x, Is.EqualTo(y).Within(10).Percent);
    

    是一个不错的选择(将其更改为相对比较,其中 x 必须在 y 的 10% 以内)。您可能需要为 0 添加额外的处理,否则在这种情况下您将获得准确的比较。

    【讨论】:

      【解决方案6】:

      这是一个快速的想法,但是如何将它们向下移动直到它们低于零?应该类似于 num/(10^ceil(log10(num))) 。 . .不确定它的效果如何,但它是一个想法。

      1632.4587642911599 / (10^ceil(log10(1632.4587642911599))) = 0.16324587642911599
      

      【讨论】:

      • 这看起来至少是一个好的开始。这个计算对精度有什么影响?是否有精度损失?显然我不关心两个可比较值的完全精度,但如果我们在两者中都失去精度,那么比较可能会产生一些误报。
      【解决方案7】:

      怎么样:

      const double significantFigures = 10;
      Assert.AreEqual(Actual / Expected, 1.0, 1.0 / Math.Pow(10, significantFigures));
      

      【讨论】:

      • 当然,如果期望值为零或接近零,则必须编写特殊情况。零有多少位有效数字?零还是无限?
      【解决方案8】:

      两个值之间的差值应小于任一值除以精度。

      Assert.Less(Math.Abs(firstValue - secondValue), firstValue / Math.Pow(10, precision));
      

      【讨论】:

        【解决方案9】:

        打开 FsUnit

        实际 |> 应该 (equalWithin errorMargin) 预期

        【讨论】:

          猜你喜欢
          • 2020-06-02
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-11-03
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多