为啥这些数字不相等?

Posted

技术标签:

【中文标题】为啥这些数字不相等?【英文标题】:Why are these numbers not equal?为什么这些数字不相等? 【发布时间】:2021-12-23 06:57:38 【问题描述】:

下面的代码显然是错误的。有什么问题?

i <- 0.1
i <- i + 0.05
i
## [1] 0.15
if(i==0.15) cat("i equals 0.15") else cat("i does not equal 0.15")
## i does not equal 0.15

【问题讨论】:

另见***.com/q/6874867 和***.com/q/2769510。 R Inferno 也是另一本很棒的读物。 一个站点范围的语言不可知的问答:Is floating point math broken? dplanet,我在下面的双精度算术中为所有比较案例(“=”、“=”)添加了一个解决方案。希望对您有所帮助。 【参考方案1】:

一般(语言无关)原因

由于并非所有数字都可以在IEEE floating point arithmetic(几乎所有计算机用来表示十进制数字并用它们进行数学运算的标准)中精确表示,因此您不会总是得到您期望的结果。尤其如此,因为一些简单的有限小数(例如 0.1 和 0.05)在计算机中没有精确表示,因此对它们的算术结果可能不会给出与“已知”的答案。

这是众所周知的计算机算术限制,并在多个地方进行了讨论:

R 常见问题解答中有专门的问题:R FAQ 7.31 The R Inferno by Patrick Burns 将第一个“圈子”用于解决此问题(从第 9 页开始) David Goldberg,“每个计算机科学家都应该了解的浮点运算知识”,ACM 计算调查23,1 (1991-03),5-48 @ 987654324@(revision also available) The Floating-Point Guide - What Every Programmer Should Know About Floating-Point Arithmetic 0.30000000000000004.com 比较不同编程语言的浮点运算 几个堆栈溢出问题,包括 Why are floating point numbers inaccurate? Why can't decimal numbers be represented exactly in binary? Is floating point math broken? Canonical duplicate for "floating point is inaccurate"(关于此问题的规范答案的元讨论)

比较标量

R 中的标准解决方案不是使用==,而是使用all.equal 函数。或者更确切地说,因为all.equal 提供了很多关于差异的详细信息,如果有的话,isTRUE(all.equal(...))

if(isTRUE(all.equal(i,0.15))) cat("i equals 0.15") else cat("i does not equal 0.15")

产量

i equals 0.15

更多使用all.equal 而不是== 的示例(最后一个示例应该表明这将正确显示差异)。

0.1+0.05==0.15
#[1] FALSE
isTRUE(all.equal(0.1+0.05, 0.15))
#[1] TRUE
1-0.1-0.1-0.1==0.7
#[1] FALSE
isTRUE(all.equal(1-0.1-0.1-0.1, 0.7))
#[1] TRUE
0.3/0.1 == 3
#[1] FALSE
isTRUE(all.equal(0.3/0.1, 3))
#[1] TRUE
0.1+0.1==0.15
#[1] FALSE
isTRUE(all.equal(0.1+0.1, 0.15))
#[1] FALSE

更多细节,直接从answer to a similar question复制:

您遇到的问题是浮点数在大多数情况下不能准确表示小数,这意味着您会经常发现完全匹配失败。

而当你说:

1.1-0.2
#[1] 0.9
0.9
#[1] 0.9

你可以用十进制找出它真正的想法:

sprintf("%.54f",1.1-0.2)
#[1] "0.900000000000000133226762955018784850835800170898437500"
sprintf("%.54f",0.9)
#[1] "0.900000000000000022204460492503130808472633361816406250"

您可以看到这些数字不同,但表示方式有点笨拙。如果我们以二进制(好吧,十六进制,这是等效的)查看它们,我们会得到更清晰的图像:

sprintf("%a",0.9)
#[1] "0x1.ccccccccccccdp-1"
sprintf("%a",1.1-0.2)
#[1] "0x1.ccccccccccccep-1"
sprintf("%a",1.1-0.2-0.9)
#[1] "0x1p-53"

您可以看到它们相差2^-53,这很重要,因为这个数字是值接近 1 的两个数字之间可表示的最小差异,就像这样。

我们可以通过查看 R 的 machine 字段来找出任何给定计算机的最小可表示数字是多少:

 ?.Machine
 #....
 #double.eps     the smallest positive floating-point number x 
 #such that 1 + x != 1. It equals base^ulp.digits if either 
 #base is 2 or rounding is 0; otherwise, it is 
 #(base^ulp.digits) / 2. Normally 2.220446e-16.
 #....
 .Machine$double.eps
 #[1] 2.220446e-16
 sprintf("%a",.Machine$double.eps)
 #[1] "0x1p-52"

