J's blog

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

foreachでprogressbarを表示する

最近、pforeachという簡単に並列処理を行うパッケージが僕の中で話題です。とても便利です。

R で超簡単に並列処理を書けるパッケージ pforeach を作った - ほくそ笑む

ただpforeachを使って並列化できるのは良いのですが、並列処理だと普通にやってもprogressbarが表示できません。(僕が知らないだけかもしれないですが)

普通にやるとこうなります。

pb <- txtProgressBar(min = 0, max = 10, style = 3)
pforeach(i = 1:10) ({
  setTxtProgressBar(pb, i)
  sqrt(i)
})
##  [1] 1.000000 1.414214 1.732051 2.000000 2.236068 2.449490 2.645751 2.828427
##  [9] 3.000000 3.162278

ダメですね...



ということで考えました。案としては2つです。ループの前に出すか後に出すか。

案1 ループの前に出す

iteratorsパッケージの関数をいじります。iterators::icount関数に関しては、各ループの実行時にイテレータに渡す値を生成して返しています。だったら一緒にprogressbarの表示も一緒にやらせよう、といった発想です。

以下に、iterators::icount関数に少し手を加えたpb_icount関数を作成したのでこれを示します。

pb_icount <- function (count)
{
  if (missing(count))
    count <- NULL
  else if (!is.numeric(count) || length(count) != 1)
    stop("count must be a numeric value")
  i <- 0L
  pb <- txtProgressBar(min = 0, max = count, style = 3)  # 追加
  nextEl <- function() {
    if (is.null(count) || i < count) {
      setTxtProgressBar(pb, i+1)  # 追加
      (i <<- i + 1L)
    }
    else {
      cat("\n")  # 追加
      stop("StopIteration", call. = FALSE)
    }
  }
  it <- list(nextElem = nextEl)
  class(it) <- c("abstractiter", "iter")
  it
}

これを使ってもう一度並列処理をやってみます。

N <- 10000
pforeach(i = pb_icount(N)) ({
  sqrt(i)
})[1:10]
  |========================================================================| 100%
##  [1] 1.000000 1.414214 1.732051 2.000000 2.236068 2.449490 2.645751 2.828427
##  [9] 3.000000 3.162278

やりました!!
ただ100%になってからちょっと間があるのが気になる所です。もしかしてループの割当を先に行っていたりするのでしょうか。そうであればこの案はボツなんですが。


他にはiterators::iter関数はなかなか複雑で難しそうではありますが、こちらでもできればやりたいですね。
その他の関数については、iterators::icount関数と似た構造であれば同様にできるのではないでしょうか。

案2 ループの後に出す

イテレータではなく、foreach関数の引数.combineに注目してprogress barを表示させている例がありました。

How do you create a progress bar when using the "foreach()" function in R? - Stack Overflow

これをもとにして、以下にpb関数を作成しました。(08/16修正)

pb <- function(N, FUN = c) {
  pbar <- txtProgressBar(min = 2, max = N, style = 3)
  count <- 0L
  if(is.character(FUN)) {
    FUN <- get(FUN)
  }
  function(...) {
    count <<- count + length(list(...)) - 1L
    setTxtProgressBar(pbar, count)
    if(count == (N-1)) {
      cat("\n")
    }
    FUN(...)
  }
}

では検証します。.combinec関数でなくなるので、引数.multicombineも付けておきます。

N <- 10000
pforeach(i = 1:N, .combine = pb(N), .multicombine = TRUE) ({
  sqrt(i)
})[1:10]
  |=======================================================================| 100%
##  [1] 1.000000 1.414214 1.732051 2.000000 2.236068 2.449490 2.645751 2.828427
##  [9] 3.000000 3.162278

やりました!!
でも実は.multicombine=TRUEにしたことでループの回数が変化しているので、ループ回数を表すprogressbarにはならずcombine回数となっています。しかし進捗状況を表すという目的であれば十分だと思います。



速度比較

最後に速度比較です。

  • progressbarなし
  • 案1
  • 案2

を比較します。

# progressbarなし
system.time({
  N <- 10000
  pforeach(i = icount(N)) ({
    sum(rnorm(10000))
  })[1:10]
})
##   user  system elapsed 
## 27.616   1.214  11.974 
# 案1
system.time({
  N <- 10000
  pforeach(i = pb_icount(N)) ({
    sum(rnorm(10000))
  })[1:10]
})
  |========================================================================| 100%
##   user  system elapsed 
## 16.797   0.945  12.455 
# 案2
system.time({
  N <- 10000
  pforeach(i = icount(N), .combine = pb(N)) ({
    sum(rnorm(10000))
  })[1:10]
})
  |========================================================================| 100%
##   user  system elapsed 
## 24.370   1.107  12.799



まとめ

案1(ループの前に出す)

  • あらかじめ関数を作成する必要がある
  • まだicount関数しか対応できていない
  • 100%になってから完了まで少し間がある

案2(ループの後に出す)

  • 引数にループ回数が必要
  • 任意の関数に適用できる
  • progressbarの進行速度が適切そう(これについては実行して確認してみてください)
  • .multicombine=TRUEにすると(.maxcombineを変化させると)progressbarの回数に影響する

.multicombineでcombine回数が変化することで、案2ではループ回数を表すprogressbarにはなりませんが、進捗状況を表すという意味では案2は十分に良い策だと思います。

きちんとループ回数にしたいという場合は案1を使っていくしかないかもしれないです。