packages feed

rhine-bayes (empty) → 0.8.1.1

raw patch · 8 files changed

+629/−0 lines, 8 filesdep +basedep +dunaidep +log-domainsetup-changed

Dependencies added: base, dunai, log-domain, mmorph, monad-bayes, rhine, rhine-bayes, rhine-gloss, time, transformers

Files

+ ChangeLog.md view
@@ -0,0 +1,7 @@+# Revision history for rhine-gloss++## 0.8.1.1++* First version. Version numbers follow rhine.+* Introduces basic stochastic processes and Sequential Monte Carlo particle filter+* Thank you, Reuben Cohn-Gordon and Dominic Steinitz
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2017, Manuel Bärenz++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Manuel Bärenz nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,12 @@+# README++This package connects `rhine` to the [`monad-bayes`](hackage.haskell.org/package/monad-bayes) library for probabilistic programming and inference.+It provides:++* Some standard stochastic processes such as Brownian Motion and Levý processes+* A particle filter inference method called Sequential Monte Carlo++This allows you to do interactive probabilistic (i.e. involving randomness) programs,+and at the same time perform online inference, or realtime machine learning.+An example for this is given in `rhine-bayes/app/Main.hs`,+where inference is performed both on simulated values as well as external input given by the user.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/Main.hs view
@@ -0,0 +1,351 @@+{- | Interactive machine learning example.++In this example, you will find the following:++* A stochastic movement of a ball in heat bath,+  where the ball is kicked around in Brownian motion+* A noisy sensor observing the ball+* Particle filter inference trying to recover the actual (latent) position of the ball,+  as well as the temperature+* Visualization of the simulation and the inference result+* Two different architectures for the whole application:+  * A simple, noninteractive architecture where simulation, inference and visualization all run synchronously+  * A more scalable, modular, interactive architecture, where all these three systems run on separate clocks,+    and the user can interactively change the temperature of the heat bath+-}++-- base+import Control.Monad (void)+import Data.Maybe (fromMaybe)+import Data.Monoid (Product (Product, getProduct))+import GHC.Float (double2Float, float2Double)+import Text.Printf (printf)++-- transformers+import Control.Monad.Trans.Class++-- time+import Data.Time (addUTCTime, getCurrentTime)++-- mmorph+import Control.Monad.Morph++-- log-domain+import Numeric.Log hiding (sum)++-- monad-bayes+import Control.Monad.Bayes.Class hiding (posterior, prior)+import Control.Monad.Bayes.Population hiding (hoist)+import Control.Monad.Bayes.Sampler.Strict++-- rhine+import FRP.Rhine++-- rhine-gloss+import FRP.Rhine.Gloss.IO++-- rhine-bayes+import FRP.Rhine.Bayes+import FRP.Rhine.Gloss.Common++type Temperature = Double+type Pos = (Double, Double)+type Sensor = Pos++-- * Model++-- ** Prior++-- | Harmonic oscillator with white noise+prior1d ::+  (MonadDistribution m, Diff td ~ Double) =>+  -- | Starting position+  Double ->+  -- | Starting velocity+  Double ->+  BehaviourF m td Temperature Double+prior1d initialPosition initialVelocity = feedback 0 $ proc (temperature, position') -> do+  impulse <- arrM (normal 0) -< temperature+  let acceleration = (-3) * position' + impulse+  -- Integral over roughly the last 100 seconds, dying off exponentially, as to model a small friction term+  velocity <- arr (+ initialVelocity) <<< decayIntegral 10 -< acceleration+  position <- integralFrom initialPosition -< velocity+  returnA -< (position, position)++-- | 2D harmonic oscillator with noise+prior :: (MonadDistribution m, Diff td ~ Double) => BehaviourF m td Temperature Pos+prior = prior1d 10 0 &&& prior1d 0 10++-- ** Observation++-- | Internal utility because `gloss` operates on floats+double2FloatTuple :: (Double, Double) -> (Float, Float)+double2FloatTuple = double2Float *** double2Float++-- | An integral where the integrated value dies of exponentially+decayIntegral :: (VectorSpace v (Diff td), Monad m, Floating (Diff td)) => Diff td -> BehaviourF m td v v+decayIntegral timeConstant = (timeConstant *^) <$> average timeConstant++-- | The assumed standard deviation of the sensor noise+sensorNoiseTemperature :: Double+sensorNoiseTemperature = 1++-- | A generative model of the sensor noise+noise :: MonadDistribution m => Behaviour m td Pos+noise = whiteNoise sensorNoiseTemperature &&& whiteNoise sensorNoiseTemperature++-- | A generative model of the sensor position, given the noise+generativeModel :: (MonadDistribution m, Diff td ~ Double) => BehaviourF m td Pos Sensor+generativeModel = proc latent -> do+  noiseNow <- noise -< ()+  returnA -< latent ^+^ noiseNow++{- | This remodels the distribution defined by `noise` as a PDF,+   as to be used in the inference later.+-}+sensorLikelihood :: Pos -> Sensor -> Log Double+sensorLikelihood (posX, posY) (sensorX, sensorY) = normalPdf posX sensorNoiseTemperature sensorX * normalPdf posY sensorNoiseTemperature sensorY++-- ** User behaviour++-- | The initial value for the temperature, and also the initial guess for the temperature inference+initialTemperature :: Temperature+initialTemperature = 7++-- | We infer the temperature by randomly moving around with a Brownian motion (Wiener process).+temperatureProcess :: (MonadDistribution m, Diff td ~ Double) => BehaviourF m td () Temperature+temperatureProcess = proc () -> do+  temperatureFactor <- wienerLogDomain 20 -< ()+  returnA -< runLogDomain temperatureFactor * initialTemperature++-- | Auxiliary conversion function belonging to the log-domain library, see https://github.com/ekmett/log-domain/issues/38+runLogDomain :: Log Double -> Double+runLogDomain = exp . ln++-- * Filtering++{- | Generate a random position and sensor value, given a temperature.+   Used for simulating a situation upon which we will perform inference.+-}+genModelWithoutTemperature :: (MonadDistribution m, Diff td ~ Double) => BehaviourF m td Temperature (Sensor, Pos)+genModelWithoutTemperature = proc temperature -> do+  latent <- prior -< temperature+  sensor <- generativeModel -< latent+  returnA -< (sensor, latent)++{- | Given sensor data, sample a latent position and a temperature, and weight them according to the likelihood of the observed sensor position.+   Used to infer position and temperature.+-}+posteriorTemperatureProcess :: (MonadMeasure m, Diff td ~ Double) => BehaviourF m td Sensor (Temperature, Pos)+posteriorTemperatureProcess = proc sensor -> do+  temperature <- temperatureProcess -< ()+  latent <- prior -< temperature+  arrM score -< sensorLikelihood latent sensor+  returnA -< (temperature, latent)++-- | A collection of all displayable inference results+data Result = Result+  { temperature :: Temperature+  , measured :: Sensor+  , latent :: Pos+  , particles :: [((Temperature, Pos), Log Double)]+  }+  deriving (Show)++-- | The number of particles used in the filter. Change according to available computing power.+nParticles :: Int+nParticles = 100++-- * Visualization++{- | The monad in which our program will run.+   'SamplerIO' is for the probabilistic effects from @monad-bayes@,+   while 'GlossConcT' adds interactive effects from @gloss@.+-}+type App = GlossConcT SamplerIO++-- | Draw the results of the simulation and inference+visualisation :: Diff td ~ Double => BehaviourF App td Result ()+visualisation = proc Result{temperature, measured, latent, particles} -> do+  constMCl clearIO -< ()+  time <- sinceInitS -< ()+  arrMCl paintIO+    -<+      toThermometer $+        pictures+          [ translate 0 (-40) $ scale 0.2 0.2 $ color white $ pictures $ do+              (n, message) <-+                zip+                  [0 ..]+                  [ printf "Temperature: %.2f" temperature+                  , printf "Particles: %i" $ length particles+                  , printf "Time: %.1f" time+                  ]+              return $ translate 0 ((-150) * n) $ text message+          , color red $ rectangleUpperSolid thermometerWidth $ double2Float temperature * thermometerScale+          ]+  drawBall -< (measured, 0.3, red)+  drawBall -< (latent, 0.3, green)+  drawParticles -< particles++-- ** Parameters for the temperature display++thermometerPos :: (Float, Float)+thermometerPos = (-300, -300)++toThermometer :: Picture -> Picture+toThermometer = uncurry translate thermometerPos++thermometerScale :: Float+thermometerScale = 20++thermometerWidth :: Float+thermometerWidth = 20++-- ** Helpers for drawing elements of the visualization++drawBall :: BehaviourF App td (Pos, Double, Color) ()+drawBall = proc (position, width, theColor) -> do+  arrMCl paintIO -< scale 20 20 $ uncurry translate (double2FloatTuple position) $ color theColor $ circleSolid $ double2Float width++drawParticle :: BehaviourF App td ((Temperature, Pos), Log Double) ()+drawParticle = proc ((temperature, position), probability) -> do+  drawBall -< (position, 0.1, withAlpha (double2Float $ exp $ 0.2 * ln probability) white)+  arrMCl paintIO -< toThermometer $ translate 0 (double2Float temperature * thermometerScale) $ color (withAlpha (double2Float $ exp $ 0.2 * ln probability) white) $ rectangleSolid thermometerWidth 2++drawParticles :: BehaviourF App td [((Temperature, Pos), Log Double)] ()+drawParticles = proc particles -> do+  case particles of+    [] -> returnA -< ()+    p : ps -> do+      drawParticle -< p+      drawParticles -< ps++glossSettings :: GlossSettings+glossSettings =+  defaultSettings+    { display = InWindow "rhine-bayes" (1024, 960) (10, 10)+    }++-- * Integration++-- | There are different architectural choices for the whole application, these can be tested against each other+mains :: [(String, IO ())]+mains =+  [ ("single rate", mainSingleRate)+  , ("multi rate, temperature process", mainMultiRate)+  ]++main :: IO ()+main = do+  putStrLn $ ("Choose between: " ++) $ unwords $ zipWith (\n (title, _program) -> "\n" ++ show n ++ ": " ++ title) [1 ..] mains+  choice <- read <$> getLine+  map snd mains !! (choice - 1)++-- ** Single-rate : One simulation step = one inference step = one display step++{- | Given an actual temperature, simulate a latent position and measured sensor position,+   and based on the sensor data infer the latent position and the temperature.+-}+filtered :: Diff td ~ Double => BehaviourF App td Temperature Result+filtered = proc temperature -> do+  (measured, latent) <- genModelWithoutTemperature -< temperature+  particles <- runPopulationCl nParticles resampleSystematic posteriorTemperatureProcess -< measured+  returnA+    -<+      Result+        { temperature+        , measured+        , latent+        , particles+        }++-- | Run simulation, inference, and visualization synchronously+mainClSF :: Diff td ~ Double => BehaviourF App td () ()+mainClSF = proc () -> do+  output <- filtered -< initialTemperature+  visualisation -< output++-- | Rescale to the 'Double' time domain+type GlossClock = RescaledClock GlossSimClockIO Double++glossClock :: GlossClock+glossClock =+  RescaledClock+    { unscaledClock = GlossSimClockIO+    , rescale = float2Double+    }++mainSingleRate =+  void $+    sampleIO $+      launchGlossThread glossSettings $+        reactimateCl glossClock mainClSF++-- ** Multi-rate: Simulation, inference, display at different rates++-- | Rescale the gloss clocks so they will be compatible with real 'UTCTime' (needed for compatibility with 'Millisecond')+type GlossClockUTC cl = RescaledClockS (GlossConcT IO) cl UTCTime (Tag cl)++glossClockUTC :: Real (Time cl) => cl -> GlossClockUTC cl+glossClockUTC cl =+  RescaledClockS+    { unscaledClockS = cl+    , rescaleS = const $ do+        now <- liftIO getCurrentTime+        return (arr $ \(timePassed, event) -> (addUTCTime (realToFrac timePassed) now, event), now)+    }++{- | The part of the program which simulates latent position and sensor,+   running 100 times a second.+-}+modelRhine :: Rhine (GlossConcT IO) (LiftClock IO GlossConcT (Millisecond 100)) Temperature (Temperature, (Sensor, Pos))+modelRhine = hoistClSF sampleIOGloss (clId &&& genModelWithoutTemperature) @@ liftClock waitClock++-- | The user can change the temperature by pressing the up and down arrow keys.+userTemperature :: ClSF (GlossConcT IO) (GlossClockUTC GlossEventClockIO) () Temperature+userTemperature = tagS >>> arr (selector >>> fmap Product) >>> mappendS >>> arr (fmap getProduct >>> fromMaybe 1 >>> (* initialTemperature))+ where+  selector (EventKey (SpecialKey KeyUp) Down _ _) = Just 1.2+  selector (EventKey (SpecialKey KeyDown) Down _ _) = Just (1 / 1.2)+  selector _ = Nothing++{- | This part performs the inference (and passes along temperature, sensor and position simulations).+   It runs as fast as possible, so this will potentially drain the CPU.+-}+inference :: Rhine (GlossConcT IO) (LiftClock IO GlossConcT Busy) (Temperature, (Sensor, Pos)) Result+inference = hoistClSF sampleIOGloss inferenceBehaviour @@ liftClock Busy+ where+  inferenceBehaviour :: (MonadDistribution m, Diff td ~ Double, MonadIO m) => BehaviourF m td (Temperature, (Sensor, Pos)) Result+  inferenceBehaviour = proc (temperature, (measured, latent)) -> do+    particles <- runPopulationCl nParticles resampleSystematic posteriorTemperatureProcess -< measured+    returnA -< Result{temperature, measured, latent, particles}++-- | Visualize the current 'Result' at a rate controlled by the @gloss@ backend, usually 30 FPS.+visualisationRhine :: Rhine (GlossConcT IO) (GlossClockUTC GlossSimClockIO) Result ()+visualisationRhine = hoistClSF sampleIOGloss visualisation @@ glossClockUTC GlossSimClockIO++-- | Compose all four asynchronous components to a single 'Rhine'.+mainRhineMultiRate =+  userTemperature+    @@ glossClockUTC GlossEventClockIO+      >-- keepLast initialTemperature -@- glossConcurrently -->+        modelRhine+        >-- keepLast (initialTemperature, (zeroVector, zeroVector)) -@- glossConcurrently -->+          inference+            >-- keepLast Result{temperature = initialTemperature, measured = zeroVector, latent = zeroVector, particles = []} -@- glossConcurrently -->+              visualisationRhine++mainMultiRate :: IO ()+mainMultiRate =+  void $+    launchGlossThread glossSettings $+      flow mainRhineMultiRate++-- * Utilities++instance MonadDistribution m => MonadDistribution (GlossConcT m) where+  random = lift random++sampleIOGloss :: App a -> GlossConcT IO a+sampleIOGloss = hoist sampleIO
+ rhine-bayes.cabal view
@@ -0,0 +1,89 @@+name:                rhine-bayes+version:             0.8.1.1+synopsis:            monad-bayes backend for Rhine+description:+  This package provides a backend to the `monad-bayes` library,+  enabling you to write stochastic processes as signal functions,+  and performing online machine learning on them.+license:             BSD3+license-file:        LICENSE+author:              Manuel Bärenz+maintainer:          programming@manuelbaerenz.de+-- copyright:+category:            FRP+build-type:          Simple+extra-source-files:  ChangeLog.md+extra-doc-files:     README.md+cabal-version:       1.18++source-repository head+  type:     git+  location: git@github.com:turion/rhine.git++source-repository this+  type:     git+  location: git@github.com:turion/rhine.git+  tag:      v0.8.1.1++library+  exposed-modules:+    FRP.Rhine.Bayes+  other-modules:+    Data.MonadicStreamFunction.Bayes+  build-depends:       base         >= 4.11 && < 4.18+                     , transformers >= 0.5+                     , rhine        == 0.8.1.1+                     , dunai        >= 0.8+                     , log-domain   >= 0.12+                     , monad-bayes  >= 1.1.0+  hs-source-dirs:      src+  default-language:    Haskell2010+  default-extensions:+    Arrows+    DataKinds+    DeriveFunctor+    FlexibleContexts+    FlexibleInstances+    GeneralizedNewtypeDeriving+    MultiParamTypeClasses+    RankNTypes+    ScopedTypeVariables+    TupleSections+    TypeFamilies++  ghc-options:         -W+  if flag(dev)+    ghc-options: -Werror++executable rhine-bayes-gloss+  main-is:             Main.hs+  hs-source-dirs:      app+  ghc-options:         -threaded+  build-depends:       base         >= 4.11 && < 4.18+                     , rhine+                     , rhine-bayes+                     , rhine-gloss+                     , monad-bayes+                     , transformers+                     , log-domain+                     , mmorph+                     , time+  default-language:    Haskell2010+  default-extensions:+    Arrows+    DataKinds+    FlexibleContexts+    NamedFieldPuns+    RankNTypes+    TupleSections+    TypeApplications+    TypeFamilies++  ghc-options:         -W -threaded -rtsopts -with-rtsopts=-N+  if flag(dev)+    ghc-options: -Werror++flag dev+  description: Enable warnings as errors. Active on ci.+  default: False+  manual: True
+ src/Data/MonadicStreamFunction/Bayes.hs view
@@ -0,0 +1,53 @@+module Data.MonadicStreamFunction.Bayes where++-- base+import Control.Arrow+import Data.Functor (($>))+import Data.Tuple (swap)++-- transformers++-- log-domain+import Numeric.Log hiding (sum)++-- monad-bayes+import Control.Monad.Bayes.Population++-- dunai+import Data.MonadicStreamFunction+import Data.MonadicStreamFunction.InternalCore (MSF (..))++-- | Run the Sequential Monte Carlo algorithm continuously on an 'MSF'+runPopulationS ::+  forall m a b.+  Monad m =>+  -- | Number of particles+  Int ->+  -- | Resampler+  (forall x. Population m x -> Population m x) ->+  MSF (Population m) a b ->+  -- FIXME Why not MSF m a (Population b)+  MSF m a [(b, Log Double)]+runPopulationS nParticles resampler = runPopulationsS resampler . (spawn nParticles $>)++-- | Run the Sequential Monte Carlo algorithm continuously on a 'Population' of 'MSF's+runPopulationsS ::+  Monad m =>+  -- | Resampler+  (forall x. Population m x -> Population m x) ->+  Population m (MSF (Population m) a b) ->+  MSF m a [(b, Log Double)]+runPopulationsS resampler = go+ where+  go msfs = MSF $ \a -> do+    -- TODO This is quite different than the dunai version now. Maybe it's right nevertheless.+    -- FIXME This normalizes, which introduces bias, whatever that means+    bAndMSFs <- runPopulation $ normalize $ resampler $ flip unMSF a =<< msfs+    return $+      second (go . fromWeightedList . return) $+        unzip $+          (swap . fmap fst &&& swap . fmap snd) . swap <$> bAndMSFs++-- FIXME see PR re-adding this to monad-bayes+normalize :: Monad m => Population m a -> Population m a+normalize = fromWeightedList . fmap (\particles -> second (/ (sum $ snd <$> particles)) <$> particles) . runPopulation
+ src/FRP/Rhine/Bayes.hs view
@@ -0,0 +1,85 @@+module FRP.Rhine.Bayes where++-- log-domain+import Numeric.Log hiding (sum)++-- monad-bayes+import Control.Monad.Bayes.Class+import Control.Monad.Bayes.Population++-- dunai+import qualified Control.Monad.Trans.MSF.Reader as DunaiReader++-- dunai-bayes+import qualified Data.MonadicStreamFunction.Bayes as DunaiBayes++-- rhine+import FRP.Rhine++-- * Inference methods++-- | Run the Sequential Monte Carlo algorithm continuously on a 'ClSF'.+runPopulationCl :: forall m cl a b . Monad m =>+  -- | Number of particles+  Int ->+  -- | Resampler (see 'Control.Monad.Bayes.Population' for some standard choices)+  (forall x . Population m x -> Population m x)+  -- | A signal function modelling the stochastic process on which to perform inference.+  --   @a@ represents observations upon which the model should condition, using e.g. 'score'.+  --   It can also additionally contain hyperparameters.+  --   @b@ is the type of estimated current state.+  -> ClSF (Population m) cl a b+  -> ClSF m cl a [(b, Log Double)]+runPopulationCl nParticles resampler = DunaiReader.readerS . DunaiBayes.runPopulationS nParticles resampler . DunaiReader.runReaderS++-- * Short standard library of stochastic processes++-- | White noise, that is, an independent normal distribution at every time step.+whiteNoise :: MonadDistribution m => Double -> Behaviour m td Double+whiteNoise sigma = constMCl $ normal 0 sigma++-- | Construct a Lévy process from the increment between time steps.+levy ::+  (MonadDistribution m, VectorSpace v (Diff td)) =>+  -- | The increment function at every time step. The argument is the difference between times.+  (Diff td -> m v) ->+  Behaviour m td v+levy incrementor = sinceLastS >>> arrMCl incrementor >>> sumS++-- | The Wiener process, also known as Brownian motion.+wiener, brownianMotion ::+  (MonadDistribution m, Diff td ~ Double) =>+  -- | Time scale of variance.+  Diff td ->+  Behaviour m td Double+wiener timescale = levy $ \diffTime -> normal 0 $ sqrt $ diffTime / timescale++brownianMotion = wiener++-- | The Wiener process, also known as Brownian motion, with varying variance parameter.+wienerVarying, brownianMotionVarying ::+  (MonadDistribution m, Diff td ~ Double) =>+  BehaviourF m td (Diff td) Double+wienerVarying = proc timeScale -> do+  diffTime <- sinceLastS -< ()+  let stdDev = sqrt $ diffTime / timeScale+  increment <- if stdDev > 0+    then arrM $ normal 0 -< stdDev+    else returnA -< 0+  sumS -< increment++brownianMotionVarying = wienerVarying++-- | The 'wiener' process transformed to the Log domain, also called the geometric Wiener process.+wienerLogDomain ::+  (MonadDistribution m, Diff td ~ Double) =>+  -- | Time scale of variance+  Diff td ->+  Behaviour m td (Log Double)+wienerLogDomain timescale = wiener timescale >>> arr Exp++-- | See 'wienerLogDomain' and 'wienerVarying'.+wienerVaryingLogDomain ::+  (MonadDistribution m, Diff td ~ Double) =>+  BehaviourF m td (Diff td) (Log Double)+wienerVaryingLogDomain = wienerVarying >>> arr Exp