在[R]中使用mclapply、foreach或其他什么东西来并行操作一个对象?

3
有没有一种方法可以并行操作R中的对象?我知道parallel中的mclapply会分叉进程并将工作区内容复制到每个进程中。我希望我的核心能够在对象上执行独立的作业,而无需拆分和组合结果。一个用例是将数据框中的所有numeric列更改为并行的factor列。另一个用例是对具有大量级别的数据框中的因子进行分箱。我尝试这样做的主要原因是1)避免耗尽内存和2)提高速度。

下面,对象b是拆分数据框a中的列然后在应用factor之后将它们组合起来的结果。相反,我想直接操作对象a。串行地,我能够通过foreach...%do%循环的副作用将a中的列转换为factor类型。在并行中,我无法将a的列转换为factor类型作为副作用,因为(据我所知),在foreach...%dopar%内部,a指的是每个生成的进程本地的对象。

R中是否有一个包可以让我做到这一点?

a <- data.frame(b=c(1,1,2,2), c=c(2,2,3,3))
str(a)

> str(a)
'data.frame':   4 obs. of  2 variables:
 $ b: num  1 1 2 2
 $ c: num  2 2 3 3

#serial
b <-
  foreach (i = iter(1:ncol(a)), .combine = data.frame) %do% {
    a[,i] <- factor(a[,i])
  }
str(a)
str(b)

> str(a)
'data.frame':   4 obs. of  2 variables:
 $ b: Factor w/ 2 levels "1","2": 1 1 2 2
 $ c: Factor w/ 2 levels "2","3": 1 1 2 2
> str(b)
'data.frame':   4 obs. of  2 variables:
 $ result.1: Factor w/ 2 levels "1","2": 1 1 2 2
 $ result.2: Factor w/ 2 levels "2","3": 1 1 2 2

#parallel
a <- data.frame(b=c(1,1,2,2), c=c(2,2,3,3))
b <-
  foreach (i = iter(1:ncol(a)), .combine = data.frame) %dopar% {
    a[,i] <- factor(a[,i])
  }
str(a)
str(b)

> str(a)
'data.frame':   4 obs. of  2 variables:
 $ b: num  1 1 2 2
 $ c: num  2 2 3 3
> str(b)
'data.frame':   4 obs. of  2 variables:
 $ result.1: Factor w/ 2 levels "1","2": 1 1 2 2
 $ result.2: Factor w/ 2 levels "2","3": 1 1 2 2
1个回答

5
首先,您需要知道 R(一般情况下)是按值调用的,因此无论您做什么,您总是会得到数据框的临时副本。这也适用于 apply 系列函数的普通版本。一旦您在函数内部更改了某些内容,对象就会首先被复制。
话虽如此,mclapply 不会将完整的工作区内容复制到子进程中。据我所知,进程共享相同的内存内容,并且只有在对其进行修改时才会复制内容。这基本上与 R 所做的相同。
如果您仍然不信任此方法,可以使用集群方法并尝试 parallel 包中的 parLapply 和相关函数。这不是基于分叉,而是基于节点集群。您可以将您的核心视为节点。在这种情况下,您必须使用 clusterExport() 显式地导出计算所需的工作区变量。我不确定这一点,但我怀疑这确实创建了一个副本。对于其他部分,parLapply 只会将它处理的元素复制到不同的集群中。因此,这与 lapply 的默认行为相当类似。
在使用数据框 a 时,您可以执行以下操作:
> require(parallel)
> cl <- makeCluster(2)
> b <- parLapply(cl,a,as.factor)
> str(as.data.frame(b))
'data.frame':   4 obs. of  2 variables:
 $ b: Factor w/ 2 levels "1","2": 1 1 2 2
 $ c: Factor w/ 2 levels "2","3": 1 1 2 2
> stopCluster(cl)       

感谢您的建议。关于复制工作区,我可能误解了http://stat.ethz.ch/R-manual/R-devel/library/parallel/doc/parallel.pdf的第2页。当使用`mclapply`在一个3GB的数据框架上运行时,每个衍生进程在UNIX的`top`中显示了约3GB的内存消耗问题。(对于像这样的代码:mclapply(chunkify(dat, chunks = mc.cores), mc.cores = mc.cores, FUN = function(x) someFunction(x)),其中chunkify返回一个由datchunks子数据框组成的列表。我认为子进程只会使用大约3GB/mc.cores的内存)。 - lockedoff
@lockedoff 好的,你应该看的那个句子是:“然而,在任何合理的操作系统中,复制将与主进程共享内存页面,直到修改为止,因此分叉非常快。” 这取决于你的操作系统而不是你的 R 版本,在我们运行的 Debian 上,我没有遇到这个问题。还要记住,操作可能导致内存中有多个数据副本。我曾经遇到情况,操作数据帧会使使用的内存膨胀到原始大小的五倍。这在 R 的后续版本中得到了改善,但也取决于你的编码方式。 - Joris Meys
昨天在Ubuntu中测试parLapply和mclapply时,我注意到总内存消耗有很大的差异 - 谢谢。我可能会在进一步调查后发布一个单独的问题。至于我的原始问题,我知道R使用按值调用,但在我的代码中,我只传递了索引给foreach,并尝试修改其作用域之外的对象a。在%do%(串行)的情况下,它可以工作(就像在for循环中一样),而在%dopar%(并行)的情况下,则失败了。我仍在寻找一种在不复制、应用、收集的情况下并行地对对象进行操作的方法。 - lockedoff
我发现在使用data.table时,结合foreach循环并按照某个键值进行分割后,直接传递数据块非常高效。我相信data.table是通过引用传递的。这非常依赖于具体的使用情况,但它帮助我解决了内存问题。 - Matt Weller

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