packages feed

fei-nn-0.2.0: src/MXNet/NN.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE FlexibleContexts #-}
module MXNet.NN (
    Parameter(..),
    Config(..),
    Session(..),
    Exc(..),
    Initializer,
    TrainM,
    CallbackClass(..), Callback(..),
    train,
    initialize,
    fit, fit_, fitAndEval, fitDataset,
    forwardOnly,
    getContext,
    sess_param, sess_context, sess_callbacks,
    module MXNet.NN.Optimizer,
    module MXNet.NN.LrScheduler,
    module MXNet.NN.EvalMetric,
    module MXNet.NN.Initializer,
    module MXNet.NN.Layer,
    module MXNet.NN.Callback,
) where

import qualified Data.HashMap.Strict as M
import qualified Control.Monad.State.Strict as ST
import Data.Maybe (isJust, fromJust, maybe)
import Data.Foldable (forM_)
import Control.Monad (when, unless, void)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Trans.Resource (MonadThrow(..))
import Control.Lens (traverseOf, use, (+=), (^.), (%=))
import System.IO (hFlush, stdout)
import System.Mem
import Text.Printf
import Data.Dynamic (toDyn)
-- import Control.Lens.Tuple

import MXNet.Base
import qualified MXNet.Base.Operators.NDArray as A

import MXNet.NN.NDArray
import MXNet.NN.Types
import MXNet.NN.Optimizer
import MXNet.NN.EvalMetric
import MXNet.NN.Layer
import MXNet.NN.Initializer
import MXNet.NN.LrScheduler
import MXNet.NN.DataIter.Class
import MXNet.NN.Callback

-- | Execute the 'TrainM' monad
train :: (DType a, Monad m) => Session a -> TrainM a m r -> m r
train sess proc = ST.evalStateT (ST.evalStateT proc sess) (Statistics 0 0)


-- | infer the shapes of input and auxiliary symbols in a symbolic neural network
inferShape' :: DType a => Symbol a -> M.HashMap String [Int] -> IO (M.HashMap String [Int], M.HashMap String [Int])
inferShape' sym known = do
    (args, _, auxs, complete) <- inferShape sym (M.toList known)
    unless complete $ throwM InferredShapeInComplete
    return (M.fromList args, M.fromList auxs)

-- | initialize all parameters
initialize :: DType a => Symbol a -> Config a -> IO (Session a)
initialize sym config = do
    -- give a initial batch_size = 1 for the placeholders
    let spec1 = M.map (1:) $ M.difference input_shapes initializers
        spec2 = initializers
        dinit = config ^. cfg_default_initializer
        cxt   = config ^. cfg_context
    (arg_with_shp, aux_with_shp) <- inferShape' sym spec1
    ---------------------
    -- important! labels should be merged into placeholders,
    -- otherwise the labels are considered to have gradient.
    ---------------------
    let lbl_with_shp = M.filterWithKey (\k v -> k `elem` label_names) arg_with_shp
    placeholders <- mapM (flip makeEmptyNDArray cxt) $ M.union spec1 lbl_with_shp

    arg_tensors <- M.traverseWithKey (initI placeholders spec2 dinit) arg_with_shp
    aux_tensors <- M.traverseWithKey (initA dinit) aux_with_shp

    return $ Session {
        _sess_symbol  = sym,
        _sess_data    = input_shapes,
        _sess_label   = label_names,
        _sess_param   = arg_tensors `M.union` aux_tensors,
        _sess_context = cxt,
        _sess_callbacks = [],
        _sess_store   = M.empty
    }
  where
    input_shapes = config ^. cfg_data
    label_names  = config ^. cfg_label
    initializers = config ^. cfg_initializers
    -- initialize input symbols.
    -- placeholders are backed by empty NDArray,
    -- other input symbols are initialized by an initializer.
    initI placeholder spec2 dinit inp shp =
        case M.lookup inp placeholder of
            Just in_arg -> do
                return $ ParameterI in_arg Nothing
            Nothing -> do
                arg_in <- case M.lookup inp spec2 of
                    Just cinit -> cinit inp shp (_cfg_context config)
                    Nothing    -> dinit inp shp (_cfg_context config)
                arg_gr <- makeEmptyNDArray shp (_cfg_context config)
                return $ ParameterI arg_in (Just arg_gr)
    -- initialize auxiliary symbols.
    initA dinit aux shp = do
        arg_aux <- dinit aux shp (_cfg_context config)
        return $ ParameterA arg_aux

