packages feed

sgd 0.6.0.0 → 0.7.0.0

raw patch · 3 files changed

+209/−36 lines, 3 filesdep +paralleldep +random-shufflePVP ok

version bump matches the API change (PVP)

Dependencies added: parallel, random-shuffle

API changes (from Hackage documentation)

- Numeric.SGD: every :: Monad m => Int -> (p -> m ()) -> Pipe p p m x
+ Numeric.SGD: [batchOverlap] :: Config -> Natural
+ Numeric.SGD: [batchSize] :: Config -> Natural
+ Numeric.SGD: batch :: Monad m => Int -> Pipe e [e] m ()
+ Numeric.SGD: batchGradPar :: (ParamSet p, NFData p) => (e -> p -> p) -> [e] -> p -> p
+ Numeric.SGD: batchGradPar' :: ParamSet p => (e -> p -> p) -> [e] -> p -> p
+ Numeric.SGD: batchGradSeq :: ParamSet p => (e -> p -> p) -> [e] -> p -> p
+ Numeric.SGD: decreasingBy :: (Monad m, Ord a) => (p -> m a) -> Pipe p p m x
+ Numeric.SGD: keepEvery :: Monad m => Int -> Pipe a a m x
+ Numeric.SGD.DataSet: shuffle :: DataSet a -> IO (DataSet a)
- Numeric.SGD: Config :: Natural -> Bool -> Double -> Config
+ Numeric.SGD: Config :: Natural -> Natural -> Natural -> Bool -> Double -> Config
- Numeric.SGD: runIO :: ParamSet p => Config -> SGD IO e p -> (e -> p -> Double) -> DataSet e -> p -> IO p
+ Numeric.SGD: runIO :: ParamSet p => Config -> SGD IO [e] p -> (e -> p -> Double) -> DataSet e -> p -> IO p

Files

