如何在遇到非有限值(NA、NaN或Inf)时强制出现错误

11

我在Matlab中错过的一个条件调试标志是:dbstop if infnan在此处描述。如果设置了这个条件,当遇到InfNaN时,代码执行将停止(如果我没记错的话,Matlab没有NA)。

我如何在R中实现这一点,比测试每个赋值操作后的所有对象更有效率呢?

目前,我看到的唯一方法是通过以下方式进行黑客攻击:

  1. 在可能遇到这些值的所有地方手动插入测试(例如,除法,其中可能会发生除以0)。测试将使用is.finite()在此Q&A中描述,应用于每个元素。
  2. 使用body()修改代码,调用单独的函数,在每个操作或可能只在每个分配之后测试所有对象(可能是所有环境中的所有对象)。
  3. 修改R的源代码(?!)
  4. 尝试使用tracemem识别已更改的变量,并仅检查其中的错误值。
  5. (新 - 请参见注释2)使用某种调用处理程序/回调来调用测试函数。

目前,我正在进行第一项操作。这很繁琐,因为我无法保证我已经检查了所有内容。第二个选项将测试所有内容,即使对象没有更新。那是浪费时间的巨大浪费。第三个选择涉及修改NA、NaN和无限值(+/- Inf)的分配,以便产生一个错误。这似乎最好留给R Core。第四个选项类似于第二个选项-我需要调用一个列出所有内存位置的单独的函数,只是为了标识那些已更改的内存位置,然后检查其值;我甚至不确定这会对所有对象起作用,因为程序可能会执行原地修改,这似乎不会调用duplicate函数。

我是否错过了更好的方法?也许是马克·布拉温顿(Mark Bravington)、卢克·蒂尔尼(Luke Tierney)或类似于options()参数或编译R时的标志等相对基本的聪明工具?

示例代码 这里是一些非常简单的示例代码,用于测试,包括Josh O'Brien提出的addTaskCallback函数。代码没有中断,但在第一种情况下发生了错误,而在第二种情况下未发生错误(即badDiv(0,0,FALSE)不会中止)。我仍在调查回调,因为这看起来很有前途。

badDiv  <- function(x, y, flag){
    z = x / y
    if(flag == TRUE){
        return(z)
    } else {
        return(FALSE)
    }
}

addTaskCallback(stopOnNaNs)
badDiv(0, 0, TRUE)

addTaskCallback(stopOnNaNs)
badDiv(0, 0, FALSE)

注意1:我会满足于使用标准的 R 操作来解决问题,但我的许多计算涉及通过 data.tablebigmemory 使用的对象(即基于磁盘的内存映射矩阵)。这些似乎具有与标准矩阵和数据框操作不同的内存行为。

注意2:回调函数的想法似乎更有前途,因为这不需要我编写可以改变 R 代码的函数,例如通过 body() 的想法。

注意3:我不知道是否存在一些简单的方法来测试非有限值的存在,例如有关对象的元信息,索引 NA、Inf 等存储在对象中的位置,还是这些信息存储在原地。到目前为止,我尝试了 Simon Urbanek 的 inspect 包,并没有找到一种方式来确定非数值值的存在。

后续:Simon Urbanek 在评论中指出,这样的信息作为对象的元信息是不可用的。

注意4:我仍在测试所提出的想法。正如 Simon 建议的那样,检测非有限值的存在应该在 C/C++ 中是最快的;这应该比编译后的 R 代码还要快,但我对任何方法都持开放态度。对于大型数据集,例如在 10-50GB 的量级上,这应该比复制数据节省时间。通过使用多个内核,可以获得进一步的改进,但那需要更高级的技能。


一些原始函数已经内置了这种功能,即如果提供的参数无效或返回非有限结果,则会返回错误或警告。例如,sin(Inf)。也许这是你可以探索的内容。 - Brandon Bertelsen
嗯,并不总是Inf或NaN应该停止您的函数/代码(NA是一个单独的情况,因为它经常被用作“填充物”或“标记”)。我经常运行一些操作,这些操作会在某些矩阵的低信号区域产生一些Inf值。我怀疑您使用is.infinite和/或is.nan对可疑变量进行更好的信息获取。 - Carl Witthoft
在我目前处理的代码+数据情况下,问题值恰好是NA、NaN和Infs这三个。在其他情况下,我肯定需要NA,但今天不需要。 :) 我需要代码尽快中止(由于数据量很大,计算成本相当高),一旦出现这些情况。因此,我真的希望触发一个错误(或至少一个警告)。 - Iterator
@BrandonBertelsen 感谢建议。我查看了基本算术运算符,但在R内部没有发现任何异常。我猜想这种特殊处理可能在某些原始函数的C代码中,但也有可能是只有R巫师才知道的那些事情。 :) - Iterator
@Iterator -- 感谢您提出这个发人深省的问题。它为我带来了一种脑力挑战,让我不断回到SO。如果我的建议对您的特定情况有所帮助,请告诉我! - Josh O'Brien
2个回答

