【问题标题】:Loop and bootstrap script takes too long to run循环和引导脚本运行时间过长
【发布时间】:2020-09-10 17:06:39
【问题描述】:

我有下面的R 脚本,它需要超过 24 小时才能运行,但最终在 10-gigabyte ramcore M7Windows 10 上运行。该脚本执行以下操作:

这就是我想用R做的事情

  • A.我已经生成了 50 个时间序列数据集。

  • B.我将相同的时间序列数据集分割成以下大小的块:2,3,...,48,49 使我从上面的步骤 1 中形成了 48 个不同的时间序列。

  • C.我将每个 48 时间序列数据集划分为 traintest 集,因此我可以使用 Metrics 包中的 rmse 函数来获取步骤 2 中形成的 48 个子序列的均方根误差 (RMSE)。

  • D.然后根据其块大小将每个系列的 RMSE 制成表格

  • E.我为每个 48 个不同的时间序列数据集获得了最好的 ARIMA 模型。

我的 R 脚本

# simulate arima(1,0,0)
library(forecast)
library(Metrics)

n=50
phi <- 0.5
set.seed(1)

wn <- rnorm(n, mean=0, sd=1)
ar1 <- sqrt((wn[1])^2/(1-phi^2))

for(i in 2:n){
  ar1[i] <- ar1[i - 1] * phi + wn[i]
}
ts <- ar1

t <- length(ts)    # the length of the time series
li <- seq(n-2)+1   # vector of block sizes to be 1 < l < n (i.e to be between 1 and n exclusively)

# vector to store block means
RMSEblk <- matrix(nrow = 1, ncol = length(li))
colnames(RMSEblk) <-li

for (b in 1:length(li)){
    l <- li[b]# block size
    m <- ceiling(t / l)                                 # number of blocks
    blk <- split(ts, rep(1:m, each=l, length.out = t))  # divides the series into blocks

    # initialize vector to receive result from for loop
    singleblock <- vector()                     
    for(i in 1:1000){
        res<-sample(blk, replace=T, 10000)        # resamples the blocks
        res.unlist<-unlist(res, use.names = F)    # unlist the bootstrap series
        # Split the series into train and test set
        train <- head(res.unlist, round(length(res.unlist) * 0.6))
        h <- length(res.unlist) - length(train)
        test <- tail(res.unlist, h)

        # Forecast for train set
        model <- auto.arima(train)
        future <- forecast(test, model=model,h=h)
        nfuture <- as.numeric(future$mean)        # makes the `future` object a vector            
        RMSE <- rmse(test, nfuture)               # use the `rmse` function from `Metrics` package

        singleblock[i] <- RMSE # Assign RMSE value to final result vector element i
    }

    RMSEblk[b] <- mean(singleblock) # store into matrix
}

RMSEblk

R 脚本实际运行,但需要超过 24 小时才能完成。 loops 中的运行次数(10000 和 1000)是使任务完美所需的最小值。

请问我该怎么做才能在更短的时间内完成脚本?

【问题讨论】:

  • 如果我正确地遵循了您的代码,您是否正在尝试通过auto.arima() 估计 48,000 个 ARIMA 模型?如果我不得不猜测,那是你的瓶颈。一些随机的想法:1)你可以并行运行吗?我看不到每次迭代之间有任何依赖关系,因此您可以利用每个可用的核心。 2) 你可以将任何示例代码移出内部 for 循环吗? 3) 是否可以设置 w/i auto.arima() 来加快计算速度? 4)您是否分析了代码以确认瓶颈在哪里?如果没有,这里有一篇好文章:adv-r.had.co.nz/Profiling.html
  • @Chase 48 ARIMA 模型不是 48,000
  • object li 的值为 2:49(长度为 48)。内部循环迭代 1000 次,这是调用 auto.arima 的地方。那么这不是对auto.arima 的 48 * 1000 = 48000 次调用吗?无论如何,我上面的所有 cmets 仍然适用...对于此代码需要 24 小时意味着您可能会增长/迭代一个对象...R 可能必须重新分配内存 48000 次,并且在每次迭代时它必须找到一个更大的内存块......由于显而易见的原因,这是低效的。
  • 如果你不相信我,把这个贴在auto.arima()的调用上方,看看有多少值被吐到屏幕上print(paste0(b, "-", i))
  • 接替@Chase:根本问题是,无论为什么你这样做,你确实在运行auto.arima()函数48000次。跨度>