sgd.cabal view
@@ -1,5 +1,5 @@ name:               sgd-version:            0.6.0.0+version:            0.7.0.0 synopsis:           Stochastic gradient descent description:     Stochastic gradient descent library.@@ -25,6 +25,7 @@       , pipes           >= 4.3      && < 4.4       , vector          >= 0.10     && < 0.13       , random          >= 1.0      && < 1.2+      , random-shuffle  >= 0.0.4    && < 0.1       , primitive       >= 0.5      && < 0.7       , logfloat        >= 0.12     && < 0.14       , monad-par       >= 0.3.4    && < 0.4@@ -36,6 +37,7 @@       , temporary       >= 1.1      && < 1.4       , hmatrix         >= 0.19     && < 0.20       , data-default    >= 0.7      && < 0.8+      , parallel        >= 3.2      && < 3.3      exposed-modules:         Numeric.SGD
src/Numeric/SGD.hs view
@@ -61,10 +61,19 @@   , runIO    -- * Combinators+  -- ** Input   , pipeSeq   , pipeRan+  -- ** Batch+  , batch+  , batchGradSeq+  , batchGradPar+  , batchGradPar'+  -- ** Output   , result-  , every+  -- ** Misc+  , keepEvery+  , decreasingBy    -- * Re-exports   , def@@ -72,13 +81,19 @@   import           GHC.Generics (Generic)+-- import           GHC.Conc (numCapabilities) import           Numeric.Natural (Natural) -import qualified System.Random as R+-- import qualified System.Random as R -import           Control.Monad (when, forM_)+import           Control.Monad (when, forM_, forever)+import           Control.Parallel.Strategies (parMap, rseq, rdeepseq, Strategy)+import           Control.DeepSeq (NFData)+import qualified Control.Monad.State.Strict as State  import           Data.Functor.Identity (Identity(..))+import           Data.List (foldl1') -- , transpose)+ import qualified Data.IORef as IO import           Data.Default @@ -127,6 +142,10 @@ data Config = Config   { iterNum :: Natural     -- ^ Number of iteration over the entire training dataset+  , batchSize :: Natural+    -- ^ Mini-batch size+  , batchOverlap :: Natural+    -- ^ The number of overlapping elements in subsequent mini-batches   , batchRandom :: Bool     -- ^ Should the mini-batch be selected at random?  If not, the subsequent     -- training elements will be picked sequentially.  Random selection gives@@ -139,6 +158,8 @@ instance Default Config where   def = Config     { iterNum = 100+    , batchSize = 1+    , batchOverlap = 0     , batchRandom = False     , reportEvery = 1.0     }@@ -149,16 +170,16 @@ -- should be convenient to use when the training dataset is large. -- -- An alternative is to use the simpler function `run`, or to build a custom--- SGD pipeline based on lower-level combinators (`pipeSeq`, `Ada.adaDelta`,--- `every`, `result`, etc.).+-- SGD pipeline based on lower-level combinators (`pipeSeq`, `batch`,+-- `Adam.adam`, `keepEvery`, `result`, etc.). runIO   :: (ParamSet p)   => Config     -- ^ SGD configuration-  -> SGD IO e p-    -- ^ Selected SGD method+  -> SGD IO [e] p+    -- ^ SGD pipe consuming mini-batches of dataset elements   -> (e -> p -> Double)-    -- ^ Value of the objective function on a sample element (needed for model+    -- ^ Value of the objective function on a dataset element (used for model     -- quality reporting)   -> DataSet e     -- ^ Training dataset@@ -166,21 +187,40 @@     -- ^ Initial parameter values   -> IO p runIO Config{..} sgd quality0 dataSet net0 = do-  report net0-  result net0 $ pipeSeq dataSet+  _ <- report net0+  result net0 $ pipeData dataSet+    >-> batch (fromIntegral batchSize)+    >-> batchFilter     >-> sgd net0-    >-> P.take realIterNum-    >-> every realReportPeriod report+    >-> keepEvery realReportPeriod+    >-> P.take (fromIntegral iterNum)+    >-> decreasingBy report   where-    -- Iteration scaling-    iterScale x = fromIntegral (size dataSet) * x+    -- Data streaming function+    pipeData = forever .+      if batchRandom+         then pipeRan+         else pipeSeq+    -- Number of new elements in each subsequent batch+    batchNew = max 1+      ( fromIntegral batchSize+      - fromIntegral batchOverlap )+    -- Batch stream filter+    batchFilter = do+      P.await >>= P.yield+      keepEvery batchNew+    -- Iteration (epoch) scaling+    iterScale x = fromIntegral (size dataSet) * x / fromIntegral batchNew     -- Number of iterations and reporting period-    realIterNum = ceiling $ iterScale (fromIntegral iterNum :: Double)+    -- realIterNum = ceiling $ iterScale (fromIntegral iterNum :: Double)+    -- realReportPeriod = floor $ iterScale reportEvery     realReportPeriod = ceiling $ iterScale reportEvery     -- Network quality over the entire training dataset     report net = do-      putStr . show =<< quality net+      q <- quality net+      putStr $ show q       putStrLn $ " (norm_2 = " ++ show (norm_2 net) ++ ")"+      return q     quality net = do       res <- IO.newIORef 0.0       forM_ [0 .. size dataSet - 1] $ \ix -> do@@ -194,29 +234,82 @@ -------------------------------  --- | Pipe the dataset sequentially in a loop.+-- | Pipe all the elements in the dataset sequentially. pipeSeq :: DataSet e -> P.Producer e IO () pipeSeq dataSet = do   go (0 :: Int)   where     go k-      | k >= size dataSet = go 0+      | k >= size dataSet = return ()       | otherwise = do           x <- P.lift $ elemAt dataSet k           P.yield x           go (k+1)  --- | Pipe the dataset randomly in a loop.+-- | Pipe all the elements in the dataset in a random order. pipeRan :: DataSet e -> P.Producer e IO ()-pipeRan dataSet = do-  x <- P.lift $ do-    ix <- R.randomRIO (0, size dataSet - 1)-    elemAt dataSet ix-  P.yield x-  pipeRan dataSet+pipeRan dataSet0 = do+  dataSet <- P.lift $ shuffle dataSet0+  pipeSeq dataSet  +-- | Group dataset elements into (mini-)batches of the given size.+batch :: (Monad m) => Int -> P.Pipe e [e] m ()+batch k = flip State.evalStateT [] . forever $ do+  x <- P.lift P.await+  xs <- State.get+  let xs' = take k (x:xs)+  when (length xs' == k) $ do+    P.lift (P.yield xs')+  State.put xs'+++-- | Adapt the gradient function to handle (mini-)batches.  Relies on the @p@'s+-- `NFData` instance to efficiently calculate gradients in parallel.+batchGradPar+  :: (ParamSet p, NFData p)+  => (e -> p -> p)+  -> ([e] -> p -> p)+batchGradPar = batchGradWith rdeepseq+++-- | A version of `batchGradPar` with no `NFData` constraint.  Evaluates the+-- sub-gradients calculated in parallel to weak head normal form.+batchGradPar'+  :: (ParamSet p)+  => (e -> p -> p)+  -> ([e] -> p -> p)+batchGradPar' = batchGradWith rseq+++-- | Adapt the gradient function to handle (mini-)batches.  The sub-gradients+-- of the individual batch elements are evaluated in parallel based on the+-- given `Strategy`.+batchGradWith+  :: (ParamSet p)+  => Strategy p+  -> (e -> p -> p)+  -> ([e] -> p -> p)+batchGradWith strategy grad xs param =+  case parMap strategy (\e -> grad e param) xs of+    [] -> param+    -- TODO: the fold is sequential, we could try to parallize it as well.+    ps -> foldl1' add ps+++-- | Adapt the gradient function to handle (mini-)batches.  The function+-- calculates the individual sub-gradients sequentially.+batchGradSeq+  :: (ParamSet p)+  => (e -> p -> p)+  -> ([e] -> p -> p)+batchGradSeq grad xs param =+  case map (flip grad param) xs of+    [] -> param+    ps -> foldl1' add ps++ -- | Extract the result of the SGD calculation (the last parameter -- set flowing downstream). result@@ -229,14 +322,77 @@ result pDef = fmap (maybe pDef id) . P.last  --- | Apply the given function every @k@ param sets flowing downstream.-every :: (Monad m) => Int -> (p -> m ()) -> P.Pipe p p m x-every k f = do-  go (1 `mod` k)+-- -- | Apply the given monadic function to every @k@-th value flowing downstream.+-- every :: (Monad m) => Int -> (p -> m ()) -> P.Pipe p p m x+-- every k f = do+--   go (1 `mod` k)+--   where+--     go i = do+--       paramSet <- P.await+--       when (i == 0) $ do+--         P.lift $ f paramSet+--       P.yield paramSet+--       go $ (i+1) `mod` k+++-- | Keep every @k@-th element flowing downstream and discard all the others.+keepEvery :: (Monad m) => Int -> P.Pipe a a m x+keepEvery k = forever $ do+  sequence_ $ replicate (k-1) P.await+  P.await >>= P.yield+-- keepEvery k = do+--   go (1 `mod` k)+--   where+--     go i = do+--       x <- P.await+--       when (i == 0) $ do+--         P.yield x+--       go $ (i+1) `mod` k+++-- -- | Keep the elements with the corresponding `True` in the argument list.+-- --+-- -- TODO: (=) or (==) in the following example?  And is this example correct?+-- -- @+-- -- keep (forever True) = P.id+-- -- @+-- keep :: (Monad m) => [Bool] -> P.Pipe a a m ()+-- keep [] = return ()+-- keep (b:bs) = do+--   x <- P.await+--   when b (P.yield x)+--   keep bs+-- +-- +-- -- | Create the mask to `keep` each @k@-th element flowing downstream.+-- every :: Int -> [Bool]+-- every k = cycle $ replicate (k-1) False ++ [True]+++-- | Make the stream decreasing in the given (monadic) function by discarding+-- elements with values higher than those already seen.+decreasingBy :: (Monad m, Ord a) => (p -> m a) -> P.Pipe p p m x+decreasingBy f = do+  x <- P.await+  v <- P.lift (f x)+  P.yield x+  go v   where-    go i = do-      paramSet <- P.await-      when (i == 0) $ do-        P.lift $ f paramSet-      P.yield paramSet-      go $ (i+1) `mod` k+    go w = do+      x <- P.await+      v <- P.lift (f x)+      when (v < w) (P.yield x)+      go (min v w)+++-------------------------------+-- Utils+-------------------------------+++-- partition :: Int -> [a] -> [[a]]+-- partition n =+--     transpose . group n+--   where+--     group _ [] = []+--     group k xs = take k xs : (group k $ drop k xs)
src/Numeric/SGD/DataSet.hs view
@@ -9,6 +9,7 @@ (  -- * Dataset   DataSet (..)+, shuffle -- * Reading , loadData , randomSample@@ -26,11 +27,13 @@ import           System.IO.Unsafe (unsafeInterleaveIO) import           System.FilePath ((</>)) import qualified System.Random as R+import           System.Random.Shuffle (shuffleM)  import           Data.Binary (Binary, encodeFile, decode) import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as BL import qualified Data.Vector as V+import qualified Data.Map.Strict as M   ------------------------------- @@ -66,6 +69,18 @@ --     let (i, g'') = R.next g' --     x <- dataset `elemAt` (i `mod` size dataset) --     return (x:xs, g'')+++-- | Shuffle the dataset.+shuffle :: DataSet a -> IO (DataSet a)+shuffle DataSet{..} = do+  let ixs = [0 .. size - 1]+  ixs' <- shuffleM ixs+  let m = M.fromList (zip ixs ixs')+  return $ DataSet+    { size = size+    , elemAt = elemAt . (m M.!)+    }   -- | Random dataset sample with a specified number of elements (loaded eagerly)