mcmc-0.6.0.0: src/Mcmc/Proposal/Hamiltonian.hs
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE UndecidableInstances #-}
-- |
-- Module : Mcmc.Proposal.Hamiltonian
-- Description : Hamiltonian Monte Carlo proposal
-- Copyright : (c) 2021 Dominik Schrempf
-- License : GPL-3.0-or-later
--
-- Maintainer : dominik.schrempf@gmail.com
-- Stability : experimental
-- Portability : portable
--
-- Creation date: Mon Jul 5 12:59:42 2021.
--
-- The Hamiltonian Monte Carlo (HMC) proposal.
--
-- For references, see:
--
-- - [1] Chapter 5 of Handbook of Monte Carlo: Neal, R. M., MCMC Using
-- Hamiltonian Dynamics, In S. Brooks, A. Gelman, G. Jones, & X. Meng (Eds.),
-- Handbook of Markov Chain Monte Carlo (2011), CRC press.
--
-- - [2] Gelman, A., Carlin, J. B., Stern, H. S., & Rubin, D. B., Bayesian data
-- analysis (2014), CRC Press.
--
-- - [3] Review by Betancourt and notes: Betancourt, M., A conceptual
-- introduction to Hamiltonian Monte Carlo, arXiv, 1701–02434 (2017).
--
-- NOTE on implementation:
--
-- - The implementation assumes the existence of the gradient. Like so, the user
-- can use automatic or manual differentiation, depending on the problem at
-- hand.
--
-- - The state needs to be list like or 'Traversable' so that the structure of
-- the state space is available. A 'Traversable' constraint on the data type
-- is nice because it is more general than, for example, a list, and
-- user-defined data structures can be used.
--
-- - The state needs to have a zip-like 'Applicative' instance so that
-- - matrix/vector operations can be performed.
module Mcmc.Proposal.Hamiltonian
( Gradient,
Masses,
LeapfrogTrajectoryLength,
LeapfrogScalingFactor,
HTune (..),
HSettings (..),
hamiltonian,
)
where
import Data.Foldable
import qualified Data.Matrix as M
import Data.Maybe
import Data.Traversable
import qualified Data.Vector as VB
import Mcmc.Prior
import Mcmc.Proposal
import Numeric.Log
import Statistics.Distribution
import Statistics.Distribution.Normal
import qualified Statistics.Function as S
import qualified Statistics.Sample as S
import System.Random.MWC
-- TODO: At the moment, the HMC proposal is agnostic of the prior and
-- likelihood, that is, the posterior function. This means, that it cannot know
-- when it reaches a point with zero posterior probability. This also affects
-- restricted or constrained parameters. See Gelman p. 303.
-- TODO: No-U-turn sampler.
-- TODO: Riemannian adaptation.
-- | Gradient of the log posterior function.
type Gradient f = f Double -> f Double
-- | Function validating the state.
--
-- Useful if parameters are constrained.
type Validate f = f Double -> Bool
-- | Masses of parameters.
--
-- NOTE: Full specification of a mass matrix including off-diagonal elements is
-- not supported.
--
-- NOTE: Parameters without masses ('Nothing') are not changed by the
-- Hamiltonian proposal.
--
-- The masses roughly describe how reluctant the particle moves through the
-- state space. If a parameter has higher mass, the momentum in this direction
-- will be changed less by the provided gradient, than when the same parameter
-- has lower mass.
--
-- The proposal is more efficient if masses are assigned according to the
-- inverse (co)-variance structure of the posterior function. That is,
-- parameters changing on larger scales should have lower masses than parameters
-- changing on lower scales. In particular, and for a diagonal mass matrix, the
-- optimal masses are the inverted variances of the parameters distributed
-- according to the posterior function.
--
-- Of course, the scales of the parameters of the posterior function are usually
-- unknown. Often, it is sufficient to
--
-- - set the masses to identical values roughly scaled with the inverted
-- estimated average variance of the posterior function; or even to
--
-- - set all masses to 1.0, and trust the tuning algorithm (see
-- 'HTuneMassesAndLeapfrog') to find the correct values.
type Masses f = f (Maybe Double)
-- | Mean leapfrog trajectory length \(L\).
--
-- Number of leapfrog steps per proposal.
--
-- To avoid problems with ergodicity, the actual number of leapfrog steps is
-- sampled proposal from a discrete uniform distribution over the interval
-- \([\text{floor}(0.8L),\text{ceiling}(1.2L)]\).
--
-- For a discussion of ergodicity and reasons why randomization is important,
-- see [1] p. 15; also mentioned in [2] p. 304.
--
-- NOTE: To avoid errors, the left bound has an additional hard minimum of 1,
-- and the right bound is required to be larger equal than the left bound.
--
-- Usually set to 10, but larger values may be desirable.
type LeapfrogTrajectoryLength = Int
-- | Mean of leapfrog scaling factor \(\epsilon\).
--
-- Determines the size of each leapfrog step.
--
-- To avoid problems with ergodicity, the actual leapfrog scaling factor is
-- sampled per proposal from a continuous uniform distribution over the interval
-- \((0.8\epsilon,1.2\epsilon]\).
--
-- For a discussion of ergodicity and reasons why randomization is important,
-- see [1] p. 15; also mentioned in [2] p. 304.
--
-- Usually set such that \( L \epsilon = 1.0 \), but smaller values may be
-- required if acceptance rates are low.
type LeapfrogScalingFactor = Double
-- Target state containing parameters.
type Positions f = f Double
-- Momenta of the parameters.
type Momenta f = f (Maybe Double)
-- | Tuning settings.
--
--
-- Tuning of leapfrog parameters:
--
-- We expect that the larger the leapfrog step size the larger the proposal step
-- size and the lower the acceptance ratio. Consequently, if the acceptance rate
-- is too low, the leapfrog step size is decreased and vice versa. Further, the
-- leapfrog trajectory length is scaled such that the product of the leapfrog
-- step size and trajectory length stays constant.
--
-- Tuning of masses:
--
-- The variances of all parameters of the posterior distribution obtained over
-- the last auto tuning interval is calculated and the masses are amended using
-- the old masses and the inverted variances. If, for a specific coordinate, the
-- sample size is too low, or if the calculated variance is out of predefined
-- bounds, the mass of the affected position is not changed.
data HTune
= -- | Tune masses and leapfrog parameters.
HTuneMassesAndLeapfrog
| -- | Tune leapfrog parameters only.
HTuneLeapfrogOnly
| -- | Do not tune at all.
HNoTune
deriving (Eq, Show)
-- | Specifications for Hamilton Monte Carlo proposal.
data HSettings f = HSettings
{ hGradient :: Gradient f,
hMaybeValidate :: Maybe (Validate f),
hMasses :: Masses f,
hLeapfrogTrajectoryLength :: LeapfrogTrajectoryLength,
hLeapfrogScalingFactor :: LeapfrogScalingFactor,
hTune :: HTune
}
checkHSettings :: Foldable f => HSettings f -> Maybe String
checkHSettings (HSettings _ _ masses l eps _)
| any f masses = Just "checkHSettings: One or more masses are zero or negative."
| l < 1 = Just "checkHSettings: Leapfrog trajectory length is zero or negative."
| eps <= 0 = Just "checkHSettings: Leapfrog scaling factor is zero or negative."
| otherwise = Nothing
where
f (Just m) = m <= 0
f Nothing = False
generateMomenta ::
Traversable f =>
Masses f ->
GenIO ->
IO (Momenta f)
generateMomenta masses gen = traverse (generateWith gen) masses
where
generateWith g (Just m) = let d = normalDistr 0 (sqrt m) in Just <$> genContVar d g
generateWith _ Nothing = pure Nothing
priorMomenta ::
(Applicative f, Foldable f) =>
Masses f ->
Momenta f ->
Prior
priorMomenta masses phi = foldl' (*) 1.0 $ f <$> masses <*> phi
where
f (Just m) (Just p) = let d = normalDistr 0 (sqrt m) in Exp $ logDensity d p
f Nothing Nothing = 1.0
f _ _ = error "priorMomenta: Got just a mass and no momentum or the other way around."
leapfrog ::
Applicative f =>
Gradient f ->
Maybe (Validate f) ->
Masses f ->
LeapfrogTrajectoryLength ->
LeapfrogScalingFactor ->
Positions f ->
Momenta f ->
-- Maybe (Positions', Momenta').
Maybe (Positions f, Momenta f)
leapfrog grad mVal masses l eps theta phi = do
let -- The first half step of the momenta.
phiHalf = leapfrogStepMomenta 0.5 eps grad theta phi
-- L-1 full steps. This gives the positions theta_{L-1}, and the momenta
-- phi_{L-1/2}.
(thetaLM1, phiLM1Half) <- go (l - 1) (Just (theta, phiHalf))
-- The last full step of the positions.
thetaL <- valF $ leapfrogStepPositions eps masses thetaLM1 phiLM1Half
let -- The last half step of the momenta.
phiL = leapfrogStepMomenta 0.5 eps grad thetaL phiLM1Half
return (thetaL, phiL)
where
valF x = case mVal of
Nothing -> Just x
Just f -> if f x then Just x else Nothing
go _ Nothing = Nothing
go 0 (Just (t, p)) = Just (t, p)
go n (Just (t, p)) =
let t' = leapfrogStepPositions eps masses t p
p' = leapfrogStepMomenta 1.0 eps grad t' p
r = (,p') <$> valF t'
in go (n - 1) r
leapfrogStepMomenta ::
Applicative f =>
-- Size of step (half or full step).
Double ->
LeapfrogScalingFactor ->
Gradient f ->
-- Current positions.
Positions f ->
-- Current momenta.
Momenta f ->
-- New momenta.
Momenta f
leapfrogStepMomenta xi eps grad theta phi = phi <+. ((xi * eps) .* grad theta)
where
(<+.) :: Applicative f => f (Maybe Double) -> f Double -> f (Maybe Double)
(<+.) xs ys = f <$> xs <*> ys
f Nothing _ = Nothing
f (Just x) y = Just $ x + y
leapfrogStepPositions ::
Applicative f =>
LeapfrogScalingFactor ->
Masses f ->
-- Current positions.
Positions f ->
-- Current momenta.
Momenta f ->
Positions f
-- The arguments are flipped to encounter the maybe momentum.
leapfrogStepPositions eps masses theta phi = theta <+. (mScaledReversed .*> phi)
where
(<+.) :: Applicative f => f Double -> f (Maybe Double) -> f Double
(<+.) xs ys = f <$> xs <*> ys
f x Nothing = x
f x (Just y) = x + y
mScaledReversed = (fmap . fmap) ((* eps) . (** (-1))) masses
(.*>) :: Applicative f => f (Maybe Double) -> f (Maybe Double) -> f (Maybe Double)
(.*>) xs ys = g <$> xs <*> ys
g (Just x) (Just y) = Just $ x * y
g Nothing Nothing = Nothing
g _ _ = error "leapfrogStepPositions: Got just a mass and no momentum or the other way around."
-- Scalar-vector multiplication.
(.*) :: Applicative f => Double -> f Double -> f Double
(.*) x ys = (* x) <$> ys
-- NOTE: Fixed parameters without mass have a tuning parameter of NaN.
massesToTuningParameters :: Foldable f => Masses f -> AuxiliaryTuningParameters
massesToTuningParameters = VB.fromList . map (fromMaybe nan) . toList
where
nan = 0 / 0
-- We need the structure in order to fill it with the given parameters.
tuningParametersToMasses ::
Traversable f =>
AuxiliaryTuningParameters ->
Masses f ->
Either String (Masses f)
tuningParametersToMasses xs ms =
if null xs'
then sequenceA msE
else Left "tuningParametersToMasses: Too many values."
where
(xs', msE) = mapAccumL setValue (VB.toList xs) ms
setValue [] _ = ([], Left "tuningParametersToMasses: Too few values.")
-- NOTE: Recover fixed parameters and unset their mass.
setValue (y : ys) _ = let y' = if isNaN y then Nothing else Just y in (ys, Right y')
hTuningParametersToSettings ::
Traversable f =>
TuningParameter ->
AuxiliaryTuningParameters ->
HSettings f ->
Either String (HSettings f)
hTuningParametersToSettings t ts (HSettings g v m l e tn) =
if tn == HTuneMassesAndLeapfrog
then case tuningParametersToMasses ts m of
Left err -> Left err
Right m' -> Right $ HSettings g v m' lTuned eTuned tn
else Right $ HSettings g v m lTuned eTuned tn
where
-- The larger epsilon, the larger the proposal step size and the lower the
-- expected acceptance ratio.
--
-- Further, we roughly keep \( L * \epsilon = 1.0 \). The equation is not
-- correct, because we pull L closer to the original value to keep the
-- runtime somewhat acceptable.
lTuned = ceiling $ fromIntegral l / (t ** 0.9) :: Int
eTuned = t * e
hamiltonianSimpleWithTuningParameters ::
(Applicative f, Traversable f) =>
HSettings f ->
TuningParameter ->
AuxiliaryTuningParameters ->
Either String (ProposalSimple (Positions f))
hamiltonianSimpleWithTuningParameters s t ts = case hTuningParametersToSettings t ts s of
Left err -> Left err
Right s' -> Right $ hamiltonianSimple s'
hamiltonianSimple ::
(Applicative f, Traversable f) =>
HSettings f ->
ProposalSimple (Positions f)
hamiltonianSimple (HSettings gradient mVal masses l e _) theta g = do
phi <- generateMomenta masses g
lRan <- uniformR (lL, lR) g
eRan <- uniformR (eL, eR) g
case leapfrog gradient mVal masses lRan eRan theta phi of
Nothing -> return (theta, 0.0, 1.0)
Just (theta', phi') ->
let prPhi = priorMomenta masses phi
-- NOTE: Neal page 12: In order for the proposal to be in detailed
-- balance, the momenta have to be negated before proposing the new value.
-- This is not required here since the prior involves normal distributions
-- centered around 0. However, if the multivariate normal distribution is
-- used, it makes a difference.
prPhi' = priorMomenta masses phi'
kernelR = prPhi' / prPhi
in return (theta', kernelR, 1.0)
where
lL = maximum [1 :: Int, floor $ (0.8 :: Double) * fromIntegral l]
lR = maximum [lL, ceiling $ (1.2 :: Double) * fromIntegral l]
eL = 0.8 * e
eR = 1.2 * e
minVariance :: Double
minVariance = 1e-6
maxVariance :: Double
maxVariance = 1e6
minSamples :: Int
minSamples = 60
computeAuxiliaryTuningParameters ::
Foldable f =>
VB.Vector (Positions f) ->
AuxiliaryTuningParameters ->
AuxiliaryTuningParameters
computeAuxiliaryTuningParameters xss ts =
VB.zipWith (\t -> rescueWith t . calcSamplesAndVariance) ts xssT
where
-- TODO: Improve matrix transposition.
xssT = VB.fromList $ M.toColumns $ M.fromLists $ VB.toList $ VB.map toList xss
calcSamplesAndVariance xs = (VB.length $ VB.uniq $ S.gsort xs, S.variance xs)
rescueWith t (sampleSize, var) =
if var < minVariance || maxVariance < var || sampleSize < minSamples
then -- then traceShow ("Rescue with " <> show t) t
t
else
let t' = sqrt (t * recip var)
in -- in traceShow ("Old mass " <> show t <> " new mass " <> show t') t'
t'
-- | Hamiltonian Monte Carlo proposal.
--
-- The 'Applicative' and 'Traversable' instances are used for element-wise
-- operations.
--
-- Assume a zip-like 'Applicative' instance so that cardinality remains
-- constant.
--
-- NOTE: The desired acceptance rate is 0.65, although the dimension of the
-- proposal is high.
--
-- NOTE: The speed of this proposal can change drastically when tuned because
-- the leapfrog trajectory length is changed.
hamiltonian ::
(Applicative f, Traversable f) =>
-- | The sample state is used to calculate the dimension of the proposal.
f Double ->
HSettings f ->
PName ->
PWeight ->
Proposal (f Double)
hamiltonian x s n w = case checkHSettings s of
Just err -> error err
Nothing ->
let desc = PDescription "Hamiltonian Monte Carlo (HMC)"
dim = PSpecial (length x) 0.65
ts = massesToTuningParameters (hMasses s)
ps = hamiltonianSimple s
p' = Proposal n desc dim w ps
fT = defaultTuningFunction dim
tS = hTune s
fTs =
if tS == HTuneMassesAndLeapfrog
then computeAuxiliaryTuningParameters
else \_ xs -> xs
in case tS of
HNoTune -> p' Nothing
_ -> p' $ Just $ Tuner 1.0 fT ts fTs (hamiltonianSimpleWithTuningParameters s)