packages feed

kmeans-vector 0.2 → 0.3

raw patch · 5 files changed

+388/−81 lines, 5 filesdep +QuickCheckdep +criteriondep +kmeans-vectornew-component:exe:kmeans-personsPVP ok

version bump matches the API change (PVP)

Dependencies added: QuickCheck, criterion, kmeans-vector, mtl

API changes (from Hackage documentation)

- Math.KMeans: center :: Cluster -> !Vector Double
- Math.KMeans: cid :: Cluster -> !Int
- Math.KMeans: computeClusters :: [[Vector Double]] -> [Cluster]
- Math.KMeans: data Cluster
- Math.KMeans: type Point a = (Vector Double, a)
+ Math.KMeans: elements :: Cluster a -> [a]
+ Math.KMeans: euclidSq :: Distance
+ Math.KMeans: instance Eq a => Eq (Cluster a)
+ Math.KMeans: instance Show a => Show (Cluster a)
+ Math.KMeans: kmeansWith :: Monad m => (Int -> [a] -> m (Clusters a)) -> (a -> Vector Double) -> Distance -> Int -> [a] -> m (Clusters a)
+ Math.KMeans: l1dist :: Distance
+ Math.KMeans: linfdist :: Distance
+ Math.KMeans: newtype Cluster a
+ Math.KMeans: partition :: Int -> [a] -> Clusters a
+ Math.KMeans: type Centroids = Vector (Vector Double)
+ Math.KMeans: type Clusters a = Vector (Cluster a)
+ Math.KMeans: type Distance = Vector Double -> Vector Double -> Double
- Math.KMeans: Cluster :: !Int -> !Vector Double -> Cluster
+ Math.KMeans: Cluster :: [a] -> Cluster a
- Math.KMeans: kmeans :: Int -> [Point a] -> [[Point a]]
+ Math.KMeans: kmeans :: (a -> Vector Double) -> Distance -> Int -> [a] -> Clusters a

Files

