Cluster kmeans fit - CyrilB1531/lodestar GitHub Wiki
Development build. This page describes
main, not a released package. The latest published Lodestar.Cluster is 0.2.0 — read its documentation.
Home › Cluster › Partitioning
Fits k-means on a row-major sample matrix.
public static KMeans Fit(ReadOnlySpan<double> samples, int featureCount, int clusterCount, KMeansOptions options = null)public static KMeans Fit(ReadOnlySpan<double> samples, int featureCount, int clusterCount, ReadOnlySpan<double> sampleWeight, KMeansOptions options = null)The second overload weighs each sample, scikit-learn's fit(X, sample_weight=w).
Parameters — samples is the sample matrix, row-major: featureCount values per row.
featureCount is how many values each row carries. clusterCount is how many clusters to find.
sampleWeight is one finite weight per row, not all zero; a negative one is accepted, as the
reference accepts it.
options says where to start and when to stop; null takes the defaults.
Returns — a fitted KMeans, whose Inertia is weighted when the weights are given.
Exceptions — ArgumentOutOfRangeException when featureCount or clusterCount is not
positive, or options asks for fewer than one iteration or a Tolerance that is negative, infinite
or NaN. ArgumentException when samples holds no row, a partial one or a NaN or infinite
value, when there are fewer rows than clusters, or when the given initial centres are the wrong
shape or not finite, or when sampleWeight is not one finite value per row or is all zeros. Both
also when options pairs InitialCentreSets with InitialCentres or Restarts, gives it no
block, pairs Restarts with InitialCentres, or asks for fewer than one restart.
Example — a starting centre no sample is nearest to leaves its cluster empty, and the fit recovers.
using Lodestar.Cluster;
double[] samples = [0.0, 0.0, 0.0, 1.0, 10.0, 10.0, 10.0, 11.0, 5.0, 5.0];
// The third centre is five hundred units away from every sample.
KMeans model = KMeans.Fit(samples, featureCount: 2, clusterCount: 3,
new KMeansOptions { InitialCentres = [0.0, 0.0, 10.0, 10.0, -500.0, -500.0] });
// It is relocated onto the sample furthest from its own centre, and the fit lands
// on the same partition as a sensible start would.
double inertia = model.Inertia; // => 1
int middle = model.Labels[4]; // => 2A weight of two is the row written twice, which is what makes weights a way to cluster deduplicated rows.
using Lodestar.Cluster;
var start = new KMeansOptions { InitialCentres = [0.5, 10.5] };
KMeans weighted = KMeans.Fit([0.0, 1.0, 10.0, 11.0], featureCount: 1, clusterCount: 2, [3.0, 1.0, 1.0, 1.0], start);
KMeans repeated = KMeans.Fit([0.0, 0.0, 0.0, 1.0, 10.0, 11.0], featureCount: 1, clusterCount: 2, start);
double centre = weighted.Centres[0]; // => 0.25
double sameInertia = weighted.Inertia - repeated.Inertia; // => 0Remarks — Tolerance is scaled before it is used, by the mean feature variance, exactly as
sklearn.cluster._kmeans._tolerance scales it. The same number therefore means the same thing on a
matrix of millimetres and one of kilometres. A Tolerance of 0 removes the shift test entirely
and iterates until the labels settle.
The loop runs on the samples minus their column means, starting centres included, and the
means are added back to Centres — what KMeans.fit does before it calls the Lloyd loop. It
changes nothing but rounding, and rounding is what Iterations turns on: uncentred, a centre shift
the reference rounds to 4.9e-32 came out exactly 0, and a fit with Tolerance 0 stopped one
iteration early.
A sample exactly equidistant from two centres takes the lowest-indexed one. That is
numpy.argmin's rule and a measured divergence from the reference, whose choice was observed
going both ways on two configurations;
decisions/0007
has both and says why neither rule reproduces the pair. No frozen case turns on a tie.
Empty clusters are relocated together, as the reference relocates them. From one pass of
distances, each empty cluster takes a distinct one of the samples furthest from their centres, in
cluster order, and that sample's old cluster gives it up; the labels are left alone. When every
sample already sits on its centre nothing moves, and a cluster still empty takes the largest
cluster's centre — before that centre is averaged when the largest cluster comes later, the
reference's own order, which the corpus freezes. The furthest samples are taken in descending
distance, ties lowest row first. The reference reads them through numpy.argpartition, whose
result changes with the CPU tier numpy dispatches to: numpy 2.5.3 on one machine returned three
different selections from the same distances under its AVX-512, AVX2 and baseline kernels. No rule
reproduces all three, so which sample fills which empty cluster can differ even with no tie, and
when the furthest distances are tied the samples chosen, the centres and Inertia can differ
too — scikit-learn 1.9.0 returned inertia_ 2.5e-31 under AVX-512 and 193.26 under the
baseline for one input. Over 1,000 random fits with distinct distances the centre sets and Inertia
matched on all 1,000, whatever the pairing; over 1,000 fits of a few points repeated, scikit-learn
disagreed with itself across tiers on 35 centre sets and this method disagreed with it on 214, and
taking ties highest row first instead leaves 210. With no cluster emptied the same kind of input
matched on all 1,000. The same reasoning as decision 0007; no frozen case turns on it yet.
Weights change every step but the tolerance. Each centre is its members' weighted mean, and
Inertia sums weight times squared distance. A cluster is empty when its members weigh exactly
nothing, and is relocated as above — onto the furthest sample by unweighted distance, taking that
sample's weight from its old cluster; if that weight is zero too, the cluster takes the heaviest
cluster's centre. k-means++ draws in proportion to weight, a negative one counted as zero. The
tolerance is still scaled by the unweighted feature variance, as _tolerance scales it.
Several starts keep the reference's winner. With
KMeansOptions.InitialCentreSets or Restarts, one Lloyd run goes from each
start and the first is kept unless a later one has a strictly lower Inertia and a different
partition — scikit-learn's n_init rule, which will not trade a partition for itself on a
rounding-level gain. Iterations is the kept run's.
The starting centres are an input, not a seed. Passing
KMeansOptions.InitialCentres replaces the choice entirely and makes the run an
ordinary parity target — the move decisions/0004
made for Ω. Without them, k-means++ draws from this package's own generator, which reproduces a run
of Lodestar and never a run of scikit-learn.
Applies to — net10.0, netstandard2.0.
See also — KMeans, KMeans.Predict,
KMeansOptions.