packages feed

som-7.5.0: test/Data/Datamining/Clustering/SSOMQC.hs

------------------------------------------------------------------------
-- |
-- Module      :  Data.Datamining.Clustering.SSOMQC
-- Copyright   :  (c) Amy de Buitléir 2012-2014
-- License     :  BSD-style
-- Maintainer  :  amy@nualeargais.ie
-- Stability   :  experimental
-- Portability :  portable
--
-- Tests
--
------------------------------------------------------------------------
{-# LANGUAGE MultiParamTypeClasses, TypeFamilies, FlexibleInstances,
    FlexibleContexts #-}
{-# OPTIONS_GHC -fno-warn-type-defaults -fno-warn-orphans #-}

module Data.Datamining.Clustering.SSOMQC
  (
    test
  ) where

import Data.Datamining.Pattern (Pattern, Metric, difference,
  euclideanDistanceSquared, makeSimilar)
import Data.Datamining.Clustering.Classifier(classify,
  classifyAndTrain, reportAndTrain, differences, diffAndTrain, models,
  train, trainBatch)
import Data.Datamining.Clustering.SSOMInternal
import qualified Data.Map.Strict as M

import Control.Applicative
import Data.Function (on)
import Data.List (sort)
import System.Random (Random)
import Test.Framework as TF (Test, testGroup)
import Test.Framework.Providers.QuickCheck2 (testProperty)
import Test.QuickCheck ((==>), Gen, Arbitrary, arbitrary, choose,
  Property, property, sized, suchThat, vectorOf)

positive :: (Num a, Ord a, Arbitrary a) => Gen a
positive = arbitrary `suchThat` (> 0)

instance
  (Random a, Num a, Ord a, Arbitrary a)
  => Arbitrary (Exponential a) where
  arbitrary = do
    r0 <- choose (0,1)
    d <- positive
    return $ Exponential r0 d

prop_Exponential_starts_at_r0
  :: Exponential Double -> Property
prop_Exponential_starts_at_r0 f@(Exponential r0 _)
  = property $ abs (rate f 0 - r0) < 0.01

prop_Exponential_ge_0
  :: Exponential Double -> Double -> Property
prop_Exponential_ge_0 f t
  = property $ rate f t' >= 0
  where t' = abs t

newtype TestPattern = MkPattern Double deriving Show

instance Eq TestPattern where
  (==) = (==) `on` toDouble

instance Ord TestPattern where
  compare = compare `on` toDouble

instance Pattern TestPattern where
  type Metric TestPattern = Double
  difference (MkPattern a) (MkPattern b) = abs (a - b)
  makeSimilar orig@(MkPattern a) r (MkPattern b)
    | r < 0     = error "Negative learning rate"
    | r > 1     = error "Learning rate > 1"
    | r == 1     = orig
    | otherwise = MkPattern (b + delta)
        where diff = a - b
              delta = r*diff

instance Arbitrary TestPattern where
  arbitrary = MkPattern <$> arbitrary

toDouble :: TestPattern -> Double
toDouble (MkPattern a) = a

absDiff :: [TestPattern] -> [TestPattern] -> Double
absDiff xs ys = euclideanDistanceSquared xs' ys'
  where xs' = map toDouble xs
        ys' = map toDouble ys

-- | A classifier and a training set. The training set will consist of
--   @j@ vectors of equal length, where @j@ is the number of patterns
--   the classifier can model. After running through the training set a
--   few times, the classifier should be very accurate at identifying
--   any of those @j@ vectors.
data SSOMandTargets = SSOMandTargets (SSOM (Exponential Double)
  Int Int TestPattern) [TestPattern]
    deriving (Eq, Show)

buildSSOMandTargets
  :: [TestPattern] -> Double -> Double -> [TestPattern] -> SSOMandTargets
buildSSOMandTargets ps r0 d targets =
  SSOMandTargets s targets
    where gm = M.fromList . zip [0..] $ ps
          s = SSOM gm (Exponential r0 d) 0

sizedSSOMandTargets :: Int -> Gen SSOMandTargets
sizedSSOMandTargets n = do
  let len = n + 1
  ps <- vectorOf len arbitrary
  r0 <- choose (0, 1)
  d <- positive
  targets <- vectorOf len arbitrary
  return $ buildSSOMandTargets ps r0 d targets

instance Arbitrary SSOMandTargets where
  arbitrary = sized sizedSSOMandTargets

prop_training_reduces_error :: SSOMandTargets -> Property
prop_training_reduces_error (SSOMandTargets s xs) = errBefore /= 0 ==>
  errAfter < errBefore
    where (bmu, s') = classifyAndTrain s x
          x = head xs
          errBefore = abs $ toDouble x - toDouble (toMap s M.! bmu)
          errAfter = abs $ toDouble x - toDouble (toMap s' M.! bmu)

--   Invoking @diffAndTrain f s p@ should give identical results to
--   @(p `classify` s, train s f p)@.
prop_classifyAndTrainEquiv :: SSOMandTargets -> Property
prop_classifyAndTrainEquiv (SSOMandTargets s ps) = property $
  bmu == s `classify` p && toMap s1 == toMap s2
    where p = head ps
          (bmu, s1) = classifyAndTrain s p
          s2 = train s p

--   Invoking @diffAndTrain f s p@ should give identical results to
--   @(s `diff` p, train s f p)@.
prop_diffAndTrainEquiv :: SSOMandTargets -> Property
prop_diffAndTrainEquiv (SSOMandTargets s ps) = property $
  diffs == s `differences` p && toMap s1 == toMap s2
    where p = head ps
          (diffs, s1) = diffAndTrain s p
          s2 = train s p

--   Invoking @trainNode s (classify s p) p@ should give
--   identical results to @train s p@.
prop_trainNodeEquiv :: SSOMandTargets -> Property
prop_trainNodeEquiv (SSOMandTargets s ps) = property $
  toMap s1 == toMap s2
    where p = head ps
          s1 = trainNode s (classify s p) p
          s2 = train s p

-- | The training set consists of the same vectors in the same order,
--   several times over. So the resulting classifications should consist
--   of the same integers in the same order, over and over.
prop_batch_training_works :: SSOMandTargets -> Property
prop_batch_training_works (SSOMandTargets s xs) = property $
  classifications == (concat . replicate 5) firstSet
  where trainingSet = (concat . replicate 5) xs
        s' = trainBatch s trainingSet
        classifications = map (classify s') trainingSet
        firstSet = take (length xs) classifications

-- | WARNING: This can fail when two nodes are close enough in
--   value so that after training they become identical.
prop_classification_is_consistent :: SSOMandTargets -> Property
prop_classification_is_consistent (SSOMandTargets s (x:_))
  = property $ bmu == bmu'
  where (bmu, _, s') = reportAndTrain s x
        (bmu', _, _) = reportAndTrain s' x
prop_classification_is_consistent _ = error "Should not happen"

-- | Same as SSOMandTargets, except that the initial models and training
--   set are designed to ensure that a single node will NOT train to
--   more than one pattern.
data SpecialSSOMandTargets = SpecialSSOMandTargets (SSOM
  (Exponential Double) Int Int TestPattern) [TestPattern]
    deriving (Eq, Show)

buildSpecialSSOMandTargets
  :: [TestPattern] -> Double -> Double -> [TestPattern]
    -> SpecialSSOMandTargets
buildSpecialSSOMandTargets ps r0 d targets =
  SpecialSSOMandTargets s targets
    where gm = M.fromList . zip [0..] $ ps
          s = SSOM gm (Exponential r0 d) 0

sizedSpecialSSOMandTargets :: Int -> Gen SpecialSSOMandTargets
sizedSpecialSSOMandTargets n = do
  let len = n + 1
  let ps = map MkPattern $ take len [0,100..]
  r0 <- choose (0, 1)
  d <- positive
  let targets = map MkPattern $ take len [5,105..]
  return $ buildSpecialSSOMandTargets ps r0 d targets

instance Arbitrary SpecialSSOMandTargets where
  arbitrary = sized sizedSpecialSSOMandTargets

-- | If we train a classifier once on a set of patterns, where the
--   number of patterns in the set is equal to the number of nodes in
--   the classifier, then the classifier should become a better
--   representation of the training set. The initial models and training
--   set are designed to ensure that a single node will NOT train to
--   more than one pattern (which would render the test invalid).
prop_batch_training_works2 :: SpecialSSOMandTargets -> Property
prop_batch_training_works2 (SpecialSSOMandTargets s xs) =
  errBefore /= 0 ==> errAfter < errBefore
    where s' = trainBatch s xs
          errBefore = absDiff (sort xs) (sort (models s))
          errAfter = absDiff (sort xs) (sort (models s'))

data IncompleteSSOMandTargets = IncompleteSSOMandTargets (SSOM
  (Exponential Double) Int Int TestPattern) [TestPattern] deriving Show

buildIncompleteSSOMandTargets
  :: [TestPattern] -> Double -> Double -> [TestPattern]
    -> IncompleteSSOMandTargets
buildIncompleteSSOMandTargets ps r0 d targets =
  IncompleteSSOMandTargets s targets
    where gm = M.fromList . zip [0..] $ ps
          s = SSOM gm (Exponential r0 d) 0

-- | Same as sizedSSOMandTargets, except some nodes don't have a value.
sizedIncompleteSSOMandTargets :: Int -> Gen IncompleteSSOMandTargets
sizedIncompleteSSOMandTargets n = do
  let len = n + 1
  ps <- vectorOf len arbitrary
  r0 <- choose (0, 1)
  d <- positive
  targets <- vectorOf len arbitrary
  return $ buildIncompleteSSOMandTargets ps r0 d targets

instance Arbitrary IncompleteSSOMandTargets where
  arbitrary = sized sizedIncompleteSSOMandTargets

prop_can_train_incomplete_SSOM :: IncompleteSSOMandTargets -> Property
prop_can_train_incomplete_SSOM (IncompleteSSOMandTargets s xs) = errBefore /= 0 ==>
  errAfter < errBefore
    where (bmu, s') = classifyAndTrain s x
          x = head xs
          errBefore = abs $ toDouble x - toDouble (toMap s M.! bmu)
          errAfter = abs $ toDouble x - toDouble (toMap s' M.! bmu)

test :: Test
test = testGroup "QuickCheck Data.Datamining.Clustering.SSOM"
  [
    testProperty "prop_Exponential_starts_at_r0"
      prop_Exponential_starts_at_r0,
    testProperty "prop_Exponential_ge_0"
      prop_Exponential_ge_0,
    testProperty "prop_training_reduces_error"
      prop_training_reduces_error,
    testProperty "prop_classifyAndTrainEquiv"
      prop_classifyAndTrainEquiv,
    testProperty "prop_diffAndTrainEquiv" prop_diffAndTrainEquiv,
    testProperty "prop_trainNodeEquiv" prop_trainNodeEquiv,
    testProperty "prop_batch_training_works" prop_batch_training_works,
    testProperty "prop_classification_is_consistent"
      prop_classification_is_consistent,
    testProperty "prop_batch_training_works2"
      prop_batch_training_works2,
    testProperty "prop_can_train_incomplete_SSOM"
      prop_can_train_incomplete_SSOM
  ]