packages feed

hanalyze-0.1.0.0: demo/bayesian/BenchMCMC.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
-- | MH / HMC / NUTS のパフォーマンス比較デモ
--
-- ケース 1 (易しい): 独立 2D 正規事後分布
--   μ₁ ~ N(0,5), μ₂ ~ N(0,5)
--   y₁ᵢ | μ₁ ~ N(μ₁,1),  y₂ᵢ | μ₂ ~ N(μ₂,1)
--   → 事後分布の等高線は円形。全手法で効率よく探索できる。
--
-- ケース 2 (難しい): 和制約による強反相関事後分布
--   α ~ N(0,5), β ~ N(0,5)
--   yᵢ | α,β ~ N(α+β, 1)
--   → 事後分布は α+β ≈ ȳ という細長い尾根 (ρ ≈ -0.998)。
--     MH は短軸 (SD≈0.2) にステップを合わせると長軸 (SD≈7) の探索が
--     ランダムウォーク化し ESS が激減する。
--     HMC/NUTS は勾配で尾根に沿って動けるため効率を維持できる。
module Main where

import qualified Data.Map.Strict as Map
import qualified Data.Text as T
import Data.Time.Clock (getCurrentTime, diffUTCTime)
import Text.Printf (printf)
import System.Random.MWC (createSystemRandom)

import Hanalyze.Model.HBM
import Hanalyze.MCMC.Core (Chain (..), chainVals, acceptanceRate, posteriorMean)
import Hanalyze.MCMC.MH   (metropolis, MCMCConfig (..))
import Hanalyze.MCMC.HMC  (hmc,  HMCConfig (..),  defaultHMCConfig)
import Hanalyze.MCMC.NUTS (nuts, NUTSConfig (..), defaultNUTSConfig)
import Hanalyze.Stat.Distribution ()
import Hanalyze.Stat.MCMC (ess)

-- ---------------------------------------------------------------------------
-- モデル定義
-- ---------------------------------------------------------------------------

-- | ケース 1: 独立 2 パラメータ
easyModel :: [Double] -> [Double] -> ModelP ()
easyModel ys1 ys2 = do
  mu1 <- sample "mu1" (Normal 0 5)
  mu2 <- sample "mu2" (Normal 0 5)
  observe "y1" (Normal mu1 1) ys1
  observe "y2" (Normal mu2 1) ys2

-- | ケース 2: 両パラメータが同じ観測に現れる → 事後分布に強反相関
hardModel :: [Double] -> ModelP ()
hardModel ys = do
  alpha <- sample "mu1" (Normal 0 5)
  beta  <- sample "mu2" (Normal 0 5)
  observe "y" (Normal (alpha + beta) 1) ys

-- ---------------------------------------------------------------------------
-- 合成データ
-- ---------------------------------------------------------------------------

-- ケース 1: 真値 μ₁=2, μ₂=-1, n=20
obsEasy1, obsEasy2 :: [Double]
obsEasy1 = [2.3,1.8,2.1,1.9,2.5,1.7,2.2,2.0,1.6,2.4
           ,2.1,1.8,2.3,2.0,1.9,2.2,1.7,2.5,1.8,2.1]
obsEasy2 = [-0.8,-1.2,-0.9,-1.1,-0.7,-1.3,-1.0,-0.9,-1.2,-1.1
           ,-1.0,-0.8,-1.2,-1.1,-0.9,-1.0,-1.3,-0.7,-1.1,-0.8]

-- ケース 2: 真値 α+β=2, n=20
obsHard :: [Double]
obsHard = [1.5,2.3,1.8,2.1,2.5,1.7,2.2,2.0,1.6,2.4
          ,2.1,1.8,2.3,2.0,1.9,2.2,1.7,2.5,1.8,2.1]

-- ---------------------------------------------------------------------------
-- MCMC 設定
-- ---------------------------------------------------------------------------

nIter, nBurnIn :: Int
nIter   = 5000
nBurnIn = 1000

-- MH (ケース 1): 事後 SD ≈ 0.22 に対してステップ 0.4
mhEasy :: MCMCConfig
mhEasy = MCMCConfig
  { mcmcIterations = nIter
  , mcmcBurnIn     = nBurnIn
  , mcmcStepSizes  = Map.fromList [("mu1", 0.4), ("mu2", 0.4)]
  }

-- MH (ケース 2): 短軸 SD ≈ 0.2 に合わせた小ステップ
--   → 受容率は高いが長軸方向は完全なランダムウォーク
mhHard :: MCMCConfig
mhHard = MCMCConfig
  { mcmcIterations = nIter
  , mcmcBurnIn     = nBurnIn
  , mcmcStepSizes  = Map.fromList [("mu1", 0.1), ("mu2", 0.1)]
  }

-- HMC (ケース 1)
hmcEasy :: HMCConfig
hmcEasy = defaultHMCConfig
  { hmcIterations    = nIter
  , hmcBurnIn        = nBurnIn
  , hmcStepSize      = 0.2
  , hmcLeapfrogSteps = 10
  }

