packages feed

chatter 0.4.0.0 → 0.5.0.0

raw patch · 18 files changed

+453/−317 lines, 18 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- NLP.POS.AvgPerceptron: Class :: String -> Class
- NLP.POS.AvgPerceptron: Feat :: Text -> Feature
- NLP.POS.AvgPerceptron: Perceptron :: Map Feature (Map Class Weight) -> Map (Feature, Class) Weight -> Map (Feature, Class) Int -> Int -> Perceptron
- NLP.POS.AvgPerceptron: averageWeights :: Perceptron -> Perceptron
- NLP.POS.AvgPerceptron: data Perceptron
- NLP.POS.AvgPerceptron: emptyPerceptron :: Perceptron
- NLP.POS.AvgPerceptron: instance Constructor C1_0Class
- NLP.POS.AvgPerceptron: instance Constructor C1_0Feature
- NLP.POS.AvgPerceptron: instance Constructor C1_0Perceptron
- NLP.POS.AvgPerceptron: instance Datatype D1Class
- NLP.POS.AvgPerceptron: instance Datatype D1Feature
- NLP.POS.AvgPerceptron: instance Datatype D1Perceptron
- NLP.POS.AvgPerceptron: instance Eq Class
- NLP.POS.AvgPerceptron: instance Eq Feature
- NLP.POS.AvgPerceptron: instance Eq Perceptron
- NLP.POS.AvgPerceptron: instance Generic Class
- NLP.POS.AvgPerceptron: instance Generic Feature
- NLP.POS.AvgPerceptron: instance Generic Perceptron
- NLP.POS.AvgPerceptron: instance NFData Perceptron
- NLP.POS.AvgPerceptron: instance Ord Class
- NLP.POS.AvgPerceptron: instance Ord Feature
- NLP.POS.AvgPerceptron: instance Read Class
- NLP.POS.AvgPerceptron: instance Read Feature
- NLP.POS.AvgPerceptron: instance Read Perceptron
- NLP.POS.AvgPerceptron: instance Selector S1_0_0Perceptron
- NLP.POS.AvgPerceptron: instance Selector S1_0_1Perceptron
- NLP.POS.AvgPerceptron: instance Selector S1_0_2Perceptron
- NLP.POS.AvgPerceptron: instance Selector S1_0_3Perceptron
- NLP.POS.AvgPerceptron: instance Serialize Class
- NLP.POS.AvgPerceptron: instance Serialize Feature
- NLP.POS.AvgPerceptron: instance Serialize Perceptron
- NLP.POS.AvgPerceptron: instance Show Class
- NLP.POS.AvgPerceptron: instance Show Feature
- NLP.POS.AvgPerceptron: instance Show Perceptron
- NLP.POS.AvgPerceptron: instances :: Perceptron -> Int
- NLP.POS.AvgPerceptron: newtype Class
- NLP.POS.AvgPerceptron: newtype Feature
- NLP.POS.AvgPerceptron: predict :: Perceptron -> Map Feature Int -> Maybe Class
- NLP.POS.AvgPerceptron: totals :: Perceptron -> Map (Feature, Class) Weight
- NLP.POS.AvgPerceptron: train :: Int -> Perceptron -> [(Map Feature Int, Class)] -> IO Perceptron
- NLP.POS.AvgPerceptron: tstamps :: Perceptron -> Map (Feature, Class) Int
- NLP.POS.AvgPerceptron: type Weight = Double
- NLP.POS.AvgPerceptron: update :: Perceptron -> Class -> Class -> [Feature] -> Perceptron
- NLP.POS.AvgPerceptron: weights :: Perceptron -> Map Feature (Map Class Weight)
+ NLP.Chunk: chunk :: (ChunkTag c, Tag t) => Chunker c t -> [TaggedSentence t] -> [ChunkedSentence c t]
+ NLP.Chunk: chunkStr :: (ChunkTag c, Tag t) => POSTagger t -> Chunker c t -> String -> String
+ NLP.Chunk: chunkText :: (ChunkTag c, Tag t) => POSTagger t -> Chunker c t -> Text -> Text
+ NLP.Corpora.Conll: LS :: Tag
+ NLP.Corpora.Conll: instance Constructor C1_47Tag
+ NLP.Extraction.Parsec: instance (Monad m, ChunkTag c, Tag t) => Stream (ChunkedSentence c t) m (ChunkOr c t)
+ NLP.ML.AvgPerceptron: Class :: String -> Class
+ NLP.ML.AvgPerceptron: Feat :: Text -> Feature
+ NLP.ML.AvgPerceptron: Perceptron :: Map Feature (Map Class Weight) -> Map (Feature, Class) Weight -> Map (Feature, Class) Int -> Int -> Perceptron
+ NLP.ML.AvgPerceptron: averageWeights :: Perceptron -> Perceptron
+ NLP.ML.AvgPerceptron: data Perceptron
+ NLP.ML.AvgPerceptron: emptyPerceptron :: Perceptron
+ NLP.ML.AvgPerceptron: instance Constructor C1_0Class
+ NLP.ML.AvgPerceptron: instance Constructor C1_0Feature
+ NLP.ML.AvgPerceptron: instance Constructor C1_0Perceptron
+ NLP.ML.AvgPerceptron: instance Datatype D1Class
+ NLP.ML.AvgPerceptron: instance Datatype D1Feature
+ NLP.ML.AvgPerceptron: instance Datatype D1Perceptron
+ NLP.ML.AvgPerceptron: instance Eq Class
+ NLP.ML.AvgPerceptron: instance Eq Feature
+ NLP.ML.AvgPerceptron: instance Eq Perceptron
+ NLP.ML.AvgPerceptron: instance Generic Class
+ NLP.ML.AvgPerceptron: instance Generic Feature
+ NLP.ML.AvgPerceptron: instance Generic Perceptron
+ NLP.ML.AvgPerceptron: instance NFData Perceptron
+ NLP.ML.AvgPerceptron: instance Ord Class
+ NLP.ML.AvgPerceptron: instance Ord Feature
+ NLP.ML.AvgPerceptron: instance Read Class
+ NLP.ML.AvgPerceptron: instance Read Feature
+ NLP.ML.AvgPerceptron: instance Read Perceptron
+ NLP.ML.AvgPerceptron: instance Selector S1_0_0Perceptron
+ NLP.ML.AvgPerceptron: instance Selector S1_0_1Perceptron
+ NLP.ML.AvgPerceptron: instance Selector S1_0_2Perceptron
+ NLP.ML.AvgPerceptron: instance Selector S1_0_3Perceptron
+ NLP.ML.AvgPerceptron: instance Serialize Class
+ NLP.ML.AvgPerceptron: instance Serialize Feature
+ NLP.ML.AvgPerceptron: instance Serialize Perceptron
+ NLP.ML.AvgPerceptron: instance Show Class
+ NLP.ML.AvgPerceptron: instance Show Feature
+ NLP.ML.AvgPerceptron: instance Show Perceptron
+ NLP.ML.AvgPerceptron: instances :: Perceptron -> Int
+ NLP.ML.AvgPerceptron: newtype Class
+ NLP.ML.AvgPerceptron: newtype Feature
+ NLP.ML.AvgPerceptron: predict :: Perceptron -> Map Feature Int -> Maybe Class
+ NLP.ML.AvgPerceptron: totals :: Perceptron -> Map (Feature, Class) Weight
+ NLP.ML.AvgPerceptron: train :: Int -> Perceptron -> [(Map Feature Int, Class)] -> IO Perceptron
+ NLP.ML.AvgPerceptron: tstamps :: Perceptron -> Map (Feature, Class) Int
+ NLP.ML.AvgPerceptron: type Weight = Double
+ NLP.ML.AvgPerceptron: update :: Perceptron -> Class -> Class -> [Feature] -> Perceptron
+ NLP.ML.AvgPerceptron: weights :: Perceptron -> Map Feature (Map Class Weight)
+ NLP.Types.Tags: isDt :: Tag a => a -> Bool
+ NLP.Types.Tree: showChunkedSent :: (ChunkTag c, Tag t) => ChunkedSentence c t -> Text

