PyMCでの階層モデルの実装例

概要

PyMC5で階層モデルの実装例を紹介します。公式ドキュメントだけではわかりにくかったり、比較的情報の多いPyMC3と仕様が違う部分も多々あるので、実装におけるポイントになった部分を整理して紹介したいと思います。

使用環境

OS Windows 11
Python 3.10.9
PyMC 5.1.1

環境構築には conda コマンドを使用しています。後述するモデルの図化のため、 python-graphviz もインストールしています。

conda create -n pymc python=3.10
conda activate pymc
conda install conda-forge::pymc
conda install conda-forge::python-graphviz

階層モデルの実装

階層モデルとして『StanとRでベイズ統計モデリング』の「8.4 ロジスティック回帰の階層モデル」の例題を題材にします。この例題では授業での学生の出席の有無を予測するモデルを構築しています。

書籍はRStanで実装されていますが、サポートページにある以下のコードとデータを利用し、同様の階層モデルをPyMCで実装しました。

実装したコードは以下の通りです。

import arviz as az
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import pymc as pm

az.style.use("arviz-docgrid")


class BeyesModel(pm.Model):
    def __init__(self, d1, d2, *args, **kwargs):
        super().__init__(*args, **kwargs)

        N = len(d1)
        C = len(d2["CourseID"].unique())
        I = len(d2)

        self.add_coord(name="person", values=[f"PID{i+1:02}" for i in range(N)])
        self.add_coord(name="course", values=[f"CID{i+1:02}" for i in range(C)])
        self.add_coord(name="record", values=[f"Record{i+1:04}" for i in range(I)])

        score = pm.MutableData(name="Score", value=d1["Score"], dims="person")
        a = pm.MutableData(name="A", value=d1["A"], dims="person")
        w = pm.MutableData(name="Weather", value=d2["Weather"], dims="record")

        b1 = pm.Flat(name="b1")
        b2 = pm.Flat(name="b2")
        b3 = pm.Flat(name="b3")
        b4 = pm.Flat(name="b4")

        s_P = pm.HalfCauchy(name="s_P", beta=10.0)
        b_P = pm.Normal(name="b_P", mu=0.0, sigma=s_P, dims="person")
        x_P = pm.Deterministic(name="x_P", var=b2 * a + b3 * score + b_P, dims="person")

        s_C = pm.HalfCauchy(name="s_C", beta=10.0)
        b_C = pm.Normal(name="b_C", mu=0.0, sigma=s_C, dims="course")
        x_C = pm.Deterministic(name="x_C", var=b_C, dims="course")

        x_J = pm.Deterministic(name="x_J", var=b4 * w, dims="record")
        x = pm.Deterministic(name="x", var=b1 + x_P[d2["PersonID"] - 1] + x_C[d2["CourseID"] - 1] + x_J, dims="record")

        pm.Bernoulli(name="Y", logit_p=x, observed=d2["Y"], dims="record")


def main():
    seed = 0
    rng = np.random.default_rng(seed)

    d1 = pd.read_csv("data-attendance-4-1.txt")
    d2 = pd.read_csv("data-attendance-4-2.txt")

    d1["Score"] = d1["Score"] / 200.0
    d2["Weather"] = d2["Weather"].map({"A": 0.0, "B": 0.2, "C": 1.0})

    model = BeyesModel(d1, d2)
    gv = pm.model_to_graphviz(model)
    gv.render(outfile="model.png")

    idata = pm.sample(
        draws=2000,
        tune=1000,
        random_seed=rng,
        model=model,
    )

    df_summary = az.summary(
        idata,
        var_names=["b1", "b2", "b3", "b4", "s_P", "s_C", "b_C"],
        kind="stats",
        hdi_prob=0.95,
    )
    df_summary.to_csv("summary.csv")

    az.plot_forest(
        idata,
        var_names=["b1", "b2", "b3", "b4", "s_P", "s_C", "b_C"],
        combined=True,
        hdi_prob=0.95,
        figsize=(8, 6),
    )
    plt.savefig("forestplot.png")


if __name__ == "__main__":
    main()

実装のポイント

継承クラスによるモデルの宣言

pymc.Model を継承したクラスによりモデルを宣言しています。階層モデルでは扱う変数が多く、個別のクラスとすることで名前空間の管理が楽になります。

