packages feed

hbayesian-0.1.0.0: examples/LogisticRegressionHMC.hs

{-# LANGUAGE DataKinds        #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}

-- | Example 3: Bayesian Logistic Regression with HMC.
--
-- Binary classification with D=3 features and n=4 observations.
-- The user provides the gradient explicitly, demonstrating the
-- manual-gradient path before Phase 5 auto-diff.
module LogisticRegressionHMC
  ( logisticRegLogPdf
  , logisticRegGrad
  , makeKernel
  , renderStepMlir
  , runChain
  , runChainV2
  ) where

import           Data.Word           (Word64)
import           Data.Text           (Text)
import           HHLO.Core.Types
import           HHLO.IR.AST         (FuncArg(..), TensorType(..))
import           HHLO.IR.Builder
import           HBayesian.Core
import           HBayesian.HHLO.Ops
import           HBayesian.HHLO.PJRT
import           HBayesian.MCMC.HMC
import           HBayesian.Chain
import           Common

-- | Log-posterior for Bayesian logistic regression.
logisticRegLogPdf :: Tensor '[3] 'F32 -> Builder (Tensor '[] 'F32)
logisticRegLogPdf beta = do
  let mkConstX [a, b, c] = do
        ca <- tconstant @'[] @'F32 (realToFrac a)
        cb <- tconstant @'[] @'F32 (realToFrac b)
        cc <- tconstant @'[] @'F32 (realToFrac c)
        tpack3 ca cb cc
      mkConstX _ = error "mkConstX: expected exactly 3 elements"

  x0 <- mkConstX [1.0 :: Float, 0.5, -0.5]
  y0 <- tconstant @'[] @'F32 1.0
  lp0 <- logLikPoint x0 y0 beta

  x1 <- mkConstX [1.0 :: Float, 1.0, -1.0]
  y1 <- tconstant @'[] @'F32 1.0
  lp1 <- logLikPoint x1 y1 beta

  x2 <- mkConstX [1.0 :: Float, 1.5, -1.5]
  y2 <- tconstant @'[] @'F32 0.0
  lp2 <- logLikPoint x2 y2 beta

  x3 <- mkConstX [1.0 :: Float, 2.0, -2.0]
  y3 <- tconstant @'[] @'F32 0.0
  lp3 <- logLikPoint x3 y3 beta

  llh01 <- tadd lp0 lp1
  llh23 <- tadd lp2 lp3
  llh   <- tadd llh01 llh23

  betaSq <- tmul beta beta
  betaSumSq <- tsumAll betaSq
  negHalf <- tconstant @'[] @'F32 (-0.5)
  prior <- tmul negHalf betaSumSq

  tadd llh prior

-- | Single data-point log-likelihood contribution.
logLikPoint :: Tensor '[3] 'F32 -> Tensor '[] 'F32 -> Tensor '[3] 'F32 -> Builder (Tensor '[] 'F32)
logLikPoint x_i y_i beta = do
  dotProd <- tsumAll =<< tmul x_i beta
  sig <- tsigmoid dotProd
  one <- tconstant @'[] @'F32 1.0
  logSig   <- tlog sig
  logOneMinusSig <- tlog =<< tsub one sig
  term1 <- tmul y_i logSig
  oneMinusY <- tsub one y_i
  term2 <- tmul oneMinusY logOneMinusSig
  tadd term1 term2

-- | User-provided gradient of the log-posterior.
logisticRegGrad :: Gradient '[3] 'F32
logisticRegGrad beta = do
  let mkConstX [a, b, c] = do
        ca <- tconstant @'[] @'F32 (realToFrac a)
        cb <- tconstant @'[] @'F32 (realToFrac b)
        cc <- tconstant @'[] @'F32 (realToFrac c)
        tpack3 ca cb cc
      mkConstX _ = error "mkConstX: expected exactly 3 elements"

  grad0 <- gradPoint [1.0, 0.5, -0.5] 1.0 beta mkConstX
  grad1 <- gradPoint [1.0, 1.0, -1.0] 1.0 beta mkConstX
  grad2 <- gradPoint [1.0, 1.5, -1.5] 0.0 beta mkConstX
  grad3 <- gradPoint [1.0, 2.0, -2.0] 0.0 beta mkConstX

  g01 <- tadd grad0 grad1
  g23 <- tadd grad2 grad3
  gradLik <- tadd g01 g23

  negOne <- tconstant @'[3] @'F32 (-1.0)
  gradPrior <- tmul negOne beta

  tadd gradLik gradPrior

-- | Gradient contribution from a single data point.
gradPoint :: [Float] -> Float -> Tensor '[3] 'F32
          -> ([Float] -> Builder (Tensor '[3] 'F32))
          -> Builder (Tensor '[3] 'F32)
gradPoint xs yi beta mkConstX = do
  x_i <- mkConstX xs
  dotProd <- tsumAll =<< tmul x_i beta
  sig <- tsigmoid dotProd
  yT <- tconstant @'[] @'F32 (realToFrac yi)
  residual <- tsub yT sig
  residBC <- tbroadcast @'[] @'[3] [] residual
  tmul x_i residBC

-- | Factory: build an HMC kernel for this model.
makeKernel :: HMCConfig -> Kernel '[3] 'F32 (HMCState '[3] 'F32) (Info '[3] 'F32)
makeKernel config = hmc logisticRegLogPdf logisticRegGrad config

-- | Tier A: render one kernel step to MLIR text.
renderStepMlir :: Text
renderStepMlir =
  renderKernelStep @'[3] @'F32
    [ FuncArg "key" (TensorType [2] UI64)
    , FuncArg "pos" (TensorType [3] F32)
    , FuncArg "p"   (TensorType [3] F32)
    , FuncArg "ld"  (TensorType [] F32)
    , FuncArg "g"   (TensorType [3] F32)
    ] $ do
      key <- arg @'[2] @'UI64
      pos <- arg @'[3] @'F32
      p   <- arg @'[3] @'F32
      ld  <- arg @'[] @'F32
      g   <- arg @'[3] @'F32
      let config = HMCConfig { hmcStepSize = 0.1, hmcNumLeapfrogSteps = 2 }
      (state', _info) <- kernelStep (makeKernel config) (Key key) (HMCState pos p ld g)
      return (hmcPosition state')

-- | Tier B: run a short chain on PJRT and return sampled beta vectors.
runChain :: IO [[Float]]
runChain = withPJRTCPU $ \api client -> do
    let config = HMCConfig { hmcStepSize = 0.1, hmcNumLeapfrogSteps = 2 }
        kernel = makeKernel config

    -- Compile the log-pdf module
    let ldMod = moduleFromBuilder @'[] @'F32 "main"
                  [ FuncArg "beta" (TensorType [3] F32) ] $ do
          beta <- arg @'[3] @'F32
          logisticRegLogPdf beta
    ldExe <- compileModule api client ldMod

    -- Compile the gradient module
    let gradMod = moduleFromBuilder @'[3] @'F32 "main"
                    [ FuncArg "beta" (TensorType [3] F32) ] $ do
          beta <- arg @'[3] @'F32
          logisticRegGrad beta
    gradExe <- compileModule api client gradMod

    -- Compile the HMC step module (single result: position)
    let stepMod = moduleFromBuilder @'[3] @'F32 "main"
                    [ FuncArg "key" (TensorType [2] UI64)
                    , FuncArg "pos" (TensorType [3] F32)
                    , FuncArg "p"   (TensorType [3] F32)
                    , FuncArg "ld"  (TensorType [] F32)
                    , FuncArg "g"   (TensorType [3] F32)
                    ] $ do
          key <- arg @'[2] @'UI64
          pos <- arg @'[3] @'F32
          p   <- arg @'[3] @'F32
          ld  <- arg @'[] @'F32
          g   <- arg @'[3] @'F32
          (state', _info) <- kernelStep kernel (Key key) (HMCState pos p ld g)
          return (hmcPosition state')
    stepExe <- compileModule api client stepMod

    let seed :: Word64 = 42
        beta0 = [0.0, 0.0, 0.0]

    -- Compute initial log-density and gradient
    betaBuf0 <- bufferFromF32 api client [3] beta0
    [ldBuf0] <- executeModule api ldExe [betaBuf0]
    [ld0] <- bufferToF32 api ldBuf0 1
    [gBuf0] <- executeModule api gradExe [betaBuf0]
    g0 <- bufferToF32 api gBuf0 3

    loop api client stepExe ldExe gradExe seed (0 :: Int) beta0 ld0 g0 (10 :: Int) []
  where
    loop _ _ _ _ _ _ _ _ _ _ 0 acc = return (reverse acc)
    loop api client stepExe ldExe gradExe seed step pos ld g n acc = do
        let key = [seed, fromIntegral step]
            zeroP = [0.0, 0.0, 0.0]
        keyBuf <- bufferFromUI64 api client [2] key
        posBuf <- bufferFromF32 api client [3] pos
        pBuf   <- bufferFromF32 api client [3] zeroP
        ldBuf  <- bufferFromF32 api client [] [ld]
        gBuf   <- bufferFromF32 api client [3] g
        [newPosBuf] <- executeModule api stepExe [keyBuf, posBuf, pBuf, ldBuf, gBuf]
        newPos <- bufferToF32 api newPosBuf 3
        -- Recompute log-density and gradient for the next step
        [newLdBuf] <- executeModule api ldExe [newPosBuf]
        [newLd] <- bufferToF32 api newLdBuf 1
        [newGBuf] <- executeModule api gradExe [newPosBuf]
        newG <- bufferToF32 api newGBuf 3
        loop api client stepExe ldExe gradExe seed (step + 1) newPos newLd newG (n - 1) (newPos : acc)

-- | v0.2: Run a chain using the 'Chain' combinators.
runChainV2 :: IO ([[Float]], [Diagnostic])
runChainV2 = do
    let config = HMCConfig { hmcStepSize = 0.1, hmcNumLeapfrogSteps = 2 }
        kernel = makeKernel config
        ck     = compileHMC kernel logisticRegLogPdf logisticRegGrad
    sampleChain ck [0.0, 0.0, 0.0] $ defaultChainConfig
        { ccNumIterations = 10
        , ccSeed = 42
        }