sgd 0.7.0.1 → 0.8.0.0
raw patch · 11 files changed
+651/−110 lines, 11 filesdep +addep +sgddep +tastydep ~basePVP ok
version bump matches the API change (PVP)
Dependencies added: ad, sgd, tasty, tasty-hunit
Dependency ranges changed: base
API changes (from Hackage documentation)
- Numeric.SGD.Adam: [alpha] :: Config -> Double
- Numeric.SGD.Momentum: [gain0] :: Config -> Double
+ Numeric.SGD: iterNumPerEpoch :: Integral a => Config -> a -> Double
+ Numeric.SGD: objectiveWith :: (e -> p -> Double) -> DataSet e -> p -> IO Double
+ Numeric.SGD: reportObjective :: ParamSet p => (e -> p -> Double) -> DataSet e -> p -> IO Double
+ Numeric.SGD.Adam: [alpha0] :: Config -> Double
+ Numeric.SGD.Adam: [tau] :: Config -> Double
+ Numeric.SGD.Adam: scaleTau :: Double -> Config -> Config
+ Numeric.SGD.Momentum: [alpha0] :: Config -> Double
+ Numeric.SGD.Momentum: scaleTau :: Double -> Config -> Config
+ Numeric.SGD.ParamSet: instance (Numeric.SGD.ParamSet.ParamSet a, Numeric.SGD.ParamSet.ParamSet b) => Numeric.SGD.ParamSet.ParamSet (a, b)
+ Numeric.SGD.Sparse.Dataset: Dataset :: Int -> (Int -> IO a) -> Dataset a
+ Numeric.SGD.Sparse.Dataset: [elemAt] :: Dataset a -> Int -> IO a
+ Numeric.SGD.Sparse.Dataset: [size] :: Dataset a -> Int
+ Numeric.SGD.Sparse.Dataset: data Dataset a
+ Numeric.SGD.Sparse.Dataset: loadData :: Dataset a -> IO [a]
+ Numeric.SGD.Sparse.Dataset: sample :: RandomGen g => g -> Int -> Dataset a -> IO ([a], g)
+ Numeric.SGD.Sparse.Dataset: withData :: Binary a => Bool -> [a] -> (Dataset a -> IO b) -> IO b
+ Numeric.SGD.Sparse.Dataset: withDisk :: Binary a => [a] -> (Dataset a -> IO b) -> IO b
+ Numeric.SGD.Sparse.Dataset: withVect :: [a] -> (Dataset a -> IO b) -> IO b
- 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 -> (p -> IO Double) -> DataSet e -> p -> IO p
- Numeric.SGD.Adam: Config :: Double -> Double -> Double -> Double -> Config
+ Numeric.SGD.Adam: Config :: Double -> Double -> Double -> Double -> Double -> Config
Files
- README.md +26/−0
- changelog +5/−0
- sgd.cabal +96/−54
- src/Numeric/SGD.hs +90/−41
- src/Numeric/SGD/Adam.hs +28/−5
- src/Numeric/SGD/DataSet.hs +2/−2
- src/Numeric/SGD/Momentum.hs +15/−8
- src/Numeric/SGD/ParamSet.hs +11/−0
- src/Numeric/SGD/Pipe.hs +169/−0
- src/Numeric/SGD/Sparse/Dataset.hs +124/−0
- test/Spec.hs +85/−0
+ README.md view
@@ -0,0 +1,26 @@+# Haskell stochastic gradient descent library++Stochastic gradient descent (SGD) is a method for optimizing a global objective+function defined as a sum of smaller, differentiable functions. In each+iteration of SGD the gradient is calculated based on a subset of the training+dataset. In Haskell, this process can be simply represented as a [fold over a+of subsequent dataset+subsets](https://blog.jle.im/entry/purely-functional-typed-models-1.html)+(singleton elements in the extreme).++However, it can be beneficial to select the subsequent subsets randomly (e.g.,+shuffle the entire dataset before each pass). Moreover, the dataset can be+large enough to make it impractical to store it all in memory. Hence, the+`sgd` library adopts a [pipe](http://hackage.haskell.org/package/pipes)-based+interface in which SGD takes the form of a process consuming dataset subsets+(the so-called mini-batches) and producing a stream of output parameter values.++The `sgd` library implements several SGD variants (SGD with momentum, AdaDelta,+Adam) and handles heterogeneous parameter representations (vectors, maps, custom+records, etc.). It can be used in combination with automatic differentiation+libraries ([ad](http://hackage.haskell.org/package/ad),+[backprop](http://hackage.haskell.org/package/backprop)), which can be used to+automatically calculate the gradient of the objective function.++Look at [the hackage repository](http://hackage.haskell.org/package/sgd) for a+library documentation.
+ changelog view
@@ -0,0 +1,5 @@+-*-change-log-*-++0.8.0 Apr 2019+ * Allow step size decay in Adam+ * Parallel calculation of the objective function
sgd.cabal view
@@ -1,59 +1,101 @@-name: sgd-version: 0.7.0.1-synopsis: Stochastic gradient descent-description:- Stochastic gradient descent library.- .- Import "Numeric.SGD" to use the library.-license: BSD3-license-file: LICENSE-cabal-version: >= 1.6-copyright: Copyright (c) 2012-2019 Jakub Waszczuk-author: Jakub Waszczuk-maintainer: waszczuk.kuba@gmail.com-stability: experimental-category: Math-homepage: https://github.com/kawu/sgd-build-type: Simple+cabal-version: 1.12 -library- hs-source-dirs: src+-- This file has been generated from package.yaml by hpack version 0.31.1.+--+-- see: https://github.com/sol/hpack+--+-- hash: c76dc64057b0ad62fdd40b4496b01d9bdbf827e48db5996639347df6c4209224 - build-depends:- base >= 4 && < 5- , containers >= 0.4 && < 0.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- , deepseq >= 1.3 && < 1.5- , binary >= 0.5 && < 0.9- , bytestring >= 0.9 && < 0.11- , mtl >= 2.0 && < 2.3- , filepath >= 1.3 && < 1.5- , temporary >= 1.1 && < 1.4- , hmatrix >= 0.19 && < 0.20- , data-default >= 0.7 && < 0.8- , parallel >= 3.2 && < 3.3+name: sgd+version: 0.8.0.0+synopsis: Stochastic gradient descent library+description: Please see the README on GitHub at <https://github.com/kawu/sgd#readme>+category: Math+homepage: https://github.com/kawu/sgd#readme+bug-reports: https://github.com/kawu/sgd/issues+author: Jakub Waszczuk+maintainer: waszczuk.kuba@gmail.com+copyright: 2012-2019 Jakub Waszczuk+license: BSD3+license-file: LICENSE+build-type: Simple+extra-source-files:+ README.md+ changelog - exposed-modules:- Numeric.SGD- , Numeric.SGD.Type- , Numeric.SGD.DataSet- , Numeric.SGD.ParamSet- , Numeric.SGD.Momentum- , Numeric.SGD.AdaDelta- , Numeric.SGD.Adam- , Numeric.SGD.Sparse- , Numeric.SGD.Sparse.Momentum- , Numeric.SGD.Sparse.LogSigned- , Numeric.SGD.Sparse.Grad+source-repository head+ type: git+ location: https://github.com/kawu/sgd - ghc-options: -Wall -O2+library+ exposed-modules:+ Numeric.SGD+ Numeric.SGD.AdaDelta+ Numeric.SGD.Adam+ Numeric.SGD.DataSet+ Numeric.SGD.Momentum+ Numeric.SGD.ParamSet+ Numeric.SGD.Pipe+ Numeric.SGD.Sparse+ Numeric.SGD.Sparse.Dataset+ Numeric.SGD.Sparse.Grad+ Numeric.SGD.Sparse.LogSigned+ Numeric.SGD.Sparse.Momentum+ Numeric.SGD.Type+ other-modules:+ Paths_sgd+ hs-source-dirs:+ src+ build-depends:+ base >=4.7 && <5+ , binary >=0.5 && <0.9+ , bytestring >=0.9 && <0.11+ , containers >=0.4 && <0.7+ , data-default >=0.7 && <0.8+ , deepseq >=1.3 && <1.5+ , filepath >=1.3 && <1.5+ , hmatrix >=0.19 && <0.20+ , logfloat >=0.12 && <0.14+ , monad-par >=0.3.4 && <0.4+ , mtl >=2.0 && <2.3+ , parallel >=3.2 && <3.3+ , pipes >=4.3 && <4.4+ , primitive >=0.5 && <0.7+ , random >=1.0 && <1.2+ , random-shuffle >=0.0.4 && <0.1+ , temporary >=1.1 && <1.4+ , vector >=0.10 && <0.13+ default-language: Haskell2010 -source-repository head- type: git- location: git://github.com/kawu/sgd.git+test-suite vine-test+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ Paths_sgd+ hs-source-dirs:+ test+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ ad >=4.3 && <4.4+ , base >=4.7 && <5+ , binary >=0.5 && <0.9+ , bytestring >=0.9 && <0.11+ , containers >=0.4 && <0.7+ , data-default >=0.7 && <0.8+ , deepseq >=1.3 && <1.5+ , filepath >=1.3 && <1.5+ , hmatrix >=0.19 && <0.20+ , logfloat >=0.12 && <0.14+ , monad-par >=0.3.4 && <0.4+ , mtl >=2.0 && <2.3+ , parallel >=3.2 && <3.3+ , pipes >=4.3 && <4.4+ , primitive >=0.5 && <0.7+ , random >=1.0 && <1.2+ , random-shuffle >=0.0.4 && <0.1+ , sgd+ , tasty >=1.2 && <1.3+ , tasty-hunit >=0.10 && <0.11+ , temporary >=1.1 && <1.4+ , vector >=0.10 && <0.13+ default-language: Haskell2010
src/Numeric/SGD.hs view
@@ -39,6 +39,9 @@ -- * IO-based SGD , Config (..)+ , iterNumPerEpoch+ , reportObjective+ , objectiveWith , runIO -- * Combinators@@ -62,20 +65,21 @@ import GHC.Generics (Generic)--- import GHC.Conc (numCapabilities)+import GHC.Conc (numCapabilities)+ import Numeric.Natural (Natural) -- import qualified System.Random as R -import Control.Monad (when, forM_, forever)+import Control.Monad (when, forever) -- forM_, 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 Data.List (foldl1', transpose) --foldl', -import qualified Data.IORef as IO+-- import qualified Data.IORef as IO import Data.Default import qualified Pipes as P@@ -107,9 +111,12 @@ or use an automatic differentiation library, for instance: > import qualified Numeric.AD as AD-> derivs = map-> (\k -> AD.diff (funs !! k))-> [0..length funs-1]+> derivs =+> [ AD.diff $ \x -> 0.3*x^2+> , AD.diff $ \x -> -2*x+> , AD.diff $ const 3+> , AD.diff $ sin+> ] Finally, `run` allows to approach a (potentially local) minimum of the global objective function:@@ -184,6 +191,67 @@ } +-- | Number of new elements in each new batch+batchNew :: Config -> Int+batchNew cfg = max 1+ ( fromIntegral (batchSize cfg)+ - fromIntegral (batchOverlap cfg)+ )+++-- | Calculate the effective number of SGD iterations (and gradient+-- calculations) performed per epoch.+iterNumPerEpoch+ :: (Integral a)+ => Config+ -> a -- ^ Dataset size+ -> Double+iterNumPerEpoch cfg size =+ fromIntegral size / fromIntegral (batchNew cfg)+++-- | Report the total objective value on stdout.+reportObjective+ :: (ParamSet p)+ => (e -> p -> Double)+ -- ^ Value of the objective function on a dataset element+ -> DataSet e+ -- ^ Training dataset+ -> p -> IO Double+reportObjective objAt dataSet net = do+ q <- objectiveWith objAt dataSet net+ putStr $ show q+ putStrLn $ " (norm_2 = " ++ show (norm_2 net) ++ ")"+ return q+++-- | Value of the objective function over the entire dataset (i.e. the sum of+-- the objectives on all dataset elements).+objectiveWith+ :: (e -> p -> Double)+ -- ^ Value of the objective function on a dataset element+ -> DataSet e+ -- ^ Training dataset+ -> p -> IO Double+objectiveWith objAt dataSet net = do++-- elems <- loadData dataSet+-- let scores = parMap rseq (flip objAt net) elems+-- return $ sum scores++ parts <- partition numCapabilities <$> loadData dataSet+ let scores = parMap rseq groupScore parts+ return $ sum scores+ where+ groupScore = sum . map (flip objAt net)++-- res <- IO.newIORef 0.0+-- forM_ [0 .. size dataSet - 1] $ \ix -> do+-- x <- elemAt dataSet ix+-- IO.modifyIORef' res (+ objAt x net)+-- IO.readIORef res++ -- | Perform SGD in the IO monad, regularly reporting the value of the -- objective function on the entire dataset. A higher-level wrapper which -- should be convenient to use when the training dataset is large.@@ -197,55 +265,36 @@ -- ^ SGD configuration -> SGD IO [e] p -- ^ SGD pipe consuming mini-batches of dataset elements- -> (e -> p -> Double)- -- ^ Value of the objective function on a dataset element (used for model- -- quality reporting)+ -> (p -> IO Double)+ -- ^ Quality reporting function (the reporting frequency is specified+ -- via `reportEvery`) -> DataSet e -- ^ Training dataset -> p -- ^ Initial parameter values -> IO p-runIO Config{..} sgd quality0 dataSet net0 = do- _ <- report net0+runIO cfg@Config{..} sgd reportObj dataSet net0 = do+ _ <- reportObj net0 result net0 $ pipeData dataSet >-> batch (fromIntegral batchSize) >-> batchFilter >-> sgd net0 >-> keepEvery realReportPeriod >-> P.take (fromIntegral iterNum)- >-> decreasingBy report+ >-> decreasingBy reportObj where -- 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+ keepEvery (batchNew cfg) -- Iteration (epoch) scaling- iterScale x = fromIntegral (size dataSet) * x / fromIntegral batchNew- -- Number of iterations and reporting period- -- realIterNum = ceiling $ iterScale (fromIntegral iterNum :: Double)- -- realReportPeriod = floor $ iterScale reportEvery- realReportPeriod = ceiling $ iterScale reportEvery- -- Network quality over the entire training dataset- report net = do- 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- x <- elemAt dataSet ix- IO.modifyIORef' res (+ quality0 x net)- IO.readIORef res+ realReportPeriod = ceiling $+ reportEvery * iterNumPerEpoch cfg (size dataSet) ------------------------------- @@ -409,9 +458,9 @@ ------------------------------- --- partition :: Int -> [a] -> [[a]]--- partition n =--- transpose . group n--- where--- group _ [] = []--- group k xs = take k xs : (group k $ drop k xs)+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/Adam.hs view
@@ -10,6 +10,7 @@ module Numeric.SGD.Adam ( Config(..)+ , scaleTau , adam ) where @@ -26,11 +27,15 @@ import Numeric.SGD.Type import Numeric.SGD.ParamSet +-- import Debug.Trace (trace) + -- | AdaDelta configuration data Config = Config- { alpha :: Double- -- ^ Step size+ { alpha0 :: Double+ -- ^ Initial step size+ , tau :: Double+ -- ^ The step size after k * `tau` iterations = `alpha0` / (k + 1) , beta1 :: Double -- ^ 1st exponential moment decay , beta2 :: Double@@ -41,13 +46,20 @@ instance Default Config where def = Config- { alpha = 0.001+ { alpha0 = 0.001+ , tau = 10000 , beta1 = 0.9 , beta2 = 0.999 , eps = 1.0e-8 } +-- | Scale the `tau` parameter. Useful e.g. to account for the size of the+-- training dataset.+scaleTau :: Double -> Config -> Config+scaleTau coef cfg = cfg {tau = coef * tau cfg}++ -- | Perform gradient descent using the Adam algorithm. -- See "Numeric.SGD.Adam" for more information. adam@@ -64,7 +76,18 @@ where + -- Gain in the k-th iteration+ alpha k+ = (alpha0 * tau)+ / (tau + fromIntegral k)++-- report t action =+-- if t `mod` 25 == 0+-- then trace (show (tau, t, alpha t)) action+-- else action+ go t m v net = do+ -- x <- report t (P.await) x <- P.await let g = gradient x net m' = pmap (*beta1) m `add` pmap (*(1-beta1)) g@@ -73,10 +96,10 @@ mb = pmap (/(1-beta1^t)) m' vb = pmap (/(1-beta2^t)) v' newNet = net `sub`- ( pmap (*alpha) mb `div`+ ( pmap (*alpha t) mb `div` (pmap (+eps) (pmap sqrt vb)) )- P.yield newNet+ newNet `seq` P.yield newNet go (t+1) m' v' newNet
src/Numeric/SGD/DataSet.hs view
@@ -123,8 +123,8 @@ -- Avoid decodeFile laziness when using some older versions of the binary -- library (as of year 2019, this could be probably simplified) let at ix = do- cs <- B.readFile (tmpDir </> show ix)- return . decode $ BL.fromChunks [cs]+ cs <- B.readFile (tmpDir </> show ix)+ return . decode $ BL.fromChunks [cs] handler $ DataSet {size = n, elemAt = at}
src/Numeric/SGD/Momentum.hs view
@@ -10,6 +10,7 @@ module Numeric.SGD.Momentum ( Config(..)+ , scaleTau , momentum ) where @@ -26,22 +27,28 @@ -- | Momentum configuration data Config = Config- { gain0 :: Double- -- ^ Initial gain parameter, used to scale the gradient+ { alpha0 :: Double+ -- ^ Initial step size, used to scale the gradient , tau :: Double- -- ^ After how many gradient calculations the gain parameter is halved+ -- ^ The step size after k * `tau` iterations = `alpha0` / (k + 1) , gamma :: Double- -- ^ Momentum term+ -- ^ Momentum term } deriving (Show, Eq, Ord, Generic) instance Default Config where def = Config- { gain0 = 0.01+ { alpha0 = 0.01 , gamma = 0.9 , tau = 1000 } +-- | Scale the `tau` parameter. Useful e.g. to account for the size of the+-- training dataset.+scaleTau :: Double -> Config -> Config+scaleTau coef cfg = cfg {tau = coef * tau cfg}++ -- | Stochastic gradient descent with momentum. See "Numeric.SGD.Momentum" for -- more information. momentum@@ -58,13 +65,13 @@ where -- Gain in the k-th iteration- gain k- = (gain0 * tau)+ alpha k+ = (alpha0 * tau) / (tau + fromIntegral k) go k moment net = do x <- P.await- let grad = scale (gain k) (gradient x net)+ let grad = scale (alpha k) (gradient x net) moment' = scale gamma moment `add` grad newNet = net `sub` moment' P.yield newNet
src/Numeric/SGD/ParamSet.hs view
@@ -431,6 +431,17 @@ norm_2 = abs +instance (ParamSet a, ParamSet b) => ParamSet (a, b) where+ pmap f (x, y) = (pmap f x, pmap f y)+ add (x1, y1) (x2, y2) = (x1 `add` x2, y1 `add` y2)+ sub (x1, y1) (x2, y2) = (x1 `sub` x2, y1 `sub` y2)+ mul (x1, y1) (x2, y2) = (x1 `mul` x2, y1 `mul` y2)+ div (x1, y1) (x2, y2) = (x1 `div` x2, y1 `div` y2)+ norm_2 (x, y)+ = sqrt . sum . map ((^(2::Int)))+ $ [norm_2 x, norm_2 y]++ instance (KnownNat n) => ParamSet (LA.R n) where zero = const 0 pmap = LA.dvmap
+ src/Numeric/SGD/Pipe.hs view
@@ -0,0 +1,169 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE DeriveGeneric #-}+++-- | Pipe-based interface+++module Numeric.SGD.Pipe where+-- ( Config(..)+-- , Method(..)+-- , sgd+-- , result+-- , every+-- , pipeSeq+-- , pipeRan+-- ) where+++-- import GHC.Generics (Generic)+-- import Numeric.Natural (Natural)+-- +-- import Control.Monad (when, forM_)+-- +-- import qualified System.Random as R+-- +-- import qualified Data.IORef as IO+-- +-- import qualified Pipes as P+-- import qualified Pipes.Prelude as P+-- import Pipes ((>->))+-- +-- import Numeric.SGD.DataSet+-- import Numeric.SGD.ParamSet+-- import qualified Numeric.SGD.AdaDelta as Ada+-- import qualified Numeric.SGD.Momentum as Mom+-- +-- +-- ------------------------------- +-- -- Data+-- -------------------------------+-- +-- +-- -- | Top-level SGD configuration+-- data Config = Config+-- { iterNum :: Natural+-- -- ^ Number of iteration over the entire training dataset+-- , batchRandom :: Bool+-- -- ^ Should the mini-batch be selected at random? If not, the subsequent+-- -- training elements will be picked sequentially. Random selection gives+-- -- no guarantee of seeing each training sample in every epoch. Use `False`+-- -- if unsure.+-- , method :: Method+-- -- ^ Selected SGD method+-- , reportPeriod :: Double+-- -- ^ How often the quality should be reported (with `1` meaning once per+-- -- pass over the training data)+-- } deriving (Show, Eq, Ord, Generic)+-- +-- +-- -- | Different SGD methods, together with the corresponding configurations+-- data Method+-- = AdaDelta {adaDeltaCfg :: Ada.Config}+-- | Momentum {momentumCfg :: Mom.Config}+-- deriving (Show, Eq, Ord, Generic)+-- +-- +-- ------------------------------- +-- -- SGD+-- -------------------------------+-- +-- +-- -- | Pipe-based SGD.+-- sgd+-- :: (ParamSet p)+-- => Config+-- -> DataSet e+-- -> (e -> p -> p)+-- -- ^ Network gradient on a sample element+-- -> (e -> p -> Double)+-- -- ^ Value of the objective function on a sample element+-- -> p+-- -- ^ Initial parameter values+-- -> IO p+-- sgd Config{..} dataSet grad0 quality0 net0 = do+-- let sgdPipe =+-- case method of+-- Momentum cfg -> Mom.momentum cfg+-- -- (cfg {Mom.tau = iterScale (Mom.tau cfg)})+-- grad0 net0+-- AdaDelta cfg -> Ada.adaDelta cfg grad0 net0+-- report net0+-- result net0 $ pipeSeq dataSet+-- >-> sgdPipe+-- >-> P.take realIterNum+-- >-> every realReportPeriod report+-- where+-- -- Iteration scaling+-- iterScale x = fromIntegral (size dataSet) * x+-- -- Number of iterations and reporting period+-- realIterNum = ceiling $ iterScale (fromIntegral iterNum :: Double)+-- realReportPeriod = ceiling $ iterScale reportPeriod+-- -- Network quality over the entire training dataset+-- report net = do+-- putStr . show =<< quality net+-- putStrLn $ " (norm_2 = " ++ show (norm_2 net) ++ ")"+-- quality net = do+-- res <- IO.newIORef 0.0+-- forM_ [0 .. size dataSet - 1] $ \ix -> do+-- x <- elemAt dataSet ix+-- IO.modifyIORef' res (+ quality0 x net)+-- IO.readIORef res+-- +-- +-- ------------------------------- +-- -- Dataset producers+-- -------------------------------+-- +-- +-- -- | Pipe the dataset sequentially in a loop.+-- pipeSeq :: DataSet e -> P.Producer e IO ()+-- pipeSeq dataSet = do+-- go (0 :: Int)+-- where+-- go k+-- | k >= size dataSet = go 0+-- | otherwise = do+-- x <- P.lift $ elemAt dataSet k+-- P.yield x+-- go (k+1)+-- +-- +-- -- | Pipe the dataset randomly in a loop.+-- 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+-- +-- +-- ------------------------------- +-- -- Utils+-- -------------------------------+-- +-- +-- -- | Extract the result of the SGD calculation (the last parameter+-- -- set flowing downstream).+-- result+-- :: (Monad m)+-- => p +-- -- ^ Default value (in case the stream is empty)+-- -> P.Producer p m ()+-- -- ^ Stream of parameter sets+-- -> m p+-- result pDef = fmap (maybe pDef id) . P.last+-- +-- +-- -- | Report every `k`-th parameter set flowing downstream.+-- every :: (Monad m) => Int -> (p -> m ()) -> P.Pipe p p m x+-- every k report = do+-- go (1 `mod` k)+-- where+-- go i = do+-- paramSet <- P.await+-- when (i == 0) $ do+-- P.lift $ report paramSet+-- P.yield paramSet+-- go $ (i+1) `mod` k
+ src/Numeric/SGD/Sparse/Dataset.hs view
@@ -0,0 +1,124 @@+{-# LANGUAGE RecordWildCards #-}+++-- | Dataset abstraction.+++module Numeric.SGD.Sparse.Dataset+( +-- * Dataset+ Dataset (..)+-- * Reading+, loadData+, sample+-- * Construction+, withVect+, withDisk+, withData+) where+++import Control.Monad (forM_)+import Data.Binary (Binary, encodeFile, decode)+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL+import System.IO.Temp (withTempDirectory)+import System.IO.Unsafe (unsafeInterleaveIO)+import System.FilePath ((</>))+import qualified System.Random as R+import qualified Data.Vector as V+import qualified Control.Monad.State.Strict as S+++-- | A dataset with elements of type @a@.+data Dataset a = Dataset {+ -- | A size of the dataset.+ size :: Int+ -- | Get dataset element with a given index. The set of indices+ -- is of a {0, 1, .., size - 1} form.+ , elemAt :: Int -> IO a + }+++-------------------------------------------+-- Reading+-------------------------------------------+++-- | Lazily load dataset from a disk.+loadData :: Dataset a -> IO [a]+loadData Dataset{..} = lazyMapM elemAt [0 .. size - 1]+++-- | A dataset sample of the given size.+sample :: R.RandomGen g => g -> Int -> Dataset a -> IO ([a], g)+sample g 0 _ = return ([], g)+sample g n dataset = do+ (xs, g') <- sample g (n-1) dataset+ let (i, g'') = R.next g'+ x <- dataset `elemAt` (i `mod` size dataset)+ return (x:xs, g'')+++-------------------------------------------+-- Construction+-------------------------------------------+++-- | Construct dataset from a vector of elements and run the+-- given handler.+withVect :: [a] -> (Dataset a -> IO b) -> IO b+withVect xs handler =+ handler dataset+ where+ v = V.fromList xs+ dataset = Dataset+ { size = V.length v+ , elemAt = \k -> return (v V.! k) }+++-- | Construct dataset from a list of elements, store it on a disk+-- and run the given handler.+withDisk :: Binary a => [a] -> (Dataset a -> IO b) -> IO b+withDisk xs handler = withTempDirectory "." ".sgd" $ \tmpDir -> do+ -- We use state monad to compute the number of dataset elements. + n <- flip S.execStateT 0 $ forM_ (zip xs [0 :: Int ..]) $ \(x, ix) -> do+ S.lift $ encodeFile (tmpDir </> show ix) x+ S.modify (+1)++ -- We need to avoid decodeFile laziness when using some older+ -- versions of the binary library.+ let at ix = do+ cs <- B.readFile (tmpDir </> show ix)+ return . decode $ BL.fromChunks [cs]++ handler $ Dataset {size = n, elemAt = at}+++-- | Use disk or vector dataset representation depending on+-- the first argument: when `True`, use `withDisk`, otherwise+-- use `withVect`.+withData :: Binary a => Bool -> [a] -> (Dataset a -> IO b) -> IO b+withData x = case x of+ True -> withDisk+ False -> withVect+++-------------------------------------------+-- Lazy IO Utils+-------------------------------------------+++-- | Lazily evaluate each action in the sequence from left to right,+-- and collect the results.+lazySequence :: [IO a] -> IO [a]+lazySequence (mx:mxs) = do+ x <- mx+ xs <- unsafeInterleaveIO (lazySequence mxs)+ return (x : xs)+lazySequence [] = return []+++-- | `lazyMapM` f is equivalent to `lazySequence` . `map` f.+lazyMapM :: (a -> IO b) -> [a] -> IO [b]+lazyMapM f = lazySequence . map f
+ test/Spec.hs view
@@ -0,0 +1,85 @@+{-# LANGUAGE NoMonomorphismRestriction #-}+++import Data.Ord (Ord(..))++import qualified Numeric.AD as AD++import Test.Tasty (TestTree, testGroup)+import qualified Test.Tasty as Tasty+import qualified Test.Tasty.HUnit as U+import Test.Tasty.HUnit ((@?=))+-- import qualified Test.Tasty.SmallCheck as SC++import qualified Numeric.SGD as SGD+import qualified Numeric.SGD.Adam as Adam+-- import qualified Numeric.SGD.AdaDelta as Ada+++main :: IO ()+main = Tasty.defaultMain tests+++tests :: TestTree+tests = testGroup "Tests" [unitTests]+++unitTests = testGroup "Unit tests"+ [ U.testCase "Simple optimization" $ do+ U.assertBool "Momentum" . approxWith 0.01 4.18 $+ sgdWith (SGD.momentum SGD.def id)+ U.assertBool "Adam" . approxWith 0.01 4.18 $+ let cfg = SGD.def {Adam.alpha0 = 0.1}+ in sgdWith (SGD.adam cfg id)+ U.assertBool "AdaDelta" . approxWith 0.25 4.18 $+ sgdWith (SGD.adaDelta SGD.def id)+-- -- the following test does not hold+-- , U.testCase "List comparison (same length)" $+-- [1, 2, 3] `compare` [1,2,2] @?= LT+ ]+++--------------------------------------------------+-- Main unit test+--------------------------------------------------+++-- | The component objective functions+funs :: [Double -> Double]+funs =+ [ \x -> 0.3*x^2+ , \x -> -2*x+ , const 3+ , sin+ ]+++-- | The corresponding derivatives+derivs :: [Double -> Double]+derivs =+ [ AD.diff $ \x -> 0.3*x^2+ , AD.diff $ \x -> -2*x+ , AD.diff $ const 3+ , AD.diff $ sin+ ]+++-- | The total objective is the sum of the objective component functions+objective :: Double -> Double+objective x = sum $ map ($x) funs+++-- | Perform SGD with the given SGD variant.+sgdWith typ = SGD.run typ (take 10000 $ cycle derivs) (0.0 :: Double)+++--------------------------------------------------+-- Utils+--------------------------------------------------+++-- | Is the second argument approximately equaly to the third one?+-- The first argument is the epsilon.+approxWith :: (Ord a, Floating a) => a -> a -> a -> Bool+approxWith eps x y =+ x >= y - eps && x <= y + eps