packages feed

sequor-0.7.5: src/NLP/Perceptron/Sequence.hs

{-# LANGUAGE NoMonomorphismRestriction 
  , BangPatterns
  , FlexibleInstances
 #-}
module NLP.Perceptron.Sequence
    (
      Model(..)
    , Trace
    , Options(..)
    , YMap
    , train
    , decode
    )
where

import qualified Data.Array.Unsafe as AU
import Data.Array.ST
import Data.Array.Unboxed
import qualified Data.Array as A
import qualified Data.Vector.Unboxed as V
import Control.Monad.ST 
import qualified Control.Monad.ST.Lazy   as LST
import qualified Control.Monad.ST.Unsafe as ST.Unsafe
import Control.Monad.Writer       
import Data.STRef
import Control.Monad
import qualified Data.Map as Map
import qualified Data.IntMap as IntMap
import qualified Data.IntSet as IntSet
import NLP.Perceptron.Vector
import System.IO
import Debug.Trace
--import NLP.Perceptron.Config 
import Data.List (inits,foldl',sortBy)
import Data.Ord (comparing)
import Helper.ListZipper 
import qualified Data.Binary as Binary
import Helper.Utils (uniq)
import qualified NLP.Scores as Scores
import Text.Printf

data Model = Model { options :: Options 
                   , weights :: UArray I Float }
type X = [Xi]
type Y = [Yi]
type Xi = V.Vector Xii
type Xii = Int
type Yi = Int
type Dot = Local -> Float

data Options = Options { oYMap       :: YMap
                       , oIndexSet   :: IntSet.IntSet
                       , oYDict      :: IntMap.IntMap [Yi]
                       , oYs         :: [Yi]
                       , oBeam       :: !Int 
                       , oRate       :: !Float
                       , oRateDecay  :: !Float
                       , oEpochs     :: !Int 
                       , oFeatBounds     :: Maybe (Int,Int)
                       , oStopWinSize    :: !Int
                       , oStopThreshold  :: !Double
                       } deriving Eq

type YMap = (Xi,A.Array Yi Xi,A.Array (Yi,Yi) Xi)

instance Binary.Binary (V.Vector Int) where
    put v = Binary.put $ V.toList v
    get = V.fromList `fmap` Binary.get
         
instance Binary.Binary Model where
    put m = do 
      Binary.put (options m)
      -- Binary.put (weights m)
      let (lo,hi) = bounds . weights $ m
          xs = filter (\(_,e) -> e /= 0.0) . assocs . weights $ m
      Binary.put (lo,hi)
      Binary.put xs

    get = {-# SCC "get1" #-} do 
      os <- Binary.get 
      os == os `seq` return ()
      ws <- do
        (lo,hi) <- Binary.get
        xs <- Binary.get
        xs == xs `seq` return ()
        return $ accumArray (+) 0 (lo,hi) $ xs
      ws == ws `seq` return ()
      return $ Model os ws

instance Binary.Binary Options where
    put (Options a b c d e f g h i j k) = 
        Binary.put a >> Binary.put b >> Binary.put c 
       >> Binary.put d >>  Binary.put e >> Binary.put f
       >> Binary.put g >> Binary.put h >> Binary.put i 
       >> Binary.put j >> Binary.put k
    get = {-# SCC "get2" #-} do
      a <- Binary.get
      a == a `seq` return ()
      b <- Binary.get
      b == b `seq` return ()
      c <- Binary.get 
      c == c `seq` return ()
      d <- Binary.get 
      d == d `seq` return ()
      e <- Binary.get
      e == e `seq` return ()
      f <- Binary.get
      f == f `seq` return ()
      g <- Binary.get
      g == g `seq` return ()
      h <- Binary.get
      h == h `seq` return ()
      i <- Binary.get
      i == i `seq` return ()
      j <- Binary.get
      j == j `seq` return ()
      k <- Binary.get
      k == k `seq` return ()
      return $ Options a b c d e f g h i j k

yDictFind :: Options -> Xi -> [Yi]
yDictFind opts fs = 
    let mk = V.find (`IntSet.member` oIndexSet opts) $ fs
        def = oYs opts
    in case mk of
         Just k -> IntMap.findWithDefault def k . oYDict $ opts
         Nothing -> def

-- | DECODING 
decode :: Model -> X -> Y
decode m = fst . decode' (options m) (weights m `dot`) 

data Cell = Cell { cScore :: !Float
                 , cPhi   :: Global
                 , cPath  :: Y
                 , cStep  :: ListZipper Xi  } deriving (Show,Eq)

decode' :: Options -> Dot -> X -> (Y,Global)
decode' opts w x = 
  bestPath opts w [Cell { cScore = 0 
                        , cPhi = Map.empty
                        , cPath = []
                        , cStep = fromList x } ]


phi :: Options -> X -> Y -> Global
phi opts x y = foldl' f Map.empty . zip x . map reverse . tail . inits $ y
    where f z (xi,ys) = z `plus` toSV  (features (oYMap opts) xi ys)

{-# INLINE features #-}          
features :: YMap -> Xi -> [Yi] -> Local
features (!zero,uni,bi) xi (y:ys) = 
    case ys of
      []            -> (Local y $ zero V.++  xi)
      [y1]          -> (Local y $ uni A.! y1 V.++ xi)
      (y1 : y2 : _) -> let r = bi A.! (y1,y2) 
                       in  if V.null r 
                           then  (Local y $ uni A.! y1  V.++ xi)
                           else  (Local y $ r           V.++ xi)

beamSearch ::  Options
           -> Dot
           -> [Cell] 
           -> [Cell]
beamSearch opts w cs = 
    let f cs = if any (atEnd . cStep) cs then cs 
               else 
                   let cs' =   [    let fs =  features (oYMap opts) xi (y':ys)
                                    in Cell { cScore = 
                                                  s + w fs 
                                            , cPhi = ph `plus`  (toSV fs)
                                            , cPath = (y':ys)
                                            , cStep = next x } 
                                        | Cell { cScore = s 
                                               , cPhi = ph 
                                               , cPath = ys 
                                               , cStep = x } <- cs 
                               , let Just xi = focus x
                               , y' <- yDictFind opts xi
                               ]
                   in f . take (oBeam opts) 
                        . sortBy  (flip $ comparing cScore) 
                        $ cs'
    in f cs 

bestPath :: Options
            -> Dot
            -> [Cell]
            -> (Y, Global)
bestPath opts w xs = 
  let xs' =  beamSearch opts w xs
      first =  (\(x:_) -> x) xs'
  in ( reverse . cPath $ first
          , cPhi first )

-- | TRAINING

iter ::    Options 
        -> Int
        -> [(X,Y)]
        -> (STRef s Int, WeightsST s, WeightsST s)
        -> ST s ()
iter opts i ss (c,params,params_a) = do
    for_ ss $ \ (x,y) -> do
      params' <- AU.unsafeFreeze params
      let (y',phi_xy') = decode' opts (params'`dot`) x
      when (y' /= y) $ do 
        let phi_xy = phi opts x y 
            update = (phi_xy `minus` phi_xy') 
                          `scale` (oRate opts * fromIntegral i ** (- oRateDecay opts))
        params `plus_` update
        c' <- readSTRef c
        params_a `plus_` (update `scale` fromIntegral c')
      modifySTRef c (+1)

type Trace = [(Double, Double, Double)]
  
train :: Options -> [(X, Y)] -> [(X,Y)] -> (Model, Trace)
train opts heldout ss = LST.runST (runWriterT (run opts heldout ss))
              
run :: Options -> [(X, Y)] -> [(X,Y)] -> WriterT Trace (LST.ST s) Model
run opts heldout ss = do
  let bs = computeBounds opts ss
  --trace ("Param vector bounds: " ++ show bs) () `seq` return ()
  params <- st $ newArray bs 0
  params_a <- st $ newArray bs 0
  c <- st $ newSTRef 1
  erref <- st $ newSTRef []
  let loop i = do
        st $ iter opts i ss (c, params, params_a)
        c' <- st $ readSTRef c
        params' <- st $ AU.unsafeFreeze params
        params_a' <- st $ AU.unsafeFreeze params_a
        let w = (fromIntegral c', params', params_a')
            pred xys = [ fst . decode' opts (w `dot'`) $ x 
                     | (x,_) <- xys ]
            err_train = Scores.errorRate (concatMap snd ss) (concat $ pred ss)  
            err_dev =   Scores.errorRate (concatMap snd heldout) (concat $ pred heldout)
        errs <- st $ readSTRef erref    
        let errs' = (err_train, err_dev):errs
        st $ writeSTRef erref errs' 
        let ch = change (oStopWinSize opts) errs'
        tell [(err_train, err_dev, ch)]
        when (continue opts i ch) $ loop (i+1)
  loop 1      
  st $ finalParams (c, params,  params_a)
  arr <- st $ AU.unsafeFreeze params
  return $! Model { options = opts , weights = arr }
       
st :: Monoid w => ST s a -> WriterT w (LST.ST s) a
st = lift . LST.strictToLazyST

change :: Int -> [(Double, Double)] -> Double
change winsize errs =
  let mi = minimum . take winsize . map snd $ errs
      ma = maximum . take winsize . map snd $ errs
  in (ma - mi)/ma 
  
continue :: Options -> Int -> Double -> Bool
continue opts i n | i >= oEpochs opts    = False
                  | i < winsize          = True                         
                  | isNaN n              = True                  
                  | True                 = n > threshold                         
  where threshold = oStopThreshold opts
        winsize = oStopWinSize opts
        
finalParams :: (STRef s Int, WeightsST s, WeightsST s) 
            -> ST s ()
finalParams (c,params,params_a) = do
  (l,u) <- getBounds params
  c' <- fmap fromIntegral (readSTRef c)
  for_ (range (l,u)) $ \i -> do
      e   <- readArray params   i
      e_a <- readArray params_a i
      writeArray params i (e - (e_a * (1/c')))

computeBounds :: Options -> [(X,Y)] -> (I,I)
computeBounds opts xys =   
    let ((yl,xl),(yh,xh)) = foldl' f ((maxBound,minimum xis)
                               ,(minBound,maximum xis)) 
                . (\(xs,ys) -> zip (concat xs) (concat ys))
                . unzip
                $ xys
    in case oFeatBounds opts of
         Just (xl',xh') -> (I yl xl',I yh xh')
         Nothing        -> (I yl xl,I yh xh)
    where f ((!miny,!minx),(!maxy,!maxx)) (xs,!y) =
              ((min miny y,V.minimum $ minx`V.cons`xs)
              ,(max maxy y,V.maximum $ maxx`V.cons`xs))
          xis = let (zero,uni,bi) = oYMap opts
                in      uniq
                      . concatMap V.toList
                      $
                      [zero]
                      ++
                      (filter (not . V.null)
                        . A.elems
                        $ bi)
                      ++
                      (filter (not . V.null) 
                        . A.elems 
                        $ uni)