Double 的 R 精度 - 为什么代码返回负值,为什么预期正结果?

R Precision for Double - Why code returns negative why positive outcome expected?

提问人:Max577 提问时间:11/10/2022 最后编辑:r2evansMax577 更新时间:11/11/2022 访问量:81

问:

我正在测试计算 Prod(b-a) 的 2 种方法,其中 a 和 b 是长度为 n 的向量。 Prod(b-a)=(b1-a1)(b2-a2)(b3-a3)*...(bn-an),其中 b_i>a_i>0 表示所有 i=1,2,3,n。对于某些特殊情况,另一种计算方法(方法2)此prod(b-a)更有效。它使用以下公式,即展开项并求和:

enter image description here

我的问题是:当a_i非常接近 b_i 时,真正的结果可能是非常非常接近 0,比如 10^(-16)。方法 1(减法和乘法)始终返回正输出。使用公式的方法 2 有时会返回负输出(我的实验大约有 7~8% 的时间返回负值)。从数学上讲,这两个方法应该返回完全相同的输出。但在计算机语言中,它显然会产生不同的输出。

以下是我运行测试的代码。当我运行测试代码 10000 次时,大约 7~8% 的方法 2 运行返回负输出。根据官方文档,R double 的精度为 “2.225074e-308”,如 R 参数所示:“.机器$double.xmin”。为什么当差异在 10^(-16) ~ 10^(-18) 之间时它会变成负值?任何能说明这一点的帮助都将被逮捕。我还希望得到一些关于如何实际将精度提高到更高级别的建议,如R文档所示。

##########  Testing code 1.  
ftest1case<-function(a,b)  {
  n<-length(a)
  if (length(b)!=n) stop("---------  length a and b are not right.")
  if ( any(b<a) ) stop("----------  b has to be greater than a all the time.")
  out1<-prod(b-a)
  
  out2<-0
  N<-2^n
  for ( i in 1:N ) {
    tidx<-rev(as.integer(intToBits(x=i-1))[1:n])
    tsign<-ifelse( (sum(tidx)%%2)==0,1.0,-1.0)
    out2<-out2+tsign*prod(b[tidx==0])*prod(a[tidx==1])
  }
  c(out1,out2)
}


##########  Testing code 2.  
ftestManyCases<-function(N,printFreq=1000,smallNum=10^(-20))
{
  tt<-matrix(0,nrow=N,ncol=2)
  n<-12
  for ( i in 1:N) {
    a<-runif(n,0,1)
    b<-a+runif(n,0,1)*0.1
    tt[i,]<-ftest1case(a=a,b=b)
  
    if ( (i%%printFreq)==0 ) cat("----- i = ",i,"\n")
    if ( tt[i,2]< smallNum ) cat("------ i = ",i, " ---- Negative summation found.\n")
  }
  
  tout<-apply(tt,2,FUN=function(x)  { round(sum(x<smallNum)/N,6) } )
  names(tout)<-c("PerLess0_Method1","PerLee0_Method2")
  list(summary=tout, data=tt)  
}  
 

########  Step 1.  Test for 1 case.
n<-12
a<-runif(n,0,1)
b<-a+runif(n,0,1)*0.1
ftest1case(a=a,b=b)   

########  Step 2   Test Code 2 for multiple cases.
N<-300
tt<-ftestManyCases(N=N,printFreq = 100) 
tt[[1]]
r 精度

评论

1赞 Allan Cameron 11/11/2022
这听起来像是为什么这两个数字不相等的一个版本,其根源是同一个问题。
0赞 Max577 11/11/2022
嗨,艾伦,感谢您对我的问题发表评论。我相信我的问题与您的链接所指向的内容很接近。我主要关心的是为什么我得到负输出。我认为本·博尔克(Ben Bolker)的解释有很大帮助。

答:

0赞 Ben Bolker 11/11/2022 #1

我很难想象一个由生成 2^n 排列并将它们相加的算法何时会比直接的差异乘积更有效,但我会相信你的话,它有一些特殊情况。

