Up

PyStan 3のためのStan入門

 

 Stanの解説はウェブサイト

https://mc-stan.org/docs/reference-manual/

などで入手できる。

 Stanの基本構文は

<program> ::= [<function_block>] [<data_block>] [<transformed_data_block>]

              [<parameters_block>] [<transformed_parameters_block>]

              [<model_block>] [<generated_quantities_block>] EOF

である(https://mc-stan.org/docs/reference-manual/syntax.html)。

すなわち、Stanプログラムは、

functionブロック] [dataブロック] [transformed dataブロック]

parametersブロック] [transformed parametersブロック]

modelブロック] [generated quantitiesブロック]

からなり、これらの順に記述する。必要がないブロックは省略する。

 以下、簡単な例で説明をすすめる。なお、StanによるMCMCサンプリングはMiniconda/Ubuntu24/WSL/Windows10Python3.12仮想環境で行い、MCMCサンプリングで得られた事後分布の描画などは、Windows10にインストールしたUbuntuではできなかったので、Windows10にインストールしたPython3.13で行った。Linuxで実行する場合は、同じ開発環境で続けて実行すればよい。WindowsUbuntuLinux)をインストールする方法は、このウェブサイトで説明した。WindowsUbuntuのソフトを並行して利用できる。

ベイズ分析の解説書としてGelman et al. 2014がある。

ファイルはまとめて圧縮ファイルintropystan3files.zipとした。

以下に、2項分布正規分布変量正規分布を例として説明する。

 

 

1.2項分布

 

 コイン投げをN試行行い、表の出た試行数がkであったときの2項分布によるStanスクリプトとPythonスクリプトをリスト1.1のように書いてみた。

 

 

リスト1.1 2項分布のStanスクリプト例

import stan       #  pystan3importされる

import pickle     #  データの保存用モジュール

 

StanCode = """

    data {

        int N;    //  2項分布の総試行数

        int k;    //  生起回数

    }

    parameters {

        real<lower=0,upper=1> p;    //  生起確率

    }

    model {

        k ~ binomial(N, p);         //  2項分布のモデル

    }

"""

 

sm = stan.build(StanCode, data={'N':10, 'k':7})

fit = sm.sample()

 

df_fit = fit.to_frame()               #  PandasDataFrameに変換

print(df_fit['p'].describe())         #  DataFameの関数describeの使用

 

with open('param_p.pkl', 'wb') as f:

    pickle.dump(df_fit['p'], f)       #  パラメータpMCMCサンプルの保存

 

 

PyStan3のインストールは

 

pip install pystan

 

であるが、import

 

import stan

 

である。

