Inspired by this blog I went on implementing my own version as a F# learning challenge. It turned out to be quite different than the original (but somewhat faster for large samples).
The first code part below defines some test types and function delegates:
namespace FSLib
open System
// Test Type: Indexed Point in plane
type Point2D(x: float, y: float, i: int) =
member this.X = x
member this.Y = y
member this.Index = i
override this.ToString() = String.Format("{0}: [{1:F6}; {2:F6}]", this.Index, this.X, this.Y)
// Test Type: Indexed Point in space
type Point3D(x: float, y: float, z: float, i: int) =
member this.X = x
member this.Y = y
member this.Z = z
member this.Index = i
override this.ToString() = String.Format("{0}: [{1:F6}; {2:F6}; {3:F6}]", this.Index, this.X, this.Y, this.Z)
// Function Prototype/delegate for 'a: a1 < a2 => 1 else a1 > a2 => -1 else a1 = a2 => 0
type Comparer<'a> = 'a -> 'a -> int
// Function Prototype/delegate for a function that calculates the 'distance' of some kind between two instances of 'a
type DistanceFunc<'a> = 'a -> 'a -> float
// Function Prototype/delegate for a function calculating a new centroid from a sequence of 'a's - returns a tuple (index, 'a)
type CentroidCalculator<'a> = int -> 'a seq -> int * 'a
Then a generic type/class that runs the optimization on the provided data:
// Type/class definition/implementation of KMeanCluster
type KMeanCluster<'a when 'a : equality>(comparer : Comparer<'a>, distanceFunc : DistanceFunc<'a>, centroidCalculator : CentroidCalculator<'a>) =
let compare = comparer
let distance = distanceFunc
let calculateCentroid = centroidCalculator
// Returns the nearest centroid in argument centroids according to argument point
let nearestCluster point centroids =
centroids |> Seq.sortBy(fun p -> distance point p) |> Seq.head
// Returns a new list of cluster centroids by grouping the argument samples around the argument (old) centroids
let calculateCentroids samples centroids =
samples
|> Seq.groupBy(fun s -> nearestCluster s centroids)
|> Seq.mapi(fun i g -> calculateCentroid i (snd g))
|> Seq.sortBy(fun c -> fst c)
|> Seq.map(fun c -> snd c)
|> Seq.toList
// Checks if two lists of same type is pairwise equal: if not => true else false
let hasChanged list1 list2 =
match List.compareWith compare list1 list2 with
| 0 -> false
| _ -> true
// Runs the input data and returns the optimized cluster centroids
member this.Calculate seedCentroids samples =
let mutable clusterCentroids = seedCentroids |> List.map(fun p -> p)
let mutable newCentroids = calculateCentroids samples clusterCentroids
// This is an iterative process continueing until completed optimization
// ctor argument 'comparer' could have some kind of tolerance build in as it is responsible for
// ending the process
while hasChanged clusterCentroids newCentroids do
clusterCentroids <- newCentroids
newCentroids <- calculateCentroids samples clusterCentroids
newCentroids
Finally the client code and a sample generator function:
open System
open FSLib
let createData count =
let rand = Random(5)
let min = -500
let max = 500
[ for i in 1 .. count -> [| (float)(rand.Next(min, max)); (float)(rand.Next(min, max)); (float)(rand.Next(min, max)) |]]
// Test Case for FSLib.Point2D:
let kmc1_2D data initailCentroids =
// Converts the initialCentroids list of float[3] to list of Point2D
let seedCentroids = initailCentroids |> List.mapi(fun i (c : float[]) -> Point2D(c.[0], c.[1], i))
// Converts the data a sequence of Point2D objects
let samples = data |> Seq.mapi(fun i (d : float[]) -> Point2D(d.[0], d.[1], i))
seedCentroids |> Seq.iter(fun x -> printfn "%A" x)
printfn "\n"
// Compares two points: as our only concern is whether they are equal or not it returns either 1 (unequal) or 0 (equal)
let compare (point1 : Point2D) (point2 : Point2D) = if point1.X <> point2.X || point1.Y <> point2.Y then 1 else 0
// Calculates and returns the geometric squared distance between two points
let squaredDistance(point1 : Point2D) (point2 : Point2D) : float =
let dx = point1.X - point2.X
let dy = point1.Y - point2.Y
dx * dx + dy * dy
// Calculates and returns a tuple of argument index and the geometric average (centroid) of the argument points (index, centroid)
let calculateCentroid index points =
let mutable x = 0.0
let mutable y = 0.0
points |> Seq.iter(fun (p : Point2D) ->
x <- x + p.X
y <- y + p.Y)
let length = (float)(Seq.length points)
(index, Point2D(x / length, y / length, index))
// Instantiate an instance of KMeanCluster, calculates and prints the result
let kmean = KMeanCluster<Point2D>(compare, squaredDistance, calculateCentroid)
let result = kmean.Calculate seedCentroids samples
result |> List.iter(fun x -> printfn "%A" x)
printfn "\nEND 2D"
ignore
[<EntryPoint>]
let main argv =
let centroids = [ [| 0.; 0.; 0. |]; [| 20.; 30.; 40. |]; [| -40.; -50.; -60. |] ]
let data = createData 1000
kmc1_2D data centroids ignore
printfn "\nEND PROGRAM"
let k = Console.ReadLine()
0
I would like any comment on the F# language/functional programming specifics ideom, workflow etc. (don't waste time on error handling and the mathematics). As a OO-programmer I find it rather F#-ish, but as a F# specialist you may have another opinion?
Index
in thePoint2D
,Point3D
. Are you sure it is necessary? In my opinion, is a bit of "white crow". \$\endgroup\$