須通り
Sudo Masaaki official site
For the reinstatement of
population ecology.

ホーム | 統計 Top | R におけるプログレスバーの扱い:progressr とその周辺

この記事は Tidyverseによるデータフレーム加工(07)グループごとに一括処理する:その 4 マルチコアによる真の並列化のための furrr::future_map 系関数からのスピンオフである。大規模処理を行う場合、現在の処理が何% 進んだかを知ることは重要である。しかし非同期プログラミングにおいて、特に処理が複数スレッドに分散して進行しているときには、プログレスバーを出すために比較的トリッキーな実装が要求されることがある。

目次

  1. progressr を用いたプログレスバーの表示
  2. furrr におけるプログレスバーの表示

progressr を用いたプログレスバーの表示

さて、future の作者である Henrik Bengtsson は progressr というパッケージも開発している。これは R 環境上で様々なタスクにプログレスバーを表示させる道具立てを用意するものだ。もちろん future ベースの並列処理にも対応しているので、vignette であるprogressr: An Introduction を拾い読みしておこう。

まず、並列化されていない簡単な動作サンプルが vignette に記述されているので紹介しておく。


library(progressr)

slow_sum <- function(x) {
    p <- progressr::progressor(along = x) # vignette に記載されているコード
#    p <- progressr::progressor(along = seq_along(x)) # 実は要素長さえ x に一致すれば良い
    sum <- 0
    for (i in seq_along(x)) { # seq_along(x) は 1:length(x) のモダンな書き方
        Sys.sleep(0.5)
        sum <- sum + x[i]
#        p(message = sprintf("Added %g", x[i])) # vignette に記載されているコード
        p() # メッセージは必ずしも必要ないっす
        print(i) # 挙動調査のため、元の関数にない print を足した。
    }
    sum
}
# slow_sum() の定義自体には、プログレスバーの出し方が指定されていないことに注意。
# ユーザーに任せるのがグッドプラクティスとされている。

# で、実際にプログレスバーを出したければ処理を with_progress() で囲む。
with_progress(slow_sum(1:10))

# 入力 x がカウントダウンになる場合も、プログレスバーはインクリメントされる。
with_progress(slow_sum(10:1))

コード例から分かる、progressr による進捗管理の枠組みは以下である。

  1. 進捗を管理したい処理を、関数として書き下す。この処理は入力としてベクトル x を持つので、length(x) の数だけ、進捗を段階分けすることにしたい。
  2. progressr パッケージの progressor() という関数を用いて、上記の処理関数の中または外に、progressor function(ここでは p() という名前)を定義する。Progressor function は、進捗の更新タイミングでシグナルを発出する役割を担う。
  3. Progressor function の定義時、progressor() に along 引数 として、長さが length(x) に一致するようなベクトルを指定する。簡単なのは x そのものを指定することだが、実は長さが一致さえすればいい。
  4. 定義した progressor function を、繰り返し処理の中で呼び出す。追加メッセージを出すことも可能だが、引数が無くても普通に動く。
  5. Progressor function が呼び出される都度、この 1 / length(along) のステップ数だけ進捗のパーセンテージが刻まれ、シグナルが発出される。
  6. 処理関数を囲む with_progress() がシグナルを受け取り、プログレスバーが更新表示される。なお上のコード例では with_progress() には処理関数 slow_sum() だけが記載されており、追加オプションはない。この場合の標準挙動は、 progressr::handlers() という関数を slow_sum の環境内で呼び出して、見つかった progressor function に応じた更新を行う。何も見つからなければ、プログレスバーを出さずに処理だけ行う。

どんなタイプのプログレスバーを表示させるかは、ユーザーが逐一設定する必要がある。上のコードには指定がないので R のデフォルトである utils::txtProgressBar() が使われる。この表示方法をカスタマイズするのが handlers() 関数である。


# progressr::handlers() 関数は、進捗の表示方法を構成する。
handlers(...,
    append = FALSE,
    on_missing = c("error", "warning", "ignore"),
    default = handler_txtprogressbar,
    global = NULL)

# 1 つないし複数の handler を列挙する形で、処理前に handler を指定しておく。
handlers("txtprogressbar", "beepr")

# その後、プログレスバーを出したい処理を実行する。
with_progress(slow_sum(1:10))

handlers("ascii_alert")     ASCII 制御文字(BEL character: \007 だが変更可能)を出す。
handlers("beepr")           要 install.packages("beepr") 音がなる
handlers("debug")           実行中ではなく実行後に、経過時間等のデバッグ情報が出る。
handlers("filesize")        handler_filesize(file = "") で指定したファイルのサイズを表示
handlers("pbcol")           要 install.packages("crayon") ANSI Background Color in the Terminal
handlers("pbmcapply")       要 install.packages("pbmcapply") rpbmcapply パッケージの progressBar() を使用
handlers("progress")        要 install.packages("progress") progress パッケージの progress_bar() を使用
handlers("tkprogressbar")   要 install.packages("tcltk") および capabilities("tcltk") GUI で窓を出す。
handlers("txtprogressbar")  (デフォルト)utils::txtProgressBar() を呼び出す。
handlers("void")            何も出さない
handlers("winprogressbar")  (Windows のみ動作)Windows 標準の GUI でプログレスバーを出す。

