あなたがそれを考える必要がある理由:コンピューターをより多く動作させる(多くの計算を同時に実行する)ことで、実験結果を待つ時間が短くなり、より多くのことができるようになります。 これはデータ分析にとって特に重要です(プラットフォームとしてのRは通常、この目的のために正確に使用されます)。
通常、コンピューターの動作を向上させるには、まずライブラリのアナリスト、プログラマー、または作成者が並列化に便利な形式で計算を整理するために一生懸命働く必要があります。 最良の場合、誰かがすでにあなたのためにこれを行っています:
- マルチスレッドBLAS / LAPACKなどの優れた並列ライブラリは、Revolution R Open(RRO、現在はMicrosoft R Open )に含まれています ( こちらを参照)。
- RevoScaleRのrxメソッドやh2o.aiのh2oメソッドなど、重要なプロシージャの独自の高性能実装を提供する専用の並列拡張機能。
- Thrust / Rthなどの抽象並列化フレームワーク。
- 並列化に関連するRアプリケーションライブラリの使用(特に、 gbm 、 boot、およびvtreat )。 (これらのライブラリの一部は、並列実行用の環境が指定されるまで、並列操作を使用しません。)
並列化のために準備されたタスクに加えて、それをサポートする機器が必要です。 例:
- 自分のコンピューター。 通常、ラップトップでも4つ以上のコアがあります。 アルゴリズムを4倍高速で実行することの潜在的な利点は非常に大きいです。
- グラフィックプロセッサ(GPU)。 多くの車には、1つ以上の強力なグラフィックカードが搭載されています。 一部のコンピューティングタスクでは、これらのプロセッサは、通常計算に使用される中央処理装置(CPU)( 詳細 )よりも10〜100倍高速です。
- コンピュータークラスター(例:Amazon ec2、 Hadoopサーバーなど)。
明らかに、 Rでの並列計算は広範で高度に専門化されたトピックです。 この魔法-計算を高速化する方法-をすぐに学べるとは思えないかもしれません。
この記事では、R言語の基本機能を使用して計算を高速化する方法を示します。
まず、並列化可能なタスクが必要です。 この種の最も明白なタスクには、反復アクションが含まれます(直感的な用語は「自然に並列」です)。
- モデルを再適用することにより、モデルパラメータを近似します( キャレットパッケージで行われます)。
- 多数の異なる変数に変換を適用する( vtreatパッケージで行われます)。
- 相互検証、ブートストラップまたはその他の繰り返しサンプリング手法によるモデル品質評価 。
単純な繰り返しの多いタスクがすでにあると仮定します。 注:この概念は常に簡単に達成できるとは限りませんが、プロセスを開始するにはそのような手順が必要です。
以下に、例として使用するタスクを示します。予測モデルを小さなデータセットに適用します。 データセットといくつかの定義をワークスペースにアップロードします。
d <- iris # "d" R vars <- c('Sepal.Length','Sepal.Width','Petal.Length') yName <- 'Species' yLevels <- sort(unique(as.character(d[[yName]]))) print(yLevels)
## [1] "setosa" "versicolor" "virginica"
(「 ## 」で始まる行は、前のRコマンドの結果の出力であるという規則を使用します。)
小さなモデリングの問題に直面しました。予測しようとしている変数には3つのレベルがあります。 使用するモデリング手法(
glm(family='binomial')
)は、「 多項式結果 」を予測できません(このために設計されたライブラリはありますが)。 「 残りに対して1つ 」戦略を使用してこの問題にアプローチし、一連の分類子を作成することにしました。それぞれが1つのターゲット変数を残りから分離します。 このタスクは、並列化の明らかな候補です。 読みやすくするために、1つの出力モデルの構築を関数でラップしましょう。
fitOneTargetModel <- function(yName,yLevel,vars,data) { formula <- paste('(',yName,'=="',yLevel,'") ~ ', paste(vars,collapse=' + '),sep='') glm(as.formula(formula),family=binomial,data=data) }
次に、すべてのモデルを構築する通常の「シリアル」方法は次のようになります。
for(yLevel in yLevels) { print("*****") print(yLevel) print(fitOneTargetModel(yName,yLevel,vars,d)) }
または、1つの変数を持つ関数でプロシージャをラップし(このパターンはカリー化と呼ばれます )、エレガントなR表記
lapply()
を適用できます。
worker <- function(yLevel) { fitOneTargetModel(yName,yLevel,vars,d) } models <- lapply(yLevels,worker) names(models) <- yLevels print(models)
lapply()
表記法の利点は、各計算の独立性を強調することです。計算を並列化するために必要な分離の種類です。 forループは、不必要な順序または操作のシーケンスを指定することにより、計算を非常に正確に定義するという意味で考えてください。
計算の再編成により、並列ライブラリの使用と計算の並列実装が機能的に準備されました。 最初に、並列クラスターをデプロイします。
# parallelCluster <- parallel::makeCluster(parallel::detectCores()) print(parallelCluster)
## socket cluster with 4 nodes on host 'localhost'
「ソケットクラスター」を作成したことに注意してください。 ソケットクラスターは、最初の近似として驚くほど柔軟な「並列分散」クラスターです。 ソケットクラスターは、動作が比較的遅いという意味で大まかな概算です(作業は「不正確に」分散されます)が、実装は非常に柔軟です。たとえば、1つのマシン上の多くのコア、同じネットワーク上の複数のマシン上の多くのコア、他のシステムの上などMPIクラスター(メッセージパッシングインターフェイス)。
この時点で、以下のコードが機能すると想定しています(
tryCatch
の詳細は
tryCatch
)。
tryCatch( models <- parallel::parLapply(parallelCluster, yLevels,worker), error = function(e) print(e) )
## <simpleError in checkForRemoteErrors(val): ## 3 nodes produced errors; first error: ## could not find function "fitOneTargetModel">
結果の代わりに、エラー「
could not find function "fitOneTargetModel">.
"が表示されました
could not find function "fitOneTargetModel">.
問題:ソケットクラスターでは、引数
parallel::parLapply
通信ソケットによって各処理ノードにコピーされます。 ただし、現在の環境 (この場合、いわゆる「グローバル環境」)の整合性はコピーされません(値のみが返されます)。 したがって、並列ノードに転送する場合、
worker()
関数は異なるクロージャーを持つ必要があり(実行環境を示すことができないため)、新しいクロージャーには
yName
、
vars
、
d
および
fitOneTargetModel
の必要な値への参照が含まれなくなります。 これは悲しいですが、理にかなっています。 Rはすべての環境を使用してクロージャーの概念を実装します。Rは、この関数が実際に必要とする特定の環境の値を知ることができません。
だから私たちは何が間違っているかを知っています。 修正方法 そこで必要な値を転送するために、グローバル以外の環境を使用してこれを修正します。 これを行う最も簡単な方法は、独自のクロージャーを使用することです。 これを実現するために、プロセス全体を関数にラップします(そして、制御された環境で実行します)。 以下のコードが機能します:
# , mkWorker <- function(yName,vars,d) { # , # force(yName) force(vars) force(d) # , # worker fitOneTargetModel <- function(yName,yLevel,vars,data) { formula <- paste('(',yName,'=="',yLevel,'") ~ ', paste(vars,collapse=' + '),sep='') glm(as.formula(formula),family=binomial,data=data) } # : worker. # "" worker # ( ) - # / mkWorker, # , . # # ( # ). worker <- function(yLevel) { fitOneTargetModel(yName,yLevel,vars,d) } return(worker) } models <- parallel::parLapply(parallelCluster,yLevels, mkWorker(yName,vars,d)) names(models) <- yLevels print(models)
上記のコードは、必要な値を新しいランタイム環境に移動し、この環境で直接使用する関数を定義したために機能します。 明らかに、必要なときに各関数を再定義するのは面倒でコストがかかります(ただし、他の値で行われたように、ラッパーに渡すこともできます)。 より柔軟なパターンは次のとおりです。ヘルパー関数「
bindToEnv
」を使用していくつかの作業を行います。
bindToEnv
コードは次のようになります。
source('bindToEnv.R') # : http://winvector.github.io/Parallel/bindToEnv.R # , mkWorker <- function() { bindToEnv(objNames=c('yName','vars','d','fitOneTargetModel')) function(yLevel) { fitOneTargetModel(yName,yLevel,vars,d) } } models <- parallel::parLapply(parallelCluster,yLevels, mkWorker()) names(models) <- yLevels print(models)
上記のパターンは簡潔でうまく機能しています。 留意すべきいくつかの注意事項:
- 並行ワーカーはそれぞれリモート環境であることを忘れないでください。 必要なライブラリが各リモートマシンで定義されていることを確認します。
- ソース環境にロードされた非コアライブラリは、必ずしもリモートのライブラリにロードされるとは限りません。 ライブラリから関数を呼び出すときに、
stats::glm()
などのパッケージで表記法を使用することは理にかなっています(各リモートノードでlibrary(...)
を呼び出すことは冗長です)。 -
bindToEnv
関数自体は、渡された関数の環境を直接変更します(転送する値にアクセスできるように)。 これは、カリー化が適用された環境で追加の問題を引き起こす可能性があります。 ここでは、この問題を回避する方法を見つけることができます。
これは検討する価値があります。 ただし、8行のラッピング/ステレオタイピングコードを追加することで、4つ以上の高速化に十分に値すると判断したと思います。
また、完了時に、クラスターへのリンクを削除することを忘れないでください。
# if(!is.null(parallelCluster)) { parallel::stopCluster(parallelCluster) parallelCluster <- c() }
これで終わります。 次の記事では、複数のマシンとAmazon ec2でソケットクラスターを構築する方法について説明します。
bindToEnv
関数自体は非常に簡単です。
#' bindTargetEnv. #' #' http://winvector.github.io/Parallel/PExample.html - . #' #' #' , #' ( ). #' , -worker #' ( , ). #' #' @param bindTargetEnv - , #' @param objNames - , #' @param doNotRebind - , bindToEnv <- function(bindTargetEnv=parent.frame(),objNames,doNotRebind=c()) { # # for(var in objNames) { val <- get(var,envir=parent.frame()) if(is.function(val) && (!(var %in% doNotRebind))) { # () environment(val) <- bindTargetEnv } # , assign(var,val,envir=bindTargetEnv) } }
こちらからダウンロードすることもできます。
この方法で並列化を使用する場合の欠点の1つは、常に別の関数またはこれが必要になる場合があることです。 これを回避する1つの方法は、R
ls()
を使用して渡す名前のリストを作成することです。 関数と重要なグローバル変数を含むソースファイルの直後に
ls()
結果を保存すると特に効率的です。 ここに戦略がなければ、リストにアイテムを追加するのは大変です。
大規模:ec2で複数のR-machineマシンを実行する方法についての詳細な説明は、 ここにあります 。