Files

appsrc/ChunkTrainer.hs view
@@ -5,7 +5,7 @@ import qualified Data.Text.IO as T import System.Environment (getArgs) -import NLP.POS.AvgPerceptron (emptyPerceptron)+import NLP.ML.AvgPerceptron (emptyPerceptron) import qualified NLP.Chunk.AvgPerceptronChunker as Avg import NLP.Chunk (train, saveChunker) import NLP.Types.IOB
changelog.md view
@@ -1,3 +1,34 @@+= 0.5.0.0 =++ - Added chunk, chunkText, and chunkStr functions to NLP.Chunk to+   make it easier to experiment with the chunker.++ - Added a formatting function to show ChunkedSententences+   (NLP.Types.Tree.showChunkedSent) that formats chunks in bracket notation, eg:++   > chunkText tgr chk "The dog jumped over the reluctant cat."+   "[NP The/DT dog/NN] [VP jumped/VBD] [NP over/IN the/DT reluctant/JJ cat/NN] ./."++   Notice that the features still need some tuning for the chunker.++ - Moved the AveragePerceptron module into a NLP.ML (Machine Learning) module.++ - Added the 'tags-since-dt' feature to the AveragePerceptronChunker features+   and retrained the Conll2000 models.++= 0.4.0.0 =++ - Added phrasal chunking via an Averaged Perceptron Chunker,+   trained on the Conll 2000 corpus.++ - Added a POS tagger trained on the Conll 2000 corpus, because the+   Conll chunker relies on that tagset.++ - Set the Conll 2000 POS tagger to the defaultTagger. Note that+   the tagset is much smaller than the Brown tagset used by the+   previous defaultTagger.  The Brown tagger is still available from+   NLP.POS.brownTagger.+ = 0.3.0.1 =   - Bumped dependency on base to >= 4.6 (from >= 4) because of
chatter.cabal view
@@ -1,5 +1,5 @@ name:                chatter-version:             0.4.0.0+version:             0.5.0.0 synopsis:            A library of simple NLP algorithms. description:         chatter is a collection of simple Natural Language                      Processing algorithms.@@ -11,10 +11,17 @@                        by Matthew Honnibal:                        (<http://honnibal.wordpress.com/2013/09/11/a-good-part-of-speechpos-tagger-in-about-200-lines-of-python/>) See 'NLP.POS' for the details of part-of-speech tagging with chatter.                      .+                     * Phrasal Chunking (also with an Averaged Perceptron) to identify arbitrary chunks based on training data.+                     .                      * Document similarity; A cosine-based similarity measure, and TF-IDF calculations,                        are available in the 'NLP.Similarity.VectorSim' module.                      .                      * Information Extraction patterns via (<http://www.haskell.org/haskellwiki/Parsec/>) Parsec+                     .+                     Chatter comes with models for POS tagging and+                     Phrasal Chunking that have been trained on the+                     Brown corpus (POS only) and the Conll2000 corpus+                     (POS and Chunking) homepage:            http://github.com/creswick/chatter Bug-Reports:         http://github.com/creswick/chatter/issues category:            Natural language processing@@ -42,12 +49,12 @@    Other-modules:    Paths_chatter     Exposed-modules:  NLP.POS-                     NLP.POS.AvgPerceptron                      NLP.POS.AvgPerceptronTagger                      NLP.POS.LiteralTagger                      NLP.POS.UnambiguousTagger                      NLP.Chunk                      NLP.Chunk.AvgPerceptronChunker+                     NLP.ML.AvgPerceptron                      NLP.Types                      NLP.Types.General                      NLP.Types.IOB
data/models/README view
@@ -21,8 +21,22 @@ 2000 training set.  +Generated by:++$ time ./dist/build/trainPOS/trainPOS ~/nltk_data/corpora/conll2000/train.txt conll2000.pos.model++real	2m48.866s+user	2m44.500s+sys	0m1.898s+ --------------------------------------------------------------------- conll2000.chunk.model.gz ---------------------------------------------------------------------  Averaged Perceptron Chunker trained on the Conll 2000 training set.++ $ time ./dist/build/trainChunker/trainChunker ~/nltk_data/corpora/conll2000/train.txt conll2000.chunk.model++real	0m20.730s+user	0m20.298s+sys	0m0.290s
data/models/conll2000.chunk.model.gz view

binary file changed (204463 → 630879 bytes)

data/models/conll2000.pos.model.gz view

file too large to diff

src/NLP/Chunk.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} module NLP.Chunk where @@ -9,26 +10,58 @@ import           Data.Map                    (Map) import qualified Data.Map                    as Map import           Data.Serialize              (decode, encode)+import           Data.Text                   (Text)+import qualified Data.Text                   as T import           System.FilePath             ((</>)) +import           NLP.POS                     (tag) import           NLP.Types-import           NLP.Chunk.AvgPerceptronChunker+import           NLP.Chunk.AvgPerceptronChunker (Chunker(..)) import qualified NLP.Chunk.AvgPerceptronChunker as Avg  import qualified NLP.Corpora.Conll as C  import           Paths_chatter +-- | A basic Phrasal chunker. defaultChunker :: IO (Chunker C.Chunk C.Tag) defaultChunker = conllChunker +-- | Convenient function to load the Conll2000 Chunker. conllChunker :: IO (Chunker C.Chunk C.Tag) conllChunker = do   dir <- getDataDir   loadChunker (dir </> "data" </> "models" </> "conll2000.chunk.model.gz") +-- | Train a chunker on a set of additional examples. train :: (ChunkTag c, Tag t) => Chunker c t -> [ChunkedSentence c t] -> IO (Chunker c t) train ch exs = chTrainer ch exs++-- | Chunk a 'TaggedSentence' that has been produced by a Chatter+-- tagger, producing a rich representation of the Chunks and the Tags+-- detected.+--+-- If you just want to see chunked output from standard text, you+-- probably want 'chunkText' or 'chunkStr'.+chunk :: (ChunkTag c, Tag t) => Chunker c t -> [TaggedSentence t] -> [ChunkedSentence c t]+chunk chk input = chChunker chk input+++-- | Convenience funciton to Tokenize, POS-tag, then Chunk the+-- provided text, and format the result in an easy-to-read format.+--+-- > > tgr <- defaultTagger+-- > > chk <- defaultChunker+-- > > chunkText tgr chk "The brown dog jumped over the lazy cat."+-- > "[NP The/DT brown/NN dog/NN] [VP jumped/VBD] [NP over/IN the/DT lazy/JJ cat/NN] ./."+--+chunkText :: (ChunkTag c, Tag t) => POSTagger t -> Chunker c t -> Text -> Text+chunkText tgr chk input = T.intercalate " " $ map showChunkedSent $ chunk chk $ tag tgr input+++-- | A wrapper around 'chunkText' that packs strings.+chunkStr :: (ChunkTag c, Tag t) => POSTagger t -> Chunker c t -> String -> String+chunkStr tgr chk str = T.unpack $ chunkText tgr chk $ T.pack str  -- | The default table of tagger IDs to readTagger functions.  Each -- tagger packaged with Chatter should have an entry here.  By
src/NLP/Chunk/AvgPerceptronChunker.hs view
@@ -12,9 +12,9 @@   ) where -import NLP.POS.AvgPerceptron ( Perceptron, Feature(..)-                             , Class(..), predict, update-                             , averageWeights)+import NLP.ML.AvgPerceptron ( Perceptron, Feature(..)+                            , Class(..), predict, update+                            , averageWeights) import NLP.Types  import Data.ByteString (ByteString)@@ -196,9 +196,21 @@              , ["pos+nextpos", T.intercalate "+" $ map showPOStag                                  [word, context!!(i+1) ]                ]---             , ["tags-since-dt", ""]+             , ["tags-since-dt", tagsSinceDt $ take idx tagged]              ]+   in foldl' add Map.empty features++tagsSinceDt :: Tag t => [POS t] -> Text+tagsSinceDt posToks =+  T.intercalate "-" $ tagsSinceHelper $ reverse posToks++tagsSinceHelper :: Tag t => [POS t] -> [Text]+tagsSinceHelper []      = []+tagsSinceHelper (pt@(POS t _):ts)+    | isDt t    = []+    | otherwise = [ (showPOStag pt) ] ++ (tagsSinceHelper ts)+  mkFeature :: Text -> Feature mkFeature txt = Feat $ T.copy txt
src/NLP/Corpora/Brown.hs view
@@ -49,6 +49,8 @@   startTag = START   endTag = END +  isDt tag = tag `elem` [DT, DTdollar, DT_pl_BEZ, DT_pl_MD, DTI, DTS, DTS_pl_BEZ, DTX]+ instance Arbitrary Tag where   arbitrary = elements [minBound ..] 
src/NLP/Corpora/Conll.hs view
@@ -52,6 +52,8 @@   startTag = START   endTag = END +  isDt tag = tag `elem` [DT]+ instance Arbitrary Tag where   arbitrary = elements [minBound ..] @@ -98,6 +100,15 @@   parseChunk txt = toEitherErr $ readEither $ T.unpack txt   notChunk = O +-- | These tags may actually be the Penn Treebank tags.  But I have+-- not (yet?) seen the punctuation tags added to the Penn set.+--+-- This particular list was complied from the union of:+--+--   * All tags used on the Conll2000 training corpus. (contributing the punctuation tags)+--   * The PennTreebank tags, listed here: <https://www.ling.upenn.edu/courses/Fall_2003/ling001/penn_treebank_pos.html> (which contributed LS over the items in the corpus).+--   * The tags: START, END, and Unk, which are used by Chatter.+-- data Tag = START -- ^ START tag, used in training.          | END -- ^ END tag, used in training.          | Hash -- ^ #@@ -109,40 +120,42 @@          | Comma -- ^ ,          | Term -- ^ . Sentence Terminator          | Colon -- ^ :-         | CC-         | CD-         | DT-         | EX-         | FW-         | IN-         | JJ-         | JJR-         | JJS-         | MD-         | NN-         | NNP-         | NNPS-         | NNS-         | PDT-         | POS-         | PRP-         | PRPdollar-         | RB-         | RBR-         | RBS-         | RP-         | SYM-         | TO-         | UH-         | VB-         | VBD-         | VBG-         | VBN-         | VBP-         | VBZ-         | WDT-         | WP-         | WPdollar-         | WRB+         | CC -- ^ Coordinating conjunction+         | CD -- ^ Cardinal number+         | DT -- ^ Determiner+         | EX -- ^ Existential there+         | FW -- ^ Foreign word+         | IN -- ^ Preposition or subordinating conjunction+         | JJ -- ^ Adjective+         | JJR -- ^ Adjective, comparative+         | JJS -- ^ Adjective, superlative+         | LS -- ^ List item marker+         | MD -- ^ Modal+         | NN -- ^ Noun, singular or mass+         | NNS -- ^ Noun, plural+         | NNP -- ^ Proper noun, singular+         | NNPS -- ^ Proper noun, plural+         | PDT -- ^ Predeterminer+         | POS -- ^ Possessive ending+         | PRP -- ^ Personal pronoun+         | PRPdollar -- ^ Possessive pronoun+         | RB -- ^ Adverb+         | RBR -- ^ Adverb, comparative+         | RBS -- ^ Adverb, superlative+         | RP -- ^ Particle+         | SYM -- ^ Symbol+         | TO -- ^ to+         | UH -- ^ Interjection+         | VB -- ^ Verb, base form+         | VBD -- ^ Verb, past tense+         | VBG -- ^ Verb, gerund or present participle+         | VBN -- ^ Verb, past participle+         | VBP -- ^ Verb, non-3rd person singular present+         | VBZ -- ^ Verb, 3rd person singular present+         | WDT -- ^ Wh-determiner+         | WP -- ^ Wh-pronoun+         | WPdollar -- ^ Possessive wh-pronoun+         | WRB -- ^ Wh-adverb          | Unk   deriving (Read, Show, Ord, Eq, Generic, Enum, Bounded)+
src/NLP/Extraction/Parsec.hs view
@@ -35,7 +35,7 @@ import Text.Parsec.Pos  (newPos)  import NLP.Types (TaggedSentence(..), Tag(..), CaseSensitive(..),-                                POS(..), Token(..))+                  POS(..), Token(..), ChunkedSentence(..), ChunkOr(..), ChunkTag)  instance (Monad m, Tag t) => Stream (TaggedSentence t) m (POS t) where   uncons (TaggedSent ts) = do@@ -43,6 +43,14 @@     case mRes of       Nothing           -> return $ Nothing       Just (mTok, rest) -> return $ Just (mTok, TaggedSent rest)+  {-# INLINE uncons #-}++instance (Monad m, ChunkTag c, Tag t) => Stream (ChunkedSentence c t) m (ChunkOr c t) where+  uncons (ChunkedSent ts) = do+    mRes <- uncons ts+    case mRes of+      Nothing           -> return $ Nothing+      Just (mTok, rest) -> return $ Just (mTok, ChunkedSent rest)   {-# INLINE uncons #-}  -- | A Parsec parser.
+ src/NLP/ML/AvgPerceptron.hs view
@@ -0,0 +1,269 @@+{-# LANGUAGE DeriveGeneric #-}+-- | Average Perceptron implementation of Part of speech tagging,+-- adapted for Haskell from this python implementation, which is described on the blog post:+--+--  * <http://honnibal.wordpress.com/2013/09/11/a-good-part-of-speechpos-tagger-in-about-200-lines-of-python/>+--+-- The Perceptron code can be found on github:+--+--  * <https://github.com/sloria/TextBlob/blob/dev/text/_perceptron.py>+--+module NLP.ML.AvgPerceptron+  ( Perceptron(..)+  , Class(..)+  , Weight+  , Feature(..)+  , emptyPerceptron+  , predict+  , train+  , update+  , averageWeights+  )+where++import Control.DeepSeq (NFData)+import Data.List (foldl')+import qualified Data.Map.Strict as Map+import Data.Map.Strict (Map)+import Data.Maybe (fromMaybe)+import Data.Serialize (Serialize)+import Data.Text (Text)+import System.Random.Shuffle (shuffleM)+import GHC.Generics++import NLP.Types ()++newtype Feature = Feat Text+    deriving (Read, Show, Eq, Ord, Generic)++instance Serialize Feature++-- | The classes that the perceptron assigns are represnted with a+-- newtype-wrapped String.+--+-- Eventually, I think this should become a typeclass, so the classes+-- can be defined by the users of the Perceptron (such as custom POS+-- tag ADTs, or more complex classes).+newtype Class = Class String+    deriving (Read, Show, Eq, Ord, Generic)++instance Serialize Class++-- | Typedef for doubles to make the code easier to read, and to make+-- this simple to change if necessary.+type Weight = Double++infinity :: Weight+infinity = recip 0++-- | An empty perceptron, used to start training.+emptyPerceptron :: Perceptron+emptyPerceptron = Perceptron { weights = Map.empty+                             , totals = Map.empty+                             , tstamps = Map.empty+                             , instances = 0 }++-- | The perceptron model.+data Perceptron = Perceptron {+    -- | Each feature gets its own weight vector, so weights is a+    -- dict-of-dicts+    weights :: Map Feature (Map Class Weight)++    -- | The accumulated values, for the averaging. These will be+    -- keyed by feature/clas tuples+    , totals :: Map (Feature, Class) Weight++    -- | The last time the feature was changed, for the averaging. Also+    -- keyed by feature/clas tuples+    -- (tstamps is short for timestamps)+    , tstamps :: Map (Feature, Class) Int++    -- | Number of instances seen+    , instances :: Int+    } deriving (Read, Show, Eq, Generic)++instance Serialize Perceptron+instance NFData Perceptron++incrementInstances :: Perceptron -> Perceptron+incrementInstances p = p { instances = 1 + (instances p) }++getTimestamp :: Perceptron -> (Feature, Class) -> Int+getTimestamp p param = Map.findWithDefault 0 param (tstamps p)++getTotal :: Perceptron -> (Feature, Class) -> Weight+getTotal p param = Map.findWithDefault 0 param (totals p)++getFeatureWeight :: Perceptron -> Feature -> Map Class Weight+getFeatureWeight p f = Map.findWithDefault Map.empty f (weights p)++-- | Predict a class given a feature vector.+--+-- Ported from python:+--+-- > def predict(self, features):+-- >     '''Dot-product the features and current weights and return the best label.'''+-- >     scores = defaultdict(float)+-- >     for feat, value in features.items():+-- >         if feat not in self.weights or value == 0:+-- >             continue+-- >         weights = self.weights[feat]+-- >         for label, weight in weights.items():+-- >             scores[label] += value * weight+-- >     # Do a secondary alphabetic sort, for stability+-- >     return max(self.classes, key=lambda label: (scores[label], label))+--+predict :: Perceptron -> Map Feature Int -> Maybe Class+predict per features = -- find highest ranked score in finalScores:+    -- trace ("features: "++ show features ++ "\n") $+    -- trace ("finalScores: "++ show sortedScores ++ "\n") $+    sortedScores+    where+      sortedScores :: Maybe Class+      sortedScores = fst $ Map.foldlWithKey ranker (Nothing, negate infinity) finalScores++      ranker r@(_, ow) nc nw | nw > ow   = (Just nc, nw)+                             | otherwise = r++      finalScores :: Map Class Weight+      finalScores = Map.foldlWithKey fn Map.empty features++      fn :: Map Class Weight -> Feature -> Int -> Map Class Weight+      fn scores f  v+         | v > 0 = case Map.lookup f (weights per) of+                     Just vec -> Map.foldlWithKey (doProd v) scores vec+                     Nothing  -> scores+         | otherwise = scores++      doProd :: Int -> Map Class Weight -> Class -> Weight -> Map Class Weight+      doProd value scores label weight =+        Map.alter (updater (weight * (fromIntegral value))) label scores++      updater :: Weight -> Maybe Weight -> Maybe Weight+      updater newVal Nothing  = Just newVal+      updater newVal (Just v) = Just (v + newVal)++-- | Update the perceptron with a new example.+--+-- > update(self, truth, guess, features)+-- >    ...+-- >         self.i += 1+-- >         if truth == guess:+-- >             return None+-- >         for f in features:+-- >             weights = self.weights.setdefault(f, {}) -- setdefault is Map.findWithDefault, and destructive.+-- >             upd_feat(truth, f, weights.get(truth, 0.0), 1.0)+-- >             upd_feat(guess, f, weights.get(guess, 0.0), -1.0)+-- >         return None+--+update :: Perceptron -> Class -> Class -> [Feature] -> Perceptron+update per truth guess features+  | truth == guess = incrementInstances per+  | otherwise      = foldr loopBody per features+    where+      loopBody :: Feature -> Perceptron -> Perceptron+      loopBody f p = let+        fweights  = getFeatureWeight p f+        cweight c = Map.findWithDefault 0 c fweights+        in upd_feat guess f (cweight guess) (-1)+             (upd_feat truth f (cweight truth) 1 p)++-- | ported from python:+--+-- > def update(self, truth, guess, features):+-- >        '''Update the feature weights.'''+-- >        def upd_feat(c, f, w, v):+-- >           param = (f, c)+-- >           self._totals[param] += (self.i - self._tstamps[param]) * w+-- >           self._tstamps[param] = self.i+-- >           self.weights[f][c] = w + v+upd_feat :: Class -> Feature -> Weight -> Weight -> Perceptron -> Perceptron+upd_feat c f w v p = let+    newInstances = 1 + (instances p) -- increment the instance counter.++    -- self._totals[param] += (self.i - self._tstamps[param]) * w+    paramTstamp = newInstances - getTimestamp p (f, c)+    tmpTotal  = (getTotal p (f, c)) + ((fromIntegral paramTstamp) * w)+    newTotals = Map.insert (f, c) tmpTotal (totals p)++    -- self._tstamps[param] = self.i+    newTstamps = Map.insert (f, c) newInstances (tstamps p)++    -- self.weights[f][c] = w + v+    newWeights = Map.insert f (Map.insert c (w + v) (getFeatureWeight p f)) (weights p)++    in p { totals = newTotals+         , tstamps = newTstamps+         , weights = newWeights }+++-- | Average the weights+--+-- Ported from Python:+--+-- > def average_weights(self):+-- >     for feat, weights in self.weights.items():+-- >         new_feat_weights = {}+-- >         for clas, weight in weights.items():+-- >             param = (feat, clas)+-- >             total = self._totals[param]+-- >             total += (self.i - self._tstamps[param]) * weight+-- >             averaged = round(total / float(self.i), 3)+-- >             if averaged:+-- >                 new_feat_weights[clas] = averaged+-- >         self.weights[feat] = new_feat_weights+-- >     return None+--+averageWeights :: Perceptron -> Perceptron+averageWeights per | (instances per) == 0 = per+                   | otherwise = per { weights = Map.mapWithKey avgWeights $ weights per+                                     , tstamps = Map.mapWithKey (\_ _->0) (tstamps per)+                                     , instances = 0 }+  where+    avgWeights :: Feature -> Map Class Weight -> Map Class Weight+    avgWeights feat ws = Map.foldlWithKey (doAvg feat) Map.empty ws++    doAvg :: Feature -> Map Class Weight -> Class -> Weight -> Map Class Weight+    doAvg f acc c w = let+      param = (f, c)+      paramTotal = instances per - getTimestamp per param++      total :: Weight+      total = (getTotal per param) + ((fromIntegral paramTotal) * w)+      averaged = roundTo 3 (total / (fromIntegral $ instances per))+      in if 0 == averaged+           then acc+           else Map.insert c averaged acc++-- | round a fractional number to a specified decimal place.+--+-- >>> roundTo 2 3.1459+-- 3.15+--+roundTo :: RealFrac a => Int -> a -> a+roundTo n f = (fromInteger $ round $ f * (10^n)) / (10.0^^n)+++-- Train a perceptron+--+-- Ported from Python:+-- > def train(nr_iter, examples):+-- >     model = Perceptron()+-- >     for i in range(nr_iter):+-- >         random.shuffle(examples)+-- >         for features, class_ in examples:+-- >             scores = model.predict(features)+-- >             guess, score = max(scores.items(), key=lambda i: i[1])+-- >             if guess != class_:+-- >                 model.update(class_, guess, features)+-- >     model.average_weights()+-- >     return model+train :: Int -> Perceptron -> [(Map Feature Int, Class)] -> IO Perceptron+train itr model exs = do+  trainingSet <- shuffleM $ concat $ take itr $ repeat exs+  return $ averageWeights $ foldl' trainEx model trainingSet++trainEx :: Perceptron -> (Map Feature Int, Class) -> Perceptron+trainEx model (feats, truth) = let+  guess = fromMaybe (Class "Unk") $ predict model feats+  in update model truth guess $ Map.keys feats
− src/NLP/POS/AvgPerceptron.hs
@@ -1,269 +0,0 @@-{-# LANGUAGE DeriveGeneric #-}--- | Average Perceptron implementation of Part of speech tagging,--- adapted for Haskell from this python implementation, which is described on the blog post:------  * <http://honnibal.wordpress.com/2013/09/11/a-good-part-of-speechpos-tagger-in-about-200-lines-of-python/>------ The Perceptron code can be found on github:------  * <https://github.com/sloria/TextBlob/blob/dev/text/_perceptron.py>----module NLP.POS.AvgPerceptron-  ( Perceptron(..)-  , Class(..)-  , Weight-  , Feature(..)-  , emptyPerceptron-  , predict-  , train-  , update-  , averageWeights-  )-where--import Control.DeepSeq (NFData)-import Data.List (foldl')-import qualified Data.Map.Strict as Map-import Data.Map.Strict (Map)-import Data.Maybe (fromMaybe)-import Data.Serialize (Serialize)-import Data.Text (Text)-import System.Random.Shuffle (shuffleM)-import GHC.Generics--import NLP.Types ()--newtype Feature = Feat Text-    deriving (Read, Show, Eq, Ord, Generic)--instance Serialize Feature---- | The classes that the perceptron assigns are represnted with a--- newtype-wrapped String.------ Eventually, I think this should become a typeclass, so the classes--- can be defined by the users of the Perceptron (such as custom POS--- tag ADTs, or more complex classes).-newtype Class = Class String-    deriving (Read, Show, Eq, Ord, Generic)--instance Serialize Class---- | Typedef for doubles to make the code easier to read, and to make--- this simple to change if necessary.-type Weight = Double--infinity :: Weight-infinity = recip 0---- | An empty perceptron, used to start training.-emptyPerceptron :: Perceptron-emptyPerceptron = Perceptron { weights = Map.empty-                             , totals = Map.empty-                             , tstamps = Map.empty-                             , instances = 0 }---- | The perceptron model.-data Perceptron = Perceptron {-    -- | Each feature gets its own weight vector, so weights is a-    -- dict-of-dicts-    weights :: Map Feature (Map Class Weight)--    -- | The accumulated values, for the averaging. These will be-    -- keyed by feature/clas tuples-    , totals :: Map (Feature, Class) Weight--    -- | The last time the feature was changed, for the averaging. Also-    -- keyed by feature/clas tuples-    -- (tstamps is short for timestamps)-    , tstamps :: Map (Feature, Class) Int--    -- | Number of instances seen-    , instances :: Int-    } deriving (Read, Show, Eq, Generic)--instance Serialize Perceptron-instance NFData Perceptron--incrementInstances :: Perceptron -> Perceptron-incrementInstances p = p { instances = 1 + (instances p) }--getTimestamp :: Perceptron -> (Feature, Class) -> Int-getTimestamp p param = Map.findWithDefault 0 param (tstamps p)--getTotal :: Perceptron -> (Feature, Class) -> Weight-getTotal p param = Map.findWithDefault 0 param (totals p)--getFeatureWeight :: Perceptron -> Feature -> Map Class Weight-getFeatureWeight p f = Map.findWithDefault Map.empty f (weights p)---- | Predict a class given a feature vector.------ Ported from python:------ > def predict(self, features):--- >     '''Dot-product the features and current weights and return the best label.'''--- >     scores = defaultdict(float)--- >     for feat, value in features.items():--- >         if feat not in self.weights or value == 0:--- >             continue--- >         weights = self.weights[feat]--- >         for label, weight in weights.items():--- >             scores[label] += value * weight--- >     # Do a secondary alphabetic sort, for stability--- >     return max(self.classes, key=lambda label: (scores[label], label))----predict :: Perceptron -> Map Feature Int -> Maybe Class-predict per features = -- find highest ranked score in finalScores:-    -- trace ("features: "++ show features ++ "\n") $-    -- trace ("finalScores: "++ show sortedScores ++ "\n") $-    sortedScores-    where-      sortedScores :: Maybe Class-      sortedScores = fst $ Map.foldlWithKey ranker (Nothing, negate infinity) finalScores--      ranker r@(_, ow) nc nw | nw > ow   = (Just nc, nw)-                             | otherwise = r--      finalScores :: Map Class Weight-      finalScores = Map.foldlWithKey fn Map.empty features--      fn :: Map Class Weight -> Feature -> Int -> Map Class Weight-      fn scores f  v-         | v > 0 = case Map.lookup f (weights per) of-                     Just vec -> Map.foldlWithKey (doProd v) scores vec-                     Nothing  -> scores-         | otherwise = scores--      doProd :: Int -> Map Class Weight -> Class -> Weight -> Map Class Weight-      doProd value scores label weight =-        Map.alter (updater (weight * (fromIntegral value))) label scores--      updater :: Weight -> Maybe Weight -> Maybe Weight-      updater newVal Nothing  = Just newVal-      updater newVal (Just v) = Just (v + newVal)---- | Update the perceptron with a new example.------ > update(self, truth, guess, features)--- >    ...--- >         self.i += 1--- >         if truth == guess:--- >             return None--- >         for f in features:--- >             weights = self.weights.setdefault(f, {}) -- setdefault is Map.findWithDefault, and destructive.--- >             upd_feat(truth, f, weights.get(truth, 0.0), 1.0)--- >             upd_feat(guess, f, weights.get(guess, 0.0), -1.0)--- >         return None----update :: Perceptron -> Class -> Class -> [Feature] -> Perceptron-update per truth guess features-  | truth == guess = incrementInstances per-  | otherwise      = foldr loopBody per features-    where-      loopBody :: Feature -> Perceptron -> Perceptron-      loopBody f p = let-        fweights  = getFeatureWeight p f-        cweight c = Map.findWithDefault 0 c fweights-        in upd_feat guess f (cweight guess) (-1)-             (upd_feat truth f (cweight truth) 1 p)---- | ported from python:------ > def update(self, truth, guess, features):--- >        '''Update the feature weights.'''--- >        def upd_feat(c, f, w, v):--- >           param = (f, c)--- >           self._totals[param] += (self.i - self._tstamps[param]) * w--- >           self._tstamps[param] = self.i--- >           self.weights[f][c] = w + v-upd_feat :: Class -> Feature -> Weight -> Weight -> Perceptron -> Perceptron-upd_feat c f w v p = let-    newInstances = 1 + (instances p) -- increment the instance counter.--    -- self._totals[param] += (self.i - self._tstamps[param]) * w-    paramTstamp = newInstances - getTimestamp p (f, c)-    tmpTotal  = (getTotal p (f, c)) + ((fromIntegral paramTstamp) * w)-    newTotals = Map.insert (f, c) tmpTotal (totals p)--    -- self._tstamps[param] = self.i-    newTstamps = Map.insert (f, c) newInstances (tstamps p)--    -- self.weights[f][c] = w + v-    newWeights = Map.insert f (Map.insert c (w + v) (getFeatureWeight p f)) (weights p)--    in p { totals = newTotals-         , tstamps = newTstamps-         , weights = newWeights }----- | Average the weights------ Ported from Python:------ > def average_weights(self):--- >     for feat, weights in self.weights.items():--- >         new_feat_weights = {}--- >         for clas, weight in weights.items():--- >             param = (feat, clas)--- >             total = self._totals[param]--- >             total += (self.i - self._tstamps[param]) * weight--- >             averaged = round(total / float(self.i), 3)--- >             if averaged:--- >                 new_feat_weights[clas] = averaged--- >         self.weights[feat] = new_feat_weights--- >     return None----averageWeights :: Perceptron -> Perceptron-averageWeights per | (instances per) == 0 = per-                   | otherwise = per { weights = Map.mapWithKey avgWeights $ weights per-                                     , tstamps = Map.mapWithKey (\_ _->0) (tstamps per)-                                     , instances = 0 }-  where-    avgWeights :: Feature -> Map Class Weight -> Map Class Weight-    avgWeights feat ws = Map.foldlWithKey (doAvg feat) Map.empty ws--    doAvg :: Feature -> Map Class Weight -> Class -> Weight -> Map Class Weight-    doAvg f acc c w = let-      param = (f, c)-      paramTotal = instances per - getTimestamp per param--      total :: Weight-      total = (getTotal per param) + ((fromIntegral paramTotal) * w)-      averaged = roundTo 3 (total / (fromIntegral $ instances per))-      in if 0 == averaged-           then acc-           else Map.insert c averaged acc---- | round a fractional number to a specified decimal place.------ >>> roundTo 2 3.1459--- 3.15----roundTo :: RealFrac a => Int -> a -> a-roundTo n f = (fromInteger $ round $ f * (10^n)) / (10.0^^n)----- Train a perceptron------ Ported from Python:--- > def train(nr_iter, examples):--- >     model = Perceptron()--- >     for i in range(nr_iter):--- >         random.shuffle(examples)--- >         for features, class_ in examples:--- >             scores = model.predict(features)--- >             guess, score = max(scores.items(), key=lambda i: i[1])--- >             if guess != class_:--- >                 model.update(class_, guess, features)--- >     model.average_weights()--- >     return model-train :: Int -> Perceptron -> [(Map Feature Int, Class)] -> IO Perceptron-train itr model exs = do-  trainingSet <- shuffleM $ concat $ take itr $ repeat exs-  return $ averageWeights $ foldl' trainEx model trainingSet--trainEx :: Perceptron -> (Map Feature Int, Class) -> Perceptron-trainEx model (feats, truth) = let-  guess = fromMaybe (Class "Unk") $ predict model feats-  in update model truth guess $ Map.keys feats
src/NLP/POS/AvgPerceptronTagger.hs view
@@ -20,9 +20,9 @@ where  import NLP.Corpora.Parsing (readPOSWith)-import NLP.POS.AvgPerceptron ( Perceptron, Feature(..)-                             , Class(..), predict, update-                             , emptyPerceptron, averageWeights)+import NLP.ML.AvgPerceptron ( Perceptron, Feature(..)+                            , Class(..), predict, update+                            , emptyPerceptron, averageWeights) import NLP.Types  import Control.Monad (foldM)
src/NLP/Types/Tags.hs view
@@ -46,6 +46,8 @@   tagTerm :: a -> Text   startTag :: a   endTag :: a+  -- | Check if a tag is a determiner tag.+  isDt :: a -> Bool  -- | A fall-back 'ChunkTag' instance, analogous to 'RawTag' newtype RawChunk = RawChunk Text@@ -77,6 +79,8 @@    startTag = RawTag "-START-"   endTag = RawTag "-END-"++  isDt (RawTag tg) = tg == "DT"  instance Arbitrary RawTag where   arbitrary = do
src/NLP/Types/Tree.hs view
@@ -46,6 +46,16 @@ data Chunk chunk tag = Chunk chunk [ChunkOr chunk tag]   deriving (Read, Show, Eq) +showChunkedSent :: (ChunkTag c, Tag t) => ChunkedSentence c t -> Text+showChunkedSent (ChunkedSent cs) = T.intercalate " " (map showChunkOr cs)+  where+    showChunkOr (POS_CN    pos)         = printPOS pos+    showChunkOr (Chunk_CN (Chunk chunk cors)) =+      let front = T.concat ["[", fromChunk chunk]+          back = "]"+          bits = map showChunkOr cors+      in T.append (T.intercalate " " (front:bits)) back+ instance (ChunkTag c, Arbitrary c, Arbitrary t, Tag t) =>   Arbitrary (ChunkedSentence c t) where   arbitrary = ChunkedSent <$> arbitrary
tests/src/AvgPerceptronTests.hs view
@@ -11,7 +11,7 @@ import Data.Map (Map) import qualified Data.Map as Map -import NLP.POS.AvgPerceptron+import NLP.ML.AvgPerceptron  tests :: Test tests = testGroup "AvgPerceptron"
tests/src/Main.hs view
@@ -31,6 +31,7 @@ import qualified NLP.Similarity.VectorSimTests as Vec import qualified NLP.TypesTests as TypeTests import qualified NLP.Types.IOBTests as IOB+import qualified NLP.Types.TreeTests as Tree import qualified NLP.Chunk.AvgPerceptronChunkerTests as APC  import Corpora@@ -69,6 +70,7 @@         , Conll.tests         , IOB.tests         , APC.tests+        , Tree.tests         ]