J's blog

趣味で統計•データ解析をしています

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%を用いていきます。(一部除く)

また余談ですが、並列化する/しないをうまく書き分ける話があるみたいです。



引数

.combine

これは返り値に関する引数です。正確には、返り値を生成する関数を指定する引数でしょうか。crbindなどの関数を指定することでベクトルや行列で返り値を構成することができます。指定しないと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関数の引数はxvaluesであり、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は大きい効果が見込めそうです。


参考