foreachパッケージで並列化する時、現在の"環境"にない変数・関数は、明示的に.export引数にて指定しなければならない

foreachパッケージについて

遅い遅いと巷で噂のR君も並列化すればそれなりにパフォーマンスが期待できるわけです。Rにおいてどうやって並列化をするのかというと、foreachパッケージ&関数で並列化するのが手っ取り早くて、これはざっくりでいうと

foreach(i in 1:3) %dopar%{
    #ここに処理
}

と書くだけで処理の並列化をすることが可能です。ここで%dopar%と書いている箇所を%do%に直せば並列化させないとして処理されるのも良いところだ。この裏側の仕組みとしてはforeachパッケージが各種並列化パッケージであるsnow/multicore/parallel/Rmpiへのフロントエンドとして機能していて、裏側ではそれらのパッケージに処理をブン投げる形になっているそうだ。詳しくは

を読むとよい。上述の書籍を読むに、このforeachパッケージとparallelパッケージを組み合わせた並列化がこれからのデファクトスタンダードになりそうなんで、ここではparallel+foreachパッケージでやってみる。というわけでパッケージのロードをしておこう。

library(foreach)
library(doParallel)

さらに並列化の準備として

registerDoParallel(detectCores())

も実行しておく。これはdetectCores関数で現在のマシンのコア数を取得し、それらを並列環境としてregisterDoParallel関数で登録しますよということだ。

俺がやりたかったことと、ハマったこと

適当な乱数生成をする関数を以下のように2つ用意する。f1のほうが並列化した版で、f2の方が並列化していない版に相当する。

f1 <- function()
{
  foreach(i=1:3) %dopar% {rnorm(i)}
}
f2 <- function()
{
  foreach(i=1:3) %do% {g(i)}
}

これを実行すると、正しく求めたい結果が返ってきて

> f1()
[[1]]
[1] 0.4182321

[[2]]
[1] 0.8150335 1.2575948

[[3]]
[1]  0.5326094 -1.3211135  1.7029272

> f2()
[[1]]
[1] -0.7583267

[[2]]
[1]  0.5325022 -0.4120185

[[3]]
[1]  1.58705166 -0.58542707  0.03252658

となる。一方、実際に並列化させたい処理なんかは、自作の関数を用いることも多々あるわけで、ここではその自作関数の例として関数gを定義して、さきほどと同じことをやろうとする・・・と・・・

g <- function(i){rnorm(i)}
f3 <- function()
{
  foreach(i=1:3) %dopar% {g(i)}
}

↓実行すると・・・

> f3()
Error in { : 
  task 1 failed - " 関数 "g" を見つけることができませんでした "
Called from: foreach(i = 1:3) %dopar% {
    g(i)
}

となって怒られる。これはどうも処理を並列化する際に、並列化するスレッドに対して適切な環境*1が渡っていないからのように見える。

解決策

この問題を解決するには明示的にgを並列計算する側の環境上にもexport(輸出)しますよーというのを、その名の通りの.export引数で明示すればいい。

f4 <- function()
{
  foreach(i=1:3, .export="g") %dopar% {g(i)}
}

こうすることで、ちゃんと結果を返すようになる。

> f4()
[[1]]
[1] -0.07732738

[[2]]
[1] -0.6724723  0.5245355

[[3]]
[1]  0.2095755 -0.2227492 -0.4902599

として実行することができる。もうちょっといろいろいじってみたところ、どうもforeachパッケージを用いると

  • foreachに実際に処理を移譲するコードを描いた環境の情報(ここだと関数f4内部で定義されている変数・関数のみ)

しか並列計算させる環境の上には持っていってくれないということだ。同様のことはパッケージについても言えて、自作の並列化処理において、例えばXXX, YYYパッケージを用いているならば、foreach関数の引数として

foreach(i=1:10, .packages=c('XXX', 'YYY'))

というようにそれらを「明示的に使いますよ」という記述が必要になる。

もうちょっと楽に

今は関数g一個を並列化させる環境上にexportさせればよかったが、もっと多数の変数や関数をexportする場合に、いちいちこれを全部書きあげるってのはとても面倒だ。なので、こういう面倒なことをするのが億劫な人、特に俺は

ls(envir=parent.frame())

を.exportの引数に入れておくことにしたい。こうすると親の環境(メインの環境だと思われるところ)の情報(関数とか変数)を全部持って行ってくれるので、楽。

f4 <- function()
{
  foreach(i=1:3, .export=ls(envir=parent.frame())) %dopar% {g(i)}
}

これでもちゃんと動作することは確認できて

> f4()
[[1]]
[1] -0.04673488

[[2]]
[1] -0.3213969 -1.3645549

[[3]]
[1] -1.9164478  0.3883101  2.2534236

となる。実際にls(envir=parent.frame())がどんな出力を返しているのかは上でいう関数f4の中でprint(ls(envir=parent.frame()))というように出力させてみるといい。出力文字列の中にgという文字がみつかるはずだ。




・・・みたいな話も上述の並列化本に書いてあった・・・ちゃんと読んでから並列化すればよかったorz

*1:自分で作ったRの変数や関数の集合というか、定義したもろもろのものと考えればOK