7

我想并行化一个循环

td        <- data.frame(cbind(c(rep(1,4),2,rep(1,5)),rep(1:10,2)))
names(td) <- c("val","id")

res <- rep(NA,NROW(td))
for(i in levels(interaction(td$id))){
res[td$id==i] <- mean(td$val[td$id!=i])
}  

库(doParallel)的foreach()的帮助下,以加快计算速度。不幸的是 foreach 似乎不支持直接分配,至少

registerDoParallel(4)
res <- rep(NA,NROW(td))
foreach(i=levels(interaction(td$id))) %dopar%{
res[td$id==i] <- mean(td$val[td$id!=i])}

没有做我想要的(给出与上面的正常循环相同的结果)。有什么想法我做错了什么,或者我如何以某种方式“破解” foreach 中的.combine选项以做我想做的事?请注意,id 变量的顺序在原始数据集中并不总是相同的。任何提示将不胜感激!

4

2 回答 2

8

为了有效地并行执行这些计算,您需要使用分块,因为单独的平均计算不会花费太多时间。使用时foreach,我经常使用itertools包中的函数进行分块。在这种情况下,我使用该isplitVector函数为每个工作人员生成一个任务。结果是向量,因此只需将它们相加即可将它们组合在一起,这就是r向量必须初始化为零向量的原因。

vadd <- function(a, ...) {
  for (v in list(...))
    a <- a + v
  a
}

res <- foreach(ids=isplitVector(unique(td$id), chunks=workers),
               .combine='vadd',
               .multicombine=TRUE,
               .inorder=FALSE) %dopar% {
  r <- rep(0, NROW(td))
  for (i in ids)
    r[td$id == i] <- mean(td$val[td$id != i])
  r
}

这是将原始顺序版本置于foreach循环中的经典示例,但仅对数据的子集进行操作。由于每个工作人员只有一个结果要组合,因此后处理很少,因此运行效率很高。

为了了解它的表现,我使用以下数据集将其与顺序版本和 Rolands 的数据表版本进行了基准测试:

set.seed(107)
n <- 1000000
m <- 10000
td <- data.frame(val=rnorm(n), id=sample(m, n, replace=TRUE))

我包括这个是因为性能非常依赖数据。您甚至可以通过使用不同的随机种子获得不同的性能结果。

以下是我使用 Xeon CPU X5650 和 12 GB RAM 的 Linux 机器的一些基准测试结果:

所以对于至少一个数据集,并行执行这个计算是值得的。这不是一个完美的加速,但也不算太糟糕。为了在您自己的机器上或使用不同的数据集运行这些基准测试中的任何一个,您可以通过上面的链接从 pastebin 下载它们。

更新

在完成这些基准测试后,我对使用data.tablewithforeach获得更快的版本很感兴趣。这就是我想出的(来自 Matthew Dowle 的建议):

cmean <- function(v, mine) if (mine) mean(v) else 0
nuniq <- length(unique(td$id))
res <- foreach(grps=isplitIndices(nuniq, chunks=workers),
               .combine='vadd',
               .multicombine=TRUE,
               .inorder=FALSE,
               .packages='data.table') %dopar% {
  td[, means := cmean(td$val[-.I], .GRP %in% grps), by=id]
  td$means
}

td现在是一个data.table对象。我isplitIndicesitertools包中使用来生成与每个任务块相关联的组号向量。该cmean函数是一个包装器,mean它为不应在该任务块中计算的组返回零。由于任务结果相同,它使用与非数据表版本相同的组合功能。

这个版本有四个工人和相同的数据集,运行时间为 56.4 秒,与顺序数据表版本相比加速了 3.7 倍,比顺序 for 循环快 6.4 倍,从而成为明显的赢家。可以从此处的 pastebin 下载基准测试。

于 2013-09-13T17:04:35.913 回答
7

如果您为此使用 data.table 而不是循环的并行化,您的性能增益将提高几个数量级:

library(data.table)
DT <- data.table(td)

DT[, means := mean(DT[-.I, val]), by = id]

identical(DT$means, res)
#[1] TRUE

如果你想使用foreach,你需要将它与 a 结合起来merge

library(foreach)
res2 <- foreach(i=levels(interaction(td$id)), .combine=rbind) %do% {
  data.frame(level = i, means = mean(td$val[td$id!=i]))}

res2 <- merge(res2, td, by.x = "level", by.y = "id", sort = FALSE)

#    level    means val
# 1      1 1.111111   1
# 2      1 1.111111   1
# 3      2 1.111111   1
# 4      2 1.111111   1
# 5      3 1.111111   1
# 6      3 1.111111   1
# 7      4 1.111111   1
# 8      4 1.111111   1
# 9      5 1.000000   2
# 10     5 1.000000   2
# 11     6 1.111111   1
# 12     6 1.111111   1
# 13     7 1.111111   1
# 14     7 1.111111   1
# 15     8 1.111111   1
# 16     8 1.111111   1
# 17     9 1.111111   1
# 18     9 1.111111   1
# 19    10 1.111111   1
# 20    10 1.111111   1
于 2013-09-12T14:57:15.577 回答