Created
March 6, 2014 17:16
-
-
Save tmcdonell/9394648 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{-# LANGUAGE TypeOperators #-} | |
{-# LANGUAGE ScopedTypeVariables #-} | |
import Prelude as P | |
import Data.Array.Accelerate as A | |
ok :: Acc (Vector (Point Float)) | |
-> Acc (Vector (Cluster Float)) | |
-> Acc (Vector (Cluster Float)) | |
ok = kmeans 1 | |
fail1 :: Acc (Vector (Cluster Float)) | |
-> Acc (Vector (Cluster Float)) | |
fail1 = | |
let points = use (fromList (Z:.0) []) | |
in kmeans 1 points | |
fail2 :: Acc (Vector (Point Float), Vector (Cluster Float)) | |
-> Acc (Vector (Cluster Float)) | |
fail2 = | |
A.uncurry (kmeans 1) | |
-- This implementation works on 2D points. In future, generalise this to some | |
-- arbitrary "feature vector". | |
-- | |
type Point a = (a, a) | |
-- Clusters consist of the centroid location as well as its identifier | |
-- | |
type Id = Word32 | |
type Cluster a = (Id, (a, a)) | |
idOfCluster :: Elt a => Exp (Cluster a) -> Exp Id | |
idOfCluster = A.fst | |
centroidOfCluster :: Elt a => Exp (Cluster a) -> Exp (Point a) | |
centroidOfCluster = A.snd | |
-- We'll use this as an intermediate structure; it contains the number of points | |
-- in the set as well as the sum of the x and y coordinates respectively. | |
-- | |
type PointSum a = (Word32, (a, a)) | |
-- Get the distance (squared) between two points. Since we only compare this | |
-- value, we elide the square root. | |
-- | |
distance :: (Elt a, IsNum a) => Exp (Point a) -> Exp (Point a) -> Exp a | |
distance u v = | |
let (x1,y1) = unlift u | |
(x2,y2) = unlift v | |
in | |
(x1-x2)^(2::Int) + (y1-y2)^(2::Int) | |
-- For each of the given points, return the cluster Id that that that point is | |
-- closest to. | |
-- | |
findClosestCluster | |
:: forall a. (Elt a, IsFloating a, RealFloat a) | |
=> Acc (Vector (Cluster a)) | |
-> Acc (Vector (Point a)) | |
-> Acc (Vector Id) | |
findClosestCluster clusters points = | |
A.map (\p -> A.fst $ A.sfoldl (nearest p) z (constant Z) clusters) points | |
where | |
z = constant (-1, inf) | |
nearest :: Exp (Point a) -> Exp (Id, a) -> Exp (Cluster a) -> Exp (Id, a) | |
nearest p st c = | |
let d = A.snd st | |
d' = distance p (centroidOfCluster c) | |
in | |
d' <* d ? ( lift (idOfCluster c, d') , st ) | |
-- Given a vector of points and a vector of clusters we, we first locate the | |
-- closest cluster to each point, assign that point to their closest cluster, | |
-- and compute the centroid of the cluster. This yields the new centroid | |
-- locations. | |
-- | |
makeNewClusters | |
:: forall a. (Elt a, IsFloating a, RealFloat a) | |
=> Int | |
-> Acc (Vector (Point a)) | |
-> Acc (Vector (Cluster a)) | |
-> Acc (Vector (Cluster a)) | |
makeNewClusters nclusters points clusters | |
-- TLM: This works fine if we combine everything into a single pipeline. | |
-- | |
-- = pointSumToCluster | |
-- . makePointSum | |
-- . findClosestCluster clusters | |
-- $ points | |
-- TLM: This is the program I want to write, because it forces the result of | |
-- findClosestCluster, thereby reducing memory traffic (due to a different | |
-- accelerate bug: #116) | |
-- | |
-- However, there is a sharing recovery bug _only_ when 'kmeans' is turned | |
-- into a function of one argument (either fail1 or fail2 above) which is | |
-- required to use 'run1'. | |
-- | |
= findClosestCluster clusters >-> pointSumToCluster . makePointSum $ points | |
where | |
npts = size points | |
-- Turn the PointSum intermediate structure into the clusters, by averaging | |
-- the cumulative (x,y) positions. | |
-- | |
pointSumToCluster :: Acc (Vector (PointSum a)) -> Acc (Vector (Cluster a)) | |
pointSumToCluster ps = | |
A.generate (A.shape ps) | |
(\ix -> lift (A.fromIntegral (unindex1 ix), average (ps ! ix))) | |
average :: Exp (PointSum a) -> Exp (Point a) | |
average ps = | |
let (n, xy) = unlift ps :: (Exp Word32, Exp (Point a)) | |
(x, y) = unlift xy | |
in | |
lift (x / A.fromIntegral n, y / A.fromIntegral n) -- TLM: what if there are no points in the cluster?? | |
-- Reduce along the rows of 'pointSum' to get the cumulative (x,y) position | |
-- and number of points assigned to each centroid. | |
-- | |
makePointSum :: Acc (Vector Id) -> Acc (Vector (PointSum a)) | |
makePointSum = A.fold1 addPointSum . pointSum | |
-- The point sum is an intermediate 2D array (it gets fused away, so does | |
-- not exist in memory). The points are laid out along the innermost | |
-- dimension (rows), and down the column is each of the clusters. | |
-- | |
-- For each point, we put its (x,y) coordinates into the row corresponding | |
-- to whichever cluster it is closest to, and zeros in each of the other | |
-- rows. | |
-- | |
pointSum :: Acc (Vector Id) -> Acc (Array DIM2 (PointSum a)) | |
pointSum nearest = | |
A.generate (lift (Z:.constant nclusters:.npts)) | |
(\ix -> let Z:.i:.j = unlift ix :: Z :. Exp Int :. Exp Int | |
near = nearest ! index1 j | |
yes = lift (constant 1, points ! index1 j) | |
no = constant (0, (0,0)) | |
in | |
near ==* A.fromIntegral i ? ( yes, no )) | |
addPointSum :: Exp (PointSum a) -> Exp (PointSum a) -> Exp (PointSum a) | |
addPointSum x y = | |
let (c1, u) = unlift x :: (Exp Word32, Exp (Point a)) | |
(c2, v) = unlift y :: (Exp Word32, Exp (Point a)) | |
(x1,y1) = unlift u :: (Exp a, Exp a) | |
(x2,y2) = unlift v :: (Exp a, Exp a) | |
in | |
lift (c1+c2, lift (x1+x2, y1+y2) :: Exp (Point a)) | |
{-- | |
-- Alternative to computing the PointSum structure. | |
-- | |
-- This method uses a forward permutation with atomic instructions to create | |
-- the array directly (this method is closer to what one might write | |
-- sequentially). This avoids a parallel reduction, but has very high | |
-- contention. Overall performance much lower, as: | |
-- | |
-- number of clusters << number of points | |
-- | |
makePointSum :: Acc (Vector (PointSum a)) | |
makePointSum = A.permute addPointSum zeros near input | |
where | |
zeros = A.fill (constant (Z:.nclusters)) (constant (0,(0,0))) | |
input = A.zip (A.fill (A.shape points) (constant 1)) points | |
near ix = index1 (A.fromIntegral (nearest ! ix)) | |
--} | |
-- To complete the k-means algorithm, we loop repeatedly generating new clusters | |
-- positions, until the positions converge (or some maximum iteration limit is | |
-- reached?) | |
-- | |
kmeans :: forall a. (Elt a, IsFloating a, RealFloat a) | |
=> Int -- number of clusters to generate | |
-> Acc (Vector (Point a)) -- the points to cluster | |
-> Acc (Vector (Cluster a)) -- initial cluster positions (guess) | |
-> Acc (Vector (Cluster a)) | |
kmeans nclusters points clusters | |
= A.asnd | |
$ A.awhile (A.uncurry keepGoing) | |
(\cs -> let (_, old) = unlift cs :: (Acc (Vector (Cluster a)), Acc (Vector (Cluster a))) | |
new = makeNewClusters nclusters points old | |
in | |
lift (old,new)) | |
(lift (clusters, makeNewClusters nclusters points clusters)) | |
where | |
keepGoing :: Acc (Vector (Cluster a)) -> Acc (Vector (Cluster a)) -> Acc (Scalar Bool) | |
keepGoing xs ys | |
= A.or | |
$ A.zipWith (\c1 c2 -> let (x1,y1) = unlift (centroidOfCluster c1) | |
(x2,y2) = unlift (centroidOfCluster c2) | |
in | |
abs (x1-x2) >* 0.01 ||* abs (y1-y2) >* 0.01) xs ys | |
-- The largest non-infinite floating point number | |
-- | |
inf :: forall a. RealFloat a => a | |
inf = encodeFloat m n | |
where | |
a = undefined :: a | |
b = floatRadix a | |
e = floatDigits a | |
(_, e') = floatRange a | |
m = b ^ e - 1 | |
n = e' - e | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment