hopfield-0.1.0.1: src/Hopfield/TestUtil.hs
{-# LANGUAGE ParallelListComp #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Hopfield.TestUtil where
-- Util functions used for test
-- Mostly properties and generators
import Control.Applicative
import Control.Monad
import Control.Monad.Random
import Data.Vector ((!))
import qualified Data.Vector as V
import Test.QuickCheck
import Hopfield.Hopfield
import Hopfield.Measurement
import Hopfield.Boltzmann.RestrictedBoltzmannMachine
import Hopfield.Util
data Type = H | BM
-- Warning: orphan instance. For more details see:
-- http://stackoverflow.com/questions/3079537/orphaned-instances-in-haskell
-- The way to avoid it is to make a warpper around Vector.
instance (Arbitrary a) => Arbitrary (V.Vector a) where
arbitrary = fmap V.fromList arbitrary
nonempty :: forall a. Gen [a] -> Gen [a]
nonempty = (`suchThat` (not . null))
mapMonad :: Monad m => (a -> b) -> m [a] -> m [b]
mapMonad f m_xs = do
xs <- m_xs
return $ map f xs
-- | Convert a list generator to a vector generator
toGenVector :: Gen [a] -> Gen (V.Vector a)
toGenVector listGen = fmap V.fromList listGen
-- | Generate a random sign (+/- 1)
signGen :: Gen Int
signGen = do
n <- choose (0,1)
return $ n*2 - 1
binaryGen :: Gen Int
binaryGen = do
n <- choose (0,1)
return n
-- | @patternGen n@: Generates patterns of size n
patternGen :: Type -> Int -> Gen Pattern
patternGen H n = toGenVector $ vectorOf n signGen
patternGen BM n = toGenVector $ vectorOf n binaryGen
-- | @patternRangeGen (min, max)@ Generates patterns of size ranging
-- between min and max
patternRangeGen :: Type -> (Int, Int) -> Gen Pattern
patternRangeGen t bounds = choose bounds >>= patternGen t
-- | @boundedListGen g n@: Generates lists (max length n) of the given Gen
boundedListGen :: Gen a -> Int -> Gen [a]
boundedListGen g n = do
len <- choose (0, n)
vectorOf len g
-- | @patListGen t maxPatSize maxPatListSize@ Generates a list of patterns.
-- The size of each pattern is less than maxPatSize.
-- The size odf the list is less than maxPatListSize.
-- The type is required in order to create types specific for Boltzmann,
-- Hopfield etc.
patListGen :: Type -> Int -> Int -> Gen [Pattern]
patListGen t maxPatSize maxPatListSize = do
i <- choose (1, maxPatSize)
nonempty $ boundedListGen (patternGen t i) maxPatListSize
-- | @patternsTupleGen g m1 m2@Generates a tuple of lists, as follows:
-- Uses patListGen to generate 1 list of patterns with length less than m2.
-- The list itself has to have length less than m1.
-- The second element of a tuple is a list of patterns which have the same size
-- as the patterns of the first list.
patternsTupleGen :: Type -> Int -> Int -> Gen ([Pattern], [Pattern])
patternsTupleGen t m1 m2 = do
fst_list <- patListGen t m1 m2
i <- choose (0, m2)
snd_list <- vectorOf i (patternGen t $ V.length $ head fst_list)
return $ (fst_list, snd_list)
-- Generate lists containing only 'x'
sameElemList :: a -> Gen [a]
sameElemList x = do
len <- arbitrary
return $ replicate len x
-- | Generate vectors containing the same element replicated
sameElemVector :: a -> Gen (V.Vector a)
sameElemVector = toGenVector . sameElemList
-- | Produces a matrix with 0's along the diagonal and 1's otherwise
allWeightsSame :: Int -> [[Double]]
allWeightsSame n
= [ [ if i==j then 0 else w | i <- [0..n-1] ] | j <- [0..n-1] ]
where w = (1 :: Int) ./. n
-- | @boundedReplicateGen n g@ Generates lists containing 'g' replicated.
-- The list is bounded in size by n.
boundedReplicateGen :: Int -> Gen a -> Gen [a]
boundedReplicateGen n g = liftM2 replicate (choose (0, n)) g
-- | Replaces the nth element in the list with 'r'
replaceAtN :: Int -> a -> [a] -> [a]
replaceAtN _ _ [] = error "index greater than list size"
replaceAtN 0 r (_:xs) = (r:xs)
replaceAtN n r (x:xs)
| n > 0 = (x:(replaceAtN (n-1) r xs))
| otherwise = error "negative index"
-- | Compute crosstalk term for a pattern and a given neuron
-- @crosstalk hopfield index neuron
-- todo think if it is better to actually pass in the hopfield data
-- strucutre
-- the pattern on which we do this has to be one of the traninig patterns
-- todo error checks
-- note that this is a very basic check
-- one should try and implement the probability error thing as
-- that would give as a good idea of how to
-- scale
crosstalk :: HopfieldData -> Int -> Int -> Int
-- the cross talk term is h(xi k ) - xi k
crosstalk hs index n = computeH (weights hs) pat n - pat ! n
where pat = (patterns hs) !! index
-- | Used as a property to check that patterns which
-- are used to create the network are stable in respect to update
trainingPatsAreFixedPoints :: LearningType -> [Pattern] -> Gen Bool
trainingPatsAreFixedPoints method pats =
and <$> mapM checkFixedPoint [0.. length pats - 1]
where
hs = buildHopfieldData method pats
ws = weights hs
checkFixedPoint index = do
i <- arbitrary
return $ evalRand (update ws (pats !! index)) (mkStdGen i) == Nothing || (not $ checkFixed hs index)
-- | Trains a network using @training_pats@ and then updates each
-- pattern in pats according to the weights of that network.
-- The aim is to check that the energy decreases after each update.
energyDecreasesAfterUpdate :: LearningType -> ([Pattern], [Pattern]) -> Gen Bool
energyDecreasesAfterUpdate method (training_pats, pats)
= and <$> (forM pats $ \pat -> do
i <- arbitrary
return $ evalRand (energyDecreases pat) (mkStdGen i)
)
where
ws = weights $ buildHopfieldData method training_pats
check pat afterPat = energy ws pat >= energy ws afterPat || energy ws afterPat - energy ws pat <= 0.00000001
energyDecreases :: (MonadRandom m) => Pattern -> m Bool
energyDecreases pat = do
maybe_pat <- update ws pat
case maybe_pat of
Nothing -> return True
Just updatedPattern -> return $ check pat updatedPattern
-- TODO mihaela unused?
repeatedUpdateCheck :: LearningType -> ([Pattern], [Pattern]) -> Gen Bool
repeatedUpdateCheck method (training_pats, pats)
= and <$> mapM s pats
where
ws = weights $ buildHopfieldData method training_pats
stopped pat = do
p <- converged_pattern
maybe_new_p <- update ws p
return $ maybe_new_p == Nothing
where
converged_pattern = repeatedUpdate ws pat
s pat = do
i <- arbitrary
return $ evalRand (stopped pat) (mkStdGen i)
-- | @boltzmannBuildGen maxPatSize maxPatListSize max_hidden@
-- Generates the structures required for creating a Boltzmann machine:
-- a list of patterns together with the number of hidden layers,
-- which has to be less than max_hidden.
boltzmannBuildGen :: Int -> Int -> Int -> Gen ([Pattern], Int)
boltzmannBuildGen maxPatSize maxPatListSize max_hidden = do
pats <- patListGen BM maxPatSize maxPatListSize
i <- choose (1, max_hidden)
return $ (pats, i)
-- | Checks that the buildBoltzmann function does not modify
-- the given patterns or the number of hidden layers.
buildBoltzmannCheck :: ([Pattern], Int) -> Gen Bool
buildBoltzmannCheck (pats, nr_h) = do
i <- arbitrary
let bd = evalRand (buildBoltzmannData' pats nr_h) (mkStdGen i)
return $ patternsB bd == pats && nr_hiddenB bd == nr_h
-- | Generates a list of patterns and the number of hidden layers
-- used to train a Boltzmann machine, as well as a generic pattern to
-- recognize on this machine.
boltzmannAndPatGen :: Int -> Int -> Int -> Gen ([Pattern], Int, Pattern)
boltzmannAndPatGen maxPatSize maxPatListSize max_hidden = do
pats_train <- patListGen BM maxPatSize maxPatListSize
i <- choose (1, max_hidden)
pats_check <- patternGen BM (V.length $ pats_train !! 0)
return $ (pats_train, i, pats_check)
-- | @probabilityCheck (pats, nr_h, pat)@. Trains a Boltzmann machine
-- using @pats@ and @nr_h@ and computes the activation probability for
-- @pat@ using this machine, and check if it is indeed a probability,
-- ie in [0,1]
probabilityCheck :: ([Pattern], Int, Pattern) -> Gen Bool
probabilityCheck (pats, nr_h, pat) = do
seed <- arbitrary
let bd = evalRand (buildBoltzmannData' pats nr_h) (mkStdGen seed)
ws = weightsB bd
return $ all (\x -> c $ getActivationProbability Matching Visible ws pat x) [0 .. nr_h - 1]
where c x = x <= 1 && x >=0
-- -- r should only be 0 or 1 for this test
updateNeuronCheck :: Int -> ([Pattern], Int, Pattern) -> Gen Bool
updateNeuronCheck r (pats, nr_h, pat)
| not (r == 0 || r == 1) = error "r has to be 0 or 1 for updateNeuronCheck"
| otherwise = do
i <- choose (0, nr_h -1)
seed <- arbitrary
let bd = evalRand (buildBoltzmannData' pats nr_h) (mkStdGen seed)
return $ updateNeuron' (fromIntegral r) Matching Visible (weightsB bd) pat i == (1 - r)
-- TODO write comment and change the name to show the restrictions
buildIntTuple :: Gen (Int, Int)
buildIntTuple = do
i <- choose (1, 100)
let min_size = ceiling $ log2 $ fromIntegral i
j <- choose (min_size + 1, min_size + 2)
return (i, j)
binaryCheck :: (Int, Int) -> Bool
binaryCheck (x, y) = x == refold
where
refold = sum [ b * 2^pos | b <- reverse bits | pos <- [(0:: Int)..] ]
bits = toBinary x y
-- Runs expressions requiring random numbers (e.g. RandomMonad) in the Gen monad
evalRandGen :: Rand StdGen a -> Gen a
evalRandGen e = do
rndInt <- arbitrary
return $ evalRand e (mkStdGen rndInt)