【问题标题】:Most efficient way to calculate function with large number of parameter combinations计算具有大量参数组合的函数的最有效方法
【发布时间】:2020-04-24 07:45:36
【问题描述】:

我正在尝试做的极简主义示例:

dX_i <- rnorm(100, 0, 0.0002540362)

p_vec <- seq(0, 1, 0.25)  
gamma_vec <- seq(1, 2, 0.25)     
a_vec <- seq(2, 6, 1)
sigma_hat_vec <- c(0.03201636, 0.05771143, 0.07932116, 0.12262327, 0.15074560)
delta_j_vec <- c(0.0000005850109, 0.0000011700217, 0.0000017550326, 0.0000035100651, 0.0000052650977)

parameters <- expand.grid("p" = p_vec, "gamma" = gamma_vec, "a" = a_vec, "sigma_hat" = sigma_hat_vec, "delta_j" = delta_j_vec)


result <- sapply(1:nrow(parameters), function(x) {
  tmp <- parameters[x,]
  p <- tmp$p
  a <- tmp$a
  gamma <- tmp$gamma
  sigma_hat <- tmp$sigma_hat
  delta_j <- tmp$delta_j

  B <- sum( (abs(dX_i)^p) * ( abs(dX_i) < gamma * a * sigma_hat * delta_j^(1/2) ))

  return(B)
})

目标:在给定 p、a、gamma、sigma_hat、delta_j 的所有组合的情况下,我需要在向量 dX 上计算 B

然而,实际上网格 parameters 有大约 600k 行,dX_i 有大约 80k 行。此外,我有一个 ~1000 dX_i 的列表。因此,我想让这个计算尽可能高效。其他方法,例如将parameters 转换为data.table 并在该data.table 中运行sapply 似乎并没有加快速度。

我尝试将函数并行化(我仅限于在虚拟 Windows 机器上运行脚本):

cl <- makePSOCKcluster(numCores)
num.iter <- 1:nrow(parameters)
parSapply(cl, num.iter, function(x, parameters, dX_i) {
  tmp <- parameters[x,]
  p <- tmp$p
  a <- tmp$a
  gamma <- tmp$gamma
  sigma_hat <- tmp$sigma_hat
  delta_j <- tmp$delta_j
  sum( (abs(dX_i)^p) * ( abs(dX_i) < gamma * a * sigma_hat * delta_j^(1/2) ))
}, parameters, dX_i)
stopCluster(cl)

虽然这让我加快了速度,但我仍然觉得我并没有真正以最有效的方式解决这个问题,如果有任何建议,我将不胜感激。

【问题讨论】:

  • 也许使用贝赛搜索?
  • 只是好奇,它目前有多快以及“足够快”有多快?
  • 你有充分的理由计算这么多项吗?
  • 你真的需要every组合吗?你的目标是什么?如果您正在寻找最小值或最大值,请考虑改用优化器,这将能够实现比网格搜索更智能/更有效的搜索模式。例如,optimoptimx 包。
  • @YalDan 回答 Jon 的问题是一个好的开始,但我认为他所要求的澄清的很大一部分是您需要它多快? 2倍加速好吗? 10 倍? 100 倍?

标签: r performance loops optimization


【解决方案1】:

当我想加速难以矢量化的代码时,我经常求助于 Rcpp。在一天结束时,您试图总结abs(dX_i)^p,限制为小于阈值gamma * a * sigma_hat * delta_j^(1/2)abs(dX_i) 的值。您想为一对p 和一个阈值执行此操作。你可以这样做:

library(Rcpp)
cppFunction(
"NumericVector proc(NumericVector dX_i, NumericVector thresh, NumericVector p) {
  const int n = thresh.size();
  const int m = dX_i.size();
  NumericVector B(n);
  for (int i=0; i < n; ++i) {
    B[i] = 0;
    for (int j=0; j < m; ++j) {
      if (dX_i[j] < thresh[i]) {
        B[i] += pow(dX_i[j], p[i]);
      } else {
        break;
      }
    }
  }
  return B;
}"
)
result2 <- proc(sort(abs(dX_i)), parameters$gamma * parameters$a * parameters$sigma_hat * parameters$delta_j^(1/2), parameters$p)
all.equal(result, result2)
# [1] TRUE

