hanalyze-0.2.0.0: src/Hanalyze/Stat/BridgeSampling.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE BangPatterns #-}
-- |
-- Module : Hanalyze.Stat.BridgeSampling
-- Description : Bridge Sampling による周辺尤度 log p(y) 推定 (Meng & Wong 1996)
-- Copyright : (c) 2026 Aelysce Project (Toshiaki Honda)
-- License : BSD-3-Clause
--
-- Bridge Sampling estimator of the marginal likelihood @log p(y)@
-- (Meng & Wong 1996).
--
-- Reference:
--
-- * Meng & Wong (1996) "Simulating ratios of normalising constants
-- via a simple identity: a theoretical exploration". Statistica
-- Sinica 6:831-860.
-- * Gronau, Sarafoglou, Matzke, Ly, Boehm, Marsman, Leslie, Forster,
-- Wagenmakers, Steingroever (2017) "A tutorial on bridge sampling".
-- Journal of Mathematical Psychology 81:80-97.
--
-- ## アルゴリズム (Phase 29-A2)
--
-- 目的: 周辺尤度 @log p(y) = log ∫ p(y|θ) p(θ) dθ@ を、 既存 MCMC chain
-- (posterior samples) と diagonal Gaussian proposal @g(θ)@ から推定する。
--
-- Bridge identity (Meng-Wong):
--
-- @
-- p(y) = E_g[α(θ) q(θ)] / E_p[α(θ) g(θ)]
-- @
--
-- 最適 bridge function @α*(θ) = 1 / (s_1 q(θ) + s_2 r g(θ))@ を使った
-- iterative scheme で @r̂@ を求める:
--
-- @
-- r̂_{t+1} = [(1/N_2) Σ_i q(θ̃_2,i) / (s_1 q(θ̃_2,i) + s_2 r̂_t g(θ̃_2,i))]
-- / [(1/N_1) Σ_j g(θ̃_1,j) / (s_1 q(θ̃_1,j) + s_2 r̂_t g(θ̃_1,j))]
-- @
--
-- ここで:
-- * @θ̃_1@ は proposal @g@ から (本実装では Gaussian fit-to-chain)
-- * @θ̃_2@ は posterior chain サンプル
-- * @s_1 = N_1/(N_1+N_2)@、 @s_2 = N_2/(N_1+N_2)@
-- * @q(θ) = p(y|θ)·p(θ)@ = 'logJoint' の exp 化
--
-- 全計算は **log space** で行い (log-sum-exp 安定化)、 浮動小数 underflow を回避。
--
-- ## SMC との関係 (Phase 29-A1/A2 統合)
--
-- SMC は副産物として log marginal を推定する (= temperature schedule の
-- incremental log-mean-weight 累積)。 Bridge Sampling は MCMC chain + proposal
-- から **独立な推定経路** で求めるので、 両者が 5% 以内で一致すれば妥当性が裏付け。
-- 不一致なら chain の収束不足 / SMC schedule 粗さ / proposal 不適切のサイン。
module Hanalyze.Stat.BridgeSampling
( BridgeConfig (..)
, defaultBridgeConfig
, BridgeResult (..)
, bridgeSampling
) where
import Control.Monad (replicateM, forM)
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import System.Random.MWC (GenIO)
import System.Random.MWC.Distributions (normal)
import Hanalyze.Model.HBM (ModelP, Params, logJoint, sampleNames)
import Hanalyze.MCMC.Core (Chain (..), chainVals)
-- ---------------------------------------------------------------------------
-- Configuration
-- ---------------------------------------------------------------------------
-- | Bridge Sampling 設定。
data BridgeConfig = BridgeConfig
{ bcNProposal :: !Int -- ^ N_1: proposal samples 数 (典型 chain サンプル数と同等)
, bcMaxIter :: !Int -- ^ 反復解の最大回数 (典型 100、 通常 < 20 で収束)
, bcTolerance :: !Double -- ^ 反復収束判定 |Δ log r̂| < tol (典型 1e-6)
} deriving (Show)
defaultBridgeConfig :: BridgeConfig
defaultBridgeConfig = BridgeConfig
{ bcNProposal = 500
, bcMaxIter = 100
, bcTolerance = 1e-6
}
-- | Bridge Sampling 結果。
data BridgeResult = BridgeResult
{ brLogMarginal :: !Double -- ^ 推定 @log p(y)@
, brIterations :: !Int -- ^ 収束に要した反復数
, brConverged :: !Bool -- ^ tol 以内で収束したか
} deriving (Show)
-- ---------------------------------------------------------------------------
-- 公開 API
-- ---------------------------------------------------------------------------
-- | Bridge Sampling で @log p(y)@ を推定。
--
-- 入力:
-- * モデル (logJoint = log q(θ) = log p(y|θ) + log p(θ))
-- * posterior chain (既存 NUTS / MH / SMC 等の結果)
-- * proposal は **diagonal Gaussian fit** to chain (各パラメータの sample
-- mean / SD から構築)
--
-- 出力: log marginal likelihood 推定値 + 収束情報。
bridgeSampling
:: forall r. ModelP r
-> BridgeConfig
-> Chain -- ^ posterior chain
-> GenIO
-> IO BridgeResult
bridgeSampling model cfg chain gen = do
let names = sampleNames model
posterior = chainSamples chain
n2 = length posterior
(mus, sds) = fitDiagGaussian names chain
-- 1. Sample N_1 from proposal g (diagonal Gaussian)
proposal <- replicateM (bcNProposal cfg) (sampleProposal names mus sds gen)
let n1 = length proposal
s1 = fromIntegral n1 / fromIntegral (n1 + n2)
s2 = fromIntegral n2 / fromIntegral (n1 + n2)
-- 2. Precompute log q (logJoint) and log g (proposal log-density)
logq2 = map (logJoint model) posterior
logq1 = map (logJoint model) proposal
logg2 = map (logProposal names mus sds) posterior
logg1 = map (logProposal names mus sds) proposal
-- 3. Iterative solve for log r̂
let (logR, niter, converged) =
iterateBridge cfg logq1 logg1 logq2 logg2 s1 s2 0.0
pure BridgeResult
{ brLogMarginal = logR
, brIterations = niter
, brConverged = converged
}
-- | Meng-Wong iterative formula in log space.
iterateBridge
:: BridgeConfig
-> [Double] -> [Double] -- ^ logq1, logg1 (proposal samples)
-> [Double] -> [Double] -- ^ logq2, logg2 (posterior samples)
-> Double -- ^ s_1
-> Double -- ^ s_2
-> Double -- ^ 初期 log r̂
-> (Double, Int, Bool)
iterateBridge cfg logq1 logg1 logq2 logg2 s1 s2 logR0 = go 0 logR0
where
ls1 = log s1
ls2 = log s2
go !it !logR
| it >= bcMaxIter cfg = (logR, it, False)
| otherwise =
let -- Numerator: posterior 側の logq2 - logSumExp(s1·q2, s2·r·g2)
numTerms =
[ lq - logSumExp2 (ls1 + lq) (ls2 + logR + lg)
| (lq, lg) <- zip logq2 logg2 ]
-- Denominator: proposal 側の logg1 - logSumExp(s1·q1, s2·r·g1)
denTerms =
[ lg - logSumExp2 (ls1 + lq) (ls2 + logR + lg)
| (lq, lg) <- zip logq1 logg1 ]
num = logMeanExp numTerms
den = logMeanExp denTerms
logR' = num - den
diff = abs (logR' - logR)
in if diff < bcTolerance cfg
then (logR', it + 1, True)
else go (it + 1) logR'
-- ---------------------------------------------------------------------------
-- Diagonal Gaussian proposal (fit-to-chain)
-- ---------------------------------------------------------------------------
-- | chain から各パラメータの sample mean / SD を抽出。 SD = 0 になりうる
-- (定数推定) 場合は 1e-6 で下駄を履かせる (g(θ) 評価で除算 0 を避ける safety)。
fitDiagGaussian
:: [Text] -> Chain -> (Map.Map Text Double, Map.Map Text Double)
fitDiagGaussian names chain =
let mus = Map.fromList
[ (n, mean (chainVals n chain)) | n <- names ]
sds = Map.fromList
[ (n, max 1e-6 (stddev (chainVals n chain))) | n <- names ]
in (mus, sds)
where
mean xs = sum xs / fromIntegral (length xs)
stddev xs =
let mu = mean xs
n = fromIntegral (length xs) :: Double
in if n <= 1 then 0
else sqrt (sum [(x - mu) ^ (2 :: Int) | x <- xs] / (n - 1))
-- | Diagonal Gaussian proposal からサンプル抽出。
sampleProposal
:: [Text] -> Map.Map Text Double -> Map.Map Text Double -> GenIO
-> IO Params
sampleProposal names mus sds gen =
fmap Map.fromList $ forM names $ \n -> do
let mu = Map.findWithDefault 0 n mus
sd = Map.findWithDefault 1 n sds
x <- normal mu sd gen
pure (n, x)
-- | log density of diagonal Gaussian proposal at θ。
logProposal
:: [Text] -> Map.Map Text Double -> Map.Map Text Double -> Params
-> Double
logProposal names mus sds theta =
sum
[ let mu = Map.findWithDefault 0 n mus
sd = Map.findWithDefault 1 n sds
x = Map.findWithDefault 0 n theta
z = (x - mu) / sd
in -0.5 * log (2 * pi) - log sd - 0.5 * z * z
| n <- names ]
-- ---------------------------------------------------------------------------
-- log-sum-exp helpers
-- ---------------------------------------------------------------------------
logSumExp2 :: Double -> Double -> Double
logSumExp2 a b
| a > b = a + log (1 + exp (b - a))
| otherwise = b + log (1 + exp (a - b))
logMeanExp :: [Double] -> Double
logMeanExp xs
| null xs = -1 / 0
| otherwise =
let m = maximum xs
s = sum [ exp (x - m) | x <- xs ]
n = fromIntegral (length xs) :: Double
in m + log (s / n)