7.2 1次元の簡単なMCMC

Rによる例

密度関数が $1/(1+x^2)$ に比例する乱数(独立である必要はない)を10万個作る問題です。『Rで楽しむベイズ統計入門』に載っているRのプログラムは(ここで使わない採択を数えるカウンタは省略して)次のようになっていました:

N = 100000              # 繰返し回数
a = numeric(N)          # 値を保存するための長さNの配列
x = 0                   # 初期値
p = 1 / (1 + x^2)       # 確率
for (i in 1:N) {
  y = rnorm(1, x, 1)    # 候補(選び方は対称なら何でもよい)
  q = 1 / (1 + y^2)     # 確率
  if (runif(1) < q/p) { # 更新
    x = y
    p = q
  }
  a[i] = x
}

実際にこれをRにコピペしてやってみるとわかるように,今どきのコンピュータなら10万個の計算はほぼ一瞬で終わります。

得られた乱数のヒストグラムを描いてみましょう:

hist(a, freq=FALSE, breaks=seq(-1000,1000,0.2), xlim=c(-5,5), col="gray")

コーシー分布の密度関数を重ね書きしてみましょう:

curve(dcauchy(x), add=TRUE)

ぴったり重なるはずです。結果は,本書の図7.3右のようになります。

Stanを使ってみる

同じことをStanでやってみましょう。

まず,次のようなStanコードを作り,例えば ex72.stan という名前で,Rの作業ディレクトリに保存します。

parameters {
    real x;
}

model {
    target += -log(1 + square(x));
}

テキストエディタで最後の } を打ち込んだ後に Enter キーを打って,カーソルが } の次の行の先頭に行く状態にして,保存してください。

改行で終わらない行は incomplete line と呼ばれ,プログラミングの世界では嫌われます。Stan でもときどき make sure Stan code ends with a blank line というメッセージが出ます。

上のコードは,パラメータとして実数(real は C++ の double に相当)の x を使うことと,モデルとして確率密度の対数(定数項を除く)が $-\log(1+x^2)$ であることを表します。target += は確率密度の対数を追加する命令です。決まり文句ですので,target = target - log(1 + square(x)) とか target -= log(1 + square(x)) とかに書き直してはいけません。square(x)x * xx^2 と同じことです。

そして,例えば次のようにRに打ち込みます:

fit = stan("ex72.stan")

この stan() という関数は,デフォルトでは4本のシミュレーションを(可能なら並行して)実行します(chains=4)。1本あたり,デフォルトでは2000回繰り返しますが,その半分はウォームアップ(バーンイン)です。つまり,1本あたり1000個のサンプル値を作りますので,全部で4000個が得られます。

これを打ち込むと,C++への変換とコンパイルでかなり長い無反応な時間が続きますが,その後で計算の経過が出力されます。最後に警告メッセージも出るかもしれませんが,とりあえず無視します。

rstan_options(auto_write=TRUE) にしている場合,ex72.stan をコンパイルした結果は ex72.rds というファイルに入ります。ex72.stan(のハッシュ値)が変わったら,コンパイルし直してくれるはずです。

fit に代入された結果を表示します:

fit
Inference for Stan model: ex72.
4 chains, each with iter=2000; warmup=1000; thin=1; 
post-warmup draws per chain=1000, total post-warmup draws=4000.

      mean se_mean    sd   2.5%   25%   50%   75% 97.5% n_eff Rhat
x    -0.66    0.47 12.54 -11.53 -1.11 -0.05  0.81  8.57   722 1.00
lp__ -1.25    0.08  1.60  -5.96 -1.64 -0.81 -0.17  0.00   379 1.01

Samples were drawn using NUTS(diag_e) at Sun Feb  4 17:32:10 2018.
For each parameter, n_eff is a crude measure of effective sample size,
and Rhat is the potential scale reduction factor on split chains (at 
convergence, Rhat=1).

x(と lp__ つまり対数事後分布 log posterior)について,平均とその標準誤差,標準偏差,%点,有効サンプルサイズ $n_{\text{eff}}$,それに $\hat{R}$ という統計量が出力されます。これらの詳しい定義は Bayesian Data Analysis, 3rd edition の11.4〜11.5に書かれています。$\hat{R} \approx 1$ なら収束したと考えられます。$n_{\text{eff}}$ が小さければ,そのパラメータの推定誤差が大きいので,後述のようにサンプルサイズを増します。

警告メッセージについては,説明へのリンクがありますので,そちらを見て,対処できるものは対処すればよいでしょう。例えば There were XXX transitions after warmup that exceeded the maximum treedepth. Increase max_treedepth above 10. See http://mc-stan.org/misc/warnings.html#maximum-treedepth-exceeded と出たら,リンクをたどって問題点を見つけ,

fit = stan("ex72.stan", control=list(max_treedepth=20))

のようにオプションでパラメータを変更します。ただ,うまく収束しなかったり警告が出たりすることは,必ずしもパラメータの変更で解消できるわけではありません。

サンプルサイズを増やしてヒストグラムを描く

とりあえずうまくいくようなら,サンプルサイズを大きくしてみましょう。このページの最初に挙げたRコードでは10万でしたので,それに合わせるために,シミュレーション1本あたりの(ウォームアップを含む)繰返し数 iter とウォームアップ(バーンイン)の数 warmup を調整します。例えば

fit = stan("ex72.stan", iter=26000, warmup=1000, control=list(max_treedepth=20))

とすれば,1本あたりのサンプルサイズが 25000 になりますので,4本でちょうど10万個が得られます。

より詳しく調べるために,4本のシミュレーション(ウォームアップを除く)の結果をつなぎ合わせた100000個の xlp__ の値を取り出しましょう:

e = extract(fit)

これで e$x に100000個の x の値が取り出されます。

本書と同様にヒストグラム表示してみましょう。とてつもなく絶対値の大きな値が混じっていることがあるので,どんな場合にもうまくいくように,少し工夫しています。

x = ifelse(abs(e$x) < 100, e$x, 100)  # 絶対値を100以内に止める
hist(x, freq=FALSE, breaks=seq(-100,100,0.2), xlim=c(-5,5), col="gray")
curve(dcauchy(x), add=TRUE)

結果のグラフは本書のRコードによる結果(図7.3右)と同じです。コーシー分布ですから,ときどきとてつもなく絶対値の大きな値が混じります。この部分が,本書のRコードより上のStanの結果のほうが正確です。これは,本書のRコードで次の候補を選ぶ y = rnorm(1, x, 1) の跳び幅が狭く,あまり絶対値の大きな値が出にくいためです。

extract() で取り出したサンプルは,デフォルトではランダムにシャフルされています。シャフルしないそのままの順番で4本のシミュレーション結果をつないだものを得るには,extract()permuted=FALSE というオプションを付けるか,あるいは

e = as.data.frame(fit)

とします。ウォームアップを含むまったくの生の値は fit@sim$samples[[1]]$x から fit@sim$samples[[4]]$x までに入っています(chains=4 の場合)。

その他の注意

関数 stan() の実行は,最初はコンパイルの時間が長くかかりますが,次からはずっと速くなります。ただ,stan() を別の関数の中で使うと,毎回コンパイルが起こってしまい,遅くなります。その場合は例えば

model = stan_model("ex72.stan")

f = function(data) {
  fit = sampling(model, iter=26000, warmup=1000)
  mean(extract(fit)$x)
}

のように stan_model()sampling() に分けて計算します。