计算具有大量参数组合的函数的最有效方法

Yal*_*Dan 7 optimization performance loops r

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

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)
})
Run Code Online (Sandbox Code Playgroud)

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

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

我尝试并行化该函数(我仅限于在虚拟 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)
Run Code Online (Sandbox Code Playgroud)

虽然这给了我一个加速,但我仍然觉得我并没有真正以最有效的方式解决这个问题,并且希望得到任何建议。

F. *_*ivé 13

@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
Run Code Online (Sandbox Code Playgroud)

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

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
)
Run Code Online (Sandbox Code Playgroud)

我们得到:

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
Run Code Online (Sandbox Code Playgroud)

通过稍微修改 R 中的原始代码,它提供了巨大的加速。这比 Rcpp 代码慢不到两倍,并且可以像以前使用parSapply().

  • 好的!我放大到问题中提到的规模(“dX_i”中的 600k 参数和 80k 值),并且 2x 比率或多或少保持不变(我的代码为 724 秒,你的代码为 1518 秒)。我希望 Rcpp 代码在阈值非常小的情况下真正发挥作用;那么一旦达到阈值就停止计算的能力特别有用。例如,当我将阈值乘以 0.01 时,我的代码将在 17 秒内完成,而您的则需要 221 秒。 (2认同)
  • 您可能可以像我一样通过对“abs(dX_i)”进行排序,然后使用“findInterval”(快速)识别 for 循环中要求和的元素数量,从而从早期停止中获得大部分加速。[[编辑:确认:在更新的示例中,排序和使用 `findInterval` 使您的时间接近 32 秒,其中我将阈值乘以 0.01]] (2认同)

jos*_*ber 10

当我想加速难以矢量化的代码时,我经常求助于 Rcpp。在一天结束时,您试图总结abs(dX_i)^p,限制为abs(dX_i)小于阈值的值gamma * a * sigma_hat * delta_j^(1/2)。您想为一堆对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
Run Code Online (Sandbox Code Playgroud)

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

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


jos*_*ber 5

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
Run Code Online (Sandbox Code Playgroud)

为了看到这一点的实际效果,让我们将原始数据集扩展到问题中描述的内容(约 600k 行参数和约 80k 值dX_i):

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
Run Code Online (Sandbox Code Playgroud)

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