Rでの並列プログラミングの簡単な紹介

Rでの並列コンピューティングの用途と利点について話しましょう。



あなたがそれを考える必要がある理由:コンピューターをより多く動作させる(多くの計算を同時に実行する)ことで、実験結果を待つ時間が短くなり、より多くのことができるようになります。 これはデータ分析にとって特に重要です(プラットフォームとしてのRは通常、この目的のために正確に使用されます)。



通常、コンピューターの動作を向上させるには、まずライブラリのアナリスト、プログラマー、または作成者が並列化に便利な形式で計算を整理するために一生懸命働く必要があります。 最良の場合、誰かがすでにあなたのためにこれを行っています:



並列化のために準備されたタスクに加えて、それをサポートする機器が必要です。 例:





明らかに、 Rでの並列計算は広範で高度に専門化されたトピックです。 この魔法-計算を高速化する方法-をすぐに学べるとは思えないかもしれません。



この記事では、R言語の基本機能を使用して計算を高速化する方法を示します。



まず、並列化可能なタスクが必要です。 この種の最も明白なタスクには、反復アクションが含まれます(直感的な用語は「自然に並列」です)。





単純な繰り返しの多いタスクがすでにあると仮定します。 注:この概念は常に簡単に達成できるとは限りませんが、プロセスを開始するにはそのような手順が必要です。



以下に、例として使用するタスクを示します。予測モデルを小さなデータセットに適用します。 データセットといくつかの定義をワークスペースにアップロードします。



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)
      
      





上記のパターンは簡潔でうまく機能しています。 留意すべきいくつかの注意事項:





これは検討する価値があります。 ただし、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マシンを実行する方法についての詳細な説明は、 ここにあります



All Articles