为什么在R中,apply()方法比for循环慢?

22

作为最佳实践的一部分,我正在努力确定是更好地创建一个函数并在矩阵中使用apply(),还是更好地通过函数循环遍历矩阵。我尝试了两种方式,并惊讶地发现apply()比较慢。任务是将向量评估为正或负,然后返回一个包含1(如果是正数)和-1(如果是负数)的向量。在mash()函数中进行循环,在apply()函数中传递squish()函数。

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 循环可以处理简单的数值向量,并返回与输入相同类型的对象。


2
使用system.time()而不是proc.time,它更适合这项任务。或者更好的方法是,按照本帖子中的一些示例,多次复制测试并取平均值,以获得更好的结果:http://stats.stackexchange.com/questions/3235/timing-functions-in-r - Chase
感谢提供时间链接。我刚开始进行基准测试。 - Milktrader
你还应该检查microbenchmark包以获得更准确的度量。 - aL3xa
5个回答

41

apply(和plyr)函数族的重点不在于速度,而是表达能力。 它们还倾向于消除循环所需的繁琐代码,从而可以防止错误。

最近,在stackoverflow上的答案过分强调速度。随着计算机变得更快和R核心优化R的内部,您的代码会自动变得更快。但您的代码永远无法自己变得更优雅或更易于理解。

在这种情况下,您可以兼得两者:使用向量化的优美答案,同时也非常快速,(million > 0) * 2 - 1


6
这与Burns在《R地狱》一书中所述相呼应,即apply函数族基本上是R循环,它们的好处并不在于速度,他将其称为循环隐藏。 - Milktrader
我想指出的是,这个解决方案(在这种和类似情况下应该是默认考虑的)不仅非常快,而且比ifelse快十倍,比OP的使用formash快十一倍,比从OP中应用squish函数的apply快162倍。(使用library(microbenchmark)进行计时,数据为OP的milliontimes=100。) - lebatsnok
2
抱歉提出这个问题,虽然我同意表达性和意图的重要性,但我不同意等待个人电脑变得更快的态度。我昨天就需要我的结果,我不能等待几天才能得到正确编写且未经优化的东西,即使它只需要几分钟。 - Net_Raider

12

正如Chase所说:利用向量化的能力。你正在比较两种低效的解决方案。

为了说明为什么您的apply方案速度更慢:

在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最好,但循环仍然似乎优于内置的apply函数。如果我遇到ifelse()无法处理的不同问题,恐怕我会更倾向于使用可怕的循环而不是apply。至少我不会轻信apply会更好,我可能会测试最佳解决方案。 - Milktrader
@Chase 谢谢你提供 system.time() 和 all.equal() 工具。 - Milktrader
@milktrader:对于非常长的向量,这变成了内部设计的问题。这可以从我的测试和Chase的测试之间的时间差异中看出来。现在请记住,选择使用apply还有其他原因。Chase已经在评论中给出了链接。还要看一下apply、sapply、lapply等函数之间的区别,以及在sapply中使用USE.NAMES=F和simplify=F选项的加速效果。 - Joris Meys

7

如果您想的话,可以在向量上使用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参与时,当然没有竞争。我可能没有用对术语,但是apply函数族是否被认为是映射函数,而我是否想象中读到过在R中映射函数比for循环更受欢迎? - Milktrader
@Joris,你能指出@Chase的回答中哪里有向量化吗?这是一个我还没有掌握但经常出现的概念。 - Milktrader
@Milktrader:函数ifelse在R中使用内部循环处理向量。这与for循环或任何apply函数不同。ifelse()采用向量,因此无需使用显式循环函数。因此,ifelse是一个矢量化函数。 - Joris Meys
@Milktrader - 在R中使用apply和for循环的好信息:https://dev59.com/73E95IYBdhLWcg3wheNR - Chase

5
在这种情况下,进行基于索引的替换比使用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

拥有所有这些工具绝对是值得的。您可以使用最适合您的工具(因为您需要在几个月或几年后理解代码),然后开始转向更优化的解决方案,如果计算时间变得不可行。


4
更加简洁、更快的方式是(million > 0) * 2 - 1 - hadley
感谢比较。我理解ifelse()和索引是向量化,或者使用C运行循环。所有向量操作都使用循环,但如果将工作传递给C,可以更快地完成。显式循环和apply函数族类似,因为它们从R内部运行循环。 - Milktrader

3
更好的使用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 Leoni
@Eduardo 我已经添加了一些示例。时间可以少于1/20或类似于for循环的时间。 - Wojciech Sobala
所以,一个使用循环的“更好的示例”输给了矢量化(和C代码)...再次。 - Eduardo Leoni

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接