packages feed

clustering 0.1.2 → 0.2.0

raw patch · 15 files changed

+434/−121 lines, 15 filesdep +Rlang-QQdep +paralleldep ~matricesPVP ok

version bump matches the API change (PVP)

Dependencies added: Rlang-QQ, parallel

Dependency ranges changed: matrices

API changes (from Hackage documentation)

- AI.Clustering.Hierarchical: computeDists :: Vector v a => DistFn a -> v a -> DistanceMat
- AI.Clustering.KMeans: data Initialization
- AI.Clustering.KMeans: instance Show KMeans
+ AI.Clustering.Hierarchical: hamming :: (Vector v a, Vector v Bool, Eq a) => DistFn (v a)
+ AI.Clustering.Hierarchical.Internal: average :: DistUpdateFn
+ AI.Clustering.Hierarchical.Internal: complete :: DistUpdateFn
+ AI.Clustering.Hierarchical.Internal: nnChain :: DistanceMat -> DistUpdateFn -> Dendrogram Int
+ AI.Clustering.Hierarchical.Internal: single :: DistUpdateFn
+ AI.Clustering.Hierarchical.Internal: ward :: DistUpdateFn
+ AI.Clustering.Hierarchical.Internal: weighted :: DistUpdateFn
+ AI.Clustering.Hierarchical.Types: computeDists :: Vector v a => DistFn a -> v a -> DistanceMat
+ AI.Clustering.Hierarchical.Types: computeDists' :: Vector v a => DistFn a -> v a -> DistanceMat
+ AI.Clustering.KMeans: data Method
+ AI.Clustering.KMeans: kmeansBy :: (PrimMonad m, Vector v a) => Gen (PrimState m) -> Method -> Int -> v a -> (a -> Vector Double) -> m KMeans
+ AI.Clustering.KMeans: withinSS :: KMeans -> Matrix Double -> [Double]
+ AI.Clustering.KMeans.Internal: forgy :: (PrimMonad m, Vector v a) => Gen (PrimState m) -> Int -> v a -> (a -> Vector Double) -> m (Matrix Double)
+ AI.Clustering.KMeans.Internal: kmeansPP :: (PrimMonad m, Vector v a) => Gen (PrimState m) -> Int -> v a -> (a -> Vector Double) -> m (Matrix Double)
+ AI.Clustering.KMeans.Internal: sumSquares :: Vector Double -> Vector Double -> Double
+ AI.Clustering.KMeans.Types: Forgy :: Method
+ AI.Clustering.KMeans.Types: KMeans :: Vector Int -> Matrix Double -> KMeans
+ AI.Clustering.KMeans.Types: KMeansPP :: Method
+ AI.Clustering.KMeans.Types: _centers :: KMeans -> Matrix Double
+ AI.Clustering.KMeans.Types: _clusters :: KMeans -> Vector Int
+ AI.Clustering.KMeans.Types: data KMeans
+ AI.Clustering.KMeans.Types: data Method
+ AI.Clustering.KMeans.Types: instance Show KMeans
- AI.Clustering.KMeans: Forgy :: Initialization
+ AI.Clustering.KMeans: Forgy :: Method
- AI.Clustering.KMeans: KMeansPP :: Initialization
+ AI.Clustering.KMeans: KMeansPP :: Method
- AI.Clustering.KMeans: kmeans :: PrimMonad m => Gen (PrimState m) -> Initialization -> Int -> Matrix Double -> m KMeans
+ AI.Clustering.KMeans: kmeans :: (PrimMonad m, Matrix mat Vector Double) => Gen (PrimState m) -> Method -> Int -> mat Vector Double -> m KMeans
- AI.Clustering.KMeans: kmeansWith :: Matrix Double -> Matrix Double -> KMeans
+ AI.Clustering.KMeans: kmeansWith :: Vector v a => Matrix Double -> v a -> (a -> Vector Double) -> KMeans

Files

