【问题标题】:Why is apply() method slower than a for loop in R?为什么 apply() 方法比 R 中的 for 循环慢?
【发布时间】:2011-07-28 20:12:42
【问题描述】:

作为最佳实践,我正在尝试确定是创建一个函数并在矩阵中apply() 它是否更好,或者是否最好在函数中简单地循环一个矩阵。我尝试了两种方式,并惊讶地发现apply() 速度较慢。任务是获取一个向量并将其评估为正数或负数,然后如果它是正数则返回一个向量,如果它是负数则返回 -1。 mash() 函数循环,squish() 函数被传递给apply() 函数。

million  <- as.matrix(rnorm(100000))

mash <- function(x){
  for(i in 1:NROW(x))
    if(x[i] > 0) {
      x[i] <- 1
    } else {
      x[i] <- -1
    }
    return(x)
}

squish <- function(x){
  if(x >0) {
    return(1)
  } else {
    return(-1)
  }
}


ptm <- proc.time()
loop_million <- mash(million)
proc.time() - ptm


ptm <- proc.time()
apply_million <- apply(million,1, squish)
proc.time() - ptm

loop_million 结果:

user  system elapsed 
0.468   0.008   0.483 

apply_million 结果:

user  system elapsed 
1.401   0.021   1.423 

如果性能下降,使用apply()for 循环有什么优势?我的测试有问题吗?我比较了两个结果对象以寻找线索,发现:

> class(apply_million)
[1] "numeric"
> class(loop_million)
[1] "matrix"

这只会加深谜团。 apply() 函数不能接受简单的数字向量,这就是为什么我在开始时使用 as.matrix() 进行转换。但随后它返回一个数字。 for 循环适用于简单的数字向量。它返回一个与传递给它的对象具有相同类的对象。

【问题讨论】:

  • 使用system.time() 而不是proc.time,它更适合这项任务。或者更好的是,按照这篇文章中的一些示例,通过多次复制测试并取其平均值来获得更好的结果:stats.stackexchange.com/questions/3235/timing-functions-in-r
  • 感谢时间链接。刚刚开始进行基准测试。
  • 您还应该检查microbenchmark 包以获得更准确的测量值。

标签: r benchmarking


【解决方案1】:

apply(和 plyr)系列函数的重点不是速度,而是表现力。它们还倾向于防止错误,因为它们消除了循环所需的簿记代码。

最近,有关 stackoverflow 的答案过分强调速度。随着计算机变得越来越快,并且 R-core 优化了 R 的内部结构,您的代码将自行变得越来越快。您的代码将永远不会变得更优雅或更容易理解。

在这种情况下,您可以两全其美:使用矢量化的优雅答案也非常快,(million &gt; 0) * 2 - 1

【讨论】:

  • 这与我在 Burns 在 R Inferno 中发现的内容相呼应,apply 系列函数基本上是 R 循环,它们的好处不是速度。他称之为循环隐藏。
  • 我想指出,这个解决方案(这应该是在这种情况和类似情况下考虑的默认解决方案)不仅非常快,它比ifelse快十倍,十一倍比 OP 的 mash(使用 for)快 162 倍,比 OP 的 squish 函数快 162 倍。 (使用library(microbenchmark)times=100 和来自OP 的million 作为数据的时序。)
  • 很抱歉提出这个问题,虽然我同意你的观点,表达力和意图很重要,但我不同意等待电脑变得更快的态度。我昨天需要我的结果,我不必等待几天才能写出正确的东西,甚至没有优化,不需要超过几分钟
【解决方案2】:

正如蔡斯所说:利用矢量化的力量。您在这里比较了两个不好的解决方案。

澄清为什么您的应用解决方案较慢:

在 for 循环中,您实际上使用了矩阵的向量化索引,这意味着没有进行类型转换。我在这里有点粗略,但基本上内部计算忽略了尺寸。它们只是作为属性保存并与表示矩阵的向量一起返回。举例说明:

> x <- 1:10
> attr(x,"dim") <- c(5,2)
> y <- matrix(1:10,ncol=2)
> all.equal(x,y)
[1] TRUE

现在,当您使用 apply 时,矩阵在内部被拆分为 100,000 个行向量,每个行向量(即一个数字)都通过函数传递,最后将结果组合成适当的形式。 apply 函数认为向量在这种情况下是最好的,因此必须连接所有行的结果。这需要时间。

此外,sapply 函数首先使用as.vector(unlist(...)) 将任何内容转换为向量,最后尝试将答案简化为合适的形式。这也需要时间,因此这里的 sapply 可能会更慢。然而,它不在我的机器上。

如果 apply 在这里是一个解决方案(但不是),你可以比较一下:

> system.time(loop_million <- mash(million))
   user  system elapsed 
   0.75    0.00    0.75    
> system.time(sapply_million <- matrix(unlist(sapply(million,squish,simplify=F))))
   user  system elapsed 
   0.25    0.00    0.25 
> system.time(sapply2_million <- matrix(sapply(million,squish)))
   user  system elapsed 
   0.34    0.00    0.34 
> all.equal(loop_million,sapply_million)
[1] TRUE
> all.equal(loop_million,sapply2_million)
[1] TRUE