通常の Windows GUI プログラムの挙動に相当するのは handlers("winprogressbar")。筆者のおすすめはコンソール上で完結して、情報量がそこそこ多い handlers("pbmcapply") である。pbcol は手元だとよく分からなかった。

furrr におけるプログレスバーの表示

さて、処理内で progressor function を定義しておけば、並列処理でも progressr は使用可能である。まず、Vignette にある方法。


# ローカルマシン上の multisession による並列処理にプログレスバーを組み込む。
library(furrr)
library(progressr)

plan(multisession)

handlers(global = TRUE) # R >= 4.0.0
handlers("pbmcapply")

my_fnc <- function(xs) {
    p <- progressor(along = xs)
    y <- future_map(xs, function(x) {
        Sys.sleep(6.0-x)
        p(sprintf("x=%g", x)) # 上で定義した p() を future_map() 内で実行。
        sqrt(x)
    })
}

my_fnc(1:5)
# / [================>-----------------------------]  40% x=2

R 3.x までだと、handlers(global = TRUE) は以下のように叱られる
警告メッセージ: 
register_global_progression_handler(action = action) で: 
 register_global_progression_handler() requires R (>= 4.0.0)

なお、furrr 自体にも .progress = TRUE という引数があるが、Bengtsson によれば一部種類の future バックエンドでしか有効に作動しないとのことで、代わりに progressr を使うことを推奨している。

上記のコードは、R 3.x 以下では handlers(global = TRUE) が効かないため、実行しても特にプログレスバーは出ない。わからないまま終わる、そんなのは嫌なので、古い R でも効果が分かるように書き換えたバージョンも置いておく。


# 古い R でも with_progress() で囲めば、並列処理でもプログレスバーが出せる。
my_fnc <- function(xs) {
    p <- progressr::progressor(along = xs)
    y <- future_map(xs, function(x) {
        Sys.sleep(6.0-x)
        p()
        print(Sys.getpid()) # 各処理を行った Rscript の ID。処理後に表示される。
        sqrt(x)
    })
}

handlers("pbmcapply")
res <- with_progress(my_fnc(1:5))

ただし、この並列処理のパターンは「Tidyverseによるデータフレーム加工(07)」で書いた実用的なコードとは、異なることに注意されたい。あちらでは「ベクトル化されていない関数 sim_dummy() を外部で furrr::future_map() で受けることでベクトル化する」というアプローチを取ったが、上のコードにある my_fnc() は、それ自身が future_map() でベクトル化されていて、内部に p() の呼び出し部がある。

つまり my_fnc() の実行時には、並列処理が n 領域中 1 領域分完了するごとに p() から発出された 1/n の進捗更新のシグナルを、future_map() が透過してグローバル環境へ届け、それを外側の with_progress() 関数が受信して、プログレスバーを更新するというメカニズムが成り立っている。

この連鎖構造を壊さないようにして非ベクトル関数を並列化&プログレスバー表示するには、以下のようなへんてこなコードを書く必要が出てくるだろう。


my_fnc2 <- function(x) {
    Sys.sleep(1)
    sqrt(x)
}

# すでに my_fnc2() が外部で定義されていて、無理やり並列化&プログレスバーを表示

library(furrr)
library(progressr)
plan(multisession)
handlers("pbmcapply")

res <- with_progress({
    p <- progressr::progressor(along = 1:30)
    future_map(.x=1:30, .f=function(.) {
        p()
        my_fnc2(.)
    })
})

# 以下のコードは、プログレスバーは表示されるし書き換えもされるが、進捗が 0% のまま完了する。

my_fnc2 <- function(x) {
    Sys.sleep(5)
    sqrt(x)
}
res <- with_progress(future_map(.x=1:30, .f=function(.) {
    p <- progressr::progressor(along = .)
    p()
    my_fnc2(.)
}))

何が重要かというと、progressor function を定義するときの along 引数に与えるベクトルの要素長が、n になっていないと進捗の値が正しく出ないのだ。上の(正しい)コードと下の(間違った)コードの差異は、上では future_map() 内部で長さ 30 のベクトルを along に受け取る操作が 1 回入る(よって p() を実行するたびに 0/30, 1/31, 2/30, ..., 30/30 が順に出力される)のに対し、下のコードでは future_map() の中で progressor function が定義されている。つまり下のコードは、0/30 のままのプログレスバーが 30 回生成されて終わるだけである。

内部構造

なぜ上記のコードがリアルタイムで進捗を更新表示できるのか、疑問に思った方も居るのではなかろうか。こちらのプレプリントによれば、p() から発出されるシグナルは immediateCondition という特殊なクラスを持つ(必ずしも全種類のバックエンドで使えるわけではないが)。Future において、バックグラウンドで走っている処理からメッセージがリレーされ、親の表示が更新されるのは、基本的に value() をこちらから打ったときだけ。しかし immediateCondition は公儀の継飛脚なみに偉いので、他の処理を押しのけてでも即座に親へリレーされる。