packages feed

hanalyze-0.1.0.0: demo/bayesian/MvNormalDemo.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
-- | MvNormal (多変量正規) 観測のデモ。
--
-- PyMC の @pm.MvNormal("y", mu=mu, cov=cov, observed=Y)@ 相当。
--
-- 例 1: 既知の共分散で平均ベクトルを推定
--   y_i ~ MvNormal(μ, Σ),  Σ = [[1, 0.7], [0.7, 1]] (固定)
--   μ ~ Normal(0, 5)        (各成分独立)
--
-- 例 2: 静的検証 (Cholesky / log density 単体テスト)
module Main where

import qualified Data.Map.Strict as Map
import Data.Maybe (fromMaybe)
import Text.Printf (printf)
import System.Random.MWC (createSystemRandom, GenIO)
import qualified System.Random.MWC.Distributions as MWC

import Hanalyze.MCMC.Core (posteriorMean, posteriorSD)
import Hanalyze.MCMC.NUTS (nuts, defaultNUTSConfig, NUTSConfig (..))
import Hanalyze.Model.HBM (ModelP, sample, observeMV, Distribution (..),
                  mvNormalLogDensity)

cfg :: NUTSConfig
cfg = defaultNUTSConfig
        { nutsIterations = 1500
        , nutsBurnIn     = 500
        , nutsStepSize   = 0.1
        }

-- ---------------------------------------------------------------------------
-- 単体テスト: 既知ケースの log density を比較
-- ---------------------------------------------------------------------------

-- | 標準 2 変量正規 N([0,0], I) で y=[0,0]:
--   log p = -k/2 log(2π) = -log(2π) ≈ -1.8379
test1 :: Double
test1 = mvNormalLogDensity [0, 0] [[1, 0], [0, 1]] [0, 0]

-- | N([0,0], I), y=[1,0]: log p = -log(2π) - 0.5 ≈ -2.3379
test2 :: Double
test2 = mvNormalLogDensity [0, 0] [[1, 0], [0, 1]] [1, 0]

-- | 相関ありケース Σ=[[1,0.7],[0.7,1]], y=[0,0]:
--   |Σ| = 1 - 0.49 = 0.51, log|Σ| = log 0.51 ≈ -0.6733
--   log p = -log(2π) - 0.5*log(0.51) ≈ -1.8379 + 0.3367 ≈ -1.5013
test3 :: Double
test3 = mvNormalLogDensity [0, 0] [[1, 0.7], [0.7, 1]] [0, 0]

-- ---------------------------------------------------------------------------
-- 平均推定モデル
-- ---------------------------------------------------------------------------

cov2 :: [[Double]]
cov2 = [[1.0, 0.7], [0.7, 1.0]]

-- | 真の μ = [2, -1] からデータ生成。
genData :: GenIO -> Int -> IO [[Double]]
genData gen n = do
  -- L = [[1,0], [0.7, sqrt(1-0.49)]] = [[1,0],[0.7, 0.7141]]
  let l00 = 1.0
      l10 = 0.7
      l11 = sqrt (1 - 0.49)
      muTrue = [2.0, -1.0]
  let drawOne = do
        z0 <- MWC.standard gen
        z1 <- MWC.standard gen
        let y0 = head muTrue + l00 * z0
            y1 = (muTrue !! 1) + l10 * z0 + l11 * z1
        return [y0, y1]
  mapM (const drawOne) [1 .. n]

mvNormalModel :: [[Double]] -> ModelP ()
mvNormalModel ys = do
  m1 <- sample "mu1" (Normal 0 5)
  m2 <- sample "mu2" (Normal 0 5)
  observeMV "y" (MvNormal [m1, m2] [[1.0, 0.7], [0.7, 1.0]]) ys

main :: IO ()
main = do
  putStrLn "═══════════════════════════════════════════════════════════════"
  putStrLn "  MvNormal (多変量正規) デモ"
  putStrLn "═══════════════════════════════════════════════════════════════"
  putStrLn ""

  -- ── 単体テスト ──
  putStrLn "[A] 単体テスト: log density 既知ケース"
  let exp1 = -log (2 * pi) :: Double
      exp2 = -log (2 * pi) - 0.5 :: Double
      exp3 = -log (2 * pi) - 0.5 * log 0.51 :: Double
  printf "  N([0,0], I) で y=[0,0]   : %+.4f  (期待 %+.4f = -log(2pi))\n" test1 exp1
  printf "  N([0,0], I) で y=[1,0]   : %+.4f  (期待 %+.4f)\n"            test2 exp2
  printf "  N([0,0], cov_corr) y=0   : %+.4f  (期待 %+.4f)\n"            test3 exp3
  putStrLn ""

  -- ── NUTS で平均ベクトル推定 ──
  putStrLn "[B] NUTS で μ を推定 (Σ 既知)"
  putStrLn "    真値 μ = [2.0, -1.0],   Σ = [[1, 0.7], [0.7, 1]]"
  gen <- createSystemRandom
  ys <- genData gen 100
  printf "    観測: %d 件 (k=2)\n" (length ys)
  ch <- nuts (mvNormalModel ys) cfg
              (Map.fromList [("mu1", 0), ("mu2", 0)]) gen
  let m1m = fromMaybe 0 (posteriorMean "mu1" ch)
      m2m = fromMaybe 0 (posteriorMean "mu2" ch)
      s1m = fromMaybe 0 (posteriorSD   "mu1" ch)
      s2m = fromMaybe 0 (posteriorSD   "mu2" ch)
  printf "  事後 μ1 = %+.3f  ± %.3f  (真値 +2.000)\n" m1m s1m
  printf "  事後 μ2 = %+.3f  ± %.3f  (真値 -1.000)\n" m2m s2m
  putStrLn ""

  putStrLn "═══════════════════════════════════════════════════════════════"
  putStrLn "  ✓ MvNormal が観測分布として動作 (Cholesky 経由 log density)"
  putStrLn "═══════════════════════════════════════════════════════════════"