+ benchmarks/Bench/Hierarchical.hs view
@@ -0,0 +1,44 @@+module Bench.Hierarchical+    ( benchHierarchical ) where++import Criterion.Main+import qualified Data.Clustering.Hierarchical as C+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as U+import System.IO.Unsafe (unsafePerformIO)++import AI.Clustering.Hierarchical+import AI.Clustering.Hierarchical.Types++import Bench.Utils++benchHierarchical :: Benchmark+benchHierarchical =+    let dists = computeDists euclidean xs+        fn i j = dists ! (i,j)+        xs = V.fromList $ unsafePerformIO $ randVectors 1000 5+    in bgroup "Hierarchical clustering"+        [ bgroup "AI.Clustering.Hierarchical"+            [ bench "Average Linkage (n = 10)" $+                  whnf (\x -> hclust Average x fn) $! U.enumFromN 0 10+            , bench "Average Linkage (n = 100)" $+                  whnf (\x -> hclust Average x fn) $! U.enumFromN 0 100+            , bench "Average Linkage (n = 500)" $+                  whnf (\x -> hclust Average x fn) $! U.enumFromN 0 500+            ]+        , bgroup "Data.Clustering.Hierarchical"+            [ bench "Average Linkage (n = 10)" $+                  whnf (\x -> C.dendrogram C.UPGMA x fn) $! [0..9]+            , bench "Average Linkage (n = 100)" $+                  whnf (\x -> C.dendrogram C.UPGMA x fn) $! [0..99]+            , bench "Average Linkage (n = 500)" $+                  whnf (\x -> C.dendrogram C.UPGMA x fn) $! [0..499]+            ]++        , bgroup "Distance matrix"+            [ bench "computeDists" $+                  whnf (\x -> computeDists euclidean x) xs+            , bench "computeDists'" $+                  whnf (\x -> computeDists' euclidean x) xs+            ]+        ]
+ benchmarks/Bench/KMeans.hs view
@@ -0,0 +1,31 @@+module Bench.KMeans+    ( benchKMeans ) where++import Criterion.Main+import qualified Data.Matrix.Unboxed as MU+import qualified Data.Vector.Unboxed as U+import System.Random.MWC+import System.IO.Unsafe++import AI.Clustering.KMeans++import Bench.Utils++gen :: GenIO+gen = unsafePerformIO createSystemRandom++dat :: MU.Matrix Double+dat = unsafePerformIO $ fmap MU.fromRows $ randVectors 1000 10++benchKMeans :: Benchmark+benchKMeans = bgroup "KMeans clustering"+    [ bgroup "AI.Clustering.KMeans"+        [ bench "k-means++ (n = 1000, k = 7)" $+            whnfIO $ kmeans' gen KMeansPP 7 dat+        , bench "forgy (n = 1000, k = 7)" $+            whnfIO $ kmeans' gen Forgy 7 dat+        ]+    ]++kmeans' :: GenIO -> Method -> Int -> MU.Matrix Double -> IO (U.Vector Int)+kmeans' g method k = fmap _clusters . kmeans g method k
+ benchmarks/Bench/Utils.hs view
@@ -0,0 +1,14 @@+module Bench.Utils+    ( randVectors+    ) where++import Control.Monad (replicateM)+import qualified Data.Vector.Unboxed as U+import System.Random.MWC++randVectors :: Int  -- ^ number of samples+            -> Int  -- ^ vector length+            -> IO [U.Vector Double]+randVectors n k = do+    g <- createSystemRandom+    replicateM n $ uniformVector g k
benchmarks/bench.hs view
@@ -7,32 +7,11 @@ import AI.Clustering.Hierarchical import AI.Clustering.Hierarchical.Types ((!)) -randSample :: IO [V.Vector Double]-randSample = do-    g <- create-    replicateM 2000 $ uniformVector g 5+import Bench.Hierarchical (benchHierarchical)+import Bench.KMeans (benchKMeans)  main :: IO ()-main = do-    xs <- randSample-    let dists = computeDists euclidean $ V.fromList xs-        fn i j = dists ! (i,j)-    defaultMain-      [ bgroup "AI.Clustering.Hierarchical"-          [ bench "Average Linkage (n = 10)" $-                whnf (\x -> hclust Average x fn) $! V.enumFromN 0 10-          , bench "Average Linkage (n = 100)" $-                whnf (\x -> hclust Average x fn) $! V.enumFromN 0 100-          , bench "Average Linkage (n = 1000)" $-                whnf (\x -> hclust Average x fn) $! V.enumFromN 0 1000-          ]--      , bgroup "Data.Clustering.Hierarchical"-          [ bench "Average Linkage (n = 10)" $-                whnf (\x -> C.dendrogram C.UPGMA x euclidean) $! take 10 xs-          , bench "Average Linkage (n = 100)" $-                whnf (\x -> C.dendrogram C.UPGMA x euclidean) $! take 100 xs-          , bench "Average Linkage (n = 1000)" $-                whnf (\x -> C.dendrogram C.UPGMA x euclidean) $! take 1000 xs-          ]-      ]+main = defaultMain+    [ benchHierarchical+    , benchKMeans+    ]
clustering.cabal view
@@ -2,7 +2,7 @@ -- documentation, see http://haskell.org/cabal/users-guide/  name:                clustering-version:             0.1.2+version:             0.2.0 synopsis:            High performance clustering algorithms description:   Following clutering methods are included in this library:@@ -26,22 +26,26 @@ library   exposed-modules:          AI.Clustering.Hierarchical+    AI.Clustering.Hierarchical.Internal     AI.Clustering.Hierarchical.Types     AI.Clustering.KMeans+    AI.Clustering.KMeans.Internal+    AI.Clustering.KMeans.Types -  other-modules:       -    AI.Clustering.Hierarchical.Internal+--  other-modules:           build-depends:       base >=4.0 && <5.0     , binary     , containers-    , matrices+    , matrices >=0.4.0     , mwc-random+    , parallel     , primitive     , vector    hs-source-dirs:      src+  ghc-options:         -Wall   default-language:    Haskell2010  test-suite test@@ -50,12 +54,15 @@   main-is: test.hs   other-modules:     Test.Hierarchical+    Test.KMeans+    Test.Utils    default-language:    Haskell2010   build-depends:        base     , binary     , mwc-random+    , matrices     , vector     , tasty     , tasty-hunit@@ -63,11 +70,17 @@     , clustering     , hierarchical-clustering     , split+    , Rlang-QQ  benchmark bench   type: exitcode-stdio-1.0   hs-source-dirs: benchmarks+  ghc-options:  -threaded -rtsopts -with-rtsopts=-N2   main-is: bench.hs+  other-modules:+    Bench.Hierarchical+    Bench.KMeans+    Bench.Utils    default-language:    Haskell2010   build-depends: @@ -77,6 +90,7 @@     , vector     , clustering     , hierarchical-clustering+    , matrices  source-repository  head   type: git
src/AI/Clustering/Hierarchical.hs view
@@ -46,8 +46,10 @@     , cutAt     , flatten     , drawDendrogram-    , computeDists++    -- * Distance functions     , euclidean+    , hamming      -- * References     -- $references@@ -56,7 +58,6 @@  import Control.Applicative ((<$>)) import qualified Data.Vector.Generic as G-import qualified Data.Vector.Unboxed as U import Text.Printf (printf)  import AI.Clustering.Hierarchical.Internal@@ -75,7 +76,7 @@ hclust :: G.Vector v a => Linkage -> v a -> DistFn a -> Dendrogram a hclust method xs f = label <$> nnChain dists fn   where-    dists = computeDists f xs+    dists = computeDists' f xs     label i = xs G.! i     fn = case method of         Single -> single@@ -109,17 +110,15 @@     draw (Leaf x) = [x,""]     shift first other = zipWith (++) (first : repeat other) -computeDists :: G.Vector v a => DistFn a -> v a -> DistanceMat-computeDists f vec = DistanceMat n . U.fromList . flip concatMap [0..n-1] $ \i ->-    flip map [i+1..n-1] $ \j -> f (vec `G.unsafeIndex` i) (vec `G.unsafeIndex` j)-  where-    n = G.length vec-{-# INLINE computeDists #-}---- | compute euclidean distance between two points+-- | Compute euclidean distance between two points. euclidean :: G.Vector v Double => DistFn (v Double) euclidean xs ys = sqrt $ G.sum $ G.zipWith (\x y -> (x-y)**2) xs ys {-# INLINE euclidean #-}++-- | Hamming distance.+hamming :: (G.Vector v a, G.Vector v Bool, Eq a) => DistFn (v a)+hamming xs = fromIntegral . G.length . G.filter id . G.zipWith (/=) xs+{-# INLINE hamming #-}  -- $references --
src/AI/Clustering/Hierarchical/Internal.hs view
@@ -1,4 +1,16 @@+--------------------------------------------------------------------------------+-- |+-- Module      :  AI.Clustering.Hierarchical.Internal+-- Copyright   :  (c) 2015 Kai Zhang+-- License     :  MIT+--+-- Maintainer  :  kai@kzhang.org+-- Stability   :  experimental+-- Portability :  portable+--+-------------------------------------------------------------------------------- module AI.Clustering.Hierarchical.Internal+{-# WARNING "To be used by developer only" #-}     ( nnChain     , single     , complete
src/AI/Clustering/Hierarchical/Types.hs view
@@ -7,12 +7,16 @@     , DistanceMat(..)     , (!)     , idx+    , computeDists+    , computeDists'     ) where  import Control.Monad (liftM, liftM4)+import Control.Parallel.Strategies (rdeepseq, parMap) import Data.Binary (Binary, put, get, getWord8) import Data.Bits (shiftR) import qualified Data.Vector.Unboxed as U+import qualified Data.Vector.Generic as G import Data.Word (Word8)  type Distance = Double@@ -59,3 +63,19 @@ idx n i j | i <= j = (i * (2 * n - i - 3)) `shiftR` 1 + j - 1           | otherwise = (j * (2 * n - j - 3)) `shiftR` 1 + i - 1 {-# INLINE idx #-}++-- | compute distance matrix+computeDists :: G.Vector v a => DistFn a -> v a -> DistanceMat+computeDists f vec = DistanceMat n . U.fromList . flip concatMap [0..n-1] $ \i ->+    flip map [i+1..n-1] $ \j -> f (vec `G.unsafeIndex` i) (vec `G.unsafeIndex` j)+  where+    n = G.length vec+{-# INLINE computeDists #-}++-- | compute distance matrix in parallel+computeDists' :: G.Vector v a => DistFn a -> v a -> DistanceMat+computeDists' f vec = DistanceMat n . U.fromList . concat . flip (parMap rdeepseq) [0..n-1] $ \i ->+    flip map [i+1..n-1] $ \j -> f (vec `G.unsafeIndex` i) (vec `G.unsafeIndex` j)+  where+    n = G.length vec+{-# INLINE computeDists' #-}
src/AI/Clustering/KMeans.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE FlexibleContexts #-} -------------------------------------------------------------------------------- -- | -- Module      :  AI.Clustering.KMeans@@ -12,54 +13,72 @@ module AI.Clustering.KMeans     ( KMeans(..)     , kmeans+    , kmeansBy     , kmeansWith      -- * Initialization methods-    , Initialization(..)+    , Method(..) +    -- * Useful functions     , decode+    , withinSS++    -- * References+    -- $references     ) where  import Control.Monad (forM_) import Control.Monad.Primitive (PrimMonad, PrimState) import qualified Data.Matrix.Unboxed as MU+import qualified Data.Matrix.Generic as MG import qualified Data.Matrix.Unboxed.Mutable as MM import Data.Ord (comparing) import qualified Data.Vector as V+import qualified Data.Vector.Generic as G import qualified Data.Vector.Mutable as VM import qualified Data.Vector.Unboxed as U import qualified Data.Vector.Unboxed.Mutable as UM-import Data.List (minimumBy, nub)-import System.Random.MWC (uniformR, Gen)+import Data.List (minimumBy, foldl')+import System.Random.MWC (Gen) --- | Results from running kmeans-data KMeans = KMeans-    { _clusters :: U.Vector Int     -- ^ A vector of integers (0 ~ k-1)-                                    -- indicating the cluster to which each-                                    -- point is allocated.-    , _centers :: MU.Matrix Double  -- ^ A matrix of cluster centres.-    } deriving (Show)+import AI.Clustering.KMeans.Types (KMeans(..), Method(..))+import AI.Clustering.KMeans.Internal (sumSquares, forgy, kmeansPP) --- | Lloyd's algorithm, also known as K-means algorithm-kmeans :: PrimMonad m+-- | Perform K-means clustering+kmeans :: (PrimMonad m, MG.Matrix mat U.Vector Double)        => Gen (PrimState m)-       -> Initialization-       -> Int                           -- ^ number of clusters-       -> MU.Matrix Double             -- ^ each row represents a point+       -> Method+       -> Int+       -> mat U.Vector Double        -> m KMeans-kmeans g method k mat = do-    initial <- case method of-        Forgy -> forgy g k mat-        _ -> undefined-    return $ kmeansWith initial mat+kmeans g method k mat = kmeansBy g method k dat (MG.takeRow mat)+  where+    dat = U.enumFromN 0 $ MG.rows mat {-# INLINE kmeans #-} --- | Lloyd's algorithm, also known as K-means algorithm-kmeansWith :: MU.Matrix Double   -- ^ initial set of k centroids-           -> MU.Matrix Double   -- ^ each row represents a point+-- | K-means algorithm+kmeansBy :: (PrimMonad m, G.Vector v a)+         => Gen (PrimState m)+         -> Method+         -> Int                   -- ^ number of clusters+         -> v a                   -- ^ data stores in rows+         -> (a -> U.Vector Double)+         -> m KMeans+kmeansBy g method k dat fn = do+    initial <- case method of+        Forgy -> forgy g k dat fn+        KMeansPP -> kmeansPP g k dat fn+    return $ kmeansWith initial dat fn+{-# INLINE kmeansBy #-}++-- | K-means algorithm+kmeansWith :: G.Vector v a+           => MU.Matrix Double   -- ^ initial set of k centroids+           -> v a                -- ^ each row represents a point+           -> (a -> U.Vector Double)            -> KMeans-kmeansWith initial mat | d /= MU.cols initial || k > n = error "check input"-                       | otherwise = KMeans member centers+kmeansWith initial dat fn | d /= MU.cols initial || k > n = error "check input"+                          | otherwise = KMeans member centers   where     (member, centers) = loop initial U.empty     loop means membership@@ -70,18 +89,20 @@      -- Assignment step     assign means = U.generate n $ \i ->-        let x = MU.takeRow mat i-        in fst $ minimumBy (comparing snd) $ zip [0..k-1] $ map (dist x) $ MU.toRows means+        let x = fn $ G.unsafeIndex dat i+        in fst $ minimumBy (comparing snd) $ zip [0..k-1] $ map (sumSquares x) $ MU.toRows means      -- Update step-    update membership = MM.create $ do+    update membership = MU.create $ do         m <- MM.replicate (k,d) 0.0         count <- UM.replicate k (0 :: Int)         forM_ [0..n-1] $ \i -> do             let x = membership U.! i             UM.unsafeRead count x >>= UM.unsafeWrite count x . (+1)++            let vec = fn $ dat G.! i             forM_ [0..d-1] $ \j ->-                MM.unsafeRead m (x,j) >>= MM.unsafeWrite m (x,j) . (+ mat MU.! (i,j))+                MM.unsafeRead m (x,j) >>= MM.unsafeWrite m (x,j) . (+ (vec U.! j))         -- normalize         forM_ [0..k-1] $ \i -> do             c <- UM.unsafeRead count i@@ -89,48 +110,11 @@                 MM.unsafeRead m (i,j) >>= MM.unsafeWrite m (i,j) . (/fromIntegral c)         return m -    dist xs = U.sum . U.zipWith (\x y -> (x - y)**2) xs--    n = MU.rows mat+    n = G.length dat     k = MU.rows initial-    d = MU.cols mat+    d = MU.cols initial {-# INLINE kmeansWith #-} --- | Different initialization methods-data Initialization = Forgy    -- ^ The Forgy method randomly chooses k unique-                               -- observations from the data set and uses these-                               -- as the initial means-                    | KMeansPP -- ^ K-means++ algorithm, not implemented.--forgy :: PrimMonad m-      => Gen (PrimState m)-      -> Int                 -- number of clusters-      -> MU.Matrix Double    -- data-      -> m (MU.Matrix Double)-forgy g k mat | k > n = error "k is larger than sample size"-              | otherwise = iter-  where-    iter = do-        vec <- sample g k . U.enumFromN 0 $ n-        let xs = map (MU.takeRow mat) . U.toList $ vec-        if length (nub xs) == length xs-           then return . MU.fromRows $ xs-           else iter-    n = MU.rows mat-{-# INLINE forgy #-}---- random select k samples from a population-sample :: PrimMonad m => Gen (PrimState m) -> Int -> U.Vector Int -> m (U.Vector Int)-sample g k xs = do-    v <- U.thaw xs-    forM_ [0..k-1] $ \i -> do-        j <- uniformR (i, lst) g-        UM.unsafeSwap v i j-    U.unsafeFreeze . UM.take k $ v-  where-    lst = U.length xs - 1-{-# INLINE sample #-}- -- | Assign data to clusters based on KMeans result decode :: KMeans -> [a] -> [[a]] decode result xs = V.toList $ V.create $ do@@ -143,4 +127,16 @@     n = U.maximum membership + 1  -- | Compute within-cluster sum of squares---withinSS :: Matrix+withinSS :: KMeans -> MU.Matrix Double -> [Double]+withinSS result mat = zipWith f (decode result [0 .. MU.rows mat-1]) .+                          MU.toRows . _centers $ result+  where+    f c center = foldl' (+) 0 $ map (sumSquares center . MU.takeRow mat) c+++-- $references+--+-- Arthur, D. and Vassilvitskii, S. (2007). k-means++: the advantages of careful +-- seeding. Proceedings of the eighteenth annual ACM-SIAM symposium on Discrete +-- algorithms. Society for Industrial and Applied Mathematics Philadelphia, PA, +-- USA. pp. 1027–1035.
+ src/AI/Clustering/KMeans/Internal.hs view
@@ -0,0 +1,101 @@+{-# LANGUAGE BangPatterns #-}+--------------------------------------------------------------------------------+-- |+-- Module      :  AI.Clustering.KMeans.Internal+-- Copyright   :  (c) 2015 Kai Zhang+-- License     :  MIT+--+-- Maintainer  :  kai@kzhang.org+-- Stability   :  experimental+-- Portability :  portable+--+-- <module description starting at first column>+--------------------------------------------------------------------------------+module AI.Clustering.KMeans.Internal+{-# WARNING "To be used by developer only" #-}+    ( forgy+    , kmeansPP+    , sumSquares+    ) where++import Control.Monad (forM_)+import Control.Monad.Primitive (PrimMonad, PrimState)+import Data.List (nub)+import qualified Data.Matrix.Unboxed as MU+import qualified Data.Vector.Generic as G+import qualified Data.Vector.Unboxed as U+import qualified Data.Vector.Unboxed.Mutable as UM+import System.Random.MWC (uniformR, Gen)++forgy :: (PrimMonad m, G.Vector v a)+      => Gen (PrimState m)+      -> Int                 -- number of clusters+      -> v a                 -- data+      -> (a -> U.Vector Double)+      -> m (MU.Matrix Double)+forgy g k dat fn | k > n = error "k is larger than sample size"+                 | otherwise = iter+  where+    iter = do+        vec <- randN g k . U.enumFromN 0 $ n+        let xs = map (\i -> fn $ dat `G.unsafeIndex` i) . U.toList $ vec+        if length (nub xs) == length xs+           then return . MU.fromRows $ xs+           else iter+    n = G.length dat+{-# INLINE forgy #-}++kmeansPP :: (PrimMonad m, G.Vector v a)+         => Gen (PrimState m)+         -> Int+         -> v a+         -> (a -> U.Vector Double)+         -> m (MU.Matrix Double)+kmeansPP g k dat fn+    | k > n = error "k is larger than sample size"+    | otherwise = do+        c1 <- uniformR (0,n-1) g+        loop [c1] 1+  where+    loop centers !k'+        | k' == k = return $ MU.fromRows $ map (\i -> fn $ dat `G.unsafeIndex` i) centers+        | otherwise = do+            c' <- chooseWithProb g $ U.map (shortestDist centers) rowIndices+            loop (c':centers) (k'+1)++    n = G.length dat+    rowIndices = U.enumFromN 0 n+    shortestDist centers x = minimum $ map (\i ->+        sumSquares (fn $ dat `G.unsafeIndex` x) (fn $ dat `G.unsafeIndex` i)) centers+{-# INLINE kmeansPP #-}++chooseWithProb :: PrimMonad m+               => Gen (PrimState m)+               -> U.Vector Double    -- ^ weights, may not be normalized+               -> m Int              -- ^ result/index+chooseWithProb g ws = do+    x <- uniformR (0,sum') g+    return $ loop x 0 0+  where+    loop v !cdf !i | cdf' >= v = i+                   | otherwise = loop v cdf' (i+1)+      where cdf' = cdf + ws `U.unsafeIndex` i++    sum' = U.sum ws+{-# INLINE chooseWithProb #-}++-- | Random select k samples from a population+randN :: PrimMonad m => Gen (PrimState m) -> Int -> U.Vector Int -> m (U.Vector Int)+randN g k xs = do+    v <- U.thaw xs+    forM_ [0..k-1] $ \i -> do+        j <- uniformR (i, lst) g+        UM.unsafeSwap v i j+    U.unsafeFreeze . UM.take k $ v+  where+    lst = U.length xs - 1+{-# INLINE randN #-}++sumSquares :: U.Vector Double -> U.Vector Double -> Double+sumSquares xs = U.sum . U.zipWith (\x y -> (x - y)**2) xs+{-# INLINE sumSquares #-}
+ src/AI/Clustering/KMeans/Types.hs view
@@ -0,0 +1,33 @@+--------------------------------------------------------------------------------+-- |+-- Module      :  AI.Clustering.KMeans.Types+-- Copyright   :  (c) 2015 Kai Zhang+-- License     :  MIT+--+-- Maintainer  :  kai@kzhang.org+-- Stability   :  experimental+-- Portability :  portable+--+-- <module description starting at first column>+--------------------------------------------------------------------------------+module AI.Clustering.KMeans.Types+    ( KMeans(..)+    , Method(..)+    ) where++import qualified Data.Matrix.Unboxed as MU+import qualified Data.Vector.Unboxed as U++-- | Results from running kmeans+data KMeans = KMeans+    { _clusters :: U.Vector Int     -- ^ A vector of integers (0 ~ k-1)+                                    -- indicating the cluster to which each+                                    -- point is allocated.+    , _centers :: MU.Matrix Double  -- ^ A matrix of cluster centers.+    } deriving (Show)++-- | Different initialization methods+data Method = Forgy    -- ^ The Forgy method randomly chooses k unique+                       -- observations from the data set and uses these+                       -- as the initial means.+            | KMeansPP -- ^ K-means++ algorithm.
tests/Test/Hierarchical.hs view
@@ -13,6 +13,7 @@ import Test.Tasty.QuickCheck  import AI.Clustering.Hierarchical+import Test.Utils  tests :: TestTree tests = testGroup "Hierarchical:"@@ -23,11 +24,6 @@     , testCase "Weighted Linkage" testWeighted     ] -randSample :: IO [V.Vector Double]-randSample = do-    g <- create-    replicateM 500 $ uniformVector g 5- isEqual :: Eq a => Dendrogram a -> C.Dendrogram a -> Bool isEqual (Leaf x) (C.Leaf x') = x == x' isEqual (Branch _ d x y) (C.Branch d' x' y') = abs (d - d') < 1e-8 &&@@ -36,7 +32,7 @@  testSingle :: Assertion testSingle = do-    xs <- randSample+    xs <- randVectors 500 5     let true = C.dendrogram C.SingleLinkage xs euclidean         test = hclust Single (V.fromList xs) euclidean     assertBool (unlines ["Expect: ", show true, "But see: ", show test]) $@@ -44,7 +40,7 @@  testComplete :: Assertion testComplete = do-    xs <- randSample+    xs <- randVectors 500 5     let true = C.dendrogram C.CompleteLinkage xs euclidean         test = hclust Complete (V.fromList xs) euclidean     assertBool (unlines ["Expect: ", show true, "But see: ", show test]) $@@ -52,7 +48,7 @@  testAverage :: Assertion testAverage = do-    xs <- randSample+    xs <- randVectors 500 5     let true = C.dendrogram C.UPGMA xs euclidean         test = hclust Average (V.fromList xs) euclidean     assertBool (unlines ["Expect: ", show true, "But see: ", show test]) $@@ -60,7 +56,7 @@  testWeighted :: Assertion testWeighted = do-    xs <- randSample+    xs <- randVectors 500 5     let true = C.dendrogram C.FakeAverageLinkage xs euclidean         test = hclust Weighted (V.fromList xs) euclidean     assertBool (unlines ["Expect: ", show true, "But see: ", show test]) $
+ tests/Test/KMeans.hs view
@@ -0,0 +1,57 @@+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE DataKinds #-}+module Test.KMeans+    ( tests+    ) where++import Control.Monad+import qualified Data.Matrix.Unboxed as MU+import qualified Data.Vector.Unboxed as V+import Data.List+import RlangQQ+import System.Random.MWC+import Test.Tasty+import Test.Tasty.HUnit+import Test.Tasty.QuickCheck++import AI.Clustering.KMeans+import AI.Clustering.KMeans.Internal++import Test.Utils++tests :: TestTree+tests = testGroup "KMeans:"+    [ testCase "KMeans" testKMeans+    ]++rKmeans :: Int -> [Double] -> [Double] -> IO [Int]+rKmeans n dat center = do+    o <- [r| x <- matrix(hs_dat, ncol=hs_n,byrow=T);+             y <- matrix(hs_center, ncol=hs_n,byrow=T);+             hs_result <- kmeans(x,y,iter.max=1000000,algorithm="Lloyd")$cluster;+         |]+    let x = Label :: Label "result"+    return $ o .!. x++testKMeans :: Assertion+testKMeans = do+    let n = 2000+        d = 15+        k = 10+    g <- createSystemRandom+    xs <- randVectors n d++    let mat = MU.fromRows xs :: MU.Matrix Double+        dat = V.enumFromN 0 $ MU.rows mat+        fn = MU.takeRow mat++    centers <- kmeansPP g k dat fn++    r <- rKmeans d (MU.toList mat) (MU.toList centers)+    let test = sort $ map sort $ decode result xs+        result = kmeansWith centers dat fn+        true = sort $ map sort $ decode result{_clusters=V.fromList $ map (subtract 1) r} xs+        show' xs = unlines $ map (show . map (unwords . map show . V.toList)) xs++    assertBool ("Expect: " ++ show' true ++ "\nBut saw: " ++ show' test) $+        test == true
+ tests/Test/Utils.hs view
@@ -0,0 +1,14 @@+module Test.Utils+    ( randVectors+    ) where++import Control.Monad (replicateM)+import qualified Data.Vector.Unboxed as U+import System.Random.MWC++randVectors :: Int  -- ^ number of samples+            -> Int  -- ^ vector length+            -> IO [U.Vector Double]+randVectors n k = do+    g <- createSystemRandom+    replicateM n $ uniformVector g k
tests/test.hs view
@@ -1,7 +1,10 @@ import Test.Tasty  import qualified Test.Hierarchical as Hierarchical+import qualified Test.KMeans as KMeans  main :: IO () main = defaultMain $ testGroup "Main"-    [ Hierarchical.tests ]+    [ Hierarchical.tests+    , KMeans.tests+    ]