メモ : CmdStanPyの環境をdockerで用意する

「[StanとRでベイズ統計モデリング](https://www.kyoritsu-pub.co.jp/bookdetail/9784320112421)」を読むために、CmdStanPy環境をdockerで用意しました。CmdStanPyの環境構築については日本語の資料はほとんどなさそうだったので、もしかしたら誰かの役に立つかもしれないと思い、メモに残しておきます。

# はじめに
[公式document](https://cmdstanpy.readthedocs.io/en/latest/getting_started.html#install-package-cmdstanpy)にもある通り、PyStanを既にインストールした環境を使っている方はご注意ください。
> Note for PyStan users: PyStan and CmdStanPy should be installed in separate environments. If you already have PyStan installed, you should take care to install CmdStanPy in its own virtual environment.

他にも依存性の問題などでもろもろ面倒になりそうなので、筆者はdockerで環境を用意しました[^1]。


# CmdStanPyとは

PythonによるCmdStanの軽量wrapper

* 公式document : https://cmdstanpy.readthedocs.io/en/latest/index.html
* github : https://github.com/stan-dev/cmdstanpy

# なぜCmdStanPyか?

PythonからStanを使いたい場合、一般的にはPyStanを利用することが多いかと思います[^2]。ではなぜCmdStanPyを使うことにしたかというと、PyStanより(現状では)速く動くからです。

詳しい経緯は以下の通りです : 

筆者も最初はPyStan環境を用意したのですが、Eight schoolsという簡単な例([PyStanのdocumentのこの部分](https://pystan.readthedocs.io/en/latest/getting_started.html)参照)でのモデルのコンパイルに1分以上かかってしまいました。

筆者のインストール方法が悪かったのか、はたまたこれが普通なのか調べたところ、StanのCommunityでの[こちらのやりとり](https://discourse.mc-stan.org/t/why-is-it-so-slow-for-stan-to-compile-model/6004)を見つけました。曰く、
> Stan compilation speedup was introduced in Stan 2.20, but Rstan is only up to Stan 2.19.2.

> right now we have lightweight wrappers CmdStanPy 11 (still in beta) and CmdStanR 18 (almost to beta release) which use the latest CmdStan (now at 2.21, 2.22 coming soon) - or any version of CmdStan that you’d like.

このやりとり自体は2年前だったのですが、筆者のPyStan環境のPyStanのversionを確認すると2.19.1.1で、speedup導入前のもののようでした[^3]。また、[こちらのStackoverflow](https://stackoverflow.com/questions/58572041/speed-up-model-compilation-in-pystan)の書き込みでも、CmdStanPyにすることで3倍以上コンパイルが速くなったとの記述がありました。

そこで、CmdStanPyを試してみることにしました。


# 環境

* OS : macOS Mojave 10.14.6
* docker : 19.03.12

# Dockerでの環境構築

```Dockerfile:Dockerfile
FROM ubuntu:20.04

RUN apt-get -y update
RUN apt-get -y install python3
RUN apt-get -y install python3-pip
RUN pip3 install --upgrade pip
RUN pip3 install cmdstanpy[all]
RUN python3 -c 'import cmdstanpy; cmdstanpy.install_cmdstan()'

WORKDIR /workdir
```

このDockerfile[^4]があるdirectoryで次のようにbuildし(YOUR_TAGは適当なTAG名に変えてください)、

```bash
docker image build -t YOUR_TAG .
```

起動します(localのソースコードを利用したいので、カレントディレクトリを`/workdir`にマウントしています。)

```bash

docker container run -it --rm --net=host  \
		--mount type=bind,src=`pwd`,dst=/workdir \
		YOUR_TAG \
		bash
```

# 実行

dockerコンテナを起動したディレクトリに

* stan file(`8schools.stan`[^5])
* データ(`8schools.data.json`)
* python script(`run-cmdstanpy.py`)

を用意しておきます。

```stan:8schools.stan
data {
    int<lower=0> J; // number of schools
    vector[J] y; // estimated treatment effects
    vector<lower=0>[J] sigma; // s.e. of effect estimates
}
parameters {
    real mu;
    real<lower=0> tau;
    vector[J] eta;
}
transformed parameters {
    vector[J] theta;
    theta = mu + tau * eta;
}
model {
    eta ~ normal(0, 1);
    y ~ normal(theta, sigma);
}
```

```JSON:8schools.data.json
{
    "J": 8,
    "y": [28,  8, -3,  7, -1,  1, 18, 12],
    "sigma": [15, 10, 16, 11,  9, 11, 10, 18]
}
```

```python:run-cmdstanpy.py
from cmdstanpy import CmdStanModel


print("Compile started")
model = CmdStanModel(stan_file="8schools.stan")
print("Compile finished")
print(model)
print("========================================")
data = "8schools.data.json"
print("Sampling started")
fit = model.sample(data=data)
print("Sampling finished")
print(fit)
```

「Dockerでの環境構築」の節で起動したコンテナ内の`/workdir`で`python3 run-cmdstanpy.py`を実行して以下のような結果が出ればひとまず成功です。

```
Compile started
INFO:cmdstanpy:compiling stan program, exe file: /workdir/8schools
INFO:cmdstanpy:compiler options: stanc_options=None, cpp_options=None
INFO:cmdstanpy:compiled model file: /workdir/8schools
Compile finished
CmdStanModel: name=8schools
	 stan_file=/workdir/8schools.stan
	 exe_file=/workdir/8schools
	 compiler_optons=stanc_options=None, cpp_options=None
========================================
Sampling started
INFO:cmdstanpy:start chain 1
INFO:cmdstanpy:start chain 2
INFO:cmdstanpy:start chain 3
INFO:cmdstanpy:start chain 4
INFO:cmdstanpy:finish chain 3
INFO:cmdstanpy:finish chain 1
INFO:cmdstanpy:finish chain 4
INFO:cmdstanpy:finish chain 2
Sampling finished
CmdStanMCMC: model=8schools chains=4['method=sample', 'algorithm=hmc', 'adapt', 'engaged=1']
 csv_files:
	/tmp/tmpd0ii4fo_/8schools-202009110125-1-0qknvwdc.csv
	/tmp/tmpd0ii4fo_/8schools-202009110125-2-qfoj4mom.csv
	/tmp/tmpd0ii4fo_/8schools-202009110125-3-cbn8wxph.csv
	/tmp/tmpd0ii4fo_/8schools-202009110125-4-h_5980t6.csv
 output_files:
	/tmp/tmpd0ii4fo_/8schools-202009110125-1-0qknvwdc-stdout.txt
	/tmp/tmpd0ii4fo_/8schools-202009110125-2-qfoj4mom-stdout.txt
	/tmp/tmpd0ii4fo_/8schools-202009110125-3-cbn8wxph-stdout.txt
	/tmp/tmpd0ii4fo_/8schools-202009110125-4-h_5980t6-stdout.txt
deleting tmpfiles dir: /tmp/tmpd0ii4fo_
done
```

CmdStanPyの詳しい使い方などは[公式document](https://cmdstanpy.readthedocs.io/en/latest/index.html) をご覧ください。


# appendix : PyStanとの速度比較

PyStanとの速度比較についても軽く記録しておきます。stan fileはいずれも「実行」の節に書いた`8schools.stan`で共通です。

## CmdStanPy

docker環境は「Dockerでの環境構築」に書いたとおりのものです。python fileは次のものを用いました:

```python:test-cmdstanpy.py
from cmdstanpy import CmdStanModel
import time

print("Compile started")
t1 = time.time()
model = CmdStanModel(stan_file="8schools.stan")
t2 = time.time()

data = "8schools.data.json"
print("Sampling started")
t3 = time.time()
fit = model.sample(data=data)
t4 = time.time()

print("Compile time :", t2 - t1, "seconds")
print("Sampling time :", t4 - t3, "seconds")
```

## PyStan

利用したDockerfileは次の通りです:

```Dockerfile:Dockerfile
FROM ubuntu:20.04

RUN apt-get -y update
RUN apt-get -y install python3
RUN apt-get -y install python3-pip
RUN pip3 install --upgrade pip
RUN pip3 install pystan

WORKDIR /workdir
```
「Dockerでの環境構築」と同様にbuild, 起動します。また、python fileは以下のものを利用しました。:

```python:test-pystan.py
import pystan
import time

schools_dat = {'J': 8,
               'y': [28,  8, -3,  7, -1,  1, 18, 12],
               'sigma': [15, 10, 16, 11,  9, 11, 10, 18]}

print("Compile started")
t1 = time.time()
sm = pystan.StanModel(file='8schools.stan')
t2 = time.time()
print("Sampling started")
t3 = time.time()
fit = sm.sampling(data=schools_dat, iter=1000, chains=4)
t4 = time.time()

print("Compile time :", t2 - t1, "seconds")
print("Sampling time :", t4 - t3, "seconds")
```

## 結果

CmdStanPy

```
Compile time : 12.916020393371582 seconds
Sampling time : 0.16101527214050293 seconds
```

PyStan

```
Compile time : 98.87509870529175 seconds
Sampling time : 0.45189523696899414 seconds
```

何度か測ってみましたが、モデルのコンパイルについては、基本的にCmdStanPyがPyStanより7倍程度速いという結果になりました[^6][^7]。

[^1]: ただし、筆者はdockerを雰囲気で使っているところがあるので、ここに書いたdocker環境の構築法は最善のものでない可能性があります。
[^2]: 「Python Stan」でGoogle検索して真っ先に出てきたのはPyStanの[document](https://pystan.readthedocs.io/en/latest/)と[githubのpage](https://github.com/stan-dev/pystan)でした。
[^3]: PyStanのversionの数字がStanのversionの数字と対応しているという認識であっているのでしょうか...?
[^4]: CmdStanPyのインストールを行う部分の詳細は https://cmdstanpy.readthedocs.io/en/latest/getting_started.html#installation　を参照。
[^5]: PyStanのgetting started のページからダウンロードしました。　https://pystan.readthedocs.io/en/latest/getting_started.html
[^6]: CmdStanPyの場合は「exe fileとstan fileの更新時刻を比べて、後者が前者より新しい場合はコンパイルをしない」という気の利いた機能がついているので、繰り返し測る場合はご注意ください。
[^7]: PyStanのbase imageをubuntu:16.04にしたときはPyStanも50秒程度で済んだのですが、原因は調べていません。