packages feed

hbayesian-0.1.0.0: src/HBayesian/MCMC/HMC.hs

{-# LANGUAGE DataKinds           #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications    #-}

module HBayesian.MCMC.HMC
  ( HMCConfig (..)
  , HMCState (..)
  , hmc
  ) where

import           HHLO.Core.Types
import           HHLO.EDSL.Ops
import           HHLO.IR.Builder
import           HBayesian.Core
import           HBayesian.HHLO.Ops
import qualified HBayesian.HHLO.RNG as RNG

-- | Configuration for Hamiltonian Monte Carlo.
data HMCConfig = HMCConfig
  { hmcStepSize         :: Double
  , hmcNumLeapfrogSteps :: Int
  }

-- | HMC-specific state.
data HMCState (s :: Shape) (d :: DType) = HMCState
  { hmcPosition  :: !(Tensor s d)
  , hmcMomentum  :: !(Tensor s d)
  , hmcLogDens   :: !(Tensor '[] d)
  , hmcGradient  :: !(Tensor s d)
  }

hmc :: forall s d.
       (KnownShape s, KnownDType d)
    => (Tensor s d -> Builder (Tensor '[] d))
    -> Gradient s d
    -> HMCConfig
    -> Kernel s d (HMCState s d) (Info s d)
hmc logpdf grad config = Kernel { kernelInit = kernelInit, kernelStep = kernelStep }
  where
    epsVal = hmcStepSize config
    nSteps = hmcNumLeapfrogSteps config

    kernelInit _key pos = do
      ld <- logpdf pos
      g <- grad pos
      zeroM <- tconstant 0.0
      return $ HMCState pos zeroM ld g

    kernelStep key state = do
      (key1, key2) <- RNG.splitKey key
      let pos0 = hmcPosition state
      let g0   = hmcGradient state
      let ld0  = hmcLogDens state

      p0 <- RNG.rngNormalF32 key1 >>= convert @s @'F32 @d

      currentK <- do
        pSq <- tmul p0 p0
        pSum <- tsumAll pSq
        half <- constant @'[] @d 0.5
        tmul half pSum
      currentH <- tsub currentK ld0

      (pos', p', g', ld') <- leapfrog pos0 p0 g0

      proposedK <- do
        pSq <- tmul p' p'
        pSum <- tsumAll pSq
        half <- constant @'[] @d 0.5
        tmul half pSum
      proposedH <- tsub proposedK ld'

      logAccept <- tsub currentH proposedH
      zero <- constant @'[] @d 0.0
      logAlpha <- tminimum logAccept zero

      u <- RNG.rngUniformF32 key2 >>= convert @'[] @'F32 @d
      logU <- tlog u
      accept <- tlessThan logU logAlpha
      acceptS <- tbroadcast @'[] @s [] accept

      newPos <- tselect acceptS pos' pos0
      newP   <- tselect acceptS p' p0
      newG   <- tselect acceptS g' g0
      newLd  <- tselect accept ld' ld0

      infoAcceptProb <- texp logAlpha
      infoNumSteps <- constant @'[] @'I64 (fromIntegral nSteps)
      let info = Info infoAcceptProb accept infoNumSteps
      return (HMCState newPos newP newLd newG, info)

    leapfrog :: Tensor s d -> Tensor s d -> Tensor s d
             -> Builder (Tensor s d, Tensor s d, Tensor s d, Tensor '[] d)
    leapfrog pos0 p0 g0 = go nSteps pos0 p0 g0
      where
        go 0 pos p g = do
          ld <- logpdf pos
          return (pos, p, g, ld)
        go k pos p g = do
          halfEps <- constant @s @d (epsVal / 2.0)
          gScaled <- tmul g halfEps
          pHalf <- tadd p gScaled

          epsT <- constant @s @d epsVal
          pScaled <- tmul pHalf epsT
          pos' <- tadd pos pScaled

          g' <- grad pos'

          gScaled' <- tmul g' halfEps
          p' <- tadd pHalf gScaled'

          go (k-1) pos' p' g'