【讨论】:

  • 您用大写字母 IF 进行了比较,我并没有忘记这一点。我需要报告的是,如果我将样本增加到 1000 万,则循环比两个 sapply 测试快 2 秒。显然 ifelse 是最好的,但循环似乎仍然胜过内置的应用函数。如果我有另一个 ifelse() 无法处理的问题,恐怕我可能更喜欢可怕的循环而不是应用。至少我不会相信 apply 会更好,我可能会测试最佳解决方案。
  • @Chase 感谢 system.time() 和 all.equal() 工具。
  • @milktrader :对于很长的向量,它变成了内部设计的问题。这也可以从我的测试和 Chases 之间的时间差异中看出。现在请记住,还有其他选择申请的理由。 Chase 已经在 cmets 中为您提供了链接。还可以看看 apply、sapply、lapply 和 friends 之间的区别,以及在 sapply 中使用 USE.NAMES=F 和 simple=F 等选项的加速。
【解决方案3】:

如果需要,您可以在向量上使用 lapplysapply。但是,为什么不使用适当的工具来完成这项工作,在这种情况下是ifelse()

> ptm <- proc.time()
> ifelse_million <- ifelse(million > 0,1,-1)
> proc.time() - ptm
   user  system elapsed 
  0.077   0.007   0.093 

> all.equal(ifelse_million, loop_million)
[1] TRUE

为了比较起见,这里是使用 for 循环和 sapply 的两个可比较的运行:

> ptm <- proc.time()
> apply_million <- sapply(million, squish)
> proc.time() - ptm
   user  system elapsed 
  0.469   0.004   0.474 
> ptm <- proc.time()
> loop_million <- mash(million)
> proc.time() - ptm
   user  system elapsed 
  0.408   0.001   0.417 

【讨论】:

  • sapply 的使用在这个例子中显然更胜一筹,但循环仍然更快。当然,如果 ifelse 参加,就没有竞争。我的术语可能不正确,但应用函数系列不被视为映射函数,我是否想象我读到映射函数比 R 中的 for 循环更受欢迎?
  • @Joris 你能指出@Chase 的回答中哪里有矢量化吗?这是一个我还没有掌握并且经常出现的概念。
  • @Milktrader :函数 ifelse 使用 R 中的内部循环在向量上工作。这与 for 循环或任何 apply 函数不同。 ifelse() 采用向量,因此无需使用显式循环函数。因此,ifelse 是一个矢量化函数。
  • @Milktrader - 关于 R 中的 apply 与 for 循环的好信息:stackoverflow.com/questions/2275896/…
【解决方案4】:

在这种情况下,进行基于索引的替换比ifelse()*apply() 系列或循环要快得多:

> million  <- million2 <- as.matrix(rnorm(100000))
> system.time(million3 <- ifelse(million > 0, 1, -1))
   user  system elapsed 
  0.046   0.000   0.044 
> system.time({million2[(want <- million2 > 0)] <- 1; million2[!want] <- -1}) 
   user  system elapsed 
  0.006   0.000   0.007 
> all.equal(million2, million3)
[1] TRUE

让所有这些工具触手可及是非常值得的。您可以使用对您最有意义的一种(因为您需要在数月或数年后理解代码),然后如果计算时间变得过高,则开始转向更优化的解决方案。

【讨论】:

  • 或者更简洁,甚至更快,(million &gt; 0) * 2 - 1
  • 感谢比较。我了解 ifelse() 和索引是矢量化,或者使用 C 来运行循环。所有向量操作都使用循环,但如果将工作传递给 C,则可以更快地完成。显式循环和应用函数族相似,因为它们在 R 中运行循环。
【解决方案5】:

for 循环速度优势的更好示例。

for_loop <- function(x){
    out <- vector(mode="numeric",length=NROW(x))
    for(i in seq(length(out)))
        out[i] <- max(x[i,])
    return(out)
    }

apply_loop <- function(x){
    apply(x,1,max)
}

million  <- matrix(rnorm(1000000),ncol=10)
> system.time(apply_loop(million))
  user  system elapsed 
  0.57    0.00    0.56 
> system.time(for_loop(million))
  user  system elapsed 
  0.32    0.00    0.33 

编辑

Eduardo 建议的版本。

max_col <- function(x){
    x[cbind(seq(NROW(x)),max.col(x))]
}

按行

> system.time(for_loop(million))
   user  system elapsed 
   0.99    0.00    1.11 
> system.time(apply_loop(million))
  user  system elapsed 
   1.40    0.00    1.44 
> system.time(max_col(million))
  user  system elapsed 
  0.06    0.00    0.06 

按列

> system.time(for_loop(t(million)))
  user  system elapsed 
  0.05    0.00    0.05 
> system.time(apply_loop(t(million)))
  user  system elapsed 
  0.07    0.00    0.07 
> system.time(max_col(t(million)))
  user  system elapsed 
  0.04    0.00    0.06 

【讨论】:

  • 如果你使用 max.col(一个基本函数),你可以将时间减半。
  • @Eduardo 我添加了一些示例。时间可以少于 for 循环的 1/20 或类似。
  • 所以,一个“更好的例子”使用循环丢失到矢量化(和 C 代码)......再次。
猜你喜欢
  • 1970-01-01
  • 2017-11-09
  • 1970-01-01
  • 2019-12-08
  • 2014-07-21
  • 1970-01-01
  • 1970-01-01
  • 2018-03-14
  • 2017-10-04
相关资源
最近更新 更多