packages feed

learning-hmm 0.2.1.0 → 0.3.0.0

raw patch · 7 files changed

+654/−164 lines, 7 filesdep −doctestdep ~basePVP ok

version bump matches the API change (PVP)

Dependencies removed: doctest

Dependency ranges changed: base

API changes (from Hackage documentation)

- Learning.HMM: new :: (Ord s, Ord o) => [s] -> [o] -> HMM s o
+ Learning.IOHMM: IOHMM :: [i] -> [s] -> [o] -> Categorical Double s -> (i -> s -> Categorical Double s) -> (s -> Categorical Double o) -> IOHMM i s o
+ Learning.IOHMM: baumWelch :: (Eq i, Eq s, Eq o) => IOHMM i s o -> [i] -> [o] -> [(IOHMM i s o, LogLikelihood)]
+ Learning.IOHMM: data IOHMM i s o
+ Learning.IOHMM: emissionDist :: IOHMM i s o -> s -> Categorical Double o
+ Learning.IOHMM: init :: (Eq i, Eq s, Eq o) => [i] -> [s] -> [o] -> RVar (IOHMM i s o)
+ Learning.IOHMM: initialStateDist :: IOHMM i s o -> Categorical Double s
+ Learning.IOHMM: inputs :: IOHMM i s o -> [i]
+ Learning.IOHMM: instance (Show i, Show s, Show o) => Show (IOHMM i s o)
+ Learning.IOHMM: outputs :: IOHMM i s o -> [o]
+ Learning.IOHMM: simulate :: IOHMM i s o -> [i] -> RVar ([s], [o])
+ Learning.IOHMM: states :: IOHMM i s o -> [s]
+ Learning.IOHMM: transitionDist :: IOHMM i s o -> i -> s -> Categorical Double s
+ Learning.IOHMM: type LogLikelihood = Double
+ Learning.IOHMM: viterbi :: (Eq i, Eq s, Eq o) => IOHMM i s o -> [i] -> [o] -> ([s], LogLikelihood)
+ Learning.IOHMM: withEmission :: (Eq i, Eq s, Eq o) => IOHMM i s o -> [i] -> [o] -> IOHMM i s o

Files