标签: r loops


【解决方案1】:

tl;dr你可能不得不以某种方式并行化它。


一个问题是您正在增长一个对象;也就是说,首先分配一个长度为零的向量 (singleblock &lt;- vector()),然后一次将其递增一个元素 (singleblock[i] &lt;- RMSE)。正如R Inferno 第 2 章中所讨论的,这是非常低效的。对于这个示例,它慢了 5 倍。

f1 <- function(x) { p <- numeric(0); for (i in 1:1000) p[i] <- 0 }
f2 <- function(x) { p <- numeric(1000); for (i in 1:1000) p[i] <- 0 }
microbenchmark(f1(),f2())
## Unit: microseconds
##  expr     min       lq      mean  median      uq     max neval cld
##  f1() 202.519 207.2105 249.84095 210.574 221.340 3504.95   100   b
##  f2()  40.274  40.6710  69.83741  40.9615  42.8275 2811.779   100  a 

但是:这并不重要。低效版本(增长向量)需要 210 微秒的中位时间。

microbenchmark(auto.arima(train),times=20L)
## Unit: milliseconds
##               expr      min       lq     mean   median       uq      max neval
##  auto.arima(train) 630.7335 648.3471 679.2703 657.6697 668.0563 829.1648    20

您的auto.arima() 调用大约需要 660 毫秒 - 大约长 3000 倍。对预测步骤使用类似的microbenchmark 调用会产生大约 20 毫秒的中值时间。

你可以做更正式的profiling,或者像这里显示的那样继续点点滴滴,但我没有在你的代码中看到任何看起来需要很长时间的东西(我可能会检查sample()下一个,但我怀疑它可以与auto.arima()相媲美。)

除非您能找到更快的 auto.arima() 版本(我对此表示怀疑),或者精简一些内容(例如限制搜索空间),否则您唯一剩下的选择就是并行化。您可以使用许多不同的工具在许多不同的级别上执行此操作,但首先要查看的是parallel option to auto.arima。您可能会选择并行化循环(在“R 中的并行计算”上进行网络搜索会提供大量资源);请注意,尝试在多个级别上进行并行化可能会给您带来麻烦。

PS 粗略计算(48000 * 660 毫秒)大约需要 9 小时 - 只占大约 1/3 的时间(我原本预计它会达到 80% 左右);也许你的处理器比我的慢?