Math/KMeans.hs view
@@ -1,89 +1,170 @@-{-# LANGUAGE BangPatterns, ScopedTypeVariables #-}+{-# LANGUAGE BangPatterns #-}  {- | Module      :  Math.KMeans-Copyright   :  (c) Alp Mestanogullari, Ville Tirronen, 2011-2012+Copyright   :  (c) Alp Mestanogullari, Ville Tirronen, 2011-2014 License     :  BSD3 Maintainer  :  Alp Mestanogullari <alpmestan@gmail.com> Stability   :  experimental -An implementation of the k-means clustering algorithm based on the efficient vector package.+An implementation of the k-means clustering algorithm based on the vector package. +The core functions of this module are 'kmeans' and 'kmeansWith'. See some examples+on <http://github.com/alpmestan/kmeans-vector github>.+ -}+module Math.KMeans+  ( -- * The meat of this package: 'kmeans' +    kmeans+  , kmeansWith -module Math.KMeans (kmeans, Point, Cluster(..), computeClusters) where+  , -- * Types+    Distance+  , Clusters+  , Cluster(..)+  , Centroids +  , -- * Misc.+    partition+  , euclidSq+  , l1dist+  , linfdist+  ) where++import Control.Monad.Identity import qualified Data.Vector.Unboxed as V import qualified Data.Vector as G import qualified Data.List as L import Data.Function (on) ---- * K-Means clustering algorithm+-- | A distance on vectors+type Distance = V.Vector Double -> V.Vector Double -> Double --- | Type holding an object of any type and its associated feature vector-type Point a = (V.Vector Double, a)+-- | The euclidean distance without taking the final square root+--   This would waste cycles without changing the behavior of the algorithm+euclidSq :: Distance+euclidSq v1 v2 = V.sum $ V.zipWith diffsq v1 v2+  where diffsq a b = (a-b)^(2::Int)+{-# INLINE euclidSq #-} --- | Type representing a cluster (group) of vectors by its center and an id-data Cluster = Cluster {-  cid :: !Int,-  center :: !(V.Vector Double)-  } -- deriving (Show,Eq)+-- | L1 distance of two vectors: d(v1, v2) = sum on i of |v1_i - v2_i|+l1dist :: Distance+l1dist v1 v2 = V.sum $ V.zipWith diffabs v1 v2+  where diffabs a b = abs (a - b)+{-# INLINE l1dist #-} --- genVec = V.fromList `fmap` vectorOf 3 arbitrary--- genPts = (flip zip) [0..] `fmap` replicateM 10 genVec--- genClusters = do---    cs <- replicateM 5 genVec---    return (zipWith Cluster [0.. ] cs)------ prop_regroup = forAll genClusters $ \c ->---                forAll genPts $ \v ->---                  s (regroupPoints c v) == s (regroupPoints' c v)---    where---     same xs = length (L.nub xs) == length xs---     s = map L.sort+-- | L-inf distance of two vectors: d(v1, v2) = max |v1_i - v2_i]+linfdist :: Distance+linfdist v1 v2 = V.maximum $ V.zipWith diffabs v1 v2+  where diffabs a b = abs (a - b)+{-# INLINE linfdist #-} +-- | This is what 'kmeans' hands you back. It's just a 'G.Vector' of clusters+--   that will hopefully be of length 'k'.+type Clusters a = G.Vector (Cluster a) -{-#INLINE distance#-}-distance :: Point a -> V.Vector Double -> Double-distance (u,_) v = V.sum $ V.zipWith (\a b -> (a - b)^2) u v+-- | This type is used internally by 'kmeans'. It represents our (hopefully)+--   @k@ centroids, obtained by computing the new centroids of a 'Cluster'+type Centroids  = G.Vector (V.Vector Double) -partition :: Int -> [a] -> [[a]]-partition k vs = go vs-  where go vs = case L.splitAt n vs of-          (vs', []) -> [vs']-          (vs', vss) -> vs' : go vss-        n = (length vs + k - 1) `div` k+-- | A 'Cluster' of points is just a list of points+newtype Cluster a = +  Cluster { elements :: [a] -- ^ elements that belong to that cluster+          } deriving (Eq, Show) -{-#INLINE computeClusters#-}-computeClusters :: [[V.Vector Double]] -> [Cluster]-computeClusters = zipWith Cluster [0..] . map f-  where f (x:xs) = let (n, v) = L.foldl' (\(k, s) v' -> (k+1, V.zipWith (+) s v')) (1, x) xs-                   in V.map (\x -> x / (fromIntegral n)) v+clusterAdd :: Cluster a -> a -> Cluster a+clusterAdd (Cluster c) x = Cluster (x:c) -{-#INLINE regroupPoints#-}-regroupPoints :: forall a. [Cluster] -> [Point a] -> [[Point a]]-regroupPoints clusters points = L.filter (not.null) . G.toList . G.accum (flip (:)) (G.replicate (length clusters) []) . map closest $ points- where-   closest p = (cid (L.minimumBy (compare `on` (distance p . center)) clusters),p)+emptyCluster :: Cluster a+emptyCluster = Cluster [] -regroupPoints' :: forall a. [Cluster] -> [Point a] -> [[Point a]]-regroupPoints' clusters points = go points-  where go points = map (map snd) . L.groupBy ((==) `on` fst) . L.sortBy (compare `on` fst) $ map (\p -> (closest p, p)) points-        closest p = cid $ L.minimumBy (compare `on` (distance p . center)) clusters+addCentroids :: V.Vector Double -> V.Vector Double -> V.Vector Double+addCentroids v1 v2 = V.zipWith (+) v1 v2 -kmeansStep :: [Point a] -> [[Point a]] -> [[Point a]]-kmeansStep points pgroups = regroupPoints (computeClusters . map (map fst) $ pgroups) points+-- | This is the current partitionning strategy used+--   by 'kmeans'. If we want @k@ clusters, we just +--   try to regroup consecutive elements in @k@ buckets+partition :: Int -> [a] -> Clusters a+partition k vs = G.fromList $ go vs+  where go l = case L.splitAt n l of+          (vs', []) -> [Cluster vs']+          (vs', vss) -> Cluster vs' : go vss+        n = (length vs + k - 1) `div` k -kmeansAux :: [Point a] -> [[Point a]] -> [[Point a]]-kmeansAux points pgroups = let pss = kmeansStep points pgroups in-  case map (map fst) pss == map (map fst) pgroups of-  True -> pgroups-  False -> kmeansAux points pss+-- | Run the kmeans clustering algorithm.+-- +--  > kmeans f distance k points+-- +-- will run the algorithm using 'f' to extract features from your type,+-- using 'distance' to measure the distance between vectors,+-- trying to separate 'points' in 'k' clusters.+--+-- Extracting features just means getting a 'V.Vector'+-- with 'Double' coordinates that will represent your type+-- in the space in which 'kmeans' will run.+kmeans :: (a -> V.Vector Double) -- ^ feature extraction+       -> Distance               -- ^ distance function+       -> Int                    -- ^ the 'k' to run 'k'-means with (i.e number of desired clusters)+       -> [a]                    -- ^ input list of 'points'+       -> Clusters a             -- ^ result, hopefully 'k' clusters of points+kmeans extract dist k points = +  runIdentity $ kmeansWith (\n ps -> return $ partition n ps) extract dist k points --- | Performs the k-means clustering algorithm---   using trying to use 'k' clusters on the given list of points-kmeans :: Int -> [Point a] -> [[Point a]]-kmeans k points = kmeansAux points pgroups-  where pgroups = partition k points+-- | Same as 'kmeans', except that instead of using 'partition', you supply your own+--   function for choosing the initial clustering. Two important things to note:+-- +--   * If you don't need any kind of effect and just have a 'partition'-like function+--     you want to use, @m@ will can just be 'Identity' here. If that's too +--     obnoxious to work with, please let me know and I may just provide a separate+--     'kmeansWith' function with no there. But most of the time, you'll probably just+--     be interested in the following scenario.+-- +--   * Most likely, you want to have something smarter than our simple 'partition' function.+--     A couple of papers I have read claim very decent results by using some precise+--     probabilistic schemas for the initial partitionning. In this case, your @m@ would+--     probably be 'IO' or 'ST' (e.g using my <http://hackage.haskell.org/package/probable probable> package)+--     and you could fine-tune the way the initial clusters are picked so that the algorithm+--     may give better results. Of course, if your initialization is monadic, so is the result. +kmeansWith :: Monad m+           => (Int -> [a] -> m (Clusters a)) -- ^ how should we partition the points?+           -> (a -> V.Vector Double)         -- ^ get the coordinates of a "point"+           -> Distance                       -- ^ what distance do we use+           -> Int                            -- ^ number of desired clusters+           -> [a]                            -- ^ list of points+           -> m (Clusters a)                 -- ^ resulting clustering+kmeansWith initF extract dist k points = go `liftM` initF k points+  +  where +    -- go :: Clusters a -> Clusters a+    go pgroups =+      case kmeansStep pgroups of+        pgroups' | pgroupsEqualUnder pgroups pgroups'  -> pgroups+                 | otherwise -> go pgroups'  +    -- kmeansStep :: Clusters a -> Clusters a+    kmeansStep clusters = +      case centroidsOf clusters of+        centroids -> +            G.filter (not . null . elements)+          . G.unsafeAccum clusterAdd (G.replicate k emptyCluster)+          . map (pairToClosestCentroid centroids)+          $ points +    -- centroidsOf :: Clusters a -> Centroids+    centroidsOf cs = G.map centroidOf cs+      where +        n = fromIntegral $ G.length cs++        centroidOf (Cluster elts) = +            V.map (/n) +          . L.foldl1' addCentroids+          $ map extract elts++    -- pairToClosestCentroid :: Centroids -> a -> (Int, a)+    pairToClosestCentroid cs a = (minDistIndex, a)+      where !minDistIndex = G.minIndexBy (compare `on` dist (extract a)) cs++    -- pgroupsEqualUnder :: Clusters a -> Clusters a -> Bool+    pgroupsEqualUnder g1 g2 = +      G.map (map extract . elements) g1 == G.map (map extract . elements) g2+{-# INLINE kmeansWith #-}
+ bench/OldKmeans.hs view
@@ -0,0 +1,93 @@+{-# LANGUAGE BangPatterns, ScopedTypeVariables #-}++{- |+Module      :  Math.KMeans+Copyright   :  (c) Alp Mestanogullari, Ville Tirronen, 2011-2014+License     :  BSD3+Maintainer  :  Alp Mestanogullari <alpmestan@gmail.com>+Stability   :  experimental++An implementation of the k-means clustering algorithm based on the efficient vector package.++-}++module OldKMeans (kmeans, Point, Cluster(..), computeClusters) where++import qualified Data.Vector.Unboxed as V+import qualified Data.Vector as G+import qualified Data.List as L+import Data.Function (on)++--- * K-Means clustering algorithm++-- | Type holding an object of any type and its associated feature vector+type Point a = (V.Vector Double, a)++-- | Type representing a cluster (group) of vectors by its center and an id+data Cluster = Cluster {+  cid    :: {-# UNPACK #-} !Int, -- ^ an identifier for the cluster+  center :: !(V.Vector Double)   -- ^ the 'position' of the center of the cluster+  } -- deriving (Show,Eq)++-- genVec = V.fromList `fmap` vectorOf 3 arbitrary+-- genPts = (flip zip) [0..] `fmap` replicateM 10 genVec+-- genClusters = do+--    cs <- replicateM 5 genVec+--    return (zipWith Cluster [0.. ] cs)+--+-- prop_regroup = forAll genClusters $ \c ->+--                forAll genPts $ \v ->+--                  s (regroupPoints c v) == s (regroupPoints' c v)+--    where+--     same xs = length (L.nub xs) == length xs+--     s = map L.sort+++{-# INLINE distance #-}+distance :: Point a -> V.Vector Double -> Double+distance (u,_) v = V.sum $ V.zipWith (\a b -> (a - b)^2) u v++partition :: Int -> [a] -> [[a]]+partition k vs = go vs+  where go vs = case L.splitAt n vs of+          (vs', []) -> [vs']+          (vs', vss) -> vs' : go vss+        n = (length vs + k - 1) `div` k++{-#INLINE computeClusters#-}+computeClusters :: [[V.Vector Double]] -> [Cluster]+computeClusters = zipWith Cluster [0..] . map f+  where f (x:xs) = let (n, v) = L.foldl' (\(k, s) v' -> (k+1, V.zipWith (+) s v')) (1, x) xs+                   in V.map (\x -> x / (fromIntegral n)) v++{-#INLINE regroupPoints#-}+regroupPoints :: forall a. [Cluster] -> [Point a] -> [[Point a]]+regroupPoints clusters points = L.filter (not.null) . G.toList . G.accum (flip (:)) (G.replicate (length clusters) []) . map closest $ points+ where+   closest p = (cid (L.minimumBy (compare `on` (distance p . center)) clusters),p)++regroupPoints' :: [Cluster] -> [Point a] -> [[Point a]]+regroupPoints' clusters points = go points+  where go points = map (map snd) . L.groupBy ((==) `on` fst) . L.sortBy (compare `on` fst) $ map (\p -> (closest p, p)) points+        closest p = cid $ L.minimumBy (compare `on` (distance p . center)) clusters++kmeansStep :: [Point a] -> [[Point a]] -> [[Point a]]+kmeansStep points pgroups = +  regroupPoints (computeClusters . map (map fst) $ pgroups) points++kmeansAux :: [Point a] -> [[Point a]] -> [[Point a]]+kmeansAux points pgroups = let pss = kmeansStep points pgroups in+  -- has anything changed since the last step?+  -- even a point jumping from one cluster to another is enough to+  -- enter the 'False' case+  case map (map fst) pss == map (map fst) pgroups of+  True -> pgroups -- nothing's changed, we're done+  False -> kmeansAux points pss -- something has changed, so let's try again++-- | Performs the k-means clustering algorithm+--   trying to use 'k' clusters on the given list of points+kmeans :: Int -> [Point a] -> [[Point a]]+kmeans k points = kmeansAux points pgroups+  where pgroups = partition k points+{-# INLINE kmeans #-}+
+ bench/bench.hs view
@@ -0,0 +1,71 @@+module Main where++import Control.Applicative+import Criterion.Main+import Test.QuickCheck++import qualified Data.Vector  as G+import qualified Data.Vector.Unboxed as V++import qualified OldKMeans   as K+import qualified Math.KMeans as K2++main :: IO ()+main = do +	persons1 <- generate persons+	persons2 <- generate persons++	defaultMain +		[ +		  bgroup "ints" [ bench "v0.2" $ whnf kmeans1 ints1+	                    , bench "v0.3" $ whnf kmeans2 ints2 +	                    ]+	    , bgroup "persons" [ bench "v0.2" $ whnf kmeansP1 persons1 +	    				   , bench "v0.3" $ whnf kmeansP2 persons2]+	    ]++ints1, ints2 :: [Int]+ints1 = [1..10000]+ints2 = [1..10000]++data Person = Person +	{ age    :: Int+	, weight :: Double+	, name   :: String+	, salary :: Int+	} deriving (Eq, Show)++instance Arbitrary Person where+	arbitrary = do+		Person <$> choose (2, 100)+			   <*> choose (5, 150)+			   <*> pure "francis"+			   <*> choose (500, 100000)++persons :: Gen [Person]+persons = vector 10000++-- kmeans of 'Int's in 3 clusters+kmeans1 = G.fromList . K.kmeans  3 . map (\i -> (extract i, i))+kmeans2 = K2.kmeans extract dist 3++-- kmeans of 'Person's in 4 clusters+kmeansP1 = G.fromList . K.kmeans 4 . map p2v+	where p2v p = (personToVec p, p)+kmeansP2 = K2.kmeans personToVec eucl 4++personToVec :: Person -> V.Vector Double+personToVec p = V.fromList +	[ fromIntegral $ age p +	, weight p +	, fromIntegral $ salary p+	]++extract :: Int -> V.Vector Double+extract = V.singleton . fromIntegral++dist :: K2.Distance+dist v1 v2 = V.sum $ V.zipWith (\x1 x2 -> abs (x1 - x2)) v1 v2++eucl :: K2.Distance+eucl v1 v2 = V.sum $ V.zipWith (\x1 x2 -> (x1 - x2)^2) v1 v2
+ examples/persons.hs view
@@ -0,0 +1,53 @@+import Control.Applicative+import Control.Monad+import Math.KMeans+import Test.QuickCheck++import qualified Data.Vector.Unboxed as V+import qualified Data.Vector         as G++data Person = Person +    { age    :: Int+    , weight :: Double+    , name   :: String+    , salary :: Int+    } deriving (Eq)++instance Show Person where+    show p = "<" ++ name p ++ ", " +          ++ show (weight p) ++ "kg, " +          ++ show (salary p) ++ "€/month, "+          ++ show (age p) ++ "y.o>"++instance Arbitrary Person where+    arbitrary = do+        Person <$> choose (2, 100)+               <*> choose (5, 150)+               <*> pure "francis"+               <*> choose (500, 100000)++persons :: Gen [Person]+persons = vector 5++d :: Distance+d v1 v2 = V.sum $ V.zipWith (\x1 x2 -> abs (x1 - x2)) v1 v2++personToVec :: Person -> V.Vector Double+personToVec p = V.fromList +    [ fromIntegral $ age p +    , weight p +    , fromIntegral $ salary p+    ]++runKMeans :: [Person] -> Clusters Person+runKMeans = kmeans personToVec d 2++main :: IO ()+main = do+    ps <- generate persons+    print ps++    let clusters = runKMeans ps+    putStrLn $ show (G.length clusters)+            ++ " cluster(s) found."+    G.mapM_ print clusters
kmeans-vector.cabal view
@@ -1,39 +1,48 @@ Name:                kmeans-vector-Version:             0.2+Version:             0.3 Synopsis:            An implementation of the kmeans clustering algorithm based on the vector package-Description:         Provides a simple (but efficient) implementation of the k-means clustering algorithm. The goal of this algorithm is to, given a list of n-dimensional points, regroup them in k groups, such that each point gets to be in the group to which it is the closest to (using the 'center' of the group).+Description:         Provides a simple (but efficient) implementation of the k-means clustering algorithm. The goal of this algorithm is to, given a set of n-dimensional points, regroup them in k groups, such that each point gets to be in the group to which it is the closest to (using the 'center' of the group).                      .                      CHANGELOG                      .-                     kmeans-vector-0.2 supports having feature vectors associated to objects, and thus computing kmeans on these vectors, letting you recover the initial objects.-+                     0.3: total rewrite of the code, the code scales much better on big inputs and is overall+                     consistently faster than the other kmeans implementations on hackage, on my laptop.+                     0.2: supports having feature vectors associated to objects, and thus computing kmeans on these vectors, letting you recover the initial objects. Homepage:            http://github.com/alpmestan/kmeans-vector--Bug-reports:	     https://github.com/alpmestan/kmeans-vector/issues-+Bug-reports:	       https://github.com/alpmestan/kmeans-vector/issues License:             BSD3- License-file:        LICENSE- Author:              Alp Mestanogullari <alpmestan@gmail.com>, Ville Tirronen- Maintainer:          Alp Mestanogullari <alpmestan@gmail.com>--Copyright:           2011-2012 Alp Mestanogullari--Stability:	     Experimental-+Copyright:           2011-2014 Alp Mestanogullari+Stability:	         Experimental Category:            Math- Build-type:          Simple+Cabal-version:       >=1.8 -Cabal-version:       >=1.6+library+  Exposed-modules:   Math.KMeans+  Build-depends:     base >= 4 && < 5, vector >= 0.7, mtl >= 2.1+  ghc-prof-options:  -prof -auto-all+  ghc-options: 	     -O2 -funbox-strict-fields -Wall -Library-  Exposed-modules:     Math.KMeans-  Build-depends:       base >= 4 && < 5, vector >= 0.7-  ghc-prof-options:    -prof -auto-all-  ghc-options: 	       -O2 -funbox-strict-fields+executable kmeans-persons+  main-is:           persons.hs+  hs-source-dirs:    examples+  ghc-options:       -O2 -funbox-strict-fields+  build-depends:     base >= 4 && < 5, vector >= 0.7, kmeans-vector, QuickCheck++benchmark bench+  main-is:           bench.hs+  other-modules:     OldKmeans+  hs-source-dirs:    bench+  ghc-options:       -O2 -funbox-strict-fields+  type:              exitcode-stdio-1.0+  build-depends:     base >= 4 && < 5,+                     vector >= 0.7,+                     kmeans-vector,+                     criterion,+                     QuickCheck  source-repository head   type: git