引き続き「Pythonで体験するベイズ推論」のJulia+Mambaによる実装に挑戦している。わざわざ特別Mediumに書くような題材は無いな、と思っていたのだが、第5章の「例題 : テレビ番組 “The Price Is Right”の最適化」のモデリング( pm.potential が出てくるところ)でちょっと詰まったので、Mambaでの実装について記しておく。

問題を単純化すると、

  1. 二つの賞品A, Bの合計価格(これを真の価格と今後呼ぶ)を予想したい
  2. 真の価格は正規分布 \( \text{Normal}(35000, 7500^2) \) に従うと仮定する
  3. 賞品A, Bの価格の事前分布はそれぞれ正規分布 \( \text{Normal}(12000, 3000^2) \) , \( \text{Normal}(3000,500^2) \) に従うと仮定する このような条件のモデリングである。

実際にモデリングをやってみて、賞品A, Bの事前分布と、その和をモデリングするところまでは下のようにすればいいので簡単であるのだが、( using 等は略した)真の価格の分布とサンプリングした賞品A, Bの価格の分布の和を結びつける段階で、はて???となった。

data = Dict{Symbol, Any}(
    :data_mu => [3e3, 12e3],
    :data_std => [5e2, 3e3],
    :mu_prior => 35e3,
    :std_prior => 75e2,
)

model = Model(
    prize = Stochastic(1, (data_mu, data_std) 
        -> MvNormal(data_mu, data_std)),
    price_estimate = Logical(prize -> sum(prize)),
)

モデルを下のようにしてしまうと、

model = Model(
    true_price = Stochastic((mu_prior, std_prior) -> Normal(mu_prior, std_prior)),
    prize = Stochastic(1, (data_mu, data_std) -> MvNormal(data_mu, data_std)),
    price_estimate = Logical(prize -> sum(prize)),
)

price_estimatetrue_price が結びつかないのである。

本文のコードを見てみると、 @pm.potential というデコレーターが付いた関数が使われている。(PyMC3では pm.Potential )

@pm.potential
def error(true_price=true_price, price_estimate=price_estimate):
    return pm.normal_like(true_price, price_estimate, 1 / (3e3)**2)

要は毎回真の価格の分布からサンプリングされている観測値 true_price を \( \text{Normal}( \) price_estimate \(, 3000^2) \) という分布に当てはめた時の尤度を制約条件として加えるということである。

参考にした pm.potential についてのStackExchangeの質問:

What is pm.Potential in PyMC3? https://stats.stackexchange.com/questions/251280/what-is-pm-potential-in-pymc3

\

pm.Potential() much needed explanation for newbie https://discourse.pymc.io/t/pm-potential-much-needed-explanation-for-newbie/2341

Mambaに pm.potential に該当する機能が見つからなかったので、ユーザー定義の分布を定義することで制約条件を実装した。

true_price に対して計算される尤度が通常の \( \text{Normal}(\mu, \sigma^2) \) に対応する尤度と今回考えたい制約に対応する \( \text{Normal}( \) price_estimate \(, 3000^2) \) の尤度の話になるような分布にすれば良いので、

@everywhere extensions = quote
    using Distributions
    import Distributions: minimum, maximum, logpdf
    
    mutable struct TruePriceDist <: ContinuousUnivariateDistribution
        mu::Float64
        sig::Float64
        price_estimate::Float64
    end
    
    minimum(d::TruePriceDist) = -Inf
    maximum(d::TruePriceDist) = Inf
    
    function logpdf(d::TruePriceDist, x::Real)
       logpdf(Normal(d.mu, d.sig), x) + logpdf(Normal(d.price_estimate, 3000), x)
    end
end
@everywhere eval(extensions)

と新しく分布を定義する。こうすると logpdf が正規化されていないものになる(pdfを積分して1にならない)がMambaで使う分布としては問題は起こらない。

モデルはこの TruePriceDist を使って、

model = Model(
    true_price = Stochastic(
        (mu_prior, std_prior, price_estimate) -> 
        TruePriceDist(mu_prior, std_prior, price_estimate)
        ),
    prize = Stochastic(1, (data_mu, data_std) -> MvNormal(data_mu, data_std)),
    price_estimate = Logical(prize -> sum(prize)),
)

とすれば良い。

事後分布を確認すると、本と同様の結果がきちんと得られていた。

事後分布

サンプリングの詳細、期待損失の計算や最小化に関しては下のipynb参照。

https://nbviewer.jupyter.org/github/matsueushi/bayesian_methods_julia/blob/master/chapter5_thepriceisright.ipynb