foreachについてまとめたい
foreachパッケージのforeach
関数についてです。
Rで並列処理を行う際に今まで使用してきましたが、引数は.combine
をいじるくらいでした。他にも%dopar%
とかよくわからないものを蔑ろにしていました。この関数は今後もよく使うことになりそうなので、頑張ってまとめてみたいと思います。
基本
基本形としてはこんな感じです。
foreach(i = 範囲) %do% { -- 処理 -- }
1~3の平方根の計算例が以下になります。返り値はデフォルトでlist
です。
> foreach(i = 1:3) %do% { sqrt(i) } [[1]] [1] 1 [[2]] [1] 1.414214 [[3]] [1] 1.732051
イテレータをここではi
としていますが、もちろんa
でもb
でもokです。さらに言えば、イテレータは2つ以上あっても大丈夫で、その場合は要素数の少ない方に合わせて終了します。
> foreach(a = 1:1000, b = rep(10, 2)) %do% { a + b } [[1]] [1] 11 [[2]] [1] 12
%do% と %dopar%
%do%
と%dopar%
の違いは並列処理をするかどうかです。
%do% : 並列処理なし
%dopar% : 並列処理あり
本エントリは並列処理ではなく、あくまでもforeach
の使い方としたいので、%do%
を用いていきます。(一部除く)
また余談ですが、並列化する/しないをうまく書き分ける話があるみたいです。
- foreachパッケージで、並列化する/しないを綺麗に書き分ける - My Life as a Mock Quant
- foreach の registerDoSEQ() について #rstatsj - Qiita
引数
.combine
これは返り値に関する引数です。正確には、返り値を生成する関数を指定する引数でしょうか。c
やrbind
などの関数を指定することでベクトルや行列で返り値を構成することができます。指定しないとlist
形式で返されます。
> foreach(i = 1:3, .combine = c) %do% { exp(i) } [1] 2.718282 7.389056 20.085537 > foreach(i = 1:3, .combine = rbind) %do% { rnorm(3) } [,1] [,2] [,3] result.1 -0.6189962 1.081181 0.8920234 result.2 -0.6117038 1.418355 0.1769341 result.3 -0.4464221 -0.357747 -0.9254935
少し応用で、以下のような用法もあります。
> foreach(i = 1:3, .combine = sum) %do% { i^2 } [1] 14
.combine
の関数はこの記事によるとReduce
関数をイメージすると良いとのことです。(Reduce
関数についてはこの記事がわかりやすかったです)
.init
この引数で指定した値が返り値の初めに入ります。その際.combine
を指定しないとエラーが出ます。若干わかりにくいかもしれません。以下に例を示します。
foreach(i = 1:5, .combine = c) %do% { i+10 } ## [1] 11 12 13 14 15 foreach(i = 1:5, .combine = c, .init = 1:2) %do% { i+10 } ## [1] 1 2 11 12 13 14 15
先ほど述べたように、そのまま最初に加わります。
rbind
など行列で返すと行(列)名が異なるようです。
foreach(i = 1:5, .combine = cbind, .init = 1:4) %do% { rnorm(4) } ## accum result.1 result.2 result.3 result.4 result.5 ## [1,] 1 -0.6002596 -1.0264209 -0.34754260 -1.6679419 0.60796432 ## [2,] 2 2.1873330 -0.7104066 -0.95161857 -0.3802265 -1.61788271 ## [3,] 3 1.5326106 0.2568837 -0.04502772 0.9189966 -0.05556197 ## [4,] 4 -0.2357004 -0.2466919 -0.78490447 -0.5753470 0.51940720
.final
これは、繰り返し処理後にかける関数を指定します。デフォルトではNULL
ですので何もかけません。単純に次にかける処理をここに指定しても良いと思います。
例えば、ベクトルで返ってきた正規乱数の和をとってみましょう。まず和をとる前が、以下です。
# 10個の正規乱数 set.seed(123) foreach(i = 1:10, .combine = c) %do% { rnorm(1) } ## [1] -0.56047565 -0.23017749 1.55870831 0.07050839 0.12928774 1.71506499 ## [7] 0.46091621 -1.26506123 -0.68685285 -0.44566197
これらの和をとります。和をとるだけなら色々な方法がありますので、複数示します。
set.seed(123) foreach(i = 1:10, .combine = c, .final = sum) %do% { rnorm(1) } ## [1] 0.7462564 set.seed(123) sum(foreach(i = 1:10, .combine = c) %do% { rnorm(1) }) ## [1] 0.7462564 set.seed(123) foreach(i = 1:10, .combine = sum) %do% { rnorm(1) } ## [1] 0.7462564
どれも正しく同じ答えが出ています。しかし、繰り返し数を増やしてみると3番目の方法がやや時間がかかるようになります。これについては.combine
のところで記述したReduce
関数をイメージすることができるとよくわかります。簡単に言えば、1番目と2番目は最後に一度関数をかけただけ、一方3番目は繰り返しのたびにsum
関数をかけているという違いがあり、その分だけ処理が増えています。
.inorder
これは返り値を正しい順番で返すかどうかの引数です。もう少し正確に言うと、正しい順番であることを保証するかどうかの引数です。デフォルトでは、順番を保証する(TRUE
)となっています。
では"正しい順番"とはどういうことでしょう。ここでの"正しい"は、イテレータが指定した順番通りに結果が返ってくることです。これが保証されるのがTRUE
の時、保証されないのがFALSE
の時です。FALSE
の時の順番を、仮に"素直な順番"とします。パラレルに計算が行われると、複数の処理が別々に行われます。しかしその処理時間はそれぞれで異なる場合があり、その処理が終わった順に並べられた時、これが"素直な順番"になります。"素直な順番"は、偶然"正しい順番"になることもあるので、そういった意味で保証されません。
詳しい説明はUsing The foreach Packageを参照下さい。
正しい順番であるかどうかが重要でない場合や、.combine='+'
とした時のように順番が影響しない場合は、この引数をFALSE
にすることで少しだけ処理が速くなるようです。以下に簡単な処理を100回ほど繰り返してsummary
で比較しました。
require(doMC) # ここは各自適切なものを registerDoMC(detectCores()) # 実行時は4コア bench1 <- bench2 <- numeric(100) for(i in 1:100) { bench1[i] <- system.time(foreach(j = 1:1000, .combine = '+', .inorder = T) %dopar% { sum(rnorm(100)) })[3] bench2[i] <- system.time(foreach(j = 1:1000, .combine = '+', .inorder = F) %dopar% { sum(rnorm(100)) })[3] } summary(bench1) # .inorder = TRUE ## Min. 1st Qu. Median Mean 3rd Qu. Max. ## 0.5300 0.5550 0.5650 0.5838 0.5920 0.7520 summary(bench2) # .inorder = FALSE ## Min. 1st Qu. Median Mean 3rd Qu. Max. ## 0.4850 0.5050 0.5170 0.5286 0.5420 0.7200
FALSE
にした方がやはり多少速くなっていることがわかります。ちなみに並列処理にしなくても(%do%
でも)少し速くなります。
.multicombine
これ、注目です。
この引数は.combine
引数で与えた関数の引数が3個以上受け取れる時にTRUE
にすることで活躍します。デフォルトでは関数の引数の数が3個以上受け取れない場合を考えているのでFALSE
です(c
,cbind
,rbind
は例外で、3個以上受け取りが可能なことがわかっているので自動でTRUE
になります)。受け取れないのにも関わらずTRUE
にしてしまうとエラーが大量発生するので注意です。
受け取り可能かどうかがわからない場合は、args
関数をその関数にかけてみるとよくわかります。
例えば以下です。
args(c) ## function (..., recursive = FALSE) ## NULL args(append) ## function (x, values, after = length(x)) ## NULL
c
関数の引数は...
であり、引数にした値全部を結合してくれます。一方でappend
関数の引数はx
とvalues
であり、x
の後ろにvalues
を結合します。つまり、同様の処理をすることを考えると、c
関数は引数を3個以上受け取ることが可能であり、append
関数は引数を2個までしか受け取ることができません。
さて、これによって何が良いかというと、適宜TRUE
にしてあげることで処理が速くなります。
以下で、100個の正規乱数の和を1000個計算し、その和をとるという処理について速度比較をしました。
bench1 <- bench2 <- numeric(100) for(i in 1:100) { bench1[i] <- system.time(foreach(j = 1:1000, .combine = sum, .multicombine = T) %do% { rnorm(100) })[3] bench2[i] <- system.time(foreach(j = 1:1000, .combine = sum, .multicombine = F) %do% { rnorm(100) })[3] } summary(bench1) # .multicombine = TRUE ## Min. 1st Qu. Median Mean 3rd Qu. Max. ## 0.3500 0.3680 0.3920 0.4388 0.4290 1.2000 summary(bench2) # .multicombine = FALSE ## Min. 1st Qu. Median Mean 3rd Qu. Max. ## 0.5230 0.5535 0.6000 0.6630 0.6665 1.7420
結構速くなりましたね。パフォーマンス向上についてはforeach の .multicombine 引数について #rstatsj - Qiitaにもありましたので、そちらも御覧ください。
.maxcombine
.combine
引数で与えた関数の引数の最大個数を指定します。.multicombine = FALSE
の時(デフォルト)では2になり、.multicombine = TRUE
の時は100になります。
.errorhandling
エラー発生時の動作を示す文字列オブジェクトを指定します。選択肢は以下になります。
'stop' : エラーが発生すると処理を停止する 'remove' : エラーが発生したイテレーションの処理結果を省く 'pass' : エラーが発生したイテレーションの処理結果をエラーオブジェクトとして返す
デフォルトではstop
です。
.packages
別のパッケージの関数を用いる場合、この引数にそのパッケージ名を文字列ベクトルとして指定します%do%
の時は省略可能です。ただ以下に示す例では外しても僕の環境では実行できるのでどう影響があるのかはわかりません。
x <- matrix(runif(500), 100) y <- gl(2, 50) require(randomForest) # %do% の場合 foreach(ntree = rep(250, 4), .combine = combine) %do% { randomForest(x, y, ntree = ntree) } ## ## Call: ## randomForest(x = x, y = y, ntree = ntree) ## Type of random forest: classification ## Number of trees: 1000 ## No. of variables tried at each split: 2 ## # %dopar% の場合 foreach(ntree = rep(250, 4), .combine = combine, .packages = 'randomForest') %dopar% { randomForest(x, y, ntree = ntree) } ## ## Call: ## randomForest(x = x, y = y, ntree = ntree) ## Type of random forest: classification ## Number of trees: 1000 ## No. of variables tried at each split: 2 ##
.export
この引数で、オブジェクト名を文字列ベクトルで指定します。ここで指定するのは、現在の環境で定義されていないオブジェクトを指定します。
この引数で、現在の環境で定義されていないオブジェクトを文字列ベクトルで指定し、使用可能にします。デフォルトはNULL
です。
foreachパッケージで並列化する時、現在の"環境"にない変数・関数は、明示的に.export引数にて指定しなければならない - My Life as a Mock Quant
この記事によれば、上の環境のオブジェクトも.export
しないといけないとありましたが、これまたどうも僕の実行環境ではexport必要なしにできました。どういうことなんでしょう。
.noexport
この引数は、上記の.export
でオブジェクトを使用可能にしてしまったせいで、同名のオブジェクトに影響が出てしまう際に用います。.export
で例えばls()
として一度にexportした場合、余計なものまでexportされてしまうので、この引数で除外するオブジェクトを文字列ベクトルで指定します。
.verbose
これをTRUE
にすることで、繰り返しの詳細を出力することができます。エラーが発生した場合に使ってみると良いでしょう。デフォルトはFALSE
です。
foreach(i = 1:2, .combine = c, .verbose = TRUE) %do% { i } ## evaluation # 1: ## $i ## [1] 1 ## ## result of evaluating expression: ## [1] 1 ## got results for task 1 ## numValues: 1, numResults: 1, stopped: FALSE ## returning status FALSE ## evaluation # 2: ## $i ## [1] 2 ## ## result of evaluating expression: ## [1] 2 ## got results for task 2 ## numValues: 2, numResults: 2, stopped: FALSE ## returning status FALSE ## numValues: 2, numResults: 2, stopped: TRUE ## first call to combine function ## evaluating call object to combine results: ## fun(result.1, result.2) ## [1] 1 2
when
ヘルプマニュアルの同ページに記載されていたのでついでに。
これは%:%
演算子と組み合わせてif
文のように利用することができます。例えば以下。
set.seed(123) foreach(a = rnorm(10), .combine = c) %do% sqrt(a) ## [1] NaN NaN 1.2484824 0.2655342 0.3595660 1.3096049 0.6789081 ## [8] NaN NaN NaN Warning messages: 1: In sqrt(a) : NaNs produced 2: In sqrt(a) : NaNs produced 3: In sqrt(a) : NaNs produced 4: In sqrt(a) : NaNs produced 5: In sqrt(a) : NaNs produced
これは10個の正規乱数について「平方根を求めベクトルで返す」という処理になっています。当然負の実数(numeric
)に対し平方根をとることはできないので、NaN
及び警告メッセージが出ています。これに対して、when
を用いた例が以下になります。
set.seed(123) foreach(a = rnorm(10), .combine = c) %:% when(a >= 0) %do% sqrt(a) ## [1] 1.2484824 0.2655342 0.3595660 1.3096049 0.6789081
when
によって、これは10個の正規乱数について「a
が0以上ならば平方根を求めベクトルで返す」という処理になっています。
条件を満たしたものがどれかがわからないので、あまり使う機会はなさそうです。
速度比較
並列処理を指定しているわけではないですが、通常のfor
文との速度比較をしてみたいと思います。雑比較です。
# for文を回します before <- proc.time() x <- numeric(10000) for(i in 1:10000) x[i] <- sum(rnorm(1000)) proc.time() - before ## user system elapsed ## 1.174 0.010 1.261 # foreach文を回します(工夫なし) before <- proc.time() x <- foreach(i = 1:10000, .combine = c) %do% { sum(rnorm(1000)) } proc.time() - before ## user system elapsed ## 6.664 0.042 6.703 # foreach文を回します(.inorder変更) before <- proc.time() x <- foreach(i = 1:10000, .combine = c, .inorder = FALSE) %do% { sum(rnorm(1000)) } proc.time() - before ## user system elapsed ## 6.447 0.047 6.489 # foreach文を回します(.multicombine変更) before <- proc.time() x <- foreach(i = 1:10000, .combine = c, .multicombine = TRUE) %do% { sum(rnorm(1000)) } proc.time() - before ## user system elapsed ## 4.936 0.036 4.879 # foreach文を回します(.inorderと.multicombine変更) before <- proc.time() x <- foreach(i = 1:10000, .combine = c, .multicombine = TRUE, .inorder = FALSE) %do% { sum(rnorm(1000)) } proc.time() - before ## user system elapsed ## 4.697 0.025 4.702
やっぱり並列処理をしないとforeach
を使う意味はなさそうですね。
工夫点として.inorder
と.multicombine
がありましたが、.inorder
は小さい効果、.multicombine
は大きい効果が見込めそうです。
参考
- foreach+doSNOWパッケージを使って、並列処理をやってみた - My Life as a Mock Quant
- Rで最小公倍数 - もうカツ丼でいいよな
- Package 'foreach'
- Using The foreach Package
- Getting Started with doMC and foreach
- Rのforeach関数を使って並列計算をしたい (その1) - 300億円欲しい
- foreachパッケージで並列化する時、現在の"環境"にない変数・関数は、明示的に.export引数にて指定しなければならない - My Life as a Mock Quant
- foreach の .multicombine 引数について #rstatsj - Qiita
- 作者: 福島真太朗
- 出版社/メーカー: ソシム
- 発売日: 2014/09/24
- メディア: 単行本
- この商品を含むブログ (2件) を見る