R 多线程并行计算

R作为当今最为流行的统计分析软件之一,其处理数据一般都是默认单线程跑的。而现今各种云平台层出不穷,其主要目的就是加快分析速度从而节省分析成本,比如现在大型平台的全基因组分析的耗时都按分钟记了(当然是最快的那种),因此有时我们平时处理数据时,也可以优先考虑下将R并行化,在计算资源足够的情况下,肯定是越快越好

关于什么是并行计算以及R并行化可以看看这篇文章R 与并行计算

简单的说R的并行化计算其实没有改变其整个并行环境,而是先启动N个附属进程,然后将数据分割成N块在N个核上进行运行,等待附属进程结束返回结果

根据上述原理,在使用一些并行方法时有一点需要注意:

当你并行调用的自定义函数时,如果用到一些需要加载其他包才有的函数,则需要在自定义中先labrary下

下面以一个计算matrix每行的标准差为例:

data <- matrix(runif(10^8), ncol = 10)

先看看for循环和apply等常规方法所用时间

system.time({
  invisible(
    for (i in 1:nrow(data)) {
      sd(data[i,])
    }
  )
})
user  system elapsed 
256.451   0.082 256.628 

system.time({
  invisible(
    apply(data, 1, sd)
  )
})
user  system elapsed 
405.927   1.506 407.701 

上面两者看起来有点差别(不知为啥。。),接着再试试向量化计算标准化

system.time({
  invisible(
    sqrt(rowSums((data - rowMeans(data))^2)/(dim(data)[2] - 1))
  )
})
user  system elapsed 
1.479   0.408   1.888  

从上看出这个例子如果将计算过程向量化后,提升的运算速度是非常明显的,所以有时向量化比apply等循环更加有效,比如参考文章提升R语言运算效率的11个实用方法

当必须用循环时,这时最好就尝试下用下面几种并行方法,如果计算资源充足的话

如果使用场景是applylapply或者sapply,那么可以尝试下parallel包的parApplyparLapplyparSapply,parallel包已经是R的默认安装包了;简单用法如下:detectCores检查核数, makeCluster设定核数,然后parApply执行

library(parallel)
detectCores(logical = F)
cl <- makeCluster(getOption("cl.cores", 4))
system.time({
  invisible(
    parApply(cl, data, 1, sd)
  )
})
stopCluster(cl)
user  system elapsed 
45.470   4.816 135.120

设定了4个核,parApply并行速度比apply提升了不少

接着再试试foreach包(个人觉得蛮好用的),其主要对for循环进行了并行,用法跟parallel包略有不同,如下:先加载下doParallel包,然后用registerDoParallel登记下集群,然后用foreach函数计算

注意:foreach返回的是list,所用如果不用.combine参数的话,需要用do.call将所有结果整合

library(foreach)
library(doParallel)
cl <- makeCluster(4)
registerDoParallel(cl)
system.time({
  invisible(
    result_df <- foreach(i = 1:nrow(data), .combine = c) %dopar% sd(data[i,])
  )
})
stopCluster(cl)

所用时间就不列了,反正是上万秒了,这里可以发现foreach并行比for循环还要慢,后来查了下,网上已经有人提出这个问题了,解释是:

foreach() is best when the number of jobs does not hugely exceed the number of processors you will be using. Or more generally, when each job takes a significant amount of time on its own (seconds or minutes, say). There is a lot of overhead in creating the threads, so you really don’t want to use it for lots of small jobs.

https://stackoverflow.com/questions/5007458/problems-using-foreach-parallelization

所以我这个例子是循环1e6次,foreach就不太合适了;上述网页中有个回答给出的解决方案如下,先将循环变少,然后在用replicate()在子循环中计算,但是这个只适合每次循环参数是一致的情况下,我这种情况似乎就不太适合了

但是呢,foreach包搭配doSNOW包可以在foreach函数中用进度条,这点蛮好用的,参考https://stackoverflow.com/questions/5423760/how-do-you-create-a-progress-bar-when-using-the-foreach-function-in-r

library(doSNOW)
cl <- makeCluster(2)
registerDoSNOW(cl)
iterations <- 100
pb <- txtProgressBar(max = iterations, style = 3)
progress <- function(n) setTxtProgressBar(pb, n)
opts <- list(progress = progress)
result <- foreach(i = 1:iterations, .combine = rbind, 
                  .options.snow = opts) %dopar%
{
    s <- summary(rnorm(1e6))[3]
    return(s)
}
close(pb)
stopCluster(cl) 

iterators包作为一个迭代器可以跟foreach搭配使用,比如上述举例求data的每行标准差,用iter函数:

foreach(d=iter(data, by = "row"), .combine = rbind) %dopar% sd(d)

从上可看出,有效的手段+加上并行计算可以明显的加快运算速度,或者再用Rcpp改写一些函数,那效果会更好,毕竟R属于高等语言,计算速度无法跟C这种比的

参考资料:
https://blog.csdn.net/sinat_26917383/article/details/52719232
https://www.cnblogs.com/litao1105/p/4605165.html
https://blog.csdn.net/sadfasdgaaaasdfa/article/details/45155103
https://blog.csdn.net/a358463121/article/details/51695054

本文出自于http://www.bioinfo-scrounger.com转载请注明出处