packages feed

hanalyze-0.1.0.0: demo/bayesian/LKJDemo.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
-- | LKJ 相関行列事前 + MvNormal 観測のデモ (Phase H4)。
--
-- 2D 観測データの相関行列 R を LKJ(η=1) 事前 (uniform on R) で推定。
-- 真の相関 ρ = 0.7 のデータを生成し、posterior の R[1][0]=ρ̂ が
-- 0.7 付近に集中することを確認。
module Main where

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

import Hanalyze.MCMC.NUTS (nuts, defaultNUTSConfig, NUTSConfig (..))
import Hanalyze.Model.HBM (ModelP, sample, observeMV, lkjCorrCholesky,
                  Distribution (..), augmentChainWithDeterministic)
import Hanalyze.Viz.MCMC (printPosteriorSummary, posteriorSummaryFile,
                 pairScatterFile)
import Hanalyze.Viz.Core (defaultConfig, OutputFormat (..), PlotConfig (..))

cfg :: NUTSConfig
cfg = defaultNUTSConfig
        { nutsIterations = 800
        , nutsBurnIn     = 400
        , nutsStepSize   = 0.1
        , nutsMaxDepth   = 6
        }

-- 真の相関 ρ = 0.7 で 2D サンプル生成
genCorr :: Int -> Double -> IO [[Double]]
genCorr n rho = do
  gen <- createSystemRandom
  let l11 = sqrt (1 - rho * rho)
      drawOne = do
        z0 <- MWC.standard gen
        z1 <- MWC.standard gen
        return [z0, rho * z0 + l11 * z1]
  mapM (const drawOne) [1 .. n]

-- σ 既知 (= 1)、相関行列を LKJ 事前で推定
lkjModel :: [[Double]] -> ModelP ()
lkjModel obs = do
  -- 相関行列 R の Cholesky factor L (2×2)
  l <- lkjCorrCholesky "R" 2 1.0   -- η = 1: uniform 事前
  -- σ_i 既知 = 1 → cov = L Lᵀ
  let cov = let row i = [ sum [ ((l !! i) !! kk) * ((l !! j) !! kk)
                              | kk <- [0 .. min i j] ]
                        | j <- [0, 1] ]
            in [row 0, row 1]
  -- μ も推定
  m0 <- sample "mu0" (Normal 0 5)
  m1 <- sample "mu1" (Normal 0 5)
  observeMV "y" (MvNormal [m0, m1] cov) obs

main :: IO ()
main = do
  putStrLn "═══════════════════════════════════════════════════════════════"
  putStrLn "  LKJ 相関行列事前 + MvNormal 観測 (Phase H4)"
  putStrLn "═══════════════════════════════════════════════════════════════"
  putStrLn ""

  putStrLn "真値: ρ = 0.7, μ = (0, 0), σ = (1, 1) (固定)"
  obs <- genCorr 100 0.7
  let xs    = [head ys | ys <- obs]
      ys    = [last ys | ys <- obs]
      n     = length obs
      mux   = sum xs / fromIntegral n
      muy   = sum ys / fromIntegral n
      cxy   = sum (zipWith (\x y -> (x - mux) * (y - muy)) xs ys)
              / fromIntegral (n - 1)
      sx    = sqrt (sum [(x - mux)^(2::Int) | x <- xs] / fromIntegral (n-1))
      sy    = sqrt (sum [(y - muy)^(2::Int) | y <- ys] / fromIntegral (n-1))
      empRho = cxy / (sx * sy)
  printf "観測 (n=%d): 標本 ρ = %.3f\n" n empRho
  putStrLn ""

  gen <- createSystemRandom
  rawCh <- nuts (lkjModel obs) cfg
                (Map.fromList [ ("R_u1_0", 0.5)
                              , ("mu0", 0), ("mu1", 0) ]) gen
  let ch = augmentChainWithDeterministic (lkjModel obs) rawCh

  putStrLn "[1] Posterior summary"
  -- pc1_0 は 2u−1 ∈ (-1,1) で、これが ρ そのもの (K=2 の場合)
  let names = [ "R_u1_0"        -- raw Beta latent
              , "R_pc1_0"       -- 2u-1 = ρ
              , "R_L1_0"        -- Cholesky off-diag = ρ (K=2)
              , "R_L1_1"        -- diag = √(1-ρ²)
              , "mu0", "mu1" ]
  printPosteriorSummary names [ch]
  putStrLn ""

  posteriorSummaryFile "lkj-summary.html" "LKJ posterior" names [ch]
  let pcfg = (defaultConfig "ρ̂ posterior")
               { plotWidth = 500, plotHeight = 400 }
  pairScatterFile HTML "lkj-pair.html" pcfg "mu0" "R_pc1_0" ch
  putStrLn "  → lkj-summary.html / lkj-pair.html"
  putStrLn ""

  putStrLn "═══════════════════════════════════════════════════════════════"
  putStrLn "  ✓ LKJ 事前で ρ ≈ 0.7 を回復、Cholesky factor も派生量化"
  putStrLn "═══════════════════════════════════════════════════════════════"