cmdstanr入門+
reduce_sum()解説
rstanはもうダメです
清水裕士
関西学院大学
2021/9/13
自己紹介
• 清水裕士
• 関西学院大学社会学部
• 専門
• 社会心理学
• Web
• @simizu706
• https://norimune.net
rstanからcmdstanrへ
• 実をいうと、rstanはもうダメです
• 突然こんなこと言ってごめんね。
• でも本当です。
• rstanはまったく更新されていない
• 2020年7月から更新されてない(2021/9/13現在)
• 最新のStanが使える機能が使えない
• Stanチームもcmdstanrを推奨している
• 君もcmdstanrに乗り換えよう!
reduce_sum()って?
• チェイン内並列化の機能
• サンプルサイズが大きかったり、尤度の計算が重い
ときに時間がかかる・・・
• これを使えばチェイン内で並列計算が可能
• Stanに結構前に実装
• しかし、rstanにはまだ実装されてないのであまり使
われてない印象
• この資料ではそのやり方を紹介
cmdstanrを使おう
Stanの家族(Rの話)
• cmdstan
• Stanをcmdで動かすためのコア部分
• これだけだと使いづらいので、さまざまなインターフェ
イスでcmdstanを動かせるようになっている
• rstan
• R内部でcmdstanを動かすためのパッケージ
• Rで使うためのさまざまな関数やオプションがある
• cmdstanr
• Rからcmdstanを動かす命令を出すパッケージ
• 出力はcsvファイルで出たものを読み取る
rstanとcmdstanrの違い
rstan
cmdstan
cmdstanr
cmdstan
Rの内部(rcpp)で
cmdstanを動かす
Rからcmdstanを動か
す命令を出す
rstanとcmdstanrの違い
• rstanのメリット
• 豊富な関数
• 出力や、結果を要約するための関数を揃えている
• 他のパッケージとの連携
• bridgesamplingとか、stanfitを入れたらそのまま動くパッ
ケージが多い
• cmdstanrのメリット
• 速い・軽い
• 安定している(爆発しにくい)
• 依存パッケージが少なく、インストールしやすい
• cmdstanのバージョンアップにすぐ対応できる
rstanはバージョンが古い
• cmdstanの最新版は2.27.0(2021/9/13現在)
• rstanは2.21
• 古い!
• 後述するreduce_sum()は2.23から対応
• cmdstanrはcmdstanを別にインストールする
• install_cmdstan(cores = 2)
• cmdstanrのバージョンに関わらず、cmdstanの最新版
を使うことができる
rstanは重い
• 最近よく爆発するようになった
• あぼーん
• Rのrcppで動かしてることが原因?
• stopボタンを押しても止まってくれない
• 結果、あぼーん
• とくにVBだとほぼ確実に止まらない
• cmdstanr
• R内部ではなく、cmdstanをR外部で動かしている
• R自体は重くならない
• stopボタンをおしたら確実にcmdstanを殺す
• VBでもしっかり殺す
rstanは遅い
• N=10000のロジスティック回帰分析
• rstanのiter=2000の平均時間 13.5秒ぐらい
• cmdstanrのiter=2000の平均時間 8秒ぐらい
rstan cmdstanr
cmdstanrの準備
cmdstanrの導入
• 基本的にはここを見てください
• https://mc-stan.org/cmdstanr/articles/cmdstanr.html
• Windowsユーザーはこちらも参考にしてみてください
• https://norimune.net/3609
今回使うパッケージ
library(magrittr)
library(rstan)
library(bayesplot)
library(bridgesampling)
コピペ用のテキスト
僕は非tidyverse民なので
magrittrを入れてますが、
普通にtidyverse入れてください
rstanは自力でいれてください
https://github.com/stan-
dev/rstan/wiki/RStan-Getting-Started-
(Japanese)
が参考になります。
インストールの仕方
• まずはインストール
• コンパイル環境の確認
• cmdstanをインストール
install.packages("cmdstanr", repos = c("https://mc-stan.org/r-packages/",
getOption("repos")))
check_cmdstan_toolchain()
library(cmdstanr)
install_cmdstan(cores = 2)
バージョンを指定したい場合
install_cmdstan(cores = 2,version = "2.26.1")
cmdstanのバージョンを変更
• バージョンの確認
• cmdstan_version()
• パスの確認
• cmdstan_path()
• バージョンの変更
set_cmdstan_path(path = "/home/[hoge]/.cmdstanr/cmdstan-2.26.1")
cmdstanrの使い方
cmdstanrの使い方の基本
• オブジェクト指向
• モデルオブジェクトにメソッドがある
• sample, variational, optimizeなど
• メソッドはRでいう関数なのでそこに引数をいれる
• ほとんどはposteriorパッケージの関数
• bayesplotパッケージと相性がいい
• bayesplotはStanチームが開発している事後分布を可
視化するためのパッケージ
モデルのコンパイル
• cmdstan_model()をつかう
• model <- cmdstan_model(“reg.stan”)
• ちょっと前は相対パスが使えなかったが今は使える
のでrstanと同じ感覚でOK
• コンパイルが速い!
• 体感的にはrstanの2/3~1/2
• あと視覚的にコンパイルしてくれてる
のがちゃんとわかるのでなんか安心
これがクルクル回る
サンプルデータ生成
set.seed(123)
N <- 10000
P <- 2
alpha <- -3
beta <- rep(1,P)
X1 <- rnorm(N, 5, 2)
X2 <- rnorm(N, 3, 4)
mu <- alpha + beta[1]*X1 + beta[2]*X2
logistic <- function(x){
1/(1+exp(-x))
}
Y <- rbinom(N,1,logistic(mu))
X <- data.frame(X1,X2)
Y %>% hist
glm(Y ~ X1 + X2,family=binomial) %>% summary
今回使うStanコード
data{
int N;
int P;
int Y[N];
matrix[N,P] X;
}
parameters{
real alpha;
vector[P] beta;
}
model{
Y ~ bernoulli_logit(alpha + X*beta);
alpha ~ normal(0, 10^2);
beta ~ normal(0, 10^2);
}
generated quantities{
vector[100] mu = alpha + X[1:100,]*beta;
}
サンプリング
• rstanとちょっと癖が違う
• fit <- model$sample(data = “datastan”)
• モデルオブジェクトにメソッドがある
• sample():MCMCを行う
• variational():VBを行う
• オプションが違う
• iter_warmup:ウォームアップ期間のサイズ
• iter_sampling:ウォームアップ後のサイズ
• chains:マルコフ連鎖の数
• parallel_chains:並列コアの数
サンプリング
datastan <- list(N=N, P=P, Y=Y, X=X)
model <- cmdstan_model(“logistic.stan")
fit <- model$sample(datastan,
iter_warmup = 1000,
iter_sampling = 1000,
chains = 4,
parallel_chains = 4,
refresh = 200)
cmdstanrの結果の要約
• 結果の要約は$print()
• fit$print(c(“alpha”,”beta”))
• $summaryもあるが、tibble表記が嫌いなので僕は
こっち。
• 注意点
• デフォルトでは「90%」信用区間が表示される
$print()メソッドのオプション
• max_rows= :表示列の最大値を選ぶ
• デフォルトでは10
• 要約統計量関数を指定できる
僕が使ってる感じ
map <- function(z){
density(z)$x[which.max(density(z)$y)]
}
quantile95 <- function(x){
quantile(x, probs = c(0.025, 0.975), names = TRUE)
}
ess_tail95 <- function(x){
min(posterior::ess_quantile(x, probs = c(0.025, 0.975), names = TRUE))
}
r_hat <- posterior::rhat
pd_out <- c("mean","sd","map","quantile95","ess_bulk","ess_tail95", "r_hat")
fit$print(c("alpha","beta"),pd_out,max_rows = 30)
貼り付け可能なテキストは
こちら
出力
• 結果の出力は$draws()
• beta <- fit$draws(“beta”)
• iteration×chain×variable
• 3次元配列になっている
• rstanと違うので注意!
収束判断 rhatのグラフ
• 収束判断はbayesplotパッケージを活用
• drawsしたものをそのまま関数にいれる
library(bayesplot)
fit %>% bayesplot::rhat() %>% hist
トレースプロット
• mcmc_traceを使う
• fit$draws("alpha") %>% mcmc_trace
自己相関
• mcmc_acf()を使う
• fit$draws("beta") %>% mcmc_acf
事後分布の可視化
• ぜんぶbayesplotに頼る
fit$draws("alpha") %>% mcmc_dens
fit$draws("alpha") %>% mcmc_hist
fit$draws("alpha") %>% mcmc_dens_overlay
fit$draws(c("beta")) %>% mcmc_pairs
結果の取り出し
MCMCサンプルの出力
• いわゆるextract
• cmdstanrではdraws
alpha <- fit$draws("alpha")
alpha %>% hist
基本的には$print()と$draws()の2つを覚えてたらOK!
tidyなデータに変える
• posteriorパッケージ
• cmdstanrの従属パッケージ
• cmdstanrをいれたら自動的に付いてくる
• as_draws_df()
library(posterior)
temp <- fit$draws("beta") %>% as_draws_df
temp
いろいろできる
• 中身は実際はこうなっている
• data.frame(temp)
• .chain
• .iteration
• .draw
• がindex変数として格納
• 特定のチェインだけ除きたい場合
temp <- fit$draws("beta") %>%
as_draws_df() %>%
dplyr::filter(.chain != 3)
事後平均値をRに保存
• 非tidyverse民の僕はapply()
• mu <- fit$draws("mu") %>% apply(3,mean)
• VBのときはchainの次元がないのでapply(2,mean)
outputまわり
cmdstanの結果はどこに?
• $output_files()メソッドをつかう
• fit$output_files()
• 注意!
• tmpフォルダにはいったcsvファイルは日をまたぐと
消える(Ubuntuだと)
• 一度$printとかをすれば多分大丈夫
• 気になるなら、場所を変えてやることもできる
• fit$save_output_files("cmdstan/")
注意!
• そのままだと消える
• tmpフォルダにはいったcsvファイルは日をまたぐと
消える(Ubuntuだと)
• あと、jobsを使うとoutputファイルが迷子になる
• 一度$print()とかをすれば多分大丈夫
• 気になるなら、場所を変えてやることもできる
• fit$save_output_files("cmdstan/")
rstanfitオブジェクトを作る
• cmdstanrfit → rstanfit
fit.rstan <- fit$output_files() %>% rstan::read_stan_csv()
fit.rstan
bridgesampling
の使い方
cmdstanrの欠点
• bridgesamplingパッケージと連携しない
• 2021年9月現在
• 一度、rstanfitオブジェクトに変換する必要がある
• どうやらcmdstan単体ではモデル全体の対数確率を
出力する機能がないので対応厳しいらしい
• ちょい面倒
• rstanfitオブジェクトを作って、iter=0でもう一度か
らのrstanfitオブジェクトを作る
fit.sf <- rstan::read_stan_csv(fit$output_files())
path <- model$stan_file()
sf <- rstan::stan(path,data=data,iter=0)
bs <- bridge_sampler(fit.sf, stanfit_model = sf)
関数を作ったよ
• cmdstanr_bs()
cmdstanr_bs <- function(fit,model,data){
require(rstan)
require(bridgesampling)
path <- model$stan_file()
fit.sf <- rstan::read_stan_csv(fit$output_files())
sf <- rstan::stan(path,data=data,iter=0)
bs <- bridge_sampler(fit.sf, stanfit_model = sf)
return(bs)
}
bs <- cmdstanr_bs(fit,model,datastan)
MCMCのサンプルが入ったfitオブジェ
クト、モデル、データの順に入れる
reduce_sum導入
reduce_sumとは?
• ベクタライズしたlpdf(lpfm)関数を並列化
• 10000人の尤度の計算
• どの順番に計算しても問題ない(交換可能だから)
• つまり分割して計算してあとで足しても問題ない
• 今のrstanは対応が追いついていない
• stan2.23から対応したため、reduce_sumを使うため
にはcmdstanrを使う必要がある
• rstan最新版は2.21.2
具体的な使い方
• functionsブロックで関数を定義
• partial_sum_lpdf()
• 目的変数が離散データなら最後をlpmfにする
• 変数宣言の順番が決まってるので注意
• 1番目:目的変数
• 2番目:int start
• 3番目:int end
• 4番目以降:モデルで使う変数たち
partial_sum_lpmfのコード
• こんな感じ
• 関数内で、bernoulli_logit_lumf()にモデルを入れる
• slice_Yは適当につけた名前で、特に決まってない。別にYで
も走る。
• 目的変数はそのまま入れればいいけど、説明変数などは
start:endで配列を指定する必要あり
• 内部システム的に、startとendには分割された配列の範囲が割
り振られる
real partial_sum_lpmf(int[] slice_Y, int start, int end, real alpha, vector beta, matrix X){
return bernoulli_logit_lupmf(slice_Y | alpha + X[start:end,]*beta);
}
reduce_sum()をtargetに
• reduce_sum()の引数
• 1番目:partial_sum_lpmf() つまり関数が引数
• 2番目:目的変数 ここではY
• 3番目:grainsize 一度に計算するサイズ
• grainsize=1の場合、Stanが自動決定する
• それ以外の整数をいれたらそのサイズで計算
• 1が常に最速とは限らないので、N/par_chainとかから試し
てみる
• 4番目以降:モデルで使う変数
target += reduce_sum(partial_sum_lpmf, Y, grainsize, alpha, beta, X);
Stanコード
テキストバージョン
functions{
real partial_sum_lpmf(int[] slice_Y, int start, int end, real alpha, vector beta, matrix X){
return bernoulli_logit_lupmf(slice_Y | alpha + X[start:end,]*beta);
}
}
data{
int N;
int P;
int Y[N];
matrix[N,P] X;
}
transformed data{
int grainsize = 1;
}
parameters{
real alpha;
vector[P] beta;
}
model{
target += reduce_sum(partial_sum_lpmf, Y, grainsize, alpha, beta, X);
alpha ~ normal(0, 10^2);
beta ~ normal(0, 10^2);
}
コンパイル時の注意
• Rstudioのコンパイルチェック機能
• rstanのバージョンに合わせている
• reduce_sum()は2.23で追加したものなので、コンパ
イルエラーがでる
• そんな関数ねぇよ
• 気にせずコンパイルする
• 並列化のためのコンパイルをやる必要がある
• cpp_options = list(stan_threads = TRUE)
model.rs <- cmdstan_model("logistic_rs.stan", cpp_options = list(stan_threads = TRUE))
サンプリングのやり方
• threads_per_chainを指定
• PCのコアが32個あるなら、4×8まで可能
• ただコアの数が多くなると計算結果の統合に時間がか
かってしまって余計効率が悪いこともある
• モデルの複雑さやサンプルサイズなどでうまく調整する
• 2~4で十分な気がする
fit.rs <- model.rs$sample(datastan,
iter_warmup = 1000,
iter_sampling = 1000,
chains = 4,
parallel_chains = 4,
threads_per_chain = 2,
refresh = 200)
スピード比較
• 2つに分割 4つに分割
• 8.5秒→5.5秒 8.5秒→4.5秒
まとめ
• cmdstanrを使おう!
• rstanは重くて遅くて爆発する
• cmdstanrは軽くてバージョンを選べて速い
• reduce_sum()も使おう!
• もしコアが多いPCをお持ちならオススメ
• rstanで普通に推定するよりも、cmdstanrを使って、
かつ、reduce_sum()を活用したら、断然スピードが
違う!
• rstanで13.5秒→cmdstanで最速4.5秒
Enjoy!

Cmdstanr入門とreduce_sum()解説