CHANGES.md view
@@ -1,6 +1,10 @@ Revision history for Haskell package learning-hmm === +## Version 0.3.0.0+- Add `Learning.IOHMM` which represents a class of input-output HMM+- Delete `Learning.HMM.new`+ ## Version 0.2.1.0 - Performance improvements with employing 'hmatrix' - `withEmission` now does iterative re-estimation based on the Viterbi path
learning-hmm.cabal view
@@ -1,11 +1,12 @@ name:                learning-hmm-version:             0.2.1.0+version:             0.3.0.0 stability:           experimental  synopsis:            Yet another library for hidden Markov models description:         This library provides functions for the maximum likelihood                      estimation of discrete hidden Markov models. At present,-                     only Baum-Welch and Viterbi algorithms are implemented.+                     only Baum-Welch and Viterbi algorithms are implemented for+                     the plain HMM and the input-output HMM. category:            Algorithms, Machine Learning, Statistics  author:              Mitsuhiro Nakamura@@ -25,10 +26,12 @@  library   exposed-modules:   Learning.HMM+                   , Learning.IOHMM   other-modules:     Data.Random.Distribution.Categorical.Util                    , Data.Random.Distribution.Simplex                    , Data.Vector.Generic.Util                    , Learning.HMM.Internal+                   , Learning.IOHMM.Internal   -- other-extensions:     build-depends:     base >=4.7 && <4.8                    , containers@@ -40,11 +43,3 @@   hs-source-dirs:    src   default-language:  Haskell2010   ghc-options:       -Wall--test-suite doctests-  main-is:           doctests.hs-  type:              exitcode-stdio-1.0-  build-depends:     base, doctest >= 0.9.11-  hs-source-dirs:    tests-  default-language:  Haskell2010-  ghc-options:       -threaded -Wall
src/Learning/HMM.hs view
@@ -1,7 +1,6 @@ module Learning.HMM (     HMM (..)   , LogLikelihood-  , new   , init   , withEmission   , viterbi@@ -12,12 +11,12 @@ import Prelude hiding (init) import Control.Applicative ((<$>)) import Control.Arrow (first)-import Data.List (elemIndex, genericLength)+import Data.List (elemIndex) import Data.Maybe (fromJust) import Data.Random.Distribution (pdf, rvar) import Data.Random.Distribution.Categorical (Categorical) import qualified Data.Random.Distribution.Categorical as C (-    fromList, fromWeightedList, normalizeCategoricalPs+    fromList, normalizeCategoricalPs   ) import Data.Random.Distribution.Categorical.Util () import Data.Random.RVar (RVar)@@ -31,12 +30,27 @@     (!), fromList, fromLists, toList   ) import qualified Numeric.LinearAlgebra.HMatrix as H (tr)-import Learning.HMM.Internal+import Learning.HMM.Internal (LogLikelihood)+import qualified Learning.HMM.Internal as I --- | Parameter set of the hidden Markov model. Direct use of the---   constructor is not recommended. Instead, call 'new' or 'init'.-data HMM s o = HMM { states  :: [s] -- ^ Hidden states-                   , outputs :: [o] -- ^ Outputs+-- | Parameter set of the hidden Markov model with discrete emission.+--   The model schema is as follows.+--+--   @+--       z_0 -> z_1 -> ... -> z_n+--        |      |             |+--        v      v             v+--       x_0    x_1           x_n+--   @+--+--   Here, @[z_0, z_1, ..., z_n]@ are hidden states and @[x_0, x_1, ..., x_n]@+--   are observed outputs. @z_0@ is determined by the 'initialStateDist'.+--   For @i = 1, ..., n@, @z_i@ is determined by the 'transitionDist'+--   conditioned by @z_{i-1}@.+--   For @i = 0, ..., n@, @x_i@ is determined by the 'emissionDist'+--   conditioned by @z_i@.+data HMM s o = HMM { states  :: [s]+                   , outputs :: [o]                    , initialStateDist :: Categorical Double s                      -- ^ Categorical distribution of initial state                    , transitionDist :: s -> Categorical Double s@@ -64,37 +78,11 @@     w   = transitionDist hmm     phi = emissionDist hmm --- | @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-                , initialStateDist = pi0-                , transitionDist   = w-                , emissionDist     = phi-                }-  where-    pi0 = C.fromWeightedList [(1, s) | s <- ss]-    w s | s `elem` ss = C.fromList [(p s', s') | s' <- ss]-        | otherwise   = C.fromList []-      where-        k = genericLength ss-        p s' | s' == s   = 1/2 * (1 + 1/k)-             | otherwise = 1/2 / k-    phi s | s `elem` ss = C.fromWeightedList [(1, o) | o <- os]-          | otherwise   = C.fromList []---- | @init states outputs@ returns a random variable of the model with+-- | @init states outputs@ returns a random variable of models with the --   @states@ and @outputs@, wherein parameters are sampled from uniform --   distributions. init :: (Eq s, Eq o) => [s] -> [o] -> RVar (HMM s o)-init ss os = fromHMM' ss os <$> init' (length ss) (length os)+init ss os = fromInternal ss os <$> I.init (length ss) (length os)  -- | @model \`withEmission\` xs@ returns a model in which the --   'emissionDist' is updated by re-estimations using the observed outputs@@ -102,12 +90,12 @@ --   which is calculated from segumentations of @xs@ based on the Viterbi --   state path. withEmission :: (Eq s, Eq o) => HMM s o -> [o] -> HMM s o-withEmission model xs = fromHMM' ss os $ withEmission' model' xs'+withEmission model xs = fromInternal ss os $ I.withEmission model' xs'   where     ss     = states model     os     = outputs model     os'    = V.fromList os-    model' = toHMM' model+    model' = toInternal model     xs'    = U.fromList $ fromJust $ mapM (`V.elemIndex` os') xs  -- | @viterbi model xs@ performs the Viterbi algorithm using the observed@@ -117,11 +105,11 @@ viterbi model xs =   checkModelIn "viterbi" model `seq`   checkDataIn "viterbi" model xs `seq`-  first toStates $ viterbi' model' xs'+  first toStates $ I.viterbi model' xs'   where     ss'    = V.fromList $ states model     os'    = V.fromList $ outputs model-    model' = toHMM' model+    model' = toInternal model     xs'    = U.fromList $ fromJust $ mapM (`V.elemIndex` os') xs     toStates = V.toList . V.map (V.unsafeIndex ss') . G.convert @@ -132,16 +120,16 @@ baumWelch model xs =   checkModelIn "baumWelch" model `seq`   checkDataIn "baumWelch" model xs `seq`-  map (first $ fromHMM' ss os) $ baumWelch' model' xs'+  map (first $ fromInternal ss os) $ I.baumWelch model' xs'   where     ss     = states model     os     = outputs model     os'    = V.fromList os-    model' = toHMM' model+    model' = toInternal model     xs'    = U.fromList $ fromJust $ mapM (`V.elemIndex` os') xs  -- | @simulate model t@ generates a Markov process of length @t@ using the---   @model@, and returns its state path and observed outputs.+--   @model@, and returns its state path and outputs. simulate :: HMM s o -> Int -> RVar ([s], [o]) simulate model step   | step < 1  = return ([], [])@@ -179,35 +167,35 @@     os = outputs hmm     err = errorIn fun --- | Convert 'HMM'' to 'HMM'.-fromHMM' :: (Eq s, Eq o) => [s] -> [o] -> HMM' -> HMM s o-fromHMM' ss os hmm' = HMM { states           = ss-                          , outputs          = os-                          , initialStateDist = C.fromList pi0'-                          , transitionDist   = \s -> case elemIndex s ss of-                                                       Nothing -> C.fromList []-                                                       Just i  -> C.fromList $ w' i-                          , emissionDist     = \s -> case elemIndex s ss of-                                                       Nothing -> C.fromList []-                                                       Just i  -> C.fromList $ phi' i-                          }+-- | Convert internal 'HMM' to 'HMM'.+fromInternal :: (Eq s, Eq o) => [s] -> [o] -> I.HMM -> HMM s o+fromInternal ss os hmm' = HMM { states           = ss+                              , outputs          = os+                              , initialStateDist = C.fromList pi0'+                              , transitionDist   = \s -> case elemIndex s ss of+                                                           Nothing -> C.fromList []+                                                           Just i  -> C.fromList $ w' i+                              , emissionDist     = \s -> case elemIndex s ss of+                                                           Nothing -> C.fromList []+                                                           Just i  -> C.fromList $ phi' i+                              }   where-    pi0 = initialStateDist' hmm'-    w   = transitionDist' hmm'-    phi = H.tr $ emissionDistT' hmm'+    pi0 = I.initialStateDist hmm'+    w   = I.transitionDist hmm'+    phi = H.tr $ I.emissionDistT hmm'     pi0'   = zip (H.toList pi0) ss     w' i   = zip (H.toList $ w H.! i) ss     phi' i = zip (H.toList $ phi H.! i) os --- | Convert 'HMM' to 'HMM''. The 'initialStateDist'', 'transitionDist'',---   and 'emissionDistT'' are normalized.-toHMM' :: (Eq s, Eq o) => HMM s o -> HMM'-toHMM' hmm = HMM' { nStates'          = length ss-                  , nOutputs'         = length os-                  , initialStateDist' = pi0-                  , transitionDist'   = w-                  , emissionDistT'    = phi'-                  }+-- | Convert 'HMM' to internal 'HMM'. The 'initialStateDist'',+--   'transitionDist'', and 'emissionDistT'' are normalized.+toInternal :: (Eq s, Eq o) => HMM s o -> I.HMM+toInternal hmm = I.HMM { I.nStates          = length ss+                       , I.nOutputs         = length os+                       , I.initialStateDist = pi0+                       , I.transitionDist   = w+                       , I.emissionDistT    = phi'+                       }   where     ss   = states hmm     os   = outputs hmm
src/Learning/HMM/Internal.hs view
@@ -1,15 +1,17 @@ module Learning.HMM.Internal (-    HMM' (..)+    HMM (..)   , LogLikelihood-  , init'-  , withEmission'-  , viterbi'-  , baumWelch'-  -- , baumWelch1'-  -- , forward'-  -- , backward'+  , init+  , withEmission+  , viterbi+  , baumWelch+  -- , baumWelch1+  -- , forward+  -- , backward+  -- , posterior   ) where +import Prelude hiding (init) import Control.Applicative ((<$>)) import Control.DeepSeq (NFData, force, rnf) import Control.Monad (forM_, replicateM)@@ -46,49 +48,49 @@ -- | More efficient data structure of the 'HMM' model. The 'states' and --   'outputs' in 'HMM' are represented by their indices. The --   'initialStateDist', 'transitionDist', and 'emissionDist' are---   represented by matrices. The 'emissionDistT'' is a transposed matrix+--   represented by matrices. The 'emissionDistT' is a transposed matrix --   in order to simplify the calculation.-data HMM' = HMM' { nStates'          :: Int -- ^ Number of states-                 , nOutputs'         :: Int -- ^ Number of outputs-                 , initialStateDist' :: H.Vector Double-                 , transitionDist'   :: H.Matrix Double-                 , emissionDistT'    :: H.Matrix Double-                 }+data HMM = HMM { nStates          :: Int -- ^ Number of states+               , nOutputs         :: Int -- ^ Number of outputs+               , initialStateDist :: H.Vector Double+               , transitionDist   :: H.Matrix Double+               , emissionDistT    :: H.Matrix Double+               } -instance NFData HMM' where-  rnf hmm' = rnf k `seq` rnf l `seq` rnf pi0 `seq` rnf w `seq` rnf phi'+instance NFData HMM where+  rnf hmm = rnf k `seq` rnf l `seq` rnf pi0 `seq` rnf w `seq` rnf phi'     where-      k    = nStates' hmm'-      l    = nOutputs' hmm'-      pi0  = initialStateDist' hmm'-      w    = transitionDist' hmm'-      phi' = emissionDistT' hmm'+      k    = nStates hmm+      l    = nOutputs hmm+      pi0  = initialStateDist hmm+      w    = transitionDist hmm+      phi' = emissionDistT hmm -init' :: Int -> Int -> RVar HMM'-init' k l = do+init :: Int -> Int -> RVar HMM+init k l = do   pi0 <- H.fromList <$> stdSimplex (k-1)   w   <- H.fromLists <$> replicateM k (stdSimplex (k-1))   phi <- H.fromLists <$> replicateM k (stdSimplex (l-1))-  return HMM' { nStates'          = k-              , nOutputs'         = l-              , initialStateDist' = pi0-              , transitionDist'   = w-              , emissionDistT'    = H.tr phi-              }+  return HMM { nStates          = k+             , nOutputs         = l+             , initialStateDist = pi0+             , transitionDist   = w+             , emissionDistT    = H.tr phi+             } -withEmission' :: HMM' -> U.Vector Int -> HMM'-withEmission' model xs = model'+withEmission :: HMM -> U.Vector Int -> HMM+withEmission model xs = model'   where     n = U.length xs-    k = nStates' model-    l = nOutputs' model+    k = nStates model+    l = nOutputs model     ss = [0..(k-1)]     os = [0..(l-1)] -    step m = fst $ baumWelch1' (m { emissionDistT' = H.tr phi }) n xs+    step m = fst $ baumWelch1 (m { emissionDistT = H.tr phi }) n xs       where         phi :: H.Matrix Double-        phi = let zs  = fst $ viterbi' m xs+        phi = let zs  = fst $ viterbi m xs                   fs  = G.frequencies $ U.zip zs xs                   hs  = H.fromLists $ map (\s -> map (\o ->                           M.findWithDefault 0 (s, o) fs) os) ss@@ -104,17 +106,17 @@     model' = fst $ head $ dropWhile ((> 1e-9) . snd) $ zip ms' ds  -- | Return the Euclidean distance between two models.-euclideanDistance :: HMM' -> HMM' -> Double+euclideanDistance :: HMM -> HMM -> Double euclideanDistance model model' =-  sqrt $ (H.sumElements $ (w - w') ** 2) + (H.sumElements $ (phi - phi') ** 2)+  sqrt $ H.sumElements ((w - w') ** 2) + H.sumElements ((phi - phi') ** 2)   where-    w    = transitionDist' model-    w'   = transitionDist' model'-    phi  = emissionDistT' model-    phi' = emissionDistT' model'+    w    = transitionDist model+    w'   = transitionDist model'+    phi  = emissionDistT model+    phi' = emissionDistT model' -viterbi' :: HMM' -> U.Vector Int -> (U.Vector Int, LogLikelihood)-viterbi' model xs = (path, logL)+viterbi :: HMM -> U.Vector Int -> (U.Vector Int, LogLikelihood)+viterbi model xs = (path, logL)   where     n = U.length xs @@ -137,9 +139,9 @@       ps' <- V.unsafeFreeze ps       return (ds', ps')       where-        pi0  = initialStateDist' model-        w'   = H.toColumns $ transitionDist' model-        phi' = emissionDistT' model+        pi0  = initialStateDist model+        w'   = H.toColumns $ transitionDist model+        phi' = emissionDistT model      deltaE = V.unsafeIndex deltas (n-1) @@ -154,29 +156,29 @@       U.unsafeFreeze ix     logL = H.maxElement deltaE -baumWelch' :: HMM' -> U.Vector Int -> [(HMM', LogLikelihood)]-baumWelch' model xs = zip models (tail logLs)+baumWelch :: HMM -> U.Vector Int -> [(HMM, LogLikelihood)]+baumWelch model xs = zip models (tail logLs)   where     n = U.length xs-    step (m, _)     = baumWelch1' m n xs+    step (m, _)     = baumWelch1 m n xs     (models, logLs) = unzip $ iterate step (model, undefined)  -- | Perform one step of the Baum-Welch algorithm and return the updated --   model and the likelihood of the old model.-baumWelch1' :: HMM' -> Int -> U.Vector Int -> (HMM', LogLikelihood)-baumWelch1' model n xs = force (model', logL)+baumWelch1 :: HMM -> Int -> U.Vector Int -> (HMM, LogLikelihood)+baumWelch1 model n xs = force (model', logL)   where-    k = nStates' model-    l = nOutputs' model+    k = nStates model+    l = nOutputs model      -- First, we calculate the alpha, beta, and scaling values using the     -- forward-backward algorithm.-    (alphas, cs) = forward' model n xs-    betas        = backward' model n xs cs+    (alphas, cs) = forward model n xs+    betas        = backward model n xs cs      -- Based on the alpha, beta, and scaling values, we calculate the     -- posterior distribution, i.e., gamma and xi values.-    (gammas, xis) = posterior' model n xs alphas betas cs+    (gammas, xis) = posterior model n xs alphas betas cs      -- Using the gamma and xi values, we obtain the optimal initial state     -- probability vector, transition probability matrix, and emission@@ -191,16 +193,16 @@            in H.fromRows $ map (\o -> ds o / ns) [0..(l-1)]      -- We finally obtain the new model and the likelihood for the old model.-    model' = model { initialStateDist' = pi0-                   , transitionDist'   = w-                   , emissionDistT'    = phi'+    model' = model { initialStateDist = pi0+                   , transitionDist   = w+                   , emissionDistT    = phi'                    }     logL = - (U.sum $ U.map log cs)  -- | Return alphas and scaling variables.-forward' :: HMM' -> Int -> U.Vector Int -> (V.Vector (H.Vector Double), U.Vector Double)-{-# INLINE forward' #-}-forward' model n xs = runST $ do+forward :: HMM -> Int -> U.Vector Int -> (V.Vector (H.Vector Double), U.Vector Double)+{-# INLINE forward #-}+forward model n xs = runST $ do   as <- MV.unsafeNew n   cs <- MU.unsafeNew n   let x0 = U.unsafeIndex xs 0@@ -219,15 +221,15 @@   cs' <- U.unsafeFreeze cs   return (as', cs')   where-    k    = nStates' model-    pi0  = initialStateDist' model-    w'   = H.tr $ transitionDist' model-    phi' = emissionDistT' model+    k    = nStates model+    pi0  = initialStateDist model+    w'   = H.tr $ transitionDist model+    phi' = emissionDistT model  -- | Return betas using scaling variables.-backward' :: HMM' -> Int -> U.Vector Int -> U.Vector Double -> V.Vector (H.Vector Double)-{-# INLINE backward' #-}-backward' model n xs cs = runST $ do+backward :: HMM -> Int -> U.Vector Int -> U.Vector Double -> V.Vector (H.Vector Double)+{-# INLINE backward #-}+backward model n xs cs = runST $ do   bs <- MV.unsafeNew n   let bE = H.konst 1 k       cE = U.unsafeIndex cs (n-1)@@ -240,19 +242,19 @@     MV.unsafeWrite bs (t-1) (H.konst c' k * b')   V.unsafeFreeze bs   where-    k    = nStates' model-    w    = transitionDist' model-    phi' = emissionDistT' model+    k    = nStates model+    w    = transitionDist model+    phi' = emissionDistT model  -- | Return the posterior distribution.-posterior' :: HMM' -> Int -> U.Vector Int -> V.Vector (H.Vector Double) -> V.Vector (H.Vector Double) -> U.Vector Double -> (V.Vector (H.Vector Double), V.Vector (H.Matrix Double))-{-# INLINE posterior' #-}-posterior' model _ xs alphas betas cs = (gammas, xis)+posterior :: HMM -> Int -> U.Vector Int -> V.Vector (H.Vector Double) -> V.Vector (H.Vector Double) -> U.Vector Double -> (V.Vector (H.Vector Double), V.Vector (H.Matrix Double))+{-# INLINE posterior #-}+posterior model _ xs alphas betas cs = (gammas, xis)   where     gammas = V.zipWith3 (\a b c -> a * b / H.konst c k)                alphas betas (G.convert cs)     xis    = V.zipWith3 (\a b x -> H.diag a H.<> w H.<> H.diag (b * (phi' H.! x)))                alphas (V.unsafeTail betas) (G.convert $ U.unsafeTail xs)-    k    = nStates' model-    w    = transitionDist' model-    phi' = emissionDistT' model+    k    = nStates model+    w    = transitionDist model+    phi' = emissionDistT model
+ src/Learning/IOHMM.hs view
@@ -0,0 +1,240 @@+module Learning.IOHMM (+    IOHMM (..)+  , LogLikelihood+  , init+  , withEmission+  , viterbi+  , baumWelch+  , simulate+  ) where++import Prelude hiding (init)+import Control.Applicative ((<$>))+import Control.Arrow (first)+import Data.List (elemIndex)+import Data.Maybe (fromJust)+import Data.Random.Distribution (pdf, rvar)+import Data.Random.Distribution.Categorical (Categorical)+import qualified Data.Random.Distribution.Categorical as C (+    fromList, normalizeCategoricalPs+  )+import Data.Random.Distribution.Categorical.Util ()+import Data.Random.RVar (RVar)+import Data.Random.Sample (sample)+import qualified Data.Vector as V (+    elemIndex, fromList, map, toList, unsafeIndex+  )+import qualified Data.Vector.Generic as G (convert)+import qualified Data.Vector.Unboxed as U (fromList, zip)+import qualified Numeric.LinearAlgebra.Data as H (+    (!), fromList, fromLists, toList+  )+import qualified Numeric.LinearAlgebra.HMatrix as H (tr)+import Learning.IOHMM.Internal (LogLikelihood)+import qualified Learning.IOHMM.Internal as I++-- | Parameter set of the input-output hidden Markov model with discrete emission.+--   This 'IOHMM' assumes that the inputs affect only the transition+--   probabilities. The model schema is as follows.+--+--   @+--       x_0    x_1           x_n+--               |             |+--               v             v+--       z_0 -> z_1 -> ... -> z_n+--        |      |             |+--        v      v             v+--       y_0    y_1           y_n+--   @+--+--   Here, @[x_0, x_1, ..., x_n]@ are given inputs, @[z_0, z_1, ..., z_n]@+--   are hidden states, and @[y_0, y_1, ..., y_n]@ are observed outputs.+--   @z_0@ is determined by the 'initialStateDist'.+--   For @i = 1, ..., n@, @z_i@ is determined by the 'transitionDist'+--   conditioned by @x_i@ and @z_{i-1}@.+--   For @i = 0, ..., n@, @y_i@ is determined by the 'emissionDist'+--   conditioned by @z_i@.+data IOHMM i s o = IOHMM { inputs  :: [i]+                         , states  :: [s]+                         , outputs :: [o]+                         , initialStateDist :: Categorical Double s+                           -- ^ Categorical distribution of initial state+                         , transitionDist :: i -> s -> Categorical Double s+                           -- ^ Categorical distribution of next state+                           --   conditioned by the input and previous state+                         , emissionDist :: s -> Categorical Double o+                           -- ^ Categorical distribution of output conditioned+                           --   by the hidden state+                         }++instance (Show i, Show s, Show o) => Show (IOHMM i s o) where+  show = showIOHMM++showIOHMM :: (Show i, Show s, Show o) => IOHMM i s o -> String+showIOHMM hmm = "IOHMM {inputs = "           ++ show is+                  ++ ", states = "           ++ show ss+                  ++ ", outputs = "          ++ show os+                  ++ ", initialStateDist = " ++ show pi0+                  ++ ", transitionDist = "   ++ show [(w i s, s) | i <- is, s <- ss]+                  ++ ", emissionDist = "     ++ show [(phi s, s) | s <- ss]+                  ++ "}"+  where+    is  = inputs hmm+    ss  = states hmm+    os  = outputs hmm+    pi0 = initialStateDist hmm+    w   = transitionDist hmm+    phi = emissionDist hmm++-- | @init inputs states outputs@ returns a random variable of models with the+--   @inputs@, @states@, and @outputs@, wherein parameters are sampled from uniform+--   distributions.+init :: (Eq i, Eq s, Eq o) => [i] -> [s] -> [o] -> RVar (IOHMM i s o)+init is ss os = fromInternal is ss os <$> I.init (length is) (length ss) (length os)++-- | @withEmission model xs ys@ returns a model in which the+--   'emissionDist' is updated by re-estimations using the inputs @xs@ and+--   outputs @ys@. The 'emissionDist' is set to be normalized histograms+--   each of which is calculated from segumentations of @ys@ based on the+--   Viterbi state path.+--   If the lengths of @xs@ and @ys@ are different, the longer one is cut+--   by the length of the shorter one.+withEmission :: (Eq i, Eq s, Eq o) => IOHMM i s o -> [i] -> [o] -> IOHMM i s o+withEmission model xs ys = fromInternal is ss os $ I.withEmission model' $ U.zip xs' ys'+  where+    is     = inputs model+    is'    = V.fromList is+    ss     = states model+    os     = outputs model+    os'    = V.fromList os+    model' = toInternal model+    xs'    = U.fromList $ fromJust $ mapM (`V.elemIndex` is') xs+    ys'    = U.fromList $ fromJust $ mapM (`V.elemIndex` os') ys++-- | @viterbi model xs ys@ performs the Viterbi algorithm using the inputs+--   @xs@ and outputs @ys@, and returns the most likely state path and its+--   log likelihood.+--   If the lengths of @xs@ and @ys@ are different, the longer one is cut+--   by the length of the shorter one.+viterbi :: (Eq i, Eq s, Eq o) => IOHMM i s o -> [i] -> [o] -> ([s], LogLikelihood)+viterbi model xs ys =+  checkModelIn "viterbi" model `seq`+  checkDataIn "viterbi" model xs ys `seq`+  first toStates $ I.viterbi model' $ U.zip xs' ys'+  where+    is'    = V.fromList $ inputs model+    ss'    = V.fromList $ states model+    os'    = V.fromList $ outputs model+    model' = toInternal model+    xs'    = U.fromList $ fromJust $ mapM (`V.elemIndex` is') xs+    ys'    = U.fromList $ fromJust $ mapM (`V.elemIndex` os') ys+    toStates = V.toList . V.map (V.unsafeIndex ss') . G.convert++-- | @baumWelch model xs ys@ iteratively performs the Baum-Welch algorithm+--   using the inputs @xs@ and outputs @ys@, and returns a list of updated+--   models and their corresponding log likelihoods.+--   If the lengths of @xs@ and @ys@ are different, the longer one is cut+--   by the length of the shorter one.+baumWelch :: (Eq i, Eq s, Eq o) => IOHMM i s o -> [i] -> [o] -> [(IOHMM i s o, LogLikelihood)]+baumWelch model xs ys =+  checkModelIn "baumWelch" model `seq`+  checkDataIn "baumWelch" model xs ys `seq`+  map (first $ fromInternal is ss os) $ I.baumWelch model' $ U.zip xs' ys'+  where+    is     = inputs model+    is'    = V.fromList is+    ss     = states model+    os     = outputs model+    os'    = V.fromList os+    model' = toInternal model+    xs'    = U.fromList $ fromJust $ mapM (`V.elemIndex` is') xs+    ys'    = U.fromList $ fromJust $ mapM (`V.elemIndex` os') ys++-- | @simulate model xs@ generates a Markov process coinciding with the+--   inputs @xs@ using the @model@, and returns its state path and observed+--   outputs.+simulate :: IOHMM i s o -> [i] -> RVar ([s], [o])+simulate model xs+  | null xs   = return ([], [])+  | otherwise = do s0 <- sample $ rvar pi0+                   y0 <- sample $ rvar $ phi s0+                   unzip . ((s0, y0) :) <$> sim s0 (tail xs)+  where+    sim _ []     = return []+    sim s (x:xs') = do s' <- sample $ rvar $ w x s+                       y' <- sample $ rvar $ phi s'+                       ((s', y') :) <$> sim s' xs'+    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 -> IOHMM i s o -> ()+checkModelIn fun hmm+  | null is   = err "empty inputs"+  | null ss   = err "empty states"+  | null os   = err "empty outputs"+  | otherwise = ()+  where+    is = inputs hmm+    ss = states hmm+    os = outputs hmm+    err = errorIn fun++-- | Check if all the elements of the given inputs (outputs) are contained+--   in the 'inputs' ('outputs') of the model.+checkDataIn :: (Eq i, Eq o) => String -> IOHMM i s o -> [i] -> [o] -> ()+checkDataIn fun hmm xs ys+  | all (`elem` is) xs && all (`elem` os) ys = ()+  | otherwise                                = err "illegal data"+  where+    is = inputs hmm+    os = outputs hmm+    err = errorIn fun++-- | Convert internal 'IOHMM' to 'IOHMM'.+fromInternal :: (Eq i, Eq s, Eq o) => [i] -> [s] -> [o] -> I.IOHMM -> IOHMM i s o+fromInternal is ss os hmm' = IOHMM { inputs           = is+                                   , states           = ss+                                   , outputs          = os+                                   , initialStateDist = C.fromList pi0'+                                   , transitionDist   = \i s -> case (elemIndex i is, elemIndex s ss) of+                                                                  (Nothing, _)     -> C.fromList []+                                                                  (_, Nothing)     -> C.fromList []+                                                                  (Just j, Just k) -> C.fromList $ w' j k+                                   , emissionDist     = \s -> case elemIndex s ss of+                                                                Nothing -> C.fromList []+                                                                Just i  -> C.fromList $ phi' i+                                   }+  where+    pi0 = I.initialStateDist hmm'+    w   = I.transitionDist hmm'+    phi = H.tr $ I.emissionDistT hmm'+    pi0'   = zip (H.toList pi0) ss+    w' j k = zip (H.toList $ V.unsafeIndex w j H.! k) ss+    phi' i = zip (H.toList $ phi H.! i) os++-- | Convert 'IOHMM' to internal 'IOHMM'. The 'initialStateDist'',+--   'transitionDist'', and 'emissionDistT'' are normalized.+toInternal :: (Eq i, Eq s, Eq o) => IOHMM i s o -> I.IOHMM+toInternal hmm = I.IOHMM { I.nInputs          = length is+                         , I.nStates          = length ss+                         , I.nOutputs         = length os+                         , I.initialStateDist = pi0+                         , I.transitionDist   = w+                         , I.emissionDistT    = phi'+                         }+  where+    is   = inputs hmm+    ss   = states hmm+    os   = outputs hmm+    pi0_ = C.normalizeCategoricalPs $ initialStateDist hmm+    w_ i = C.normalizeCategoricalPs . (transitionDist hmm) i+    phi_ = C.normalizeCategoricalPs . emissionDist hmm+    pi0  = H.fromList [pdf pi0_ s | s <- ss]+    w    = V.fromList $ map (\i -> H.fromLists [[pdf (w_ i s) s' | s' <- ss] | s <- ss]) is+    phi' = H.fromLists [[pdf (phi_ s) o | s <- ss] | o <- os]++errorIn :: String -> String -> a+errorIn fun msg = error $ "Learning.IOHMM." ++ fun ++ ": " ++ msg
+ src/Learning/IOHMM/Internal.hs view
@@ -0,0 +1,269 @@+module Learning.IOHMM.Internal (+    IOHMM (..)+  , LogLikelihood+  , init+  , withEmission+  , viterbi+  , baumWelch+  -- , baumWelch1+  -- , forward+  -- , backward+  -- , posterior+  ) where++import Prelude hiding (init)+import Control.Applicative ((<$>))+import Control.DeepSeq (NFData, force, rnf)+import Control.Monad (forM_, replicateM)+import Control.Monad.ST (runST)+import qualified Data.Map.Strict as M (findWithDefault)+import Data.Random.RVar (RVar)+import Data.Random.Distribution.Simplex (stdSimplex)+import qualified Data.Vector as V (+    Vector, filter, foldl1', generate, map, replicateM, unsafeFreeze+  , unsafeIndex , unsafeTail , zip, zipWith3+  )+import qualified Data.Vector.Generic as G (convert)+import qualified Data.Vector.Generic.Util as G (frequencies)+import qualified Data.Vector.Mutable as MV (+    unsafeNew, unsafeRead, unsafeWrite+  )+import qualified Data.Vector.Unboxed as U (+    Vector, fromList, length, map, sum, unsafeFreeze, unsafeIndex+  , unsafeTail, unzip, zip+  )+import qualified Data.Vector.Unboxed.Mutable as MU (+    unsafeNew, unsafeRead, unsafeWrite+  )+import qualified Numeric.LinearAlgebra.Data as H (+    (!), Matrix, Vector, diag, fromColumns, fromList, fromLists+  , fromRows, konst, maxElement, maxIndex, toColumns, tr+  )+import qualified Numeric.LinearAlgebra.HMatrix as H (+    (<>), (#>), sumElements+  )++type LogLikelihood = Double++-- | More efficient data structure of the 'IOHMM' model. The 'inputs',+--   'states', and 'outputs' in 'IOHMM' are represented by their indices.+--   The 'initialStateDist', 'transitionDist', and 'emissionDist' are+--   represented by matrices. The 'emissionDistT' is a transposed matrix+--   in order to simplify the calculation.+data IOHMM = IOHMM { nInputs          :: Int -- ^ Number of inputs+                   , nStates          :: Int -- ^ Number of states+                   , nOutputs         :: Int -- ^ Number of outputs+                   , initialStateDist :: H.Vector Double+                   , transitionDist   :: V.Vector (H.Matrix Double)+                   , emissionDistT    :: H.Matrix Double+                   }++instance NFData IOHMM where+  rnf hmm = rnf m `seq` rnf k `seq` rnf l `seq` rnf pi0 `seq` rnf w `seq` rnf phi'+    where+      m    = nInputs hmm+      k    = nStates hmm+      l    = nOutputs hmm+      pi0  = initialStateDist hmm+      w    = transitionDist hmm+      phi' = emissionDistT hmm++init :: Int -> Int -> Int -> RVar IOHMM+init m k l = do+  pi0 <- H.fromList <$> stdSimplex (k-1)+  w   <- V.replicateM m (H.fromLists <$> replicateM k (stdSimplex (k-1)))+  phi <- H.fromLists <$> replicateM k (stdSimplex (l-1))+  return IOHMM { nInputs          = m+               , nStates          = k+               , nOutputs         = l+               , initialStateDist = pi0+               , transitionDist   = w+               , emissionDistT    = H.tr phi+               }++withEmission :: IOHMM -> U.Vector (Int, Int) -> IOHMM+withEmission model xys = model'+  where+    n = U.length xys+    k = nStates model+    l = nOutputs model+    ss = [0..(k-1)]+    os = [0..(l-1)]+    ys = U.map snd xys++    step m = fst $ baumWelch1 (m { emissionDistT = H.tr phi }) n xys+      where+        phi :: H.Matrix Double+        phi = let zs  = fst $ viterbi m xys+                  fs  = G.frequencies $ U.zip zs ys+                  hs  = H.fromLists $ map (\s -> map (\o ->+                          M.findWithDefault 0 (s, o) fs) os) ss+                  -- hs' is needed to not yield NaN vectors+                  hs' = hs + H.konst 1e-9 (k, l)+                  ns  = hs' H.#> H.konst 1 k+              in hs' / H.fromColumns (replicate l ns)++    ms  = iterate step model+    ms' = tail ms+    ds  = zipWith euclideanDistance ms ms'++    model' = fst $ head $ dropWhile ((> 1e-9) . snd) $ zip ms' ds++-- | Return the Euclidean distance between two models.+euclideanDistance :: IOHMM -> IOHMM -> Double+euclideanDistance model model' =+  sqrt $ sum $ H.sumElements ((phi - phi') ** 2) :+               map (\i -> H.sumElements ((w i - w' i) ** 2)) is+  where+    is   = [0..(nInputs model - 1)]+    w    = V.unsafeIndex (transitionDist model)+    w'   = V.unsafeIndex (transitionDist model')+    phi  = emissionDistT model+    phi' = emissionDistT model'++viterbi :: IOHMM -> U.Vector (Int, Int) -> (U.Vector Int, LogLikelihood)+viterbi model xys = (path, logL)+  where+    n = U.length xys++    -- First, we calculate the value function and the state maximizing it+    -- for each time.+    deltas :: V.Vector (H.Vector Double)+    psis   :: V.Vector (U.Vector Int)+    (deltas, psis) = runST $ do+      ds <- MV.unsafeNew n+      ps <- MV.unsafeNew n+      let (_, y0) = U.unsafeIndex xys 0+      MV.unsafeWrite ds 0 $ log (phi' H.! y0) + log pi0+      forM_ [1..(n-1)] $ \t -> do+        d <- MV.unsafeRead ds (t-1)+        let (x, y) = U.unsafeIndex xys t+            dws    = map (\wj -> d + log wj) (w' x)+        MV.unsafeWrite ds t $ log (phi' H.! y) + H.fromList (map H.maxElement dws)+        MV.unsafeWrite ps t $ U.fromList (map H.maxIndex dws)+      ds' <- V.unsafeFreeze ds+      ps' <- V.unsafeFreeze ps+      return (ds', ps')+      where+        pi0  = initialStateDist model+        w'   = H.toColumns . V.unsafeIndex (transitionDist model)+        phi' = emissionDistT model++    deltaE = V.unsafeIndex deltas (n-1)++    -- The most likely path and corresponding log likelihood are as follows.+    path = runST $ do+      ix <- MU.unsafeNew n+      MU.unsafeWrite ix (n-1) $ H.maxIndex deltaE+      forM_ [n-l | l <- [1..(n-1)]] $ \t -> do+        i <- MU.unsafeRead ix t+        let psi = V.unsafeIndex psis t+        MU.unsafeWrite ix (t-1) $ U.unsafeIndex psi i+      U.unsafeFreeze ix+    logL = H.maxElement deltaE++baumWelch :: IOHMM -> U.Vector (Int, Int) -> [(IOHMM, LogLikelihood)]+baumWelch model xys = zip models (tail logLs)+  where+    n = U.length xys+    step (m, _)     = baumWelch1 m n xys+    (models, logLs) = unzip $ iterate step (model, undefined)++-- | Perform one step of the Baum-Welch algorithm and return the updated+--   model and the likelihood of the old model.+baumWelch1 :: IOHMM -> Int -> U.Vector (Int, Int) -> (IOHMM, LogLikelihood)+baumWelch1 model n xys = force (model', logL)+  where+    m = nInputs model+    k = nStates model+    l = nOutputs model+    (xs, ys) = U.unzip xys++    -- First, we calculate the alpha, beta, and scaling values using the+    -- forward-backward algorithm.+    (alphas, cs) = forward model n xys+    betas        = backward model n xys cs++    -- Based on the alpha, beta, and scaling values, we calculate the+    -- posterior distribution, i.e., gamma and xi values.+    (gammas, xis) = posterior model n xys alphas betas cs++    -- Using the gamma and xi values, we obtain the optimal initial state+    -- probability vector, transition probability matrix, and emission+    -- probability matrix.+    pi0  = V.unsafeIndex gammas 0+    w    = let xis' i = V.map snd $ V.filter ((== i) . fst) $ V.zip (G.convert $ U.unsafeTail xs) xis+               ds     = V.foldl1' (+) . xis'  -- denominators+               ns i   = ds i H.#> H.konst 1 k -- numerators+           in V.map (\i -> H.diag (H.konst 1 k / ns i) H.<> ds i) (V.generate m id)+    phi' = let gs' o = V.map snd $ V.filter ((== o) . fst) $ V.zip (G.convert ys) gammas+               ds    = V.foldl1' (+) . gs'  -- denominators+               ns    = V.foldl1' (+) gammas -- numerators+           in H.fromRows $ map (\o -> ds o / ns) [0..(l-1)]++    -- We finally obtain the new model and the likelihood for the old model.+    model' = model { initialStateDist = pi0+                   , transitionDist   = w+                   , emissionDistT    = phi'+                   }+    logL = - (U.sum $ U.map log cs)++-- | Return alphas and scaling variables.+forward :: IOHMM -> Int -> U.Vector (Int, Int) -> (V.Vector (H.Vector Double), U.Vector Double)+{-# INLINE forward #-}+forward model n xys = runST $ do+  as <- MV.unsafeNew n+  cs <- MU.unsafeNew n+  let (_, y0) = U.unsafeIndex xys 0+      a0      = (phi' H.! y0) * pi0+      c0      = 1 / H.sumElements a0+  MV.unsafeWrite as 0 (H.konst c0 k * a0)+  MU.unsafeWrite cs 0 c0+  forM_ [1..(n-1)] $ \t -> do+    a <- MV.unsafeRead as (t-1)+    let (x, y) = U.unsafeIndex xys t+        a'     = (phi' H.! y) * (w' x H.#> a)+        c'     = 1 / H.sumElements a'+    MV.unsafeWrite as t (H.konst c' k * a')+    MU.unsafeWrite cs t c'+  as' <- V.unsafeFreeze as+  cs' <- U.unsafeFreeze cs+  return (as', cs')+  where+    k    = nStates model+    pi0  = initialStateDist model+    w'   = H.tr . V.unsafeIndex (transitionDist model)+    phi' = emissionDistT model++-- | Return betas using scaling variables.+backward :: IOHMM -> Int -> U.Vector (Int, Int) -> U.Vector Double -> V.Vector (H.Vector Double)+{-# INLINE backward #-}+backward model n xys cs = runST $ do+  bs <- MV.unsafeNew n+  let bE = H.konst 1 k+      cE = U.unsafeIndex cs (n-1)+  MV.unsafeWrite bs (n-1) (H.konst cE k * bE)+  forM_ [n-l | l <- [1..(n-1)]] $ \t -> do+    b <- MV.unsafeRead bs t+    let (x, y) = U.unsafeIndex xys t+        b'     = w x H.#> ((phi' H.! y) * b)+        c'     = U.unsafeIndex cs (t-1)+    MV.unsafeWrite bs (t-1) (H.konst c' k * b')+  V.unsafeFreeze bs+  where+    k    = nStates model+    w    = V.unsafeIndex (transitionDist model)+    phi' = emissionDistT model++-- | Return the posterior distribution.+posterior :: IOHMM -> Int -> U.Vector (Int, Int) -> V.Vector (H.Vector Double) -> V.Vector (H.Vector Double) -> U.Vector Double -> (V.Vector (H.Vector Double), V.Vector (H.Matrix Double))+{-# INLINE posterior #-}+posterior model _ xys alphas betas cs = (gammas, xis)+  where+    gammas = V.zipWith3 (\a b c -> a * b / H.konst c k)+               alphas betas (G.convert cs)+    xis    = V.zipWith3 (\a b (x, y) -> H.diag a H.<> w x H.<> H.diag (b * (phi' H.! y)))+               alphas (V.unsafeTail betas) (G.convert $ U.unsafeTail xys)+    k    = nStates model+    w    = V.unsafeIndex (transitionDist model)+    phi' = emissionDistT model
− tests/doctests.hs
@@ -1,8 +0,0 @@-module Main (main) where--import Test.DocTest--main :: IO ()-main = doctest [ "-isrc"-               , "src/Learning/HMM.hs"-               ]