並列化の書き方
並列化とは、複数のCPUを使って計算を同時に行うことで、計算時間を短くするプログラムの書き方です。今回は、Rの並列化パッケージであるdoParallelを用いて、シンプルな並列計算を行う方法をシェアします。
基本的な形は以下のようになります。
library(doParallel)
cores <- detectCores()-1
cl <- makeCluster(cores)
registerDoParallel(cl)
## 100回のループの場合
result <- foreach(i=1:100) %dopar% {
## ここに並列化したい計算を入れる
}
stopCluster(cl)
では、順番に見ていきます。
doParallelパッケージの読み込み
doParallelを読み込むと、foreachやparallelといったパッケージも自動で読み込まれます。
## インストールしていない場合
# install.package("doParallel")
library(doParallel)
コア数の取得
PCのコア数を自動で取得します。
cores <- getOption("mc.cores",detectCores())
クラスターの作成
cl <- makeCluster(cores)
registerDoParallel(cl)
これでバックエンド側の設定は完了です。
foreach
次にフロント側の記述です。foreach関数でループ計算を行いますが、その際に %do% の代わりに %dopar% を記述することで、並列化を行います。
result <- foreach(i=1:100) %dopar% {
## ここに並列化したい計算を入れる
}
並列化の終了
最後に並列計算の終了を明示します。
stopCluster(cl)
計算速度の比較
では、実例を用いて実際に並列化により計算時間が短縮されるかを見てみます。
今回は、veganパッケージを使ったNMDS(非計量多次元尺度法)という解析を例とします。NMDSとは、多変量のデータを、類似度をもとに2次元などの低次元の空間に配置する方法です。計算方法は違いますが、有名なPCA(主成分分析)と同じような次元圧縮の手法です。
そこで、多数の微生物種のデータを含む仮想のマイクロバイオームデータを作成し、NMDSにより二次元に展開します。NMDSでは、stress値と呼ばれる値が小さいほど当てはまりが良いとされています。そこで、set.seed値を1~100の中からstress値が最小になるようなseed値をループ計算により探索します。このループ計算を並列化する場合としない場合とで、計算時間を比較します。
veganの読み込みと仮想データの作成
## install.package("vegan")
## veganパッケージ
library(vegan)
## 仮想の community-by-species matrix をランダムに生成
## 行が群集、列が種に相当
community_matrix=matrix(sample(1:100,25000,replace=T),nrow=50,dimnames=list(paste("community",1:50,sep=""),paste("sp",1:500,sep="")))
## 50行×500列の多次元データを作成した
## dim(community_matrix)
## [1] 50 500
## サンプルごとにリードのカウントの総数が異なるので、希釈(rarefy)することによりカウントを揃えています。
rared_matrix <- rrarefy(community_matrix, min(rowSums(community_matrix)))
並列化無し
まずは、並列化していないループ計算で、計算時間を測定します。計算時間の測定方法はいくつもありますが、ここではtictocというパッケージを使っています。
なお、foreachによるループの特徴として、forと違って出力が一つであるという特徴があります。何も指定をしないデフォルトではリスト形式で各計算結果が出力されますが、今回は行列形式の方が扱いやすいので、引数 .combine = に出力の仕方を指定しています。ここでは、結果を縦に結合していく、つまり行方向に結合していくために**'rbind'**を引数に渡します。
install.package('tictoc')
## 並列化なし
tictoc::tic()
stress.value <- foreach(i=1:100,.combine = "rbind") %do% {
set.seed(i)
nmds<-metaMDS(data.micr,k=2,trymax=50)
nmds$stress
}
grep(min(stress.value),stress.value)
tictoc::toc()
結果は
> tictoc::toc()
51.699 sec elapsed
約51秒でした。
並列化あり
今度は、並列化ありのパターンで同じように測定を行います。
## 並列化あり
tictoc::tic()
cores <- getOption("mc.cores",detectCores())
cl <- makeCluster(cores)
registerDoParallel(cl)
stress.value <- foreach(i=1:100,.packages='vegan',.combine = "rbind") %dopar% {
set.seed(i)
nmds<-metaMDS(data.micr,k=2,trymax=50)
nmds$stress
}
grep(min(stress.value),stress.value)
tictoc::toc()
ここでは、**%do%を%dopar%**に変更することで並列化を実施しています。この時、ループの中で使用するパッケージを引数「.packages=''」で指定する必要があることに注意が必要です。library(vegan)をコードしていても、上記の「.packages='vegan'」の部分がないと
## " 関数 "metaMDS" を見つけることができませんでした "
というエラーが出て、計算が進みません。
さて、並列化をしたときの計算時間は
> tictoc::toc()
13.448 sec elapsed
約13秒でした。並列化なしの場合に比べて5分の1程度に短縮しています💪
以上、簡単なコードで並列化ができることを紹介しました。
少しでも参考になれば嬉しいです。