您可以使用这个事实来创建一个“几乎等于”函数,该函数检查差值是否接近浮点中可表示的最小数字。事实上,这已经存在:all.equal

?all.equal
#....
#all.equal(x,y) is a utility to compare R objects x and y testing ‘near equality’.
#....
#all.equal(target, current,
#      tolerance = .Machine$double.eps ^ 0.5,
#      scale = NULL, check.attributes = TRUE, ...)
#....

所以 all.equal 函数实际上是检查数字之间的差是两个尾数之间最小差的平方根。

这个算法在被称为非正规数的极小数附近有点有趣,但你不必担心。

比较向量

上面的讨论假设了两个单一值的比较。在 R 中,没有标量,只有向量,隐式向量化是该语言的优势。对于逐元素比较向量的值,前面的原则是成立的,但实现方式略有不同。 == 是矢量化的(按元素进行比较),而 all.equal 将整个矢量作为一个实体进行比较。

使用前面的例子

a <- c(0.1+0.05, 1-0.1-0.1-0.1, 0.3/0.1, 0.1+0.1)
b <- c(0.15,     0.7,           3,       0.15)

== 没有给出“预期”的结果,all.equal 没有按元素执行

a==b
#[1] FALSE FALSE FALSE FALSE
all.equal(a,b)
#[1] "Mean relative difference: 0.01234568"
isTRUE(all.equal(a,b))
#[1] FALSE

相反,必须使用循环两个向量的版本

mapply(function(x, y) isTRUE(all.equal(x, y)), a, b)
#[1]  TRUE  TRUE  TRUE FALSE

如果需要这样的功能版本,可以编写

elementwise.all.equal <- Vectorize(function(x, y) isTRUE(all.equal(x, y)))

可以直接调用

elementwise.all.equal(a, b)
#[1]  TRUE  TRUE  TRUE FALSE

或者,您可以复制all.equal.numeric 的相关内部结构并使用隐式矢量化,而不是将all.equal 包装在更多的函数调用中:

tolerance = .Machine$double.eps^0.5
# this is the default tolerance used in all.equal,
# but you can pick a different tolerance to match your needs

abs(a - b) < tolerance
#[1]  TRUE  TRUE  TRUE FALSE

这是dplyr::near 采用的方法,将自己记录为

这是比较两个浮点数向量是否(成对)相等的安全方法。这比使用== 更安全,因为它具有内置容差

dplyr::near(a, b)
#[1]  TRUE  TRUE  TRUE FALSE

【讨论】:

【参考方案2】:

添加到 Brian 的评论(这就是原因),您可以改用 all.equal 来解决这个问题:

# i <- 0.1
# i <- i + 0.05
# i
#if(all.equal(i, .15)) cat("i equals 0.15\n") else cat("i does not equal 0.15\n")
#i equals 0.15

根据 Joshua 的警告,这里是更新后的代码(感谢 Joshua):

 i <- 0.1
 i <- i + 0.05
 i
if(isTRUE(all.equal(i, .15)))  #code was getting sloppy &went to multiple lines
    cat("i equals 0.15\n") 
 else 
    cat("i does not equal 0.15\n")

#i equals 0.15

【讨论】:

all.equal 在存在差异时不会返回FALSE,因此在if 语句中使用它时需要用isTRUE 包装它。【参考方案3】:

这是 hackish,但很快:

if(round(i, 10)==0.15) cat("i equals 0.15") else cat("i does not equal 0.15")

【讨论】:

但是可以使用all.equal(... tolerance)参数。 all.equal(0.147, 0.15, tolerance=0.05) 是真的。【参考方案4】:

dplyr::near() 是用于测试两个浮点数向量是否相等的选项。这是来自docs 的示例:

sqrt(2) ^ 2 == 2
#> [1] FALSE
library(dplyr)
near(sqrt(2) ^ 2, 2)
#> [1] TRUE

该函数有一个内置的容差参数:tol = .Machine$double.eps^0.5,可以调整。默认参数与all.equal()的默认参数相同。

【讨论】:

【参考方案5】:

我遇到了类似的问题。我使用了以下解决方案。

@我发现这个解决方案是关于不等切割间隔的解决方案。 @ 一世 在 R 中使用了 round 函数。通过将选项设置为 2 位,确实 没有解决问题。

