lda (empty) → 0.0.1
raw patch · 7 files changed
+410/−0 lines, 7 filesdep +basedep +containersdep +ghc-primsetup-changed
Dependencies added: base, containers, ghc-prim, mtl, random-fu, random-source, rvar, vector
Files
- LICENSE +30/−0
- NLP/LDA.hs +241/−0
- NLP/LDA/UnboxedMaybeVector.hs +52/−0
- NLP/LDA/Utils.hs +12/−0
- README +0/−0
- Setup.hs +2/−0
- lda.cabal +73/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c)2012, Grzegorz Chrupala++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 Grzegorz Chrupala 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.
+ NLP/LDA.hs view
@@ -0,0 +1,241 @@+{-# LANGUAGE DeriveGeneric , BangPatterns #-}+-- | Latent Dirichlet Allocation+--+-- Simple implementation of a collapsed Gibbs sampler for LDA. This+-- library uses the topic modeling terminology (documents, words,+-- topics), even though it is generic. For example if used for word+-- class induction, replace documents with word types, words with+-- features and topics with word classes.++module NLP.LDA+ ( -- * Running samplers+ runSampler+ , pass+ , runLDA+ -- * Datatypes+ , Sampler + , LDA+ , Finalized+ , Doc+ , D+ , W+ , Z+ -- * Access model information+ , docTopics+ , wordTopics+ , topics+ , alphasum+ , beta+ , topicNum+ , vSize+ , model+ , topicDocs+ , topicWords+ -- * Initialization and finalization+ , initial+ , finalize+ -- * Prediction+ , docTopicWeights+ -- * Miscelaneous+ , compress+ , Table2D+ , Table1D+ )+where +-- Standard libraries +import qualified Data.IntMap as IntMap +import qualified Data.Vector.Unboxed as U+import qualified Data.Vector as V+import qualified Data.List as List+import Prelude hiding (sum)++-- Third party module+import GHC.Generics (Generic)+import Data.Random (rvarT)+import Data.RVar+import Data.Random.Distribution.Categorical +import Control.Monad.State+import Data.Random.Source.PureMT (pureMT)+import Data.Word (Word64)++-- Package modules+import NLP.LDA.Utils (count)+import NLP.LDA.UnboxedMaybeVector () ++-- Exported types+type D = Int+type Z = Int +type W = Int+type Doc = (D, U.Vector (W, Maybe Z))++type Table2D = IntMap.IntMap Table1D+type Table1D = IntMap.IntMap Double++-- | Abstract type holding the settings and the state of the sampler+data LDA = + LDA + { docTopics :: Table2D -- ^ Document-topic counts+ , wordTopics :: Table2D -- ^ Word-topic counts+ , topics :: Table1D -- ^ Topic counts+ , alphasum :: !Double -- ^ alpha * K Dirichlet parameter (topic sparseness)+ , beta :: !Double -- ^ beta Dirichlet parameter (word sparseness)+ , topicNum :: !Int -- ^ Number of topics K+ , vSize :: !Int -- ^ Number of unique words+ } deriving (Generic)++-- | Abstract type holding the LDA model, and the inverse count tables+data Finalized = + Finalized + { model :: LDA -- ^ LDA model+ , topicDocs :: Table2D -- ^ Inverse document-topic counts+ , topicWords :: Table2D -- ^ Inverse word-topic counts+ }+ deriving (Generic)++-- | Custom random variable representing the LDA Gibbs sampler+type Sampler a = RVarT (State LDA) a+ +-- Exported functions + +-- | @initial k a b@ initializes model with @k@ topics, @a/k@ alpha+-- hyperparameter and @b@ beta hyperparameter.+initial :: Int -> Double -> Double -> LDA+initial k a b = + LDA { docTopics = IntMap.empty+ , wordTopics = IntMap.empty+ , topics = IntMap.empty+ , alphasum = a+ , beta = b+ , topicNum = k + , vSize = 0 + }+ +-- | @finalize m@ creates a finalized model from LDA model @m@+finalize :: LDA -> Finalized +finalize m = + Finalized { model = m + , topicDocs = invert . docTopics $ m+ , topicWords = invert . wordTopics $ m }++++-- | @pass batch@ runs one pass of Gibbs sampling on documents in @batch@ +pass :: V.Vector Doc -> Sampler (V.Vector Doc)+pass = V.mapM passOne+++-- | @runSampler seed m s@ runs sampler @s@ with @seed@ and initial+-- model @m@. The random number generator used is+-- System.Random.Mersenne.Pure64.+runSampler :: Word64 -> LDA -> Sampler a -> (a, LDA)+runSampler seed m = + flip runState m+ . flip evalStateT (pureMT seed)+ . sampleRVarTWith lift ++ +-- | @runLDA seed n m ds@ creates and runs an LDA sampler with @seed@+-- for @n@ passes with initial model @m@ on the batch of documents+-- @ds@. The random number generator used is+-- System.Random.Mersenne.Pure64.+runLDA :: Word64 + -> Int + -> LDA + -> V.Vector Doc + -> (V.Vector Doc, LDA)+runLDA seed n m ds = runSampler seed m . foldM (const . pass) ds + $ [1..n]++-- | Remove zero counts from the doc/topic table+compress :: IntMap.IntMap (IntMap.IntMap Double) + -> IntMap.IntMap (IntMap.IntMap Double) +compress = IntMap.map dezero++-- Private functions --++-- | Run a pass on a single doc+passOne :: Doc -> Sampler Doc+passOne (d, wz) = do + zs <- U.mapM one wz+ return (d, U.zip (U.map fst wz) (U.map Just zs))+ where one (w, mz) = do+ m <- lift get+ let m' = maybe m (update (-1) m d w) mz -- decrement counts+ lift $ put m'+ z <- randomZ d w -- sample topic+ lift $ put (update 1 m' d w z) -- increment counts+ return z++-- | Sample a random topic for doc d and word w+randomZ :: D -> W -> Sampler Z+randomZ d w = do+ m <- lift get+ sampleCategorical . fromWeightedList . U.toList . U.map swap . U.indexed + . wordTopicWeights m d + $ w+ +-- | @topicWeights m d w@ returns the unnormalized probabilities of+-- topics for word @w@ in document @d@ given LDA model @m@.+wordTopicWeights :: LDA -> D -> W -> U.Vector Double+wordTopicWeights m d w =+ let k = topicNum m+ a = alphasum m / fromIntegral k+ b = beta m+ dt = IntMap.findWithDefault IntMap.empty d . docTopics $ m+ wt = IntMap.findWithDefault IntMap.empty w . wordTopics $ m+ v = fromIntegral . vSize $ m+ weights = [ (count z dt + a) -- n(z,d) + alpha + * (count z wt + b) -- n(z,w) + beta+ * (1/(count z (topics m) + v * b)) + -- n(.,w) + V * beta+ | z <- [0..k-1] ]+ in U.fromList weights+{-# INLINE wordTopicWeights #-}++-- | @docTopicWeights m doc@ returns unnormalized topic probabilities+-- for document doc given LDA model @m@+docTopicWeights :: LDA -> Doc -> U.Vector Double+docTopicWeights m (d, ws) = + U.accumulate (+) (U.replicate (topicNum m) 0)+ . U.concatMap (U.indexed . wordTopicWeights m d)+ . U.map fst + $ ws+{-# INLINE docTopicWeights #-}+ +-- | Update counts in the model corresponding to given doc, word and topic+update :: Double -> LDA -> D -> W -> Z -> LDA+update c m d w z = + m { docTopics = upd c (docTopics m) d z+ , wordTopics = upd c (wordTopics m) w z+ , topics = IntMap.insertWith' (+) z c (topics m) + , vSize = vSize m + (fromEnum . IntMap.notMember w . wordTopics $ m)+ }+ +-- FIXME: define a more efficient version+-- | Increment table m by c at key (k,k')+upd :: Double -> Table2D -> Int -> Int -> Table2D+upd c m k k' = IntMap.insertWith' (flip (IntMap.unionWith (+)))+ k + (IntMap.singleton k' c)+ m+{-# INLINE upd #-}++sampleCategorical :: Categorical Double Z -> Sampler Z+sampleCategorical = sampleRVarT . rvarT +{-# INLINE sampleCategorical #-}++dezero :: IntMap.IntMap Double -> IntMap.IntMap Double+dezero = IntMap.filter (/=0)+{-# INLINE dezero #-}++-- | Swap the order of keys on Table2D+invert :: Table2D -> Table2D+invert outer = + List.foldl' (\z (k,k',v) -> upd v z k k') IntMap.empty + [ (k',k,v)+ | (k, inner) <- IntMap.toList outer+ , (k', v) <- IntMap.toList inner ]+{-# INLINE invert #-}++swap :: (Int, Double) -> (Double, Int)+swap (!a, !b) = (b, a)
+ NLP/LDA/UnboxedMaybeVector.hs view
@@ -0,0 +1,52 @@+{-# LANGUAGE Rank2Types, MultiParamTypeClasses, FlexibleContexts,+ TypeFamilies, ScopedTypeVariables, BangPatterns, + TupleSections #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+module NLP.LDA.UnboxedMaybeVector ()+where+import qualified Data.Vector.Generic as G+import qualified Data.Vector.Generic.Mutable as M+import Data.Vector.Unboxed.Base (MVector, Vector, Unbox)++import Control.Monad ( liftM )++newtype instance MVector s (Maybe a) = MV_Maybe (MVector s (a,Bool))+newtype instance Vector (Maybe a) = V_Maybe (Vector (a,Bool))++instance (Num a, Unbox a) => Unbox (Maybe a)++nothing :: (Num a, Unbox a) => a+nothing = 0+{-# INLINE nothing #-}++instance (Num a, Unbox a) => M.MVector MVector (Maybe a) where+ {-# INLINE basicLength #-}+ {-# INLINE basicUnsafeSlice #-}+ {-# INLINE basicOverlaps #-}+ {-# INLINE basicUnsafeNew #-}+ {-# INLINE basicUnsafeRead #-}+ {-# INLINE basicUnsafeWrite #-}+ basicLength (MV_Maybe v) = M.basicLength v+ basicUnsafeSlice i n (MV_Maybe v) = MV_Maybe $ M.basicUnsafeSlice i n v+ basicOverlaps (MV_Maybe v1) (MV_Maybe v2) = M.basicOverlaps v1 v2+ basicUnsafeNew n = MV_Maybe `liftM` M.basicUnsafeNew n+ basicUnsafeRead (MV_Maybe v) i = fromPair `liftM` M.basicUnsafeRead v i+ basicUnsafeWrite (MV_Maybe v) i mx = M.basicUnsafeWrite v i (maybe (nothing,False) (,True) mx)+ +instance (Num a, Unbox a) => G.Vector Vector (Maybe a) where+ {-# INLINE basicLength #-}+ {-# INLINE basicUnsafeFreeze #-}+ {-# INLINE basicUnsafeThaw #-}+ {-# INLINE basicUnsafeSlice #-}+ {-# INLINE basicUnsafeIndexM #-}+ basicLength (V_Maybe v) = G.basicLength v+ basicUnsafeFreeze (MV_Maybe v) = V_Maybe `liftM` G.basicUnsafeFreeze v+ basicUnsafeThaw (V_Maybe v) = MV_Maybe `liftM` G.basicUnsafeThaw v+ basicUnsafeSlice i n (V_Maybe v) = V_Maybe $ G.basicUnsafeSlice i n v+ basicUnsafeIndexM (V_Maybe v) i+ = fromPair `liftM` G.basicUnsafeIndexM v i+ +fromPair :: (Unbox a) => (a, Bool) -> Maybe a +fromPair (_, False) = Nothing+fromPair (x, True) = Just x+{-# INLINE fromPair #-}
+ NLP/LDA/Utils.hs view
@@ -0,0 +1,12 @@+module NLP.LDA.Utils+ ( count+ )+where +import qualified Data.IntMap as IntMap ++count :: Int -> IntMap.IntMap Double -> Double+count z t = case IntMap.findWithDefault 0 z t of+ n | n < 0 -> error "NLP.LDA.Utils.count: negative count"+ n -> n+{-# INLINE count #-} +
+ README view
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ lda.cabal view
@@ -0,0 +1,73 @@+-- lda.cabal auto-generated by cabal init. For additional options, see+-- http://www.haskell.org/cabal/release/cabal-latest/doc/users-guide/authors.html#pkg-descr.+-- The name of the package.+Name: lda++-- The package version. See the Haskell package versioning policy+-- (http://www.haskell.org/haskellwiki/Package_versioning_policy) for+-- standards guiding when and how versions should be incremented.+Version: 0.0.1++-- A short (one-line) description of the package.+Synopsis: Online Latent Dirichlet Allocation++-- A longer description of the package.+Description: Online Gibbs sampler for Latent Dirichlet Allocation. + LDA is a generative admixture model frequently used + for topic modeling and other applications. The primary+ goal of this implementation is to be used for probabilistic + soft word class induction.+ The sampler can be used in an online as well as batch mode.++-- URL for the project homepage or repository.+Homepage: https://bitbucket.org/gchrupala/colada++-- The license under which the package is released.+License: BSD3++-- The file containing the license text.+License-file: LICENSE++-- The package author(s).+Author: Grzegorz Chrupała++-- An email address to which users can send suggestions, bug reports,+-- and patches.+Maintainer: pitekus@gmail.com++-- A copyright notice.+-- Copyright: ++Category: Natural Language Processing+++-- Extra files to be distributed with the package, such as examples or+-- a README.+Extra-source-files: README++Build-type: Simple++-- Constraint on the version of Cabal needed to build this package.+Cabal-version: >=1.2+++Library+ -- Modules exported by the library.+ Exposed-modules: NLP.LDA+ , NLP.LDA.UnboxedMaybeVector+ + -- Packages needed in order to build this package.+ Build-depends: base >= 3 && < 5+ , containers >= 0.4 + , random-fu >= 0.2.1.1+ , rvar >= 0.2+ , random-source >= 0.3.0.2+ , mtl >= 2.0+ , ghc-prim >= 0.2+ , vector >= 0.9+ -- Modules not exported by this package.+ Other-modules: NLP.LDA.Utils+ + -- Extra tools (e.g. alex, hsc2hs, ...) needed to build the source.+ -- Build-tools: + GHC-options: -O2