Clustering
C++20 header-only: DBSCAN, HDBSCAN, k-means.
Loading...
Searching...
No Matches
kmeans.h
Go to the documentation of this file.
1#pragma once
2
3#include <cstddef>
4#include <cstdint>
5#include <type_traits>
6
13#include "clustering/ndarray.h"
14
15namespace clustering {
16
36template <class T, class Algo = kmeans::LloydFusedGemm<T>, class Seeder = kmeans::AutoSeeder<T>>
38class KMeans {
39 static_assert(std::is_same_v<T, float>,
40 "KMeans<T> supports only float; add a double specialization to extend.");
41
42public:
51 explicit KMeans(std::size_t k, std::size_t nJobs = 0)
52 : m_k(k), m_nJobs(math::clampedJobCount(nJobs)),
53 m_pool(m_nJobs > 1 ? &math::sharedPool(m_nJobs) : nullptr), m_centroids({0, 0}),
54 m_labels({0}) {
56 }
57
65 KMeans(std::size_t k, std::size_t nJobs, math::OwnedPool &externalPool)
66 : m_k(k), m_nJobs(math::clampedJobCount(nJobs)), m_pool(&externalPool), m_centroids({0, 0}),
67 m_labels({0}) {
69 }
70
71 KMeans(const KMeans &) = delete;
72 KMeans &operator=(const KMeans &) = delete;
73 KMeans(KMeans &&) = delete;
74 KMeans &operator=(KMeans &&) = delete;
75 ~KMeans() = default;
76
101 void run(const NDArray<T, 2> &X, std::size_t maxIter = 300, T tol = T{1e-4},
102 std::uint64_t seedFirst = 0, std::size_t nInit = 1) {
103 const std::size_t n = X.dim(0);
104 const std::size_t d = X.dim(1);
105
106 CLUSTERING_ALWAYS_ASSERT(m_k >= 1);
107 CLUSTERING_ALWAYS_ASSERT(n >= m_k);
108 CLUSTERING_ALWAYS_ASSERT(nInit >= 1);
109
110 ensureOutputShape(n, d);
111
112 if (n == 0 || d == 0) {
113 m_nIter = 0;
114 m_converged = true;
115 m_inertia = 0.0;
116 return;
117 }
118
119 const math::Pool pool{m_pool};
120
121 if (nInit == 1) {
122 m_seeder.run(X, m_k, seedFirst, pool, m_centroids);
123 m_lloyd.run(X, m_centroids, m_k, maxIter, tol, pool, m_labels, m_inertia, m_nIter,
124 m_converged);
125 return;
126 }
127
128 ensureBestOfScratch(n, d);
129 bool anyImprovement = false;
130
131 for (std::size_t i = 0; i < nInit; ++i) {
132 const std::uint64_t seed = seedFirst + static_cast<std::uint64_t>(i);
133 m_seeder.run(X, m_k, seed, pool, m_centroids);
134 m_lloyd.run(X, m_centroids, m_k, maxIter, tol, pool, m_labels, m_inertia, m_nIter,
135 m_converged);
136 if (!anyImprovement || m_inertia < m_bestInertia) {
137 anyImprovement = true;
138 m_bestInertia = m_inertia;
139 m_bestNIter = m_nIter;
140 m_bestConverged = m_converged;
141 std::memcpy(m_bestCentroids.data(), m_centroids.data(), m_k * d * sizeof(T));
142 std::memcpy(m_bestLabels.data(), m_labels.data(), n * sizeof(std::int32_t));
143 }
144 }
145
146 // Promote the best-of-restarts result onto the public accessors. Swapping the storage
147 // (rather than copying) keeps the work-side buffers the same shape so the next call
148 // skips reallocation.
149 std::swap(m_centroids, m_bestCentroids);
150 std::swap(m_labels, m_bestLabels);
151 m_inertia = m_bestInertia;
152 m_nIter = m_bestNIter;
153 m_converged = m_bestConverged;
154 }
155
157 [[nodiscard]] const NDArray<std::int32_t, 1> &labels() const noexcept { return m_labels; }
159 [[nodiscard]] const NDArray<T, 2, Layout::Contig> &centroids() const noexcept {
160 return m_centroids;
161 }
162
163 [[nodiscard]] double inertia() const noexcept { return m_inertia; }
165 [[nodiscard]] std::size_t nIter() const noexcept { return m_nIter; }
167 [[nodiscard]] bool converged() const noexcept { return m_converged; }
168
170 void reset() {
171 m_centroids = NDArray<T, 2, Layout::Contig>({0, 0});
172 m_labels = NDArray<std::int32_t, 1>({0});
173 m_bestCentroids = NDArray<T, 2, Layout::Contig>({0, 0});
174 m_bestLabels = NDArray<std::int32_t, 1>({0});
175 m_inertia = 0.0;
176 m_nIter = 0;
177 m_converged = false;
178 m_bestInertia = 0.0;
179 m_bestNIter = 0;
180 m_bestConverged = false;
181 m_lloyd = Algo{};
182 m_seeder = Seeder{};
183 }
184
185private:
186 void ensureOutputShape(std::size_t n, std::size_t d) {
187 if (m_centroids.dim(0) != m_k || m_centroids.dim(1) != d) {
188 m_centroids = NDArray<T, 2, Layout::Contig>({m_k, d});
189 }
190 if (m_labels.dim(0) != n) {
191 m_labels = NDArray<std::int32_t, 1>({n});
192 }
193 }
194
195 void ensureBestOfScratch(std::size_t n, std::size_t d) {
196 if (m_bestCentroids.dim(0) != m_k || m_bestCentroids.dim(1) != d) {
197 m_bestCentroids = NDArray<T, 2, Layout::Contig>({m_k, d});
198 }
199 if (m_bestLabels.dim(0) != n) {
200 m_bestLabels = NDArray<std::int32_t, 1>({n});
201 }
202 }
203
204 std::size_t m_k;
205 std::size_t m_nJobs;
206 math::OwnedPool *m_pool = nullptr;
207 NDArray<T, 2, Layout::Contig> m_centroids;
208 NDArray<std::int32_t, 1> m_labels;
209 double m_inertia = 0.0;
210 std::size_t m_nIter = 0;
211 bool m_converged = false;
212
213 // Scratch holding the best-of-restarts result while iterating (@c nInit > 1). On exit
214 // these are swapped with the public buffers so accessors return the lowest-inertia run.
215 NDArray<T, 2, Layout::Contig> m_bestCentroids{NDArray<T, 2, Layout::Contig>({0, 0})};
216 NDArray<std::int32_t, 1> m_bestLabels{NDArray<std::int32_t, 1>({0})};
217 double m_bestInertia = 0.0;
218 std::size_t m_bestNIter = 0;
219 bool m_bestConverged = false;
220
221 Algo m_lloyd{};
222 Seeder m_seeder{};
223};
224
225} // namespace clustering
#define CLUSTERING_ALWAYS_ASSERT(cond)
Release-active assertion: evaluates cond in every build configuration.
double inertia() const noexcept
Final inertia: Kahan-summed f64 total of per-point squared distance to assignment.
Definition kmeans.h:163
KMeans & operator=(KMeans &&)=delete
std::size_t nIter() const noexcept
Iterations executed before tol or maxIter fired.
Definition kmeans.h:165
bool converged() const noexcept
True iff the last run stopped because centroid shift fell at or below tol.
Definition kmeans.h:167
void reset()
Release every scratch buffer. The next run call reallocates against its shape.
Definition kmeans.h:170
void run(const NDArray< T, 2 > &X, std::size_t maxIter=300, T tol=T{1e-4}, std::uint64_t seedFirst=0, std::size_t nInit=1)
Fit to X with optional best-of-restarts.
Definition kmeans.h:101
KMeans & operator=(const KMeans &)=delete
KMeans(KMeans &&)=delete
const NDArray< T, 2, Layout::Contig > & centroids() const noexcept
k x d fitted centroids.
Definition kmeans.h:159
KMeans(std::size_t k, std::size_t nJobs, math::OwnedPool &externalPool)
Construct a reusable k-means fitter that borrows a caller-owned thread pool.
Definition kmeans.h:65
KMeans(const KMeans &)=delete
KMeans(std::size_t k, std::size_t nJobs=0)
Construct a reusable k-means fitter.
Definition kmeans.h:51
const NDArray< std::int32_t, 1 > & labels() const noexcept
Length-n assignment; each entry is in [0, k).
Definition kmeans.h:157
Represents a multidimensional array (NDArray) of a fixed number of dimensions N and element type T.
Definition ndarray.h:136
size_t dim(std::size_t index) const noexcept
Returns the size of a specific dimension of the NDArray.
Definition ndarray.h:462
Contract for the Lloyd driver that KMeans<T> delegates to.
Definition lloyd.h:23
Contract for the seeder that produces initial centroids for the Lloyd driver.
Definition seeder.h:23
citor::ThreadPool OwnedPool
Type alias for the owning pool the algorithm wrappers (KMeans, DBSCAN, HDBSCAN) hold inside an std::o...
Definition thread.h:28