【讨论】:

  • 您能证明您在脚本中提供的补救措施吗?
  • 抱歉,我想我在这方面花费的时间与我现在可用的时间一样多。这些是非常标准的事情,我给出了很多参考/指针。如果您尝试它们并在某个地方遇到问题,请随时发布后续问题。
  • @DanielJames - 这是关于设置并行框架的一个很好的介绍 - 我可能会设置它,以便您的内核每个都采用外循环的一个子集:cran.r-project.org/web/packages/doParallel/vignettes/…。正如本在上面指出的那样,找到减少auto.arima() 时间的方法将带来最大的收益。我对auto.arima() 的内容不是很熟悉,但是您可以检查一下是否在错误检查中占用了过多的时间,您可以删掉(类似于lm() 如何进行一系列预- 在拟合模型之前进行处理。
  • @DanielJames - 这个问题是一个很好的用例,可以从您选择的云提供商那里租用多核机器......我会弄清楚如何并行运行您的脚本,然后将其农场到 EC2 的一个大实例:aws.amazon.com/blogs/big-data/running-r-on-aws
【解决方案2】:

为了演示,为避免循环中的对象增长,请考虑apply family 解决方案,例如vapply。请注意 RMSEblksingleblock 现在是如何直接分配 vapply 的结果,而不需要按索引分配元素的簿记。

...

# DEFINED METHOD
proc_bootstrap <- function(b) {
    l <- li[b]                                          # block size
    m <- ceiling(t / l)                                 # number of blocks
    blk <- split(ts, rep(1:m, each=l, length.out = t))  # divides the series into blocks

    # initialize vector to receive result from for loop
    singleblock <- vapply(1:1000, function(i) {
      res <- sample(blk, replace=TRUE, 10000)        # resamples the blocks
      res.unlist <- unlist(res, use.names = FALSE)   # unlist the bootstrap series

      # Split the series into train and test set
      train <- head(res.unlist, round(length(res.unlist) * 0.6))
      h <- length(res.unlist) - length(train)
      test <- tail(res.unlist, h)

      # Forecast for train set
      model <- auto.arima(train)
      future <- forecast(test, model=model,h=h)
      nfuture <- as.numeric(future$mean)        # makes the `future` object a vector

      RMSE <- Metrics::rmse(test, nfuture)      # RETURN RMSE
    }, numeric(1))

    mean(singleblock)                           # RETURN MEAN
  }

# VAPPLY CALL
RMSEblk <- vapply(1:length(li), proc_bootstrap, numeric(1))

或者,填充您最初定义的单行矩阵(也许作为命名向量更好?):

# MATRIX to store block means
RMSEblk <- matrix(nrow = 1, ncol = length(li))
colnames(RMSEblk) <-li

RMSEblk[] <- vapply(1:length(li), proc_bootstrap, numeric(1))

注意:上面的时间可能与嵌套的 for 循环没有本质区别,因为您仍会迭代 48,000 个模型调用。不过,这个解决方案可能在更大的迭代中可以更好地扩展。但正如所讨论的,请查看并行处理(参见paralleldoParallelforeach 包),它可以从forapply 解决方案翻译。


请务必同时 profile 显示(在建模调用之外)unlistheadtail 有时间问题:

utils::Rprof(tmp <- tempfile(), memory.profiling = TRUE)
RMSEblk <- vapply(1:length(li), proc_bootstrap, numeric(1))
utils::Rprof(NULL)
summaryRprof(tmp, memory="both")
unlink(tmp)

【讨论】:

  • 我认为这很有趣/有用,但也有点切题。 (1) 正如你所说的(正如我上面所说的),对象增长问题实际上只是整个问题的一小部分。 (2) 一旦你记得预分配,apply 系列解决方案实际上与for 循环没有任何不同(参见 R Inferno 第 4 章,“过度矢量化”)。最后一点是最有用的/对我的回答最有帮助。
  • 明白了,@BenBolker。当我打开时,这是为了演示,可以说是比 OP 的原始方法更紧凑的方法。但是,是的,另一种有效的方法是用长度初始化对象,然后分配。并重新讨论 forapply 的古老 R 辩论,请参阅这个较旧的答案:Is the “*apply” family really not vectorized?(当然,没有经验法则,因为上下文很重要)。
  • RMSE 从未在您的脚本中使用,是遗漏还是故意? singleblock[i] &lt;- RMSE # Assign RMSE value to final result vector element i 正如我提出的问题。在您的情况下,您确实在答案中定义了 RMSE &lt;- Metrics::rmse(test, nfuture) # RETURN RMSE 它,但 RMSE 的使用从未超过其定义。
猜你喜欢
  • 2018-07-02
  • 1970-01-01
  • 2019-02-12
  • 1970-01-01
  • 2018-11-08
  • 1970-01-01
  • 1970-01-01
  • 2022-06-10
  • 1970-01-01
相关资源
最近更新 更多