這裡介紹如何藉由 R 的 parallel
套件,使用多個 CPU 核心進行平行運算,提高計算速度。
現在電腦的 CPU 都有好幾個核心,在 R 中處理大計算量的問題時,如果感覺計算速度不夠快,就可以考慮將計算工作平行化,藉著 parallel
平行計算套件,將工作分散至多個 CPU 核心來計算,讓計算速度大幅提昇。
lapply
函數
由於 parallel
套件的實做與使用方式非常類似 R 內建的 lapply
這個隱式迴圈,所以在學習使用 parallel
套件之前,要先熟悉一下 lapply
用法。
假設我們有三組常態分佈的抽樣資料:
# 產生三組常態分佈的抽樣資料 x.list <- list( a = rnorm(10), b = rnorm(10), c = rnorm(10) ) x.list
$a [1] 1.89632434 -0.90004876 0.67664475 0.49483551 0.05591163 0.04862564 [7] -0.46838166 -0.36932839 -0.03425141 0.76694261 $b [1] -1.65421202 -1.39584410 -0.65933271 0.01404326 -0.67322945 -0.54062796 [7] -1.03605538 -0.68641190 -0.35319759 -1.58982312 $c [1] 1.74472367 -0.75457928 0.72840523 -0.89119458 0.52035511 -1.55435785 [7] -0.40364765 0.07631279 -0.65365062 -1.13598909
如果想要計算這三組資料的平均值與標準差,就可以使用 lapply
來處理:
# 計算平均值與標準差 lapply(x.list, function(x) c(mean(x), sd(x)))
$a [1] 0.2167274 0.7880909 $b [1] -0.8574691 0.5486911 $c [1] -0.2323622 0.9984217
在執行時,lapply
會從第一個參數所指定的列表(或是向量)中,逐一取出所有元素,放進第二個參數所指定的函數中處理,處理完之後,將所有的結果串成一個列表再傳回。
有關於更多 lapply
函數的教學,請參考 R 進階迴圈。
使用 parallel
套件
R 在 2.14.0 版之後就已經將 parallel
納入內建的套件,所以使用時只要直接載入即可:
# 載入 parallel 套件 library(parallel)
在實際進行計算之前,要先建立 cluster,指定需要使用的 CPU 核心數,若要發揮電腦的最大效能的話,可以透過 detectCores
自動偵測 CPU 的核心數,使用所有的核心:
# 取得 CPU 核心數 cpu.cores <- detectCores()
然後建立一個 cluster:
# 建立 Cluster cl <- makeCluster(cpu.cores)
在 cluster 建立之後,就可以利用它來進行平行化的計算了。
parallel
套件提供了一個 parLapply
平行計算函數,其用法跟 lapply
幾乎相同(只多了一個指定 cluster 的參數),它可將不同的工作分配至不同的節點上運算,以平行的方式加速計算,若將上面 lapply
的範例以 parLapply
改寫,就會像這樣:
# 平行化計算 parLapply(cl, x.list, function(x) c(mean(x), sd(x)))
執行之後的計算結果跟普通的 lapply
計算結果相同,只不過計算速度會比較快:
$a [1] 0.2167274 0.7880909 $b [1] -0.8574691 0.5486911 $c [1] -0.2323622 0.9984217
計算完成之後,再將 cluster 關閉:
# 關閉 Cluster stopCluster(cl)
以上就是 parallel
套件典型的使用模式。
Cluster 類型
parallel
支援的 cluster 有好多種,以下是幾種常用的類型:
PSOCK
- 預設的 cluster 類型,其以
system("Rscript")
這類的方式建立 cluster 節點,透過 parallel socket 交換資料,支援所有平台。 FORK
- 以 fork 的方式建立 cluster 節點,不同節點之間可以共用唯讀資料的記憶體,所以執行效能非常好,但是 Windows 平台不支援此種 cluster 節點。
- 其他
- 其他的類型則是靠
snow
套件來支援(例如MPI
),請參考snow
套件的說明。
一般來說若在 Windows 系統上要使用多核心的 CPU 做運算,都是使用預設的 PSOCK
,而在 Linux 系統上則可以選擇效能較好的 FORK
,若在大型的叢集電腦中使用多台電腦同時做運算的話,才會使用到 snow
所支援的 MPI
。
變數空間
在使用預設的 PSOCK
cluster 時,若想要讓所有的節點可以使用預先設定好的全域變數,必須以 clusterExport
將變數傳遞至所有節點,之後才能在各個節點中使用:
# 建立 PSOCK 的 cluster cl <- makeCluster(cpu.cores) # 建立全域變數 y <- 2 # 將變數傳遞至所有節點 clusterExport(cl, "y") # 在各節點使用全域變數 parLapply(cl, c(1, 2, 3), function(x) x^y) stopCluster(cl)
在 Linux 系統上若使用 FORK
cluster 的話,由於各個節點都是以 forking 的方式產生的,所以可直接共用目前工作空間的資料:
# 建立 FORK 的 cluster cl <- makeCluster(cpu.cores, type = "FORK") # 建立全域變數 y <- 2 # 在各節點使用全域變數 parLapply(cl, c(1, 2, 3), function(x) x^y) stopCluster(cl)
載入套件
若要在各個節點上使用特定的套件,可以直接將載入套件的 library
指令寫在 parLapply
所呼叫的函數中,或是使用 clusterEvalQ
讓所有節點預先載入套件:
cl <- makeCluster(cpu.cores) # 讓所有節點載入指定套件 clusterEvalQ(cl, library(e1071)) # 產生測試資料的索引 s <- lapply(1:3, function(i) { sample(1:nrow(iris), 50) } ) # 在各節點使用載入的套件 parLapply(cl, s, function(x) { svm(Species ~ ., data = iris[x,]) }) stopCluster(cl)
clusterEvalQ
的作用就是單純讓每個節點執行指定的指令,只不過它不能像 parLapply
一樣傳入參數而已。
亂數種子
在平行化的計算中,如何產生不會重複的亂數是一個比較複雜的問題,而 parallel
套件幫我們處理掉大部分了,在使用上唯一需要注意的大概就是亂數種子不可以使用一般 set.seed
函數,要改用 clusterSetRNGStream
:
cl <- makeCluster(cpu.cores) # 初始化 RNG Stream clusterSetRNGStream(cl, 123) # 產生亂數 clusterEvalQ(cl, runif(5)) stopCluster(cl)
mclapply
函數
在 parallel
套件中還有一個 mclapply
函數,它會自動使用多核心的 CPU 進行平行計算,不需要手動建立 cluster,使用起來更方便,不過它不支援 Windows 系統,只能在 Linux 或 Mac OS 中使用:
# 使用多核心 CPU 平行計算(不適用於 Windows 平台) mclapply(x.list, function(x) c(mean(x), sd(x)))
以上只是基本的 parallel
套件使用方式,更詳細的用法請參它的官方文件。
計算 π 值範例
以下是一個用蒙地卡羅方法計算 π 值的程式,輸入的 n
值為模擬次數,傳回的結果就是 π 的近似值:
# 估計 PI 值 est.pi <- function(n){ x <- runif(n, 0, 1) y <- runif(n, 0, 1) r <- sqrt(x^2 + y^2) sum(r < 1) / n * 4 }
我們可以使用 system.time
來測量計算時間,比較有無平行化的差異:
# 普通計算 system.time(lapply(rep(1e7, 4), est.pi)) # 平行計算 system.time({ cl <- makeCluster(cpu.cores) parLapply(cl, rep(1e7, 4), est.pi) stopCluster(cl) }) # 平行計算(不適用於 Windows 平台) system.time(mclapply(rep(1e7, 4), est.pi))
參考資料:Stranity Blog、G-FORGE、R-posts.com、Parallel Processing in R