Julia で階層ベイズモデル

2022-01-03

概要

Julia で GLMJulia で GLMM の続編です。引き続き データ解析のための統計モデリング入門 (通称緑本) に沿ってやっていきます。

理論的背景の解説が主目的でないので、説明なしで専門用語を使うことがあります。

準備

Stan

MCMC ソフトウェアとして Stan を使います。

cmdstanwiki にある通りにインストールします。

# 適当なディレクトリに clone
$ cd ~/stan
$ git clone https://github.com/stan-dev/cmdstan.git --recursive
$ cd cmdstan

# make コマンドでビルド
$ meke build -j4

# 環境変数を設定
$ export JULIA_CMDSTAN_HOME=$HOME/stan/cmdstan

Julia

Julia から Stan を扱うにはいくつかのパッケージを使います。

  • StanSample
  • MCMCChains

コード例は Stan.jl にまとまっているので参考にすると良いでしょう。

Bayesian GLM

GLM や GLMM は探索的にコードを実行してたので REPL で事足りましたが、ベイズモデリングはまとまったコードが必要となるため、ファイルに書くことにします。

データは引き続き badhealth を使うことにします。Julia で GLM の記事で以下のようにモデリングしましたが、これをベイズ化します。

glm(@formula(NumVisit ~ Age + BadH), badhealthData, Poisson(), LogLink())

単純に回帰係数に事前分布を与えるだけです。とはいっても stan コード上では明示的に事前分布を書かず、improper prior を使うことにします。

data {
  int<lower=1> N;
  array[N] int<lower=1> Age;
  array[N] int<lower=0, upper=1> BadH;
  array[N] int<lower=0> NumVisit;
}

parameters {
  real alpha;
  real beta1;
  real beta2;
}

model {
  for (n in 1:N) {
    NumVisit[n] ~ poisson(exp(alpha + beta1 * Age[n] + beta2 * BadH[n]));
  }
}

モデリングに必要なコードを関数にまとめたのが以下です。GLMM のベイズ化でも使い回します。

# hierarchical_bayes.jl
using StanSample, MCMCChains, RDatasets, StatsPlots, DataFrames, CategoricalArrays

function load_data()
    df = dataset("COUNT", "badhealth")

    data = Dict(pairs(eachcol(df)))
    data = merge(data, Dict(:N => length(data[:NumVisit])))
    return df, data
end

function read_stan_model(filename::String)
    f = open(filename, "r")
    model = read(f, String)
    close(f)
    return SampleModel(filename, model)
end

function mcmc(
    sample_model::SampleModel, data::Dict, num_warmups::Int, num_samples::Int, save_warmup::Bool;
    num_chains::Int=4
)
    stan_sample(
        sample_model,
        data=data,
        num_chains=num_chains,
        num_warmups=num_warmups,
        num_samples=num_samples,
        save_warmup=save_warmup
    )
    return read_samples(sample_model, :mcmcchains)
end

function plot_glm_prediction(df::DataFrame, chns::Chains)
    # ひとつめの chain のサンプルだけを取り出し Matrix に変換
    # append_chains を false にすることで、chain をひとまとめにしないようにしてくれる
    samples = Array(chns, append_chains=false)[1]

    goodHlines = map(eachrow(samples)) do coefs
        x -> exp([1, x, 0]' * coefs)
    end
    badHlines = map(eachrow(samples)) do coefs
        x -> exp([1, x, 1]' * coefs)
    end

    plot(
        df.Age,
        df.NumVisit,
        group=df.BadH,
        t=:scatter,
        xaxis="Age",
        yaxis="NumVisit",
        legend=nothing,
    )
    plot!(goodHlines, color=:blue, alpha=0.05)
    plot!(badHlines, color=:red, alpha=0.05)
end

function plot_glmm_prediction(df::DataFrame, chns::Chains)
    samples = Array(chns, append_chains=false)[1]

    # ランダム効果 r のサンプルを取り除く
    samples = samples[:, [1, 2, 3, end]]

    goodHlines = map(eachrow(samples)) do coefs
        x -> exp([1, x, 0, 1]' * coefs)
    end
    badHlines = map(eachrow(samples)) do coefs
        x -> exp([1, x, 1, 1]' * coefs)
    end

    plot(
        df.Age,
        df.NumVisit,
        group=df.BadH,
        t=:scatter,
        xaxis="Age",
        yaxis="NumVisit",
        legend=nothing,
    )
    plot!(goodHlines, color=:blue, alpha=0.05)
    plot!(badHlines, color=:red, alpha=0.05)
end

最後の plot 関数により以下のような画像が出力されるはずです。初期値にもよるでしょうが、少なくとも 200 サンプル以降では定常分布に収束してそうですね。

GLMモデルからのサンプリングの収束具合

定常分布からのサンプルは数百から数千は必要でしょうが、ここでは図示に使いたいので 100 だけにしてみます。事後分布からの 100 個のサンプルから回帰曲線 (exp にかませているので曲線) を 100 本生成し、図示してみます。それが plot_glm_prediction 関数です。散布図も一緒にプロットしています。

julia> chns = mcmc(sm, data, 200, 100, false)
julia> plot_glm_prediction(df, chns)

すると以下のような画像が得られます。事後分布の分散がとても小さいため、ほとんど 1 本の曲線かのように寄り集まっています。

GLMモデルの予測回帰曲線群

Bayesian GLMM

続いて GLMM をベイズ化していきます。前回の記事 では個人差をモデルに組み込むために以下のように GLMM によりモデリングをしました。

data.Id = categorical(1:(length(data.NumVisit)))
model = fit(MixedModel, @formula(NumVisit ~ Age + BadH + (1|Id)), data, Poisson(), LogLink())

ランダム効果である個人差は平均 0 の正規分布に従うとしています。今回は分散パラメータに事前分布を与えることで階層ベイズモデルにします。stan コードは以下です。

# glmm.stan

data {
  int<lower=1> N;
  array[N] int<lower=1> Age;
  array[N] int<lower=0, upper=1> BadH;
  array[N] int<lower=0> NumVisit;
}

parameters {
  real alpha;
  real beta1;
  real beta2;
  vector[N] r;
  real<lower=0> s;
}

model {
  s ~ gamma(2, 0.1);
  for (n in 1:N) {
    r[n] ~ normal(0, s);
    NumVisit[n] ~ poisson(exp(alpha + beta1 * Age[n] + beta2 * BadH[n] + r[n]));
  }
}

s の事前分布にガンマ分布を設定していますが、ガンマ分布のパラメータについては こちら を参考に設定しました。いろいろ変えて試すなら、外から渡すようにしたほうが良いかもしれませんね。

まずは先ほどと同じように、warmup も含めて収束の様子を見てみましょう。chns の中にはランダム効果のサンプルも含まれているため、plot 関数には図示したいパラメータだけを渡すようにしてください。

julia> sm = read_stan_model("glmm.stan");
julia> chns = mcmc(sm, data, 200, 200, true);
julia> plot(chns, [:alpha, :beta1, :beta2, :s])
GLMMモデルからのサンプリングの収束具合

GLM のときと同じように warmup は 200 サンプルくらいあれば十分そうですね。こちらも回帰曲線群を図示してみます。

julia> chns = mcmc(sm, data, 200, 100, false);
julia> plot_glmm_prediction(df, chns)
GLMMモデルの予測回帰曲線群

さいごに

GLM と GLMM をそれぞれベイズ化するための一連のコードを見てきました。単に本を読むだけではなく実際に手を動かすことでより理解が深まったように思います。今後は社内にあるデータで遊んでみようと思います。