-- HMC (ケース 2): 長軸を踏破するためステップ数を多く
hmcHard :: HMCConfig
hmcHard = defaultHMCConfig
  { hmcIterations    = nIter
  , hmcBurnIn        = nBurnIn
  , hmcStepSize      = 0.05
  , hmcLeapfrogSteps = 50
  }

-- NUTS (ケース 1)
nutsEasy :: NUTSConfig
nutsEasy = defaultNUTSConfig
  { nutsIterations = nIter
  , nutsBurnIn     = nBurnIn
  , nutsStepSize   = 0.2
  }

-- NUTS (ケース 2): U-Turn 判定で軌跡長を自動調整
nutsHard :: NUTSConfig
nutsHard = defaultNUTSConfig
  { nutsIterations = nIter
  , nutsBurnIn     = nBurnIn
  , nutsStepSize   = 0.05
  }

-- ---------------------------------------------------------------------------
-- ユーティリティ
-- ---------------------------------------------------------------------------

getESS :: T.Text -> Chain -> Double
getESS name ch =
  ess (chainVals name ch)

timed :: IO a -> IO (a, Double)
timed action = do
  t0 <- getCurrentTime
  x  <- action
  t1 <- getCurrentTime
  return (x, realToFrac (diffUTCTime t1 t0))

report :: String -> Chain -> Double -> IO ()
report method ch secs = do
  let e1   = getESS "mu1" ch
      e2   = getESS "mu2" ch
      minE = min e1 e2
      m1   = maybe 0 id (posteriorMean "mu1" ch)
      m2   = maybe 0 id (posteriorMean "mu2" ch)
  printf
    "  %-5s | acc=%5.3f | mean(μ₁)=%6.3f mean(μ₂)=%6.3f \
    \| ESS(μ₁)=%5.0f ESS(μ₂)=%5.0f | minESS/s=%6.1f | %5.2fs\n"
    method (acceptanceRate ch) m1 m2 e1 e2 (minE / secs) secs

-- ---------------------------------------------------------------------------
-- Main
-- ---------------------------------------------------------------------------

mEasy :: ModelP ()
mEasy = easyModel obsEasy1 obsEasy2

mHard :: ModelP ()
mHard = hardModel obsHard

main :: IO ()
main = do
  gen <- createSystemRandom

  let initP = Map.fromList [("mu1", 0.0 :: Double), ("mu2", 0.0)]

  -- ---- ケース 1: 独立 2D 正規 ----
  let n     = length obsEasy1
      sigPost = 1 / sqrt (fromIntegral n + 1/25 :: Double)

  putStrLn ""
  putStrLn "══════════════════════════════════════════════════════════════════"
  putStrLn " ケース 1: 独立 2D 正規事後分布  (全手法で収束しやすい)"
  printf   "  真値: μ₁≈2.0, μ₂≈-1.0  事後 SD≈%.3f  ρ=0\n" sigPost
  putStrLn "══════════════════════════════════════════════════════════════════"

  (ch1, t1) <- timed $ metropolis mEasy mhEasy  initP gen
  report "MH"   ch1 t1
  (ch2, t2) <- timed $ hmc  mEasy hmcEasy  initP gen
  report "HMC"  ch2 t2
  (ch3, t3) <- timed $ nuts mEasy nutsEasy initP gen
  report "NUTS" ch3 t3

  -- ---- ケース 2: 強反相関 ----
  let ybar     = sum obsHard / fromIntegral (length obsHard)
      n2       = fromIntegral (length obsHard) :: Double
      -- 事後の短軸/長軸 SD を解析的に計算
      -- Λ = [[1/25+n, n],[n, 1/25+n]], Σ = Λ^{-1}
      lam      = 1/25 + n2
      detLam   = lam*lam - n2*n2
      sig11    = lam / detLam
      sig12    = negate n2 / detLam
      rhoPost  = sig12 / sig11
      sdShort  = sqrt (sig11 + sig12)  -- SD of (μ₁-μ₂)/√2
      sdLong   = sqrt (sig11 - sig12)  -- SD of (μ₁+μ₂)/√2

  putStrLn ""
  putStrLn "══════════════════════════════════════════════════════════════════"
  putStrLn " ケース 2: 和制約 α+β≈ȳ  (MH で収束しにくい)"
  printf   "  ȳ=%.2f  事後: 短軸 SD≈%.3f  長軸 SD≈%.2f  ρ≈%.4f\n"
           ybar sdShort sdLong rhoPost
  putStrLn "══════════════════════════════════════════════════════════════════"

  (ch4, t4) <- timed $ metropolis mHard mhHard  initP gen
  report "MH"   ch4 t4
  (ch5, t5) <- timed $ hmc  mHard hmcHard  initP gen
  report "HMC"  ch5 t5
  (ch6, t6) <- timed $ nuts mHard nutsHard initP gen
  report "NUTS" ch6 t6

  putStrLn ""
  putStrLn "凡例: acc=受容率  mean=事後平均  ESS=有効サンプル数  minESS/s=効率"