7
我担心没有这样的快捷方式。理论上,在unix上有SIGFPE可以陷阱,但实际上:
  1. 没有标准的方法来启用FP操作以便陷阱它(即使C99也不包括此功能)-它高度依赖于系统(例如Linux上的feenableexcept,AIX上的fp_enable_all等),或者需要使用汇编器针对您的目标CPU
  2. 现在FP操作通常是在矢量单元(如SSE)中完成的,因此您甚至无法确定FPU是否参与其中
  3. R截取了一些关于NaNNA之类的操作,并将其分别处理,因此它们不会传递给FP代码
那么,如果您足够努力,您可以自己编写一个R,以便在您的平台和CPU上捕获一些异常情况(禁用SSE等)。虽然我们不考虑将其构建到R中,但对于特定目的而言,这可能是可行的。
但是,除非您更改R内部代码,否则它仍将无法捕获NaN/NA操作。此外,您还必须检查您正在使用的每个软件包,因为它们可能在其C代码中使用FP操作,并且也可能单独处理NA/NaN
如果您只担心诸如除以零或溢出/下溢之类的情况,则上述方法将起作用,并且可能是最接近解决方案的方法。
仅检查结果可能不是非常可靠,因为您不知道结果是否基于一些中间的NaN计算,该计算更改了可能不需要是NaN的聚合值。如果您愿意放弃这种情况,那么您可以简单地递归地遍历结果对象或工作区。这应该不会非常低效,因为您只需要关心REALSXP而不需要其他任何内容(除非您也不喜欢NA - 那么您将有更多的工作)。
以下是一个示例代码,可用于递归遍历R对象:
static int do_isFinite(SEXP x) {
    /* recurse into generic vectors (lists) */
    if (TYPEOF(x) == VECSXP) {
        int n = LENGTH(x);
        for (int i = 0; i < n; i++)
            if (!do_isFinite(VECTOR_ELT(x, i))) return 0;
    }
    /* recurse into pairlists */ 
    if (TYPEOF(x) == LISTSXP) {
         while (x != R_NilValue) {
             if (!do_isFinite(CAR(x))) return 0;
             x = CDR(x);
         }
         return 1;
    }
    /* I wouldn't bother with attributes except for S4
       where attributes are slots */
    if (IS_S4_OBJECT(x) && !do_isFinite(ATTRIB(x))) return 0;
    /* check reals */
    if (TYPEOF(x) == REALSXP) {
        int n = LENGTH(x);
        double *d = REAL(x);
        for (int i = 0; i < n; i++) if (!R_finite(d[i])) return 0;
    }
    return 1; 
}

SEXP isFinite(SEXP x) { return ScalarLogical(do_isFinite(x)); }

# in R: .Call("isFinite", x)

真糟糕,我一直在等待云彩散开、天使唱歌,以及你的发帖。当我看到“我担心…”这句话时,我想:“哦,等着瞧吧,西蒙一出现,这家伙就会知道错了…让我们看看是谁…” :) 至于为什么我不在R-Devel上发帖——R-Core太可怕了。 :) - Iterator
更加认真地说,我一直在研究回调函数、条件处理程序和您的 inspect 软件包。 1:对象的内部结构是否不会显示是否存在Fp的Infs或NAs?也就是说,是否有任何关于非有限值存在/位置的元信息?2:如果有这样的信息,我是否可以使用调用处理程序,在每个语句执行后调用检查错误值的调用? - Iterator
我看起来吓人吗?;) 很抱歉让你失望 - 在我看来, SIGFPE 真的是最好的选择(我怀疑这就是 Matlab 使用的方式),但缺乏标准真的很令人沮丧(而 Matlab 不需要特殊处理 NAs)。 - Simon Urbanek
但是,除了查看其内容之外,没有办法确定向量是否包含 Inf/NaN/NA。部分原因是 REALSXP 作为一个 double* 指针传递给任何 C 代码,因此没有访问器宏,也没有控制哪些值被写入到哪里(与例如 STRSXP 不同)- 因此您必须每次检查整个向量来实现这样的位。顺便说一句,inspect 现在已成为 R 的一部分,您可以通过 .Internal(inspect(x)) 调用它。 - Simon Urbanek
关于 SIGFPE - 你可能是对的。不幸的是,我无法接触到Matlab的内部。;-) 我希望它是一个.M文件,这样我就可以检查了,但是噢,不透明度是我避免Matlab的许多原因之一。关于 inspect() - Joshua Ulrich在这个答案中建议使用单独的包。这是否意味着该包已被弃用? - Iterator
我没有意识到人们仍在使用它(我是为自己编写的;)) - 我可以将“inspect”函数映射到最近的R版本的内部函数... - Simon Urbanek