正如评论中所建议的,问题的根源是添加不同量级的值时浮点误差的累积;有关浮点的 R 特定问题,请参阅此处,有关一般解释,请参阅此处

首先,举个简单的例子:

n <- 12
set.seed(1001)
a <- runif(a,0,1)
b <- a + 0.01
prod(a-b) ## 1e-24
out2 <- 0
N <- 2^n
out2v <- numeric(N)
for ( i in 1:N ) {
    tidx <- rev(as.integer(intToBits(x=i-1))[1:n])
    tsign <- ifelse( (sum(tidx)%%2)==0,1.0,-1.0)
    j <- as.logical(tidx)
    out2v[i] <- tsign*prod(b[!j])*prod(a[j])
}
sum(out2v)  ## -2.011703e-21

使用扩展精度(精度为 1000 位)来检查简单/蛮力计算是否更可靠:

library(Rmpfr)
a_m <- mpfr(a, 1000)
b_m <- mpfr(b, 1000)
prod(a_m-b_m) 
##  1.00000000000000857647286522936696473705868726043995807429578968484409120647055193862325070279593735821154440625984047036486664599510856317884962563644275433171621778761377125514191564456600405460403870124263023336542598111475858881830547350667868450934867675523340703947491662460873009229537576817962228e-24

这证明了这种情况的重点,但一般来说,执行扩展精度算术可能会扼杀您获得的任何性能提升。

用值重做基于排列的计算(使用 ,并返回运行求和,而不是在向量中累积值并调用)给出准确的答案(至少到前 20 位左右,我没有进一步检查),但在我的机器上需要 6.5 秒(而不是使用常规浮点时的 0.03 秒)。mpfrout2 <- mpfr(0, 1000)out2 <- out2 + ...sum()

为什么这个计算有问题?首先,请注意 (approx 2e-308) 和 (approx 2e-16) 之间的差异,前者是系统可以存储的最小浮点值,后者是 1+x > x 的最小值,即可以在不发生灾难性抵消的情况下添加的最小相对值(比此量级大一点的值将经历严重,但不是灾难性的, 取消)。.Machine$double.xmin.Machine$double.eps

现在看一下 值的分布 ,值的序列 :out2vout2v

hist(out2v)

enter image description here

存在类似大小的负数和正数聚类。如果我们的求和碰巧加上了一堆几乎取消的值(因此结果非常接近 0),然后将其添加到另一个接近零的值,我们将得到错误的取消。

完全有可能有一种方法可以重新排列此计算,以免发生错误的取消,但我想不出一个容易的办法。

评论

0赞 Max577 11/11/2022
多谢!我认为参数“.Machine$double.eps(约2e-16)“解释了我在计算中观察到的内容。每次我观察到错误的负数时,输出都低于 2e-16。换句话说,使用此设置,小于 2e-16 的加法/减法是不可靠的。在这些情况下,预期的积极结果可能会因此而变成消极的”。机器$double.eps(约2e-16)”
0赞 Max577 11/11/2022
还有一个问题:如果我只做求和,似乎没有问题。换句话说,如果我添加非常小的正数,则输出是正确的。只有当我同时进行求和减法时,才会产生负输出。也许是减法导致了“取消”,对吧?
0赞 Max577 11/11/2022
我计算的背景是计算贝叶斯后验概率。我有大约 600 种疾病和 4000 种症状。我可以汇总疾病的概率,也可以汇总症状的概率。无论哪种方式,我都需要经历指数级的聚合时间。我使用的涉及减法的公式是,当我汇总症状空间的概率时,阳性症状的数量可能远小于疾病的数量。
0赞 Ben Bolker 11/11/2022
没有更多细节,我不太确定减法从何而来。这个问题介于 Stack Overflow 和 CrossValidate 之间;如果你在简历上发布了你的后验概率表达式的详细描述,那么有人可能会熟悉一种高效而稳定的计算方法(简历也允许在问题中使用 TeX 标记......
0赞 Max577 11/22/2022
我已经写了一篇关于我的问题的甲板。我可以把它寄给你。您的电子邮件是: [email protected]