【问题标题】:Speeding up a function involving mapply and integrate加速涉及映射和集成的功能
【发布时间】:2015-06-20 07:36:57
【问题描述】:

我继承了 R 的一些代码,它的运行速度非常慢。大部分时间都花在评估表单的函数上(大约有 15 个这样的函数具有不同的被积函数 G):

TMin <- 0.5

F <- function (t, d) {
    result <- ifelse(((d > 0) & (t > TMin)),
                     mapply(function(t, d) integrate(G, lower=0, upper=t, t, d)$value, t, d),
                     0)

    return(result)

}

为了测试,我使用了下面的虚拟函数,但在实际代码中,Gs 要复杂得多,涉及 exp()、log()、dlnorm()、plnorm() 等。

G <- function(x, t, d) {
    mean(rnorm(1e5))
    x + t - d
}   

在最坏的情况下,F 将被计算大约 200 万次。 该函数以 3 种不同的方式调用:
t 是单个数字,d 是数字向量,或者,
t 是数值向量,d 是单个数字,或者,
t是数值向量,是数值向量

有没有(简单的)方法可以加速这个功能?

到目前为止,我已经尝试了以下方面的变化(以摆脱 ifelse 循环):

F2 <- function (t,d) {
    TempRes <- mapply(function(t, d) integrate(G, lower=0, upper=t, t, d)$value, t, d)
    TempRes[(d <= 0) | (t <= TMin)] <- 0
    result <- TempRes

    return(result)
}

F3 <- function (t,d) {
    result <- rep(0, max(length(t),length(d)))
    test <- ((d > 0) & (t > TMin))
    result[test] <- mapply(function(t, d) integrate(G, lower=0, upper=t, t, d)$value, t, d)[test]

    return(result)
}

但它们几乎花费了完全相同的时间。

【问题讨论】:

  • 你确定积分没有闭式解吗?因为到目前为止,您拥有提高性能的最佳潜力。如果你的数学技能生疏了,你可以问问 CAS。
  • 和往常一样,如果您对性能不满意,请分析您的代码。
  • 如果有一个封闭的,我会感到非常惊讶,但是,你是对的,这值得一看。
  • 这可能是个坏主意,但您可以尝试将 15 个函数合并为一个向量值函数,并使用 cubature 包中的 adaptIntegrate。它比 R 的一维积分要慢,但具有处理向量值函数的优势。如果您以后想用更快的语言编写被积函数,它can be easily interfaced in C++ code
  • result = …; return(result) 在 R 中真的没有意义:函数的最后一个表达式自动是函数的结果。无需将其分配给变量,无需return

标签: r optimization integrate mapply


【解决方案1】:

您正在执行大量独立集成。您可以通过同时在单独的内核上执行这些集成来加快速度(如果您有可用的多核处理器)。问题是 R 默认以单线程方式执行其计算。但是,有许多可用的包允许多线程支持。我最近回答了几个类似的问题herehere,并提供了一些关于相关包和功能的附加信息。

此外,正如@Mike Dunlavey 已经提到的,您应该避免对不符合您的条件的td 值执行积分。 (您当前正在对这些值执行不需要的函数评估,然后用 0 覆盖结果)。

我在下面添加了一个可能的改进。请注意,您必须创建一个包含函数G 的单独文件,以便在集群节点上对其进行评估。在下面的代码中假设这个文件被称为functionG.R

sn-p:

library(doParallel)
F4 <- function(t,d) {
  results = vector(mode="numeric",max(length=length(t),length(d))) # Zero vector

  logicalVector <- ((d > 0) & (t > TMin))
  relevantT <- t[logicalVector]
  relevantD <- d[logicalVector] # when d is single element, NA values created

  if(length(relevantT) > 1 | length(relevantD) > 1)
  {
    if(length(d)==1) # d is only one element instead of vector --> replicate it
      relevantD <- rep(d,length(relevantT))
    if(length(t)==1) # t is only one element instead of vector --> replicate it
      relevantT <- rep(t,length(relevantD))

    cl <- makeCluster(detectCores()); 
    registerDoParallel(cl)
    clusterEvalQ(cl,eval(parse("functionG.R")))

    integrationResults <- foreach(i=1:length(relevantT),.combine="c") %dopar%
    {
      integrate(G,lower=0,upper=relevantT[i],relevantT[i],relevantD[i])$value;
    }
    stopCluster(cl)
    results[logicalVector] <- integrationResults
  }
  else if(length(relevantT==1)) # Cluster overhead not needd
  {
    results[logicalVector] = integrate(G,lower=0,upper=relevantT,relevantT,relevantD)$value;
  }

  return(results)
}

我的 CPU 包含 6 个启用超线程 (x2) 的物理内核。结果如下:

> t = -5000:20000
> d = -5000:20000
> 
> start = Sys.time()
> testF3 = F3(t,d)
> timeNeededF3 = Sys.time()-start
> 
> start = Sys.time()
> testF4 = F4(t,d)
> timeNeededF4 = Sys.time()-start;