Stanスクリプトは、変数StanCodeで指し示されている文字列で与えられている。上のStanスクリプトでは、dataブロック、parametersブロックとmodelブロックが使われており、各ブロックの内容は、{}で囲まれた部分である。各ブロックは文からなるが、各文の終わりにはセミコロン;を置く。{;は、半角文字であることに注意。Stanスクリプトは、コメントを除き、半角英数字で書く(https://mc-stan.org/docs/reference-manual/encoding.html)。1行に、複数の文を書くこともできる。行において、/2本書くと「//」、その右側はコメントになる。「/*」と「*/」で囲まれた部分もコメントとして扱われ、これは複数行に亘ることができる。

data {}」にデータを記述する。リスト1.1では、整数値のデータNkが宣言されている。具体的な値は、stan.buildで与えられている。

Stanではいろいろな型が用意されているが(https://mc-stan.org/docs/reference-manual/types.html)、リスト1.1では整数を表すint型が用いられている。変数名は、Nkである。データを表す変数(識別子)の宣言の形式は

 

型 識別子

 

である。

parameters {}」に、確率モデルのパラメータとしてpが宣言されている。この変数は、確率なので0以上1以下の値を取る。変数の最小値と最大値を指定した変数宣言の形式

 

real<lower=0,upper=1> p; 

 

となっている。変数の型名に

 

<lower=1,upper=値2>

 

の形式で、最小値と最大値を設定する。最小値あるいは最大値の制約が無いときは省く。

パラメータ変数に最小値と最大値を設定すると、その間の一様分布が事前分布として設定されたと見なされる。「modelブロック」において、pの事前分布を

 

p ~ beta(1, 1);

 

と設定すると、実行時に

 

Messages from stanc:

Warning: The parameter p has 2 priors.

 

と、警告が出される。一様分布は確率密度関数が定数値であるので、事前分布が2つ重なって設定されてもMCMCへの影響はない。

modelブロック」に確率モデルが設定される。

 

k ~ binomial(N, p);

 

2項分布についてはhttps://mc-stan.org/docs/functions-reference/bounded_discrete_distributions.htmlに説明がある。

 

Stanスクリプトをデータとともに次のコードでビルドする(https://pystan.readthedocs.io/en/latest/reference.html)。

 

sm = stan.build(StanCode, data={'N':10, 'k':7})

 

第1引数にStanスクリプトを置き、引数dataにデータ値を辞書型で設定している。

ビルドしたものの関数sample()を実行してMCMCサンプリングが行われる。

 

fit = sm.sample()

 

MCMCサンプルを、次のコードでpandasDataFrame型に変換している。

 

df_fit = fit.to_frame()

 

パラメータpMCMCサンプルをファイルparam_p.pklに保存して、スクリプトの実行終了である。

 

with open('param_p.pkl', 'wb') as f:

    pickle.dump(df_fit['p'], f)  

 

 

ファイルparam_p.pklに保存されたパラメータpのMCMCサンプルを読み出して、KDE (Kernel Density Estimate)曲線を描くスクリプトをリスト1.2のように用意した。

 

 

リスト1.2 事後分布の描画

import pickle

import matplotlib.pyplot as plt

import seaborn as sb

 

with open('param_p.pkl', 'rb') as f:

    param_p = pickle.load(f)

 

sb.kdeplot(param_p)

plt.show()

 

 

リスト1.2のスクリプトをWndows10にインストールしたPython 3.13で実行すると図1.1のグラフを得る。

1.1

 

 

 

2.正規分布

 

正規分布モデルを用いるStanスクリプト例をリスト2.1のように作成してみた。

 

 

リスト2.1 正規分布を用いるStanスクリプト

import numpy as np

import stan

import pickle

 

x = [54, 73, 47, 61, 62, 53, 58, 63, 65, 58,

     68, 69, 49, 63, 62, 60, 63, 64, 44, 61]

 

with open('x.pkl', 'wb') as f:

    pickle.dump(x, f)

 

StanCode = """

    data {

        int n;

        array[n] real x;

        real min_x;

        real max_x;

    }

    parameters {

        real mu;

        real<lower=0> sgm;

    }

    model {

        mu ~ normal((min_x+max_x)/2, max_x-min_x);    //  muの事前分布

        sgm ~ exponential(1/(max_x-min_x));           //  sgmの事前分布

        x ~ normal(mu, sgm);                          //  正規分布モデル

    }

"""

 

sm = stan.build(StanCode, data={'n':len(x), 'x':x,

                                'min_x':np.min(x), 'max_x':np.max(x)})

fit = sm.sample()

 

df_fit = fit.to_frame()

mus = df_fit['mu']          #  muの事後分布サンプル

sgms = df_fit['sgm']        #  sgmの事後分布サンプル

print(mus.describe())

print(sgms.describe())

 

with open('mus.pkl', 'wb') as f:

    pickle.dump(mus, f)

   

with open('sgms.pkl', 'wb') as f:

    pickle.dump(sgms, f)

 

 

 

dataブロックでは、データ数n、データの1次元配列x、最小値min_xと最大値max_xが宣言されている。

配列は、arrayで始まる形式で

 

array[要素の数] 要素の型 配列名

 

の形式で宣言される(https://mc-stan.org/docs/stan-users-guide/matrices-arrays.html)。

パラメータsgmは、標準偏差を表すので非負の制約を置いて

 

real<lower=0> sgm;

 

と宣言している。

 

リスト2.1を実行すると、次の警告が提示される。

 

Warning: The parameter sgm has no priors. This means either no prior is

    provided, or the prior(s) depend on data variables. In the later case,

    this may be a false positive.

Warning: The parameter mu has no priors. This means either no prior is

    provided, or the prior(s) depend on data variables. In the later case,

    this may be a false positive.

 

これは、「false positive」の場合で、事前分布がデータ変数をパラメータとしている(https://mc-stan.org/docs/functions-reference/unbounded_continuous_distributions.htmlhttps://mc-stan.org/docs/functions-reference/positive_continuous_distributions.html#exponential-distribution)。

 

mu ~ normal((min_x+max_x)/2, max_x-min_x);    //  muの事前分布

sgm ~ exponential(1/(max_x-min_x));           //  sgmの事前分布

 

事前分布として一様分布に近いものを選んでいる(Gelman et al., 2021)。

データxの分布が

 

x ~ normal(mu, sgm);

 

で与えられているが、これは次の文

 

for (i in 1:n)

    x[i] ~ normal(mu, sgm);

 

をベクトル化したものである。

 

MCMCサンプリングにより得られたサンプルは、パラメータmuの事後分布のサンプルmusとパラメータsgmの事後分布のサンプルsgmsが取り出される。

 

mus = df_fit['mu']          #  muの事後分布サンプル

sgms = df_fit['sgm']        #  sgmの事後分布サンプル

 

それらは、それぞれファイルmus.pklsgms.pklに保存される。

 

with open('mus.pkl', 'wb') as f:

    pickle.dump(mus, f)

   

with open('sgms.pkl', 'wb') as f:

    pickle.dump(sgms, f)

 

 

 

ファイルmus.pklsgms.pklに保存されたMCMCサンプルを読み込んで分析するスクリプトをリスト2.2のように用意した。

 

 

リスト2.2 MCMCサンプルの分析

import pickle

import matplotlib.pyplot as plt

import seaborn as sb

import numpy as np

import scipy.stats as ss

 

with open('x.pkl', 'rb') as f:

    x = pickle.load(f)

 

with open('mus.pkl', 'rb') as f:

    mus = pickle.load(f)

   

with open('sgms.pkl', 'rb') as f:

    sgms = pickle.load(f)

 

 

sb.kdeplot(mus)

plt.show()

 

sb.kdeplot(sgms)

plt.show()

#

#      データと分析結果のグラフの表示

#

mu = np.median(mus)      #  中央値を推定値とする

sgm = np.median(sgms)    #  中央値を推定値とする

x_min = np.min(x)

x_max = np.max(x)

x_coord = np.linspace(x_min, x_max, 1000)

y_coord = ss.norm.pdf(x_coord, loc=mu, scale=sgm)    #  正規分布の値

plt.hist(x, density=True)

plt.plot(x_coord, y_coord, lw=3, label=f'mu={mu:.1f}\nsgm={sgm:.1f}')

plt.xlabel('x', fontsize=18)

plt.ylabel('Density', fontsize=14)

plt.legend()

plt.show()

 

 

 

リスト2.2のスクリプトを、リスト2.1のスクリプトの次に実行する。リスト2.1Miniconda/Ubuntu/WSL/WindowsPython 3.12仮想環境で実行したが、リスト2.2のスクリプトはWindows上のPython 3.13で実行した。まずパラメータmuの事後分布のグラフが表示される(図2.1)。

2.1

 

2.1Windowsを閉じると、パラメータsgmの事後分布のグラフが表示される(図2.2)。

2.2

 

2.2Windowを閉じると、図2.3のグラフが表示される。

2.3

 

データのヒストグラムと事後分布の点推定値に基づく正規分布関数のグラフである。点推定値は中央値を採用している(Gelman et al., 2021)。

2.3Windowを閉じると、スクリプトの実行終了である。

 

 

 

2変量正規分布

 

2変量正規分布を用いたStanスクリプト例をリスト3.1に示す。

 

 

リスト3.1 2変量正規分布を用いたStanスクリプトの例

import numpy as np

import stan

import pickle

 

xy = [[25, 40], [57, 65], [60, 68], [38, 50], [55, 65],

      [45, 49], [36, 55], [42, 54], [54, 61], [44, 59],

      [33, 73], [47, 56], [39, 54], [44, 61], [58, 63],

      [53, 58], [32, 53], [52, 70], [55, 64], [21, 43]]

xy = np.array(xy)

 

with open('xy.pkl', 'wb') as f:

    pickle.dump(xy, f)

 

StanCode = """

    data {

        int n;

        array[n] vector[2] xy;

        real xmin;

        real xmax;

        real ymin;

        real ymax;

    }

    parameters {

        /*

                再スケール前のサンプル値

        */

        real<lower=0, upper=1> vrho;

        real<lower=0, upper=1> vxmu;

        real<lower=0, upper=1> vymu;

        real<lower=0, upper=1> vxsgm;

        real<lower=0, upper=1> vysgm;

    }

    transformed parameters {

        /*

             データに合わせてパラメータのスケーリングを行う

        */

        real rho;

        real xmu;

        real ymu;

        real xsgm;

        real ysgm;

        rho = -0.9999 + vrho*1.9998;

        xmu = xmin + (xmax - xmin) * vxmu;

        ymu = ymin + (ymax - ymin) * vymu;

        xsgm = 0.0001 + (xmax - xmin) * vxsgm;

        ysgm = 0.0001 + (ymax - ymin) * vysgm;

    }

    model {

        /*

                スケーリング前のパラメータの事前分布

        */

        vrho ~ uniform(0,1);

        vxmu ~ uniform(0,1); 

        vymu ~ uniform(0,1);

        vxsgm ~ uniform(0,1);

        vysgm ~ uniform(0,1);

        /*

                 2変量正規分布

        */

        xy ~ multi_normal([xmu, ymu],

                              [[xsgm^2, xsgm*ysgm*rho],

                              [ysgm*xsgm*rho, ysgm^2]]);

    }

"""

 

sm = stan.build(StanCode,

                data={'n':len(xy), 'xy':xy,

                      'xmin':np.min(xy.T[0]), 'xmax':np.max(xy.T[1]),

                      'ymin':np.min(xy.T[1]), 'ymax':np.max(xy.T[1])})

fit = sm.sample()

 

df_fit = fit.to_frame()     #  MCMCサンプルのDataFrame型への変換

 

xmus = df_fit['xmu']

xsgms = df_fit['xsgm']

ymus = df_fit['ymu']

ysgms = df_fit['ysgm']

rhos = df_fit['rho']

print(xmus.describe())

print(xsgms.describe())

print(rhos.describe())

"""

       MCMCサンプルの保存

"""

with open('rhos.pkl', 'wb') as f:

    pickle.dump(rhos, f)

with open('xmus.pkl', 'wb') as f:

    pickle.dump(xmus, f)

with open('xsgms.pkl', 'wb') as f:

    pickle.dump(xsgms, f)

with open('ymus.pkl', 'wb') as f:

    pickle.dump(ymus, f)

with open('ysgms.pkl', 'wb') as f:

    pickle.dump(ysgms, f)

 

 

 

dataブロックにおいて、2変量のデータを

 

array[n] vector[2] xy;

 

と宣言している。

2変量正規分布(https://mc-stan.org/docs/functions-reference/distributions_over_unbounded_vectors.html)のパラメータは、[0, 1]上の一様分布を事前分布とするものを再スケールしている。これは、事前分布において変域を大きくとる

 

vxmu ~ uniform(0,100); 

 

と、以下のような警告が出るからである。

 

Warning in '/tmp/httpstan_nexxzom0/model_vtzo6piu.stan', line 31, column 25: Argument

    100 suggests there may be parameters that are not unit scale; consider

    rescaling with a multiplier (see manual section 22.12).

 

再スケーリングにおいて、rhoの範囲をー1と+1を含まないように

 

rho = -0.9999 + vrho*1.9998;

 

としているのは、分散共分散行列が非特異行列になるのを避けるためである。

リスト3.1のスクリプトは、MCMCサンプルをファイルに保存して、実行が終了する。

 

リスト3.1のスクリプトにより保存されたMCMCサンプルは、Windows上で実行されるリスト3.2のスクリプトにより読み込んで分析される。

 

 

リスト3.2 MCMCサンプルの分析

import numpy as np

import pickle

import matplotlib.pyplot as plt

import seaborn as sb

import scipy.stats as ss

"""

       MCMCサンプルの読み込み

"""

with open('xy.pkl', 'rb') as f:

    xy = pickle.load(f)

with open('rhos.pkl', 'rb') as f:

    rhos = pickle.load(f)

with open('xmus.pkl', 'rb') as f:

    xmus = pickle.load(f)

with open('xsgms.pkl', 'rb') as f:

    xsgms = pickle.load(f)

with open('ymus.pkl', 'rb') as f:

    ymus = pickle.load(f)

with open('ysgms.pkl', 'rb') as f:

    ysgms = pickle.load(f)

"""

       事後分布の描画

"""

sb.kdeplot(rhos)

plt.show()

 

sb.kdeplot(xmus)

plt.show()

 

sb.kdeplot(ymus)

plt.show()

 

sb.kdeplot(xsgms)

plt.show()

 

sb.kdeplot(ysgms)

plt.show()

"""

        データのプロットと

        推定された2変量正規分布のグラフ

"""

rho = np.median(rhos)

xmu = np.median(xmus)

xsgm = np.median(xsgms)

ymu = np.median(ymus)

ysgm = np.median(ysgms)

 

mins = np.min(xy, axis=0) - np.array([2, 2])

maxs = np.max(xy, axis=0) + np.array([2, 2])

x_range = np.linspace(mins[0], maxs[0], 100)

y_range = np.linspace(mins[1], maxs[1], 100)

X, Y = np.meshgrid(x_range, y_range)

xypos = np.dstack((X, Y))

Z = ss.multivariate_normal.pdf(xypos, [xmu, ymu],

                               [[xsgm**2, xsgm*ysgm*rho],

                                [ysgm*xsgm*rho, ysgm**2]])

fig = plt.figure()

ax = fig.add_subplot(111)

ax.plot(xy.T[0], xy.T[1], 'o')      #  データの散布図

ax.contour(x_range, y_range, Z)     #  正規分布の等高線図

plt.title(fr'$\rho$={rho:.3f}', fontsize=20)

plt.show()

 

 

 

リスト3.2のスクリプトを実行すると、まずパラメータrhoの事後分布のグラフが表示される(図3.1)。

3.1

 

3.1Windowを閉じると、パラメータxmuの事後分布のグラフが表示される(図3.2)。

3.2

 

3.2Windowを閉じると、パラメータymuの事後分布のグラフが表示される(図3.3)。

3.3

 

3.3Windowを閉じると、xsgmの事後分布のグラフが表示される(図3.4)。

3.4

 

3.4Windowを閉じると、ysgmの事後分布のグラフが表示っされる(図3.5)。

3.5

 

図3.5Windowを閉じると、図3.6のグラフが表示される。

3.6

 

正規分布の等高線は、各パラメータの事後分布の中央値を点推定値として描かれている(Gelman et al., 2021)。

等高線は、次のコード

 

ax.contour(x_range, y_range, Z)

 

により描いている(https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.contour.html)。

 

3.6Windowを閉じると、スクリプトの実行終了である。

 

 

 

 

 

参考文献

Gelman, A., Carlin, J. B., Stern, H. S., Dunson, D. B., Vehtari, A., & Rubin, D. B. (2014). Bayesian data analysis, 3rd. Ed. CRC Press. https://sites.stat.columbia.edu/gelman/book/BDA3.pdf

Gelman, A., Hill, J., & Vehtari, A. (2021). Regression and other stories. Cambridge University Press.

岡本安晴(2019)「いまさら聞けないPythonでデータ分析」丸善出版

 

 

Home