packages feed

learning-hmm 0.1.0.0 → 0.1.1.0

raw patch · 7 files changed

+198/−47 lines, 7 filesdep +containersdep +random-sourcePVP ok

version bump matches the API change (PVP)

Dependencies added: containers, random-source

API changes (from Hackage documentation)

+ Learning.HMM: init :: (Ord s, Ord o) => [s] -> [o] -> RVar (HMM s o)
+ Learning.HMM: simulate :: HMM s o -> Int -> RVar ([s], [o])
+ Learning.HMM: withEmission :: (Ord s, Ord o) => HMM s o -> [o] -> HMM s o

Files

+ CHANGES.md view
@@ -0,0 +1,9 @@+Revision history for Haskell package learning-hmm+===++## Version 0.1.1.0+- Add function `init` for random initialization+- Add function `simulate` for running a Markov process++## Version 0.1.0.0+- Original version
learning-hmm.cabal view
@@ -1,5 +1,5 @@ name:                learning-hmm-version:             0.1.0.0+version:             0.1.1.0 stability:           experimental  synopsis:            Yet another library for hidden Markov models@@ -17,6 +17,7 @@  cabal-version:       >=1.10 build-type:          Simple+extra-source-files:  CHANGES.md  source-repository head   type:              git@@ -25,13 +26,17 @@ library   exposed-modules:   Learning.HMM   other-modules:     Data.Random.Distribution.Categorical.Util+                   , Data.Random.Distribution.Simplex+                   , Data.Random.Distribution.Uniform.Util                    , Data.Vector.Util                    , Data.Vector.Util.LinearAlgebra                    , Learning.HMM.Internal   -- other-extensions:     build-depends:     base >=4.7 && <4.8+                   , containers                    , logfloat                    , random-fu+                   , random-source                    , vector   hs-source-dirs:    src   default-language:  Haskell2010
+ src/Data/Random/Distribution/Simplex.hs view
@@ -0,0 +1,50 @@+{-# LANGUAGE+    MultiParamTypeClasses,+    FlexibleContexts, FlexibleInstances,+    UndecidableInstances, GADTs+  #-}++module Data.Random.Distribution.Simplex+    ( StdSimplex(..)+    , stdSimplex+    , stdSimplexT+    , fractionalStdSimplex+    ) where++import Control.Applicative+import Control.Monad+import Data.List+import Data.Random.RVar+import Data.Random.Distribution+import Data.Random.Distribution.Uniform++-- |Uniform distribution over a standard simplex.+newtype StdSimplex as =+    -- | @StdSimplex k@ constructs a standard simplex of dimension @k@+    -- (standard /k/-simplex).+    -- An element of the simplex represents a vector variable @as = (a_0,+    -- a_1, ..., a_k)@. The elements of @as@ are more than or equal to @0@+    -- and @sum as@ is always equal to @1@.+    StdSimplex Int+    deriving (Eq, Show)++instance (Ord a, Fractional a, Distribution StdUniform a) => Distribution StdSimplex [a] where+    rvar (StdSimplex k) = fractionalStdSimplex k++-- |@stdSimplex k@ returns a random variable being uniformly distributed over+-- a standard simplex of dimension @k@.+stdSimplex :: Distribution StdSimplex [a] => Int -> RVar [a]+stdSimplex k = rvar (StdSimplex k)++stdSimplexT :: Distribution StdSimplex [a] => Int -> RVarT m [a]+stdSimplexT k = rvarT (StdSimplex k)++-- |An algorithm proposed by Rubinstein & Melamed (1998).+-- See, /e.g./, S. Onn, I. Weissman.+-- Generating uniform random vectors over a simplex with implications to+-- the volume of a certain polytope and to multivariate extremes.+-- /Ann Oper Res/ (2011) __189__:331-342.+fractionalStdSimplex :: (Ord a, Fractional a, Distribution StdUniform a) => Int -> RVar [a]+fractionalStdSimplex k = do us <- replicateM k stdUniform+                            let us' = sort us ++ [1]+                            return $ zipWith (-) us' (0 : us')
+ src/Data/Random/Distribution/Uniform/Util.hs view
@@ -0,0 +1,17 @@+{-# LANGUAGE FlexibleContexts, FlexibleInstances, MultiParamTypeClasses #-}++module Data.Random.Distribution.Uniform.Util () where++import Control.Applicative ((<$>))+import Data.Number.LogFloat (LogFloat, logFloat, fromLogFloat)+import Data.Random.Distribution (Distribution)+import Data.Random.Distribution.Uniform -- (StdUniform(..), Uniform(..), doubleUniform)+import Data.Random (rvarT)+import Data.Random.Source (getRandomDouble)++instance Distribution Uniform LogFloat where+  rvarT (Uniform a b) = do x <- doubleUniform (fromLogFloat a) (fromLogFloat b)+                           return $ logFloat x++instance Distribution StdUniform LogFloat where+  rvarT _ = logFloat <$> getRandomDouble
src/Learning/HMM.hs view
@@ -2,17 +2,24 @@     HMM (..)   , LogLikelihood   , new+  , init+  , withEmission   , viterbi   , baumWelch+  , simulate   ) where +import Prelude hiding (init)+import Control.Applicative ((<$>)) import Control.Arrow ((***), first)-import Data.Random.Distribution (pdf)+import Data.Random.Distribution (pdf, rvar) import Data.Random.Distribution.Categorical (Categorical) import qualified Data.Random.Distribution.Categorical as C (     fromList, fromWeightedList, normalizeCategoricalPs   ) import Data.Random.Distribution.Categorical.Util ()+import Data.Random.RVar (RVar)+import Data.Random.Sample (sample) import Data.List (genericLength) import Data.Number.LogFloat (fromLogFloat, logFloat, logFromLogFloat) import Data.Vector ((!))@@ -23,11 +30,11 @@ type LogLikelihood = Double  -- | Parameter set of the hidden Markov model. Direct use of the---   constructor is not recommended. Instead, call 'new'.+--   constructor is not recommended. Instead, call 'new' or 'init'. data HMM s o = HMM { states  :: [s] -- ^ Hidden states-                   , outputs :: [o] -- ^ Observed outputs+                   , outputs :: [o] -- ^ Outputs                    , initialStateDist :: Categorical Double s-                     -- ^ Categorical distribusion of initial states+                     -- ^ Categorical distribution of initial states                    , transitionDist :: s -> Categorical Double s                      -- ^ Categorical distribution of next states                      --   conditioned by the previous states@@ -53,11 +60,14 @@     w   = transitionDist hmm     phi = emissionDist hmm --- | Construct a 'HMM' from the given states and outputs. The---   'initialStateDist' and 'emissionDist' are set to be uniform+-- | @new states outputs@ returns a model from the @states@ and @outputs@.+--   The 'initialStateDist' and 'emissionDist' are set to be uniform --   distributions. The 'transitionDist' is specified as follows: with --   probability 1/2, move to the same state, otherwise, move to a random --   state (which might be the same state).+--+--   >>> new [1, 2 :: Int] ['C', 'D']+--   HMM {states = [1,2], outputs = "CD", initialStateDist = fromList [(0.5,1),(0.5,2)], transitionDist = [(fromList [(0.75,1),(0.25,2)],1),(fromList [(0.25,1),(0.75,2)],2)], emissionDist = [(fromList [(0.5,'C'),(0.5,'D')],1),(fromList [(0.5,'C'),(0.5,'D')],2)]} new :: (Ord s, Ord o) => [s] -> [o] -> HMM s o new ss os = HMM { states           = ss                 , outputs          = os@@ -76,8 +86,24 @@     phi s | s `elem` ss = C.fromWeightedList [(1, o) | o <- os]           | otherwise   = C.fromList [] --- | Perform the Viterbi algorithm and return the most likely state path---   and its log likelihood.+-- | @init states outputs@ returns a random variable of the model with+--   @states@ and @outputs@, wherein parameters are sampled from uniform+--   distributions.+init :: (Ord s, Ord o) => [s] -> [o] -> RVar (HMM s o)+init ss os = do hmm' <- init' (V.fromList ss) (V.fromList os)+                return $ fromHMM' hmm'++-- | @model \`withEmission\` xs@ returns a model in which the+--   'emissionDist' is updated by using the observed outputs @xs@. The+--   'emissionDist' is set to be normalized histograms each of which is+--   calculated from a partial set of @xs@ for each state. The partition is+--   based on the most likely state path obtained by the Viterbi algorithm.+withEmission :: (Ord s, Ord o) => HMM s o -> [o] -> HMM s o+withEmission model xs = fromHMM' $ withEmission' (toHMM' model) (V.fromList xs)++-- | @viterbi model xs@ performs the Viterbi algorithm using the observed+--   outputs @xs@, and returns the most likely state path and its log+--   likelihood. viterbi :: (Eq s, Eq o) => HMM s o -> [o] -> ([s], LogLikelihood) viterbi model xs =   checkModelIn "viterbi" model `seq`@@ -87,9 +113,9 @@     model' = toHMM' model     xs'    = V.fromList xs --- | Perform the Baum-Welch algorithm steps iteratively and return---   a list of updated 'HMM' parameters and their corresponding log---   likelihoods.+-- | @baumWelch model xs@ performs the Baum-Welch algorithm using the+--   observed outputs @xs@, and iteratively returns a list of updated+--   models and their corresponding log likelihoods. baumWelch :: (Eq s, Eq o) => HMM s o -> [o] -> [(HMM s o, LogLikelihood)] baumWelch model xs =   checkModelIn "baumWelch" model `seq`@@ -99,7 +125,23 @@     model' = toHMM' model     xs'    = V.fromList xs --- | Check if the 'HMM' is valid in the sense of whether the 'states' and+-- | @simulate model t@ generates a Markov process of length @t@ using the+--   @model@, and returns its state path and observed outputs.+simulate :: HMM s o -> Int -> RVar ([s], [o])+simulate model step | step < 1  = return ([], [])+                    | otherwise = do s0 <- sample $ rvar pi0+                                     x0 <- sample $ rvar $ phi s0+                                     unzip . ((s0, x0) :) <$> sim s0 (step - 1)+  where+    sim _ 0 = return []+    sim s t = do s' <- sample $ rvar $ w s+                 x' <- sample $ rvar $ phi s+                 ((s', x') :) <$> sim s' (t - 1)+    pi0 = initialStateDist model+    w   = transitionDist model+    phi = emissionDist model++-- | Check if the model is valid in the sense of whether the 'states' and --   'outputs' are not empty. checkModelIn :: String -> HMM s o -> () checkModelIn fun hmm@@ -111,8 +153,8 @@     os = outputs hmm     err = errorIn fun --- | Check if all the elements of the data are contained in the 'outputs'---   of the 'HMM'.+-- | Check if all the elements of the observed outputs are contained in the+--   'outputs' of the model. checkDataIn :: Eq o => String -> HMM s o -> [o] -> () checkDataIn fun hmm xs   | all (`elem` os) xs = ()@@ -143,7 +185,7 @@     w' i   = V.toList $ V.map (first fromLogFloat) $ V.zip (w ! i) ss     phi' i = V.toList $ V.map (first fromLogFloat) $ V.zip (phi ! i) os --- | Convert 'HMM' to 'HMM''. The 'initialStateDist'', 'transisionDist'',+-- | Convert 'HMM' to 'HMM''. The 'initialStateDist'', 'transitionDist'', --   and 'emissionDistT'' are normalized. toHMM' :: (Eq s, Eq o) => HMM s o -> HMM' s o toHMM' hmm = HMM' { states'           = V.fromList ss
src/Learning/HMM/Internal.hs view
@@ -2,6 +2,8 @@     HMM' (..)   , Likelihood   , Probability+  , init'+  , withEmission'   , viterbi'   , baumWelch'   -- , baumWelch1'@@ -9,15 +11,20 @@   -- , backward'   ) where -import Control.Monad (forM_)+import Control.Applicative ((<$>))+import Control.Monad (forM_, replicateM) import Control.Monad.ST (runST)+import qualified Data.Map.Strict as M (empty, insertWith, findWithDefault) import Data.Number.LogFloat (LogFloat, logFloat)+import Data.Random.RVar (RVar)+import Data.Random.Distribution.Simplex (stdSimplex)+import Data.Random.Distribution.Uniform.Util () import Data.Vector (Vector, (!)) import qualified Data.Vector as V (-    filter, foldl1', freeze, last, length, map, maximum, maxIndex-  , replicate, sum , tail, zip , zipWith, zipWith3, zipWith4+    filter, foldl', foldl1', freeze, fromList, last, length, map, maximum+  , maxIndex, replicate, sum, tail, zip, zipWith, zipWith3, zipWith4   )-import qualified Data.Vector.Mutable as M (new, read, write)+import qualified Data.Vector.Mutable as MV (new, read, write) import qualified Data.Vector.Util as V (unsafeElemIndex) import Data.Vector.Util.LinearAlgebra (     (>+>), (>.>), (>/>), (#+#), (.>), (>/), (#/), (<.>), (#.>), (<.#)@@ -27,7 +34,7 @@ type Likelihood  = LogFloat type Probability = LogFloat --- | More efficient data structure of the HMM parameters. This should be+-- | More efficient data structure of the 'HMM' model. This should be --   only used internally. The 'emissionDistT'' is a transposed matrix in --   order to simplify the calculation. data HMM' s o = HMM' { states'           :: Vector s@@ -37,19 +44,41 @@                      , emissionDistT'    :: Vector (Vector Probability)                      } --- | Perform the Viterbi algorithm and return the most likely state path---   and its likelihood.+init' :: Vector s -> Vector o -> RVar (HMM' s o)+init' ss os = do+  let n = V.length ss+      m = V.length os+  pi0 <- V.fromList <$> stdSimplex (n-1)+  w   <- V.fromList <$> replicateM n (V.fromList <$> stdSimplex (n-1))+  phi <- V.fromList <$> replicateM n (V.fromList <$> stdSimplex (m-1))+  return HMM' { states'           = ss+              , outputs'          = os+              , initialStateDist' = pi0+              , transitionDist'   = w+              , emissionDistT'    = V.transpose phi+              }++withEmission' :: (Ord s, Ord o) => HMM' s o -> Vector o -> HMM' s o+withEmission' model xs = model { emissionDistT' = phi' }+  where+    ss = states' model+    os = outputs' model+    (path, _) = viterbi' model xs+    mp    = V.foldl' (\m k -> M.insertWith (+) k 1 m) M.empty $ V.zip path xs+    hists = V.map (\s -> V.map (\o -> M.findWithDefault 0 (s, o) mp) os) ss+    phi'  = V.transpose $ V.map (\h -> h >/ V.sum h) hists+ viterbi' :: Eq o => HMM' s o -> Vector o -> (Vector s, Likelihood) viterbi' model xs = (path, likelihood)   where     -- The following procedure is based on     -- http://ibisforest.org/index.php?cmd=read&page=Viterbi%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0&word=Viterbi     path = V.map (ss !) $ runST $ do-      ix <- M.new n-      ix `M.write` (n-1) $ V.maxIndex $ deltas ! (n-1)+      ix <- MV.new n+      ix `MV.write` (n-1) $ V.maxIndex $ deltas ! (n-1)       forM_ (reverse [0..(n-2)]) $ \i -> do-        j <- ix `M.read` (i+1)-        ix `M.write` i $ psis ! (i+1) ! j+        j <- ix `MV.read` (i+1)+        ix `MV.write` i $ psis ! (i+1) ! j       V.freeze ix       where         ss = states' model@@ -58,15 +87,15 @@     deltas :: Vector (Vector Probability)     psis   :: Vector (Vector Int)     (deltas, psis) = runST $ do-      ds <- M.new n-      ps <- M.new n-      ds `M.write` 0 $ (phi' ! x 0) >.> pi0-      ps `M.write` 0 $ V.replicate k (0 :: Int)+      ds <- MV.new n+      ps <- MV.new n+      ds `MV.write` 0 $ (phi' ! x 0) >.> pi0+      ps `MV.write` 0 $ V.replicate k (0 :: Int)       forM_ [1..(n-1)] $ \i -> do-        d <- ds `M.read` (i-1)+        d <- ds `MV.read` (i-1)         let dws = V.map (d >.>) w'-        ds `M.write` i $ phi' ! x i >.> V.map V.maximum dws-        ps `M.write` i $ V.map V.maxIndex dws+        ds `MV.write` i $ phi' ! x i >.> V.map V.maximum dws+        ps `MV.write` i $ V.map V.maxIndex dws       ds' <- V.freeze ds       ps' <- V.freeze ps       return (ds', ps')@@ -82,15 +111,13 @@     -- Here we assumed that     n = V.length xs --- | Perform the Baum-Welch algorithm steps iteratively and return---   a list of updated 'HMM'' parameters and their corresponding likelihoods. baumWelch' :: (Eq s, Eq o) => HMM' s o -> Vector o -> [(HMM' s o, Likelihood)] baumWelch' model xs = zip ms $ tail ells   where     (ms, ells) = unzip $ iterate ((`baumWelch1'` xs) . fst) (model, undefined)  -- | Perform one step of the Baum-Welch algorithm and return the updated---   'HMM'' parameters and the likelihood of the old parameters.+--   model and the likelihood of the old model. baumWelch1' :: (Eq s, Eq o) => HMM' s o -> Vector o -> (HMM' s o, Likelihood) baumWelch1' model xs = (model', likelihood)   where@@ -137,14 +164,13 @@     -- Here we assumed that     os = outputs' model --- | Baum-Welch forward algorithm that generates α values forward' :: Eq o => HMM' s o -> Vector o -> Vector (Vector Probability) forward' model xs = runST $ do-  v <- M.new n-  v `M.write` 0 $ (phi' ! x 0) >.> pi0+  v <- MV.new n+  v `MV.write` 0 $ (phi' ! x 0) >.> pi0   forM_ [1..(n-1)] $ \i -> do-    a <- v `M.read` (i-1)-    v `M.write` i $ (phi' ! x i) >.> (a <.# w)+    a <- v `MV.read` (i-1)+    v `MV.write` i $ (phi' ! x i) >.> (a <.# w)   V.freeze v   where     n   = V.length xs@@ -155,14 +181,13 @@     w    = transitionDist' model     phi' = emissionDistT' model --- | Baum-Welch backward algorithm that generates β values backward' :: Eq o => HMM' s o -> Vector o -> Vector (Vector Probability) backward' model xs = runST $ do-  v <- M.new n-  v `M.write` (n-1) $ V.replicate k $ logFloat (1 :: Double)+  v <- MV.new n+  v `MV.write` (n-1) $ V.replicate k $ logFloat (1 :: Double)   forM_ (reverse [0..(n-2)]) $ \i -> do-    b <- v `M.read` (i+1)-    v `M.write` i $ w #.> ((phi' ! x (i+1)) >.> b)+    b <- v `MV.read` (i+1)+    v `MV.write` i $ w #.> ((phi' ! x (i+1)) >.> b)   V.freeze v   where     n   = V.length xs
tests/doctests.hs view
@@ -1,6 +1,9 @@+module Main (main) where+ import Test.DocTest  main :: IO () main = doctest [ "-isrc"                , "src/Data/Vector/Util/LinearAlgebra.hs"+               , "src/Learning/HMM.hs"                ]