An extended Kalman filter in Haskell using type level literals and automatic differentiation to provide some guarantees of correctness.
Suppose we wish to model population growth of bees via the logistic equation
We assume the growth rate is unknown and drawn from a normal distribution but the carrying capacity is known and we wish to estimate the growth rate by observing noisy values of the population at discrete times . Note that is entirely deterministic and its stochasticity is only as a result of the fact that the unknown parameter of the logistic equation is sampled from a normal distribution (we could for example be observing different colonies of bees and we know from the literature that bee populations obey the logistic equation and each colony will have different growth rates).
> {-# OPTIONS_GHC -Wall #-}
> {-# OPTIONS_GHC -fno-warn-name-shadowing #-}
> {-# OPTIONS_GHC -fno-warn-type-defaults #-}
> {-# OPTIONS_GHC -fno-warn-unused-do-bind #-}
> {-# OPTIONS_GHC -fno-warn-missing-methods #-}
> {-# OPTIONS_GHC -fno-warn-orphans #-}
> {-# LANGUAGE DataKinds #-}
> {-# LANGUAGE ScopedTypeVariables #-}
> {-# LANGUAGE RankNTypes #-}
> {-# LANGUAGE BangPatterns #-}
> {-# LANGUAGE TypeOperators #-}
> {-# LANGUAGE TypeFamilies #-}
> module FunWithKalman3 where
> import GHC.TypeLits
> import Numeric.LinearAlgebra.Static
> import Data.Maybe ( fromJust )
> import Numeric.AD
> import Data.Random.Source.PureMT
> import Data.Random
> import Control.Monad.State
> import qualified Control.Monad.Writer as W
> import Control.Monad.Loops
The logistic equation is a well known example of a dynamical system which has an analytic solution
Here it is in Haskell
> logit :: Floating a => a -> a -> a -> a
> logit p0 k x = k * p0 * (exp x) / (k + p0 * (exp x - 1))
We observe a noisy value of population at regular time intervals (where is the time interval)
Using the semi-group property of our dynamical system, we can re-write this as
To convince yourself that this re-formulation is correct, think of the population as starting at ; after 1 time step it has reached and and after two time steps it has reached and this ought to be the same as the point reached after 1 time step starting at , for example
> oneStepFrom0, twoStepsFrom0, oneStepFrom1 :: Double
> oneStepFrom0 = logit 0.1 1.0 (1 * 0.1)
> twoStepsFrom0 = logit 0.1 1.0 (1 * 0.2)
> oneStepFrom1 = logit oneStepFrom0 1.0 (1 * 0.1)
ghci> twoStepsFrom0
0.11949463171139338
ghci> oneStepFrom1
0.1194946317113934
We would like to infer the growth rate not just be able to predict the population so we need to add another variable to our model.
This is almost in the form suitable for estimation using a Kalman filter but the dependency of the state on the previous state is non-linear. We can modify the Kalman filter to create the extended Kalman filter (EKF) by making a linear approximation.
Since the measurement update is trivially linear (even in this more general form), the measurement update step remains unchanged.
By Taylor we have
where is the Jacobian of evaluated at (for the exposition of the extended filter we take to be vector valued hence the use of a bold font). We take to be normally distributed with mean of 0 and ignore any difficulties there may be with using Taylor with stochastic variables.
Applying this at we have
Using the same reasoning as we did as for Kalman filters and writing for we obtain
Note that we pass in the Jacobian of the update function as a function itself in the case of the extended Kalman filter rather than the matrix representing the linear function as we do in the case of the classical Kalman filter.
> k, p0 :: Floating a => a
> k = 1.0
> p0 = 0.1 * k
> r, deltaT :: Floating a => a
> r = 10.0
> deltaT = 0.0005
Relating ad and hmatrix is somewhat unpleasant but this can probably be ameliorated by defining a suitable datatype.
> a :: R 2 -> R 2
> a rpPrev = rNew # pNew
> where
> (r, pPrev) = headTail rpPrev
> rNew :: R 1
> rNew = konst r
>
> (p, _) = headTail pPrev
> pNew :: R 1
> pNew = fromList $ [logit p k (r * deltaT)]
> bigA :: R 2 -> Sq 2
> bigA rp = fromList $ concat $ j [r, p]
> where
> (r, ps) = headTail rp
> (p, _) = headTail ps
> j = jacobian (\[r, p] -> [r, logit p k (r * deltaT)])
For some reason, hmatrix with static guarantees does not yet provide an inverse function for matrices.
> inv :: (KnownNat n, (1 <=? n) ~ 'True) => Sq n -> Sq n
> inv m = fromJust $ linSolve m eye
Here is the extended Kalman filter itself. The type signatures on the expressions inside the function are not necessary but did help the implementor discover a bug in the mathematical derivation and will hopefully help the reader.
> outer :: forall m n . (KnownNat m, KnownNat n,
> (1 <=? n) ~ 'True, (1 <=? m) ~ 'True) =>
> R n -> Sq n ->
> L m n -> Sq m ->
> (R n -> R n) -> (R n -> Sq n) -> Sq n ->
> [R m] ->
> [(R n, Sq n)]
> outer muPrior sigmaPrior bigH bigSigmaY
> littleA bigABuilder bigSigmaX ys = result
> where
> result = scanl update (muPrior, sigmaPrior) ys
>
> update :: (R n, Sq n) -> R m -> (R n, Sq n)
> update (xHatFlat, bigSigmaHatFlat) y =
> (xHatFlatNew, bigSigmaHatFlatNew)
> where
>
> v :: R m
> v = y - (bigH #> xHatFlat)
>
> bigS :: Sq m
> bigS = bigH <> bigSigmaHatFlat <> (tr bigH) + bigSigmaY
>
> bigK :: L n m
> bigK = bigSigmaHatFlat <> (tr bigH) <> (inv bigS)
>
> xHat :: R n
> xHat = xHatFlat + bigK #> v
>
> bigSigmaHat :: Sq n
> bigSigmaHat = bigSigmaHatFlat - bigK <> bigS <> (tr bigK)
>
> bigA :: Sq n
> bigA = bigABuilder xHat
>
> xHatFlatNew :: R n
> xHatFlatNew = littleA xHat
>
> bigSigmaHatFlatNew :: Sq n
> bigSigmaHatFlatNew = bigA <> bigSigmaHat <> (tr bigA) + bigSigmaX
Now let us create some sample data.
> obsVariance :: Double
> obsVariance = 1e-2
> bigSigmaY :: Sq 1
> bigSigmaY = fromList [obsVariance]
> nObs :: Int
> nObs = 300
> singleSample :: Double -> RVarT (W.Writer [Double]) Double
> singleSample p0 = do
> epsilon <- rvarT (Normal 0.0 obsVariance)
> let p1 = logit p0 k (r * deltaT)
> lift $ W.tell [p1 + epsilon]
> return p1
> streamSample :: RVarT (W.Writer [Double]) Double
> streamSample = iterateM_ singleSample p0
> samples :: [Double]
> samples = take nObs $ snd $
> W.runWriter (evalStateT (sample streamSample) (pureMT 3))
We created our data with a growth rate of
ghci> r
10.0
but let us pretend that we have read the literature on growth rates of bee colonies and we have some big doubts about growth rates but we are almost certain about the size of the colony at .
> muPrior :: R 2
> muPrior = fromList [5.0, 0.1]
>
> sigmaPrior :: Sq 2
> sigmaPrior = fromList [ 1e2, 0.0
> , 0.0, 1e-10
> ]
We only observe the population and not the rate itself.
> bigH :: L 1 2
> bigH = fromList [0.0, 1.0]
Strictly speaking this should be 0 but this is close enough.
> bigSigmaX :: Sq 2
> bigSigmaX = fromList [ 1e-10, 0.0
> , 0.0, 1e-10
> ]
Now we can run our filter and watch it switch away from our prior belief as it accumulates more and more evidence.
> test :: [(R 2, Sq 2)]
> test = outer muPrior sigmaPrior bigH bigSigmaY
> a bigA bigSigmaX (map (fromList . return) samples)
Suppose we have a vector of weights which sum to 1.0 and we wish to sample n samples randomly according to these weights. There is a well known trick in Matlab / Octave using sampling from a uniform distribution.
num_particles = 2*10^7
likelihood = zeros(num_particles,1);
likelihood(:,1) = 1/num_particles;
[_,index] = histc(rand(num_particles,1),[0;cumsum(likelihood/sum(likelihood))]);
s = sum(index);
Using tic and toc this produces an answer with
Elapsed time is 10.7763 seconds.
I could find no equivalent function in Haskell nor could I easily find a binary search function.
> {-# OPTIONS_GHC -Wall #-}
> {-# OPTIONS_GHC -fno-warn-name-shadowing #-}
> {-# OPTIONS_GHC -fno-warn-type-defaults #-}
> {-# OPTIONS_GHC -fno-warn-unused-do-bind #-}
> {-# OPTIONS_GHC -fno-warn-missing-methods #-}
> {-# OPTIONS_GHC -fno-warn-orphans #-}
> {-# LANGUAGE BangPatterns #-}
> import System.Random.MWC
> import qualified Data.Vector.Unboxed as V
> import Control.Monad.ST
> import qualified Data.Vector.Algorithms.Search as S
> import Data.Bits
> n :: Int
> n = 2*10^7
Let’s create some random data. For a change let’s use mwc-random rather than random-fu.
> vs :: V.Vector Double
> vs = runST (create >>= (asGenST $ \gen -> uniformVector gen n))
Again, I could find no equivalent of cumsum but we can write our own.
> weightsV, cumSumWeightsV :: V.Vector Double
> weightsV = V.replicate n (recip $ fromIntegral n)
> cumSumWeightsV = V.scanl (+) 0 weightsV
Binary search on a sorted vector is straightforward and a cumulative sum ensures that the vector is sorted.
> binarySearch :: (V.Unbox a, Ord a) =>
> V.Vector a -> a -> Int
> binarySearch vec x = loop 0 (V.length vec - 1)
> where
> loop !l !u
> | u <= l = l
> | otherwise = let e = vec V.! k in if x <= e then loop l k else loop (k+1) u
> where k = l + (u - l) `shiftR` 1
> indices :: V.Vector Double -> V.Vector Double -> V.Vector Int
> indices bs xs = V.map (binarySearch bs) xs
To see how well this performs, let’s sum the indices (of course, we wouldn’t do this in practice) as we did for the Matlab implementation.
> js :: V.Vector Int
> js = indices (V.tail cumSumWeightsV) vs
> main :: IO ()
> main = do
> print $ V.foldl' (+) 0 js
Using +RTS -s we get
Total time 10.80s ( 11.06s elapsed)
which is almost the same as the Matlab version.
I did eventually find a binary search function in vector-algorithms and since one should not re-invent the wheel, let us try using it.
> indices' :: (V.Unbox a, Ord a) => V.Vector a -> V.Vector a -> V.Vector Int
> indices' sv x = runST $ do
> st <- V.unsafeThaw (V.tail sv)
> V.mapM (S.binarySearch st) x
> main' :: IO ()
> main' = do
> print $ V.foldl' (+) 0 $ indices' cumSumWeightsV vs
Again using +RTS -s we get
Total time 11.34s ( 11.73s elapsed)
So the library version seems very slightly slower.
Suppose we have an random variable with pdf and we wish to find its second moment numerically. However, the random-fu package does not support sampling from such as distribution. We notice that
So we can sample from and evaluate
> {-# OPTIONS_GHC -Wall #-}
> {-# OPTIONS_GHC -fno-warn-name-shadowing #-}
> {-# OPTIONS_GHC -fno-warn-type-defaults #-}
> {-# OPTIONS_GHC -fno-warn-unused-do-bind #-}
> {-# OPTIONS_GHC -fno-warn-missing-methods #-}
> {-# OPTIONS_GHC -fno-warn-orphans #-}
> module Importance where
> import Control.Monad
> import Data.Random.Source.PureMT
> import Data.Random
> import Data.Random.Distribution.Binomial
> import Data.Random.Distribution.Beta
> import Control.Monad.State
> import qualified Control.Monad.Writer as W
> sampleImportance :: RVarT (W.Writer [Double]) ()
> sampleImportance = do
> x <- rvarT $ Normal 0.0 2.0
> let x2 = x^2
> u = x2 * 0.5 * exp (-(abs x))
> v = (exp ((-x2)/8)) * (recip (sqrt (8*pi)))
> w = u / v
> lift $ W.tell [w]
> return ()
> runImportance :: Int -> [Double]
> runImportance n =
> snd $
> W.runWriter $
> evalStateT (sample (replicateM n sampleImportance))
> (pureMT 2)
We can run this 10,000 times to get an estimate.
ghci> import Formatting
ghci> format (fixed 2) (sum (runImportance 10000) / 10000)
"2.03"
Since we know that the -th moment of the exponential distribution is where is the rate (1 in this example), the exact answer is 2 which is not too far from our estimate using importance sampling.
The value of
is called the weight, is the pdf from which we wish to sample and is the pdf of the importance distribution.
Suppose that the posterior distribution of a model in which we are interested has a complicated functional form and that we therefore wish to approximate it in some way. First assume that we wish to calculate the expectation of some arbitrary function of the parameters.
Using Bayes
where is some normalizing constant.
As before we can re-write this using a proposal distribution
We can now sample repeatedly to obtain
where the weights are defined as before by
We follow Alex Cook and use the example from (Rerks-Ngarm et al. 2009). We take the prior as and use as the proposal distribution. In this case the proposal and the prior are identical just expressed differently and therefore cancel.
Note that we use the log of the pdf in our calculations otherwise we suffer from (silent) underflow, e.g.,
ghci> pdf (Binomial nv (0.4 :: Double)) xv
0.0
On the other hand if we use the log pdf form
ghci> logPdf (Binomial nv (0.4 :: Double)) xv
-3900.8941170876574
> xv, nv :: Int
> xv = 51
> nv = 8197
> sampleUniform :: RVarT (W.Writer [Double]) ()
> sampleUniform = do
> x <- rvarT StdUniform
> lift $ W.tell [x]
> return ()
> runSampler :: RVarT (W.Writer [Double]) () ->
> Int -> Int -> [Double]
> runSampler sampler seed n =
> snd $
> W.runWriter $
> evalStateT (sample (replicateM n sampler))
> (pureMT (fromIntegral seed))
> sampleSize :: Int
> sampleSize = 1000
> pv :: [Double]
> pv = runSampler sampleUniform 2 sampleSize
> logWeightsRaw :: [Double]
> logWeightsRaw = map (\p -> logPdf (Beta 1.0 1.0) p +
> logPdf (Binomial nv p) xv -
> logPdf StdUniform p) pv
> logWeightsMax :: Double
> logWeightsMax = maximum logWeightsRaw
>
> weightsRaw :: [Double]
> weightsRaw = map (\w -> exp (w - logWeightsMax)) logWeightsRaw
> weightsSum :: Double
> weightsSum = sum weightsRaw
> weights :: [Double]
> weights = map (/ weightsSum) weightsRaw
> meanPv :: Double
> meanPv = sum $ zipWith (*) pv weights
>
> meanPv2 :: Double
> meanPv2 = sum $ zipWith (\p w -> p * p * w) pv weights
>
> varPv :: Double
> varPv = meanPv2 - meanPv * meanPv
We get the answer
ghci> meanPv
6.400869727227364e-3
But if we look at the size of the weights and the effective sample size
ghci> length $ filter (>= 1e-6) weights
9
ghci> (sum weights)^2 / (sum $ map (^2) weights)
4.581078458313967
so we may not be getting a very good estimate. Let’s try
> sampleNormal :: RVarT (W.Writer [Double]) ()
> sampleNormal = do
> x <- rvarT $ Normal meanPv (sqrt varPv)
> lift $ W.tell [x]
> return ()
> pvC :: [Double]
> pvC = runSampler sampleNormal 3 sampleSize
> logWeightsRawC :: [Double]
> logWeightsRawC = map (\p -> logPdf (Beta 1.0 1.0) p +
> logPdf (Binomial nv p) xv -
> logPdf (Normal meanPv (sqrt varPv)) p) pvC
> logWeightsMaxC :: Double
> logWeightsMaxC = maximum logWeightsRawC
>
> weightsRawC :: [Double]
> weightsRawC = map (\w -> exp (w - logWeightsMaxC)) logWeightsRawC
> weightsSumC :: Double
> weightsSumC = sum weightsRawC
> weightsC :: [Double]
> weightsC = map (/ weightsSumC) weightsRawC
> meanPvC :: Double
> meanPvC = sum $ zipWith (*) pvC weightsC
> meanPvC2 :: Double
> meanPvC2 = sum $ zipWith (\p w -> p * p * w) pvC weightsC
>
> varPvC :: Double
> varPvC = meanPvC2 - meanPvC * meanPvC
Now the weights and the effective size are more re-assuring
ghci> length $ filter (>= 1e-6) weightsC
1000
ghci> (sum weightsC)^2 / (sum $ map (^2) weightsC)
967.113872888872
And we can take more confidence in the estimate
ghci> meanPvC
6.371225269833208e-3
Rerks-Ngarm, Supachai, Punnee Pitisuttithum, Sorachai Nitayaphan, Jaranit Kaewkungwal, Joseph Chiu, Robert Paris, Nakorn Premsri, et al. 2009. “Vaccination with ALVAC and AIDSVAX to Prevent HIV-1 Infection in Thailand.” New England Journal of Medicine 361 (23) (December 3): 2209–2220. doi:10.1056/nejmoa0908492. http://dx.doi.org/10.1056/nejmoa0908492.
Suppose we have particle moving in at constant velocity in 1 dimension, where the velocity is sampled from a distribution. We can observe the position of the particle at fixed intervals and we wish to estimate its initial velocity. For generality, let us assume that the positions and the velocities can be perturbed at each interval and that our measurements are noisy.
A point of Haskell interest: using type level literals caught a bug in the mathematical description (one of the dimensions of a matrix was incorrect). Of course, this would have become apparent at run-time but proof checking of this nature is surely the future for mathematicians. One could conceive of writing an implementation of an algorithm or proof, compiling it but never actually running it purely to check that some aspects of the algorithm or proof are correct.
We take the position as and the velocity :
where and are all IID normal with means of 0 and variances of and
We can re-write this as
where
Let us denote the mean and variance of as and respectively and note that
Since and are jointly Gaussian and recalling that = as covariance matrices are symmetric, we can calculate their mean and covariance matrix as
We can now use standard formulæ which say if
then
and apply this to
to give
This is called the measurement update; more explicitly
Sometimes the measurement residual , the measurement prediction covariance and the filter gain are defined and the measurement update is written as
We further have that
We thus obtain the Kalman filter prediction step:
Further information can be found in (Boyd 2008), (Kleeman 1996) and (Särkkä 2013).
The hmatrix now uses type level literals via the DataKind extension in ghc to enforce compatibility of matrix and vector operations at the type level. See here for more details. Sadly a bug in the hmatrix implementation means we can’t currently use this excellent feature and we content ourselves with comments describing what the types would be were it possible to use it.
> {-# OPTIONS_GHC -Wall #-}
> {-# OPTIONS_GHC -fno-warn-name-shadowing #-}
> {-# OPTIONS_GHC -fno-warn-type-defaults #-}
> {-# OPTIONS_GHC -fno-warn-unused-do-bind #-}
> {-# OPTIONS_GHC -fno-warn-missing-methods #-}
> {-# OPTIONS_GHC -fno-warn-orphans #-}
> {-# LANGUAGE DataKinds #-}
> {-# LANGUAGE ScopedTypeVariables #-}
> {-# LANGUAGE RankNTypes #-}
> module FunWithKalmanPart1a where
> import Numeric.LinearAlgebra.HMatrix hiding ( outer )
> import Data.Random.Source.PureMT
> import Data.Random hiding ( gamma )
> import Control.Monad.State
> import qualified Control.Monad.Writer as W
> import Control.Monad.Loops
Let us make our model almost deterministic but with noisy observations.
> stateVariance :: Double
> stateVariance = 1e-6
> obsVariance :: Double
> obsVariance = 1.0
And let us start with a prior normal distribution with a mean position and velocity of 0 with moderate variances and no correlation.
> -- muPrior :: R 2
> muPrior :: Vector Double
> muPrior = vector [0.0, 0.0]
> -- sigmaPrior :: Sq 2
> sigmaPrior :: Matrix Double
> sigmaPrior = (2 >< 2) [ 1e1, 0.0
> , 0.0, 1e1
> ]
We now set up the parameters for our model as outlined in the preceeding section.
> deltaT :: Double
> deltaT = 0.001
> -- bigA :: Sq 2
> bigA :: Matrix Double
> bigA = (2 >< 2) [ 1, deltaT
> , 0, 1
> ]
> a :: Double
> a = 1.0
> -- bigH :: L 1 2
> bigH :: Matrix Double
> bigH = (1 >< 2) [ a, 0
> ]
> -- bigSigmaY :: Sq 1
> bigSigmaY :: Matrix Double
> bigSigmaY = (1 >< 1) [ obsVariance ]
> -- bigSigmaX :: Sq 2
> bigSigmaX :: Matrix Double
> bigSigmaX = (2 >< 2) [ stateVariance, 0.0
> , 0.0, stateVariance
> ]
The implementation of the Kalman filter using the hmatrix package is straightforward.
> -- outer :: forall m n . (KnownNat m, KnownNat n) =>
> -- R n -> Sq n -> L m n -> Sq m -> Sq n -> Sq n -> [R m] -> [(R n, Sq n)]
> outer :: Vector Double
> -> Matrix Double
> -> Matrix Double
> -> Matrix Double
> -> Matrix Double
> -> Matrix Double
> -> [Vector Double]
> -> [(Vector Double, Matrix Double)]
> outer muPrior sigmaPrior bigH bigSigmaY bigA bigSigmaX ys = result
> where
> result = scanl update (muPrior, sigmaPrior) ys
>
> -- update :: (R n, Sq n) -> R m -> (R n, Sq n)
> update (xHatFlat, bigSigmaHatFlat) y =
> (xHatFlatNew, bigSigmaHatFlatNew)
> where
> -- v :: R m
> v = y - bigH #> xHatFlat
> -- bigS :: Sq m
> bigS = bigH <> bigSigmaHatFlat <> (tr bigH) + bigSigmaY
> -- bigK :: L n m
> bigK = bigSigmaHatFlat <> (tr bigH) <> (inv bigS)
> -- xHat :: R n
> xHat = xHatFlat + bigK #> v
> -- bigSigmaHat :: Sq n
> bigSigmaHat = bigSigmaHatFlat - bigK <> bigS <> (tr bigK)
> -- xHatFlatNew :: R n
> xHatFlatNew = bigA #> xHat
> -- bigSigmaHatFlatNew :: Sq n
> bigSigmaHatFlatNew = bigA <> bigSigmaHat <> (tr bigA) + bigSigmaX
We create some ranodm data using our model parameters.
> singleSample ::(Double, Double) ->
> RVarT (W.Writer [(Double, (Double, Double))]) (Double, Double)
> singleSample (xPrev, vPrev) = do
> psiX <- rvarT (Normal 0.0 stateVariance)
> let xNew = xPrev + deltaT * vPrev + psiX
> psiV <- rvarT (Normal 0.0 stateVariance)
> let vNew = vPrev + psiV
> upsilon <- rvarT (Normal 0.0 obsVariance)
> let y = a * xNew + upsilon
> lift $ W.tell [(y, (xNew, vNew))]
> return (xNew, vNew)
> streamSample :: RVarT (W.Writer [(Double, (Double, Double))]) (Double, Double)
> streamSample = iterateM_ singleSample (1.0, 1.0)
> samples :: ((Double, Double), [(Double, (Double, Double))])
> samples = W.runWriter (evalStateT (sample streamSample) (pureMT 2))
Here are the actual values of the randomly generated positions.
> actualXs :: [Double]
> actualXs = map (fst . snd) $ take nObs $ snd samples
> test :: [(Vector Double, Matrix Double)]
> test = outer muPrior sigmaPrior bigH bigSigmaY bigA bigSigmaX
> (map (\x -> vector [x]) $ map fst $ snd samples)
And using the Kalman filter we can estimate the positions.
> estXs :: [Double]
> estXs = map (!!0) $ map toList $ map fst $ take nObs test
> nObs :: Int
> nObs = 1000
And we can see that the estimates track the actual positions quite nicely.
Of course we really wanted to estimate the velocity.
> actualVs :: [Double]
> actualVs = map (snd . snd) $ take nObs $ snd samples
> estVs :: [Double]
> estVs = map (!!1) $ map toList $ map fst $ take nObs test
Boyd, Stephen. 2008. “EE363 Linear Dynamical Systems.” http://stanford.edu/class/ee363.
Kleeman, Lindsay. 1996. “Understanding and Applying Kalman Filtering.” In Proceedings of the Second Workshop on Perceptive Systems, Curtin University of Technology, Perth Western Australia (25-26 January 1996).
Särkkä, Simo. 2013. Bayesian Filtering and Smoothing. Vol. 3. Cambridge University Press.
Suppose we wish to estimate the mean of a sample drawn from a normal distribution. In the Bayesian approach, we know the prior distribution for the mean (it could be a non-informative prior) and then we update this with our observations to create the posterior, the latter giving us improved information about the distribution of the mean. In symbols
Typically, the samples are chosen to be independent, and all of the data is used to perform the update but, given independence, there is no particular reason to do that, updates can performed one at a time and the result is the same; nor is the order of update important. Being a bit imprecise, we have
The standard notation in Bayesian statistics is to denote the parameters of interest as and the observations as . For reasons that will become apparent in later blog posts, let us change notation and label the parameters as and the observations as .
Let us take a very simple example of a prior where is known and then sample from a normal distribution with mean and variance for the -th sample where is known (normally we would not know the variance but adding this generality would only clutter the exposition unnecessarily).
The likelihood is then
As we have already noted, instead of using this with the prior to calculate the posterior, we can update the prior with each observation separately. Suppose that we have obtained the posterior given samples (we do not know this is normally distributed yet but we soon will):
Then we have
Writing
and then completing the square we also obtain
Now let’s be a bit more formal about conditional probability and use the notation of -algebras to define and where , is as before and . We have previously calculated that and that and the tower law for conditional probabilities then allows us to conclude . By Jensen’s inequality, we have
Hence is bounded in and therefore converges in and almost surely to . The noteworthy point is that if if and only if converges to 0. Explicitly we have
which explains why we took the observations to have varying and known variances. You can read more in Williams’ book (Williams 1991).
We have reformulated our estimation problem as a very simple version of the celebrated Kalman filter. Of course, there are much more interesting applications of this but for now let us try “tracking” the sample from the random variable.
> {-# OPTIONS_GHC -Wall #-}
> {-# OPTIONS_GHC -fno-warn-name-shadowing #-}
> {-# OPTIONS_GHC -fno-warn-type-defaults #-}
> {-# OPTIONS_GHC -fno-warn-unused-do-bind #-}
> {-# OPTIONS_GHC -fno-warn-missing-methods #-}
> {-# OPTIONS_GHC -fno-warn-orphans #-}
> module FunWithKalmanPart1 (
> obs
> , nObs
> , estimates
> , uppers
> , lowers
> ) where
>
> import Data.Random.Source.PureMT
> import Data.Random
> import Control.Monad.State
> var, cSquared :: Double
> var = 1.0
> cSquared = 1.0
>
> nObs :: Int
> nObs = 100
> createObs :: RVar (Double, [Double])
> createObs = do
> x <- rvar (Normal 0.0 var)
> ys <- replicateM nObs $ rvar (Normal x cSquared)
> return (x, ys)
>
> obs :: (Double, [Double])
> obs = evalState (sample createObs) (pureMT 2)
>
> updateEstimate :: (Double, Double) -> (Double, Double) -> (Double, Double)
> updateEstimate (xHatPrev, varPrev) (y, cSquared) = (xHatNew, varNew)
> where
> varNew = recip (recip varPrev + recip cSquared)
> xHatNew = varNew * (y / cSquared + xHatPrev / varPrev)
>
> estimates :: [(Double, Double)]
> estimates = scanl updateEstimate (y, cSquared) (zip ys (repeat cSquared))
> where
> y = head $ snd obs
> ys = tail $ snd obs
>
> uppers :: [Double]
> uppers = map (\(x, y) -> x + 3 * (sqrt y)) estimates
>
> lowers :: [Double]
> lowers = map (\(x, y) -> x - 3 * (sqrt y)) estimates
Williams, David. 1991. Probability with Martingales. Cambridge University Press.
Imagine an insect, a grasshopper, trapped on the face of a clock which wants to visit each hour an equal number of times. However, there is a snag: it can only see the value of the hour it is on and the value of the hours immediately anti-clockwise and immediately clockwise. For example, if it is standing on 5 then it can see the 5, the 4, and the 6 but no others.
It can adopt the following strategy: toss a fair coin and move anti-clockwise for a head and move clockwise for a tail. Intuition tells us that over a large set of moves the grasshopper will visit each hour (approximately) the same number of times.
Can we confirm our intuition somehow? Suppose that the strategy has worked and the grasshopper is now to be found with equal probability on any hour. Then at the last jump, the grasshopper must either have been at the hour before the one it is now on or it must have been at the hour after the one it is now on. Let us denote the probability that the grasshopper is on hour by and the (conditional) probability that the grasshopper jumps to state given it was in state by . Then we have
Substituting in where is a normalising constant (12 in this case) we obtain
This tells us that the required distribution is a fixed point of the grasshopper’s strategy. But does the strategy actually converge to the fixed point? Let us perform an experiment.
First we import some modules from hmatrix.
> {-# LANGUAGE FlexibleContexts #-}
> module Chapter1 where
> import Data.Packed.Matrix
> import Numeric.LinearAlgebra.Algorithms
> import Numeric.Container
> import Data.Random
> import Control.Monad.State
> import qualified Control.Monad.Writer as W
> import qualified Control.Monad.Loops as ML
> import Data.Random.Source.PureMT
Let us use a clock with 5 hours to make the matrices sufficiently small to fit on one page.
Here is the strategy encoded as a matrix. For example the first row says jump to position 1 with probablity 0.5 or jump to position 5 with probability 0.5.
> eqProbsMat :: Matrix Double
> eqProbsMat = (5 >< 5)
> [ 0.0, 0.5, 0.0, 0.0, 0.5
> , 0.5, 0.0, 0.5, 0.0, 0.0
> , 0.0, 0.5, 0.0, 0.5, 0.0
> , 0.0, 0.0, 0.5, 0.0, 0.5
> , 0.5, 0.0, 0.0, 0.5, 0.0
> ]
We suppose the grasshopper starts at 1 o’clock.
> startOnOne :: Matrix Double
> startOnOne = ((1 >< 5) [1.0, 0.0, 0.0, 0.0, 0.0])
If we allow the grasshopper to hop 1000 times then we see that it is equally likely to be found on any hour hand with a 20% probability.
ghci> eqProbsMat
(5><5)
[ 0.0, 0.5, 0.0, 0.0, 0.5
, 0.5, 0.0, 0.5, 0.0, 0.0
, 0.0, 0.5, 0.0, 0.5, 0.0
, 0.0, 0.0, 0.5, 0.0, 0.5
, 0.5, 0.0, 0.0, 0.5, 0.0 ]
ghci> take 1 $ drop 1000 $ iterate (<> eqProbsMat) startOnOne
[(1><5)
[ 0.20000000000000007, 0.2, 0.20000000000000004, 0.20000000000000004, 0.2 ]]
In this particular case, the strategy does indeed converge.
Now suppose the grasshopper wants to visit each hour in proportion the value of the number on the hour. Lacking pen and paper (and indeed opposable thumbs), it decides to adopt the following strategy: toss a fair coin as in the previous strategy but only move if the number is larger than the one it is standing on; if, on the other hand, the number is smaller then choose a number at random from between 0 and 1 and move if this value is smaller than the ratio of the proposed hour and the hour on which it is standing otherwise stay put. For example, if the grasshopper is standing on 5 and gets a tail then it will move to 6 but if it gets a head then four fifths of the time it will move to 4 but one fifth of the time it will stay where it is.
Suppose that the strategy has worked (it is not clear that is has) and the grasshopper is now to be found at 12 o’clock 12 times as often as at 1 o’clock, at 11 o’clock 11 times as often as at 1 o’clock, etc. Then at the last jump, the grasshopper must either have been at the hour before the one it is now on, the hour after the one it is now on or the same hour it is now on. Let us denote the probability that the grasshopper is on hour by .
Substituting in at 4 say
The reader can check that this relationship holds for all other hours. This tells us that the required distribution is a fixed point of the grasshopper’s strategy. But does this strategy actually converge to the fixed point?
Again, let us use a clock with 5 hours to make the matrices sufficiently small to fit on one page.
Here is the strategy encoded as a matrix. For example the first row says jump to position 1 with probablity 0.5 or jump to position 5 with probability 0.5.
> incProbsMat :: Matrix Double
> incProbsMat = scale 0.5 $
> (5 >< 5)
> [ 0.0, 1.0, 0.0, 0.0, 1.0
> , 1.0/2.0, 1.0/2.0, 1.0, 0.0, 0.0
> , 0.0, 2.0/3.0, 1.0/3.0, 1.0, 0.0
> , 0.0, 0.0, 3.0/4.0, 1.0/4.0, 1.0
> , 1.0/5.0, 0.0, 0.0, 4.0/5.0, 1.0/5.0 + 4.0/5.0
> ]
We suppose the grasshopper starts at 1 o’clock.
If we allow the grasshopper to hop 1000 times then we see that it is equally likely to be found on any hour hand with a probability of times the probability of being found on 1.
ghci> incProbsMat
(5><5)
[ 0.0, 0.5, 0.0, 0.0, 0.5
, 0.25, 0.25, 0.5, 0.0, 0.0
, 0.0, 0.3333333333333333, 0.16666666666666666, 0.5, 0.0
, 0.0, 0.0, 0.375, 0.125, 0.5
, 0.1, 0.0, 0.0, 0.4, 0.5 ]
ghci> take 1 $ drop 1000 $ iterate (<> incProbsMat) startOnOne
[(1><5)
[ 6.666666666666665e-2, 0.1333333333333333, 0.19999999999999996, 0.2666666666666666, 0.33333333333333326 ]]
In this particular case, the strategy does indeed converge.
Surprisingly, this strategy produces the desired result and is known as the Metropolis Algorithm. What the grasshopper has done is to construct a (discrete) Markov Process which has a limiting distribution (the stationary distribution) with the desired feature: sampling from this process will result in each hour being sampled in proportion to its value.
Let us examine what is happening in a bit more detail.
The grasshopper has started with a very simple Markov Chain: one which jumps clockwise or anti-clockwise with equal probability and then modified it. But what is a Markov Chain?
A time homogeneous Markov chain is a countable sequence of random variables
such that
We sometimes say that a Markov Chain is discrete time stochastic process with the above property.
So the very simple Markov Chain can be described by
The grasshopper knows that so it can calculate without knowing . This is important because now, without knowing , the grasshopper can evaluate
where takes the maximum of its arguments. Simplifying the above by substituing in the grasshopper’s probabilities and noting that is somewhat obscure way of saying jump clockwise or anti-clockwise we obtain
In most studies of Markov chains, one is interested in whether a chain has a stationary distribution. What we wish to do is take a distribution and create a chain with this distribution as its stationary distribution. We will still need to show that our chain does indeed have the correct stationary distribution and we state the relevant theorem somewhat informally and with no proof.
An irreducible, aperiodic and positive recurrent Markov chain has a unique stationary distribution.
Roughly speaking
Irreducible means it is possible to get from any state to any other state.
Aperiodic means that returning to a state having started at that state occurs at irregular times.
Positive recurrent means that the first time to hit a state is finite (for every state and more pedantically except on sets of null measure).
Note that the last condition is required when the state space is infinite – see Skrikant‘s lecture notes for an example and also for a more formal definition of the theorem and its proof.
Let be a probability distribution on the state space with for all and let be an ergodic Markov chain on with transition probabilities (the latter condition is slightly stronger than it need be but we will not need fully general conditions).
Create a new (ergodic) Markov chain with transition probabilities
where takes the maximum of its arguments.
Calculate the value of interest on the state space e.g. the total magnetization for each step produced by this new chain.
Repeat a sufficiently large number of times and take the average. This gives the estimate of the value of interest.
Let us first note that the Markov chain produced by this algorithm almost trivially satisfies the detailed balance condition, for example,
Secondly since we have specified that is ergodic then clearly is also ergodic (all the transition probabilities are ).
So we know the algorithm will converge to the unique distribution we specified to provide estimates of values of interest.
For simplicity let us consider a model with two parameters and that we sample from either parameter with equal probability. In this sampler, We update the parameters in a single step.
The transition density kernel is then given by
where is the Dirac delta function.
This sampling scheme satisifies the detailed balance condition. We have
In other words
Hand waving slightly, we can see that this scheme satisfies the premises of the ergodic theorem and so we can conclude that there is a unique stationary distribution and must be that distribution.
Most references on Gibbs sampling do not describe the random scan but instead something called a systematic scan.
Again for simplicity let us consider a model with two parameters. In this sampler, we update the parameters in two steps.
We observe that this is not time-homegeneous; at each step the transition matrix flips between the two transition matrices given by the individual steps. Thus although, as we show below, each individual transtion satisifies the detailed balance condition, we cannot apply the ergodic theorem as it only applies to time-homogeneous processes.
The transition density kernel is then given by
where .
Thus
Suppose that we have two states and and that . Then . Trivially we have
Now suppose that
So again we have
Similarly we can show
But note that
whereas
and these are not necessarily equal.
So the detailed balance equation is not satisfied, another sign that we cannot appeal to the ergodic theorem.
Let us demonstrate the Gibbs sampler with a distribution which we actually know: the bivariate normal.
The conditional distributions are easily calculated to be
Let’s take a correlation of 0.8, a data point of (0.0, 0.0) and start the chain at (2.5, 2.5).
> rho :: Double
> rho = 0.8
>
> y :: (Double, Double)
> y = (0.0, 0.0)
>
> y1, y2 :: Double
> y1 = fst y
> y2 = snd y
>
> initTheta :: (Double, Double)
> initTheta = (2.5, 2.5)
We pre-calculate the variance needed for the sampler.
> var :: Double
> var = 1.0 - rho^2
In Haskell and in the random-fu package, sampling from probability distributions is implemented as a monad. We sample from the relevant normal distributions and keep the trajectory using a writer monad.
> gibbsSampler :: Double -> RVarT (W.Writer [(Double,Double)]) Double
> gibbsSampler oldTheta2 = do
> newTheta1 <- rvarT (Normal (y1 + rho * (oldTheta2 - y2)) var)
> lift $ W.tell [(newTheta1, oldTheta2)]
> newTheta2 <- rvarT (Normal (y2 + rho * (newTheta1 - y1)) var)
> lift $ W.tell [(newTheta1, newTheta2)]
> return $ newTheta2
It is common to allow the chain to “burn in” so as to “forget” its starting position. We arbitrarily burn in for 10,000 steps.
> burnIn :: Int
> burnIn = 10000
We sample repeatedly from the sampler using the monadic form of iterate. Running the monadic stack is slightly noisy but nonetheless straightforward. We use mersenne-random-pure64 (albeit indirectly via random-source) as our source of entropy.
> runMCMC :: Int -> [(Double, Double)]
> runMCMC n =
> take n $
> drop burnIn $
> snd $
> W.runWriter (evalStateT (sample (ML.iterateM_ gibbsSampler (snd initTheta))) (pureMT 2))
We can look at the trajectory of our sampler for various run lengths.
For bigger sample sizes, plotting the distribution sampled re-assures us that we are indeed sampling from a bivariate normal distribution as the theory predicted.
Some of what is here and here excluding JAGS and STAN (after all this is a book about Haskell).
Applications to Physics
Most of what is here.
Let and be Hilbert spaces then as vector spaces we can form the tensor product . The tensor product can be defined as the free vector space on and as sets (that is purely formal sums of ) modulo a relation defined by
Slightly overloading notation, we can define an inner product on the tensored space by
Of course this might not be complete so we define the tensor product on Hilbert spaces to be the completion of this inner product.
For Hilbert spaces to form a monoidal category, we take the arrows (in the categorical sense) to be linear continuous maps and the bifunctor to be the tensor product. We also need an identity object which we take to be considered as a Hilbert space. We should check the coherence conditions but the associativity of the tensor product and the fact that our Hilbert spaces are over the make this straightforward.
Now for some slightly interesting properties of this category.
The tensor product is not the product in the categorical sense. If and are (orthonormal) bases for and then is a (orthonormal) basis for . Thus a linear combination of basis vectors in the tensor product cannot be expressed as the tensor of basis vectors in the component spaces.
There is no diagonal arrow . Suppose there were such a diagonal then for arbitrary we would have and since must be linear this is not possible.
Presumably the latter is equivalent to the statement in quantum mechanics of “no cloning”.
I have seen Hölder’s inequality and Minkowski’s inequality proved in several ways but this seems the most perspicuous (to me at any rate).
If and such that
then
A and satisfying the premise are known as conjugate indices.
Proof
Since is convex we have
Substituting in appropriate values gives
or
Now take exponents.
Let and be conjugate indices with and let and then and
Proof
By Young’s inequality
By applying a counting measure to we also obtain
Proof
By Hölder’s inequality
where
and is finite since is a vector space.
It’s possible to Gibbs sampling in most languages and since I am doing some work in R and some work in Haskell, I thought I’d present a simple example in both languages: estimating the mean from a normal distribution with unknown mean and variance. Although one can do Gibbs sampling directly in R, it is more common to use a specialised language such as JAGS or STAN to do the actual sampling and do pre-processing and post-processing in R. This blog post presents implementations in native R, JAGS and STAN as well as Haskell.
> {-# OPTIONS_GHC -Wall #-}
> {-# OPTIONS_GHC -fno-warn-name-shadowing #-}
> {-# OPTIONS_GHC -fno-warn-type-defaults #-}
> {-# OPTIONS_GHC -fno-warn-unused-do-bind #-}
> {-# OPTIONS_GHC -fno-warn-missing-methods #-}
> {-# OPTIONS_GHC -fno-warn-orphans #-}
> {-# LANGUAGE NoMonomorphismRestriction #-}
> module Gibbs (
> main
> , m
> , Moments(..)
> ) where
>
> import qualified Data.Vector.Unboxed as V
> import qualified Control.Monad.Loops as ML
> import Data.Random.Source.PureMT
> import Data.Random
> import Control.Monad.State
> import Data.Histogram ( asList )
> import Data.Histogram.Fill
> import Data.Histogram.Generic ( Histogram )
> import Data.List
> import qualified Control.Foldl as L
>
> import Diagrams.Backend.Cairo.CmdLine
>
> import LinRegAux
>
> import Diagrams.Backend.CmdLine
> import Diagrams.Prelude hiding ( sample, render )
The length of our chain and the burn-in.
> nrep, nb :: Int
> nb = 5000
> nrep = 105000
Data generated from .
> xs :: [Double]
> xs = [
> 11.0765808082301
> , 10.918739177542
> , 15.4302462747137
> , 10.1435649220266
> , 15.2112705014697
> , 10.441327659703
> , 2.95784054883142
> , 10.2761068139607
> , 9.64347295100318
> , 11.8043359297675
> , 10.9419989262713
> , 7.21905367667346
> , 10.4339807638017
> , 6.79485294803006
> , 11.817248658832
> , 6.6126710570584
> , 12.6640920214508
> , 8.36604701073303
> , 12.6048485320333
> , 8.43143879537592
> ]
For a multi-parameter situation, Gibbs sampling is a special case of Metropolis-Hastings in which the proposal distributions are the posterior conditional distributions.
Referring back to the explanation of the metropolis algorithm, let us describe the state by its parameters and the conditional posteriors by where then
where we have used the rules of conditional probability and the fact that
Thus we always accept the proposed jump. Note that the chain is not in general reversible as the order in which the updates are done matters.
It is fairly standard to use an improper prior
The likelihood is
re-writing in terms of precision
Thus the posterior is
We can re-write the sum in terms of the sample mean and variance using
Thus the conditional posterior for is
which we recognise as a normal distribution with mean of and a variance of .
The conditional posterior for is
which we recognise as a gamma distribution with a shape of and a scale of
In this particular case, we can calculate the marginal posterior of analytically. Writing we have
Finally we can calculate
This is the non-standardized Student’s t-distribution .
Alternatively the marginal posterior of is
where is the standard t distribution with degrees of freedom.
Following up on a comment from a previous blog post, let us try using the foldl package to calculate the length, the sum and the sum of squares traversing the list only once. An improvement on creating your own strict record and using foldl’ but maybe it is not suitable for some methods e.g. calculating the skewness and kurtosis incrementally, see below.
> x2Sum, xSum, n :: Double
> (x2Sum, xSum, n) = L.fold stats xs
> where
> stats = (,,) <$>
> (L.premap (\x -> x * x) L.sum) <*>
> L.sum <*>
> L.genericLength
And re-writing the sample variance using
we can then calculate the sample mean and variance using the sums we have just calculated.
> xBar, varX :: Double
> xBar = xSum / n
> varX = n * (m2Xs - xBar * xBar) / (n - 1)
> where m2Xs = x2Sum / n
In random-fu, the Gamma distribution is specified by the rate paratmeter, .
> beta, initTau :: Double
> beta = 0.5 * n * varX
> initTau = evalState (sample (Gamma (n / 2) beta)) (pureMT 1)
Our sampler takes an old value of and creates new values of and .
> gibbsSampler :: MonadRandom m => Double -> m (Maybe ((Double, Double), Double))
> gibbsSampler oldTau = do
> newMu <- sample (Normal xBar (recip (sqrt (n * oldTau))))
> let shape = 0.5 * n
> scale = 0.5 * (x2Sum + n * newMu^2 - 2 * n * newMu * xBar)
> newTau <- sample (Gamma shape (recip scale))
> return $ Just ((newMu, newTau), newTau)
From which we can create an infinite stream of samples.
> gibbsSamples :: [(Double, Double)]
> gibbsSamples = evalState (ML.unfoldrM gibbsSampler initTau) (pureMT 1)
As our chains might be very long, we calculate the mean, variance, skewness and kurtosis using an incremental method.
> data Moments = Moments { mN :: !Double
> , m1 :: !Double
> , m2 :: !Double
> , m3 :: !Double
> , m4 :: !Double
> }
> deriving Show
> moments :: [Double] -> Moments
> moments xs = foldl' f (Moments 0.0 0.0 0.0 0.0 0.0) xs
> where
> f :: Moments -> Double -> Moments
> f m x = Moments n' m1' m2' m3' m4'
> where
> n = mN m
> n' = n + 1
> delta = x - (m1 m)
> delta_n = delta / n'
> delta_n2 = delta_n * delta_n
> term1 = delta * delta_n * n
> m1' = m1 m + delta_n
> m4' = m4 m +
> term1 * delta_n2 * (n'*n' - 3*n' + 3) +
> 6 * delta_n2 * m2 m - 4 * delta_n * m3 m
> m3' = m3 m + term1 * delta_n * (n' - 2) - 3 * delta_n * m2 m
> m2' = m2 m + term1
In order to examine the posterior, we create a histogram.
> numBins :: Int
> numBins = 400
> hb :: HBuilder Double (Data.Histogram.Generic.Histogram V.Vector BinD Double)
> hb = forceDouble -<< mkSimple (binD lower numBins upper)
> where
> lower = xBar - 2.0 * sqrt varX
> upper = xBar + 2.0 * sqrt varX
And fill it with the specified number of samples preceeded by a burn-in.
> hist :: Histogram V.Vector BinD Double
> hist = fillBuilder hb (take (nrep - nb) $ drop nb $ map fst gibbsSamples)
Now we can plot this.
And calculate the skewness and kurtosis.
> m :: Moments
> m = moments (take (nrep - nb) $ drop nb $ map fst gibbsSamples)
ghci> import Gibbs
ghci> putStrLn $ show $ (sqrt (mN m)) * (m3 m) / (m2 m)**1.5
8.733959917065126e-4
ghci> putStrLn $ show $ (mN m) * (m4 m) / (m2 m)**2
3.451374739494607
We expect a skewness of 0 and a kurtosis of for . Not too bad.
JAGS is a mature, declarative, domain specific language for building Bayesian statistical models using Gibbs sampling.
Here is our model as expressed in JAGS. Somewhat terse.
model {
for (i in 1:N) {
x[i] ~ dnorm(mu, tau)
}
mu ~ dnorm(0, 1.0E-6)
tau <- pow(sigma, -2)
sigma ~ dunif(0, 1000)
}
To run it and examine its results, we wrap it up in some R
## Import the library that allows R to inter-work with jags.
library(rjags)
## Read the simulated data into a data frame.
fn <- read.table("example1.data", header=FALSE)
jags <- jags.model('example1.bug',
data = list('x' = fn[,1], 'N' = 20),
n.chains = 4,
n.adapt = 100)
## Burnin for 10000 samples
update(jags, 10000);
mcmc_samples <- coda.samples(jags, variable.names=c("mu", "sigma"), n.iter=20000)
png(file="diagrams/jags.png",width=400,height=350)
plot(mcmc_samples)
dev.off()
And now we can look at the posterior for .
STAN is a domain specific language for building Bayesian statistical models similar to JAGS but newer and which allows variables to be re-assigned and so cannot really be described as declarative.
Here is our model as expressed in STAN. Again, somewhat terse.
data {
int<lower=0> N;
real x[N];
}
parameters {
real mu;
real<lower=0,upper=1000> sigma;
}
model{
x ~ normal(mu, sigma);
mu ~ normal(0, 1000);
}
Just as with JAGS, to run it and examine its results, we wrap it up in some R.
library(rstan)
## Read the simulated data into a data frame.
fn <- read.table("example1.data", header=FALSE)
## Running the model
fit1 <- stan(file = 'Stan.stan',
data = list('x' = fn[,1], 'N' = 20),
pars=c("mu", "sigma"),
chains=3,
iter=30000,
warmup=10000)
png(file="diagrams/stan.png",width=400,height=350)
plot(fit1)
dev.off()
Again we can look at the posterior although we only seem to get medians and 80% intervals.
Write the histogram produced by the Haskell code to a file.
> displayHeader :: FilePath -> Diagram B R2 -> IO ()
> displayHeader fn =
> mainRender ( DiagramOpts (Just 900) (Just 700) fn
> , DiagramLoopOpts False Nothing 0
> )
> main :: IO ()
> main = do
> displayHeader "diagrams/DataScienceHaskPost.png"
> (barDiag
> (zip (map fst $ asList hist) (map snd $ asList hist)))
The code can be downloaded from github.
The other speaker at the Machine Learning Meetup at which I gave my talk on automatic differentiation gave a very interesting talk on A/B testing. Apparently this is big business these days as attested by the fact I got 3 ads above the wikipedia entry when I googled for it.
It seems that people tend to test with small sample sizes and to do so very often, resulting in spurious results. Of course readers of XKCD will be well aware of some of the pitfalls.
I thought a Bayesian approach might circumvent some of the problems and set out to write a blog article only to discover that there was no Haskell library for sampling from Student’s t. Actually there was one but is currently an unreleased part of random-fu. So I set about fixing this shortfall.
I thought I had better run a few tests so I calculated the sampled mean, variance, skewness and kurtosis.
I wasn’t really giving this my full attention and as a result ran into a few problems with space. I thought these were worth sharing and that is what this blog post is about. Hopefully, I will have time soon to actually blog about the Bayesian equivalent of A/B testing.
> {-# OPTIONS_GHC -Wall #-}
> {-# OPTIONS_GHC -fno-warn-name-shadowing #-}
> {-# OPTIONS_GHC -fno-warn-type-defaults #-}
> {-# OPTIONS_GHC -fno-warn-unused-do-bind #-}
> {-# OPTIONS_GHC -fno-warn-missing-methods #-}
> {-# OPTIONS_GHC -fno-warn-orphans #-}
>
> {-# LANGUAGE NoMonomorphismRestriction #-}
>
> module StudentTest (
> main
> ) where
>
> import qualified Data.Vector.Unboxed as V
> import Data.Random.Source.PureMT
> import Data.Random
> import Data.Random.Distribution.T
> import Control.Monad.State
> import Data.Histogram.Fill
> import Data.Histogram.Generic ( Histogram )
> import Data.List
Let’s create a reasonable number of samples as the higher moments converge quite slowly.
> nSamples :: Int
> nSamples = 1000000
An arbitrary seed for creating the samples.
> arbSeed :: Int
> arbSeed = 8
Student’s t only has one parameter, the number of degrees of freedom.
> nu :: Integer
> nu = 6
Now we can do our tests by calculating the sampled values.
> ts :: [Double]
> ts =
> evalState (replicateM nSamples (sample (T nu)))
> (pureMT $ fromIntegral arbSeed)
> mean, variance, skewness, kurtosis :: Double
> mean = (sum ts) / fromIntegral nSamples
> variance = (sum (map (**2) ts)) / fromIntegral nSamples
> skewness = (sum (map (**3) ts) / fromIntegral nSamples) / variance**1.5
> kurtosis = (sum (map (**4) ts) / fromIntegral nSamples) / variance**2
This works fine for small sample sizes but not for the number we have chosen.
./StudentTest +RTS -hc
Stack space overflow: current size 8388608 bytes.
Use `+RTS -Ksize -RTS' to increase it.
It seems a shame that the function in the Prelude has this behaviour but never mind let us ensure that we consume values strictly (they are being produced lazily).
> mean' = (foldl' (+) 0 ts) / fromIntegral nSamples
> variance' = (foldl' (+) 0 (map (**2) ts)) / fromIntegral nSamples
> skewness' = (foldl' (+) 0 (map (**3) ts) / fromIntegral nSamples) / variance'**1.5
> kurtosis' = (foldl' (+) 0 (map (**4) ts) / fromIntegral nSamples) / variance'**2
We now have a space leak on the heap as using the ghc profiler below shows. What went wrong?
If we only calculate the mean using foldl then all is well. Instead of 35M we only use 45K.
Well that gives us a clue. The garbage collector cannot reclaim the samples as they are needed for other calculations. What we need to do is calculate the moments strictly altogether.
Let’s create a strict record to do this.
> data Moments = Moments { m1 :: !Double
> , m2 :: !Double
> , m3 :: !Double
> , m4 :: !Double
> }
> deriving Show
And calculate the results strictly.
>
> m = foldl' (\m x -> Moments { m1 = m1 m + x
> , m2 = m2 m + x**2
> , m3 = m3 m + x**3
> , m4 = m4 m + x**4
> }) (Moments 0.0 0.0 0.0 0.0) ts
>
> mean'' = m1 m / fromIntegral nSamples
> variance'' = m2 m / fromIntegral nSamples
> skewness'' = (m3 m / fromIntegral nSamples) / variance''**1.5
> kurtosis'' = (m4 m / fromIntegral nSamples) / variance''**2
Now we have what we want; the program runs in small constant space.
> main :: IO ()
> main = do
> putStrLn $ show mean''
> putStrLn $ show variance''
> putStrLn $ show skewness''
> putStrLn $ show kurtosis''
Oh and the moments give the expected answers.
ghci> mean''
3.9298418844289093e-4
ghci> variance''
1.4962681916693004
ghci> skewness''
1.0113188204317015e-2
ghci> kurtosis''
5.661776268997382
To run this you will need my version of random-fu. The code for this article is here. You will need to compile everything with profiling, something like
ghc -O2 -main-is StudentTest StudentTest.lhs -prof
-package-db=.cabal-sandbox/x86_64-osx-ghc-7.6.2-packages.conf.d
Since you need all the packages to be built with profiling, you will probably want to build using a sandbox as above. The only slightly tricky aspect is building random-fu so it is in your sandbox.
runghc Setup.lhs configure --enable-library-profiling
--package-db=/HasBayes/.cabal-sandbox/x86_64-osx-ghc-7.6.2-packages.conf.d
--libdir=/HasBayes/.cabal-sandbox/lib