> timeNeededF3
Time difference of 3.452825 mins
> timeNeededF4
Time difference of 29.52558 secs
> identical(testF3,testF4)
[1] TRUE

运行此代码时,内核似乎一直在使用。但是,您可以通过在内核周围更有效地预拆分数据,然后在单独的内核上使用应用类型函数来进一步优化此代码。

如果需要更多优化,您还可以深入了解integrate 函数。您可以通过允许不太严格的数值近似来调整设置并获得性能提升。作为替代方案,您可以实现自己的简单版本的自适应辛普森正交并使用离散步长。您很可能会像这样获得巨大的性能提升(如果您能够/愿意在近似值中允许更多错误)。

编辑: 更新代码以使其适用于所有场景:d 和/或 t 有效/无效数字或向量

回复评论 @mawir:你是对的。 ifelse(test, yes, no) 将为 test 评估为 TRUE 的行返回相应的 yes 值,它将为 test 评估为 FALSE 的行返回相应的 no 值。但是,它首先必须评估您的 yes 表达式才能创建 length(test)yes 向量。这段代码演示了这一点:

> t = -5000:5
> d = -5000:5
> 
> start = Sys.time()
> testF1 = F(t,d)
> timeNeededF1 = Sys.time()-start
> timeNeededF1
Time difference of 43.31346 secs
> 
> start = Sys.time()
> testF4 = F4(t,d)
> timeNeededF4 = Sys.time()-start
> timeNeededF4
Time difference of 2.284134 secs

只有 td 的最后 5 个值与此方案相关。 但是,在F1 函数内部,ifelse 首先对所有dt 值评估mapply,以便创建yes 向量。这就是函数执行需要这么长时间的原因。接下来,它选择满足条件的元素,否则为 0。 F4 函数可以解决这个问题。

此外,您说您在td 是非向量的情况下获得加速。但是,在这种情况下,没有使用并行化。您通常应该在 t/d 中的一个或两个是向量的情况下获得最大加速。

ANOTHER EDIT,回应 Roland 的评论: 如果您不想创建单独的函数文件,您可以将 clusterEvalQ(cl,eval(parse("functionG.R"))) 替换为 clusterExport(cl,"G")

【讨论】:

  • 您可以通过在循环内定义G 来避免clusterEvalQ(性能成本可以忽略不计)。而且我相信您可以使用.export 参数将其导出到核心(如果这不能自动工作)。
  • @Roland 是他们的地方,我可以确切地查找 ifelse(test, yes, no) 正在做什么(除了源代码)?我的印象是它实际上是一个封装的 for 循环,遍历测试并在每个条目处评估相关的是或否。但是在其他地方我看到它声明它完全评估是和否表达式,然后通过选择相关值进行迭代,这显然要慢得多。此外,F、F2 和 F3 都花费大约相同的时间。这种行为实现是否依赖(我目前正在 64 位窗口上测试 RGui)?
  • @Jellen Vermeir 我刚刚测试了您的代码,并且在 t 或 d 是单个值(即使没有并行计算)的情况下,如果其他人没有反对意见,它会明显更快,我会接受你的回答。
  • @mawir |Roland 我在下面的回答中添加了关于 ifelse 问题的回复。
  • @Roland:您对 for 循环内函数的评估是正确的。但是,我个人认为在单独的文件中定义函数更干净,看起来也不那么杂乱,但这是一种主观意见。集群导出用于将变量导出到子节点的全局环境中。我刚刚检查过,你是对的。显然也可以导出函数。
【解决方案2】:

一般来说,查看的位置位于最内层循环中,您可以通过减少时间或调用次数来加快速度。您有一个运行mapply 的内部循环,但随后您从中提取元素[test]。这是否意味着所有其他元素都被丢弃了?如果是这样,为什么还要花时间计算额外的元素?

【讨论】:

  • 我不明白这个。无论如何,它充其量只是一个评论。
  • 我认为 Mike Dunlavey 的观点是,我尝试的解决方案必须至少与原始函数一样长,因为它们在理论上进行完全相同的计算,然后花费额外的时间过滤结果。我尝试了它们,因为我不知道 ifelse() 在“幕后”做什么(以及它需要多少时间)。
  • 我知道ifelse 在“幕后”做什么,这不是你的瓶颈。如果你分析你的代码,你会看到这一点。
  • @Roland:我指的是 OP 的函数F3,它执行mapply,后跟[test]。看起来它正在构建一个列表/数组,获取一个元素,然后丢弃其余元素(以及创建它所花费的精力)。还是我错过了什么?我同意这需要一些分析 - 我使用 rprofile,然后只查看它生成的堆栈跟踪。
猜你喜欢
  • 2014-03-06
  • 2013-03-28
  • 2021-05-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-30
  • 2011-08-12
  • 1970-01-01
相关资源
最近更新 更多