packages feed

simple-neural-networks (empty) → 0.2.0.0

raw patch · 6 files changed

+500/−0 lines, 6 filesdep +basedep +containersdep +deepseqsetup-changed

Dependencies added: base, containers, deepseq, parallel, random, split

Files

+ LICENSE view
@@ -0,0 +1,23 @@+Copyright (c) 2014, Alexander Alexeev+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++* Redistributions of source code must retain the above copyright notice, this+  list of conditions and the following disclaimer.++* Redistributions in binary form must reproduce the above copyright notice,+  this list of conditions and the following disclaimer in the documentation+  and/or other materials provided with the distribution.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ simple-neural-networks.cabal view
@@ -0,0 +1,54 @@+name:                simple-neural-networks+version:             0.2.0.0+synopsis:            Simple parallel neural networks implementation+description:         Simple parallel neural networks implementation+homepage:            http://eax.me/haskell-neural-networks/+license:             BSD3+license-file:        LICENSE+author:              Alexander Alexeev+maintainer:          mail@eax.me+-- copyright:           +category:            AI+build-type:          Simple+-- extra-source-files:  +cabal-version:       >=1.10++source-repository head+    type:     git+    location: https://github.com/afiskon/simple-neural-networks++library+  exposed-modules:     AI.NeuralNetworks.Simple+  ghc-options:         -O2 -Wall -fno-warn-missing-signatures+  build-depends:       base >=4.5 && < 4.8,+                       random >= 1.0 && < 1.1,+                       split >= 0.2 && < 0.3,+                       containers >= 0.5 && < 0.6,+                       deepseq >= 1.3 && < 1.4,+                       parallel >= 3.2 && < 3.3+  hs-source-dirs:      src+  default-language:    Haskell2010++executable nn-xor-logistic-example+  ghc-options:         -O2 -Wall -fno-warn-missing-signatures -threaded -rtsopts+  main-is:             MainXorLogistic.hs+  build-depends:       base >= 4.5 && < 4.8,+                       random >= 1.0 && < 1.1,+                       split >= 0.2 && < 0.3,+                       containers >= 0.5 && < 0.6,+                       deepseq >= 1.3 && < 1.4,+                       parallel >= 3.2 && < 3.3+  hs-source-dirs:      src+  default-language:    Haskell2010++executable nn-xor-tanh-example+  ghc-options:         -O2 -Wall -fno-warn-missing-signatures -threaded -rtsopts+  main-is:             MainXorTanh.hs+  build-depends:       base >= 4.5 && < 4.8,+                       random >= 1.0 && < 1.1,+                       split >= 0.2 && < 0.3,+                       containers >= 0.5 && < 0.6,+                       deepseq >= 1.3 && < 1.4,+                       parallel >= 3.2 && < 3.3+  hs-source-dirs:      src+  default-language:    Haskell2010
+ src/AI/NeuralNetworks/Simple.hs view
@@ -0,0 +1,353 @@+{-|+  Simple parallel neural networks implementation++  @+  import AI.NeuralNetworks.Simple+  import Text.Printf+  import System.Random+  import Control.Monad+    +  calcXor net x y =+    let [r] = runNeuralNetwork net [x, y]+    in  r+    +  mse net =+    let square x = x * x+        e1 = square $ calcXor net 0 0+        e2 = square $ calcXor net 1 0 - 1+        e3 = square $ calcXor net 0 1 - 1+        e4 = square $ calcXor net 1 1+    in 0.5 * (e1 + e2 + e3 + e4)+    +  stopf best gnum = do+    let e = mse best+    when (gnum \`rem\` 100 == 0) $+      printf \"Generation: %02d, MSE: %.4f\\n\" gnum e+    return $ e \< 0.002 || gnum \>= 10000++  main = do+    gen \<- newStdGen+    let (randomNet, _) = randomNeuralNetwork gen [2,2,1] [Logistic, Logistic] 0.45+        examples = [ ([0,0],[0]), ([0,1],[1]), ([1,0],[1]), ([1,1],[0]) ]+    net \<- backpropagationBatchParallel randomNet examples 0.4 stopf :: IO (NeuralNetwork Double)+    putStrLn \"\"+    putStrLn $ \"Result: \" ++ show net+    _ \<- printf \"0 xor 0 = %.4f\\n\" (calcXor net 0 0)+    _ \<- printf \"1 xor 0 = %.4f\\n\" (calcXor net 1 0)+    _ \<- printf \"0 xor 1 = %.4f\\n\" (calcXor net 0 1)+    printf \"1 xor 1 = %.4f\" (calcXor net 1 1)+  @+-}+module AI.NeuralNetworks.Simple (+    ActivationFunction(..),+    NeuralNetwork,+    WeightDeltas,+    emptyNeuralNetwork,+    getWeights,+    setWeights,+    runNeuralNetwork,+    backpropagationOneStep,+    backpropagationStochastic,+    backpropagationBatchParallel,+    applyWeightDeltas,+    unionWeightDeltas,+    randomNeuralNetwork,+    crossoverCommon,+    crossoverMerge,+    mutationCommon+  ) where++import System.Random+import Data.List (unfoldr, foldl')+import qualified Data.Map.Strict as M+import qualified Data.IntMap.Strict as IM+import Control.Applicative+import Control.Arrow+import Control.DeepSeq+import Control.Parallel.Strategies+import GHC.Conc (numCapabilities)+import Data.List.Split (chunksOf)+import Data.Maybe (fromJust)+import Data.Word+import Data.Bits++-- | Activation function+data ActivationFunction = Tanh     -- ^ Hyperbolic tangent+                        | Logistic -- ^ Logistic function : 1 / (1 + exp (-x))+                        deriving (Show, Read, Eq)++logistic x = 1 / (1 + exp (-x))++applyAF Tanh = tanh+applyAF Logistic = logistic++applyAFDerivative Tanh x = let t = tanh x in (1 + t) * (1 - t) -- = 1 / (cosh x ^ (2 :: Int) )+applyAFDerivative Logistic x = let t = logistic x in t * (1 - t)++-- | Neural network+data NeuralNetwork a = NeuralNetwork [Word16] [ ActivationFunction ] (M.Map Word64 a)+                       deriving (Show, Read, Eq)++instance NFData a => NFData (NeuralNetwork a) where+    rnf (NeuralNetwork _ _ m) = rnf m `seq` ()++-- | Deltas calculated by backpropagation algorithm+newtype WeightDeltas a = WeightDeltas (M.Map Word64 a)+                         deriving (Show, Read, Eq)++instance NFData a => NFData (WeightDeltas a) where+    rnf (WeightDeltas m) = rnf m `seq` ()++-- | Neural network with all weights set to zero.+-- +-- @+-- {- +--    2 input neurons,+--    one hidden layer with 2 neurons and tanh activation function,+--    one output layer with 1 neuron and tanh activation function+-- -}+-- emptyNeuralNetwork [2, 2, 1] [Tanh, Tanh]+-- @+emptyNeuralNetwork :: [Word16]                -- ^ Number of neurons in each layer+                   -> [ ActivationFunction ]  -- ^ Activation functions+                   -> NeuralNetwork a         -- ^ New neural network+emptyNeuralNetwork sx ax =+    NeuralNetwork sx ax M.empty++-- | Weights of the given neural network.+getWeights :: NeuralNetwork a                 -- ^ Neural network+           -> [((Word16, Word16, Word16), a)] -- ^ Weights (layer 0.., neuron 1.., input 0..)+getWeights (NeuralNetwork _ _ wx) =+    map (first decodeKey) $ M.toList wx++-- | Change weights of the given neural network.+setWeights :: [((Word16, Word16, Word16), a)] -- ^ Weights+           -> NeuralNetwork a                 -- ^ Neural network+           -> NeuralNetwork a                 -- ^ Neural network with changed weights+setWeights lst (NeuralNetwork sx ax _) =+    let wx = M.fromList $ map (\((k1, k2, k3), v) -> (makeKey k1 k2 k3, v)) lst+    in  NeuralNetwork sx ax wx++-- | Run neural network.+runNeuralNetwork :: (Num a, Floating a) +                 => NeuralNetwork a          -- ^ Neural network+                 -> [a]                      -- ^ Input signal+                 -> [a]                      -- ^ Output signal+runNeuralNetwork (NeuralNetwork sx ax m) input =+    let (result, _, _) = runNeuralNetwork' (head sx) (tail sx) ax 0 m [] [] input+    in result++runNeuralNetwork' _ [] _ _ _ ilfacc outacc xs = (xs, ilfacc, outacc)+runNeuralNetwork' _ _ [] _ _ _ _ _ = -- actually should never happen+    error "runNeuralNetwork' - invalid number of activation functions"+runNeuralNetwork' prevs (so:sx) (af:ax) layer ws ilfacc outacc xs =+    let ilfs = [ ( (layer, n), inducedLocalField n layer ws xs) | n <- [1..so] ]+        ilfacc' = ilfs ++ ilfacc+        outs = [ ( (layer, n), x ) | (x, n) <- zip xs [1..prevs] ]+        outacc' = outs ++ outacc+    in runNeuralNetwork' so sx ax (layer+1) ws ilfacc' outacc' (map (\(_, v) -> applyAF af v) ilfs)++inducedLocalField neuron layer ws xs =+    let weight k = getWeight (makeKey layer neuron k) ws+    in  weight 0 + sum [ weight i * x | (x, i) <- zip xs [1..] ]++-- | Run one step of the backpropagation algorithm.+backpropagationOneStep :: (Num a, Floating a)+                       => NeuralNetwork a   -- ^ Current neural network+                       -> a                 -- ^ Learning rate+                       -> [a]               -- ^ Input+                       -> [a]               -- ^ Expected output+                       -> WeightDeltas a    -- ^ Calculated deltas+backpropagationOneStep (NeuralNetwork sx ax wx) learningRate input expout =+    let (result, inducedLocalFields, outputs) = runNeuralNetwork' (head sx) (tail sx) ax 0 wx [] [] input+        errors = [ d - o | (d, o) <- zip expout result ]+        inducedLocalFieldsMap = M.fromList inducedLocalFields+        outputsMap = M.fromList outputs+        deltasMap = calculateDeltas sx ax wx errors inducedLocalFieldsMap+        wdx = M.mapWithKey +                (\k _ ->+                    let (ln, n, i) = decodeKey k+                        out = if i == 0 then 1+                                        else fromJust $ M.lookup (ln, i) outputsMap+                    in learningRate * out * fromJust (M.lookup (ln, n) deltasMap)+                ) wx+    in WeightDeltas wdx++-- | Run backpropagation algorithm in stochastic mode.+backpropagationStochastic :: (Num a, Floating a)+                          => NeuralNetwork a                        -- ^ Neural network+                          -> [([a],[a])]                            -- ^ Trainset: inputs and expected outputs+                          -> a                                      -- ^ Learning rate+                          -> (NeuralNetwork a -> Int -> IO Bool)    -- ^ Stop function, 1st arg - current NN, 2nd arg - generation number+                          -> IO (NeuralNetwork a)                   -- ^ Trained neural network+backpropagationStochastic net0 set0 learningRate stopf = do+    g0 <- newStdGen+    run g0 net0 set0 0+    where+        len = length set0+        run rg net set gnum = do+            let (rg', set') = shuffleList rg len set+                net' = foldl' (\n (i, o) -> let wdx = backpropagationOneStep n learningRate i o+                                            in applyWeightDeltas wdx n) net set'+            stop <- stopf net' gnum+            if stop then return net'+                    else run rg' net' set' (gnum+1)++-- | Run backpropagation algorithm in batch mode. This code runs faster in parallel, so don't forget to use +RTS -N.+backpropagationBatchParallel :: (Num a, Floating a, NFData a) +                             => NeuralNetwork a                     -- ^ Neural network+                             -> [([a],[a])]                         -- ^ Trainset: inputs and expected outputs+                             -> a                                   -- ^ Learning rate+                             -> (NeuralNetwork a -> Int -> IO Bool) -- ^ Stop function, 1st arg - current NN, 2nd arg - generation number+                             -> IO (NeuralNetwork a)                -- ^ Trained neural network+backpropagationBatchParallel net0 set learningRate stopf =+    run net0 0+    where+        chunks = chunksOf ( ceiling $ fromIntegral (length set) / (fromIntegral numCapabilities :: Double) ) set+        run net gnum = do+            let wdx = map (unionWeightDeltas . map (uncurry $ backpropagationOneStep net learningRate)) chunks+                        `using` parList rdeepseq+                totalWdx = unionWeightDeltas wdx+                net' = applyWeightDeltas totalWdx net+            stop <- stopf net' gnum+            if stop then return net'+                    else run net' (gnum+1)++-- | Apply deltas to the neural netwotk.+applyWeightDeltas :: (Num a, Floating a)+                  => WeightDeltas a     -- ^ Deltas+                  -> NeuralNetwork a    -- ^ Neural network+                  -> NeuralNetwork a    -- ^ Neural network with updated weights+applyWeightDeltas (WeightDeltas dwx) (NeuralNetwork sx ax wx) =+    let wx' = M.mapWithKey (\k w -> w + fromJust (M.lookup k dwx)) wx+    in NeuralNetwork sx ax wx'++-- | Union list of deltas into one WeightDeltas.+unionWeightDeltas :: (Num a, Floating a)+                  => [WeightDeltas a]   -- ^ List of WeightDeltas+                  -> WeightDeltas a     -- ^ United WeightDeltas+unionWeightDeltas [] = error "Empty list"+unionWeightDeltas [x] = x+unionWeightDeltas (WeightDeltas hd : tl) =+    let tm = foldl' (\acc (WeightDeltas m) -> M.mapWithKey (\k w -> w + fromJust (M.lookup k m)) acc) hd tl+    in WeightDeltas tm++calculateDeltas sx ax wx errors ilfm =+    let (s:sx') = reverse sx+        (a:ax') = reverse ax+        cl = fromIntegral $ length sx - 2+        acc = M.fromList [ ((cl, n), err * applyAFDerivative a (fromJust $ M.lookup (cl, n) ilfm )) | (err, n) <- zip errors [1..s] ]+    in calculateDeltas' (cl - 1) s sx' ax' wx ilfm acc++calculateDeltas' _ _ _ [] _ _ acc = acc+calculateDeltas' cl sprev sx ax wx ilfm acc = +    let (s:sx') = sx+        (a:ax') = ax+        err n = sum [ fromJust $ (*) <$> M.lookup (cl+1, k) acc <*> M.lookup (makeKey (cl+1) k n) wx | k <- [1..sprev] ]+        newDeltas = [ ((cl, n), err n * applyAFDerivative a (fromJust $ M.lookup (cl, n) ilfm)) | n <- [1..s] ] +        acc' = foldl' (\m (k, v) -> M.insert k v m) acc newDeltas+    in calculateDeltas' (cl - 1) s sx' ax' wx ilfm acc'++-- | Generate random neural network.+randomNeuralNetwork :: (RandomGen g, Random a, Num a, Ord a)+                    => g                        -- ^ RandomGen+                    -> [Word16]                 -- ^ Number of neurons in each layer+                    -> [ ActivationFunction ]   -- ^ Activation functions+                    -> a                        -- ^ Maximum weight; all weights in NN will be between -maxw and maxw+                    -> (NeuralNetwork a, g)     -- ^ Random neural network and new RandomGen+randomNeuralNetwork gen sx ax maxw +    | length sx /= length ax + 1 = error "Number of layers and activation functions mismatch"+    | maxw < 0 = randomNeuralNetwork gen sx ax (-maxw)+    | otherwise =+        let keys = generateKeys sx+            (weights, gen') = generateWeights gen maxw+            ws = M.fromList $ zip keys weights+        in  (NeuralNetwork sx ax ws, gen')++makeKey :: Word16 -> Word16 -> Word16 -> Word64+makeKey layer n i =+    let t1 = fromIntegral layer+        t2 = fromIntegral n+        t3 = fromIntegral i+    in shiftL t1 32 .|. shiftL t2 16 .|. t3++decodeKey :: Word64 -> (Word16, Word16, Word16)+decodeKey k =+    let t1 = fromIntegral $ shiftR k 32+        t2 = fromIntegral $ shiftR k 16 .&. 0xFFFF+        t3 = fromIntegral $ k .&. 0xFFFF+    in (t1, t2, t3)++generateKeys sx =+    [ makeKey layer n i | (layer, inputs, neurons) <- zip3 [0..] (init sx) (tail sx), n <- [1 .. neurons], i <- [0 .. inputs ] ]++generateWeights gen maxw =+    let (gen1, gen2) = split gen+    in (unfoldr (Just . randomR (-maxw, maxw) ) gen1, gen2)++-- | Crossover of two neural networks.+crossoverCommon :: (Num a, RandomGen g)+                => g                        -- ^ RandomGen+                -> NeuralNetwork a          -- ^ First neural network+                -> NeuralNetwork a          -- ^ Second neural network+                -> ([NeuralNetwork a],g)    -- ^ Children and new RandomGen+crossoverCommon g0 (NeuralNetwork sx1 ax1 wx1) (NeuralNetwork _ _ wx2) =+    let keys = generateKeys sx1+        (idx, g1) = randomR (1, length keys - 1) g0+        (keys1, keys2) = splitAt idx keys+        tmpMap wx lst = M.fromList [ (k, getWeight k wx) | k <- lst ]+        wx1' = tmpMap wx1 keys1 `M.union` tmpMap wx2 keys2+        wx2' = tmpMap wx1 keys2 `M.union` tmpMap wx2 keys1+    in ( [ NeuralNetwork sx1 ax1 wx1', NeuralNetwork sx1 ax1 wx2' ], g1)++-- | Another implementation of crossover. Weights of a child are just some function of corresponding parent weights.+crossoverMerge :: (Num a, RandomGen g)+               => (a -> a -> a)         -- ^ Mentioned 'some function'+               -> g                     -- ^ Not used+               -> NeuralNetwork a       -- ^ First neural network+               -> NeuralNetwork a       -- ^ Second neural netwrok+               -> ([NeuralNetwork a],g) -- ^ Children (actually - exactly one child) and exact copy of the 2nd argument+crossoverMerge avgf gen (NeuralNetwork sx1 ax1 wx1) (NeuralNetwork _ _ wx2) =+    let wx' = M.fromList [ (k, getWeight k wx1 `avgf` getWeight k wx2) | k <- generateKeys sx1]+    in  ( [ NeuralNetwork sx1 ax1 wx' ], gen )++-- | Mutate given neural netwrok.+mutationCommon :: (Random a, Num a, RandomGen g)+               => Double                -- ^ Percent of mutating weights, (0.0; 1.0)+               -> a                     -- ^ Maximum weight, mutated weights will be between -maxw and maxw+               -> g                     -- ^ RandomGen+               -> NeuralNetwork a       -- ^ Neural network+               -> (NeuralNetwork a, g)  -- ^ New neural network and RandomGen+mutationCommon percent maxw gen (NeuralNetwork sx ax wx) =+    let layers = length sx - 1+        mutnum = truncate $ percent * fromIntegral (M.size wx) :: Int+        (wx', gen') = mutationCommon' mutnum (abs maxw) gen wx (init sx) (tail sx) layers+    in (NeuralNetwork sx ax wx', gen')++mutationCommon' mutnum maxw g0 wx inputs outputs layers+    | mutnum <= 0 = (wx, g0)+    | otherwise =+        let (layer, g1) = randomR (0, layers - 1) g0+            (neuron, g2) = randomR (1, outputs !! layer) g1+            (weightIdx, g3) = randomR (0, inputs !! layer) g2+            (newWeight, g4) = randomR (- maxw, maxw) g3+            wx' = M.insert (makeKey (fromIntegral layer) neuron weightIdx) newWeight wx+        in mutationCommon' (mutnum - 1) maxw g4 wx' inputs outputs layers++getWeight :: (Num a, Ord k) => k -> M.Map k a -> a+getWeight = M.findWithDefault 0++shuffleList :: (RandomGen g) => g -> Int -> [a] -> (g, [a])+shuffleList g lstlen lst =+    shuffleList' g (lstlen-1) (lstlen-1) (IM.fromList $ zip [0..] lst)++shuffleList' g maxpos step m0+    | step < 0 = (g, map snd $ IM.toList m0)+    | otherwise =+        let (pos, g') = randomR (0, maxpos) g+            v1 = fromJust $ IM.lookup step m0+            v2 = fromJust $ IM.lookup pos m0+            m1 = IM.insert step v2 m0+            m2 = IM.insert pos v1 m1+        in shuffleList' g' maxpos (step-1) m2+
+ src/MainXorLogistic.hs view
@@ -0,0 +1,34 @@+import AI.NeuralNetworks.Simple+import Text.Printf+import System.Random+import Control.Monad++calcXor net x y =+    let [r] = runNeuralNetwork net [x, y]+    in  r++mse net =+    let square x = x * x+        e1 = square $ calcXor net 0 0+        e2 = square $ calcXor net 1 0 - 1+        e3 = square $ calcXor net 0 1 - 1+        e4 = square $ calcXor net 1 1+    in 0.5 * (e1 + e2 + e3 + e4)++stopf best gnum = do+    let e = mse best+    when (gnum `rem` 100 == 0) $+        printf "Generation: %02d, MSE: %.4f\n" gnum e+    return $ e < 0.002 || gnum >= 10000++main = do+    gen <- newStdGen+    let (randomNet, _) = randomNeuralNetwork gen [2,2,1] [Logistic, Logistic] 0.45+        examples = [ ([0,0],[0]), ([0,1],[1]), ([1,0],[1]), ([1,1],[0]) ]+    net <- backpropagationBatchParallel randomNet examples 0.4 stopf :: IO (NeuralNetwork Double)+    putStrLn ""+    putStrLn $ "Result: " ++ show net+    _ <- printf "0 xor 0 = %.4f\n" (calcXor net 0 0)+    _ <- printf "1 xor 0 = %.4f\n" (calcXor net 1 0)+    _ <- printf "0 xor 1 = %.4f\n" (calcXor net 0 1)+    printf "1 xor 1 = %.4f" (calcXor net 1 1)
+ src/MainXorTanh.hs view
@@ -0,0 +1,34 @@+import AI.NeuralNetworks.Simple+import Text.Printf+import System.Random+import Control.Monad++calcXor net x y =+    let [r] = runNeuralNetwork net [x, y]+    in  r++mse net =+    let square x = x * x+        e1 = square $ calcXor net (-1) (-1) - (-1)+        e2 = square $ calcXor net   1  (-1) -   1+        e3 = square $ calcXor net (-1)   1  -   1+        e4 = square $ calcXor net   1    1  - (-1)+    in 0.5 * (e1 + e2 + e3 + e4)++stopf best gnum = do+    let e = mse best+    when (gnum `rem` 100 == 0) $+        printf "Generation: %02d, MSE: %.4f\n" gnum e+    return $ e < 0.002 || gnum >= 10000++main = do+    gen <- newStdGen+    let (randomNet, _) = randomNeuralNetwork gen [2,2,1] [Tanh, Tanh] 0.25+        examples = [ ([-1,-1],[-1]), ([-1,1],[1]), ([1,-1],[1]), ([1,1],[-1]) ]+    net <- backpropagationBatchParallel randomNet examples 0.4 stopf :: IO (NeuralNetwork Double)+    putStrLn ""+    putStrLn $ "Result: " ++ show net+    _ <- printf "0 xor 0 = %.4f\n" (calcXor net (-1) (-1))+    _ <- printf "1 xor 0 = %.4f\n" (calcXor net 1 (-1))+    _ <- printf "0 xor 1 = %.4f\n" (calcXor net (-1) 1)+    printf "1 xor 1 = %.4f" (calcXor net 1 1)