-
Notifications
You must be signed in to change notification settings - Fork 21.2k
Expand file tree
/
Copy pathClustering.java
More file actions
304 lines (270 loc) · 12 KB
/
Copy pathClustering.java
File metadata and controls
304 lines (270 loc) · 12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
package com.thealgorithms.machinelearning;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Random;
public final class Clustering {
private Clustering() {
}
/**
* Runs K-Means using explicit, caller-supplied initial centroids. Deterministic — the
* recommended entry point for reproducible results and tests.
*
* @param points the dataset to cluster; non-empty, consistent dimensionality
* @param initialCentroids exactly {@code k} initial centroids, matching {@code points}'
* dimensionality
* @param maxIterations maximum number of iterations; must be positive
* @param tolerance convergence tolerance on center movement; must be non-negative
* @return the clustering result
*/
public static ClusteringResult kMeans(double[][] points, double[][] initialCentroids, int maxIterations, double tolerance) {
validateParameters(maxIterations, tolerance);
double[][] centroids = validateAndCopyCentroids(points, initialCentroids);
return run(points, centroids, maxIterations, tolerance, Clustering::squaredEuclideanDistance, Clustering::mean);
}
public static ClusteringResult kMeans(double[][] points, int k, long seed, int maxIterations, double tolerance) {
validateParameters(maxIterations, tolerance);
double[][] centroids = randomInitialCentroids(points, k, seed);
return run(points, centroids, maxIterations, tolerance, Clustering::squaredEuclideanDistance, Clustering::mean);
}
/**
* Runs K-Medians using explicit, caller-supplied initial centers. Deterministic — the
* recommended entry point for reproducible results and tests.
*
* @param points the dataset to cluster; non-empty, consistent dimensionality
* @param initialCenters exactly {@code k} initial centers, matching {@code points}'
* dimensionality
* @param maxIterations maximum number of iterations; must be positive
* @param tolerance convergence tolerance on center movement; must be non-negative
* @return the clustering result
*/
public static ClusteringResult kMedians(double[][] points, double[][] initialCenters, int maxIterations, double tolerance) {
validateParameters(maxIterations, tolerance);
double[][] centers = validateAndCopyCentroids(points, initialCenters);
return run(points, centers, maxIterations, tolerance, Clustering::manhattanDistance, Clustering::median);
}
/**
* Runs K-Medians, sampling {@code k} distinct points from the dataset (via a seeded
* {@link Random}) as initial centers. Reproducible across runs given the same seed.
*
* @param points the dataset to cluster; non-empty, at least {@code k} points
* @param k the number of clusters; must be positive and ≤ number of points
* @param seed seed used to pick initial centers
* @param maxIterations maximum number of iterations; must be positive
* @param tolerance convergence tolerance on center movement; must be non-negative
* @return the clustering result
*/
public static ClusteringResult kMedians(double[][] points, int k, long seed, int maxIterations, double tolerance) {
validateParameters(maxIterations, tolerance);
double[][] centers = randomInitialCentroids(points, k, seed);
return run(points, centers, maxIterations, tolerance, Clustering::manhattanDistance, Clustering::median);
}
@FunctionalInterface
private interface DistanceFunction {
double distance(double[] a, double[] b);
}
@FunctionalInterface
private interface CenterFunction {
double[] center(List<double[]> clusterPoints, int dimension);
}
private static ClusteringResult run(double[][] points, double[][] initialCenters, int maxIterations, double tolerance, DistanceFunction assignmentDistance, CenterFunction centerFunction) {
int n = points.length;
int k = initialCenters.length;
int dimension = points[0].length;
double[][] centers = initialCenters;
int[] labels = new int[n];
Arrays.fill(labels, -1);
int iteration = 0;
boolean converged = false;
while (iteration < maxIterations && !converged) {
boolean anyAssignmentChanged = assign(points, centers, labels, assignmentDistance);
double[][] newCenters = updateCenters(points, labels, centers, k, dimension, centerFunction);
double maxShift = maxCenterShift(centers, newCenters);
centers = newCenters;
iteration++;
converged = !anyAssignmentChanged || maxShift < tolerance;
}
return new ClusteringResult(centers, labels, iteration, converged);
}
private static boolean assign(double[][] points, double[][] centers, int[] labels, DistanceFunction distanceFunction) {
boolean changed = false;
for (int i = 0; i < points.length; i++) {
int best = 0;
double bestDist = distanceFunction.distance(points[i], centers[0]);
for (int c = 1; c < centers.length; c++) {
double dist = distanceFunction.distance(points[i], centers[c]);
if (dist < bestDist) {
bestDist = dist;
best = c;
}
}
if (labels[i] != best) {
labels[i] = best;
changed = true;
}
}
return changed;
}
private static double[][] updateCenters(double[][] points, int[] labels, double[][] oldCenters, int k, int dimension, CenterFunction centerFunction) {
List<List<double[]>> groups = new ArrayList<>();
for (int c = 0; c < k; c++) {
groups.add(new ArrayList<>());
}
for (int i = 0; i < points.length; i++) {
groups.get(labels[i]).add(points[i]);
}
double[][] newCenters = new double[k][];
for (int c = 0; c < k; c++) {
if (groups.get(c).isEmpty()) {
// Keep the previous center if the cluster lost all its points.
newCenters[c] = Arrays.copyOf(oldCenters[c], dimension);
} else {
newCenters[c] = centerFunction.center(groups.get(c), dimension);
}
}
return newCenters;
}
private static double maxCenterShift(double[][] oldCenters, double[][] newCenters) {
double max = 0.0;
for (int c = 0; c < oldCenters.length; c++) {
max = Math.max(max, euclideanDistance(oldCenters[c], newCenters[c]));
}
return max;
}
private static double squaredEuclideanDistance(double[] a, double[] b) {
double sum = 0.0;
for (int d = 0; d < a.length; d++) {
double diff = a[d] - b[d];
sum += diff * diff;
}
return sum;
}
private static double euclideanDistance(double[] a, double[] b) {
return Math.sqrt(squaredEuclideanDistance(a, b));
}
private static double manhattanDistance(double[] a, double[] b) {
double sum = 0.0;
for (int d = 0; d < a.length; d++) {
sum += Math.abs(a[d] - b[d]);
}
return sum;
}
private static double[] mean(List<double[]> clusterPoints, int dimension) {
double[] result = new double[dimension];
for (double[] p : clusterPoints) {
for (int d = 0; d < dimension; d++) {
result[d] += p[d];
}
}
for (int d = 0; d < dimension; d++) {
result[d] /= clusterPoints.size();
}
return result;
}
private static double[] median(List<double[]> clusterPoints, int dimension) {
int n = clusterPoints.size();
double[] result = new double[dimension];
double[] values = new double[n];
for (int d = 0; d < dimension; d++) {
for (int i = 0; i < n; i++) {
values[i] = clusterPoints.get(i)[d];
}
Arrays.sort(values);
if (n % 2 == 1) {
result[d] = values[n / 2];
} else {
result[d] = (values[n / 2 - 1] + values[n / 2]) / 2.0;
}
}
return result;
}
private static void validateParameters(int maxIterations, double tolerance) {
if (maxIterations <= 0) {
throw new IllegalArgumentException("maxIterations must be positive, got " + maxIterations);
}
if (tolerance < 0) {
throw new IllegalArgumentException("tolerance must be non-negative, got " + tolerance);
}
}
private static void validatePoints(double[][] points, int k) {
if (points == null || points.length == 0) {
throw new IllegalArgumentException("Dataset must not be empty");
}
if (k <= 0) {
throw new IllegalArgumentException("k must be positive, got " + k);
}
if (k > points.length) {
throw new IllegalArgumentException("k (" + k + ") cannot exceed the number of points (" + points.length + ")");
}
int dimension = points[0].length;
if (dimension == 0) {
throw new IllegalArgumentException("Points must have at least one dimension");
}
for (int i = 0; i < points.length; i++) {
if (points[i] == null || points[i].length != dimension) {
throw new IllegalArgumentException("All points must share the same dimensionality; point " + i + " does not match");
}
}
}
private static double[][] validateAndCopyCentroids(double[][] points, double[][] initialCenters) {
Objects.requireNonNull(initialCenters, "initial centers must not be null");
validatePoints(points, initialCenters.length);
int dimension = points[0].length;
double[][] centers = new double[initialCenters.length][];
for (int i = 0; i < initialCenters.length; i++) {
if (initialCenters[i] == null || initialCenters[i].length != dimension) {
throw new IllegalArgumentException("Initial center " + i + " has inconsistent dimensionality");
}
centers[i] = Arrays.copyOf(initialCenters[i], dimension);
}
return centers;
}
private static double[][] randomInitialCentroids(double[][] points, int k, long seed) {
validatePoints(points, k);
int[] indices = new int[points.length];
for (int i = 0; i < indices.length; i++) {
indices[i] = i;
}
Random random = new Random(seed);
for (int i = indices.length - 1; i > 0; i--) {
int j = random.nextInt(i + 1);
int tmp = indices[i];
indices[i] = indices[j];
indices[j] = tmp;
}
double[][] centers = new double[k][];
for (int i = 0; i < k; i++) {
centers[i] = Arrays.copyOf(points[indices[i]], points[indices[i]].length);
}
return centers;
}
public static final class ClusteringResult {
private final double[][] centers;
private final int[] labels;
private final int iterations;
private final boolean converged;
ClusteringResult(double[][] centers, int[] labels, int iterations, boolean converged) {
this.centers = centers;
this.labels = labels;
this.iterations = iterations;
this.converged = converged;
}
public double[][] getCenters() {
double[][] copy = new double[centers.length][];
for (int i = 0; i < centers.length; i++) {
copy[i] = Arrays.copyOf(centers[i], centers[i].length);
}
return copy;
}
public int[] getLabels() {
return Arrays.copyOf(labels, labels.length);
}
public int getIterations() {
return iterations;
}
public boolean hasConverged() {
return converged;
}
}
}