7
下面的想法(及其实现)非常不完美。我甚至都不敢建议它,但是:(a)我认为即使在所有丑陋的情况下,它也很有趣;(b)我可以想象出一些有用的情况。考虑到您现在听起来正在每个计算后手动插入检查,我希望您的情况是其中之一。
我的方法分两步。首先,我定义了一个函数nanDetector(),它旨在检测可能由您的计算返回的多个对象类型中的NaN。然后,它使用addTaskCallback()在完成每个顶级任务/计算后调用函数nanDetector() on .Last.value。当它在这些返回值中发现一个NaN时,它会抛出一个错误,您可以使用它来避免任何进一步的计算。
它的缺点包括:
  • 如果您执行类似于设置stop(error = recover)的操作,则很难确定错误触发的位置,因为错误总是从stopOnNaNs()内部抛出。

  • 当它抛出一个错误时,stopOnNaNs()在返回TRUE之前被终止。因此,它从任务列表中删除了,并且如果您想再次使用它,则需要重置addTaskCallback(stopOnNaNs)。(有关更多详细信息,请参见?addTaskCallback的“参数”部分)。

话不多说,这就是它:
# Sketch of a function that tests for NaNs in several types of objects
nanDetector <- function(X) {
   # To examine data frames
   if(is.data.frame(X)) { 
       return(any(unlist(sapply(X, is.nan))))
   }
   # To examine vectors, matrices, or arrays
   if(is.numeric(X)) {
       return(any(is.nan(X)))
   }
   # To examine lists, including nested lists
   if(is.list(X)) {
       return(any(rapply(X, is.nan)))
   }
   return(FALSE)
}

# Set up the taskCallback
stopOnNaNs <- function(...) {
    if(nanDetector(.Last.value)) {stop("NaNs detected!\n")}
    return(TRUE)
}
addTaskCallback(stopOnNaNs)


# Try it out
j <- 1:00
y <- rnorm(99)
l <- list(a=1:4, b=list(j=1:4, k=NaN))
# Error in function (...)  : NaNs detected!

# Subsequent time consuming code that could be avoided if the
# error thrown above is used to stop its evaluation.

这个东西包含了我刚开始考虑的几个有趣的想法,即使用回调函数。不管它是否适用于我的代码,我还得看一下,但它无疑是很有启发性的。谢谢! - Iterator
1
就算你在 R 代码中执行这些检查,效率也会非常低下。因为实际上只需要使用 C 语言就可以轻松地完成这些操作,例如 if (TYPEOF(x) == REALSXP) { double *d = REAL(x); int n = LENGTH(x); for(int i = 0; i < n; i++) if (!R_finite(d[i])) return ScalarLogical(0); } return ScalarLogical(1);,而且你还可以更快速地递归到递归对象中,而无需进行复制。 - Simon Urbanek
@SimonUrbanek 感谢您提供的 C 语言指导 - 这肯定会很快。这种方法是否容易开发,可以通过例如 inline 包使用?或者我能否许愿它出现在 R 中? :) (我猜这是一个 R-devel 的问题。;-)) - Iterator
@SimonUrbanek -- 听起来非常有趣,但我不知道如何在实践中实现它。我从未使用过C进行编程,但并不害怕学习...那么,您在评论中的基本想法是将上述代码与适当的R相关标头放入文本文件中,使用gcc编译它,然后加载已编译的代码,并在调用R函数nanDetector()的位置使用.C(myCnanDetector)来调用它? - Josh O'Brien
@JoshO'Brien 我还在测试这个函数。它通常可以工作,但有时不会被调用。似乎如果代码被包裹在 system.time({}) 中,则回调不会被执行。虽然这并不是最终要紧的事情(我会放弃 system.time),但对于其他尝试此操作的人来说,这值得注意。 - Iterator
啊,我现在明白问题了。addTaskCallback是用于顶层任务的。似乎这不会停止函数内部的执行。我需要再仔细看一下。我刚刚添加了示例代码,说明了发生的情况-第二种情况没有被终止。 - Iterator

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接