hbayesian-0.1.0.0: src/HBayesian/PPL.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
-- | A shallow probabilistic programming layer for HBayesian.
--
-- The PPL is a monad that carries:
-- * a read-only parameter vector of shape @[n]@
-- * a mutable log-density accumulator (scalar)
--
-- It desugars to the same 'Tensor s d -> Builder (Tensor '[] d)'
-- that samplers expect.
--
-- Example:
--
-- > myModel :: PPL 2 ()
-- > myModel = do
-- > alpha <- param 0
-- > beta <- param 1
-- > observe "alpha_prior" (normal 0.0 1.0) alpha
-- > observe "beta_prior" (normal 0.0 1.0) beta
-- > forM_ dataset $ \(x, y) -> do
-- > let mu = alpha + beta * x
-- > observe "y" (normal mu 0.5) y
-- >
-- > logpdf :: Tensor '[2] 'F32 -> Builder (Tensor '[] 'F32)
-- > logpdf = runPPL myModel
module HBayesian.PPL
( -- * PPL monad
PPL
, runPPL
, liftBuilder
-- * Model vocabulary
, param
, observe
-- * Distribution primitives
, normal
, normalT
, uniform
, uniformT
, halfNormal
, bernoulli
, bernoulliT
) where
import GHC.TypeNats (Nat)
import HHLO.Core.Types
import HHLO.IR.Builder
import HBayesian.HHLO.Ops hiding (map)
import qualified HHLO.EDSL.Ops as EDSL
-----------------------------------------------------------------------------
-- PPL monad (manual implementation, no mtl dependency)
-----------------------------------------------------------------------------
-- | The PPL monad. Carries:
-- * a read-only parameter vector of shape @[n]@
-- * a mutable log-density accumulator (scalar)
newtype PPL (n :: Nat) a = PPL
{ unPPL :: Tensor '[n] 'F32 -> Tensor '[] 'F32 -> Builder (a, Tensor '[] 'F32)
}
instance Functor (PPL n) where
fmap f (PPL m) = PPL $ \theta acc -> do
(x, acc') <- m theta acc
return (f x, acc')
instance Applicative (PPL n) where
pure x = PPL $ \_ acc -> return (x, acc)
PPL mf <*> PPL mx = PPL $ \theta acc -> do
(f, acc1) <- mf theta acc
(x, acc2) <- mx theta acc1
return (f x, acc2)
instance Monad (PPL n) where
return = pure
PPL mx >>= f = PPL $ \theta acc -> do
(x, acc1) <- mx theta acc
unPPL (f x) theta acc1
-- | Lift a raw 'Builder' action into the PPL.
liftBuilder :: Builder a -> PPL n a
liftBuilder mx = PPL $ \_ acc -> do
x <- mx
return (x, acc)
-- | Run a PPL model starting from a parameter vector and zero log-density.
-- Returns the accumulated log-posterior.
runPPL :: KnownShape '[n] => PPL n () -> Tensor '[n] 'F32 -> Builder (Tensor '[] 'F32)
runPPL (PPL m) theta = do
zero <- tconstant 0.0
((), acc) <- m theta zero
return acc
-----------------------------------------------------------------------------
-- Model vocabulary
-----------------------------------------------------------------------------
-- | Extract the i-th scalar parameter from the parameter vector.
param :: forall n. (KnownShape '[n], KnownDType 'F32)
=> Int -> PPL n (Tensor '[] 'F32)
param i = PPL $ \theta acc -> do
x <- tslice1 @n @'F32 theta (fromIntegral i)
return (x, acc)
-- | Condition the model on an observed value from a distribution.
-- The distribution is a function @value -> Builder (Tensor '[] 'F32)@
-- that computes the log-density.
observe :: String -> (Tensor '[] 'F32 -> Builder (Tensor '[] 'F32)) -> Tensor '[] 'F32 -> PPL n ()
observe _name dist value = PPL $ \_ acc -> do
ld <- dist value
newAcc <- tadd acc ld
return ((), newAcc)
-----------------------------------------------------------------------------
-- Distribution primitives
-----------------------------------------------------------------------------
-- | Normal distribution with constant mean and std (unnormalised log-pdf).
normal :: Double -> Double -> Tensor '[] 'F32 -> Builder (Tensor '[] 'F32)
normal mean std x = do
meanT <- tconstant mean
stdT <- tconstant std
normalT meanT stdT x
-- | Normal distribution with tensor mean and std.
normalT :: Tensor '[] 'F32 -> Tensor '[] 'F32 -> Tensor '[] 'F32 -> Builder (Tensor '[] 'F32)
normalT meanT stdT x = do
diff <- tsub x meanT
ratio <- tdiv diff stdT
sq <- tmul ratio ratio
negHalf <- tconstant (-0.5)
tmul negHalf sq
-- | Uniform distribution on @[a, b]@ with constant bounds.
uniform :: Double -> Double -> Tensor '[] 'F32 -> Builder (Tensor '[] 'F32)
uniform a b x = do
aT <- tconstant a
bT <- tconstant b
uniformT aT bT x
-- | Uniform distribution with tensor bounds.
uniformT :: Tensor '[] 'F32 -> Tensor '[] 'F32 -> Tensor '[] 'F32 -> Builder (Tensor '[] 'F32)
uniformT aT bT x = do
geA <- EDSL.compare x aT "GE"
leB <- EDSL.compare x bT "LE"
zero <- tconstant 0.0
negInf <- tconstant (-1.0e30) -- proxy for -inf
-- inside = geA && leB via nested tselect
inner <- tselect leB zero negInf
tselect geA inner negInf
-- | Half-normal distribution (positive support) with constant std.
halfNormal :: Double -> Tensor '[] 'F32 -> Builder (Tensor '[] 'F32)
halfNormal std x = do
zero <- tconstant 0.0
isPos <- EDSL.compare x zero "GE"
stdT <- tconstant std
lp <- normalT zero stdT x
negInf <- tconstant (-1.0e30)
tselect isPos lp negInf
-- | Bernoulli distribution with constant probability.
bernoulli :: Double -> Tensor '[] 'F32 -> Builder (Tensor '[] 'F32)
bernoulli p y = do
pT <- tconstant p
bernoulliT pT y
-- | Bernoulli distribution with tensor probability.
bernoulliT :: Tensor '[] 'F32 -> Tensor '[] 'F32 -> Builder (Tensor '[] 'F32)
bernoulliT pT y = do
logP <- tlog pT
one <- tconstant 1.0
oneMinP <- tsub one pT
logOneMinP <- tlog oneMinP
term1 <- tmul y logP
oneMinY <- tsub one y
term2 <- tmul oneMinY logOneMinP
tadd term1 term2