请注意,我的代码对 dX_i 的绝对值进行排序,因此一旦遇到第一个超过阈值的值,它就可以停止计算。

在我的机器上,我看到了 20 倍的加速,从您的代码的 0.158 秒到 Rcpp 代码的 0.007 秒(使用 system.time 测量)。

【讨论】:

  • 非常感谢!加速是显着的,使用我的代码一次迭代需要几分钟,而我在使用你的函数时几乎立即得到我的结果。没想到这么快又简单的解决方案,估计是时候学点C++了
【解决方案2】:

@josliber 的回答非常好。然而,它让 R 看起来很糟糕......而且你必须切换到 C++ 以获得性能。

在他们的回答中实现了三个技巧:

  • 预计算阈值向量
  • 预计算dX_i的绝对值
  • 对这些值进行排序以尽早停止求和

前两个技巧只是一个称为“向量化”的 R 技巧 -> 基本上是对整个向量而不是循环中的单个元素进行操作(例如 gamma * a * sigma_hat * delta_j^(1/2)abs())。

这正是你在使用sum( dX_i^p * vec_boolean ) 时所做的;它是矢量化的(*sum),因此它应该非常快。

如果我们只实现这两个技巧(我们不能真正以相同的方式执行第三个技巧,因为它破坏了矢量化),它会给出:

abs_dX_i <- abs(dX_i)
thresh <- with(parameters, gamma * a * sigma_hat * sqrt(delta_j))
p <- parameters$p
result3 <- sapply(1:nrow(parameters), function(i) {
  in_sum <- (abs_dX_i < thresh[i])
  sum(abs_dX_i[in_sum]^p[i])
})
all.equal(result, result3) # TRUE

如果我们对所有三种解决方案进行基准测试:

microbenchmark::microbenchmark(
  OP = {
    result <- sapply(1:nrow(parameters), function(x) {
      tmp <- parameters[x,]
      p <- tmp$p
      a <- tmp$a
      gamma <- tmp$gamma
      sigma_hat <- tmp$sigma_hat
      delta_j <- tmp$delta_j

      B <- sum( (abs(dX_i)^p) * ( abs(dX_i) < gamma * a * sigma_hat * delta_j^(1/2) ))

      return(B)
    })
  },
  RCPP = {
    result2 <- proc(sort(abs(dX_i)), parameters$gamma * parameters$a *
                      parameters$sigma_hat * parameters$delta_j^(1/2), parameters$p)
  },
  R_VEC = {
    abs_dX_i <- abs(dX_i)
    thresh <- with(parameters, gamma * a * sigma_hat * sqrt(delta_j))
    p <- parameters$p
    result3 <- sapply(1:nrow(parameters), function(i) {
      in_sum <- (abs_dX_i < thresh[i])
      sum(abs_dX_i[in_sum]^p[i])
    })
  },
  times = 10
)

我们得到:

Unit: milliseconds
  expr      min       lq      mean   median       uq      max neval
    OP 224.8414 235.4075 289.90096 270.2767 347.1727 399.3262    10
  RCPP  14.8172  15.4691  18.83703  16.3979  20.3829  29.6624    10
 R_VEC  28.3136  29.5964  32.82456  31.4124  33.2542  45.8199    10

只需稍微修改 R 中的原始代码,它就可以大大加快速度。 这比 Rcpp 代码慢两倍,并且可以像以前使用 parSapply() 那样轻松并行化。

