霞と側杖を食らう

ほしいものです。なにかいただけるとしあわせです。[https://www.amazon.jp/hz/wishlist/ls/2EIEFTV4IKSIJ?ref_=wl_share]

バドミントン戦略シミュレーションとの衝動遊戯:第3ゲーム

 

はじめに

「バドミントン戦略シミュレーションとの衝動遊戯:第2ゲーム」

moratoriamuo.hatenablog.com

で, 高速化を図ってRcppによる書き換えを試みたが上手くいかなかったことを書いたところ, ill-identifiedさんからコメントをいただき, コメントにしたがってRcppの書き換えたところ, 上手くいった. 以下ではRcppの書き換えと, それによる速度比較について書く.

Rcppによる書き換え

書き換えたコードが以下のものである.

#include <Rcpp.h>

using namespace Rcpp;

// [[Rcpp::export]]

NumericMatrix frallycpp(Function f1, Function f0, int ngame, double reffect){
  NumericMatrix mresult(6,0);
  NumericVector vresult(6);
  
  for(int game = 0; game < ngame; ++game){
    NumericVector vcrally(0);
    int p1point = 0;
    int p0point = 0;
    int player = 1;
    
    while(p1point < 21 && p0point < 21){
      int judge = 1;
      int shot = 0;
      int crally = 0;
      
      while(judge==1){
        double effect = (shot==0)? 0: reffect;
        shot = (player==1)? as<int>(f1(shot)): as<int>(f0(shot));
        double prob = (shot==0)? 0.9-effect: 0.7-effect;
        judge = (R::runif(0, 1) < prob)? 1: 0;
        player = (player==1)? 0: 1;
        crally = crally + 1;
      }
      vcrally.insert(vcrally.end(),crally);
      
      if(player==1){
        p1point = p1point + 1;
      }else{p0point = p0point + 1;}
    }
    vresult[0] = mean(vcrally);
    vresult[1] = var(vcrally);
    vresult[2] = max(vcrally);
    vresult[3] = p1point;
    vresult[4] = p0point;
    vresult[5] = (p1point>p0point)? 1: 0;
    
    mresult = cbind(mresult,vresult);
  }
  mresult = transpose(mresult);
  return mresult;
}  

前回詰まっていた2点のうち、一点は型の間違いによるもの、もう一点はRcppのrunif()とRのrunif()で返り値の型が違うことによるものだった. これらを修正し, 対戦する戦略関数とシミュレーション回数とrisky shotの効果を引数にとって、試合結果の行列を返す関数を作成した.

Rの関数についても同様に書き換えた.

frallyR <- function(fstr1,fstr0,ngame,reffect){
  #### vresultをrbindするmatrixの作成 ####
  mresult <- NULL
  vresult <- NULL
  
  #シミュレーション
  for(i in 1:ngame){
    #### game result 変数の初期値 ####
    vcrally <- c()    #rally count vector
    p1point <- 0    #player1のポイント
    p0point <- 0    #player0のポイント
    player <- 1    #first serverはplayer1
    
    #### rallyの定義とgameの定義 ####
    #rallyをjudge==0になるまでのwhileループとする.
    #gameをどちらかのplayerのpointが21になるまでのwhileループとする.
    
    while(p1point < 21 & p0point < 21){
      #ラリー前に初期化する
      judge <- 1    #shotの成功失敗の判定(1=>成功,0=>失敗)
      shot <- 0    #ラリー最初のサーブは成功率へのeffectは0なので相手のshotはsafetyとみなす.
      crally <- 0    #rally count
      
      
      while(judge==1){
        
        #手番のplayerの戦略関数をfstrに代入
        fstr <- ifelse(player==1,fstr1,fstr0)
        
        #相手のshotからeffectを決定
        effect <- ifelse(shot==0, 0, reffect)
        
        #自分のshotの決定  
        shot <- fstr(shot)
        prob <- ifelse(shot==0, 0.9-effect, 0.7-effect)
        
        #shotの成功判定
        judge <- ifelse(runif(1)<prob, 1, 0)
        
        #手番playerの交代
        player <- ifelse(player==1, 0, 1)
        
        #rally count追加
        crally <- crally + 1
      }
      
      #ラリーカウントベクトルの更新
      vcrally <- c(vcrally, crally)
      #ポイントの更新
      if(player==1){
        p1point <- p1point + 1 
      }else{p0point <- p0point + 1}
      
    }
    
    #試合結果(ラリー数平均,ラリー数分散,最大ラリー数p1point,p0point,gamewinplayer)
    vresult <- c(mean(vcrally),var(vcrally),max(vcrally),p1point,p0point,ifelse(p1point>p0point,1,0))
    mresult <- rbind(mresult,vresult)
  }
  return(mresult)
}

さらに, 速度比較の対象として並列計算するケースを用意するために, シミュレーションを繰り返さずに結果を返す関数も作成しておく.

fonerallyR <- function(fstr1,fstr0,reffect){

  vresult <- NULL
  
    #### game result 変数の初期値 ####
    vcrally <- c()    #rally count vector
    p1point <- 0    #player1のポイント
    p0point <- 0    #player0のポイント
    player <- 1    #first serverはplayer1
    
    
    #### rallyの定義とgameの定義 ####
    #rallyをjudge==0になるまでのwhileループとする.
    #gameをどちらかのplayerのpointが21になるまでのwhileループとする.
    
    while(p1point < 21 & p0point < 21){
      #ラリー前に初期化する
      judge <- 1    #shotの成功失敗の判定(1=>成功,0=>失敗)
      shot <- 0    #ラリー最初のサーブは成功率へのeffectは0なので相手のshotはsafetyとみなす.
      crally <- 0    #rally count
      
      
      while(judge==1){
        
        #手番のplayerの戦略関数をfstrに代入
        fstr <- ifelse(player==1,fstr1,fstr0)
        
        #相手のshotからeffectを決定
        effect <- ifelse(shot==0, 0, reffect)
        
        #自分のshotの決定  
        shot <- fstr(shot)
        prob <- ifelse(shot==0, 0.9-effect, 0.7-effect)
        
        #shotの成功判定
        judge <- ifelse(runif(1)<prob, 1, 0)
        
        #手番playerの交代
        player <- ifelse(player==1, 0, 1)
        
        #rally count追加
        crally <- crally + 1
      }
      
      
      #ラリーカウントベクトルの更新
      vcrally <- c(vcrally, crally)
      #ポイントの更新
      if(player==1){
        p1point <- p1point + 1 
      }else{p0point <- p0point + 1}
      
    }
    
    #試合結果(ラリー数平均,ラリー数分散,最大ラリー数p1point,p0point,gamewinplayer)
    vresult <- c(mean(vcrally),var(vcrally),max(vcrally),p1point,p0point,ifelse(p1point>p0point,1,0))
  return(vresult)
}

速度比較

必要なライブラリーの読み込みと戦略関数と上で定義した3つ関数の読み込みを行う.

library(Rcpp)
library(foreach)
library(doSNOW)
## Loading required package: iterators
## Loading required package: snow
source("strategy.R")
sourceCpp("frally.cpp")
source("frally.R")
source("fonerally.R")

単純にRで計算するケース, Rcppを使用して高速化するケース, foreachを使用して並列計算するケースの3つのケースで比較する. 設定としては, プレイヤー1がrisky戦略, プレイヤー0がsafety戦略をとり, シミュレーション回数は10000回, risky shotの効果の大きさを0.284とする.

単純にRで計算するケース

system.time(
  mr <- frallyR(fstr_allrisky,fstr_allsafety,10000,0.284)
)
##    user  system elapsed 
##   16.03    0.11   16.22

Rcppを使用して高速化するケース

system.time(
  mcpp <- frallycpp(fstr_allrisky,fstr_allsafety,10000,0.284)
)
##    user  system elapsed 
##   23.60    0.00   23.64

foreachを使用して並列計算するケース

cl <- makeCluster(4, type = "SOCK")
registerDoSNOW(cl)
system.time(
  mrp <- foreach(i=1:10000, .combine = "rbind") %dopar% fonerallyR(fstr_allrisky,fstr_allsafety,0.284)
)
##    user  system elapsed 
##    8.21    0.47   13.58
stopCluster(cl)

結果の確認

速度比較はできたが, 一応, 中で同様のことができているか確認する. 扱いやすいようにデータフレームの形式に変換する. mr以外の2つの行列についても同様.

dfr <- as.data.frame(mr)
colnames(dfr) <- c("mean","var","max","p1point","p2point","winp")
rownames(dfr) <- NULL
head(dfr)
##       mean       var max p1point p2point winp
## 1 3.175000  3.276282   9      19      21    0
## 2 3.973684 14.999289  19      17      21    0
## 3 4.121212  8.047348  13      21      12    1
## 4 3.684211  7.411095  11      21      17    1
## 5 3.390244  8.343902  12      20      21    0
## 6 2.825000  3.276282   9      19      21    0
dim(dfr)
## [1] 10000     6
colMeans(dfr[,c("winp","mean","var","p1point","p2point")])
##      winp      mean       var   p1point   p2point 
##  0.495700  3.328941  5.794026 18.440500 18.600500
head(dfcpp)
##       mean       var max p1point p2point winp
## 1 3.171429 11.969748  21      14      21    0
## 2 3.000000  3.600000   8      20      21    0
## 3 3.625000  5.596774   9      21      11    1
## 4 3.400000  4.041026  10      21      19    1
## 5 3.051282  5.418354  11      18      21    0
## 6 3.317073  5.671951   9      21      20    1
dim(dfcpp)
## [1] 10000     6
colMeans(dfcpp[,c("winp","mean","var","p1point","p2point")])
##      winp      mean       var   p1point   p2point 
##  0.495600  3.331092  5.813098 18.517200 18.557800
head(dfrp)
##       mean       var max p1point p2point winp
## 1 3.390244  4.693902   8      20      21    0
## 2 3.111111  3.473016   9      21      15    1
## 3 3.151515  5.570076  12      21      12    1
## 4 4.027778 14.027778  16      15      21    0
## 5 2.536585  2.004878   6      21      20    1
## 6 3.717949 10.628880  15      18      21    0
dim(dfrp)
## [1] 10000     6
colMeans(dfrp[,c("winp","mean","var","p1point","p2point")])
##      winp      mean       var   p1point   p2point 
##  0.488000  3.328949  5.789926 18.462100 18.596600

おそらく出力に問題はないと思われる.

おわりに

昨年学んだ並列計算で高速化できたのは良かったけど, Rcppで書き換えたのに、むしろ遅いってどういうことなの……. コードの書き方がおそらく非効率なのだろう. コードの最適化やC++について今後少しずつ学んでみようと思う.

<追記: 2019/01/02>

C++側で, Rで定義した関数を呼び出すところがどうしても遅くなるようで,  高速化をしたいのなら, それは避けるべきみたい. C++で定義し直してやるとよい. 

加えて, cbindはサイズが大きくなると遅くなるので, mresultのmatrixを先に用意して代入していく方がおそらく速くなる.

Twitter有識者(鍵アカウントなので名前の公開は避けます.)の方々のご指摘で気付きました. ありがとうございます.

<追記おわり>

 

 

Rcppとforeachという持ち札が増えたのは良かった. Rcppのコードで詰まっていたところについてのコメントは大変助かりました. ill-identifiedさん, ありがとうございました.