Home

Stan事始め

PyStan 3 試用記

 

ベイズ分析のプラットフォームとしてStanは使い易いと思われるが、StanPython用ライブラリがPyStanであり、現在PyStan 3がリリースされている。PyStan 3Windowsで利用するときは、WindowsUbuntuをインストールして、Ubuntu上で利用する。

WindowsUbuntuPyStan 3をインストールする方法については、このウェブサイトを参照されたい。これらのインストール手順は、簡単である。

PyStan 3の使用例として、1変量データに正規分布モデルを適用して平均と標準偏差の母数パラメータ値の事後分布を求めるものと、2変量データに多変量正規分布モデルを適用して平均、標準偏差と相関係数を求めるものを作成してみた。、まず、1変量データの場合について説明する。その後、2変量データの場合について説明する。

ベイズ分析は、データからの情報を事後分布として得るものである。分布のサンプル値の代表値としてとして平均値がよく用いられるが、これは統計分析が手計算で行われていた時代におけるものと言え、PCで統計計算が行われる現代では一考されるべき事柄である。Gelmanら(2021は、事後分布の代表値としてサンプルの変動に対する安定性に問題のある平均値ではなく中央値を薦めている。Gelmanら(2021は、事後分布のモードは、データと事前分布に対して最良の全体的適合を与えるとしている(p. 107)。モード(MAP推定値)が精度の観点からよい点推定値であるという報告がある(岡本、2022a, b)。MAPmaximum a posteriory)推定値を求めるときは、モード付近のKDEkernel density estimate)密度関数の形状が、山が崩れたような形ではなく、きれいな山形でなければならないが、このためにはサンプル数を多く取る必要があり、それに伴って計算時間も長くなる。ここでは、Gelmanら(2021に倣って、事後分布の代表値として中央値を採用した。

事前分布として無情報事前分布を採用するときは、一様分布、あるいはそれに近いものとして分散の大きい正規分布あるいは指数分布を用いることができるが、PyStan 3では分散が大きすぎるという警告が出されるようになった。特に理由がない限り、事前分布として正規分布あるいは指数分布を用いるが(cf. Gelmanら(2021, p. 124)、大きい分散の分布を設定すると、Stanから警告が提示されるので、まず、警告の提示されない程度の分散である分布を設定して、それをデータの分散に合わせてスケールするという方法を取った。

なお、岡本安晴(著)「いまさら聞けないPythonでデータ分析―多変量解析,ベイズ統計分析(PyStan,PyMC)―」Stan (PyStan 2)の説明を行っている。

 

 

1変量データの場合

 

1変量正規分布モデルに基づくベイズ分析スクリプトをリスト1のように用意した。リスト1は、Anaconda/Ubuntu/WSL/WindowsPython 3.11仮想環境にPyStan 3をインストールして開発した。スクリプトは、WindowsにインストールしたPython IDLEのエディタを用いて編集した。すなわち、スクリプトファイルの編集はWindowsにインストールされたエディタで、スクリプトファイルの実行はWSL上の端末で行った。WindowsのエディタとWSLの端末を同時に開いておけば、スクリプトファイルの編集と実行がシームレスに行える。

スクリプトファイルは、圧縮ファイルSingleFilePS3.zipをダウンロード・展開すれば利用できる。

 

リスト1 1変量正規分布によるベイズ分析スクリプト(OneVar.py

 

import stan

import matplotlib.pyplot as plt

import seaborn as sb

import arviz as az

import numpy as np

 

OneVar_code = """

data {

        int<lower=0> J;

        array[J] real y;

        real mu_p;

        real sgm_p;

}

parameters {

        real mu_t;

        real<lower=0> sigma_t;

}

transformed parameters {

        real mu;

        real sigma;

        mu = sgm_p * mu_t + mu_p;

        sigma = sgm_p * sigma_t + 1.0e-7;

}

model {

        mu_t ~ normal(0.0, 10.0);

        sigma_t ~ exponential(0.1);

        y ~ normal(mu, sigma);

}

"""

 

LData =  [ 44, 44, 56, 59, 44, 44, 63, 60, 34, 58,

           55, 50, 32, 61, 47, 25, 38, 47, 30, 42]

Data = np.array(LData)

mu_p = Data.mean()

sgm_p = np.std(Data)

 

OneVar_dat = { 'J': len(LData), 'y': LData, 'mu_p': mu_p, 'sgm_p': sgm_p}

sm = stan.build(OneVar_code, data = OneVar_dat)

fit = sm.sample()

 

i_data = az.from_pystan(posterior = fit, posterior_model = sm)

smry = az.summary(i_data)

print('Summary =\n',smry)

 

az.plot_trace(i_data, var_names = ['mu', 'sigma'])

plt.show()

 

d_fm = fit.to_frame()

q1_mu, q2_mu, q3_mu = np.percentile(d_fm['mu'], [25, 50, 75])

plt.title('Posterior Distribution of $\mu$' +

          f'\nQ1 = {q1_mu:.2f}, Med.= {q2_mu:.2f}, Q3 = {q3_mu:.2f}')

sb.kdeplot(d_fm['mu'])

plt.xlabel('$\mu$', fontsize = 16)

plt.show()

 

q1_sgm, q2_sgm, q3_sgm = np.percentile(d_fm['sigma'], [25, 50, 75])

plt.title('Posterior Distribution of $\sigma$' +

          f'\nQ1 = {q1_sgm:.2f}, Med.= {q2_sgm:.2f}, Q3 = {q3_sgm:.2f}')

sb.kdeplot(d_fm['sigma'])

plt.xlabel('$\sigma$', fontsize = 16)

plt.show()

 

 

リスト1では、Stanスクリプトは変数OneVar_codeの文字列で与えられている。

配列は、予約語arrayを用いて

 

array[J] real y;

 

と記述されている。

データy[i]が正規分布に従うという記述

 

for (I in 1:N)

    y[i] ~ normal(mu, sigma);

 

は、ベクトル化して

 

        y ~ normal(mu, sigma);

 

と記述されている。

 

正規分布のパラメータmumuは、まず、mu_tsigma_t

 

        mu_t ~ normal(0.0, 10.0);

        sigma_t ~ exponential(0.1);

 

と生成して、それらを次式

 

        mu = sgm_p * mu_t + mu_p;

        sigma = sgm_p * sigma_t + 1.0e-7;

 

でスケーリングしている。

Stanスクリプトのビルドは、次のコードで行っている。

 

sm = stan.build(OneVar_code, data = OneVar_dat)

 

ビルドに基づいたMCMCサンプリングは次のコードで行っている。

 

fit = sm.sample()

 

MCMCサンプリングの結果をInferenceData型に変換してサンプリングの統計量を表示することを以下のコードで行っている。

 

i_data = az.from_pystan(posterior = fit, posterior_model = sm)

smry = az.summary(i_data)

print('Summary =\n',smry)

 

MCMCサンプリングのトレース図を次のコードで行っている。

 

az.plot_trace(i_data, var_names = ['mu', 'sigma'])

plt.show()

 

次のコードでは、MCMCサンプリングのサンプルがPandasDataFrame型オブジェクトとして表される。

 

d_fm = fit.to_frame()

 

MCMCサンプルの統計量やグラフがPnadasDataFrameに対するものとして求めることができる。

 

リスト1のスクリプトをAnaconda/Ubuntu/WSL/WindowsPython 3.11仮想環境において次のコマンドで実行した。

 

 

(py311) ・・・/SingleFilePS3$ python OneVar.py

 

MCMCサンプリングが終わると、トレース図が表示される(図1)。

 

図1

 

図1のフォームを閉じると、パラメータの事後分布が表示される(図2)。

図2

 

図2のフォームを閉じると、パラメータの事後分布が表示される(図3)。

図3

 

図3のフォームを閉じると、プログラムの実行終了である。

端末画面には、MCMCサンプリングの統計量が以下のように表示されている。

 

Summary =

            mean     sd  hdi_3%  hdi_97%  mcse_mean  mcse_sd  ess_bulk  ess_tail  r_hat

mu_t      0.002  0.254  -0.481    0.491      0.006    0.005    2272.0    2091.0   1.00

sigma_t   1.099  0.197   0.769    1.465      0.005    0.004    1586.0    1576.0   1.01

mu       46.670  2.747  41.435   51.968      0.060    0.043    2272.0    2091.0   1.00

sigma    11.903  2.134   8.325   15.864      0.056    0.041    1586.0    1576.0   1.01

 

 

Stanスクリプトファイルを独立して作成する場合

 

リスト1では、StanスクリプトがPythonスクリプト内において文字列として与えられている。StanスクリプトをPythonスクリプトのファイルとは別のファイルとして用意した場合は、Stanスクリプトファイルを読み込んで文字列としてからbuildの第1引数として与えればよい。

いま、Stanスクリプトをリスト2のように用意したとする。これらのファイルは、圧縮ファイルMultiFilesPS3.zipとした。ダウンロード・展開して自由に利用されたい。

 

 

リスト2 Stanスクリプトファイル(OneVar.stan

 

data {

        int<lower=0> J;

        array[J] real y;

        real mu_p;

        real sgm_p;

}

parameters {

        real mu_t;

        real<lower=0> sigma_t;

}

transformed parameters {

        real mu;

        real sigma;

        mu = sgm_p * mu_t + mu_p;

        sigma = sgm_p * sigma_t + 1.0e-7;

}

model {

        mu_t ~ normal(0.0, 10.0);

        sigma_t ~ exponential(0.1);

        y ~ normal(mu, sigma);

}

 

 

リスト2のStanスクリプトを利用するPythonスクリプトをリスト3のように作成した。

 

 

リスト3 Stanスクリプトファイルを読み込むPythonスクリプト(OneVar_mf.py

 

import stan

import matplotlib.pyplot as plt

import seaborn as sb

import arviz as az

import numpy as np

 

LData =  [ 44, 44, 56, 59, 44, 44, 63, 60, 34, 58,

           55, 50, 32, 61, 47, 25, 38, 47, 30, 42]

Data = np.array(LData)

mu_p = Data.mean()

sgm_p = np.std(Data)

 

OneVar_dat = { 'J': len(LData), 'y': LData, 'mu_p': mu_p, 'sgm_p': sgm_p}

with open('OneVar.stan', 'r') as fstan:

    sm = stan.build(fstan.read(), data = OneVar_dat)

fit = sm.sample()

 

i_data = az.from_pystan(posterior = fit, posterior_model = sm)

smry = az.summary(i_data)

print('Summary =\n',smry)

 

az.plot_trace(i_data, var_names = ['mu', 'sigma'])

plt.show()

 

d_fm = fit.to_frame()

q1_mu, q2_mu, q3_mu = np.percentile(d_fm['mu'], [25, 50, 75])

plt.title('Posterior Distribution of $\mu$' +

          f'\nQ1 = {q1_mu:.2f}, Med.= {q2_mu:.2f}, Q3 = {q3_mu:.2f}')

sb.kdeplot(d_fm['mu'])

plt.xlabel('$\mu$', fontsize = 16)

plt.show()

 

q1_sgm, q2_sgm, q3_sgm = np.percentile(d_fm['sigma'], [25, 50, 75])

plt.title('Posterior Distribution of $\sigma$' +

          f'\nQ1 = {q1_sgm:.2f}, Med.= {q2_sgm:.2f}, Q3 = {q3_sgm:.2f}')

sb.kdeplot(d_fm['sigma'])

plt.xlabel('$\sigma$', fontsize = 16)

plt.show()

 

 

リスト3では、次のコードにより、リスト2のファイルを文字列として、stan.buildの第1引数としている。

 

with open('OneVar.stan', 'r') as fstan:

    sm = stan.build(fstan.read(), data = OneVar_dat)

 

上のコード以外は、リスト1と同じである。

次のコマンドで実行すると、リスト1の場合と同じように実行が進む。

 

(py311) ・・・/MultiFilesPS3$ python OneVar_mf.py

 

 

 

2変量データの場合

多変量正規分布の簡単な場合として、2変量正規分布を取り上げる。

ベイズ分析のためのStanスクリプトをリスト4のように作成した。これらのファイルは、まとめて圧縮ファイルBiVariateNormalPS3.zipとした。ダウンロード・展開して自由に利用されたい。

 

 

リスト4 2変量正規分布モデルのStanスクリプト(TwoVarNormal.stan

 

data {

        int N;

        array[N] vector[2] Data;

        real m1;

        real m2;

        real sd1;

        real sd2;

}

parameters {

        real p_mu1;

        real p_mu2;

        real<lower = 0.0> p_sgm1;

        real<lower = 0.0> p_sgm2;

        real p_r;

}

transformed parameters {

        vector[2] mu;

        real sgm1;

        real sgm2;

        matrix[2, 2] cov;

        real<lower=-1, upper=1> r;

        r = (inv_logit(p_r) - 0.5) * 2;

        mu[1] = p_mu1 * sd1 + m1; 

        mu[2] = p_mu2 * sd2 + m2; 

        sgm1 = p_sgm1 * sd1 + 0.001;

        sgm2 = p_sgm2 * sd2 + 0.001;

        cov[1][1] = sgm1 * sgm1;

        cov[1][2] = sgm1 * sgm2 * r;

        cov[2][1] = sgm1 * sgm2 * r;

        cov[2][2] = sgm2 * sgm2;

}

model {

        p_mu1 ~ normal(0.0, 10.0); 

        p_mu2 ~ normal(0.0, 10.0); 

        p_sgm1 ~ exponential(0.1); 

        p_sgm2 ~ exponential(0.1);

        p_r ~ normal(0.0, 5.0);

        Data ~ multi_normal(mu, cov);

}

 

2変量データの配列は、予約語arrayを用いて次のように宣言されている。

 

        array[N] vector[2] Data;

 

Dataの個々のData[i]の分布は、for文ではなく、ベクトル化されたコードで記述されている。

 

        Data ~ multi_normal(mu, cov);

 

パラメータmucovを設定するパラメータsgm1sgm2は、次のスケーリングにより与えられている。

 

        mu[1] = p_mu1 * sd1 + m1; 

        mu[2] = p_mu2 * sd2 + m2; 

        sgm1 = p_sgm1 * sd1 + 0.001;

        sgm2 = p_sgm2 * sd2 + 0.001;

 

スケーリングのもとのパラメータ値は次のコードで与えられ、これらはパラメータの分布の「分散が大きすぎる」という警告を避けるように設定されている。

 

        p_mu1 ~ normal(0.0, 10.0); 

        p_mu2 ~ normal(0.0, 10.0); 

        p_sgm1 ~ exponential(0.1); 

        p_sgm2 ~ exponential(0.1);

 

相関係数rは、次のコードによるp_rの変換値として与えられている。

 

        r = (inv_logit(p_r) - 0.5) * 2;

 

相関係数rの事前分布を区間上の一様分布で与えると、一様分布は数値微分で問題を起こす可能性があるという警告が出されるので、正規分布に従う値p_rを変換している。

リストStanスクリプトを用いて2変量正規分布モデルによるベイズ分析を行うPythonスクリプト例をリスト5のように用意した。

 

リスト5 2変量正規分布モデルによるベイズ分析Pythonスクリプト(BiVariateNormal.py

 

import numpy as np

import stan

import matplotlib.pyplot as plt

import seaborn as sb

import arviz as az

 

Data = [

 [179, 121], [185, 89], [181, 106], [210, 100], [202, 104],

 [197, 91], [227, 107], [208, 101], [188, 99], [189, 112],

 [240, 111], [203, 103], [202, 85], [190, 95], [221, 96],

 [150, 83], [205, 103], [177, 90], [221, 92], [172, 86],

 [190, 87], [218, 101], [205, 100], [208, 120], [224, 98],

 [199, 116], [203, 96], [213, 91], [200, 109], [190, 100],

 [211, 115], [204, 85], [185, 92], [178, 86], [231, 122],

 [206, 84], [197, 93], [236, 93], [213, 106], [205, 103],

 [212, 102], [177, 86], [193, 96], [183, 93], [204, 87],

 [203, 108], [203, 103], [180, 100], [196, 101], [167, 75],

 [202, 106], [182, 98], [225, 109], [219, 115], [205, 102],

 [200, 99], [219, 94], [189, 102], [220, 86], [186, 102],

 [203, 101], [210, 124], [254, 103], [208, 95], [192, 106],

 [176, 91], [173, 97], [195, 100], [189, 98], [220, 115],

 [202, 112], [257, 108], [207, 104], [199, 96], [173, 98],

 [230, 101], [230, 111], [212, 105], [196, 82], [183, 82],

 [230, 103], [207, 112], [225, 101], [192, 104], [175, 83],

 [223, 105], [192, 106], [202, 87], [204, 87], [184, 97],

 [193, 94], [204, 109], [196, 92], [216, 109], [219, 113],

 [256, 117], [210, 86], [217, 103], [206, 87], [213, 105] ]

 

mu = np.mean(Data, axis = 0)

print('mu =\n', mu)

cov = np.cov(np.array(Data).T)

print('cov =\n', cov)

sgms = [cov[0][0]**0.5, cov[1][1]**0.5]

print('sgm =\n', sgms)

 

 

def f_init():

        return {'p_mu1': 0.0, 'p_mu2': 0.0,

                'p_sgm1': 1.0, 'p_sgm2': 1.0, 'p_r': 0.0}

       

stan_data = {'N': len(Data), 'Data': Data, 'm1': mu[0], 'm2': mu[1],

             'sd1': sgms[0], 'sd2': sgms[1]}

with open('TwoVarNormal.stan', 'r') as fstan:

        sm = stan.build(fstan.read(), data = stan_data)

fit = sm.sample(num_chains = 4,  init = [f_init()]*4)

 

i_data = az.from_pystan(posterior = fit, posterior_model = sm)

smry = az.summary(i_data)

print('Summary =\n',smry)

 

az.plot_trace(i_data, var_names = ['mu', 'sgm1', 'sgm2', 'r'])

plt.show()

 

d_fm = fit.to_frame()

print('d_fm keys =\n', d_fm.keys())

 

samples_mu1 = d_fm['mu.1'] 

samples_mu2 = d_fm['mu.2']

med_mu1 = np.median(samples_mu1)

med_mu2 = np.median(samples_mu2)

plt.title('Posterior Distributions', fontsize = 20)

sb.kdeplot(samples_mu1, label = f'$\mu_1$ \nmed.={med_mu1:.2f}')

sb.kdeplot(samples_mu2, label = f'$\mu_2$ \nmed.={med_mu2:.2f}')

plt.xlabel('')

plt.legend(fontsize = 14)

plt.show()

 

samples_sgm1 = d_fm['sgm1'] 

samples_sgm2 = d_fm['sgm2']

med_sgm1 = np.median(samples_sgm1)

med_sgm2 = np.median(samples_sgm2)

plt.title('Posterior Distributions', fontsize = 20)

sb.kdeplot(samples_sgm1, label = f'$\sigma_1$ \nmed.={med_sgm1:.2f}')

sb.kdeplot(samples_sgm2, label = f'$\sigma_2$ \nmed.={med_sgm2:.2f}')

plt.xlabel('')

plt.legend(fontsize = 14)

plt.show()

 

samples_r = d_fm['r'] 

med_r = np.median(samples_r)

plt.title('Posterior Distributions', fontsize = 20)

sb.kdeplot(samples_r, label = f'r \nmed.={med_r:.2f}')

plt.xlabel('r', fontsize = 20)

plt.legend(fontsize = 14)

plt.show()

 

 

リスト5では、MCMCサンプリング時に次のコードでパラメータの初期値を指定している。

 

fit = sm.sample(num_chains = 4,  init = [f_init()]*4)

 

これは、サンプリングにおいて不適切な値が生じた旨のInformation Messageが表示される回数を減らすためである。共分散行列のような制約の強いパラメータの場合、このようなメッセージが時々出るのはサンプリングがよい証拠であると説明されている。初期値の指定は、各マルコフ鎖ごとに設定するので、マルコフ鎖の数を4と明示して、それに合わせて初期値の指定を行っている。

リスト5のスクリプトを、Anaconda/Ubuntu/WSL/WindowsPython 3.11仮想環境において次のコマンドで実行した。

 

(py311) ・・・/BiVariateNormalPS3$ python BiVariateNormal.py

 

実行されると、MCMCサンプリング後、サンプリングのトレース図が表示される(図4)。

図4

 

図4のフォームを閉じると、平均値パラメータの事後分布のグラフが表示される(図5)。

図5

 

図5のフォームを閉じると、標準偏差パラメータの事後分布が表示される(図6)。

図6

 

図6のフォームを閉じると、相関係数の事後分布のグラフが表示される(図7)。

図7

 

図7のフォームを閉じると、スクリプトの実行終了である。

実行終了後の端末の画面には以下の出力がある。

 

 

Summary =

              mean      sd   hdi_3%  hdi_97%  mcse_mean  mcse_sd  ess_bulk  ess_tail  r_hat

p_mu1       0.002   0.101   -0.180    0.198      0.002    0.002    4141.0    2613.0    1.0

p_mu2       0.002   0.102   -0.178    0.199      0.002    0.001    4054.0    3314.0    1.0

p_sgm1      1.018   0.074    0.880    1.153      0.001    0.001    4297.0    3055.0    1.0

p_sgm2      1.016   0.074    0.886    1.158      0.001    0.001    3969.0    3012.0    1.0

p_r         0.927   0.202    0.551    1.303      0.003    0.002    3512.0    2971.0    1.0

mu[0]     203.050   1.923  199.565  206.800      0.030    0.021    4141.0    2613.0    1.0

mu[1]      99.662   1.035   97.826  101.665      0.016    0.011    4054.0    3314.0    1.0

sgm1       19.477   1.414   16.848   22.072      0.022    0.015    4297.0    3055.0    1.0

sgm2       10.342   0.750    9.011   11.785      0.012    0.009    3969.0    3012.0    1.0

cov[0,0]  381.356  55.892  283.346  486.657      0.863    0.620    4297.0    3055.0    1.0

cov[0,1]   87.290  22.444   48.330  131.030      0.394    0.289    3349.0    2627.0    1.0

cov[1,0]   87.290  22.444   48.330  131.030      0.394    0.289    3349.0    2627.0    1.0

cov[1,1]  107.519  15.736   81.189  138.891      0.253    0.181    3969.0    3012.0    1.0

r           0.429   0.082    0.270    0.574      0.001    0.001    3512.0    2971.0    1.0

d_fm keys =

 Index(['lp__', 'accept_stat__', 'stepsize__', 'treedepth__', 'n_leapfrog__',

       'divergent__', 'energy__', 'p_mu1', 'p_mu2', 'p_sgm1', 'p_sgm2', 'p_r',

       'mu.1', 'mu.2', 'sgm1', 'sgm2', 'cov.1.1', 'cov.2.1', 'cov.1.2',

       'cov.2.2', 'r'],

      dtype='object', name='parameters')

 

 

前半の統計量の出力は、コード

 

i_data = az.from_pystan(posterior = fit, posterior_model = sm)

smry = az.summary(i_data)

print('Summary =\n',smry)

 

によるものである。

後半の「d_fm keys =に続く部分は、コード

 

d_fm = fit.to_frame()

print('d_fm keys =\n', d_fm.keys())

 

によるものである。パラメータcov[0,1]などが、PandasDataFrameでは'cov.1.2'などのキーで表されていることが解る。InferenceData型では添え字は0から始まり、[ ]内にコンマ,で区切って並べられるが、DataFrame型では添え字は1から始まり、ピリオド.で繋いで並べられる。

 

 

 

参考文献

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

岡本安晴(2019)いまさら聞けないPythonでデータ分析――多変量解析、ベイズ統計分析(PyStan, PyMC)――.丸善出版

岡本安晴(2022a)ベイズ分析における点推定値例のRMSEbiasによる比較――実験心理学の観点からの検討――.基礎心理学研究,4111-7https://doi.org/10.14947/psychono.41.1

岡本安晴(2022b)検出力指標d′の推定における実験上の問題.基礎心理学研究、412107-114https://doi.org/10.14947/psychono.41.16

 

 

 

Home