options(digits = 2)
cbind(
  seq(      from = 1, to = 9, by = 1 ), 
  cut( seq( from = 1, to = 9, by = 1),          c( 0, 3, 6, 9 ) ),
  seq(      from = 0.1, to = 0.9, by = 0.1 ), 
  cut( seq( from = 0.1, to = 0.9, by = 0.1),    c( 0, 0.3, 0.6, 0.9 )),
  seq(      from = 0.01, to = 0.09, by = 0.01 ), 
  cut( seq( from = 0.01, to = 0.09, by = 0.01),    c( 0, 0.03, 0.06, 0.09 ))
)

基于options(digits = 2)的不等切割间隔输出:

  [,1] [,2] [,3] [,4] [,5] [,6]
 [1,]    1    1  0.1    1 0.01    1
 [2,]    2    1  0.2    1 0.02    1
 [3,]    3    1  0.3    2 0.03    1
 [4,]    4    2  0.4    2 0.04    2
 [5,]    5    2  0.5    2 0.05    2
 [6,]    6    2  0.6    2 0.06    3
 [7,]    7    3  0.7    3 0.07    3
 [8,]    8    3  0.8    3 0.08    3
 [9,]    9    3  0.9    3 0.09    3


options(digits = 200)
cbind(
  seq(      from = 1, to = 9, by = 1 ), 
  cut( round(seq( from = 1, to = 9, by = 1), 2),          c( 0, 3, 6, 9 ) ),
  seq(      from = 0.1, to = 0.9, by = 0.1 ), 
  cut( round(seq( from = 0.1, to = 0.9, by = 0.1), 2),    c( 0, 0.3, 0.6, 0.9 )),
  seq(      from = 0.01, to = 0.09, by = 0.01 ), 
  cut( round(seq( from = 0.01, to = 0.09, by = 0.01), 2),    c( 0, 0.03, 0.06, 0.09 ))
)

基于轮函数的等切区间输出:

      [,1] [,2] [,3] [,4] [,5] [,6]
 [1,]    1    1  0.1    1 0.01    1
 [2,]    2    1  0.2    1 0.02    1
 [3,]    3    1  0.3    1 0.03    1
 [4,]    4    2  0.4    2 0.04    2
 [5,]    5    2  0.5    2 0.05    2
 [6,]    6    2  0.6    2 0.06    2
 [7,]    7    3  0.7    3 0.07    3
 [8,]    8    3  0.8    3 0.08    3
 [9,]    9    3  0.9    3 0.09    3

【讨论】:

【参考方案6】:

双精度算术中的广义比较(“=”、“=”):

比较一个

IsSmallerOrEqual <- function(a,b)    
# Control the existence of "Mean relative difference..." in all.equal; 
# if exists, it results in character, not logical:
if (   class(all.equal(a, b)) == "logical" && (a<b | all.equal(a, b)))  return(TRUE)
  else if (a < b)  return(TRUE)
      else  return(FALSE) 


IsSmallerOrEqual(abs(-2-(-2.2)), 0.2) # TRUE
IsSmallerOrEqual(abs(-2-(-2.2)), 0.3) # TRUE
IsSmallerOrEqual(abs(-2-(-2.2)), 0.1) # FALSE
IsSmallerOrEqual(3,3); IsSmallerOrEqual(3,4); IsSmallerOrEqual(4,3) 
# TRUE; TRUE; FALSE

比较 a >= b:

IsBiggerOrEqual <- function(a,b) 
# Control the existence of "Mean relative difference..." in all.equal; 
# if exists, it results in character, not logical:
if (   class(all.equal(a, b)) == "logical" && (a>b | all.equal(a, b)))  return(TRUE)
  else if (a > b)  return(TRUE)
      else  return(FALSE) 

IsBiggerOrEqual(3,3); IsBiggerOrEqual(4,3); IsBiggerOrEqual(3,4) 
# TRUE; TRUE; FALSE

比较 a = b:

IsEqual <- function(a,b) 
# Control the existence of "Mean relative difference..." in all.equal; 
# if exists, it results in character, not logical:
if (   class(all.equal(a, b)) == "logical" )  return(TRUE)
  else  return(FALSE) 


IsEqual(0.1+0.05,0.15) # TRUE

【讨论】:

以上是关于为啥这些数字不相等?的主要内容,如果未能解决你的问题,请参考以下文章

为啥这些日期时间对象时区不相等?

为啥要测试这些数字 (2^16, 2^31 ....)

为啥这个 C# 正则表达式不起作用?

检查mysql中n个数字的相等性

Revolving Digits(hdu 4333)

数字的二进制。如何检查相等的数字系列?