packages feed

nn (empty) → 0.2.0

raw patch · 6 files changed

+446/−0 lines, 6 filesdep +basedep +nndep +randomsetup-changed

Dependencies added: base, nn, random, split, tasty, tasty-hspec, tasty-quickcheck

Files

+ LICENSE view
@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2018 Sascha Grunert++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ README.md view
@@ -0,0 +1,62 @@+# ηn+## A tiny neural network 🧠++This small neural network is based on the+[backpropagation](https://en.wikipedia.org/wiki/Backpropagation) algorithm.++## Usage++A minimal usage example would look like this:++```haskell+import AI.Nn (new+             ,predict+             ,train)++main :: IO ()+main = do+  {- Creates a new network with two inputs,+     two hidden layers and one output -}+  network <- new [2, 2, 1]++  {- Train the network for a common logical AND,+     until the maximum error of 0.01 is reached -}+  let trainedNetwork = train 0.01 network [([0, 0], [0])+                                          ,([0, 1], [0])+                                          ,([1, 0], [0])+                                          ,([1, 1], [1])]++  {- Predict the learned values -}+  let r00 = predict trainedNetwork [0, 0]+  let r01 = predict trainedNetwork [0, 1]+  let r10 = predict trainedNetwork [1, 0]+  let r11 = predict trainedNetwork [1, 1]++  {- Print the results -}+  putStrLn $ printf "0 0 -> %.2f" (head r00)+  putStrLn $ printf "0 1 -> %.2f" (head r01)+  putStrLn $ printf "1 0 -> %.2f" (head r10)+  putStrLn $ printf "1 1 -> %.2f" (head r11)+```++The result should be something like:++```console+0 0 -> -0.02+0 1 -> -0.02+1 0 -> -0.01+1 1 -> 1.00+```++## Hacking+To start hacking simply clone this repository and make sure that+[stack](https://docs.haskellstack.org/en/stable/README/) is installed. Then+simply hack around and build the project with:++```console+> stack build --file-watch+```++## Contributing+You want to contribute to this project? Wow, thanks! So please just fork it and+send me a pull request.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ nn.cabal view
@@ -0,0 +1,57 @@+-- This file has been generated from package.yaml by hpack version 0.27.0.+--+-- see: https://github.com/sol/hpack+--+-- hash: 51c1408159910b3ceba84d3d8adc4b8f1e0e45c55f43cdb4d20bdd03fc8cfd76++name:           nn+version:        0.2.0+synopsis:       A tiny neural network+description:    Please see the README on Github at <https://github.com/saschagrunert/nn#readme>+category:       AI+homepage:       https://github.com/saschagrunert/nn#readme+bug-reports:    https://github.com/saschagrunert/nn/issues+author:         Sascha Grunert+maintainer:     mail@saschagrunert.de+copyright:      2018 Sascha Grunert+license:        MIT+license-file:   LICENSE+build-type:     Simple+cabal-version:  >= 1.10++extra-source-files:+    README.md++source-repository head+  type: git+  location: https://github.com/saschagrunert/nn++library+  exposed-modules:+      AI.Nn+  other-modules:+      Paths_nn+  hs-source-dirs:+      src+  ghc-options: -Wall -Wcompat+  build-depends:+      base >=4.7 && <5+    , random+    , split+  default-language: Haskell2010++test-suite nn-test+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      Paths_nn+  hs-source-dirs:+      test+  ghc-options: -Wall -Wcompat -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      base >=4.7 && <5+    , nn+    , tasty+    , tasty-hspec+    , tasty-quickcheck+  default-language: Haskell2010
+ src/AI/Nn.hs view
@@ -0,0 +1,234 @@+-- | This module contains everything related to the main library interface+--+-- @since 0.1.0++module AI.Nn+  ( Network+  , predict+  , new+  , train+  ) where++import Data.List       (find+                       ,transpose)+import Data.List.Split (chunksOf)+import Data.Maybe      (fromJust)+import System.Random   (StdGen+                       ,getStdGen+                       ,randomRs)++-- | The network+--+-- @since 0.1.0+type Network = Network' ()++-- | The alias for a list of layers+--+-- @since 0.1.0+type Network' a = [Layer a]++-- | The network layer+--+-- @since 0.1.0+type Layer a = [(Neuron,a)]++-- | A network neuron+--+-- @since 0.1.0+data Neuron = Neuron { inputWeights :: [Double]      -- ^ The input weights+                     , activate :: Double -> Double  -- ^ The activation function+                     , activate' :: Double -> Double -- ^ The first derivation of the activation function+                     }++-- | The forward layer type+--+-- @since 0.1.0+data Forward = Forward { output :: Double+                       , sumInputWeight :: Double+                       , inputs :: [Double]+                       } deriving Show++-- | The alias for a list of input weights+--+-- @since 0.1.0+type Neuron' = [Double]++-- | The sigmoid activation function+--+-- @since 0.1.0+sigmoid :: Double -> Double+sigmoid x = 1.0 / (1 + exp (-x))++-- | The first derivation of the sigmoid function+--+-- @since 0.1.0+sigmoid' :: Double -> Double+sigmoid' x = sigmoid x * (1 - sigmoid x)++-- | Create a sigmoid neuron from given input weights+--+-- @since 0.1.0+sigmoidNeuron :: Neuron' -> Neuron+sigmoidNeuron ws = Neuron ws sigmoid sigmoid'++-- | Create a output neuron from given weights+--+-- @since 0.1.0+outputNeuron :: Neuron' -> Neuron+outputNeuron ws = Neuron ws id (const 1)++-- | Create a bias neuron from given number of inputs+--+-- @since 0.1.0+biasNeuron :: Int -> Neuron+biasNeuron i = Neuron (replicate i 1) (const 1) (const 0)++-- | Create a new Layer from a list of Neuron'+--+-- @since 0.1.0+createLayer :: Functor f => f t -> (t -> a) -> f (a, ())+createLayer n x = (\p -> (x p, ())) <$> n++-- | Create a new sigmoid Layer from a list of Neuron'+--+-- @since 0.1.0+sigmoidLayer :: [Neuron'] -> Layer ()+sigmoidLayer n = (biasNeuron x, ()) : createLayer n sigmoidNeuron+  where x = length $ head n++-- | Create a new standard network for a number of layer and neurons+--+-- @since 0.1.0+new :: [Int] -> IO Network+new n = newGen n <$> getStdGen++-- | Create a new output Layer from a list of Neuron'+--+-- @since 0.1.0+outputLayer :: [Neuron'] -> Layer ()+outputLayer n = createLayer n outputNeuron++-- | Create a new network for a StdGen and a number of layer and neurons+--+-- @since 0.1.0+newGen :: [Int] -> StdGen -> Network+newGen n g = (sigmoidLayer <$> init wss) ++ [outputLayer (last wss)]+ where+  rest                 = init n+  hiddenIcsNcs         = zip ((+ 1) <$> rest) (tail rest)+  (outputIc, outputNc) = (snd (last hiddenIcsNcs) + 1, last n)+  rs                   = randomRs (-1, 1) g+  (hidden, rs')        = foldl+    ( \(wss', rr') (ic, nc) ->+      let (sl, rs'') = pack ic nc rr' in (wss' ++ [sl], rs'')+    )+    ([], rs)+    hiddenIcsNcs+  (outputWss, _) = pack outputIc outputNc rs'+  wss            = hidden ++ [outputWss]+  pack ic nc ws = (take nc $ chunksOf ic ws, drop (ic * nc) ws)++-- | Do the complete back propagation+--+-- @since 0.1.0+backpropagate :: Network -> ([Double], [Double]) -> Network+backpropagate nw (xs, ys) = weightUpdate (forwardLayer nw xs) ys++-- | The learning rate+--+-- @since 0.1.0+rate :: Double+rate = 0.5++-- | Generate forward pass info+--+-- @since 0.1.0+forwardLayer :: Network -> [Double] -> Network' Forward+forwardLayer nw xs = reverse . fst $ foldl pf ([], 1 : xs) nw+ where+  pf (nw', xs') l = (l' : nw', xs'')+   where+    l'   = (\(n, _) -> (n, forwardNeuron n xs')) <$> l+    xs'' = (output . snd) <$> l'++-- | Generate forward pass info for a neuron+--+-- @since 0.1.0+forwardNeuron :: Neuron -> [Double] -> Forward+forwardNeuron n xs = Forward+  { output         = activate n net'+  , sumInputWeight = net'+  , inputs         = xs+  }+  where net' = calcNet xs (inputWeights n)++-- | Calculate the product sum+--+-- @since 0.1.0+calcNet :: [Double] -> [Double] -> Double+calcNet xs ws = sum $ zipWith (*) xs ws++-- | Updates the weights for an entire network+--+-- @since 0.1.0+weightUpdate+  :: Network' Forward+  -> [Double] -- ^ desired output value+  -> Network+weightUpdate fpnw ys = fst $ foldr updateLayer ([], ds) fpnw+  where ds = zipWith (-) ys ((output . snd) <$> last fpnw)++-- | Updates the weights for a layer+--+-- @since 0.1.0+updateLayer :: Layer Forward -> (Network, [Double]) -> (Network, [Double])+updateLayer fpl (nw, ds) = (l' : nw, ds')+ where+  (l, es) = unzip $ zipWith updateNeuron fpl ds+  ds' =+    map sum . transpose $ map (\(n, e) -> (* e) <$> inputWeights n) (zip l es)+  l' = (\n -> (n, ())) <$> l++-- | Updates the weights for a neuron+--+-- @since 0.1.0+updateNeuron :: (Neuron, Forward) -> Double -> (Neuron, Double)+updateNeuron (n, fpi) d = (n { inputWeights = ws' }, e)+ where+  e   = activate' n (sumInputWeight fpi) * d+  ws' = zipWith (\x w -> w + (rate * e * x)) (inputs fpi) (inputWeights n)++-- | Trains a network with a set of vector pairs until the global error is+-- smaller than epsilon+--+-- @since 0.1.0+train :: Double -> Network -> [([Double], [Double])] -> Network+train epsilon nw samples = fromJust+  $ find (\x -> globalQuadError x samples < epsilon) (trainUl nw samples)++-- | Create an indefinite sequence of networks+--+-- @since 0.1.0+trainUl :: Network -> [([Double], [Double])] -> [Network]+trainUl nw samples = iterate (\x -> foldl backpropagate x samples) nw++-- | Quadratic error for multiple pairs+--+-- @since 0.1.0+globalQuadError :: Network -> [([Double], [Double])] -> Double+globalQuadError nw samples = sum $ quadErrorNet nw <$> samples++-- | Quadratic error for a single vector pair+--+-- @since 0.1.0+quadErrorNet :: Network -> ([Double], [Double]) -> Double+quadErrorNet nw (xs, ys) =+  sum $ zipWith (\o y -> (y - o) ** 2) (predict nw xs) ys++-- | Calculates the output of a network for a given input vector+--+-- @since 0.1.0+predict :: Network -> [Double] -> [Double]+predict nw xs = foldl calculateLayer (1 : xs) nw+ where+  calculateLayer s = map (\(n, _) -> activate n (calcNet s (inputWeights n)))
+ test/Spec.hs view
@@ -0,0 +1,70 @@+-- | The main test module+--+-- @since 0.1.0++module Main+  ( main+  ) where++import AI.Nn                 (new+                             ,predict+                             ,train)+import Test.Tasty            (TestTree+                             ,defaultMain+                             ,localOption+                             ,testGroup)+import Test.Tasty.Hspec      (Spec+                             ,it+                             ,parallel+                             ,shouldBe+                             ,testSpec)+import Test.Tasty.QuickCheck (QuickCheckTests (QuickCheckTests))++-- The main test routine+main :: IO ()+main = do+  uTests <- unitTests+  defaultMain . opts $ testGroup "Tests" [uTests]+  where opts = localOption $ QuickCheckTests 5000++-- Unit tests based on hspec+unitTests :: IO TestTree+unitTests = do+  actionUnitTests <- testSpec "Nn" nnSpec+  return $ testGroup "Unit Tests" [actionUnitTests]++-- Nn.hs related tests+nnSpec :: Spec+nnSpec = parallel $ do+  it "should succeed to train logical AND" $ do+    n <- new [2, 2, 1]+    let+      nw = train 0.001+                 n+                 [([0, 0], [0]), ([0, 1], [0]), ([1, 0], [0]), ([1, 1], [1])]+    round (head $ predict nw [1, 1]) `shouldBe` (1 :: Int)+    round (head $ predict nw [1, 0]) `shouldBe` (0 :: Int)+    round (head $ predict nw [0, 1]) `shouldBe` (0 :: Int)+    round (head $ predict nw [0, 0]) `shouldBe` (0 :: Int)++  it "should succeed to train logical OR" $ do+    n <- new [2, 2, 1]+    let+      nw = train 0.001+                 n+                 [([0, 0], [0]), ([0, 1], [1]), ([1, 0], [1]), ([1, 1], [1])]+    round (head $ predict nw [1, 1]) `shouldBe` (1 :: Int)+    round (head $ predict nw [1, 0]) `shouldBe` (1 :: Int)+    round (head $ predict nw [0, 1]) `shouldBe` (1 :: Int)+    round (head $ predict nw [0, 0]) `shouldBe` (0 :: Int)++  it "should succeed to train addition" $ do+    n <- new [2, 2, 1]+    let+      nw = train 0.001+                 n+                 [([0, 1], [1]), ([1, 1], [2]), ([1, 0], [1]), ([1, 2], [3])]+    round (head $ predict nw [0, 1]) `shouldBe` (1 :: Int)+    round (head $ predict nw [1, 0]) `shouldBe` (1 :: Int)+    round (head $ predict nw [1, 1]) `shouldBe` (2 :: Int)+    round (head $ predict nw [1, 2]) `shouldBe` (3 :: Int)