継承クラスによるモデルの宣言についてはこちらの記事で紹介しています。

モデルの図化

モデルは pymc.model_to_graphviz により Digraph オブジェクトを生成することで、ファイル出力を行う render() メソッドが利用できます。

gv = pm.model_to_graphviz(model)
gv.render(outfile="model.png")

出力したモデルの図は以下の通りです。

モデルの図化は宣言したモデルが意図通りに構成されているか確認するのに便利です。

dims による次元の設定

モデルが有する次元やインデックスに命名できる機能として coordsdims があります。

pymc.Model のコンストラクタの coords 引数や、 add_coord() メソッドにより次元やインデックスを定義することができます。実装したモデルでは、学生、授業、出席データの次元があるので、以下のように定義しています。

self.add_coord(name="person", values=[f"PID{i+1:02}" for i in range(N)])
self.add_coord(name="course", values=[f"CID{i+1:02}" for i in range(C)])
self.add_coord(name="record", values=[f"Record{i+1:04}" for i in range(I)])

変数の宣言の際に、ここで定義した次元名を dims で指定することで、各次元のサイズやインデックスを指定することができます。

s_P = pm.HalfCauchy(name="s_P", beta=10.0)
b_P = pm.Normal(name="b_P", mu=0.0, sigma=s_P, dims="person")
x_P = pm.Deterministic(name="x_P", var=b2 * a + b3 * score + b_P, dims="person")

このように次元を指定しておくと、前述のモデルの図化において次元の情報が記載されたり、指定した次元と計算結果が一致しない時にエラーが送出されてデバッグしやすくなるなどのメリットがあります。

モデルへのデータ変数の登録

尤度を算出する確率変数に対して observed で観測データを渡すなど、モデル内で確率変数ではないデータも扱う必要があります。numpyの配列やpandasのデータフレームを渡すこともできますが、そういったデータをモデルに登録するためのクラスとして pymc.Data があります。

pymc.Datamutable という引数によりメモリの共有の可否を指定できますが、このオプションを使用する代わりに pymc.ConstantDatapymc.MutableData を利用するのが推奨されています。そのため、この実装でも pymc.MutableData を利用してデータフレームのデータをモデルに登録しています。また、このデータに対しても dims 引数で次元を明示するようにしています。

score = pm.MutableData(name="Score", value=d1["Score"] / 200.0, dims="person")
a = pm.MutableData(name="A", value=d1["A"], dims="person")
w = pm.MutableData(name="Weather", value=d2["Weather"].map({"A": 0.0, "B": 0.2, "C": 1.0}), dims="record")

こうしてデータをモデルに登録することで、モデルの図化において入力データの情報が記載されたり、pymc.set_data を用いてモデル宣言後に動的にデータを変更することができるようになります。

実行結果

正しい計算ができているかを確認するため、出力結果を確認します。

フォレストプロット forestplot.png は以下のようになりました。点で示されるのが事後平均、太い線が50%信用区間、細い線が95%信用区間になります。

summary.csv に出力した数値としては以下の通りです。

mean sd hdi_2.5% hdi_97.5%
b1 0.817 0.597 -0.363 2.001
b2 -0.802 0.153 -1.105 -0.503
b3 1.69 0.586 0.565 2.847
b4 -0.779 0.143 -1.051 -0.495
s_P 0.353 0.089 0.186 0.536
s_C 1.411 0.417 0.769 2.228
b_C[CID01] 0.423 0.529 -0.61 1.484
b_C[CID02] -1.371 0.494 -2.324 -0.392
b_C[CID03] -1.005 0.5 -2.019 -0.035
b_C[CID04] 1.339 0.524 0.288 2.361
b_C[CID05] -0.283 0.494 -1.234 0.712
b_C[CID06] -1.936 0.505 -2.955 -0.969
b_C[CID07] 0.865 0.511 -0.122 1.894
b_C[CID08] -0.556 0.494 -1.556 0.402
b_C[CID09] 1.184 0.526 0.168 2.242
b_C[CID10] 1.265 0.528 0.242 2.342

書籍の図8-9と概ね同等の結果が得られており、意図通り計算できているようです。

参考文献