-- | bind the symbolic network with actual parameters
bind :: (DType a, MonadIO m, MonadThrow m) => M.HashMap String (Maybe (NDArray a)) -> Bool -> TrainM a m (Executor a)
bind dat train_ = do
    Context{..} <- use sess_context
    net <- use sess_symbol

    let inputs = M.map fromJust $ M.filter isJust dat
    input_shapes <- liftIO $ mapM ndshape inputs
    (inp_shps, aux_shps) <- liftIO $ inferShape' net input_shapes
    modifyT . traverseOf sess_param $ M.traverseWithKey $ \k p ->
        case p of
          ParameterI {} -> do
            let ishp = inp_shps M.! k
            case M.lookup k dat of
                -- if the name is given in the binding data, we check its consistency.
                Just a  -> liftIO $ ensure_consistency (maybe (Right ishp) Left a) p
                -- if the name is missing in the binding data, we check the infered shape
                -- matches both the _param_in and _param_grad
                Nothing -> do
                    pshp1 <- liftIO $ ndshape (_param_in p)
                    when (ishp /= pshp1 ) (throwM $ MismatchedShapeOfSym (k ++ "[i]") ishp pshp1)
                    case (train_, _param_grad p) of
                        (True, Just ndarray) -> do
                            pshp2 <- liftIO $ ndshape ndarray
                            when (ishp /= pshp2) (throwM $ MismatchedShapeOfSym (k ++ "[t]") ishp pshp2)
                        _ -> return ()
                    return p
          ParameterA {} -> do
            let ishp = aux_shps M.! k
            pshp1 <- liftIO $ ndshape (_param_aux p)
            when (ishp /= pshp1 ) (throwM $ MismatchedShapeOfSym (k ++ "[i]") ishp pshp1)
            return p

    args <- use sess_param
    exec_handle <- liftIO $ do
        names <- mxSymbolListArguments (unSymbol net)
        -- the parameters to bind should be arranged in the same order as the names
        let num_args = length names
            arg_in  = map (unNDArray . _param_in) $ map (args M.!) names
            arg_gr  = if train_
                        then map (fmap unNDArray . _param_grad) $ map (args M.!) names
                        else replicate num_args Nothing
            arg_gr_req = replicate num_args (if train_ then 1 else 0)

        auxnames <- mxSymbolListAuxiliaryStates (unSymbol net)
        let aux_arg_aux = map (unNDArray . _param_aux) $ map (args M.!) auxnames
        mxExecutorBind (unSymbol net) _device_type _device_id
                        arg_in arg_gr arg_gr_req
                        aux_arg_aux
    return $ Executor exec_handle
  where
    -- make sure the _param_in can be used in the inference and backpropagation
    -- + user data input can be in a different context w.r.t. session configuration
    --   + copy inp with the right context
    -- + batch size can be different from the initial configuration, or at the time
    --   to swap training and inference
    --   + create one and copy it
    -- + for inferenceOnly, labels' NDArray can be uninitialized.
    --   + just create one
    ensure_consistency :: DType a => Either (NDArray a) [Int] -> Parameter a -> IO (Parameter a)
    ensure_consistency (Left a) p = do
        src_cxt <- context a
        src_shp <- ndshape a
        dst_cxt <- context (_param_in p)
        dst_shp <- ndshape (_param_in p)
        case (src_cxt == dst_cxt, src_shp == dst_shp) of
            (True , True) -> return $ p {_param_in = a}
            (False, True) -> do
                A._copyto_upd [unNDArray (_param_in p)] (#data := unNDArray a .& Nil)
                return p
            _ -> do
                a_copy <- makeEmptyNDArray src_shp dst_cxt
                A._copyto_upd [unNDArray a_copy] (#data := unNDArray a .& Nil)
                return $! p {_param_in = a_copy}
    ensure_consistency (Right src_shp) p = do
        dst_cxt <- context (_param_in p)
        dst_shp <- ndshape (_param_in p)
        if src_shp == dst_shp
            then return p
            else do
                dummy <- makeEmptyNDArray src_shp dst_cxt
                return $! p {_param_in = dummy}

-- | single step train. Must provide all the placeholders.
fit :: (DType a, MonadIO m, MonadThrow m, Optimizer opt)
    => opt a -> M.HashMap String (NDArray a) -> TrainM a m (Executor a)
fit opt datAndLbl = do
    exec <- bind (M.map Just datAndLbl) True
    liftIO $ do
        mxExecutorForward (unExecutor exec) True
        mxExecutorBackward (unExecutor exec) []
    -- forward/backward are asynchronised operation in mxnet, in a
    -- sense that only opcodes are pushed onto an internal execution
    -- stack, and there is a executor running in a separate thread.
    -- It is possible that an OOM of CPU memory occurs, if 'fit' are
    -- called so fast that too many opcodes and data on the stack,
    -- as described in issue #1
    updateParameters opt datAndLbl
    return exec

-- | single step train. Must provide all the placeholders.
fit_ :: (DType a, MonadIO m, MonadThrow m, Optimizer opt)
     => opt a -> M.HashMap String (NDArray a) -> TrainM a m ()
fit_ opt datAndLbl = void $ fit opt datAndLbl

-- | single step train. Must provide all the placeholders.
--   After fitting, it also update the evaluation metric.
fitAndEval :: (DType a, MonadIO m, MonadThrow m, Optimizer opt, EvalMetricMethod mtr)
           => opt a -> M.HashMap String (NDArray a) -> MetricData mtr a -> TrainM a m ()
fitAndEval opt datAndLbl metric = do
    Executor exec  <- fit opt datAndLbl
    pred <- liftIO $ map NDArray <$> mxExecutorOutputs exec
    eval_results <- evaluate metric datAndLbl pred
    sess_store %= M.union (M.map toDyn eval_results)

fitDataset :: (Dataset d, DatasetProp d e, DType a,
        MonadIO m, MonadThrow m, DatasetConstraint d (TrainM a m),
        Optimizer opt, EvalMetricMethod mtr)
    => d e
    -> d e
    -> ([String] -> e -> M.HashMap String (NDArray a))
    -> opt a
    -> mtr a
    -> Int
    -> TrainM a m ()
fitDataset trainDataset valDataset make_binding opt metric epochs = do
    callbacks <- use sess_callbacks

    data_vars <- M.keys <$> use sess_data
    labl_vars <- use sess_label

    total     <- sizeD trainDataset
    batchSize <- batchSizeD trainDataset >>= maybe (throwM DatasetOfUnknownBatchSize) return

    liftIO $ putStrLn $ "[Train]"
    forM_ (enumFromTo 1 epochs) $ \epochInd -> do
        trainMetricData <- newMetric "train" metric

        liftIO $ putStrLn $ "epoch " ++ show epochInd
        forM_ callbacks (begOfEpoch epochInd total)

        void $ forEachD_i trainDataset $ \(i, item) -> do
            forM_ callbacks (begOfBatch i batchSize)
            let binding = make_binding (data_vars ++ labl_vars) item
            fitAndEval opt binding trainMetricData
            eval <- format trainMetricData
            liftIO $ putStr $ printf "\r\ESC[K%d/%d %s" i total eval
            forM_ callbacks (endOfBatch i batchSize)
            liftIO $ hFlush stdout

        forM_ callbacks (endOfEpoch epochInd total)
        liftIO $ hFlush stdout
        liftIO performGC

        liftIO $ putStrLn "\n[Validate]"
        valMetricData <- newMetric "val" metric
        void $ forEachD valDataset $ \item -> do
            let whole_binding = make_binding (data_vars ++ labl_vars) item
                infer_binding = M.map Just $ M.filterWithKey (const . (`elem` data_vars)) whole_binding
            pred <- forwardOnly infer_binding
            evaluate valMetricData whole_binding pred
        eval <- format valMetricData
        liftIO $ putStrLn eval

        forM_ callbacks (endOfVal epochInd total)
        liftIO $ putStrLn ""

fitDataset_ :: (Dataset d, DatasetProp d e, DType a,
               MonadIO m, MonadThrow m, DatasetConstraint d (TrainM a m),
               Optimizer opt, EvalMetricMethod mtr)
    => d e
    -> ([String] -> e -> M.HashMap String (NDArray a))
    -> opt a
    -> MetricData mtr a
    -> TrainM a m ()
fitDataset_ dataset make_binding opt metric = do
    callbacks <- use sess_callbacks
    total     <- sizeD dataset
    batchSize <- batchSizeD dataset >>= maybe (throwM DatasetOfUnknownBatchSize) return

    data_vars <- M.keys <$> use sess_data
    labl_vars <- use sess_label

    void $ forEachD_i dataset $ \(i, item) -> do
        forM_ callbacks (begOfBatch i batchSize)
        let binding = make_binding (data_vars ++ labl_vars) item
        fitAndEval opt binding metric
        eval <- format metric
        liftIO $ putStr $ printf "\r\ESC[K%d/%d %s" i total eval
        forM_ callbacks (endOfBatch i batchSize)
        liftIO $ hFlush stdout
        liftIO performGC

    liftIO $ hFlush stdout
-- fitDataset :: (Dataset d, DType a, DataItem e a,
--                MonadIO m, MonadThrow m, DatasetConstraint d (TrainM a m),
--                Optimizer opt, EvalMetricMethod mtr)
--     => opt a -> Symbol a
--     -> [String]
--     -> d e
--     -> mtr a
--     -> TrainM a m ()
-- fitDataset opt net varnames dataset metric = do
--     callbacks <- use sess_callbacks
--     shapes <- use sess_placeholders
--     total <- sizeD dataset
--     [example0] <- takeD 1 dataset
--     batchSize <- batchSizeD example0
--     -- assuming the data shape axis-0 is the batch-size
--     Executor exec <- bind' net (M.map (\shp -> batchSize:tail shp) shapes) True

--     forM_ callbacks (begOfEpoch total)
--     void $ forEachD_i dataset $ \(i, e) -> do
--         forM_ callbacks (begOfBatch i batchSize)

--         t1 <- liftIO getCurrentTime

--         placeHolders <- makePlaceholderMapD e varnames
--         setPlaceholders placeHolders

--         t2 <- liftIO getCurrentTime
--         sess_prof . _1 += diffUTCTime t2 t1

--         liftIO $ do
--             mxExecutorForward exec True
--             mxExecutorBackward exec []

--         t3 <- liftIO getCurrentTime
--         sess_prof . _2 += diffUTCTime t3 t2

--         updateParameters opt placeHolders

--         t4 <- liftIO getCurrentTime
--         sess_prof . _3 += diffUTCTime t4 t3

--         preds <- liftIO $ map NDArray <$> mxExecutorOutputs exec

--         t5 <- liftIO getCurrentTime
--         sess_prof . _4 += diffUTCTime t5 t4

--         evaluate metric placeHolders preds
--         eval <- format metric

--         t6 <- liftIO getCurrentTime
--         sess_prof . _5 += diffUTCTime t6 t5

--         liftIO $ putStr $ "\r\ESC[K" ++ show i ++ "/" ++ show total ++ " " ++ eval
--         forM_ callbacks (endOfBatch i batchSize)
--         liftIO $ hFlush stdout
--         liftIO performGC

--         t7 <- liftIO getCurrentTime
--         sess_prof . _6 += diffUTCTime t7 t6
--     forM_ callbacks (endOfEpoch total)
--     liftIO $ hFlush stdout

updateParameters :: (MonadIO m, Optimizer opt, DType dtype)
                 => opt dtype -> M.HashMap String any -> TrainM dtype m ()
updateParameters opt blacklist = do
    params <- use sess_param
    forM_ (M.toList params) $ \ (k, v) ->
        case (v, M.member k blacklist, _param_grad v) of
          (ParameterI {}, False, Just grad) -> ST.lift $ optimize opt k (_param_in v) grad
          _ -> return ()
    ST.lift (stat_num_upd += 1)
    -- waitParams

-- | forward only. Must provide all the placeholders, setting the data to @Just xx@, and set label to @Nothing@.
--
-- Note that the batch size here can be different from that in the training phase.
forwardOnly :: (DType a, MonadIO m, MonadThrow m) => M.HashMap String (Maybe (NDArray a)) -> TrainM a m [NDArray a]
forwardOnly dat = do
    Executor exec <- bind dat False
    liftIO $ mxExecutorForward exec False
    liftIO $ map NDArray <$> mxExecutorOutputs exec

waitParams :: (MonadIO m, DType a) => TrainM a m ()
waitParams = do
    params <- use sess_param
    forM_ params (\param ->
        case param of
            ParameterA arr1 ->
                wait arr1
            ParameterI arr1 Nothing ->
                wait arr1
            ParameterI arr1 (Just arr2) -> do
                wait arr1
                wait arr2
        )

wait :: (MonadIO m, DType a) => NDArray a -> TrainM a m ()
wait (NDArray hdl) = liftIO $ mxNDArrayWaitToRead hdl

getContext :: Monad m => TrainM a m Context
getContext = use sess_context

-- | modify the state within the inner monad
--
-- thanks to lens, we can modify the first field of the state with following
-- combinator:
--
-- modifyT . traverseOf _1
--  :: (Field1 s s a b, Monad m) => (a -> m b) -> StateT s m ()
modifyT :: Monad m => (s -> m s) -> ST.StateT s m ()
modifyT func = do
    s0 <- ST.get
    s1 <- ST.lift $ func s0
    ST.put s1