PyMCのモデル宣言におけるクラス継承の実装例

概要

PyMCのモデルの宣言において、公式チュートリアル等では、下記のような with ステートメントを使った記法がよく使用されています。

with Model() as model:  # model specifications in PyMC are wrapped in a with-statement
    # Define priors
    sigma = HalfCauchy("sigma", beta=10)
    intercept = Normal("Intercept", 0, sigma=20)
    slope = Normal("slope", 0, sigma=20)

    # Define likelihood
    likelihood = Normal("y", mu=intercept + slope * x, sigma=sigma, observed=y)

    # Inference!
    # draw 3000 posterior samples using NUTS sampling
    idata = sample(3000)

上記の例では、 with ステートメント内で宣言された変数は自動的に model 内の変数として扱われています。

しかし、pymc.Modelドキュメントでも解説されている通り、PyMCでは pymc.Model クラスを継承したモデルの宣言もサポートされています。モデルを個別のクラスとして宣言することで、名前空間を切り分けて記述することができ、保守性の高いコードになります。

この記事では、クラス継承によるPyMCのモデルの宣言の使用例を紹介したいと思います。

使用環境

ここでは以下の環境で検証しています。

OS Windows 11
Python 3.10.9
PyMC 5.1.1

環境構築には conda コマンドを使用しています。PyMC 5.1.1はconda-forgeリポジトリで配布されているので、下記のようにチャンネルを指定することでインストール可能です。

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

※チャンネル指定を行わないと、anacondaリポジトリのPyMC 2.3.8がインストールされます。

with ステートメントを使った実装

まず、基本の実装として、モデル宣言に with ステートメントを使用したコードを実装してみます。公式のGLMチュートリアルを参考に作成しました。

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

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-darkgrid")


def main():
    RANDOM_SEED = 8927
    rng = np.random.default_rng(RANDOM_SEED)

    size = 200
    true_intercept = 1
    true_slope = 2
    true_sigma = 0.5

    x = np.linspace(0, 1, size)
    true_regression_line = true_intercept + true_slope * x
    y = true_regression_line + rng.normal(scale=true_sigma, size=size)
    df = pd.DataFrame(dict(x=x, y=y))
    print(df)

    with pm.Model() as model:
        sigma = pm.HalfCauchy("sigma", beta=10)
        intercept = pm.Normal("Intercept", 0, sigma=20)
        slope = pm.Normal("slope", 0, sigma=20)

        pm.Normal("y", mu=intercept + slope * df.x, sigma=sigma, observed=df.y)

        idata = pm.sample(draws=3000, random_seed=rng)

    az.plot_trace(idata)
    plt.savefig("traceplot.png")


if __name__ == "__main__":
    main()

出力として以下のようなトレースプロットが得られます。

pymc.Model の継承を利用したコード

上記の with ステートメントを使用したコードを、pymc.Model を継承したクラスを宣言して書き換えてみます。実装したコードは以下の通りです。

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-darkgrid")


class GeneralizedLinearModel(pm.Model):
    def __init__(self, df, *args, **kwargs):
        super().__init__(*args, **kwargs)

        sigma = pm.HalfCauchy("sigma", beta=10)
        intercept = pm.Normal("Intercept", 0, sigma=20)
        slope = pm.Normal("slope", 0, sigma=20)

        pm.Normal("y", mu=intercept + slope * df.x, sigma=sigma, observed=df.y)


def main():
    RANDOM_SEED = 8927
    rng = np.random.default_rng(RANDOM_SEED)

    size = 200
    true_intercept = 1
    true_slope = 2
    true_sigma = 0.5

    x = np.linspace(0, 1, size)
    true_regression_line = true_intercept + true_slope * x
    y = true_regression_line + rng.normal(scale=true_sigma, size=size)
    df = pd.DataFrame(dict(x=x, y=y))
    print(df)

    model = GeneralizedLinearModel(df=df)
    idata = pm.sample(draws=3000, random_seed=rng, model=model)

    az.plot_trace(idata)
    plt.savefig("traceplot.png")


if __name__ == "__main__":
    main()

出力のトレースプロットは全く同じものが得られましたので省略します。

実装のポイントとしては、 pm.Model を継承したサブクラス GeneralizedLinearModel を宣言しているところが大きな違いです。ここでは、一般的なクラス継承の処理と同様にコンストラクタでスーパークラスのコンストラクタを呼び出しています。その後の各変数の宣言については with ステートメントで行っていたものと全く同じです。

class GeneralizedLinearModel(pm.Model):
    def __init__(self, df, *args, **kwargs):
        super().__init__(*args, **kwargs)

        sigma = pm.HalfCauchy("sigma", beta=10)
        intercept = pm.Normal("Intercept", 0, sigma=20)
        slope = pm.Normal("slope", 0, sigma=20)

        pm.Normal("y", mu=intercept + slope * df.x, sigma=sigma, observed=df.y)

そしてメイン処理では宣言したサブクラスをインスタンス化し、 model に代入しています。ここで観測データの df を引数としてモデルに渡しています。

model = GeneralizedLinearModel(df=df)

最後のサンプリングですが、with ステートメントを使用しない場合には、以下のように model 引数でインスタンス化したモデルを指定する必要があります。(with ステートメントを使用していた場合ではモデルが自動的に選択されるため指定不要でした。)

idata = pm.sample(draws=3000, random_seed=rng, model=model)

以上のように実装することで、モデルの宣言を個別のクラスで行うことができます。

このシンプルな例では with ステートメントを使用した方がわかりやすいのかもしれませんが、以下の記事で紹介しているような階層モデルなどでは扱う確率変数が増え、変数の管理が煩雑になってきます。その際、モデルを個別のクラスとして宣言しておくことで変数の名前空間が分けられるため、保守しやすいコードが実装できます。