packages feed

nerf-0.5.4: app/Main.hs

{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RecordWildCards #-}

import           System.Console.CmdArgs
import           System.IO
    ( Handle, hGetBuffering, hSetBuffering
    , stdout, BufferMode (..), hClose, hFlush )
import           System.IO.Unsafe (unsafePerformIO)
import qualified System.IO.Temp as Temp
import qualified Network as N
import           System.Directory (getDirectoryContents)
import           System.FilePath (takeBaseName, (</>), (<.>))
import           Control.Applicative ((<$>), (<*>))
import           Control.Arrow (second)
import           Control.Monad (forM_)
import           Data.Maybe (catMaybes)
import           Data.Binary (encodeFile, decodeFile)
import           Data.Text.Binary ()
import           Text.Named.Enamex (parseEnamex, showForest)
import qualified Data.Foldable as F
import qualified Data.Map as M
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.Dict
    ( extractPoliMorf, extractPNEG, extractNELexicon, extractProlexbase
    , extractIntTriggers, extractExtTriggers, Dict )
import           NLP.Nerf.XCES as XCES
import qualified NLP.Nerf.Server as S

import           NLP.Nerf.Compare ((.+.))
import qualified NLP.Nerf.Compare as C


-- | Default port number.
portDefault :: Int
portDefault = 10090


---------------------------------------
-- Command line options
---------------------------------------


-- | Data formats. 
data Format
    = Text
    | XCES
    deriving (Data, Typeable, Show)


data Nerf
  = Train
    { trainPath     :: FilePath
    , evalPath      :: Maybe FilePath
    , poliMorf      :: Maybe FilePath
    , prolex        :: Maybe FilePath
    , pneg          :: Maybe FilePath
    , neLex         :: Maybe FilePath
    , pnet          :: Maybe FilePath
    , iterNum       :: Double
    , batchSize     :: Int
    , regVar        :: Double
    , gain0         :: Double
    , tau           :: Double
    , outNerf       :: Maybe FilePath }
  | CV
    { dataDir       :: FilePath
    , poliMorf      :: Maybe FilePath
    , prolex        :: Maybe FilePath
    , pneg          :: Maybe FilePath
    , neLex         :: Maybe FilePath
    , pnet          :: Maybe FilePath
    , iterNum       :: Double
    , batchSize     :: Int
    , regVar        :: Double
    , gain0         :: Double
    , tau           :: Double
    , outDir        :: Maybe FilePath }
  | NER
    { inModel       :: FilePath
    , format        :: Format }
  | Server
    { inModel       :: FilePath
    , port          :: Int }
  | Client
    { format        :: Format
    , host          :: String
    , port          :: Int }
  | Ox
    { dataPath      :: FilePath
    , poliMorf      :: Maybe FilePath
    , prolex        :: Maybe FilePath
    , pneg          :: Maybe FilePath
    , neLex         :: Maybe FilePath
    , pnet          :: Maybe FilePath }
  | Compare
    { dataPath      :: FilePath
    , dataPath'     :: FilePath }
  deriving (Data, Typeable, Show)


trainMode :: Nerf
trainMode = Train
    { trainPath = def &= argPos 0 &= typ "TRAIN-FILE"
    , evalPath = def &= typFile &= help "Evaluation file"
    , poliMorf = def &= typFile &= help "Path to PoliMorf"
    , prolex = def &= typFile &= help "Path to Prolexbase"
    , pneg = def &= typFile &= help "Path to PNEG-LMF"
    , neLex = def &= typFile &= help "Path to NELexicon"
    , pnet = def &= typFile &= help "Path to PNET"
    , iterNum = 10 &= help "Number of SGD iterations"
    , batchSize = 30 &= help "Batch size"
    , regVar = 10.0 &= help "Regularization variance"
    , gain0 = 1.0 &= help "Initial gain parameter"
    , tau = 5.0 &= help "Initial tau parameter"
    , outNerf = def &= typFile &= help "Output model file" }


cvMode :: Nerf
cvMode = CV
    { dataDir = def &= argPos 0 &= typ "DATA-DIR"
    , poliMorf = def &= typFile &= help "Path to PoliMorf"
    , prolex = def &= typFile &= help "Path to Prolexbase"
    , pneg = def &= typFile &= help "Path to PNEG-LMF"
    , neLex = def &= typFile &= help "Path to NELexicon"
    , pnet = def &= typFile &= help "Path to PNET"
    , iterNum = 10 &= help "Number of SGD iterations"
    , batchSize = 30 &= help "Batch size"
    , regVar = 10.0 &= help "Regularization variance"
    , gain0 = 1.0 &= help "Initial gain parameter"
    , tau = 5.0 &= help "Initial tau parameter"
    , outDir = def &= typFile &= help "Output model directory" }


nerMode :: Nerf
nerMode = NER
    { inModel  = def &= argPos 0 &= typ "MODEL-FILE"
    , format   = enum
        [ Text &= help "Raw text"
        , XCES &= help "XCES" ] }


serverMode :: Nerf
serverMode = Server
    { inModel = def &= argPos 0 &= typ "MODEL-FILE"
    , port    = portDefault &= help "Port number" }


clientMode :: Nerf
clientMode = Client
    { port   = portDefault &= help "Port number"
    , host   = "localhost" &= help "Server host name"
    , format   = enum
        [ Text &= help "Raw text"
        , XCES &= help "XCES" ] }


oxMode :: Nerf
oxMode = Ox
    { dataPath = def &= argPos 0 &= typ "DATA-FILE"
    , poliMorf = def &= typFile &= help "Path to PoliMorf"
    , prolex = def &= typFile &= help "Path to Prolexbase"
    , pneg = def &= typFile &= help "Path to PNEG-LMF"
    , neLex = def &= typFile &= help "Path to NELexicon"
    , pnet = def &= typFile &= help "Path to PNET" }


cmpMode :: Nerf
cmpMode = Compare
    { dataPath  = def &= argPos 0 &= typ "REFERENCE"
    , dataPath' = def &= argPos 1 &= typ "COMPARED" }


argModes :: Mode (CmdArgs Nerf)
argModes = cmdArgsMode $ modes
    [trainMode, cvMode, nerMode, serverMode, clientMode, cmpMode, oxMode]


data Resources = Resources
    { poliDict      :: Maybe Dict
    , prolexDict    :: Maybe Dict
    , pnegDict      :: Maybe Dict
    , neLexDict     :: Maybe Dict
    , intDict       :: Maybe Dict
    , extDict       :: Maybe Dict }


extract :: Nerf -> IO Resources
extract nerf = withBuffering stdout NoBuffering $ Resources
    <$> extractDict "PoliMorf" extractPoliMorf (poliMorf nerf)
    <*> extractDict "Prolexbase" extractProlexbase (prolex nerf)
    <*> extractDict "PNEG" extractPNEG (pneg nerf)
    <*> extractDict "NELexicon" extractNELexicon (neLex nerf)
    <*> extractDict "internal triggers" extractIntTriggers (pnet nerf)
    <*> extractDict "external triggers" extractExtTriggers (pnet nerf)


withBuffering :: Handle -> BufferMode -> IO a -> IO a
withBuffering h mode io = do
    oldMode <- hGetBuffering h
    hSetBuffering h mode
    x <- io
    hSetBuffering h oldMode
    return x


extractDict :: String -> (a -> IO Dict) -> Maybe a -> IO (Maybe Dict)
extractDict msg f (Just x) = do
    putStr $ "Reading " ++ msg ++ "..."
    dict <- f x
    let k = D.numStates dict
    k `seq` putStrLn $ " Done"
    putStrLn $ "Number of automaton states = " ++ show k
    return (Just dict)
extractDict _ _ Nothing = return Nothing


main :: IO ()
main = exec =<< cmdArgsRun argModes


exec :: Nerf -> IO ()


exec nerfArgs@Train{..} = do
    Resources{..} <- extract nerfArgs
    cfg <- defaultConf
        (catMaybes [poliDict, prolexDict, pnegDict, neLexDict])
        intDict extDict
    nerf <- train sgdArgs cfg trainPath evalPath
    flip F.traverse_ outNerf $ \path -> do
        putStrLn $ "\nSaving model in " ++ path ++ "..."
        encodeFile path nerf
  where
    sgdArgs = SGD.SgdArgs
        { SGD.batchSize = batchSize
        , SGD.regVar = regVar
        , SGD.iterNum = iterNum
        , SGD.gain0 = gain0
        , SGD.tau = tau }


exec nerfArgs@CV{..} = do
    Resources{..} <- extract nerfArgs
    cfg <- defaultConf
        (catMaybes [poliDict, prolexDict, pnegDict, neLexDict])
        intDict extDict
    parts <- getParts dataDir
    forM_ (enumDivs parts) $ \(evalPath, trainPaths) -> do
        putStrLn $ "\nPart: " ++ evalPath
        withParts trainPaths $ \trainPath -> do
            nerf <- train sgdArgs cfg trainPath (Just evalPath)
            flip F.traverse_ outDir $ \dir -> do
                let path = dir </> takeBaseName evalPath <.> ".bin"
                putStrLn $ "\nSaving model in " ++ path ++ "..."
                encodeFile path nerf
  where
    sgdArgs = SGD.SgdArgs
        { SGD.batchSize = batchSize
        , SGD.regVar = regVar
        , SGD.iterNum = iterNum
        , SGD.gain0 = gain0
        , SGD.tau = tau }


exec NER{..} = case format of
    Text -> do
        nerf <- decodeFile inModel
        inp  <- L.lines <$> L.getContents
        forM_ inp $ \sent -> do
            let forest = ner nerf (L.unpack sent)
            L.putStrLn (showForest forest)
    XCES -> do
        nerf <- decodeFile inModel
        L.putStrLn . XCES.nerXCES (ner nerf) =<< L.getContents


exec Server{..} = do
    putStr "Loading model..." >> hFlush stdout
    nerf <- decodeFile inModel
    nerf `seq` putStrLn " done"
    let portNum = N.PortNumber $ fromIntegral port
    putStrLn $ "Listening on port " ++ show port
    S.runNerfServer nerf portNum


exec Client{..} = case format of
    Text -> do
        inp  <- L.lines <$> L.getContents
        forM_ inp $ \sent -> do
            forest <- S.ner host portNum $ L.unpack sent
            L.putStrLn (showForest forest)
    XCES -> do
        let nerRemote = unsafePerformIO . S.ner host portNum
        L.putStrLn . XCES.nerXCES nerRemote =<< L.getContents
  where
     portNum = N.PortNumber $ fromIntegral port


exec nerfArgs@Ox{..} = do
    Resources{..} <- extract nerfArgs
    cfg <- defaultConf
        (catMaybes [poliDict, prolexDict, pnegDict, neLexDict])
        intDict extDict
    tryOx cfg dataPath


exec Compare{..} = do
    x <- parseEnamex <$> L.readFile dataPath
    y <- parseEnamex <$> L.readFile dataPath'
    let statMap = C.compare $ zip x y
    forM_ (M.toList statMap) $ uncurry printStats
    printStats "<all>" (foldl1 (.+.) $ M.elems statMap)
  where
    printStats neType stats = do
        putStrLn $ "# " ++ T.unpack neType
        putStrLn $ "true positive: "    ++ show (C.tp stats)
        putStrLn $ "false positive: "   ++ show (C.fp stats)
        -- putStrLn $ "true negative: "    ++ show (C.tn stats)
        putStrLn $ "false negative: "   ++ show (C.fn stats)


-- readRaw :: FilePath -> IO [L.Text]
-- readRaw = fmap L.lines . L.readFile


----------------------------------------
-- Cross-validation
----------------------------------------


-- | Get paths of the individual parts of the dataset
-- stored in the given directory.
getParts :: FilePath -> IO [FilePath]
getParts path = do
    xs <- filter (\x -> not (x `elem` [".", ".."]))
      <$> getDirectoryContents path
    return $ map (path </>) xs


-- | Take data from the given list of paths and store
-- it all in a temporary file, than run the given handler.
withParts :: [FilePath] -> (FilePath -> IO a) -> IO a
withParts paths handler = Temp.withSystemTempFile "train." $ \tempPath _h -> do
    hClose _h
    forM_ paths $ \srcPath -> do
        L.readFile srcPath >>= L.appendFile tempPath
    handler tempPath


-- | Enumerate subsequent partitionings of the dataset.
enumDivs :: [a] -> [(a, [a])]
enumDivs []     = []
enumDivs (x:xs) = (x, xs) : map (second (x:)) (enumDivs xs)