nerf 0.3.0 → 0.4.0
raw patch · 22 files changed
+980/−987 lines, 22 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ NLP.Nerf.Tokenize: word :: Word a => a -> String
- NLP.Nerf: ner :: Nerf -> [Word] -> NeForest NE Word
+ NLP.Nerf: ner :: Nerf -> String -> NeForest NE Word
- NLP.Nerf.Tokenize: moveNEs :: Word b => NeForest a b -> [b] -> NeForest a b
+ NLP.Nerf.Tokenize: moveNEs :: (Word b, Word c) => NeForest a b -> [c] -> NeForest a c
Files
- NLP/Nerf.hs +0/−96
- NLP/Nerf/Dict.hs +0/−79
- NLP/Nerf/Dict/Base.hs +0/−116
- NLP/Nerf/Dict/NELexicon.hs +0/−24
- NLP/Nerf/Dict/PNEG.hs +0/−44
- NLP/Nerf/Dict/PNET.hs +0/−53
- NLP/Nerf/Dict/Prolexbase.hs +0/−25
- NLP/Nerf/Schema.hs +0/−360
- NLP/Nerf/Tokenize.hs +0/−151
- NLP/Nerf/Types.hs +0/−25
- nerf.cabal +4/−2
- src/NLP/Nerf.hs +97/−0
- src/NLP/Nerf/Dict.hs +79/−0
- src/NLP/Nerf/Dict/Base.hs +116/−0
- src/NLP/Nerf/Dict/NELexicon.hs +24/−0
- src/NLP/Nerf/Dict/PNEG.hs +44/−0
- src/NLP/Nerf/Dict/PNET.hs +53/−0
- src/NLP/Nerf/Dict/Prolexbase.hs +25/−0
- src/NLP/Nerf/Schema.hs +360/−0
- src/NLP/Nerf/Tokenize.hs +150/−0
- src/NLP/Nerf/Types.hs +25/−0
- tools/nerf.hs +3/−12
− NLP/Nerf.hs
@@ -1,96 +0,0 @@-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE OverloadedStrings #-}---- | Main module of the Nerf tool.--module NLP.Nerf-( Nerf (..)-, train-, ner-, tryOx-, module NLP.Nerf.Types-) where--import Control.Applicative ((<$>), (<*>))-import Data.Binary (Binary, put, get)-import Data.Foldable (foldMap)-import Data.List (intercalate)-import qualified Data.Text as T-import qualified Data.Text.Lazy.IO as L--import Text.Named.Enamex (parseEnamex)-import qualified Data.Named.Tree as N-import qualified Data.Named.IOB as IOB--import Numeric.SGD (SgdArgs)-import qualified Data.CRF.Chain1 as CRF--import NLP.Nerf.Types-import NLP.Nerf.Tokenize (tokenize, moveNEs)-import NLP.Nerf.Schema (SchemaConf, Schema, fromConf, schematize)---- | A Nerf consists of the observation schema configuration and the CRF model.-data Nerf = Nerf- { schemaConf :: SchemaConf- , crf :: CRF.CRF Ob Lb }--instance Binary Nerf where- put Nerf{..} = put schemaConf >> put crf- get = Nerf <$> get <*> get--flatten :: Schema a -> N.NeForest NE Word -> CRF.SentL Ob Lb-flatten schema forest =- [ CRF.annotate x y- | (x, y) <- zip xs ys ]- where- iob = IOB.encodeForest forest- xs = schematize schema (map IOB.word iob)- ys = map IOB.label iob---- | Tokenize sentence with the Nerf tokenizer.-reTokenize :: N.NeForest NE Word -> N.NeForest NE Word-reTokenize ft = - moveNEs ft ((doTok . leaves) ft)- where - doTok = map T.pack . tokenize . intercalate " " . map T.unpack- leaves = concatMap $ foldMap (either (const []) (:[]))--readDeep :: FilePath -> IO [N.NeForest NE Word]-readDeep path = map reTokenize . parseEnamex <$> L.readFile path--readFlat :: Schema a -> FilePath -> IO [CRF.SentL Ob Lb]-readFlat schema path = map (flatten schema) <$> readDeep path--drawSent :: CRF.SentL Ob Lb -> IO ()-drawSent sent = do- let unDist (x, y) = (x, CRF.unDist y)- mapM_ (print . unDist) sent- putStrLn "" ---- | Show results of observation extraction on the input ENAMEX file.-tryOx :: SchemaConf -> FilePath -> IO ()-tryOx cfg path = do- input <- readFlat (fromConf cfg) path- mapM_ drawSent input---- | Train Nerf on the input data using the SGD method.-train- :: SgdArgs -- ^ Args for SGD- -> SchemaConf -- ^ Observation schema configuration- -> FilePath -- ^ Train data (ENAMEX)- -> Maybe FilePath -- ^ Maybe eval data (ENAMEX)- -> IO Nerf -- ^ Nerf with resulting codec and model-train sgdArgs cfg trainPath evalPathM = do- let schema = fromConf cfg- readTrain = readFlat schema trainPath- readEvalM = evalPathM >>= \evalPath ->- Just ([], readFlat schema evalPath)- _crf <- CRF.train sgdArgs readTrain readEvalM CRF.presentFeats- return $ Nerf cfg _crf---- | Perform named entity recognition (NER) using the Nerf.-ner :: Nerf -> [Word] -> N.NeForest NE Word-ner nerf ws =- let schema = fromConf (schemaConf nerf)- xs = CRF.tag (crf nerf) (schematize schema ws)- in IOB.decodeForest [IOB.IOB w x | (w, x) <- zip ws xs]
− NLP/Nerf/Dict.hs
@@ -1,79 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}---- | Extraction utilities for various dictionary resources.--module NLP.Nerf.Dict-( extractPoliMorf-, extractPNEG-, extractNELexicon-, extractProlexbase-, extractIntTriggers-, extractExtTriggers-, module NLP.Nerf.Dict.Base-) where--import Control.Applicative ((<$>), (<*>))-import qualified Data.PoliMorf as Poli--import NLP.Nerf.Dict.Base-import NLP.Nerf.Dict.PNEG (readPNEG)-import NLP.Nerf.Dict.NELexicon (readNELexicon)-import NLP.Nerf.Dict.Prolexbase (readProlexbase)-import qualified NLP.Nerf.Dict.PNET as PNET---- | Is it a single word entry?-atomic :: Entry -> Bool-atomic = not . isMultiWord . neOrth---- | Extract NEs dictionary from PNEG.-extractPNEG- :: FilePath -- ^ Path to PNEG in the LMF format- -> IO Dict-extractPNEG lmfPath =- fromEntries . filter atomic <$> readPNEG lmfPath---- | Extract NEs dictionary from NELexicon.-extractNELexicon- :: FilePath -- ^ Path to NELexicon- -> IO Dict-extractNELexicon nePath =- fromEntries . filter atomic <$> readNELexicon nePath---- | Extract NEs dictionary from PoliMorf.-extractPoliMorf- :: FilePath -- ^ Path to PoliMorf- -> IO Dict-extractPoliMorf poliPath- = fromPairs . filter (cond . snd)- . map ((,) <$> Poli.form <*> Poli.cat)- <$> Poli.readPoliMorf poliPath- where- cond x = x /= "pospolita" && x /= ""---- | Extract NEs dictionary from Prolexbase.-extractProlexbase- :: FilePath -- ^ Path to Prolexbase- -> IO Dict-extractProlexbase proPath = do- fromEntries . filter atomic <$> readProlexbase proPath---- | Extract internal triggers from PNET dictionary.-extractIntTriggers- :: FilePath -- ^ Path to PNET- -> IO Dict-extractIntTriggers pnetPath =- mkTriggers PNET.Internal <$> PNET.readPNET pnetPath---- | Extract external triggers from PNET dictionary.-extractExtTriggers- :: FilePath -- ^ Path to PNET- -> IO Dict-extractExtTriggers pnetPath =- mkTriggers PNET.External <$> PNET.readPNET pnetPath--mkTriggers :: PNET.Typ -> [PNET.Entry] -> Dict-mkTriggers typ- = fromPairs- . filter (not . isMultiWord . fst) - . map ((,) <$> PNET.orth <*> PNET.neTyp)- . filter (PNET.hasTyp typ)
− NLP/Nerf/Dict/Base.hs
@@ -1,116 +0,0 @@-{-# LANGUAGE TupleSections #-}---- | Basic types for dictionary handling. --module NLP.Nerf.Dict.Base-(--- * Lexicon entry- NeType-, Form-, isMultiWord-, Entry (..)---- * Dictionary-, Label-, DAWG-, Dict-, fromPairs-, fromEntries-, siftDict-, saveDict-, loadDict---- * Merging dictionaries-, merge-, diff-) where--import Control.Applicative ((<$>), (<*>))-import Data.Binary (encodeFile, decodeFile)-import Data.Text.Binary ()-import qualified Data.Text as T-import qualified Data.Set as S-import qualified Data.DAWG.Static as D-import qualified Data.DAWG.Trans.Vector as D---- | A type of named entity.-type NeType = T.Text---- | A orthographic form.-type Form = T.Text---- | Is the form a multiword one?-isMultiWord :: Form -> Bool-isMultiWord = (>1) . length . T.words---- | A Named Entity entry from the LMF dictionary.-data Entry = Entry- { neOrth :: !Form -- ^ Orthographic form of the NE- , neType :: !NeType -- ^ Type of the NE- } deriving (Show, Read, Eq, Ord)---- | Dictionary label.-type Label = T.Text---- | A 'Dict' is a map from forms to labels. Each form may be annotated--- with multiple labels. The map is represented using the directed acyclic--- word graph.--- type Dict = D.DAWG (S.Set Label)-type DAWG = D.DAWG D.Trans Char ()-type Dict = DAWG (S.Set Label)---- | Construct dictionary from the list of form/label pairs.-fromPairs :: [(Form, Label)] -> Dict-fromPairs xs = D.fromListWith S.union- [ ( T.unpack x- , S.singleton y)- | (x, y) <- xs ]---- | Construct dictionary from the list of entries.-fromEntries :: [Entry] -> Dict-fromEntries = fromPairs . map ((,) <$> neOrth <*> neType)---- | Remove dictionary entries which do not satisfy the predicate.-siftDict :: (Form -> S.Set Label -> Bool) -> Dict -> Dict-siftDict f dict = D.fromList [(k, v) | (k, v) <- D.assocs dict, f (T.pack k) v]---- | Save the dictionary in the file.-saveDict :: FilePath -> Dict -> IO ()-saveDict = encodeFile---- | Load the dictionary from the file.-loadDict :: FilePath -> IO Dict-loadDict = decodeFile---- | Merge dictionary resources.-merge :: [Dict] -> Dict-merge = unionsWith S.union---- | Replacement implementation of unionsWith while there is--- no such function in dawg library. -unionsWith :: Ord a => (a -> a -> a) -> [DAWG a] -> DAWG a-unionsWith f = foldl (unionWith f) D.empty---- | Replacement implementation of unionWith while there is--- no such function in dawg library. -unionWith :: Ord a => (a -> a -> a) -> DAWG a -> DAWG a -> DAWG a-unionWith f d d' = D.fromListWith f (D.assocs d ++ D.assocs d')---- | Differentiate labels from separate dictionaries using--- dictionary-unique prefixes.-diff :: [Dict] -> [Dict]-diff ds =- [ mapS (addPrefix i) `mapD` dict- | (i, dict) <- zip [0..] ds ]---- | Map function over the DAWG elements.-mapD :: Ord b => (a -> b) -> DAWG a -> DAWG b-mapD f d = D.fromList [(x, f y) | (x, y) <- D.assocs d]---- | Map function over the set.-mapS :: Ord b => (a -> b) -> S.Set a -> S.Set b-mapS f s = S.fromList [f x | x <- S.toList s]---- | Add integer prefix.-addPrefix :: Int -> T.Text -> T.Text-addPrefix = T.append . T.pack . show
− NLP/Nerf/Dict/NELexicon.hs
@@ -1,24 +0,0 @@--- | Handling the NELexicon dictionary.--module NLP.Nerf.Dict.NELexicon-( parseNELexicon-, readNELexicon-) where--import qualified Data.Text.Lazy as L-import qualified Data.Text.Lazy.IO as L--import NLP.Nerf.Dict.Base---- | Parse the NELexicon into a list of entries.-parseNELexicon :: L.Text -> [Entry]-parseNELexicon = map parseLine . L.lines--parseLine :: L.Text -> Entry-parseLine line = case L.break (==';') line of- (_type, _form) -> Entry (L.toStrict $ L.drop 2 _form) (L.toStrict _type)- _ -> error $ "parseLine: invalid line \"" ++ L.unpack line ++ "\""---- | Read the dictionary from the file.-readNELexicon :: FilePath -> IO [Entry]-readNELexicon = fmap parseNELexicon . L.readFile
− NLP/Nerf/Dict/PNEG.hs
@@ -1,44 +0,0 @@-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE OverloadedStrings #-}---- | Parsing the Gazetteer for Polish Named Entities (used formerly within--- the SProUT platform) in the LMF format.--module NLP.Nerf.Dict.PNEG-( parsePNEG-, readPNEG-) where--import Text.XML.PolySoup-import qualified Data.Text as T-import qualified Data.Text.Lazy as L-import qualified Data.Text.Lazy.IO as L--import NLP.Nerf.Dict.Base--lmfP :: XmlParser L.Text [Entry]-lmfP = true ##> lexEntryP--lexEntryP :: XmlParser L.Text [Entry]-lexEntryP = tag "LexicalEntry" `joinR` do- many_ $ cut $ tag "feat"- _words <- many wordP- sense <- senseP- return [Entry x sense | x <- _words]--wordP :: XmlParser L.Text Form-wordP = head <$> (tag "Lemma" <|> tag "WordForm" /> featP "writtenForm")--senseP :: XmlParser L.Text NeType-senseP = head <$> (tag "Sense" //> featP "externalReference" <|> featP "label")--featP :: L.Text -> XmlParser L.Text T.Text-featP x = L.toStrict <$> cut (tag "feat" *> hasAttr "att" x *> getAttr "val")---- | Parse the dictionary to the list of entries.-parsePNEG :: L.Text -> [Entry]-parsePNEG = parseXml lmfP---- | Read the dictionary from the file.-readPNEG :: FilePath -> IO [Entry]-readPNEG = fmap parsePNEG . L.readFile
− NLP/Nerf/Dict/PNET.hs
@@ -1,53 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}---- | Polish Named Entity Triggers <http://zil.ipipan.waw.pl/PNET> dictionary.--module NLP.Nerf.Dict.PNET-( parsePNET-, readPNET-, Typ (..)-, hasTyp-, Entry (..)-) where--import qualified Data.Text as T-import qualified Data.Text.Lazy as L-import qualified Data.Text.Lazy.IO as L---- | Trigger type.-data Typ- = Internal- | External- deriving (Show, Eq, Ord)--readTyp :: T.Text -> Typ-readTyp "int" = Internal-readTyp "ext" = External-readTyp x = error $ "readTyp: typ " ++ T.unpack x ++ " unknown"---- | PNET entry.-data Entry = Entry- { orth :: T.Text- , base :: T.Text- , tag :: T.Text- , typ :: Typ- , neTyp :: T.Text- , example :: T.Text }---- | Does entry represents a trigger of the given type?-hasTyp :: Typ -> Entry -> Bool-hasTyp x = (==x) . typ--parseLine :: L.Text -> Entry-parseLine line = case map L.toStrict (L.split (=='\t') line) of- [_orth, _base, _tag, _typ, _neTyp, _example] ->- Entry _orth _base _tag (readTyp _typ) _neTyp _example- _ -> error $ "parseLine: invalid row \"" ++ L.unpack line ++ "\""---- | Parse dictionary into a list of entries.-parsePNET :: L.Text -> [Entry]-parsePNET = map parseLine . L.lines---- | Read dictionary from the file.-readPNET :: FilePath -> IO [Entry]-readPNET = fmap parsePNET . L.readFile
− NLP/Nerf/Dict/Prolexbase.hs
@@ -1,25 +0,0 @@--- | Handling Prolexbase dictionaries, both with the--- same storage format.--module NLP.Nerf.Dict.Prolexbase-( parseProlexbase-, readProlexbase-) where--import qualified Data.Text.Lazy as L-import qualified Data.Text.Lazy.IO as L--import NLP.Nerf.Dict.Base---- | Parse dictionary into a list of entries.-parseProlexbase :: L.Text -> [Entry]-parseProlexbase = map parseLine . L.lines--parseLine :: L.Text -> Entry-parseLine row = case map L.toStrict (L.split (=='\t') row) of- [_form, _base, _tag, _cat] -> Entry _form _cat- _ -> error $ "parseLine: invalid row \"" ++ L.unpack row ++ "\""---- | Read the dictionary from the file.-readProlexbase :: FilePath -> IO [Entry]-readProlexbase = fmap parseProlexbase . L.readFile
− NLP/Nerf/Schema.hs
@@ -1,360 +0,0 @@-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE OverloadedStrings #-}---- | Observation schema blocks for Nerf.--module NLP.Nerf.Schema-( --- * Types- Ox-, Schema-, void-, sequenceS_---- * Usage-, schematize---- ** Configuration-, Body (..)-, Entry-, entry-, entryWith-, SchemaConf (..)-, nullConf-, defaultConf-, fromConf---- ** Schema blocks-, Block-, fromBlock-, orthB-, splitOrthB-, lowPrefixesB-, lowSuffixesB-, lemmaB-, shapeB-, packedB-, shapePairB-, packedPairB-, dictB-) where--import Control.Applicative ((<$>), (<*>))-import Control.Monad (forM_, join)-import Data.Maybe (maybeToList)-import Data.Binary (Binary, put, get)-import qualified Data.Char as C-import qualified Data.Set as S-import qualified Data.Vector as V-import qualified Data.Text as T-import qualified Data.DAWG.Static as D--import qualified Data.CRF.Chain1 as CRF-import qualified Control.Monad.Ox as Ox-import qualified Control.Monad.Ox.Text as Ox--import NLP.Nerf.Types-import NLP.Nerf.Dict (Dict)---- | The Ox monad specialized to word token type and text observations.-type Ox a = Ox.Ox Word T.Text a---- | A schema is a block of the Ox computation performed within the--- context of the sentence and the absolute sentence position.-type Schema a = V.Vector Word -> Int -> Ox a---- | A dummy schema block.-void :: a -> Schema a-void x _ _ = return x---- | Sequence the list of schemas (or blocks) and discard individual values.-sequenceS_ :: [V.Vector Word -> a -> Ox b] -> V.Vector Word -> a -> Ox ()-sequenceS_ xs sent =- let ys = map ($sent) xs- in \k -> sequence_ (map ($k) ys)---- | Record structure of the basic observation types.-data BaseOb = BaseOb- { orth :: Int -> Maybe T.Text- , lowOrth :: Int -> Maybe T.Text }---- | Construct the 'BaseOb' structure given the sentence.-mkBaseOb :: V.Vector Word -> BaseOb-mkBaseOb sent = BaseOb- { orth = _orth- , lowOrth = _lowOrth }- where- at = Ox.atWith sent- _orth = (id `at`)- _lowOrth i = T.toLower <$> _orth i---- | A block is a chunk of the Ox computation performed within the--- context of the sentence and the list of absolute sentence positions.-type Block a = V.Vector Word -> [Int] -> Ox a---- | Transform the block to the schema depending on the list of--- relative sentence positions.-fromBlock :: Block a -> [Int] -> Schema a-fromBlock blk xs sent =- let blkSent = blk sent- in \k -> blkSent [x + k | x <- xs]---- | Orthographic form at the current position.-orthB :: Block ()-orthB sent = \ks ->- let orthOb = Ox.atWith sent id- in mapM_ (Ox.save . orthOb) ks---- | Orthographic form split into two observations: the lowercased form and--- the original form (only when different than the lowercased one).-splitOrthB :: Block ()-splitOrthB sent = \ks -> do- mapM_ (Ox.save . lowOrth) ks- mapM_ (Ox.save . upOnlyOrth) ks- where- BaseOb{..} = mkBaseOb sent- upOnlyOrth i = orth i >>= \x -> case T.any C.isUpper x of- True -> Just x- False -> Nothing---- | List of lowercased prefixes of given lengths.-lowPrefixesB :: [Int] -> Block ()-lowPrefixesB ns sent = \ks ->- forM_ ks $ \i ->- mapM_ (Ox.save . lowPrefix i) ns- where- BaseOb{..} = mkBaseOb sent- lowPrefix i j = Ox.prefix j =<< lowOrth i---- | List of lowercased suffixes of given lengths.-lowSuffixesB :: [Int] -> Block ()-lowSuffixesB ns sent = \ks ->- forM_ ks $ \i ->- mapM_ (Ox.save . lowSuffix i) ns- where- BaseOb{..} = mkBaseOb sent- lowSuffix i j = Ox.suffix j =<< lowOrth i---- | Lemma substitute parametrized by the number specifying the span--- over which lowercased prefixes and suffixes will be 'Ox.save'd.--- For example, @lemmaB 2@ will take affixes of lengths @0, -1@ and @-2@--- on account.-lemmaB :: Int -> Block ()-lemmaB n sent = \ks -> do- mapM_ lowLemma ks- where- BaseOb{..} = mkBaseOb sent- lowPrefix i j = Ox.prefix j =<< lowOrth i- lowSuffix i j = Ox.suffix j =<< lowOrth i- lowLemma i = Ox.group $ do- mapM_ (Ox.save . lowPrefix i) [0, -1 .. -n]- mapM_ (Ox.save . lowSuffix i) [0, -1 .. -n]---- | Shape of the word.-shapeB :: Block ()-shapeB sent = \ks -> do- mapM_ (Ox.save . shape) ks- where- BaseOb{..} = mkBaseOb sent- shape i = Ox.shape <$> orth i---- | Packed shape of the word.-packedB :: Block ()-packedB sent = \ks -> do- mapM_ (Ox.save . shapeP) ks- where- BaseOb{..} = mkBaseOb sent- shape i = Ox.shape <$> orth i- shapeP i = Ox.pack <$> shape i---- -- | Shape and packed shape of the word.--- shapeAndPackedB :: Block ()--- shapeAndPackedB sent = \ks -> do--- mapM_ (Ox.save . shape) ks--- mapM_ (Ox.save . shapeP) ks--- where--- BaseOb{..} = mkBaseOb sent--- shape i = Ox.shape <$> orth i--- shapeP i = Ox.pack <$> shape i---- | Combined shapes of two consecutive (at @k-1@ and @k@ positions) words.-shapePairB :: Block ()-shapePairB sent = \ks ->- forM_ ks $ \i -> do- Ox.save $ link <$> shape i <*> shape (i - 1)- where- BaseOb{..} = mkBaseOb sent- shape i = Ox.shape <$> orth i- link x y = T.concat [x, "-", y]---- | Combined packed shapes of two consecutive (at @k-1@ and @k@ positions)--- words.-packedPairB :: Block ()-packedPairB sent = \ks ->- forM_ ks $ \i -> do- Ox.save $ link <$> shapeP i <*> shapeP (i - 1)- where- BaseOb{..} = mkBaseOb sent- shape i = Ox.shape <$> orth i- shapeP i = Ox.pack <$> shape i- link x y = T.concat [x, "-", y]---- | Plain dictionary search determined with respect to the list of--- relative positions.-dictB :: Dict -> Block ()-dictB dict sent = \ks -> do- mapM_ (Ox.saves . searchDict) ks- where- BaseOb{..} = mkBaseOb sent- searchDict i = join . maybeToList $- S.toList <$> (orth i >>= flip D.lookup dict . T.unpack)---- | Body of configuration entry.-data Body a = Body {- -- | Range argument for the schema block. - range :: [Int]- -- | Additional arguments for the schema block.- , args :: a }- deriving (Show)--instance Binary a => Binary (Body a) where- put Body{..} = put range >> put args- get = Body <$> get <*> get---- | Maybe entry.-type Entry a = Maybe (Body a)---- | Entry with additional arguemnts.-entryWith :: a -> [Int] -> Entry a-entryWith v xs = Just (Body xs v)---- | Maybe entry with additional arguemnts.-entryWith'Mb :: Maybe a -> [Int] -> Entry a-entryWith'Mb (Just v) xs = Just (Body xs v)-entryWith'Mb Nothing _ = Nothing---- | Plain entry with no additional arugments.-entry :: [Int] -> Entry ()-entry = entryWith ()---- | Configuration of the schema. All configuration elements specify the--- range over which a particular observation type should be taken on account.--- For example, the @[-1, 0, 2]@ range means that observations of particular--- type will be extracted with respect to previous (@k - 1@), current (@k@)--- and after the next (@k + 2@) positions when identifying the observation--- set for position @k@ in the input sentence.-data SchemaConf = SchemaConf {- -- | The 'orthB' schema block.- orthC :: Entry ()- -- | The 'splitOrthB' schema block.- , splitOrthC :: Entry ()- -- | The 'lowPrefixesB' schema block. The first list of ints- -- represents lengths of prefixes.- , lowPrefixesC :: Entry [Int]- -- | The 'lowSuffixesB' schema block. The first list of ints- -- represents lengths of suffixes.- , lowSuffixesC :: Entry [Int]- -- | The 'lemmaB' schema block.- , lemmaC :: Entry Int- -- | The 'shapeB' schema block.- , shapeC :: Entry ()- -- | The 'packedB' schema block.- , packedC :: Entry ()- -- | The 'shapePairB' schema block.- , shapePairC :: Entry ()- -- | The 'packedPairB' schema block.- , packedPairC :: Entry ()- -- | Dictionaries of NEs ('dictB' schema block).- , dictC :: Entry [Dict]- -- | Dictionary of internal triggers.- , intTrigsC :: Entry Dict- -- | Dictionary of external triggers.- , extTrigsC :: Entry Dict- } deriving (Show)--instance Binary SchemaConf where- put SchemaConf{..} = do- put orthC- put splitOrthC- put lowPrefixesC- put lowSuffixesC- put lemmaC- put shapeC- put packedC- put shapePairC- put packedPairC- put dictC- put intTrigsC- put extTrigsC- get = SchemaConf- <$> get <*> get <*> get <*> get- <*> get <*> get <*> get <*> get- <*> get <*> get <*> get <*> get---- | Null configuration of the observation schema.-nullConf :: SchemaConf-nullConf = SchemaConf- Nothing Nothing Nothing Nothing- Nothing Nothing Nothing Nothing- Nothing Nothing Nothing Nothing---- | Default configuration of the observation schema.-defaultConf- :: [Dict] -- ^ Named Entity dictionaries- -> Maybe Dict -- ^ Dictionary of internal triggers- -> Maybe Dict -- ^ Dictionary of external triggers- -> IO SchemaConf-defaultConf neDicts intDict extDict = do- return $ SchemaConf- { orthC = Nothing- , splitOrthC = entry [-1, 0]- , lowPrefixesC = Nothing- , lowSuffixesC = entryWith [2, 3, 4] [0]- , lemmaC = entryWith 3 [-1, 0]- , shapeC = entry [-1, 0]- , packedC = entry [-1, 0]- , shapePairC = entry [0]- , packedPairC = entry [0]- , dictC = entryWith neDicts [-1, 0]- , intTrigsC = entryWith'Mb intDict [0]- , extTrigsC = entryWith'Mb extDict [-1] }--mkArg0 :: Block () -> Entry () -> Schema ()-mkArg0 blk (Just x) = fromBlock blk (range x)-mkArg0 _ Nothing = void ()--mkArg1 :: (a -> Block ()) -> Entry a -> Schema ()-mkArg1 blk (Just x) = fromBlock (blk (args x)) (range x)-mkArg1 _ Nothing = void ()--mkArgs1 :: (a -> Block ()) -> Entry [a] -> Schema ()-mkArgs1 blk (Just x) = sequenceS_- [ fromBlock- (blk dict)- (range x)- | dict <- args x ]-mkArgs1 _ Nothing = void ()---- | Build the schema based on the configuration.-fromConf :: SchemaConf -> Schema ()-fromConf SchemaConf{..} = sequenceS_- [ mkArg0 orthB orthC- , mkArg0 splitOrthB splitOrthC- , mkArg1 lowPrefixesB lowPrefixesC- , mkArg1 lowSuffixesB lowSuffixesC- , mkArg1 lemmaB lemmaC- , mkArg0 shapeB shapeC- , mkArg0 packedB packedC- , mkArg0 shapePairB shapePairC- , mkArg0 packedPairB packedPairC- , mkArgs1 dictB dictC- , mkArg1 dictB intTrigsC- , mkArg1 dictB extTrigsC ]---- | Use the schema to extract observations from the sentence.-schematize :: Schema a -> [Word] -> CRF.Sent Ob-schematize schema xs =- map (S.fromList . Ox.execOx . schema v) [0 .. n - 1]- where- v = V.fromList xs- n = V.length v
− NLP/Nerf/Tokenize.hs
@@ -1,151 +0,0 @@-{-# LANGUAGE TypeSynonymInstances #-}-{-# LANGUAGE FlexibleInstances #-}---- | The module implements the tokenization used within Nerf--- and some other tokenization-related stuff.--module NLP.Nerf.Tokenize-(--- * Tokenization- tokenize--- * Synchronization-, Word-, moveNEs-) where--import Control.Monad ((>=>))-import Data.Foldable (foldMap)-import qualified Data.Char as Char-import qualified Data.List as L-import qualified Data.Tree as T-import qualified Data.Traversable as Tr-import qualified Data.Text as Text-import qualified Data.Text.Lazy as LazyText-import qualified NLP.Tokenize as Tok--import Data.Named.Tree (NeForest, NeTree, groupForestLeaves)-------------------------------- Tokenization definition.-------------------------------- | Default tokenizator.-defaultTokenizer :: Tok.Tokenizer-defaultTokenizer- = Tok.whitespace- >=> Tok.uris- >=> Tok.punctuation---- | Tokenize sentence using the default tokenizer.-tokenize :: String -> [String]-tokenize = Tok.run defaultTokenizer-------------------------------------------------------------------- Synchronizing named entities with new sentence tokenization.-------------------------------------------------------------------- | A class of objects with size.-class Word a where- size :: a -> Int- rmSpaces :: a -> a--instance Word String where- size = length- rmSpaces = filter (not . Char.isSpace)--instance Word Text.Text where- size = Text.length- rmSpaces = Text.filter (not . Char.isSpace)--instance Word LazyText.Text where- size = fromInteger . toInteger . LazyText.length- rmSpaces = LazyText.filter (not . Char.isSpace)--essence :: Word a => a -> Int-essence = size . rmSpaces-{-# INLINE essence #-}---- | Syncronization between two sentences. Each (xs, ys) pair represents--- tokens from the two input sentences which corresponds to each other.-type Sync a = [([a], [a])]---- | Synchronize two tokenizations of the sentence.-sync :: Word a => [a] -> [a] -> Sync a-sync = sync' 0--sync' :: Word a => Int -> [a] -> [a] -> Sync a-sync' r (x:xs) (y:ys)- | n + r == m = ([x], [y]) : sync' 0 xs ys- | n + r < m = join x $ sync' (n + r) xs (y:ys)- | otherwise = swap . join y $ sync' (m - r) ys (x:xs)- where- n = essence x- m = essence y- join l ((ls, rs) : ps) = (l:ls, rs) : ps- join _ [] = error "sync'.join: bad arguments"- swap ((ls, rs) : ps) = (rs, ls) : swap ps- swap [] = []-sync' 0 [] [] = []-sync' _ _ _ = error "sync': bad arguments"---- | Match the `Sync` with the given list, return the matching result--- (snd elements of the `Sync` list) and the rest of the `Sync` list.-match :: Word a => [a] -> Sync a -> ([a], Sync a)-match xs ss =- let (sl, sr) = splitAcc isMatch 0 ss- in (concatMap snd sl, sr)- where- n = sum (map essence xs)- isMatch r (ys, _)- | m + r < n = (m + r, False)- | m + r == n = (m + r, True)- | otherwise = error "match.isMatch: no match"- where- m = sum (map essence ys)---- | Split the list with the help of the accumulating function.-splitAcc :: (acc -> a -> (acc, Bool)) -> acc -> [a] -> ([a], [a])-splitAcc _ _ [] = ([], [])-splitAcc f acc (x:xs)- | cond = ([x], xs)- | otherwise = join x (splitAcc f acc' xs)- where- (acc', cond) = f acc x- join y (ys, zs) = (y:ys, zs)---- | List forest leaves.-leaves :: NeForest a b -> [b]-leaves = concatMap $ foldMap (either (const []) (:[]))--unGroupLeaves :: NeForest a [b] -> NeForest a b-unGroupLeaves = concatMap unGroupLeavesT--unGroupLeavesT :: NeTree a [b] -> [NeTree a b]-unGroupLeavesT (T.Node (Left v) xs) =- [T.Node (Left v) (unGroupLeaves xs)]-unGroupLeavesT (T.Node (Right vs) _) =- [T.Node (Right v) [] | v <- vs]--substGroups :: Word b => NeForest a [b] -> Sync b -> NeForest a [b]-substGroups fs ss = snd $ L.mapAccumL substGroupsT ss fs--substGroupsT :: Word b => Sync b -> NeTree a [b] -> (Sync b, NeTree a [b])-substGroupsT =- Tr.mapAccumL f- where- f s (Left v) = (s, Left v)- f s (Right v) =- let (v', s') = match v s- in (s', Right v')---- | Synchronize named entities with tokenization represented--- by the second function argument. Of course, both arguments--- should relate to the same sentence.-moveNEs :: Word b => NeForest a b -> [b] -> NeForest a b-moveNEs ft ys- = unGroupLeaves- $ substGroups- (groupForestLeaves true ft)- (sync (leaves ft) ys)- where- true _ _ = True
− NLP/Nerf/Types.hs
@@ -1,25 +0,0 @@--- | Basic types.--module NLP.Nerf.Types-( Word-, NE-, Ob-, Lb-) where--import qualified Data.Text as T-import qualified Data.Named.IOB as IOB---- | A word.-type Word = T.Text---- | A named entity.-type NE = T.Text---- | An observation consist of an index (of list type) and an actual--- observation value.-type Ob = ([Int], T.Text)---- | A label is created by encoding the named entity forest using the--- IOB method.-type Lb = IOB.Label NE
nerf.cabal view
@@ -1,5 +1,5 @@ name: nerf-version: 0.3.0+version: 0.4.0 synopsis: Nerf, the named entity recognition tool based on linear-chain CRFs description: The package provides the named entity recognition (NER) tool divided into a@@ -26,6 +26,8 @@ build-type: Simple library+ hs-source-dirs: src+ build-depends: base >= 4 && < 5 , containers@@ -62,6 +64,6 @@ location: https://github.com/kawu/nerf.git executable nerf- hs-source-dirs: ., tools+ hs-source-dirs: src, tools main-is: nerf.hs ghc-options: -Wall -O2 -threaded -rtsopts
+ src/NLP/Nerf.hs view
@@ -0,0 +1,97 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE OverloadedStrings #-}++-- | Main module of the Nerf tool.++module NLP.Nerf+( Nerf (..)+, train+, ner+, tryOx+, module NLP.Nerf.Types+) where++import Control.Applicative ((<$>), (<*>))+import Data.Binary (Binary, put, get)+import Data.Foldable (foldMap)+import Data.List (intercalate)+import qualified Data.Text as T+import qualified Data.Text.Lazy.IO as L++import Text.Named.Enamex (parseEnamex)+import qualified Data.Named.Tree as N+import qualified Data.Named.IOB as IOB++import Numeric.SGD (SgdArgs)+import qualified Data.CRF.Chain1 as CRF++import NLP.Nerf.Types+import NLP.Nerf.Tokenize (tokenize, moveNEs)+import NLP.Nerf.Schema (SchemaConf, Schema, fromConf, schematize)++-- | A Nerf consists of the observation schema configuration and the CRF model.+data Nerf = Nerf+ { schemaConf :: SchemaConf+ , crf :: CRF.CRF Ob Lb }++instance Binary Nerf where+ put Nerf{..} = put schemaConf >> put crf+ get = Nerf <$> get <*> get++flatten :: Schema a -> N.NeForest NE Word -> CRF.SentL Ob Lb+flatten schema forest =+ [ CRF.annotate x y+ | (x, y) <- zip xs ys ]+ where+ iob = IOB.encodeForest forest+ xs = schematize schema (map IOB.word iob)+ ys = map IOB.label iob++-- | Tokenize sentence with the Nerf tokenizer.+reTokenize :: N.NeForest NE Word -> N.NeForest NE Word+reTokenize ft = + moveNEs ft ((doTok . leaves) ft)+ where + doTok = map T.pack . tokenize . intercalate " " . map T.unpack+ leaves = concatMap $ foldMap (either (const []) (:[]))++readDeep :: FilePath -> IO [N.NeForest NE Word]+readDeep path = map reTokenize . parseEnamex <$> L.readFile path++readFlat :: Schema a -> FilePath -> IO [CRF.SentL Ob Lb]+readFlat schema path = map (flatten schema) <$> readDeep path++drawSent :: CRF.SentL Ob Lb -> IO ()+drawSent sent = do+ let unDist (x, y) = (x, CRF.unDist y)+ mapM_ (print . unDist) sent+ putStrLn "" ++-- | Show results of observation extraction on the input ENAMEX file.+tryOx :: SchemaConf -> FilePath -> IO ()+tryOx cfg path = do+ input <- readFlat (fromConf cfg) path+ mapM_ drawSent input++-- | Train Nerf on the input data using the SGD method.+train+ :: SgdArgs -- ^ Args for SGD+ -> SchemaConf -- ^ Observation schema configuration+ -> FilePath -- ^ Train data (ENAMEX)+ -> Maybe FilePath -- ^ Maybe eval data (ENAMEX)+ -> IO Nerf -- ^ Nerf with resulting codec and model+train sgdArgs cfg trainPath evalPathM = do+ let schema = fromConf cfg+ readTrain = readFlat schema trainPath+ readEvalM = evalPathM >>= \evalPath ->+ Just ([], readFlat schema evalPath)+ _crf <- CRF.train sgdArgs readTrain readEvalM CRF.presentFeats+ return $ Nerf cfg _crf++-- | Perform named entity recognition (NER) using the Nerf.+ner :: Nerf -> String -> N.NeForest NE Word+ner nerf sent =+ let ws = map T.pack . tokenize $ sent+ schema = fromConf (schemaConf nerf)+ xs = CRF.tag (crf nerf) (schematize schema ws)+ in IOB.decodeForest [IOB.IOB w x | (w, x) <- zip ws xs]
+ src/NLP/Nerf/Dict.hs view
@@ -0,0 +1,79 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Extraction utilities for various dictionary resources.++module NLP.Nerf.Dict+( extractPoliMorf+, extractPNEG+, extractNELexicon+, extractProlexbase+, extractIntTriggers+, extractExtTriggers+, module NLP.Nerf.Dict.Base+) where++import Control.Applicative ((<$>), (<*>))+import qualified Data.PoliMorf as Poli++import NLP.Nerf.Dict.Base+import NLP.Nerf.Dict.PNEG (readPNEG)+import NLP.Nerf.Dict.NELexicon (readNELexicon)+import NLP.Nerf.Dict.Prolexbase (readProlexbase)+import qualified NLP.Nerf.Dict.PNET as PNET++-- | Is it a single word entry?+atomic :: Entry -> Bool+atomic = not . isMultiWord . neOrth++-- | Extract NEs dictionary from PNEG.+extractPNEG+ :: FilePath -- ^ Path to PNEG in the LMF format+ -> IO Dict+extractPNEG lmfPath =+ fromEntries . filter atomic <$> readPNEG lmfPath++-- | Extract NEs dictionary from NELexicon.+extractNELexicon+ :: FilePath -- ^ Path to NELexicon+ -> IO Dict+extractNELexicon nePath =+ fromEntries . filter atomic <$> readNELexicon nePath++-- | Extract NEs dictionary from PoliMorf.+extractPoliMorf+ :: FilePath -- ^ Path to PoliMorf+ -> IO Dict+extractPoliMorf poliPath+ = fromPairs . filter (cond . snd)+ . map ((,) <$> Poli.form <*> Poli.cat)+ <$> Poli.readPoliMorf poliPath+ where+ cond x = x /= "pospolita" && x /= ""++-- | Extract NEs dictionary from Prolexbase.+extractProlexbase+ :: FilePath -- ^ Path to Prolexbase+ -> IO Dict+extractProlexbase proPath = do+ fromEntries . filter atomic <$> readProlexbase proPath++-- | Extract internal triggers from PNET dictionary.+extractIntTriggers+ :: FilePath -- ^ Path to PNET+ -> IO Dict+extractIntTriggers pnetPath =+ mkTriggers PNET.Internal <$> PNET.readPNET pnetPath++-- | Extract external triggers from PNET dictionary.+extractExtTriggers+ :: FilePath -- ^ Path to PNET+ -> IO Dict+extractExtTriggers pnetPath =+ mkTriggers PNET.External <$> PNET.readPNET pnetPath++mkTriggers :: PNET.Typ -> [PNET.Entry] -> Dict+mkTriggers typ+ = fromPairs+ . filter (not . isMultiWord . fst) + . map ((,) <$> PNET.orth <*> PNET.neTyp)+ . filter (PNET.hasTyp typ)
+ src/NLP/Nerf/Dict/Base.hs view
@@ -0,0 +1,116 @@+{-# LANGUAGE TupleSections #-}++-- | Basic types for dictionary handling. ++module NLP.Nerf.Dict.Base+(+-- * Lexicon entry+ NeType+, Form+, isMultiWord+, Entry (..)++-- * Dictionary+, Label+, DAWG+, Dict+, fromPairs+, fromEntries+, siftDict+, saveDict+, loadDict++-- * Merging dictionaries+, merge+, diff+) where++import Control.Applicative ((<$>), (<*>))+import Data.Binary (encodeFile, decodeFile)+import Data.Text.Binary ()+import qualified Data.Text as T+import qualified Data.Set as S+import qualified Data.DAWG.Static as D+import qualified Data.DAWG.Trans.Vector as D++-- | A type of named entity.+type NeType = T.Text++-- | A orthographic form.+type Form = T.Text++-- | Is the form a multiword one?+isMultiWord :: Form -> Bool+isMultiWord = (>1) . length . T.words++-- | A Named Entity entry from the LMF dictionary.+data Entry = Entry+ { neOrth :: !Form -- ^ Orthographic form of the NE+ , neType :: !NeType -- ^ Type of the NE+ } deriving (Show, Read, Eq, Ord)++-- | Dictionary label.+type Label = T.Text++-- | A 'Dict' is a map from forms to labels. Each form may be annotated+-- with multiple labels. The map is represented using the directed acyclic+-- word graph.+-- type Dict = D.DAWG (S.Set Label)+type DAWG = D.DAWG D.Trans Char ()+type Dict = DAWG (S.Set Label)++-- | Construct dictionary from the list of form/label pairs.+fromPairs :: [(Form, Label)] -> Dict+fromPairs xs = D.fromListWith S.union+ [ ( T.unpack x+ , S.singleton y)+ | (x, y) <- xs ]++-- | Construct dictionary from the list of entries.+fromEntries :: [Entry] -> Dict+fromEntries = fromPairs . map ((,) <$> neOrth <*> neType)++-- | Remove dictionary entries which do not satisfy the predicate.+siftDict :: (Form -> S.Set Label -> Bool) -> Dict -> Dict+siftDict f dict = D.fromList [(k, v) | (k, v) <- D.assocs dict, f (T.pack k) v]++-- | Save the dictionary in the file.+saveDict :: FilePath -> Dict -> IO ()+saveDict = encodeFile++-- | Load the dictionary from the file.+loadDict :: FilePath -> IO Dict+loadDict = decodeFile++-- | Merge dictionary resources.+merge :: [Dict] -> Dict+merge = unionsWith S.union++-- | Replacement implementation of unionsWith while there is+-- no such function in dawg library. +unionsWith :: Ord a => (a -> a -> a) -> [DAWG a] -> DAWG a+unionsWith f = foldl (unionWith f) D.empty++-- | Replacement implementation of unionWith while there is+-- no such function in dawg library. +unionWith :: Ord a => (a -> a -> a) -> DAWG a -> DAWG a -> DAWG a+unionWith f d d' = D.fromListWith f (D.assocs d ++ D.assocs d')++-- | Differentiate labels from separate dictionaries using+-- dictionary-unique prefixes.+diff :: [Dict] -> [Dict]+diff ds =+ [ mapS (addPrefix i) `mapD` dict+ | (i, dict) <- zip [0..] ds ]++-- | Map function over the DAWG elements.+mapD :: Ord b => (a -> b) -> DAWG a -> DAWG b+mapD f d = D.fromList [(x, f y) | (x, y) <- D.assocs d]++-- | Map function over the set.+mapS :: Ord b => (a -> b) -> S.Set a -> S.Set b+mapS f s = S.fromList [f x | x <- S.toList s]++-- | Add integer prefix.+addPrefix :: Int -> T.Text -> T.Text+addPrefix = T.append . T.pack . show
+ src/NLP/Nerf/Dict/NELexicon.hs view
@@ -0,0 +1,24 @@+-- | Handling the NELexicon dictionary.++module NLP.Nerf.Dict.NELexicon+( parseNELexicon+, readNELexicon+) where++import qualified Data.Text.Lazy as L+import qualified Data.Text.Lazy.IO as L++import NLP.Nerf.Dict.Base++-- | Parse the NELexicon into a list of entries.+parseNELexicon :: L.Text -> [Entry]+parseNELexicon = map parseLine . L.lines++parseLine :: L.Text -> Entry+parseLine line = case L.break (==';') line of+ (_type, _form) -> Entry (L.toStrict $ L.drop 2 _form) (L.toStrict _type)+ _ -> error $ "parseLine: invalid line \"" ++ L.unpack line ++ "\""++-- | Read the dictionary from the file.+readNELexicon :: FilePath -> IO [Entry]+readNELexicon = fmap parseNELexicon . L.readFile
+ src/NLP/Nerf/Dict/PNEG.hs view
@@ -0,0 +1,44 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE OverloadedStrings #-}++-- | Parsing the Gazetteer for Polish Named Entities (used formerly within+-- the SProUT platform) in the LMF format.++module NLP.Nerf.Dict.PNEG+( parsePNEG+, readPNEG+) where++import Text.XML.PolySoup+import qualified Data.Text as T+import qualified Data.Text.Lazy as L+import qualified Data.Text.Lazy.IO as L++import NLP.Nerf.Dict.Base++lmfP :: XmlParser L.Text [Entry]+lmfP = true ##> lexEntryP++lexEntryP :: XmlParser L.Text [Entry]+lexEntryP = tag "LexicalEntry" `joinR` do+ many_ $ cut $ tag "feat"+ _words <- many wordP+ sense <- senseP+ return [Entry x sense | x <- _words]++wordP :: XmlParser L.Text Form+wordP = head <$> (tag "Lemma" <|> tag "WordForm" /> featP "writtenForm")++senseP :: XmlParser L.Text NeType+senseP = head <$> (tag "Sense" //> featP "externalReference" <|> featP "label")++featP :: L.Text -> XmlParser L.Text T.Text+featP x = L.toStrict <$> cut (tag "feat" *> hasAttr "att" x *> getAttr "val")++-- | Parse the dictionary to the list of entries.+parsePNEG :: L.Text -> [Entry]+parsePNEG = parseXml lmfP++-- | Read the dictionary from the file.+readPNEG :: FilePath -> IO [Entry]+readPNEG = fmap parsePNEG . L.readFile
+ src/NLP/Nerf/Dict/PNET.hs view
@@ -0,0 +1,53 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Polish Named Entity Triggers <http://zil.ipipan.waw.pl/PNET> dictionary.++module NLP.Nerf.Dict.PNET+( parsePNET+, readPNET+, Typ (..)+, hasTyp+, Entry (..)+) where++import qualified Data.Text as T+import qualified Data.Text.Lazy as L+import qualified Data.Text.Lazy.IO as L++-- | Trigger type.+data Typ+ = Internal+ | External+ deriving (Show, Eq, Ord)++readTyp :: T.Text -> Typ+readTyp "int" = Internal+readTyp "ext" = External+readTyp x = error $ "readTyp: typ " ++ T.unpack x ++ " unknown"++-- | PNET entry.+data Entry = Entry+ { orth :: T.Text+ , base :: T.Text+ , tag :: T.Text+ , typ :: Typ+ , neTyp :: T.Text+ , example :: T.Text }++-- | Does entry represents a trigger of the given type?+hasTyp :: Typ -> Entry -> Bool+hasTyp x = (==x) . typ++parseLine :: L.Text -> Entry+parseLine line = case map L.toStrict (L.split (=='\t') line) of+ [_orth, _base, _tag, _typ, _neTyp, _example] ->+ Entry _orth _base _tag (readTyp _typ) _neTyp _example+ _ -> error $ "parseLine: invalid row \"" ++ L.unpack line ++ "\""++-- | Parse dictionary into a list of entries.+parsePNET :: L.Text -> [Entry]+parsePNET = map parseLine . L.lines++-- | Read dictionary from the file.+readPNET :: FilePath -> IO [Entry]+readPNET = fmap parsePNET . L.readFile
+ src/NLP/Nerf/Dict/Prolexbase.hs view
@@ -0,0 +1,25 @@+-- | Handling Prolexbase dictionaries, both with the+-- same storage format.++module NLP.Nerf.Dict.Prolexbase+( parseProlexbase+, readProlexbase+) where++import qualified Data.Text.Lazy as L+import qualified Data.Text.Lazy.IO as L++import NLP.Nerf.Dict.Base++-- | Parse dictionary into a list of entries.+parseProlexbase :: L.Text -> [Entry]+parseProlexbase = map parseLine . L.lines++parseLine :: L.Text -> Entry+parseLine row = case map L.toStrict (L.split (=='\t') row) of+ [_form, _base, _tag, _cat] -> Entry _form _cat+ _ -> error $ "parseLine: invalid row \"" ++ L.unpack row ++ "\""++-- | Read the dictionary from the file.+readProlexbase :: FilePath -> IO [Entry]+readProlexbase = fmap parseProlexbase . L.readFile
+ src/NLP/Nerf/Schema.hs view
@@ -0,0 +1,360 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE OverloadedStrings #-}++-- | Observation schema blocks for Nerf.++module NLP.Nerf.Schema+( +-- * Types+ Ox+, Schema+, void+, sequenceS_++-- * Usage+, schematize++-- ** Configuration+, Body (..)+, Entry+, entry+, entryWith+, SchemaConf (..)+, nullConf+, defaultConf+, fromConf++-- ** Schema blocks+, Block+, fromBlock+, orthB+, splitOrthB+, lowPrefixesB+, lowSuffixesB+, lemmaB+, shapeB+, packedB+, shapePairB+, packedPairB+, dictB+) where++import Control.Applicative ((<$>), (<*>))+import Control.Monad (forM_, join)+import Data.Maybe (maybeToList)+import Data.Binary (Binary, put, get)+import qualified Data.Char as C+import qualified Data.Set as S+import qualified Data.Vector as V+import qualified Data.Text as T+import qualified Data.DAWG.Static as D++import qualified Data.CRF.Chain1 as CRF+import qualified Control.Monad.Ox as Ox+import qualified Control.Monad.Ox.Text as Ox++import NLP.Nerf.Types+import NLP.Nerf.Dict (Dict)++-- | The Ox monad specialized to word token type and text observations.+type Ox a = Ox.Ox Word T.Text a++-- | A schema is a block of the Ox computation performed within the+-- context of the sentence and the absolute sentence position.+type Schema a = V.Vector Word -> Int -> Ox a++-- | A dummy schema block.+void :: a -> Schema a+void x _ _ = return x++-- | Sequence the list of schemas (or blocks) and discard individual values.+sequenceS_ :: [V.Vector Word -> a -> Ox b] -> V.Vector Word -> a -> Ox ()+sequenceS_ xs sent =+ let ys = map ($sent) xs+ in \k -> sequence_ (map ($k) ys)++-- | Record structure of the basic observation types.+data BaseOb = BaseOb+ { orth :: Int -> Maybe T.Text+ , lowOrth :: Int -> Maybe T.Text }++-- | Construct the 'BaseOb' structure given the sentence.+mkBaseOb :: V.Vector Word -> BaseOb+mkBaseOb sent = BaseOb+ { orth = _orth+ , lowOrth = _lowOrth }+ where+ at = Ox.atWith sent+ _orth = (id `at`)+ _lowOrth i = T.toLower <$> _orth i++-- | A block is a chunk of the Ox computation performed within the+-- context of the sentence and the list of absolute sentence positions.+type Block a = V.Vector Word -> [Int] -> Ox a++-- | Transform the block to the schema depending on the list of+-- relative sentence positions.+fromBlock :: Block a -> [Int] -> Schema a+fromBlock blk xs sent =+ let blkSent = blk sent+ in \k -> blkSent [x + k | x <- xs]++-- | Orthographic form at the current position.+orthB :: Block ()+orthB sent = \ks ->+ let orthOb = Ox.atWith sent id+ in mapM_ (Ox.save . orthOb) ks++-- | Orthographic form split into two observations: the lowercased form and+-- the original form (only when different than the lowercased one).+splitOrthB :: Block ()+splitOrthB sent = \ks -> do+ mapM_ (Ox.save . lowOrth) ks+ mapM_ (Ox.save . upOnlyOrth) ks+ where+ BaseOb{..} = mkBaseOb sent+ upOnlyOrth i = orth i >>= \x -> case T.any C.isUpper x of+ True -> Just x+ False -> Nothing++-- | List of lowercased prefixes of given lengths.+lowPrefixesB :: [Int] -> Block ()+lowPrefixesB ns sent = \ks ->+ forM_ ks $ \i ->+ mapM_ (Ox.save . lowPrefix i) ns+ where+ BaseOb{..} = mkBaseOb sent+ lowPrefix i j = Ox.prefix j =<< lowOrth i++-- | List of lowercased suffixes of given lengths.+lowSuffixesB :: [Int] -> Block ()+lowSuffixesB ns sent = \ks ->+ forM_ ks $ \i ->+ mapM_ (Ox.save . lowSuffix i) ns+ where+ BaseOb{..} = mkBaseOb sent+ lowSuffix i j = Ox.suffix j =<< lowOrth i++-- | Lemma substitute parametrized by the number specifying the span+-- over which lowercased prefixes and suffixes will be 'Ox.save'd.+-- For example, @lemmaB 2@ will take affixes of lengths @0, -1@ and @-2@+-- on account.+lemmaB :: Int -> Block ()+lemmaB n sent = \ks -> do+ mapM_ lowLemma ks+ where+ BaseOb{..} = mkBaseOb sent+ lowPrefix i j = Ox.prefix j =<< lowOrth i+ lowSuffix i j = Ox.suffix j =<< lowOrth i+ lowLemma i = Ox.group $ do+ mapM_ (Ox.save . lowPrefix i) [0, -1 .. -n]+ mapM_ (Ox.save . lowSuffix i) [0, -1 .. -n]++-- | Shape of the word.+shapeB :: Block ()+shapeB sent = \ks -> do+ mapM_ (Ox.save . shape) ks+ where+ BaseOb{..} = mkBaseOb sent+ shape i = Ox.shape <$> orth i++-- | Packed shape of the word.+packedB :: Block ()+packedB sent = \ks -> do+ mapM_ (Ox.save . shapeP) ks+ where+ BaseOb{..} = mkBaseOb sent+ shape i = Ox.shape <$> orth i+ shapeP i = Ox.pack <$> shape i++-- -- | Shape and packed shape of the word.+-- shapeAndPackedB :: Block ()+-- shapeAndPackedB sent = \ks -> do+-- mapM_ (Ox.save . shape) ks+-- mapM_ (Ox.save . shapeP) ks+-- where+-- BaseOb{..} = mkBaseOb sent+-- shape i = Ox.shape <$> orth i+-- shapeP i = Ox.pack <$> shape i++-- | Combined shapes of two consecutive (at @k-1@ and @k@ positions) words.+shapePairB :: Block ()+shapePairB sent = \ks ->+ forM_ ks $ \i -> do+ Ox.save $ link <$> shape i <*> shape (i - 1)+ where+ BaseOb{..} = mkBaseOb sent+ shape i = Ox.shape <$> orth i+ link x y = T.concat [x, "-", y]++-- | Combined packed shapes of two consecutive (at @k-1@ and @k@ positions)+-- words.+packedPairB :: Block ()+packedPairB sent = \ks ->+ forM_ ks $ \i -> do+ Ox.save $ link <$> shapeP i <*> shapeP (i - 1)+ where+ BaseOb{..} = mkBaseOb sent+ shape i = Ox.shape <$> orth i+ shapeP i = Ox.pack <$> shape i+ link x y = T.concat [x, "-", y]++-- | Plain dictionary search determined with respect to the list of+-- relative positions.+dictB :: Dict -> Block ()+dictB dict sent = \ks -> do+ mapM_ (Ox.saves . searchDict) ks+ where+ BaseOb{..} = mkBaseOb sent+ searchDict i = join . maybeToList $+ S.toList <$> (orth i >>= flip D.lookup dict . T.unpack)++-- | Body of configuration entry.+data Body a = Body {+ -- | Range argument for the schema block. + range :: [Int]+ -- | Additional arguments for the schema block.+ , args :: a }+ deriving (Show)++instance Binary a => Binary (Body a) where+ put Body{..} = put range >> put args+ get = Body <$> get <*> get++-- | Maybe entry.+type Entry a = Maybe (Body a)++-- | Entry with additional arguemnts.+entryWith :: a -> [Int] -> Entry a+entryWith v xs = Just (Body xs v)++-- | Maybe entry with additional arguemnts.+entryWith'Mb :: Maybe a -> [Int] -> Entry a+entryWith'Mb (Just v) xs = Just (Body xs v)+entryWith'Mb Nothing _ = Nothing++-- | Plain entry with no additional arugments.+entry :: [Int] -> Entry ()+entry = entryWith ()++-- | Configuration of the schema. All configuration elements specify the+-- range over which a particular observation type should be taken on account.+-- For example, the @[-1, 0, 2]@ range means that observations of particular+-- type will be extracted with respect to previous (@k - 1@), current (@k@)+-- and after the next (@k + 2@) positions when identifying the observation+-- set for position @k@ in the input sentence.+data SchemaConf = SchemaConf {+ -- | The 'orthB' schema block.+ orthC :: Entry ()+ -- | The 'splitOrthB' schema block.+ , splitOrthC :: Entry ()+ -- | The 'lowPrefixesB' schema block. The first list of ints+ -- represents lengths of prefixes.+ , lowPrefixesC :: Entry [Int]+ -- | The 'lowSuffixesB' schema block. The first list of ints+ -- represents lengths of suffixes.+ , lowSuffixesC :: Entry [Int]+ -- | The 'lemmaB' schema block.+ , lemmaC :: Entry Int+ -- | The 'shapeB' schema block.+ , shapeC :: Entry ()+ -- | The 'packedB' schema block.+ , packedC :: Entry ()+ -- | The 'shapePairB' schema block.+ , shapePairC :: Entry ()+ -- | The 'packedPairB' schema block.+ , packedPairC :: Entry ()+ -- | Dictionaries of NEs ('dictB' schema block).+ , dictC :: Entry [Dict]+ -- | Dictionary of internal triggers.+ , intTrigsC :: Entry Dict+ -- | Dictionary of external triggers.+ , extTrigsC :: Entry Dict+ } deriving (Show)++instance Binary SchemaConf where+ put SchemaConf{..} = do+ put orthC+ put splitOrthC+ put lowPrefixesC+ put lowSuffixesC+ put lemmaC+ put shapeC+ put packedC+ put shapePairC+ put packedPairC+ put dictC+ put intTrigsC+ put extTrigsC+ get = SchemaConf+ <$> get <*> get <*> get <*> get+ <*> get <*> get <*> get <*> get+ <*> get <*> get <*> get <*> get++-- | Null configuration of the observation schema.+nullConf :: SchemaConf+nullConf = SchemaConf+ Nothing Nothing Nothing Nothing+ Nothing Nothing Nothing Nothing+ Nothing Nothing Nothing Nothing++-- | Default configuration of the observation schema.+defaultConf+ :: [Dict] -- ^ Named Entity dictionaries+ -> Maybe Dict -- ^ Dictionary of internal triggers+ -> Maybe Dict -- ^ Dictionary of external triggers+ -> IO SchemaConf+defaultConf neDicts intDict extDict = do+ return $ SchemaConf+ { orthC = Nothing+ , splitOrthC = entry [-1, 0]+ , lowPrefixesC = Nothing+ , lowSuffixesC = entryWith [2, 3, 4] [0]+ , lemmaC = entryWith 3 [-1, 0]+ , shapeC = entry [-1, 0]+ , packedC = entry [-1, 0]+ , shapePairC = entry [0]+ , packedPairC = entry [0]+ , dictC = entryWith neDicts [-1, 0]+ , intTrigsC = entryWith'Mb intDict [0]+ , extTrigsC = entryWith'Mb extDict [-1] }++mkArg0 :: Block () -> Entry () -> Schema ()+mkArg0 blk (Just x) = fromBlock blk (range x)+mkArg0 _ Nothing = void ()++mkArg1 :: (a -> Block ()) -> Entry a -> Schema ()+mkArg1 blk (Just x) = fromBlock (blk (args x)) (range x)+mkArg1 _ Nothing = void ()++mkArgs1 :: (a -> Block ()) -> Entry [a] -> Schema ()+mkArgs1 blk (Just x) = sequenceS_+ [ fromBlock+ (blk dict)+ (range x)+ | dict <- args x ]+mkArgs1 _ Nothing = void ()++-- | Build the schema based on the configuration.+fromConf :: SchemaConf -> Schema ()+fromConf SchemaConf{..} = sequenceS_+ [ mkArg0 orthB orthC+ , mkArg0 splitOrthB splitOrthC+ , mkArg1 lowPrefixesB lowPrefixesC+ , mkArg1 lowSuffixesB lowSuffixesC+ , mkArg1 lemmaB lemmaC+ , mkArg0 shapeB shapeC+ , mkArg0 packedB packedC+ , mkArg0 shapePairB shapePairC+ , mkArg0 packedPairB packedPairC+ , mkArgs1 dictB dictC+ , mkArg1 dictB intTrigsC+ , mkArg1 dictB extTrigsC ]++-- | Use the schema to extract observations from the sentence.+schematize :: Schema a -> [Word] -> CRF.Sent Ob+schematize schema xs =+ map (S.fromList . Ox.execOx . schema v) [0 .. n - 1]+ where+ v = V.fromList xs+ n = V.length v
+ src/NLP/Nerf/Tokenize.hs view
@@ -0,0 +1,150 @@+{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE FlexibleInstances #-}++-- | The module implements the tokenization used within Nerf+-- and some other tokenization-related stuff.++module NLP.Nerf.Tokenize+(+-- * Tokenization+ tokenize+-- * Synchronization+, Word (..)+, moveNEs+) where++import Control.Monad ((>=>))+import Data.Foldable (foldMap)+import qualified Data.Char as Char+import qualified Data.List as L+import qualified Data.Tree as T+import qualified Data.Traversable as Tr+import qualified Data.Text as Text+import qualified Data.Text.Lazy as LazyText+import qualified NLP.Tokenize as Tok++import Data.Named.Tree (NeForest, NeTree, groupForestLeaves)++---------------------------+-- Tokenization definition.+---------------------------++-- | Default tokenizator.+defaultTokenizer :: Tok.Tokenizer+defaultTokenizer+ = Tok.whitespace+ >=> Tok.uris+ >=> Tok.punctuation++-- | Tokenize sentence using the default tokenizer.+tokenize :: String -> [String]+tokenize = Tok.run defaultTokenizer++---------------------------------------------------------------+-- Synchronizing named entities with new sentence tokenization.+---------------------------------------------------------------++-- | A class of objects with size.+class Word a where+ word :: a -> String++instance Word String where+ word = id++instance Word Text.Text where+ word = Text.unpack++instance Word LazyText.Text where+ word = LazyText.unpack++essence :: Word a => a -> Int+essence = length . filter (not . Char.isSpace) . word+{-# INLINE essence #-}++-- | Syncronization between two sentences. Each (xs, ys) pair represents+-- tokens from the two input sentences which corresponds to each other.+type Sync a b = [([a], [b])]++-- | Synchronize two tokenizations of the sentence.+sync :: (Word a, Word b) => [a] -> [b] -> Sync a b+sync = sync' 0++sync' :: (Word a, Word b) => Int -> [a] -> [b] -> Sync a b+sync' r (x:xs) (y:ys)+ | n + r == m = ([x], [y]) : sync' 0 xs ys+ | n + r < m = join x $ sync' (n + r) xs (y:ys)+ | otherwise = swap . join y $ sync' (m - r) ys (x:xs)+ where+ n = essence x+ m = essence y+ join l ((ls, rs) : ps) = (l:ls, rs) : ps+ join _ [] = error "sync'.join: bad arguments"+ swap ((ls, rs) : ps) = (rs, ls) : swap ps+ swap [] = []+sync' 0 [] [] = []+sync' _ _ _ = error "sync': bad arguments"++-- | Match the `Sync` with the given list, return the matching result+-- (snd elements of the `Sync` list) and the rest of the `Sync` list.+match :: (Word a, Word b) => [a] -> Sync a b -> ([b], Sync a b)+match xs ss =+ let (sl, sr) = splitAcc isMatch 0 ss+ in (concatMap snd sl, sr)+ where+ n = sum (map essence xs)+ isMatch r (ys, _)+ | m + r < n = (m + r, False)+ | m + r == n = (m + r, True)+ | otherwise = error "match.isMatch: no match"+ where+ m = sum (map essence ys)++-- | Split the list with the help of the accumulating function.+splitAcc :: (acc -> a -> (acc, Bool)) -> acc -> [a] -> ([a], [a])+splitAcc _ _ [] = ([], [])+splitAcc f acc (x:xs)+ | cond = ([x], xs)+ | otherwise = join x (splitAcc f acc' xs)+ where+ (acc', cond) = f acc x+ join y (ys, zs) = (y:ys, zs)++-- | List forest leaves.+leaves :: NeForest a b -> [b]+leaves = concatMap $ foldMap (either (const []) (:[]))++unGroupLeaves :: NeForest a [b] -> NeForest a b+unGroupLeaves = concatMap unGroupLeavesT++unGroupLeavesT :: NeTree a [b] -> [NeTree a b]+unGroupLeavesT (T.Node (Left v) xs) =+ [T.Node (Left v) (unGroupLeaves xs)]+unGroupLeavesT (T.Node (Right vs) _) =+ [T.Node (Right v) [] | v <- vs]++substGroups :: (Word b, Word c) => NeForest a [b] -> Sync b c -> NeForest a [c]+substGroups fs ss = snd $ L.mapAccumL substGroupsT ss fs++substGroupsT+ :: (Word b, Word c)+ => Sync b c -> NeTree a [b]+ -> (Sync b c, NeTree a [c])+substGroupsT =+ Tr.mapAccumL f+ where+ f s (Left v) = (s, Left v)+ f s (Right v) =+ let (v', s') = match v s+ in (s', Right v')++-- | Synchronize named entities with tokenization represented+-- by the second function argument. Of course, both arguments+-- should relate to the same sentence.+moveNEs :: (Word b, Word c) => NeForest a b -> [c] -> NeForest a c+moveNEs ft ys+ = unGroupLeaves+ $ substGroups+ (groupForestLeaves true ft)+ (sync (leaves ft) ys)+ where+ true _ _ = True
+ src/NLP/Nerf/Types.hs view
@@ -0,0 +1,25 @@+-- | Basic types.++module NLP.Nerf.Types+( Word+, NE+, Ob+, Lb+) where++import qualified Data.Text as T+import qualified Data.Named.IOB as IOB++-- | A word.+type Word = T.Text++-- | A named entity.+type NE = T.Text++-- | An observation consist of an index (of list type) and an actual+-- observation value.+type Ob = ([Int], T.Text)++-- | A label is created by encoding the named entity forest using the+-- IOB method.+type Lb = IOB.Label NE
tools/nerf.hs view
@@ -14,14 +14,12 @@ import Data.Text.Binary () import Text.Named.Enamex (showForest) import qualified Numeric.SGD as SGD-import qualified Data.Text as T import qualified Data.Text.Lazy as L import qualified Data.Text.Lazy.IO as L import qualified Data.DAWG.Static as D import NLP.Nerf (train, ner, tryOx) import NLP.Nerf.Schema (defaultConf)-import NLP.Nerf.Tokenize (tokenize) import NLP.Nerf.Dict ( extractPoliMorf, extractPNEG, extractNELexicon, extractProlexbase , extractIntTriggers, extractExtTriggers, Dict )@@ -147,7 +145,7 @@ nerf <- decodeFile inNerf input <- readRaw dataPath forM_ input $ \sent -> do- let forest = ner nerf sent+ let forest = ner nerf (L.unpack sent) L.putStrLn (showForest forest) exec nerfArgs@Ox{..} = do@@ -157,12 +155,5 @@ intDict extDict tryOx cfg dataPath --- | Prepare input data: divide it into a list of sentences and tokenize--- each sentence using the default tokenizer.-parseRaw :: L.Text -> [[T.Text]]-parseRaw =- let doTok = map T.pack . tokenize . L.unpack- in map doTok . L.lines--readRaw :: FilePath -> IO [[T.Text]]-readRaw = fmap parseRaw . L.readFile+readRaw :: FilePath -> IO [L.Text]+readRaw = fmap L.lines . L.readFile