【讨论】:

  • 感谢您的努力,看到 R 可以通过适当的设计选择相当快,这令人鼓舞!我在我尝试优化的原始脚本中实现了@josliber 的方法,并且能够在大约 4.5 小时内执行所有计算(涉及大量开销)。我也会尝试您的解决方案,看看它有多快,尤其是与并行化结合使用时(虽然我不确定它是否有益,这将取决于一次迭代的总持续时间)
  • 不错!我按比例放大到问题中提到的比例(dX_i 中的 600k 参数和 80k 值),2x 比率或多或少保持不变(我的代码为 724 秒,您的代码为 1518 秒)。我希望 Rcpp 代码在阈值非常小的情况下真正发挥作用。那么一旦达到阈值就停止计算的能力尤其有益。例如,当我将阈值乘以 0.01 时,我的代码在 17 秒内完成,而您的代码需要 221 秒。
  • 您可以通过像我一样对abs(dX_i) 进行排序然后使用findInterval 来(快速)确定要在for 循环中求和的元素数量,从而从早期停止中获得大部分加速。 [[编辑:已确认:在我将阈值乘以 0.01 的更新示例中,排序和使用 findInterval 使您的方法达到 32 秒]]
  • @josliber 这是一个有趣的想法!我试图看看如果我在运行proc() 之前执行dX_i &lt;- sort(abs(dX_i))[1:findInterval(g * 2 * sigmahat * deltaj^(1/2), sort(abs(dX_i)) )] 会发生什么。这给了我另一个加速。与并行化一起,我能够在大约 80 分钟内执行所有计算!我现在将看看如果我将它与预先计算阈值结合起来会发生什么。几毫秒可以产生如此大的变化,真是令人惊讶。
  • @YalDan 这看起来不太像我所期望的表达式 - 确保检查它是否为您提供了正确的结果。我在这个答案中想R_VEC,但用abs_dX_i &lt;- sort(abs(dX_i))替换abs_dX_i &lt;- abs(dX_i),用pos &lt;- findInterval(thresh, abs_dX_i)计算阈值位置,然后在sapply调用中只使用sum(head(abs_dX_i, pos[i])^p[i])
【解决方案3】:

一个观察结果是,您的参数集中的每个 p 值实际上有大量重复。您可以单独处理每个 p 值;这样,您只需将 dX_i 提升到特定的 p 值求和一次。

result4 <- rep(NA, nrow(parameters))
sa_dX_i <- sort(abs(dX_i))
thresh <- parameters$gamma * parameters$a * parameters$sigma_hat * parameters$delta_j^(1/2)
loc <- findInterval(thresh, sa_dX_i)
loc[loc == 0] <- NA  # Handle threshold smaller than everything in dX_i
for (pval in unique(parameters$p)) {
  this.p <- parameters$p == pval
  cs_dX_i_p <- cumsum(sa_dX_i^pval)
  result4[this.p] <- cs_dX_i_p[loc[this.p]]
}
result4[is.na(result4)] <- 0  # Handle threshold smaller than everything in dX_i
all.equal(result, result4)
# [1] TRUE

要查看实际情况,让我们将原始数据集扩大到问题中描述的内容(dX_i 中的约 600k 行参数和约 80k 值):

set.seed(144)
dX_i <- rnorm(80000, 0, 0.0002540362)
p_vec <- seq(0, 1, 0.025)  
gamma_vec <- seq(1, 2, 0.025)     
a_vec <- seq(2, 6, 0.3)
sigma_hat_vec <- c(0.03201636, 0.05771143, 0.07932116, 0.12262327, 0.15074560)
delta_j_vec <- c(0.0000005850109, 0.0000011700217, 0.0000017550326, 0.0000035100651, 0.0000052650977)
parameters <- expand.grid("p" = p_vec, "gamma" = gamma_vec, "a" = a_vec, "sigma_hat" = sigma_hat_vec, "delta_j" = delta_j_vec)
dim(parameters)
# [1] 588350      5
length(unique(parameters$p))
# [1] 41

加速非常显着——这段代码在我的计算机上需要 0.27 秒,而我在此问题的其他答案中发布的 Rcpp 代码需要 655 秒(使用纯 R 时加速了 2400 倍!)。显然,这种加速只有在 parameters 数据帧中的 p 值相对较少(每个重复多次)时才有效。如果每个 p 值都是唯一的,那么这可能会比其他建议的方法慢得多。

【讨论】:

  • 这太不可思议了@josliber。我在大约 9500 万行的数据集上运行了这段代码,速度从 1 天到 40 分钟到惊人的 12 秒!非常感谢您的帮助!顺便说一句,p 通常不超过seq(0, 6, 0.25),所以这应该不是问题。
猜你喜欢
  • 2021-12-13
  • 1970-01-01
  • 2011-12-11
  • 2023-02-10
  • 2019-10-08
  • 1970-01-01
  • 2011-05-16
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多